Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/anthonyn99_a1.json
T

1 line
50 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "anthonyn99/a1", "files": {"Vault/vault-ai-prompt.js": {"41c1e095bee65fc60a46c7448b7f6244f023ec09": {"t": 1785747200.6151562, "n": 12510, "h": "71d15f9612e401e3", "c": "// ─────────────────────────────────────────────────────────────────────────────\n// vault-ai-prompt.js — types a prompt into an AI chat site and submits it.\n//\n// WHY THIS EXISTS\n// TradeHub's \"Launch Analysis\" button used to only copy the prompt and open\n// ChatGPT — you still had to press Ctrl+V + Enter yourself. A web page can't\n// type into another site (cross-origin), which is exactly why the Trading Auto\n// Launch (Python) had to drive it with OS-level keystrokes. A content script\n// CAN, because it runs inside the AI page. So TradeHub now asks the extension\n// to open the AI tab, the background remembers the prompt for that tab id, and\n// this script — running in the new tab — collects it and does the paste + send.\n//\n// DELIVERY IS A PULL, NOT A PUSH\n// The background can't reliably message a tab that may not have loaded yet, so\n// this script asks (\"anything pending for my tab?\") on load and retries a few\n// times to cover the case where the tab id was registered a beat after the tab\n// was created. The background hands the prompt over exactly ONCE and forgets\n// it, so a reload / redirect can never re-submit the same prompt.\n//\n// TYPING METHOD\n// Every one of these composers is a framework-controlled editor (ProseMirror,\n// Quill, controlled React textarea). Writing .value or .textContent leaves the\n// framework's own state empty, so the send button stays disabled and Enter\n// sends nothing. document.execCommand('insertText') is routed through the\n// browser's real editing pipeline and fires the same events a human keystroke\n// does, so it works on all of them — plain textarea included.\n// ─────────────────────────────────────────────────────────────────────────────\n\n(function () {\n if (window.__vaultAiPromptLoaded) return; window.__vaultAiPromptLoaded = true;\n // Top frame only — the composer is never in a subframe, and all_frames would\n // otherwise run this in every ad/embed iframe on the page.\n try { if (window.top !== window) return; } catch (e) { return; }\n\n var COMPOSER_WAIT_MS = 30000; // cold ChatGPT load + auth redirect can be slow\n var POLL_MS = 250;\n var PULL_TRIES = 6; // × PULL_GAP_MS — covers a late tab-id registration\n var PULL_GAP_MS = 400;\n\n // Per-site selectors. First match wins; the generic finder below is the\n // safety net when a site ships a redesign, so a stale selector degrades to\n // \"still works\" rather than \"does nothing\".\n var PROFILES = [\n { name: 'ChatGPT', test: /(^|\\.)chatgpt\\.com$|(^|\\.)chat\\.openai\\.com$/i,\n input: ['#prompt-textarea', 'div[contenteditable=\"true\"]#prompt-textarea', 'textarea[data-id]'],\n send: ['button[data-testid=\"send-button\"]', '#composer-submit-button', 'button[aria-label=\"Send prompt\"]'] },\n { name: 'Claude', test: /(^|\\.)claude\\.ai$/i,\n input: ['div[contenteditable=\"true\"].ProseMirror', 'div[contenteditable=\"true\"][aria-label*=\"prompt\" i]'],\n send: ['button[aria-label=\"Send message\"]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Gemini', test: /(^|\\.)gemini\\.google\\.com$/i,\n input: ['rich-textarea div[contenteditable=\"true\"]', 'div.ql-editor[contenteditable=\"true\"]'],\n send: ['button.send-button', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Perplexity', test: /(^|\\.)perplexity\\.ai$/i,\n input: ['#ask-input', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[aria-label=\"Submit\"]', 'button[data-testid=\"submit-button\"]'] },\n { name: 'Grok', test: /(^|\\.)grok\\.com$|(^|\\.)x\\.com$/i,\n input: ['textarea[aria-label]', 'textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Copilot', test: /(^|\\.)copilot\\.microsoft\\.com$|(^|\\.)bing\\.com$/i,\n input: ['textarea#userInput', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[title*=\"Submit\" i]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'DeepSeek', test: /(^|\\.)deepseek\\.com$/i,\n input: ['textarea#chat-input', 'textarea'],\n send: ['div[role=\"button\"][aria-disabled=\"false\"]', 'button[type=\"submit\"]'] },\n { name: 'Mistral', test: /(^|\\.)mistral\\.ai$/i,\n input: ['textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Send\" i]'] },\n ];\n\n function profileFor(host) {\n for (var i = 0; i < PROFILES.length; i++) if (PROFILES[i].test.test(host)) return PROFILES[i];\n return { name: host, input: [], send: [] };\n }\n\n function visible(el, minW, minH) {\n if (!el || !el.isConnected) return false;\n var r = el.getBoundingClientRect();\n // minW/minH default to composer-sized. Send buttons MUST pass a much smaller\n // floor: ChatGPT's is a ~36px circular icon, so a 40px minimum silently\n // rejected it and we fell through to synthetic Enter, which ProseMirror\n // ignores — the prompt got typed in and just sat there.\n if (r.width < (minW == null ? 40 : minW) || r.height < (minH == null ? 12 : minH)) return false;\n var s = getComputedStyle(el);\n return s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0';\n }\n\n function editable(el) {\n if (!el) return false;\n if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;\n return el.isContentEditable;\n }\n\n // Fallback composer finder for when a site's selectors have gone stale:\n // the biggest visible editable box, preferring ones that announce themselves\n // as a message/prompt input.\n function findComposerGeneric() {\n var all = [];\n try { all = Array.prototype.slice.call(document.querySelectorAll('textarea, [contenteditable=\"true\"], [contenteditable=\"\"], [role=\"textbox\"]')); }\n catch (e) { return null; }\n var best = null, bestScore = -1;\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (!editable(el) || !visible(el)) continue;\n var r = el.getBoundingClientRect();\n var hint = ((el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' +\n (el.getAttribute('data-testid') || '') + ' ' + (el.id || '')).toLowerCase();\n var score = r.width * r.height;\n if (/message|prompt|ask|chat|question|input/.test(hint)) score *= 4;\n if (/search|url|address/.test(hint)) score *= 0.1; // site search box, not the composer\n if (score > bestScore) { bestScore = score; best = el; }\n }\n return best;\n }\n\n function findComposer(profile) {\n for (var i = 0; i < profile.input.length; i++) {\n var list;\n try { list = document.querySelectorAll(profile.input[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (editable(list[j]) && visible(list[j])) return list[j];\n }\n return findComposerGeneric();\n }\n\n function clickable(el) {\n return !!el && !el.disabled && el.getAttribute('aria-disabled') !== 'true' && visible(el, 10, 10);\n }\n\n function findSend(profile) {\n var sels = profile.send.concat([\n 'button[data-testid=\"send-button\"]', 'button[aria-label*=\"Send\" i]',\n 'button[aria-label*=\"Submit\" i]', 'button[title*=\"Send\" i]',\n 'button[title*=\"Submit\" i]', 'form button[type=\"submit\"]',\n ]);\n for (var i = 0; i < sels.length; i++) {\n var list;\n try { list = document.querySelectorAll(sels[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (clickable(list[j])) return list[j];\n }\n return null;\n }\n\n function composerText(el) {\n if (!el) return '';\n return (el.tagName === 'TEXTAREA' ? (el.value || '') : (el.innerText || el.textContent || '')).trim();\n }\n\n function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n function waitFor(fn, timeout) {\n var deadline = Date.now() + timeout;\n return new Promise(function (resolve) {\n (function tick() {\n var v = null;\n try { v = fn(); } catch (e) {}\n if (v) return resolve(v);\n if (Date.now() >= deadline) return resolve(null);\n setTimeout(tick, POLL_MS);\n })();\n });\n }\n\n // Write the prompt the way a keyboard would. Selecting first means a draft\n // the composer restored from a previous session is replaced, not appended to.\n function typeInto(el, text) {\n try { el.focus(); el.click(); } catch (e) {}\n try {\n if (el.tagName === 'TEXTAREA') { el.select(); }\n else {\n var range = document.createRange(); range.selectNodeContents(el);\n var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);\n }\n } catch (e) {}\n var ok = false;\n try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }\n if (!ok && el.tagName === 'TEXTAREA') {\n // Last resort for a textarea: the native value setter bypasses React's\n // property override, and the input event tells React to re-read it.\n try {\n var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;\n setter.call(el, text);\n el.dispatchEvent(new Event('input', { bubbles: true }));\n ok = true;\n } catch (e) {}\n }\n return ok;\n }\n\n function pressEnter(el) {\n var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true };\n try {\n el.dispatchEvent(new KeyboardEvent('keydown', opts));\n el.dispatchEvent(new KeyboardEvent('keypress', opts));\n el.dispatchEvent(new KeyboardEvent('keyup', opts));\n } catch (e) {}\n }\n\n async function deliver(text) {\n var profile = profileFor(location.hostname);\n var el = await waitFor(function () { return findComposer(profile); }, COMPOSER_WAIT_MS);\n // Returning false leaves the prompt pending in the background, so a reload\n // (or the real app document, if this one was an auth interstitial) retries.\n if (!el) { console.warn('[Vault] AI prompt: no composer found on ' + location.hostname); return false; }\n\n if (!typeInto(el, text)) { console.warn('[Vault] AI prompt: could not type into the composer'); return false; }\n\n // Keep trying to send until the composer actually empties — that's the only\n // reliable proof it went. One fire-and-hope click is how \"the prompt is\n // typed in but just sits there\" happens: the send button may not exist yet\n // (it appears once the site registers input), may be disabled for a beat, or\n // the click may land on the wrong control.\n var head = text.trim().slice(0, 40);\n var sent = false;\n for (var i = 0; i < 10 && !sent; i++) {\n var btn = findSend(profile);\n if (btn) { try { btn.click(); } catch (e) {} }\n // Enter only when there's no button to click, or after the button has\n // repeatedly failed — on ProseMirror sites a synthetic Enter is usually\n // ignored, so it's the fallback, not the first move.\n if (!btn || i >= 3) { try { el.focus(); } catch (e) {} pressEnter(el); }\n await wait(600);\n if (composerText(el).slice(0, 40) !== head) sent = true;\n }\n\n if (!sent) {\n console.warn('[Vault] AI prompt: typed it in, but no send control fired — press Enter yourself. ' +\n 'Send button found: ' + !!findSend(profile));\n return false; // stays pending, so a reload retries\n }\n // Only now tell the background it's done — a failed attempt must stay\n // pending so the next document (or a reload) can have another go.\n report();\n console.log('[Vault] AI prompt submitted on ' + profile.name);\n return true;\n }\n\n function report() {\n try { chrome.runtime.sendMessage({ action: 'vaultAiPromptDone' }, function () { void chrome.runtime.lastError; }); }\n catch (e) {}\n }\n\n // Ask the background whether this tab was opened to receive a prompt.\n function pull() {\n return new Promise(function (resolve) {\n try {\n chrome.runtime.sendMessage({ action: 'vaultAiPromptPending' }, function (resp) {\n if (chrome.runtime.lastError) return resolve(null);\n resolve(resp && resp.text ? String(resp.text) : null);\n });\n } catch (e) { resolve(null); }\n });\n }\n\n (async function () {\n for (var i = 0; i < PULL_TRIES; i++) {\n var text = await pull();\n if (text) {\n console.log('[Vault] AI prompt: ' + text.length + ' chars pending for this tab');\n deliver(text);\n return;\n }\n await wait(PULL_GAP_MS);\n }\n })();\n})();\n"}, "05f541949ec01ffe7a41ba8d6e160cf760426e3c": {"t": 1785771334.280627, "n": 12510, "h": "71d15f9612e401e3", "c": "// ─────────────────────────────────────────────────────────────────────────────\n// vault-ai-prompt.js — types a prompt into an AI chat site and submits it.\n//\n// WHY THIS EXISTS\n// TradeHub's \"Launch Analysis\" button used to only copy the prompt and open\n// ChatGPT — you still had to press Ctrl+V + Enter yourself. A web page can't\n// type into another site (cross-origin), which is exactly why the Trading Auto\n// Launch (Python) had to drive it with OS-level keystrokes. A content script\n// CAN, because it runs inside the AI page. So TradeHub now asks the extension\n// to open the AI tab, the background remembers the prompt for that tab id, and\n// this script — running in the new tab — collects it and does the paste + send.\n//\n// DELIVERY IS A PULL, NOT A PUSH\n// The background can't reliably message a tab that may not have loaded yet, so\n// this script asks (\"anything pending for my tab?\") on load and retries a few\n// times to cover the case where the tab id was registered a beat after the tab\n// was created. The background hands the prompt over exactly ONCE and forgets\n// it, so a reload / redirect can never re-submit the same prompt.\n//\n// TYPING METHOD\n// Every one of these composers is a framework-controlled editor (ProseMirror,\n// Quill, controlled React textarea). Writing .value or .textContent leaves the\n// framework's own state empty, so the send button stays disabled and Enter\n// sends nothing. document.execCommand('insertText') is routed through the\n// browser's real editing pipeline and fires the same events a human keystroke\n// does, so it works on all of them — plain textarea included.\n// ─────────────────────────────────────────────────────────────────────────────\n\n(function () {\n if (window.__vaultAiPromptLoaded) return; window.__vaultAiPromptLoaded = true;\n // Top frame only — the composer is never in a subframe, and all_frames would\n // otherwise run this in every ad/embed iframe on the page.\n try { if (window.top !== window) return; } catch (e) { return; }\n\n var COMPOSER_WAIT_MS = 30000; // cold ChatGPT load + auth redirect can be slow\n var POLL_MS = 250;\n var PULL_TRIES = 6; // × PULL_GAP_MS — covers a late tab-id registration\n var PULL_GAP_MS = 400;\n\n // Per-site selectors. First match wins; the generic finder below is the\n // safety net when a site ships a redesign, so a stale selector degrades to\n // \"still works\" rather than \"does nothing\".\n var PROFILES = [\n { name: 'ChatGPT', test: /(^|\\.)chatgpt\\.com$|(^|\\.)chat\\.openai\\.com$/i,\n input: ['#prompt-textarea', 'div[contenteditable=\"true\"]#prompt-textarea', 'textarea[data-id]'],\n send: ['button[data-testid=\"send-button\"]', '#composer-submit-button', 'button[aria-label=\"Send prompt\"]'] },\n { name: 'Claude', test: /(^|\\.)claude\\.ai$/i,\n input: ['div[contenteditable=\"true\"].ProseMirror', 'div[contenteditable=\"true\"][aria-label*=\"prompt\" i]'],\n send: ['button[aria-label=\"Send message\"]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Gemini', test: /(^|\\.)gemini\\.google\\.com$/i,\n input: ['rich-textarea div[contenteditable=\"true\"]', 'div.ql-editor[contenteditable=\"true\"]'],\n send: ['button.send-button', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Perplexity', test: /(^|\\.)perplexity\\.ai$/i,\n input: ['#ask-input', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[aria-label=\"Submit\"]', 'button[data-testid=\"submit-button\"]'] },\n { name: 'Grok', test: /(^|\\.)grok\\.com$|(^|\\.)x\\.com$/i,\n input: ['textarea[aria-label]', 'textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Copilot', test: /(^|\\.)copilot\\.microsoft\\.com$|(^|\\.)bing\\.com$/i,\n input: ['textarea#userInput', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[title*=\"Submit\" i]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'DeepSeek', test: /(^|\\.)deepseek\\.com$/i,\n input: ['textarea#chat-input', 'textarea'],\n send: ['div[role=\"button\"][aria-disabled=\"false\"]', 'button[type=\"submit\"]'] },\n { name: 'Mistral', test: /(^|\\.)mistral\\.ai$/i,\n input: ['textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Send\" i]'] },\n ];\n\n function profileFor(host) {\n for (var i = 0; i < PROFILES.length; i++) if (PROFILES[i].test.test(host)) return PROFILES[i];\n return { name: host, input: [], send: [] };\n }\n\n function visible(el, minW, minH) {\n if (!el || !el.isConnected) return false;\n var r = el.getBoundingClientRect();\n // minW/minH default to composer-sized. Send buttons MUST pass a much smaller\n // floor: ChatGPT's is a ~36px circular icon, so a 40px minimum silently\n // rejected it and we fell through to synthetic Enter, which ProseMirror\n // ignores — the prompt got typed in and just sat there.\n if (r.width < (minW == null ? 40 : minW) || r.height < (minH == null ? 12 : minH)) return false;\n var s = getComputedStyle(el);\n return s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0';\n }\n\n function editable(el) {\n if (!el) return false;\n if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;\n return el.isContentEditable;\n }\n\n // Fallback composer finder for when a site's selectors have gone stale:\n // the biggest visible editable box, preferring ones that announce themselves\n // as a message/prompt input.\n function findComposerGeneric() {\n var all = [];\n try { all = Array.prototype.slice.call(document.querySelectorAll('textarea, [contenteditable=\"true\"], [contenteditable=\"\"], [role=\"textbox\"]')); }\n catch (e) { return null; }\n var best = null, bestScore = -1;\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (!editable(el) || !visible(el)) continue;\n var r = el.getBoundingClientRect();\n var hint = ((el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' +\n (el.getAttribute('data-testid') || '') + ' ' + (el.id || '')).toLowerCase();\n var score = r.width * r.height;\n if (/message|prompt|ask|chat|question|input/.test(hint)) score *= 4;\n if (/search|url|address/.test(hint)) score *= 0.1; // site search box, not the composer\n if (score > bestScore) { bestScore = score; best = el; }\n }\n return best;\n }\n\n function findComposer(profile) {\n for (var i = 0; i < profile.input.length; i++) {\n var list;\n try { list = document.querySelectorAll(profile.input[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (editable(list[j]) && visible(list[j])) return list[j];\n }\n return findComposerGeneric();\n }\n\n function clickable(el) {\n return !!el && !el.disabled && el.getAttribute('aria-disabled') !== 'true' && visible(el, 10, 10);\n }\n\n function findSend(profile) {\n var sels = profile.send.concat([\n 'button[data-testid=\"send-button\"]', 'button[aria-label*=\"Send\" i]',\n 'button[aria-label*=\"Submit\" i]', 'button[title*=\"Send\" i]',\n 'button[title*=\"Submit\" i]', 'form button[type=\"submit\"]',\n ]);\n for (var i = 0; i < sels.length; i++) {\n var list;\n try { list = document.querySelectorAll(sels[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (clickable(list[j])) return list[j];\n }\n return null;\n }\n\n function composerText(el) {\n if (!el) return '';\n return (el.tagName === 'TEXTAREA' ? (el.value || '') : (el.innerText || el.textContent || '')).trim();\n }\n\n function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n function waitFor(fn, timeout) {\n var deadline = Date.now() + timeout;\n return new Promise(function (resolve) {\n (function tick() {\n var v = null;\n try { v = fn(); } catch (e) {}\n if (v) return resolve(v);\n if (Date.now() >= deadline) return resolve(null);\n setTimeout(tick, POLL_MS);\n })();\n });\n }\n\n // Write the prompt the way a keyboard would. Selecting first means a draft\n // the composer restored from a previous session is replaced, not appended to.\n function typeInto(el, text) {\n try { el.focus(); el.click(); } catch (e) {}\n try {\n if (el.tagName === 'TEXTAREA') { el.select(); }\n else {\n var range = document.createRange(); range.selectNodeContents(el);\n var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);\n }\n } catch (e) {}\n var ok = false;\n try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }\n if (!ok && el.tagName === 'TEXTAREA') {\n // Last resort for a textarea: the native value setter bypasses React's\n // property override, and the input event tells React to re-read it.\n try {\n var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;\n setter.call(el, text);\n el.dispatchEvent(new Event('input', { bubbles: true }));\n ok = true;\n } catch (e) {}\n }\n return ok;\n }\n\n function pressEnter(el) {\n var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true };\n try {\n el.dispatchEvent(new KeyboardEvent('keydown', opts));\n el.dispatchEvent(new KeyboardEvent('keypress', opts));\n el.dispatchEvent(new KeyboardEvent('keyup', opts));\n } catch (e) {}\n }\n\n async function deliver(text) {\n var profile = profileFor(location.hostname);\n var el = await waitFor(function () { return findComposer(profile); }, COMPOSER_WAIT_MS);\n // Returning false leaves the prompt pending in the background, so a reload\n // (or the real app document, if this one was an auth interstitial) retries.\n if (!el) { console.warn('[Vault] AI prompt: no composer found on ' + location.hostname); return false; }\n\n if (!typeInto(el, text)) { console.warn('[Vault] AI prompt: could not type into the composer'); return false; }\n\n // Keep trying to send until the composer actually empties — that's the only\n // reliable proof it went. One fire-and-hope click is how \"the prompt is\n // typed in but just sits there\" happens: the send button may not exist yet\n // (it appears once the site registers input), may be disabled for a beat, or\n // the click may land on the wrong control.\n var head = text.trim().slice(0, 40);\n var sent = false;\n for (var i = 0; i < 10 && !sent; i++) {\n var btn = findSend(profile);\n if (btn) { try { btn.click(); } catch (e) {} }\n // Enter only when there's no button to click, or after the button has\n // repeatedly failed — on ProseMirror sites a synthetic Enter is usually\n // ignored, so it's the fallback, not the first move.\n if (!btn || i >= 3) { try { el.focus(); } catch (e) {} pressEnter(el); }\n await wait(600);\n if (composerText(el).slice(0, 40) !== head) sent = true;\n }\n\n if (!sent) {\n console.warn('[Vault] AI prompt: typed it in, but no send control fired — press Enter yourself. ' +\n 'Send button found: ' + !!findSend(profile));\n return false; // stays pending, so a reload retries\n }\n // Only now tell the background it's done — a failed attempt must stay\n // pending so the next document (or a reload) can have another go.\n report();\n console.log('[Vault] AI prompt submitted on ' + profile.name);\n return true;\n }\n\n function report() {\n try { chrome.runtime.sendMessage({ action: 'vaultAiPromptDone' }, function () { void chrome.runtime.lastError; }); }\n catch (e) {}\n }\n\n // Ask the background whether this tab was opened to receive a prompt.\n function pull() {\n return new Promise(function (resolve) {\n try {\n chrome.runtime.sendMessage({ action: 'vaultAiPromptPending' }, function (resp) {\n if (chrome.runtime.lastError) return resolve(null);\n resolve(resp && resp.text ? String(resp.text) : null);\n });\n } catch (e) { resolve(null); }\n });\n }\n\n (async function () {\n for (var i = 0; i < PULL_TRIES; i++) {\n var text = await pull();\n if (text) {\n console.log('[Vault] AI prompt: ' + text.length + ' chars pending for this tab');\n deliver(text);\n return;\n }\n await wait(PULL_GAP_MS);\n }\n })();\n})();\n"}, "4a2ae422cbe7119224ac45c679a27a16880258d3": {"t": 1785771335.5312662, "n": 11602, "h": "b111ddb3c1e75e52", "c": "// ─────────────────────────────────────────────────────────────────────────────\n// vault-ai-prompt.js — types a prompt into an AI chat site and submits it.\n//\n// WHY THIS EXISTS\n// TradeHub's \"Launch Analysis\" button used to only copy the prompt and open\n// ChatGPT — you still had to press Ctrl+V + Enter yourself. A web page can't\n// type into another site (cross-origin), which is exactly why the Trading Auto\n// Launch (Python) had to drive it with OS-level keystrokes. A content script\n// CAN, because it runs inside the AI page. So TradeHub now asks the extension\n// to open the AI tab, the background remembers the prompt for that tab id, and\n// this script — running in the new tab — collects it and does the paste + send.\n//\n// DELIVERY IS A PULL, NOT A PUSH\n// The background can't reliably message a tab that may not have loaded yet, so\n// this script asks (\"anything pending for my tab?\") on load and retries a few\n// times to cover the case where the tab id was registered a beat after the tab\n// was created. The background hands the prompt over exactly ONCE and forgets\n// it, so a reload / redirect can never re-submit the same prompt.\n//\n// TYPING METHOD\n// Every one of these composers is a framework-controlled editor (ProseMirror,\n// Quill, controlled React textarea). Writing .value or .textContent leaves the\n// framework's own state empty, so the send button stays disabled and Enter\n// sends nothing. document.execCommand('insertText') is routed through the\n// browser's real editing pipeline and fires the same events a human keystroke\n// does, so it works on all of them — plain textarea included.\n// ─────────────────────────────────────────────────────────────────────────────\n\n(function () {\n if (window.__vaultAiPromptLoaded) return; window.__vaultAiPromptLoaded = true;\n // Top frame only — the composer is never in a subframe, and all_frames would\n // otherwise run this in every ad/embed iframe on the page.\n try { if (window.top !== window) return; } catch (e) { return; }\n\n var COMPOSER_WAIT_MS = 30000; // cold ChatGPT load + auth redirect can be slow\n var POLL_MS = 250;\n var PULL_TRIES = 6; // × PULL_GAP_MS — covers a late tab-id registration\n var PULL_GAP_MS = 400;\n\n // Per-site selectors. First match wins; the generic finder below is the\n // safety net when a site ships a redesign, so a stale selector degrades to\n // \"still works\" rather than \"does nothing\".\n var PROFILES = [\n { name: 'ChatGPT', test: /(^|\\.)chatgpt\\.com$|(^|\\.)chat\\.openai\\.com$/i,\n input: ['#prompt-textarea', 'div[contenteditable=\"true\"]#prompt-textarea', 'textarea[data-id]'],\n send: ['button[data-testid=\"send-button\"]', '#composer-submit-button', 'button[aria-label=\"Send prompt\"]'] },\n { name: 'Claude', test: /(^|\\.)claude\\.ai$/i,\n input: ['div[contenteditable=\"true\"].ProseMirror', 'div[contenteditable=\"true\"][aria-label*=\"prompt\" i]'],\n send: ['button[aria-label=\"Send message\"]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Gemini', test: /(^|\\.)gemini\\.google\\.com$/i,\n input: ['rich-textarea div[contenteditable=\"true\"]', 'div.ql-editor[contenteditable=\"true\"]'],\n send: ['button.send-button', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Perplexity', test: /(^|\\.)perplexity\\.ai$/i,\n input: ['#ask-input', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[aria-label=\"Submit\"]', 'button[data-testid=\"submit-button\"]'] },\n { name: 'Grok', test: /(^|\\.)grok\\.com$|(^|\\.)x\\.com$/i,\n input: ['textarea[aria-label]', 'textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Copilot', test: /(^|\\.)copilot\\.microsoft\\.com$|(^|\\.)bing\\.com$/i,\n input: ['textarea#userInput', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[title*=\"Submit\" i]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'DeepSeek', test: /(^|\\.)deepseek\\.com$/i,\n input: ['textarea#chat-input', 'textarea'],\n send: ['div[role=\"button\"][aria-disabled=\"false\"]', 'button[type=\"submit\"]'] },\n { name: 'Mistral', test: /(^|\\.)mistral\\.ai$/i,\n input: ['textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Send\" i]'] },\n ];\n\n function profileFor(host) {\n for (var i = 0; i < PROFILES.length; i++) if (PROFILES[i].test.test(host)) return PROFILES[i];\n return { name: host, input: [], send: [] };\n }\n\n function visible(el) {\n if (!el || !el.isConnected) return false;\n var r = el.getBoundingClientRect();\n if (r.width < 40 || r.height < 12) return false;\n var s = getComputedStyle(el);\n return s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0';\n }\n\n function editable(el) {\n if (!el) return false;\n if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;\n return el.isContentEditable;\n }\n\n // Fallback composer finder for when a site's selectors have gone stale:\n // the biggest visible editable box, preferring ones that announce themselves\n // as a message/prompt input.\n function findComposerGeneric() {\n var all = [];\n try { all = Array.prototype.slice.call(document.querySelectorAll('textarea, [contenteditable=\"true\"], [contenteditable=\"\"], [role=\"textbox\"]')); }\n catch (e) { return null; }\n var best = null, bestScore = -1;\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (!editable(el) || !visible(el)) continue;\n var r = el.getBoundingClientRect();\n var hint = ((el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' +\n (el.getAttribute('data-testid') || '') + ' ' + (el.id || '')).toLowerCase();\n var score = r.width * r.height;\n if (/message|prompt|ask|chat|question|input/.test(hint)) score *= 4;\n if (/search|url|address/.test(hint)) score *= 0.1; // site search box, not the composer\n if (score > bestScore) { bestScore = score; best = el; }\n }\n return best;\n }\n\n function findComposer(profile) {\n for (var i = 0; i < profile.input.length; i++) {\n var list;\n try { list = document.querySelectorAll(profile.input[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (editable(list[j]) && visible(list[j])) return list[j];\n }\n return findComposerGeneric();\n }\n\n function clickable(el) {\n return !!el && !el.disabled && el.getAttribute('aria-disabled') !== 'true' && visible(el);\n }\n\n function findSend(profile) {\n var sels = profile.send.concat([\n 'button[data-testid=\"send-button\"]', 'button[aria-label*=\"Send\" i]',\n 'button[aria-label*=\"Submit\" i]', 'button[title*=\"Send\" i]',\n 'button[title*=\"Submit\" i]', 'form button[type=\"submit\"]',\n ]);\n for (var i = 0; i < sels.length; i++) {\n var list;\n try { list = document.querySelectorAll(sels[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (clickable(list[j])) return list[j];\n }\n return null;\n }\n\n function composerText(el) {\n if (!el) return '';\n return (el.tagName === 'TEXTAREA' ? (el.value || '') : (el.innerText || el.textContent || '')).trim();\n }\n\n function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n function waitFor(fn, timeout) {\n var deadline = Date.now() + timeout;\n return new Promise(function (resolve) {\n (function tick() {\n var v = null;\n try { v = fn(); } catch (e) {}\n if (v) return resolve(v);\n if (Date.now() >= deadline) return resolve(null);\n setTimeout(tick, POLL_MS);\n })();\n });\n }\n\n // Write the prompt the way a keyboard would. Selecting first means a draft\n // the composer restored from a previous session is replaced, not appended to.\n function typeInto(el, text) {\n try { el.focus(); el.click(); } catch (e) {}\n try {\n if (el.tagName === 'TEXTAREA') { el.select(); }\n else {\n var range = document.createRange(); range.selectNodeContents(el);\n var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);\n }\n } catch (e) {}\n var ok = false;\n try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }\n if (!ok && el.tagName === 'TEXTAREA') {\n // Last resort for a textarea: the native value setter bypasses React's\n // property override, and the input event tells React to re-read it.\n try {\n var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;\n setter.call(el, text);\n el.dispatchEvent(new Event('input', { bubbles: true }));\n ok = true;\n } catch (e) {}\n }\n return ok;\n }\n\n function pressEnter(el) {\n var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true };\n try {\n el.dispatchEvent(new KeyboardEvent('keydown', opts));\n el.dispatchEvent(new KeyboardEvent('keypress', opts));\n el.dispatchEvent(new KeyboardEvent('keyup', opts));\n } catch (e) {}\n }\n\n async function deliver(text) {\n var profile = profileFor(location.hostname);\n var el = await waitFor(function () { return findComposer(profile); }, COMPOSER_WAIT_MS);\n // Returning false leaves the prompt pending in the background, so a reload\n // (or the real app document, if this one was an auth interstitial) retries.\n if (!el) { console.warn('[Vault] AI prompt: no composer found on ' + location.hostname); return false; }\n\n if (!typeInto(el, text)) { console.warn('[Vault] AI prompt: could not type into the composer'); return false; }\n\n // Give the site a moment to register the text and enable its send button.\n var btn = await waitFor(function () { return findSend(profile); }, 3000);\n if (btn) { try { btn.click(); } catch (e) { pressEnter(el); } }\n else { pressEnter(el); }\n\n // Report the submit straight away — if this page navigates on send (a new\n // conversation URL), the confirmation has to be already in flight, or the\n // next document would find the prompt still pending and send it twice.\n report();\n\n // Then confirm: if our text is still sitting in the composer, the click\n // landed on the wrong control — fall back to Enter once.\n await wait(1200);\n if (composerText(el) && composerText(el).slice(0, 40) === text.trim().slice(0, 40)) {\n try { el.focus(); } catch (e) {}\n pressEnter(el);\n }\n console.log('[Vault] AI prompt submitted on ' + profile.name);\n return true;\n }\n\n function report() {\n try { chrome.runtime.sendMessage({ action: 'vaultAiPromptDone' }, function () { void chrome.runtime.lastError; }); }\n catch (e) {}\n }\n\n // Ask the background whether this tab was opened to receive a prompt.\n function pull() {\n return new Promise(function (resolve) {\n try {\n chrome.runtime.sendMessage({ action: 'vaultAiPromptPending' }, function (resp) {\n if (chrome.runtime.lastError) return resolve(null);\n resolve(resp && resp.text ? String(resp.text) : null);\n });\n } catch (e) { resolve(null); }\n });\n }\n\n (async function () {\n for (var i = 0; i < PULL_TRIES; i++) {\n var text = await pull();\n if (text) {\n console.log('[Vault] AI prompt: ' + text.length + ' chars pending for this tab');\n deliver(text);\n return;\n }\n await wait(PULL_GAP_MS);\n }\n })();\n})();\n"}, "2a08c7e492a679a957bea065f21056657f8667a6": {"t": 1785771336.8886201, "n": 10927, "h": "a3af2c87c4a689ed", "c": "// ─────────────────────────────────────────────────────────────────────────────\n// vault-ai-prompt.js — types a prompt into an AI chat site and submits it.\n//\n// WHY THIS EXISTS\n// TradeHub's \"Launch Analysis\" button used to only copy the prompt and open\n// ChatGPT — you still had to press Ctrl+V + Enter yourself. A web page can't\n// type into another site (cross-origin), which is exactly why the Trading Auto\n// Launch (Python) had to drive it with OS-level keystrokes. A content script\n// CAN, because it runs inside the AI page. So TradeHub now asks the extension\n// to open the AI tab, the background remembers the prompt for that tab id, and\n// this script — running in the new tab — collects it and does the paste + send.\n//\n// DELIVERY IS A PULL, NOT A PUSH\n// The background can't reliably message a tab that may not have loaded yet, so\n// this script asks (\"anything pending for my tab?\") on load and retries a few\n// times to cover the case where the tab id was registered a beat after the tab\n// was created. The background hands the prompt over exactly ONCE and forgets\n// it, so a reload / redirect can never re-submit the same prompt.\n//\n// TYPING METHOD\n// Every one of these composers is a framework-controlled editor (ProseMirror,\n// Quill, controlled React textarea). Writing .value or .textContent leaves the\n// framework's own state empty, so the send button stays disabled and Enter\n// sends nothing. document.execCommand('insertText') is routed through the\n// browser's real editing pipeline and fires the same events a human keystroke\n// does, so it works on all of them — plain textarea included.\n// ─────────────────────────────────────────────────────────────────────────────\n\n(function () {\n if (window.__vaultAiPromptLoaded) return; window.__vaultAiPromptLoaded = true;\n // Top frame only — the composer is never in a subframe, and all_frames would\n // otherwise run this in every ad/embed iframe on the page.\n try { if (window.top !== window) return; } catch (e) { return; }\n\n var COMPOSER_WAIT_MS = 30000; // cold ChatGPT load + auth redirect can be slow\n var POLL_MS = 250;\n var PULL_TRIES = 6; // × PULL_GAP_MS — covers a late tab-id registration\n var PULL_GAP_MS = 400;\n\n // Per-site selectors. First match wins; the generic finder below is the\n // safety net when a site ships a redesign, so a stale selector degrades to\n // \"still works\" rather than \"does nothing\".\n var PROFILES = [\n { name: 'ChatGPT', test: /(^|\\.)chatgpt\\.com$|(^|\\.)chat\\.openai\\.com$/i,\n input: ['#prompt-textarea', 'div[contenteditable=\"true\"]#prompt-textarea', 'textarea[data-id]'],\n send: ['button[data-testid=\"send-button\"]', '#composer-submit-button', 'button[aria-label=\"Send prompt\"]'] },\n { name: 'Claude', test: /(^|\\.)claude\\.ai$/i,\n input: ['div[contenteditable=\"true\"].ProseMirror', 'div[contenteditable=\"true\"][aria-label*=\"prompt\" i]'],\n send: ['button[aria-label=\"Send message\"]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Gemini', test: /(^|\\.)gemini\\.google\\.com$/i,\n input: ['rich-textarea div[contenteditable=\"true\"]', 'div.ql-editor[contenteditable=\"true\"]'],\n send: ['button.send-button', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Perplexity', test: /(^|\\.)perplexity\\.ai$/i,\n input: ['#ask-input', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[aria-label=\"Submit\"]', 'button[data-testid=\"submit-button\"]'] },\n { name: 'Grok', test: /(^|\\.)grok\\.com$|(^|\\.)x\\.com$/i,\n input: ['textarea[aria-label]', 'textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'Copilot', test: /(^|\\.)copilot\\.microsoft\\.com$|(^|\\.)bing\\.com$/i,\n input: ['textarea#userInput', 'textarea[placeholder]', 'div[contenteditable=\"true\"]'],\n send: ['button[title*=\"Submit\" i]', 'button[aria-label*=\"Submit\" i]', 'button[aria-label*=\"Send\" i]'] },\n { name: 'DeepSeek', test: /(^|\\.)deepseek\\.com$/i,\n input: ['textarea#chat-input', 'textarea'],\n send: ['div[role=\"button\"][aria-disabled=\"false\"]', 'button[type=\"submit\"]'] },\n { name: 'Mistral', test: /(^|\\.)mistral\\.ai$/i,\n input: ['textarea', 'div[contenteditable=\"true\"]'],\n send: ['button[type=\"submit\"]', 'button[aria-label*=\"Send\" i]'] },\n ];\n\n function profileFor(host) {\n for (var i = 0; i < PROFILES.length; i++) if (PROFILES[i].test.test(host)) return PROFILES[i];\n return { name: host, input: [], send: [] };\n }\n\n function visible(el) {\n if (!el || !el.isConnected) return false;\n var r = el.getBoundingClientRect();\n if (r.width < 40 || r.height < 12) return false;\n var s = getComputedStyle(el);\n return s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0';\n }\n\n function editable(el) {\n if (!el) return false;\n if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;\n return el.isContentEditable;\n }\n\n // Fallback composer finder for when a site's selectors have gone stale:\n // the biggest visible editable box, preferring ones that announce themselves\n // as a message/prompt input.\n function findComposerGeneric() {\n var all = [];\n try { all = Array.prototype.slice.call(document.querySelectorAll('textarea, [contenteditable=\"true\"], [contenteditable=\"\"], [role=\"textbox\"]')); }\n catch (e) { return null; }\n var best = null, bestScore = -1;\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (!editable(el) || !visible(el)) continue;\n var r = el.getBoundingClientRect();\n var hint = ((el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' +\n (el.getAttribute('data-testid') || '') + ' ' + (el.id || '')).toLowerCase();\n var score = r.width * r.height;\n if (/message|prompt|ask|chat|question|input/.test(hint)) score *= 4;\n if (/search|url|address/.test(hint)) score *= 0.1; // site search box, not the composer\n if (score > bestScore) { bestScore = score; best = el; }\n }\n return best;\n }\n\n function findComposer(profile) {\n for (var i = 0; i < profile.input.length; i++) {\n var list;\n try { list = document.querySelectorAll(profile.input[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (editable(list[j]) && visible(list[j])) return list[j];\n }\n return findComposerGeneric();\n }\n\n function clickable(el) {\n return !!el && !el.disabled && el.getAttribute('aria-disabled') !== 'true' && visible(el);\n }\n\n function findSend(profile) {\n var sels = profile.send.concat([\n 'button[data-testid=\"send-button\"]', 'button[aria-label*=\"Send\" i]',\n 'button[aria-label*=\"Submit\" i]', 'button[title*=\"Send\" i]',\n 'button[title*=\"Submit\" i]', 'form button[type=\"submit\"]',\n ]);\n for (var i = 0; i < sels.length; i++) {\n var list;\n try { list = document.querySelectorAll(sels[i]); } catch (e) { continue; }\n for (var j = 0; j < list.length; j++) if (clickable(list[j])) return list[j];\n }\n return null;\n }\n\n function composerText(el) {\n if (!el) return '';\n return (el.tagName === 'TEXTAREA' ? (el.value || '') : (el.innerText || el.textContent || '')).trim();\n }\n\n function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n function waitFor(fn, timeout) {\n var deadline = Date.now() + timeout;\n return new Promise(function (resolve) {\n (function tick() {\n var v = null;\n try { v = fn(); } catch (e) {}\n if (v) return resolve(v);\n if (Date.now() >= deadline) return resolve(null);\n setTimeout(tick, POLL_MS);\n })();\n });\n }\n\n // Write the prompt the way a keyboard would. Selecting first means a draft\n // the composer restored from a previous session is replaced, not appended to.\n function typeInto(el, text) {\n try { el.focus(); el.click(); } catch (e) {}\n try {\n if (el.tagName === 'TEXTAREA') { el.select(); }\n else {\n var range = document.createRange(); range.selectNodeContents(el);\n var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);\n }\n } catch (e) {}\n var ok = false;\n try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }\n if (!ok && el.tagName === 'TEXTAREA') {\n // Last resort for a textarea: the native value setter bypasses React's\n // property override, and the input event tells React to re-read it.\n try {\n var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;\n setter.call(el, text);\n el.dispatchEvent(new Event('input', { bubbles: true }));\n ok = true;\n } catch (e) {}\n }\n return ok;\n }\n\n function pressEnter(el) {\n var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true };\n try {\n el.dispatchEvent(new KeyboardEvent('keydown', opts));\n el.dispatchEvent(new KeyboardEvent('keypress', opts));\n el.dispatchEvent(new KeyboardEvent('keyup', opts));\n } catch (e) {}\n }\n\n async function deliver(text) {\n var profile = profileFor(location.hostname);\n var el = await waitFor(function () { return findComposer(profile); }, COMPOSER_WAIT_MS);\n if (!el) { console.warn('[Vault] AI prompt: no composer found on ' + location.hostname); return false; }\n\n if (!typeInto(el, text)) { console.warn('[Vault] AI prompt: could not type into the composer'); return false; }\n\n // Give the site a moment to register the text and enable its send button.\n var btn = await waitFor(function () { return findSend(profile); }, 3000);\n if (btn) { try { btn.click(); } catch (e) { pressEnter(el); } }\n else { pressEnter(el); }\n\n // Confirm it actually went: if our text is still sitting in the composer,\n // the click landed on the wrong control — fall back to Enter once.\n await wait(1200);\n if (composerText(el) && composerText(el).slice(0, 40) === text.trim().slice(0, 40)) {\n try { el.focus(); } catch (e) {}\n pressEnter(el);\n }\n console.log('[Vault] AI prompt submitted on ' + profile.name);\n return true;\n }\n\n // Ask the background whether this tab was opened to receive a prompt.\n function pull() {\n return new Promise(function (resolve) {\n try {\n chrome.runtime.sendMessage({ action: 'vaultAiPromptPending' }, function (resp) {\n if (chrome.runtime.lastError) return resolve(null);\n resolve(resp && resp.text ? String(resp.text) : null);\n });\n } catch (e) { resolve(null); }\n });\n }\n\n (async function () {\n for (var i = 0; i < PULL_TRIES; i++) {\n var text = await pull();\n if (text) { deliver(text); return; }\n await wait(PULL_GAP_MS);\n }\n })();\n})();\n"}}}}