← back to Draft Viewer
refresh.mjs
74 lines
#!/usr/bin/env node
// refresh.mjs — re-verify the curated draft triage against LIVE George. TK-11231.
//
// Design note: this deliberately does NOT rebuild data/drafts.json from the API. The value of
// this file is the READY-vs-DELETE *classification*, which is human/agent judgement and is not
// derivable from Gmail. The old version fetched a flat list and overwrote the triage with it —
// that would have destroyed the only thing the viewer is for. Instead we keep the triage and
// refresh LIVENESS, so an entry that has been sent or permanently deleted shows as GONE rather
// than the viewer quietly lying about a draft that no longer exists.
//
// Auth: George's basic auth, same credential the george MCP already uses. Resolution order
// mirrors george-mcp/index.js. The previous version grepped GEORGE_BASIC_AUTH_PASS out of
// secrets-manager/.env — that key is not there (verified), which is why it 401'd and the
// viewer froze on a 2026-09-04 snapshot.
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
const OUT = new URL('./data/drafts.json', import.meta.url).pathname;
const BASE = process.env.GEORGE_URL || 'http://127.0.0.1:9850';
function resolveAuth() {
if (process.env.GEORGE_BASIC_AUTH) return process.env.GEORGE_BASIC_AUTH;
try {
const j = JSON.parse(readFileSync(`${process.env.HOME}/.claude.json`, 'utf8'));
const v = j?.mcpServers?.george?.env?.GEORGE_BASIC_AUTH;
if (v) return v;
} catch { /* fall through */ }
try {
const pw = execSync('security find-generic-password -s dw-agents -a admin -w', { encoding: 'utf8' }).trim();
if (pw) return Buffer.from(`admin:${pw}`).toString('base64');
} catch { /* fall through */ }
return null;
}
const AUTH = resolveAuth();
if (!AUTH) { console.error('no George auth resolvable — snapshot retained, viewer unchanged'); process.exit(1); }
async function liveMessageIds(account) {
const url = `${BASE}/api/drafts?maxResults=500${account === 'info' ? '&account=info' : ''}`;
const r = await fetch(url, { headers: { Authorization: `Basic ${AUTH}` } });
if (!r.ok) throw new Error(`HTTP ${r.status} on ${account}`);
const arr = await r.json();
return new Set((arr || []).map((d) => d?.message?.id).filter(Boolean));
}
if (!existsSync(OUT)) { console.error(`no triage file at ${OUT} — nothing to verify`); process.exit(1); }
const snap = JSON.parse(readFileSync(OUT, 'utf8'));
let live;
try {
const [so, info] = await Promise.all([liveMessageIds('steve-office'), liveMessageIds('info')]);
live = { 'steve-office': so, info };
} catch (e) {
// Never blank the viewer on a fetch failure, and never silently mark everything GONE —
// a false "all clear" is worse than a stale board.
console.error(`live check failed (${e.message}) — snapshot retained, liveness NOT updated`);
process.exit(1);
}
let checked = 0, gone = 0;
for (const cls of ['ready', 'delete']) {
for (const item of snap[cls] || []) {
const set = live[item.account];
if (!set) continue; // unknown account: leave untouched rather than guess
item.live = set.has(item.id);
checked++;
if (!item.live) gone++;
}
}
snap.verified_at = new Date().toISOString();
snap.liveness_note = `${checked} entries re-checked against live George; ${gone} no longer exist (sent or permanently deleted).`;
writeFileSync(OUT, JSON.stringify(snap, null, 2));
console.log(`verified ${checked} drafts — ${checked - gone} still live, ${gone} gone`);