← back to Ai Workforce

lib/collect.js

194 lines

'use strict';
// AI Workforce — live snapshot collector. All sources are LOCAL + READ-ONLY.
// Each source is wrapped so a single failure degrades gracefully (never blanks
// the whole snapshot). Nothing here writes, deploys, or touches the network
// except a localhost GET to CNCP.
const { execSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');

const HOME = os.homedir();
const sh = (cmd, ms = 6000) => execSync(cmd, { timeout: ms, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
const safe = (fn, fallback) => { try { return fn(); } catch { return fallback; } };
const todayISO = () => new Date().toISOString().slice(0, 10);

// ---- Machines (offices) ---------------------------------------------------
function machines(pm2Count) {
  const list = [];
  // Mac2 — the local box, fully probeable.
  const cores = safe(() => parseInt(sh('sysctl -n hw.ncpu').trim(), 10), null);
  const memBytes = safe(() => parseInt(sh('sysctl -n hw.memsize').trim(), 10), null);
  const load = safe(() => {
    const m = sh('sysctl -n vm.loadavg').match(/[\d.]+/g);
    return m ? { one: +m[0], five: +m[1], fifteen: +m[2] } : null;
  }, null);
  list.push({
    id: 'mac2', name: 'Mac2', label: 'Primary workstation', probed: true,
    cores, memGB: memBytes ? Math.round(memBytes / 1e9) : null, load,
    loadPct: load && cores ? Math.min(100, Math.round((load.one / cores) * 100)) : null,
    working: pm2Count.working, staff: pm2Count.total,
  });
  // Mac1 / Kamatera — known offices, but not locally probeable from here.
  // Declared honestly rather than faked.
  list.push({ id: 'mac1', name: 'Mac1', label: 'Secondary workstation', probed: false, note: 'remote — live probe not wired in this prototype' });
  list.push({ id: 'kamatera', name: 'Kamatera', label: 'Production host (45.61.58.125)', probed: false, note: 'remote — live probe not wired in this prototype' });
  return list;
}

// ---- pm2 services (live employees) ----------------------------------------
function pm2Employees() {
  const raw = safe(() => sh('pm2 jlist', 8000), '[]');
  let arr = [];
  try { arr = JSON.parse(raw); } catch { arr = []; }
  return arr.map((p) => {
    const env = p.pm2_env || {};
    const status = env.status === 'online' ? 'working' : 'stopped';
    return {
      id: 'pm2:' + p.name,
      name: p.name,
      dept: 'Service',
      role: 'Live process (pm2)',
      machine: 'Mac2',
      status,
      metrics: {
        cpu: p.monit ? p.monit.cpu : null,
        memMB: p.monit && p.monit.memory ? Math.round(p.monit.memory / 1e6) : null,
        restarts: env.restart_time,
        uptimeMs: env.pm_uptime ? Date.now() - env.pm_uptime : null,
      },
      hired: env.created_at || env.pm_uptime || null,
      lastActive: env.pm_uptime || null,
    };
  });
}

// ---- launchd com.steve.* jobs (shift workers) -----------------------------
function launchdEmployees() {
  const raw = safe(() => sh("launchctl list | grep com.steve", 6000), '');
  const rows = raw.split('\n').map((l) => l.trim()).filter(Boolean);
  const agentsDir = path.join(HOME, 'Library', 'LaunchAgents');
  return rows.map((line) => {
    // format: <PID|-> \t <last exit status> \t <label>
    const parts = line.split(/\s+/);
    const pid = parts[0];
    const exit = parseInt(parts[1], 10);
    const label = parts.slice(2).join(' ');
    let status;
    if (pid && pid !== '-') status = 'working';       // currently running
    else if (exit === 0) status = 'idle';             // scheduled, last run clean
    else status = 'error';                            // last run failed
    const short = label.replace(/^com\.steve\./, '');
    const plist = path.join(agentsDir, label + '.plist');
    const hired = safe(() => fs.statSync(plist).mtimeMs, null);
    return {
      id: 'launchd:' + label,
      name: short,
      dept: 'Scheduled',
      role: 'launchd job',
      machine: 'Mac2',
      status,
      metrics: { pid: pid !== '-' ? pid : null, lastExit: isNaN(exit) ? null : exit },
      hired,
      lastActive: hired,
    };
  });
}

// ---- cabinet.yaml (management + named agents) -----------------------------
function cabinetEmployees() {
  const file = path.join(HOME, 'Projects', 'agent-cabinet', 'cabinet.yaml');
  const txt = safe(() => fs.readFileSync(file, 'utf8'), '');
  if (!txt) return [];
  const hired = safe(() => fs.statSync(file).mtimeMs, null);
  const lines = txt.split('\n');
  const out = [];
  for (let i = 0; i < lines.length; i++) {
    const vp = lines[i].match(/^\s*-\s*vp:\s*(\S+)/);
    if (vp) {
      let domain = '';
      for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
        const d = lines[j].match(/^\s*domain:\s*(.+)/);
        if (d) { domain = d[1].trim(); break; }
      }
      out.push({ id: 'cabinet:' + vp[1], name: vp[1], dept: 'Officer', role: domain || 'VP', machine: '—', status: 'roster', metrics: {}, hired, lastActive: hired });
      continue;
    }
    const sub = lines[i].match(/^\s*-\s*subagent:\s*(\S+)/);
    if (sub) {
      out.push({ id: 'cabinet:' + sub[1], name: sub[1], dept: 'Agent', role: 'Specialist subagent', machine: '—', status: 'roster', metrics: {}, hired, lastActive: hired });
    }
  }
  // de-dup names (a subagent can appear under several VPs)
  const seen = new Set();
  return out.filter((e) => (seen.has(e.id) ? false : (seen.add(e.id), true)));
}

// ---- CNCP wins (today's output) -------------------------------------------
function winsToday() {
  return new Promise((resolve) => {
    const req = http.get('http://127.0.0.1:3333/api/wins', { timeout: 3500 }, (res) => {
      let body = '';
      res.on('data', (c) => (body += c));
      res.on('end', () => {
        let arr = [];
        try { arr = JSON.parse(body); } catch { arr = []; }
        const t = todayISO();
        const today = (Array.isArray(arr) ? arr : []).filter((w) => (w.date || '').slice(0, 10) === t);
        resolve({ today, total: Array.isArray(arr) ? arr.length : 0 });
      });
    });
    req.on('error', () => resolve({ today: [], total: 0, unavailable: true }));
    req.on('timeout', () => { req.destroy(); resolve({ today: [], total: 0, unavailable: true }); });
  });
}

// ---- cost ledger (payroll / spend today) ----------------------------------
function costToday() {
  const file = path.join(HOME, '.claude', 'cost-ledger.jsonl');
  const txt = safe(() => fs.readFileSync(file, 'utf8'), '');
  if (!txt) return { totalUSD: 0, byApp: [], calls: 0 };
  const t = todayISO();
  const byApp = {};
  let total = 0, calls = 0;
  for (const line of txt.split('\n')) {
    if (!line.includes('"ts":"' + t)) continue;
    let row; try { row = JSON.parse(line); } catch { continue; }
    const c = +row.cost_usd || 0;
    total += c; calls++;
    const key = row.app || row.api || 'other';
    byApp[key] = (byApp[key] || 0) + c;
  }
  const list = Object.entries(byApp).map(([app, usd]) => ({ app, usd })).sort((a, b) => b.usd - a.usd).slice(0, 8);
  return { totalUSD: +total.toFixed(4), byApp: list, calls };
}

async function snapshot() {
  const pm2 = pm2Employees();
  const launchd = launchdEmployees();
  const cabinet = cabinetEmployees();
  const pm2Count = { total: pm2.length, working: pm2.filter((e) => e.status === 'working').length };
  const employees = [...pm2, ...launchd, ...cabinet];
  const byStatus = employees.reduce((a, e) => ((a[e.status] = (a[e.status] || 0) + 1), a), {});
  const wins = await winsToday();
  return {
    generatedAt: Date.now(),
    machines: machines(pm2Count),
    employees,
    counts: {
      total: employees.length,
      working: byStatus.working || 0,
      idle: byStatus.idle || 0,
      error: byStatus.error || 0,
      stopped: byStatus.stopped || 0,
      roster: byStatus.roster || 0,
      byDept: employees.reduce((a, e) => ((a[e.dept] = (a[e.dept] || 0) + 1), a), {}),
    },
    wins,
    cost: costToday(),
  };
}

module.exports = { snapshot };