Skip to content

Instantly share code, notes, and snippets.

@ildunari
Last active June 3, 2025 23:52
Show Gist options
  • Select an option

  • Save ildunari/dfc8759ae9fcf6410fa3bd8150c70a18 to your computer and use it in GitHub Desktop.

Select an option

Save ildunari/dfc8759ae9fcf6410fa3bd8150c70a18 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name AgentClaude File Bridge
// @description Upload-on-demand file bridge for TypingMind with dynamic context injection
// @version 0.7
// @match https://*
// @grant none
// ==/UserScript==
;(function () {
// File storage
window.__agentClaudeFiles = window.__agentClaudeFiles || {};
let pendingFiles = {}; // Files waiting to be mentioned in next message
/* ─── helpers ─── */
const toast = (msg, ok = true, ms = 3200) => {
const t = Object.assign(document.createElement('div'), { textContent: msg });
t.style.cssText =
`position:fixed;top:16px;right:16px;z-index:99999;
background:${ok ? '#16a34a' : '#dc2626'};color:#fff;padding:6px 11px;
border-radius:6px;font:14px/1.3 sans-serif;box-shadow:0 3px 8px rgba(0,0,0,.2);
opacity:0;transition:opacity .25s`;
document.body.appendChild(t);
requestAnimationFrame(() => (t.style.opacity = 1));
setTimeout(() => { t.style.opacity = 0; setTimeout(() => t.remove(), 250); }, ms);
};
const formatSize = (bytes) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
const generateFileId = () => {
return `file_${Math.random().toString(36).substr(2, 8)}`;
};
/* ─── File handling ─── */
const handleFile = (file) => {
try {
// Store file in both permanent and pending storage
window.__agentClaudeFiles[file.name] = file;
pendingFiles[file.name] = file;
console.log(`[AgentClaude] Registered ${file.name} (${formatSize(file.size)})`);
toast(`📎 ${file.name} ready for agent_claude`, true, 2000);
} catch (e) {
console.error('[AgentClaude] Registration error:', e);
toast(`Registration failed: ${e.message}`, false, 3000);
}
};
/* ─── Dynamic context injection ─── */
const createFileContext = () => {
const fileList = Object.keys(pendingFiles)
.map(name => `${name} (${pendingFiles[name].type}, ${formatSize(pendingFiles[name].size)})`)
.join(', ');
return `\n\n[Context: User has uploaded files: ${fileList}. These files are available for analysis via the agent_claude plugin. To use any file, call the function with this exact format:
Function name: agent_claude.run
Parameters: {"filename": "exact_filename.ext", "task": "description of what to do"}
Example: {"name": "agent_claude.run", "arguments": {"filename": "${Object.keys(pendingFiles)[0] || 'document.pdf'}", "task": "analyze this file"}}]`;
};
const isLLMRequest = (url, options) => {
return options?.method === 'POST' &&
typeof url === 'string' &&
(url.includes('api.openai.com') ||
url.includes('api.anthropic.com') ||
url.includes('typingmind.com/api') ||
url.includes('chat/completions'));
};
/* ─── Monkey-patch fetch for dual purposes ─── */
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
// 1. Inject file context into LLM requests
if (isLLMRequest(url, options) && Object.keys(pendingFiles).length > 0) {
try {
const body = JSON.parse(options.body);
const userMessage = body.messages?.[body.messages.length - 1];
if (userMessage?.role === 'user' && typeof userMessage.content === 'string') {
// Enhance user message with file context
const fileContext = createFileContext();
userMessage.content = userMessage.content + fileContext;
console.log('[AgentClaude] Injected file context into user message');
// Clear pending files (only inject once per upload batch)
pendingFiles = {};
options.body = JSON.stringify(body);
}
} catch (e) {
console.error('[AgentClaude] Context injection error:', e);
}
}
// 2. Intercept plugin calls and upload files
if (options?.method === 'POST' &&
typeof url === 'string' &&
url.includes('api.pluginpapi.dev') &&
url.includes('agent_claude')) {
try {
const body = JSON.parse(options.body);
const filename = body.filename;
if (filename && window.__agentClaudeFiles[filename]) {
const file = window.__agentClaudeFiles[filename];
console.log(`[AgentClaude] Uploading ${filename} for plugin...`);
toast(`🔄 Uploading ${filename}...`, true, 1000);
// Upload file to temporary storage
const tempUrl = await uploadFileToTemp(file);
// Modify request: replace filename with temp URL
body.url = tempUrl;
delete body.filename;
options.body = JSON.stringify(body);
console.log('[AgentClaude] Modified plugin request with file URL');
// Clean up file storage
delete window.__agentClaudeFiles[filename];
}
} catch (e) {
console.error('[AgentClaude] Plugin intercept error:', e);
}
}
return originalFetch.call(this, url, options);
};
/* ─── File upload to temp storage ─── */
const uploadFileToTemp = async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await originalFetch('https://api.pluginpapi.dev/tmp/upload', {
method: 'POST',
headers: { 'X-API-Key': 'sk_plugin_123456789' },
body: formData
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Temp upload failed (${response.status}): ${errorText}`);
}
const result = await response.json();
return result.url;
};
/* ─── File attachment handlers ─── */
const hookInput = (inp) => {
if (inp.dataset.agentClaudeHooked) return;
inp.dataset.agentClaudeHooked = 'true';
inp.addEventListener('change', () => {
if (inp.files && inp.files.length > 0) {
// Handle multiple files
Array.from(inp.files).forEach(handleFile);
}
});
};
// Hook existing and future file inputs
document.querySelectorAll('input[type=file]').forEach(hookInput);
new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.tagName === 'INPUT' && node.type === 'file') {
hookInput(node);
}
node.querySelectorAll?.('input[type=file]').forEach(hookInput);
}
});
});
}).observe(document.body, { childList: true, subtree: true });
// Drag-drop handlers
let dragCounter = 0;
window.addEventListener('dragenter', (e) => {
if (e.dataTransfer?.types?.includes('Files')) {
dragCounter++;
e.preventDefault();
}
}, true);
window.addEventListener('dragleave', (e) => {
if (e.dataTransfer?.types?.includes('Files')) {
dragCounter--;
}
}, true);
window.addEventListener('dragover', (e) => {
if (e.dataTransfer?.types?.includes('Files')) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}
}, true);
window.addEventListener('drop', (e) => {
if (e.dataTransfer?.files?.length) {
e.preventDefault();
dragCounter = 0;
// Handle multiple files
Array.from(e.dataTransfer.files).forEach(handleFile);
}
}, true);
// Paste handlers
window.addEventListener('paste', (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
e.preventDefault();
handleFile(file);
break;
}
}
}
});
// File cleanup on page unload (optional)
window.addEventListener('beforeunload', () => {
window.__agentClaudeFiles = {};
pendingFiles = {};
});
console.log('[AgentClaude] File Bridge 0.7 active - Dynamic context injection mode');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment