{"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"}}}}