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