← back to AbramsOS

scripts/mode-claims-watch.js

66 lines

#!/usr/bin/env node
// "As soon as email lands, run it" — the Mode claims watcher (autonomous, corpus-path).
//
// Runs via launchd every 15 min. Each pass:
//   1. BEST-EFFORT refresh the local Mode corpus from Gmail via the George bridge
//      (lib/mode-corpus). Non-breaking: if George is down / the steve-personal token is
//      invalid_grant / the bridge auth is stale, it logs the reason and continues.
//   2. ALWAYS run the corpus ingester (idempotent UNIQUE(user_id,slug) upsert) so new mailers
//      that landed in the corpus become settlement_claim rows. No OAuth connector required.
//   3. Stage an openclaw "fill (not submit)" brief for any settlement Steve has already marked
//      ELIGIBLE that isn't yet queued. NEVER submits.
//
// This replaces the old hard dependency on a connected Gmail OAuth *connector* row (which was
// never provisioned — connector_account has 0 rows), the reason the watch used to SKIP silently.
//
// Usage: node scripts/mode-claims-watch.js
// Load .env so the launchd job (which injects only PG_*) also gets GEORGE_URL /
// GEORGE_BASIC_AUTH for the corpus refresh. dotenv never overrides vars the plist already set.
try { require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); } catch (_) {}
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

const LOG = path.join(__dirname, '..', 'logs', 'mode-claims-watch.log');
const log = (m) => { const line = `${new Date().toISOString()} ${m}\n`; try { fs.appendFileSync(LOG, line); } catch (_) {} process.stdout.write(line); };

async function main() {
  const USER = process.env.ABRAMSOS_USER_ID || 'user_steve';

  // 1. Best-effort corpus refresh from George (never throws).
  try {
    const { refreshFromGeorge } = require('../lib/mode-corpus');
    const r = await refreshFromGeorge({ days: 20 });
    log(`corpus refresh: ${r.ok ? 'ok' : 'skip'} — ${r.reason}` + (r.added ? ` (+${r.added})` : ''));
    if (r.invalidGrant) log(`ACTION NEEDED (gated): George account 'steve-personal' token is invalid_grant — new Mode mailers cannot flow until Steve reconnects it.`);
  } catch (e) { log('corpus refresh error (ignored): ' + e.message); }

  // 2. Always ingest the corpus (idempotent).
  try {
    execFileSync(process.execPath, [path.join(__dirname, 'ingest-mode-claims-corpus.js')], { stdio: 'inherit' });
  } catch (e) { log('ingest error: ' + e.message); }

  // 3. Stage fills for eligible-but-unqueued claims (Steve flips eligibility in the dashboard).
  let db, filler;
  try { db = require('../lib/db'); filler = require('../lib/openclaw-claim-filler'); }
  catch (e) { log('SKIP stage (module load): ' + e.message); process.exit(0); return; }
  try {
    const elig = await db.query(
      `SELECT * FROM settlement_claim WHERE user_id=$1 AND eligibility_state='eligible'
         AND fill_state='none' AND (deadline IS NULL OR deadline>=current_date)`, [USER]);
    // person has no dedicated address column — read address out of metadata_jsonb (null-safe).
    const prof = await db.query(
      `SELECT full_name, email, phone, metadata_jsonb->>'address' AS address
         FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`, [USER])
      .then(r => r.rows[0] || {}).catch(() => ({}));
    for (const c of elig.rows) {
      filler.stage(c, prof);
      await db.query(`UPDATE settlement_claim SET fill_state='queued',updated_at=now() WHERE id=$1`, [c.id]);
      log(`STAGED openclaw fill brief (fill-not-submit) for: ${c.name}`);
    }
    if (!elig.rows.length) log('no eligible-unqueued claims to stage.');
  } catch (e) { log('stage error: ' + e.message); }
  process.exit(0);
}
main().catch(e => { log('FATAL ' + e.message); process.exit(1); });