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