Last active
June 3, 2025 23:52
-
-
Save ildunari/dfc8759ae9fcf6410fa3bd8150c70a18 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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