Snapshot of the working tree exactly as it stood, no edits. This is the predecessor
PRICEGOD was rewritten from ("kept intact, untouched" per pricegod/README.md), and it
is still the only place the DYMO scale is actually implemented —
rfid-daemon/index.js:1068-1265: HID discovery, parseScaleReport, one-shot read, and a
streaming /weight + /scale/start|stop session whose JSON shape PRICEGOD's daemon.js
already speaks.
Preserved verbatim on purpose (hence -og), including the known bug: DYMO_PIDS at
index.js:1082 is [0x8003, 0x8004], so it cannot see the bench M25 (0x8009). Fix that
in whatever daemon inherits the scale, not here.
node_modules stays ignored (22M of the 24M tree). No credentials in the import: the
two PEM markers in utils.js/sheets.js only strip headers off a key read from settings,
and mrpadmin / johnking are an SSH and a Postgres username, both key/trust auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
514 lines
19 KiB
JavaScript
514 lines
19 KiB
JavaScript
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
async function saveDebug(data) {
|
|
try {
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiDebugLast: { ...data, at: new Date().toISOString() } }, resolve));
|
|
} catch {}
|
|
}
|
|
|
|
function normMarker(v) {
|
|
return String(v || '')
|
|
.trim()
|
|
.replace(/^"+|"+$/g, '')
|
|
.replace(/[\s_]+/g, '')
|
|
.toUpperCase();
|
|
}
|
|
|
|
function isVisible(el) {
|
|
if (!el) return false;
|
|
const style = window.getComputedStyle(el);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
const rect = el.getBoundingClientRect();
|
|
return rect.width > 0 && rect.height > 0;
|
|
}
|
|
|
|
async function waitForSelectorAny(selectors, timeoutMs) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
for (const sel of selectors) {
|
|
const el = document.querySelector(sel);
|
|
if (isVisible(el)) return el;
|
|
}
|
|
await sleep(250);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function waitForInputEl(timeoutMs) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
const els = [
|
|
...document.querySelectorAll('textarea'),
|
|
...document.querySelectorAll('div[contenteditable="true"]'),
|
|
].filter(isVisible);
|
|
const best = els.find(e => (e.getAttribute('aria-label') || '').toLowerCase().includes('prompt'))
|
|
|| els.find(e => (e.getAttribute('aria-label') || '').toLowerCase().includes('message'))
|
|
|| els[0];
|
|
if (best) return best;
|
|
await sleep(250);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setInputText(el, text) {
|
|
el.focus();
|
|
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
|
const proto = el.tagName === 'TEXTAREA'
|
|
? HTMLTextAreaElement.prototype
|
|
: HTMLInputElement.prototype;
|
|
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
if (desc && desc.set) desc.set.call(el, text);
|
|
else el.value = text;
|
|
try {
|
|
el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertFromPaste', data: text }));
|
|
} catch {}
|
|
try {
|
|
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: text }));
|
|
} catch {
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return;
|
|
}
|
|
const editable = el.getAttribute('contenteditable') === 'true';
|
|
if (editable) {
|
|
el.focus();
|
|
const sel = window.getSelection && window.getSelection();
|
|
if (sel) {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(el);
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
}
|
|
try {
|
|
document.execCommand('insertText', false, text);
|
|
} catch {
|
|
el.textContent = text;
|
|
}
|
|
} else {
|
|
el.textContent = text;
|
|
}
|
|
try {
|
|
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: text }));
|
|
} catch {
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
function findSendButton() {
|
|
const selectors = [
|
|
'button[type="submit"]',
|
|
'button[aria-label="Send message"]',
|
|
'button[aria-label="Send"]',
|
|
'button[aria-label*="Send"]',
|
|
'button[aria-label*="send"]',
|
|
'button[aria-label*="Submit"]',
|
|
'button[aria-label*="submit"]',
|
|
'button[data-testid*="send"]',
|
|
'button.send-button',
|
|
'button.submit',
|
|
];
|
|
for (const sel of selectors) {
|
|
const btns = [...document.querySelectorAll(sel)];
|
|
for (const btn of btns) {
|
|
if (isVisible(btn)) return btn;
|
|
}
|
|
}
|
|
const btns = [...document.querySelectorAll('button')].filter(isVisible);
|
|
const labeled = btns.filter(b => {
|
|
const label = `${b.getAttribute('aria-label') || ''} ${b.getAttribute('title') || ''} ${b.textContent || ''}`.toLowerCase();
|
|
return label.includes('send message') || label === 'send' || label.includes(' send ') || label.includes('submit');
|
|
});
|
|
if (labeled.length) return labeled[labeled.length - 1];
|
|
return null;
|
|
}
|
|
|
|
function triggerEnter(el) {
|
|
const down = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' });
|
|
const up = new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' });
|
|
el.dispatchEvent(down);
|
|
el.dispatchEvent(up);
|
|
}
|
|
|
|
function isAriaDisabled(el) {
|
|
const v = (el?.getAttribute('aria-disabled') || '').toLowerCase();
|
|
return v === 'true';
|
|
}
|
|
|
|
function clickLikeAUser(el) {
|
|
if (!el) return;
|
|
const r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
const clientX = r ? Math.round(r.left + r.width / 2) : 1;
|
|
const clientY = r ? Math.round(r.top + r.height / 2) : 1;
|
|
try { el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX, clientY, pointerType: 'mouse', isPrimary: true })); } catch {}
|
|
try { el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX, clientY })); } catch {}
|
|
try { el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX, clientY, pointerType: 'mouse', isPrimary: true })); } catch {}
|
|
try { el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX, clientY })); } catch {}
|
|
try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX, clientY })); } catch {}
|
|
try { el.click(); } catch {}
|
|
}
|
|
|
|
function findSendButtonNear(inputEl) {
|
|
const containers = [];
|
|
const form = inputEl?.closest && inputEl.closest('form');
|
|
if (form) containers.push(form);
|
|
let p = inputEl;
|
|
for (let i = 0; i < 6 && p; i++) {
|
|
if (p.parentElement) containers.push(p.parentElement);
|
|
p = p.parentElement;
|
|
}
|
|
|
|
const selectors = [
|
|
'button[aria-label="Send message"]',
|
|
'button.send-button',
|
|
'button.submit',
|
|
'button[type="submit"]',
|
|
];
|
|
|
|
for (const c of containers) {
|
|
for (const sel of selectors) {
|
|
const btns = [...c.querySelectorAll(sel)].filter(isVisible);
|
|
if (btns.length) return btns[btns.length - 1];
|
|
}
|
|
}
|
|
return findSendButton();
|
|
}
|
|
|
|
async function waitForSendEnabled(inputEl, timeoutMs) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
const btn = findSendButtonNear(inputEl);
|
|
if (btn && isVisible(btn) && !btn.disabled && !isAriaDisabled(btn)) return btn;
|
|
await sleep(250);
|
|
}
|
|
return findSendButtonNear(inputEl);
|
|
}
|
|
|
|
async function sendMessageNow(inputEl) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const sendBtn = await waitForSendEnabled(inputEl, 4000);
|
|
await saveDebug({
|
|
attempt: i + 1,
|
|
hasSendBtn: !!sendBtn,
|
|
sendBtnDisabled: !!sendBtn?.disabled,
|
|
sendBtnAriaDisabled: (sendBtn?.getAttribute && sendBtn.getAttribute('aria-disabled')) || null,
|
|
sendBtnLabel: (sendBtn?.getAttribute && sendBtn.getAttribute('aria-label')) || null,
|
|
});
|
|
if (sendBtn && !sendBtn.disabled && !isAriaDisabled(sendBtn)) {
|
|
try { sendBtn.scrollIntoView({ block: 'center', inline: 'center' }); } catch {}
|
|
clickLikeAUser(sendBtn);
|
|
await sleep(600);
|
|
if (isGenerating()) return true;
|
|
const form = sendBtn.closest && sendBtn.closest('form');
|
|
if (form && form.requestSubmit) {
|
|
try { form.requestSubmit(); } catch {}
|
|
await sleep(600);
|
|
if (isGenerating()) return true;
|
|
}
|
|
}
|
|
triggerEnter(inputEl);
|
|
await sleep(600);
|
|
if (isGenerating()) return true;
|
|
try {
|
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
} catch {}
|
|
await sleep(250);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isGenerating() {
|
|
const stopBtn = document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"]');
|
|
if (isVisible(stopBtn)) return true;
|
|
const progress = document.querySelector('[role="progressbar"]');
|
|
if (isVisible(progress)) return true;
|
|
return false;
|
|
}
|
|
|
|
function extractJsonObjects(text, maxObjects = 20) {
|
|
if (!text) return [];
|
|
const s = String(text);
|
|
const out = [];
|
|
let i = 0;
|
|
while (i < s.length && out.length < maxObjects) {
|
|
const start = s.indexOf('{', i);
|
|
if (start === -1) break;
|
|
let depth = 0;
|
|
let inStr = false;
|
|
let esc = false;
|
|
for (let j = start; j < s.length; j++) {
|
|
const ch = s[j];
|
|
if (inStr) {
|
|
if (esc) esc = false;
|
|
else if (ch === '\\\\') esc = true;
|
|
else if (ch === '"') inStr = false;
|
|
continue;
|
|
}
|
|
if (ch === '"') { inStr = true; continue; }
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') {
|
|
depth--;
|
|
if (depth === 0) {
|
|
out.push(s.slice(start, j + 1));
|
|
i = j + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (j === s.length - 1) i = s.length;
|
|
}
|
|
if (i === start) i = start + 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function chooseBestJsonFromText(text, marker) {
|
|
const markerNorm = normMarker(marker);
|
|
const candidates = extractJsonObjects(text, 25);
|
|
const expectedKeys = [
|
|
'summary',
|
|
'store_description',
|
|
'features',
|
|
'sound_profile',
|
|
'highlight_tracks',
|
|
'production',
|
|
'market_insight',
|
|
'sales_angles',
|
|
'embedding',
|
|
];
|
|
|
|
let best = null;
|
|
let bestScore = -Infinity;
|
|
for (const cand of candidates) {
|
|
let parsed;
|
|
try { parsed = JSON.parse(cand); } catch { continue; }
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
|
|
|
|
let score = 0;
|
|
const eomNorm = normMarker(parsed.eom);
|
|
if (markerNorm && eomNorm === markerNorm) score += 200;
|
|
|
|
for (const k of expectedKeys) if (Object.prototype.hasOwnProperty.call(parsed, k)) score += 10;
|
|
|
|
const placeholderSummary = 'Short factual description (1-2 sentences)';
|
|
const placeholderStore = 'Punchy, slightly opinionated record store blurb (max 80 words, human tone, not generic AI)';
|
|
if (parsed.summary === placeholderSummary) score -= 50;
|
|
if (parsed.store_description === placeholderStore) score -= 50;
|
|
|
|
if (parsed.embedding && typeof parsed.embedding === 'object') score += 5;
|
|
if (parsed.embedding?.description && typeof parsed.embedding.description === 'string') score += 5;
|
|
if (parsed.embedding?.vibe && typeof parsed.embedding.vibe === 'string') score += 5;
|
|
if (parsed.embedding?.sales && typeof parsed.embedding.sales === 'string') score += 5;
|
|
|
|
if (cand.length > 2000) score += 5;
|
|
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
best = parsed;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function extractLastResponseText() {
|
|
const selectors = [
|
|
'main div[class*="markdown"]',
|
|
'main div[class*="response"]',
|
|
'main div[class*="model"]',
|
|
'main [role="article"]',
|
|
'main article',
|
|
];
|
|
const seen = new Set();
|
|
const nodes = [];
|
|
for (const sel of selectors) {
|
|
for (const el of document.querySelectorAll(sel)) {
|
|
if (!isVisible(el)) continue;
|
|
const txt = (el.innerText || '').trim();
|
|
if (txt.length < 40) continue;
|
|
if (seen.has(txt)) continue;
|
|
seen.add(txt);
|
|
nodes.push({ el, txt });
|
|
}
|
|
}
|
|
if (!nodes.length) {
|
|
const main = document.querySelector('main');
|
|
const txt = (main?.innerText || '').trim();
|
|
return txt.length ? txt : null;
|
|
}
|
|
const scored = nodes.map(n => {
|
|
const t = n.txt;
|
|
let s = t.length / 1000;
|
|
if (t.includes('__PLICE_EOM__')) s += 50;
|
|
if (t.trimStart().startsWith('{')) s += 10;
|
|
if (t.includes('You said')) s -= 20;
|
|
if (t.includes('INPUT JSON:')) s -= 10;
|
|
if (t.includes('OUTPUT SCHEMA:')) s -= 10;
|
|
if (t.includes('Gemini is AI and can make mistakes')) s -= 5;
|
|
return { ...n, score: s };
|
|
}).sort((a, b) => b.score - a.score);
|
|
return scored[0].txt;
|
|
}
|
|
|
|
async function waitForStableResponse(timeoutMs) {
|
|
const start = Date.now();
|
|
let last = null;
|
|
let stable = 0;
|
|
while (Date.now() - start < timeoutMs) {
|
|
const txt = extractLastResponseText();
|
|
const gen = isGenerating();
|
|
if (txt && txt === last) stable += 1;
|
|
else stable = 0;
|
|
last = txt;
|
|
if (txt && txt.includes('__PLICE_EOM__') && !gen && stable >= 2) return txt;
|
|
if (txt && !gen && stable >= 6) return txt;
|
|
await sleep(500);
|
|
}
|
|
return extractLastResponseText();
|
|
}
|
|
|
|
function parseJsonMaybe(text) {
|
|
if (!text) return null;
|
|
const trimmed = text.trim();
|
|
const start = trimmed.indexOf('{');
|
|
const end = trimmed.lastIndexOf('}');
|
|
if (start === -1 || end === -1 || end <= start) return null;
|
|
const candidate = trimmed.slice(start, end + 1);
|
|
try { return JSON.parse(candidate); } catch { return null; }
|
|
}
|
|
|
|
function findJsonWithMarker(text, marker) {
|
|
if (!text || !marker) return null;
|
|
const i = text.indexOf(marker);
|
|
if (i === -1) return null;
|
|
const before = text.lastIndexOf('{', i);
|
|
const after = text.indexOf('}', i);
|
|
if (before === -1 || after === -1 || after <= before) return null;
|
|
const candidate = text.slice(before, after + 1);
|
|
try { return JSON.parse(candidate); } catch { return null; }
|
|
}
|
|
|
|
async function waitForJsonAndText(marker, timeoutMs) {
|
|
const start = Date.now();
|
|
const want = normMarker(marker);
|
|
while (Date.now() - start < timeoutMs) {
|
|
const txt = extractLastResponseText() || '';
|
|
if (txt) {
|
|
const parsed = chooseBestJsonFromText(txt, marker);
|
|
if (parsed) {
|
|
const got = normMarker(parsed?.eom);
|
|
if (!want || got === want) return { text: txt, json: parsed };
|
|
}
|
|
if (marker && txt.includes(marker) && !isGenerating()) return { text: txt, json: parsed || chooseBestJsonFromText(txt, marker) || parseJsonMaybe(txt) };
|
|
}
|
|
await sleep(500);
|
|
}
|
|
const finalTxt = extractLastResponseText();
|
|
return { text: finalTxt, json: chooseBestJsonFromText(finalTxt, marker) || parseJsonMaybe(finalTxt) };
|
|
}
|
|
|
|
async function sendToBackground(message) {
|
|
return await new Promise(resolve => {
|
|
try {
|
|
chrome.runtime.sendMessage(message, (resp) => {
|
|
const err = chrome.runtime?.lastError?.message;
|
|
if (err) resolve({ ok: false, error: err });
|
|
else resolve(resp || { ok: false, error: 'No response' });
|
|
});
|
|
} catch (e) {
|
|
resolve({ ok: false, error: e.message || 'sendMessage failed' });
|
|
}
|
|
});
|
|
}
|
|
|
|
async function run() {
|
|
const job = await new Promise(resolve => chrome.storage.local.get(['geminiJob'], r => resolve(r.geminiJob || null)));
|
|
if (!job || !job.prompt || (job.status !== 'pending' && job.status !== 'running')) return;
|
|
|
|
// If the job is older than 15 minutes and still pending/running, it's stale — abandon it
|
|
// so it doesn't re-inject text into Gemini on every page load.
|
|
const jobAge = job.created_at ? (Date.now() - new Date(job.created_at).getTime()) : Infinity;
|
|
if (jobAge > 15 * 60 * 1000) {
|
|
await new Promise(resolve => chrome.storage.local.set({
|
|
geminiJob: { ...job, status: 'error', error: 'Job abandoned (stale — older than 15 minutes)', failed_at: new Date().toISOString() }
|
|
}, resolve));
|
|
return;
|
|
}
|
|
|
|
if (job.status === 'pending') {
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'running', started_at: new Date().toISOString() } }, resolve));
|
|
}
|
|
|
|
const inputEl = await waitForInputEl(45000);
|
|
if (!inputEl) {
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'Gemini input box not found.', failed_at: new Date().toISOString() } }, resolve));
|
|
return;
|
|
}
|
|
|
|
setInputText(inputEl, job.prompt);
|
|
await sleep(500);
|
|
const sent = await sendMessageNow(inputEl);
|
|
if (!sent) {
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'Failed to trigger Gemini send.', failed_at: new Date().toISOString() } }, resolve));
|
|
return;
|
|
}
|
|
|
|
const marker = '__PLICE_EOM__';
|
|
const { text: responseText, json: parsedFromWait } = await waitForJsonAndText(marker, 300000);
|
|
if (!responseText) {
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'No Gemini response detected.', failed_at: new Date().toISOString() } }, resolve));
|
|
return;
|
|
}
|
|
|
|
let outputJson = parsedFromWait || parseJsonMaybe(responseText);
|
|
if (!outputJson) outputJson = findJsonWithMarker(responseText, marker);
|
|
|
|
await saveDebug({
|
|
stage: 'captured',
|
|
response_chars: responseText.length,
|
|
has_json: !!outputJson,
|
|
has_marker_text: responseText.includes(marker),
|
|
eom_value: outputJson ? outputJson.eom : null,
|
|
});
|
|
|
|
const result = {
|
|
job_id: job.job_id,
|
|
model: job.model || 'gemini-web',
|
|
release_id: job.release_id || null,
|
|
discogs_url: job.discogs_url || null,
|
|
gemini_url: location.href,
|
|
created_at: job.created_at,
|
|
prompt: job.prompt,
|
|
input_json: job.input_json || null,
|
|
output_text: responseText,
|
|
output_json: outputJson,
|
|
};
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(job.prompt + '\n\n' + responseText);
|
|
} catch {}
|
|
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'posting', posting_at: new Date().toISOString() } }, resolve));
|
|
const postResp = await sendToBackground({ action: 'postGeminiResult', result });
|
|
await saveDebug({ stage: 'posted', postResp });
|
|
|
|
if (!postResp || !postResp.ok) {
|
|
const err = postResp?.error || 'Post failed.';
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: err, failed_at: new Date().toISOString() }, geminiLastExchange: result }, resolve));
|
|
return;
|
|
}
|
|
|
|
await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'done', done_at: new Date().toISOString() }, geminiLastExchange: result }, resolve));
|
|
|
|
const closeOnSuccess = await new Promise(resolve =>
|
|
chrome.storage.local.get(['geminiCloseTabOnSuccess'], r => resolve(r.geminiCloseTabOnSuccess !== false))
|
|
);
|
|
if (closeOnSuccess) {
|
|
await sendToBackground({ action: 'closeSenderTab' });
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
run();
|
|
} else {
|
|
window.addEventListener('DOMContentLoaded', () => run(), { once: true });
|
|
}
|