pliceclogs-og/gemini.js
type-two e5b8504bc6 Import pliceclogs as-is — the original Discogs seller extension
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>
2026-07-24 13:47:09 +10:00

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 });
}