← back to George Gmail
bulk-label-sample-followup-drafts.js
143 lines
#!/usr/bin/env node
/*
* bulk-label-sample-followup-drafts.js — apply a Gmail label to all info@ drafts
* whose subject starts with "Sample Follow-Up".
*
* Context: TK-10744 — 3,685 un-labeled drafts in info@designerwallcoverings.com
* generated by the sample-followup agent with no per-source label. This script
* applies a label so agents can filter their own drafts cleanly.
*
* FLOW:
* 1. GET /api/labels?account=info — discover existing labels
* 2. If LABEL_NAME doesn't exist, create it via the Gmail API (via George health call
* or direct Gmail call — George doesn't expose a label-create endpoint yet, so we
* call Gmail directly using George's stored OAuth token, OR you create the label
* manually in Gmail and pass its ID as LABEL_ID env var).
* 3. POST /api/messages/bulk-label?account=info — search + apply (dry run first).
* 4. DRY_RUN=0 to actually apply.
*
* USAGE:
* node bulk-label-sample-followup-drafts.js # dry run — shows count
* DRY_RUN=0 node bulk-label-sample-followup-drafts.js # live — applies labels
* LABEL_ID=Label_XXXXX DRY_RUN=0 node ... # skip discovery, use known ID
*
* NOTE: George must be running locally (default port 9850).
* The script targets the 'info' account (info@designerwallcoverings.com).
*/
'use strict';
const fs = require('fs');
const path = require('path');
const BASE = process.env.GEORGE_BASE || 'http://127.0.0.1:9850';
const ACC = process.env.ACCOUNT || 'info';
const LABEL_NAME = process.env.LABEL_NAME || 'sample-followup';
const LABEL_ID = process.env.LABEL_ID || ''; // skip discovery if known
const DRY_RUN = process.env.DRY_RUN !== '0'; // default dry run
const QUERY = `in:drafts subject:"Sample Follow-Up"`;
function resolveAuth() {
let u = 'admin', p = '';
const envPath = path.join(process.env.HOME || '', 'Projects/Designer-Wallcoverings/DW-MCP/.env');
try {
const t = fs.readFileSync(envPath, 'utf8');
const m = t.match(/^GEORGE_BASIC_AUTH[^_]*=(.+)$/m);
if (m) {
const v = m[1].trim();
if (v.includes(':')) { const s = v.split(':'); u = s[0]; p = s.slice(1).join(':'); }
}
if (!p) {
const mp = t.match(/^GEORGE_BASIC_AUTH_PASS=(.+)$/m);
if (mp) p = mp[1].trim();
}
} catch (_) { /* fall back */ }
return { u, p };
}
const { u, p } = resolveAuth();
const AUTH = 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
async function jget(url) {
const r = await fetch(url, { headers: H });
if (!r.ok) throw new Error(`GET ${url} → ${r.status} ${await r.text()}`);
return r.json();
}
async function jpost(url, body) {
const r = await fetch(url, { method: 'POST', headers: H, body: JSON.stringify(body) });
if (!r.ok) throw new Error(`POST ${url} → ${r.status} ${await r.text()}`);
return r.json();
}
async function main() {
console.log(`[bulk-label] George: ${BASE} | Account: ${ACC} | DRY_RUN: ${DRY_RUN}`);
console.log(`[bulk-label] Query: ${QUERY}`);
console.log(`[bulk-label] Label: ${LABEL_ID ? LABEL_ID : `"${LABEL_NAME}" (will discover)`}`);
console.log('');
// Step 1: Discover or confirm label ID
let labelId = LABEL_ID;
if (!labelId) {
console.log('[bulk-label] Fetching label list...');
const labels = await jget(`${BASE}/api/labels?account=${ACC}`);
const found = labels.find((l) => l.name.toLowerCase() === LABEL_NAME.toLowerCase());
if (found) {
labelId = found.id;
console.log(`[bulk-label] Found label "${found.name}" → ${labelId}`);
} else {
console.log(`[bulk-label] Label "${LABEL_NAME}" NOT FOUND in Gmail.`);
console.log('[bulk-label] Create the label in Gmail first, then re-run with LABEL_ID=<id>.');
console.log('[bulk-label] Available labels:');
labels.filter((l) => !l.id.startsWith('CATEGORY_')).forEach((l) => console.log(` ${l.id} ${l.name}`));
process.exit(1);
}
}
// Step 2: Dry run to count matching drafts
console.log('\n[bulk-label] Running dry-run search...');
const dryResult = await jpost(`${BASE}/api/messages/bulk-label?account=${ACC}`, {
q: QUERY,
addLabelIds: [labelId],
dryRun: true,
});
console.log(`[bulk-label] Matching drafts: ${dryResult.total}`);
if (dryResult.total > 0) {
console.log(`[bulk-label] Sample IDs: ${dryResult.ids.slice(0, 5).join(', ')}...`);
}
if (DRY_RUN) {
console.log('\n[bulk-label] DRY RUN — no labels applied. Set DRY_RUN=0 to apply.');
return;
}
if (dryResult.total === 0) {
console.log('[bulk-label] No matching drafts found — nothing to label.');
return;
}
// Step 3: Live label application
console.log(`\n[bulk-label] LIVE — applying label "${labelId}" to ${dryResult.total} drafts...`);
const result = await jpost(`${BASE}/api/messages/bulk-label?account=${ACC}`, {
q: QUERY,
addLabelIds: [labelId],
dryRun: false,
});
console.log(`[bulk-label] Done: labeled=${result.labeled} failed=${result.failed} total=${result.total}`);
// Write result heartbeat
const hbPath = path.join(__dirname, 'data', 'bulk-label-sample-followup-latest.json');
fs.mkdirSync(path.dirname(hbPath), { recursive: true });
fs.writeFileSync(hbPath, JSON.stringify({
ts: new Date().toISOString(),
account: ACC,
query: QUERY,
labelId,
labeled: result.labeled,
failed: result.failed,
total: result.total,
}, null, 2));
console.log(`[bulk-label] Heartbeat written: ${hbPath}`);
}
main().catch((e) => { console.error('[bulk-label] FATAL:', e.message); process.exit(1); });