@volter/supercode-health 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # @volter/supercode-health
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The machine-health pack is an optional supercode machine pack. It runs a cheap,
4
+ deterministic probe of a machine's vitals and mails the machine's maintainer when a
5
+ threshold is crossed. It decides nothing and changes nothing on the machine. The
6
+ [ADR](../../docs/adr/0006-machine-health-pack.md) covers what it measures and why:
7
+ prior art, instruments and their cost, each alert window and its basis, and each
8
+ feature's permission barrier.
9
+
10
+ ## Running
11
+
12
+ An enrolled machine's connector (`supercode teams connect`) starts the probe as a child
13
+ when the pack is installed. `SUPERCODE_HEALTH=0` turns it off. The pack runs one probe
14
+ per OS user and holds a pidfile so it stays one. Run it by hand with `supercode health
15
+ run`, or `supercode-health run`.
16
+
17
+ ## Reading
18
+
19
+ ```
20
+ supercode health status [--all] [--json] this machine
21
+ supercode health status --machine NAME an enrolled machine, through its doors
22
+ supercode health features every feature: on/off, barrier, why
23
+ supercode health alarms
24
+ supercode health once two samples 5 s apart, without the loop
25
+ ```
26
+
27
+ The machine daemon's node doors are `health.read` and `health.subscribe` /
28
+ `health.unsubscribe`. A read needs an operator or `view *`; a subscription needs an
29
+ operator. The probe writes `latest.json` (each reading), `history.jsonl` (one summary
30
+ a minute, 24 h) and `probe.log` to `$SUPERCODE_HOME/health`, which defaults to
31
+ `~/.config/supercode/health`. `SUPERCODE_HEALTH_DIR` overrides that directory.
32
+
33
+ ## Alarms
34
+
35
+ `supercode health subscribe sc:<machine>:<harness>:<id>` subscribes a session or board.
36
+ Each raise, escalation and clear mails the subscribers. A still-raised alarm mails again
37
+ every `repeatMin` (60). Mail comes from `sc:<machine>:operator:machine-health` through
38
+ `supercode message remote-deliver`. Undelivered mail is kept and retried for a day.
39
+
40
+ ## Config
41
+
42
+ The config is `config.json` in the health directory. The machine's maintainer owns it
43
+ (RFC 0022: the values are the org's, not the pack's). Any default in
44
+ [`lib/thresholds.mjs`](lib/thresholds.mjs) can be overridden:
45
+
46
+ ```json
47
+ {
48
+ "maintainers": ["sc:my-mac:claude-code:<session-id>"],
49
+ "alarms": { "memory.free": { "warn": 20, "crit": 10 }, "tcp.timeWaitTarget": { "warn": 25 } },
50
+ "volumes": { "/System/Volumes/Data": { "warnGB": 50, "critGB": 20, "clearGB": 100 } },
51
+ "services": [{ "label": "ai.example.worker", "name": "worker", "log": "/path/to/worker.log", "maxSilentMin": 30 }],
52
+ "integrations": { "browserController": { "portFile": "/tmp/controller-port.txt" } },
53
+ "features": { "disk-io": false }
54
+ }
55
+ ```
56
+
57
+ ## Permission barriers
58
+
59
+ | Barrier | Features |
60
+ |---|---|
61
+ | none | load, memory, processes, process-footprint (this user's processes; python3 on macOS), renderers, daemons, disk, disk-io, tcp, files, services |
62
+ | root | all-users-footprint, power |
63
+ | integration | supercode (the machine daemon), browser-tabs (a browser controller's `/tabs`) |
64
+
65
+ With no extra permission, every `none` feature runs. `features` lists every other
66
+ feature as off and names what it needs. The pack never asks for a permission or
67
+ escalates. A root feature turns on only when the probe already runs as root.
package/bin/health.mjs ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // supercode-health: the machine-health pack's own CLI. On a connected machine the connector runs `run`;
3
+ // people and maintainers read it through `supercode health …`, which also reaches other machines.
4
+ //
5
+ // run [--machine NAME] the probe loop (one per OS user; exits 3 when one already runs)
6
+ // once [--json] two samples 5 s apart, judged and printed, without the loop or mail
7
+ // status [--json] [--all] this machine's latest reading (latest.json)
8
+ // features [--json] every feature, on or off, its permission barrier and why
9
+ // alarms [--json] the alarms raised now
10
+ // subscribe <address> | unsubscribe <address> alarm mail for a session or board address
11
+ // config where the config lives and what it currently resolves to
12
+ import { readFileSync } from 'node:fs';
13
+ import { parseArgs } from 'node:util';
14
+ import { Probe, subscribe, loadConfig } from '../lib/probe.mjs';
15
+ import { healthDir, files } from '../lib/paths.mjs';
16
+ import { formatStatus, formatFeatures } from '../lib/format.mjs';
17
+
18
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: { json: { type: 'boolean' }, all: { type: 'boolean' }, machine: { type: 'string' }, help: { type: 'boolean' } } });
19
+ const verb = positionals[0] ?? 'status';
20
+ const dir = healthDir();
21
+ const f = files(dir);
22
+ const print = (value, text) => process.stdout.write((values.json ? JSON.stringify(value, null, 2) : text) + '\n');
23
+ const latest = () => { try { return JSON.parse(readFileSync(f.latest, 'utf8')); } catch { return null; } };
24
+
25
+ if (values.help || verb === 'help') {
26
+ process.stdout.write(readFileSync(new URL(import.meta.url), 'utf8').split('\n').filter((l) => l.startsWith('//')).map((l) => l.slice(3)).join('\n') + '\n');
27
+ } else if (verb === 'run') {
28
+ const probe = new Probe({ machine: values.machine });
29
+ if (!probe.lock()) { process.stderr.write(`supercode-health: a probe already runs here (pid ${probe.holder})\n`); process.exit(3); }
30
+ const abort = new AbortController();
31
+ const stop = () => abort.abort();
32
+ process.on('SIGTERM', stop); process.on('SIGINT', stop);
33
+ process.on('exit', () => probe.unlock());
34
+ await probe.run({ signal: abort.signal });
35
+ probe.unlock();
36
+ } else if (verb === 'once') {
37
+ const probe = new Probe({ machine: values.machine, dir: `${dir}/once-${process.pid}` });
38
+ await probe.init();
39
+ await probe.sampler.sample(probe.features(), Date.now(), { wait: true });
40
+ await new Promise((r) => setTimeout(r, 5_000));
41
+ const now = Date.now();
42
+ const { readings, observations, families, errors } = await probe.sampler.sample(probe.features(), now, { wait: true });
43
+ const { judge, raised } = await import('../lib/thresholds.mjs');
44
+ // A single look has no windows behind it: report what each threshold meets right now.
45
+ for (const o of observations) o.def = { ...o.def, forSec: 0, critForSec: 0 };
46
+ judge(probe.state, observations, families, now);
47
+ const snapshot = { version: 1, machine: probe.machine, platform: probe.platform, at: new Date(now).toISOString(), probe: { pid: process.pid, intervalSec: 15, once: true }, features: probe.features().map((x) => ({ ...x, ...(errors[x.id] ? { error: errors[x.id] } : {}) })), alarms: raised(probe.state), readings };
48
+ (await import('node:fs')).rmSync(probe.dir, { recursive: true, force: true });
49
+ print(snapshot, formatStatus(snapshot, { all: values.all }));
50
+ } else if (verb === 'status') {
51
+ const s = latest();
52
+ print(s, formatStatus(s, { all: values.all }));
53
+ if (!s) process.exitCode = 4;
54
+ } else if (verb === 'features') {
55
+ const s = latest();
56
+ let features = s?.features;
57
+ if (!features) { const probe = new Probe({ dir: `${dir}/features-${process.pid}` }); await probe.init(); features = probe.features(); (await import('node:fs')).rmSync(probe.dir, { recursive: true, force: true }); }
58
+ print(features, formatFeatures(features));
59
+ } else if (verb === 'alarms') {
60
+ const s = latest();
61
+ print(s?.alarms ?? [], s?.alarms?.length ? s.alarms.map((a) => `${a.level.toUpperCase()} ${a.key}: ${a.summary} (since ${a.since})`).join('\n') : 'no alarms raised');
62
+ } else if (verb === 'subscribe' || verb === 'unsubscribe') {
63
+ print(subscribe(dir, positionals[1], verb === 'unsubscribe'), `${verb}d ${positionals[1]}`);
64
+ } else if (verb === 'config') {
65
+ const config = loadConfig(f.config);
66
+ print({ file: f.config, config }, `config file: ${f.config}${config.source ? '' : ' (absent: defaults)'}\n${JSON.stringify(config, null, 2)}`);
67
+ } else {
68
+ process.stderr.write(`supercode-health: unknown verb ${verb} (run, once, status, features, alarms, subscribe, unsubscribe, config)\n`);
69
+ process.exit(2);
70
+ }
package/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ export { Probe, loadConfig, subscribe, subscribers, platformReader } from './lib/probe.mjs';
2
+ export { Sampler } from './lib/sample.mjs';
3
+ export { FEATURES, resolveFeatures } from './lib/features.mjs';
4
+ export { DEFAULTS, judge, meets, raised } from './lib/thresholds.mjs';
5
+ export { healthDir, machineName } from './lib/paths.mjs';
6
+ export { formatStatus, formatFeatures } from './lib/format.mjs';
package/lib/chrome.mjs ADDED
@@ -0,0 +1,68 @@
1
+ // Which tab a Chrome renderer serves, and who owns each tab. A renderer serving an origin that uses the
2
+ // origin-private file system holds that origin's bucket open (`<profile>/File System/<nnn>/`), and each profile's
3
+ // `File System/Origins` index names the origin of every bucket. A tab on a local port is owned by whatever
4
+ // process serves that port (an editor, a dev server), and so by that process's session.
5
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { run } from './run.mjs';
9
+
10
+ const ROOTS = { darwin: join(homedir(), 'Library/Application Support/Google/Chrome'), linux: join(homedir(), '.config/google-chrome') };
11
+ let cache = { key: '', map: new Map() };
12
+
13
+ /** `<profile>/File System/<nnn>` → origin, from every profile's Origins index (re-read when an index changes). */
14
+ export function bucketOrigins(platform = process.platform) {
15
+ const root = ROOTS[platform];
16
+ if (!root) return new Map();
17
+ const files = [];
18
+ let profiles = [];
19
+ try { profiles = readdirSync(root).filter((name) => name === 'Default' || name.startsWith('Profile ')); } catch { return new Map(); }
20
+ for (const profile of profiles) {
21
+ const dir = join(root, profile, 'File System', 'Origins');
22
+ let names = [];
23
+ try { names = readdirSync(dir).filter((name) => /\.(log|ldb)$/.test(name)); } catch { continue; }
24
+ for (const name of names) { try { files.push([profile, join(dir, name), statSync(join(dir, name)).mtimeMs]); } catch { /* rotated */ } }
25
+ }
26
+ const key = files.map(([, file, mtime]) => `${file}:${mtime}`).join('|');
27
+ if (key === cache.key) return cache.map;
28
+ const map = new Map();
29
+ for (const [profile, file] of files) {
30
+ const text = readFileSync(file).toString('latin1');
31
+ for (const m of text.matchAll(/ORIGIN:(https?)_([a-z0-9.\-[\]:]+?)_(\d+)[\x00-\x1f]{1,3}(\d{3})/g)) {
32
+ const port = m[3] === '0' ? '' : `:${m[3]}`;
33
+ map.set(`${profile}/File System/${m[4]}`, `${m[1]}://${m[2]}${port}`);
34
+ }
35
+ }
36
+ cache = { key, map };
37
+ return map;
38
+ }
39
+
40
+ /** pid → origins whose storage buckets the renderer holds open (one lsof for every pid asked). */
41
+ export async function rendererOrigins(pids, platform = process.platform) {
42
+ const out = new Map();
43
+ if (!pids.length) return out;
44
+ const buckets = bucketOrigins(platform);
45
+ const r = await run(platform === 'darwin' ? '/usr/sbin/lsof' : 'lsof', ['-nP', '-Fpn', '-p', pids.join(',')], { timeoutMs: 15_000 });
46
+ let pid = null;
47
+ for (const line of r.stdout.split('\n')) {
48
+ if (line[0] === 'p') pid = Number(line.slice(1));
49
+ else if (line[0] === 'n' && pid) {
50
+ const m = /\/((?:Default|Profile \d+)\/File System\/\d{3})\//.exec(line);
51
+ const origin = m && buckets.get(m[1]);
52
+ if (origin) { if (!out.has(pid)) out.set(pid, new Set()); out.get(pid).add(origin); }
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ export function originOf(url) {
59
+ try { const u = new URL(url); return u.protocol.startsWith('http') ? u.origin : null; } catch { return null; }
60
+ }
61
+
62
+ export function localPort(url) {
63
+ try {
64
+ const u = new URL(url);
65
+ if (!['localhost', '127.0.0.1', '[::1]'].includes(u.hostname)) return null;
66
+ return Number(u.port || (u.protocol === 'https:' ? 443 : 80));
67
+ } catch { return null; }
68
+ }
package/lib/darwin.mjs ADDED
@@ -0,0 +1,171 @@
1
+ // macOS readers. Every one runs unprivileged; each returns raw cumulative values and leaves rates to derive.mjs.
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { run, clockSeconds, topBytes } from './run.mjs';
5
+
6
+ const RUSAGE = fileURLToPath(new URL('./rusage.py', import.meta.url));
7
+ const PRESSURE = { 1: 'normal', 2: 'warn', 4: 'critical' }; // dispatch/source.h DISPATCH_MEMORYPRESSURE_*
8
+
9
+ async function sysctl(...names) {
10
+ const r = await run('/usr/sbin/sysctl', ['-n', ...names], { timeoutMs: 5000 });
11
+ if (!r.ok) throw new Error(r.error);
12
+ return r.stdout.split('\n');
13
+ }
14
+
15
+ export async function memory() {
16
+ const [[level, memsize, swap], vm, pressure] = await Promise.all([
17
+ sysctl('kern.memorystatus_vm_pressure_level', 'hw.memsize', 'vm.swapusage'),
18
+ run('/usr/bin/vm_stat', [], { timeoutMs: 5000 }),
19
+ run('/usr/bin/memory_pressure', ['-Q'], { timeoutMs: 5000 }),
20
+ ]);
21
+ if (!vm.ok) throw new Error(vm.error);
22
+ const pageSize = Number(/page size of (\d+)/.exec(vm.stdout)?.[1] ?? 16384);
23
+ const counter = (name) => Number(new RegExp(`^"?${name}"?:\\s+(\\d+)`, 'm').exec(vm.stdout)?.[1] ?? NaN);
24
+ const mb = (key) => Number(new RegExp(`${key} = ([\\d.]+)M`).exec(swap)?.[1] ?? NaN) * 1024 * 1024;
25
+ return {
26
+ pressureLevel: PRESSURE[Number(level)] ?? null,
27
+ totalBytes: Number(memsize),
28
+ freePct: pressure.ok ? Number(/free percentage: (\d+)%/.exec(pressure.stdout)?.[1] ?? NaN) : null,
29
+ swapUsedBytes: mb('used'), swapTotalBytes: mb('total'),
30
+ counters: {
31
+ swapouts: counter('Swapouts'), swapins: counter('Swapins'), pageouts: counter('Pageouts'), pageins: counter('Pageins'),
32
+ compressions: counter('Compressions'), decompressions: counter('Decompressions'),
33
+ },
34
+ pageSize,
35
+ };
36
+ }
37
+
38
+ /** Every process: pid, parent, owner, cumulative CPU seconds, RSS, command line. */
39
+ export async function processes() {
40
+ const r = await run('/bin/ps', ['-axww', '-o', 'pid=,ppid=,uid=,time=,rss=,command='], { timeoutMs: 10_000 });
41
+ if (!r.ok) throw new Error(r.error);
42
+ const rows = [];
43
+ for (const line of r.stdout.split('\n')) {
44
+ const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(.*)$/.exec(line);
45
+ if (m) rows.push({ pid: +m[1], ppid: +m[2], uid: +m[3], cpuSec: clockSeconds(m[4]), rssBytes: +m[5] * 1024, command: m[6] });
46
+ }
47
+ return rows;
48
+ }
49
+
50
+ let python;
51
+ /** python3 that will not raise the Command Line Tools install dialog (/usr/bin/python3 is a stub without them). */
52
+ async function pythonPath() {
53
+ if (python !== undefined) return python;
54
+ python = null;
55
+ for (const candidate of ['/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/Library/Frameworks/Python.framework/Versions/Current/bin/python3']) {
56
+ if (existsSync(candidate)) { python = candidate; return python; }
57
+ }
58
+ if (existsSync('/usr/bin/python3') && (await run('/usr/bin/xcode-select', ['-p'], { timeoutMs: 3000 })).ok) python = '/usr/bin/python3';
59
+ return python;
60
+ }
61
+
62
+ /** Footprint and disk I/O per readable process (this user's; every user's as root). */
63
+ export async function rusage() {
64
+ const py = await pythonPath();
65
+ if (!py) throw Object.assign(new Error('python3 not found (needed for libproc reads)'), { barrier: 'dependency' });
66
+ const r = await run(py, [RUSAGE], { timeoutMs: 10_000 });
67
+ if (!r.ok) throw new Error(r.error);
68
+ const map = new Map();
69
+ for (const line of r.stdout.split('\n')) {
70
+ const [pid, phys, read, written] = line.split(' ').map(Number);
71
+ if (pid) map.set(pid, { footprintBytes: phys, diskReadBytes: read, diskWrittenBytes: written });
72
+ }
73
+ return map;
74
+ }
75
+ export async function rusageAvailable() { return Boolean(await pythonPath()); }
76
+
77
+ /** The largest processes of every user by footprint (top's MEM is phys_footprint). About 2 s; run rarely. */
78
+ export async function topFootprints(n = 25) {
79
+ const r = await run('/usr/bin/top', ['-l', '1', '-o', 'mem', '-n', String(n), '-stats', 'pid,mem'], { timeoutMs: 15_000 });
80
+ if (!r.ok) throw new Error(r.error);
81
+ const map = new Map();
82
+ for (const line of r.stdout.split('\n')) {
83
+ const m = /^\s*(\d+)\s+(\S+)\s*$/.exec(line);
84
+ if (m) { const bytes = topBytes(m[2]); if (bytes != null) map.set(+m[1], bytes); }
85
+ }
86
+ return map;
87
+ }
88
+
89
+ function splitAddress(text) {
90
+ const at = text.lastIndexOf('.');
91
+ return { addr: text.slice(0, at), port: Number(text.slice(at + 1)) || 0 };
92
+ }
93
+
94
+ /** Every TCP socket as [local, foreign, state]. netstat reads the kernel's whole table unprivileged. */
95
+ export async function tcp() {
96
+ const r = await run('/usr/sbin/netstat', ['-an', '-p', 'tcp'], { timeoutMs: 15_000 });
97
+ if (!r.ok) throw new Error(r.error);
98
+ const sockets = [];
99
+ for (const line of r.stdout.split('\n')) {
100
+ const f = line.trim().split(/\s+/);
101
+ if (f.length < 6 || !f[0].startsWith('tcp')) continue;
102
+ sockets.push({ local: splitAddress(f[3]), foreign: splitAddress(f[4]), state: f[5] });
103
+ }
104
+ const [first, last] = (await sysctl('net.inet.ip.portrange.first', 'net.inet.ip.portrange.last')).map(Number);
105
+ const [msl] = await sysctl('net.inet.tcp.msl');
106
+ return { sockets, range: { first, last }, timeWaitSec: (Number(msl) * 2) / 1000 };
107
+ }
108
+
109
+ /** Which pid listens on each port (this user's listeners; every user's as root). */
110
+ export async function listeners() {
111
+ const r = await run('/usr/sbin/lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fpcn'], { timeoutMs: 15_000 });
112
+ const map = new Map();
113
+ let pid = null, command = null;
114
+ for (const line of r.stdout.split('\n')) {
115
+ if (line[0] === 'p') pid = Number(line.slice(1));
116
+ else if (line[0] === 'c') command = line.slice(1);
117
+ else if (line[0] === 'n') { const port = Number(line.slice(line.lastIndexOf(':') + 1)); if (port && !map.has(port)) map.set(port, { pid, command }); }
118
+ }
119
+ return map;
120
+ }
121
+
122
+ export async function files() {
123
+ const [open, max] = (await sysctl('kern.num_files', 'kern.maxfiles')).map(Number);
124
+ return { open, max };
125
+ }
126
+
127
+ const SKIP_MOUNT = /^\/(System\/Volumes\/(?!Data$)|Library\/Developer\/CoreSimulator\/|dev$|private\/var\/vm$)/;
128
+ export async function volumes() {
129
+ const r = await run('/bin/df', ['-k', '-P', '-l'], { timeoutMs: 10_000 });
130
+ if (!r.ok && !r.stdout) throw new Error(r.error);
131
+ const rows = [];
132
+ for (const line of r.stdout.split('\n').slice(1)) {
133
+ const m = /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\S+\s+(\/.*)$/.exec(line);
134
+ if (!m || !m[1].startsWith('/dev/') || SKIP_MOUNT.test(m[5])) continue;
135
+ rows.push({ mount: m[5], device: m[1], totalBytes: +m[2] * 1024, freeBytes: +m[4] * 1024 });
136
+ }
137
+ // APFS volumes in one container share its free space: report the container once, by its largest volume.
138
+ const hasData = rows.some((row) => row.mount === '/System/Volumes/Data');
139
+ const kept = [];
140
+ for (const row of rows.sort((a, b) => b.totalBytes - a.totalBytes)) {
141
+ if (row.mount === '/' && hasData) continue;
142
+ if (kept.some((k) => k.freeBytes === row.freeBytes && k.device.replace(/s\d+(s\d+)?$/, '') === row.device.replace(/s\d+(s\d+)?$/, ''))) continue;
143
+ kept.push(row);
144
+ }
145
+ return kept;
146
+ }
147
+
148
+ /** Cumulative MB moved per disk since boot (`iostat -I`); the delta is device throughput. */
149
+ export async function diskIO() {
150
+ const r = await run('/usr/sbin/iostat', ['-Id', '-c', '1'], { timeoutMs: 5000 });
151
+ if (!r.ok) throw new Error(r.error);
152
+ const lines = r.stdout.split('\n').filter(Boolean);
153
+ const names = lines[0].trim().split(/\s+/);
154
+ const values = lines[2].trim().split(/\s+/).map(Number);
155
+ return Object.fromEntries(names.map((name, i) => [name, { bytes: values[i * 3 + 2] * 1024 * 1024 }]));
156
+ }
157
+
158
+ /** The user's launchd jobs: label → { pid, lastExit }. */
159
+ export async function services() {
160
+ const r = await run('/bin/launchctl', ['list'], { timeoutMs: 5000 });
161
+ if (!r.ok) throw new Error(r.error);
162
+ const map = new Map();
163
+ for (const line of r.stdout.split('\n').slice(1)) {
164
+ const [pid, status, label] = line.split('\t');
165
+ if (label) map.set(label, { pid: pid === '-' ? null : Number(pid), lastExit: Number(status) });
166
+ }
167
+ return map;
168
+ }
169
+
170
+ export const isRoot = () => process.getuid?.() === 0;
171
+ export const powermetricsAvailable = () => existsSync('/usr/bin/powermetrics');
@@ -0,0 +1,36 @@
1
+ // The features, each behind its own barrier. `none` runs for any user; `root`, `full-disk-access` and
2
+ // `accessibility` are OS grants the pack never asks for; `integration:*` needs another program to answer.
3
+ // A feature is on when its platform has it, its barrier is met, and config does not turn it off.
4
+ export const FEATURES = [
5
+ { id: 'load', barrier: 'none', platforms: ['darwin', 'linux', 'win32'], reads: 'load average per core' },
6
+ { id: 'memory', barrier: 'none', platforms: ['darwin', 'linux', 'win32'], reads: 'pressure level, free %, swapouts/s, compressions/s, swap used' },
7
+ { id: 'processes', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'every process: CPU % from CPU-time deltas, memory, growth, owning session' },
8
+ { id: 'process-footprint', barrier: 'none', platforms: ['darwin', 'linux'], reads: "this user's processes: phys_footprint and disk writes (libproc / /proc/<pid>/io)" },
9
+ { id: 'all-users-footprint', barrier: 'root', platforms: ['darwin', 'linux'], reads: "every user's processes: footprint and disk writes" },
10
+ { id: 'renderers', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'browser renderers: hot, runaway; orphaned headless browsers' },
11
+ { id: 'daemons', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'system daemons: CPU, and footprint of the largest' },
12
+ { id: 'disk', barrier: 'none', platforms: ['darwin', 'linux', 'win32'], reads: 'free space per volume, fill rate, time to full' },
13
+ { id: 'disk-io', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'device throughput; top writers per process' },
14
+ { id: 'tcp', barrier: 'none', platforms: ['darwin', 'linux', 'win32'], reads: 'sockets per state, ephemeral ports in use, TIME_WAIT by listener and its pid' },
15
+ { id: 'files', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'open files against the system limit' },
16
+ { id: 'services', barrier: 'none', platforms: ['darwin', 'linux'], reads: "configured launchd/systemd user jobs: running, last exit, log silence" },
17
+ { id: 'supercode', barrier: 'integration:machine-daemon', platforms: ['darwin', 'linux', 'win32'], reads: "this machine's supercode connector: answering, server link, stuck launches" },
18
+ { id: 'browser-tabs', barrier: 'integration:browser-controller', platforms: ['darwin', 'linux', 'win32'], reads: "every tab with its owner (controller holder, or the process and session serving its local port) and the renderer serving it" },
19
+ { id: 'power', barrier: 'root', platforms: ['darwin'], reads: 'thermal pressure (powermetrics)' },
20
+ ];
21
+
22
+ /** Every feature with on/off and the reason, for this platform, this user and this config. */
23
+ export function resolveFeatures({ platform = process.platform, reader, config = {}, root = false, extras = {} }) {
24
+ return FEATURES.map((feature) => {
25
+ const row = { id: feature.id, barrier: feature.barrier, reads: feature.reads };
26
+ if (!feature.platforms.includes(platform)) return { ...row, on: false, reason: `not implemented on ${platform}` };
27
+ if (config.features?.[feature.id] === false) return { ...row, on: false, reason: 'turned off in config (features.' + feature.id + ': false)' };
28
+ if (feature.barrier === 'root' && !root) return { ...row, on: false, reason: 'needs root: run the probe as root (a LaunchDaemon or system unit); the pack never escalates' };
29
+ if (feature.id === 'process-footprint' && extras.rusage === false) return { ...row, on: false, reason: 'needs python3 for libproc reads (python.org, Homebrew or the Command Line Tools)' };
30
+ if (feature.id === 'power' && !reader.powermetricsAvailable?.()) return { ...row, on: false, reason: 'powermetrics not found' };
31
+ if (feature.id === 'browser-tabs' && !config.integrations?.browserController) return { ...row, on: false, reason: 'no browser controller configured (integrations.browserController.portFile or .url)' };
32
+ if (feature.id === 'services' && !(config.services ?? []).length) return { ...row, on: false, reason: 'no services configured (services: [{label, log?, maxSilentMin?}])' };
33
+ if (feature.id === 'supercode' && extras.supercode === false) return { ...row, on: false, reason: 'supercode not found (SUPERCODE_BIN)' };
34
+ return { ...row, on: true, reason: null };
35
+ });
36
+ }
package/lib/format.mjs ADDED
@@ -0,0 +1,58 @@
1
+ // The snapshot as a person reads it: alarms first, then each reading, then which features are off and why.
2
+ const n = (v, unit = '') => (v == null ? '-' : `${v}${unit}`);
3
+ const who = (p) => (p?.session?.address ? ` ← ${p.session.address}` : p?.session?.harness ? ` ← ${p.session.harness} pid ${p.session.agentPid}` : '');
4
+
5
+ export function formatStatus(s, { all = false } = {}) {
6
+ if (!s) return 'machine health: no reading yet (is the probe running? `supercode health run`)';
7
+ const r = s.readings ?? {};
8
+ const age = Math.round((Date.now() - Date.parse(s.at)) / 1000);
9
+ const out = [`machine health · ${s.machine} (${s.platform}) · read ${age}s ago by probe pid ${s.probe?.pid}${s.probe?.config ? ` · config ${s.probe.config}` : ''}`];
10
+ if (age > 3 * (s.probe?.intervalSec ?? 15)) out.push(` STALE: the probe has not written for ${age}s; its readings below are old`);
11
+ out.push('', s.alarms.length ? `ALARMS (${s.alarms.length})` : 'ALARMS: none');
12
+ for (const a of s.alarms) out.push(` ${a.level.toUpperCase().padEnd(4)} ${a.summary}${a.detail?.pid ? ` [${a.detail.command?.slice(0, 80)}${who(a.detail)}]` : ''} (since ${a.since})`);
13
+ if (s.maintainers) out.push(` mailed to: ${s.maintainers.length ? s.maintainers.join(', ') : 'no maintainer subscribed (supercode health subscribe <address>)'}${s.mail?.pending ? ` · ${s.mail.pending} undelivered: ${s.mail.error}` : ''}`);
14
+ if (r.load) out.push('', `load ${r.load.one} / ${r.load.five} / ${r.load.fifteen} on ${r.load.cores} cores (${r.load.perCore}× per core)`);
15
+ if (r.memory) {
16
+ const m = r.memory;
17
+ out.push(`memory pressure ${n(m.pressureLevel)} · free ${n(m.freePct, '%')} · swapouts ${n(m.swapoutsPerSec, '/s')} · compressions ${n(m.compressionsPerSec, '/s')} · swap ${n(m.swapUsedGB)}/${n(m.swapTotalGB)} GB · swapped ${n(m.swappedPctRamWindow, '% RAM')} in ${n(m.swappedWindowMin, ' min')}`);
18
+ }
19
+ if (r.processes) {
20
+ out.push(`processes ${r.processes.count}; by CPU:`);
21
+ for (const p of r.processes.topCpu.slice(0, all ? 10 : 5)) out.push(` ${String(p.cpuPct ?? '-').padStart(4)}% ${String(p.footprintMB).padStart(6)} MB ${p.pid} ${p.command.slice(0, 90)}${who(p)}`);
22
+ out.push(' by memory:');
23
+ for (const p of r.processes.topMemory.slice(0, all ? 10 : 5)) out.push(` ${String(p.footprintMB).padStart(6)} MB ${p.growthMBPerMin != null ? `(${p.growthMBPerMin >= 0 ? '+' : ''}${p.growthMBPerMin}/min) ` : ''} ${p.pid} ${p.command.slice(0, 90)}${who(p)}`);
24
+ }
25
+ if (r.renderers) {
26
+ out.push(`renderers ${r.renderers.count} using ${r.renderers.totalMB} MB${r.renderers.orphanedHeadless.length ? `; ${r.renderers.orphanedHeadless.length} orphaned headless browser(s): ${r.renderers.orphanedHeadless.map((p) => p.pid).join(', ')}` : ''}`);
27
+ for (const p of r.renderers.top.slice(0, 3)) out.push(` ${String(p.cpuPct ?? '-').padStart(4)}% ${String(p.footprintMB).padStart(6)} MB ${p.pid}${who(p)}${p.tab ? ` · ${p.tab.url.slice(0, 60)} · ${p.tab.owner}${who(p.tab.server)}` : ' · tab not matched'}`);
28
+ }
29
+ if (r.browserTabs) {
30
+ out.push(`tabs ${r.browserTabs.count} open${Object.keys(r.browserTabs.byHolder).length ? ` (${Object.entries(r.browserTabs.byHolder).map(([h, c]) => `${h} ${c}`).join(', ')})` : ''}`);
31
+ for (const t of r.browserTabs.tabs ?? []) out.push(` ${t.url.slice(0, 70)} · ${t.owner}${who(t.server)}${t.renderers.length ? ` · renderer ${t.renderers.join(', ')}` : ''}`);
32
+ }
33
+ if (r.daemons) out.push(`daemons ${r.daemons.filter((d) => (d.cpuPct ?? 0) >= 5 || d.footprintMB >= 512).map((d) => `${d.name} ${n(d.cpuPct, '%')} ${d.footprintMB} MB`).join(' · ') || 'all quiet'}`);
34
+ if (r.disk) for (const d of r.disk) out.push(`disk ${d.mount}: ${d.freeGB} GB free of ${d.totalGB} (${d.freePct}%)${d.fillGBPerHour != null ? ` · ${d.fillGBPerHour >= 0 ? 'filling' : 'freeing'} ${Math.abs(d.fillGBPerHour)} GB/h` : ''}${d.hoursToFull != null ? ` · full in ${d.hoursToFull} h` : ''}`);
35
+ if (r.diskIO) out.push(`disk I/O ${Object.entries(r.diskIO).map(([k, v]) => `${k} ${n(v.MBps, ' MB/s')}`).join(' · ')}`);
36
+ if (r.diskWriters?.length) out.push(`writers ${r.diskWriters.slice(0, 3).map((w) => `${w.pid} ${w.writeMBps} MB/s ${w.command.slice(0, 50)}${who(w)}`).join(' · ')}`);
37
+ if (r.tcp) {
38
+ const t = r.tcp;
39
+ out.push(`tcp ${Object.entries(t.states).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${v}`).join(' · ')}`);
40
+ out.push(` ephemeral range ${t.range.first}-${t.range.last} (${t.range.size}); busiest ${t.ephemeral[0] ? `${t.ephemeral[0].addr} ${t.ephemeral[0].used} (${t.ephemeral[0].pct}%)` : '-'}; TIME_WAIT lasts ${t.timeWaitSec}s`);
41
+ for (const g of t.timeWaitByTarget.slice(0, 3)) out.push(` TIME_WAIT ${g.count} (${g.pct}%) against ${g.target}${g.listener ? ` · pid ${g.listener.pid} ${g.listener.command.slice(0, 60)}${who(g.listener)}` : ''}`);
42
+ }
43
+ if (r.files) out.push(`files ${r.files.open} of ${r.files.max} open (${r.files.pct}%)`);
44
+ if (r.services) for (const sv of r.services) out.push(`service ${sv.name ?? sv.label}: ${sv.running ? `running pid ${sv.pid}` : `NOT running (last exit ${sv.lastExit})`}${sv.logAgeMin != null ? ` · log written ${sv.logAgeMin} min ago` : ''}`);
45
+ if (r.supercode) {
46
+ const c = r.supercode;
47
+ out.push(`supercode connector ${c.connector ? `${c.connector.running ? 'running' : 'NOT running'}, server sees ${c.connector.connection ?? '-'}${c.connector.notSyncing ? `, ${c.connector.notSyncing} sessions not syncing (${c.connector.lastError})` : ''}` : (c.statusError ?? 'unknown')} · daemon ${c.daemon.answered ? `answered in ${c.daemon.ms} ms` : `NOT answering (${c.daemon.error})`} · launches ${c.launches.read ? `${c.launches.recent} in 24 h, ${c.launches.stuck.length} stuck, ${c.launches.failed.length} failed` : `unread (${c.launches.error})`}`);
48
+ }
49
+ if (r.power) out.push(`power thermal pressure ${r.power.thermalPressure}`);
50
+ out.push('', formatFeatures(s.features, { offOnly: !all }));
51
+ return out.join('\n');
52
+ }
53
+
54
+ export function formatFeatures(features, { offOnly = false } = {}) {
55
+ const rows = offOnly ? features.filter((f) => !f.on || f.error) : features;
56
+ const head = offOnly ? (rows.length ? 'features off or failing:' : 'every feature is on') : 'features:';
57
+ return [head, ...rows.map((f) => ` ${f.on ? (f.error ? 'ERR' : 'on ') : 'off'} ${f.id.padEnd(20)} ${f.barrier.padEnd(31)} ${f.error ?? f.reason ?? f.reads}`)].join('\n');
58
+ }
package/lib/linux.mjs ADDED
@@ -0,0 +1,148 @@
1
+ // Linux readers, from /proc and statfs. Every one runs unprivileged; another user's /proc/<pid>/io needs root.
2
+ import { readFileSync, readdirSync, readlinkSync, statfsSync } from 'node:fs';
3
+ import { run } from './run.mjs';
4
+
5
+ const read = (path) => readFileSync(path, 'utf8');
6
+ const TICKS = 100; // USER_HZ: the unit of /proc/<pid>/stat CPU times on every mainstream architecture
7
+ const PAGE = 4096;
8
+
9
+ export async function memory() {
10
+ const meminfo = Object.fromEntries(read('/proc/meminfo').split('\n').filter(Boolean).map((line) => {
11
+ const [key, value] = line.split(':'); return [key, Number.parseInt(value, 10) * 1024];
12
+ }));
13
+ const vmstat = Object.fromEntries(read('/proc/vmstat').split('\n').filter(Boolean).map((line) => line.split(' ')).map(([k, v]) => [k, Number(v)]));
14
+ let psi = null;
15
+ try {
16
+ const text = read('/proc/pressure/memory');
17
+ psi = { someAvg10: Number(/some avg10=([\d.]+)/.exec(text)?.[1]), fullAvg10: Number(/full avg10=([\d.]+)/.exec(text)?.[1]) };
18
+ } catch { /* kernels without PSI */ }
19
+ const total = meminfo.MemTotal;
20
+ const level = psi ? (psi.fullAvg10 >= 10 ? 'critical' : psi.someAvg10 >= 10 ? 'warn' : 'normal') : null;
21
+ return {
22
+ pressureLevel: level, psi, totalBytes: total,
23
+ freePct: Math.round((meminfo.MemAvailable / total) * 100),
24
+ swapUsedBytes: meminfo.SwapTotal - meminfo.SwapFree, swapTotalBytes: meminfo.SwapTotal,
25
+ counters: { swapouts: vmstat.pswpout, swapins: vmstat.pswpin, pageouts: vmstat.pgpgout, pageins: vmstat.pgpgin, compressions: null, decompressions: null, majorFaults: vmstat.pgmajfault },
26
+ pageSize: PAGE,
27
+ };
28
+ }
29
+
30
+ export async function processes() {
31
+ const rows = [];
32
+ for (const name of readdirSync('/proc')) {
33
+ if (!/^\d+$/.test(name)) continue;
34
+ try {
35
+ const stat = read(`/proc/${name}/stat`);
36
+ const close = stat.lastIndexOf(')');
37
+ const f = stat.slice(close + 2).split(' ');
38
+ const uid = Number(/^Uid:\s+(\d+)/m.exec(read(`/proc/${name}/status`))?.[1] ?? -1);
39
+ let command = read(`/proc/${name}/cmdline`).replace(/\0+$/, '').replace(/\0/g, ' ');
40
+ if (!command) command = `[${stat.slice(stat.indexOf('(') + 1, close)}]`;
41
+ rows.push({ pid: Number(name), ppid: Number(f[1]), uid, cpuSec: (Number(f[11]) + Number(f[12])) / TICKS, rssBytes: Number(f[21]) * PAGE, command });
42
+ } catch { /* exited while read */ }
43
+ }
44
+ return rows;
45
+ }
46
+
47
+ /** Per-process disk I/O (readable for this user's processes; every user's as root). */
48
+ export async function rusage() {
49
+ const map = new Map();
50
+ for (const name of readdirSync('/proc')) {
51
+ if (!/^\d+$/.test(name)) continue;
52
+ try {
53
+ const io = read(`/proc/${name}/io`);
54
+ const status = read(`/proc/${name}/status`);
55
+ const kb = (key) => Number(new RegExp(`^${key}:\\s+(\\d+)`, 'm').exec(status)?.[1] ?? 0) * 1024;
56
+ map.set(Number(name), {
57
+ footprintBytes: kb('VmRSS') + kb('VmSwap'),
58
+ diskReadBytes: Number(/^read_bytes: (\d+)/m.exec(io)?.[1] ?? 0),
59
+ diskWrittenBytes: Number(/^write_bytes: (\d+)/m.exec(io)?.[1] ?? 0),
60
+ });
61
+ } catch { /* another user's, or exited */ }
62
+ }
63
+ return map;
64
+ }
65
+ export async function rusageAvailable() { return true; }
66
+ export async function topFootprints() { return new Map(); }
67
+
68
+ const STATES = ['', 'ESTABLISHED', 'SYN_SENT', 'SYN_RECV', 'FIN_WAIT1', 'FIN_WAIT2', 'TIME_WAIT', 'CLOSE', 'CLOSE_WAIT', 'LAST_ACK', 'LISTEN', 'CLOSING'];
69
+ function hexAddress(text) {
70
+ const [addr, port] = text.split(':');
71
+ return { addr: addr.length === 8 ? addr.match(/../g).reverse().map((b) => Number.parseInt(b, 16)).join('.') : addr, port: Number.parseInt(port, 16) };
72
+ }
73
+ export async function tcp() {
74
+ const sockets = [];
75
+ for (const file of ['/proc/net/tcp', '/proc/net/tcp6']) {
76
+ let text; try { text = read(file); } catch { continue; }
77
+ for (const line of text.split('\n').slice(1)) {
78
+ const f = line.trim().split(/\s+/);
79
+ if (f.length < 4) continue;
80
+ sockets.push({ local: hexAddress(f[1]), foreign: hexAddress(f[2]), state: STATES[Number.parseInt(f[3], 16)] ?? f[3], inode: f[9] });
81
+ }
82
+ }
83
+ const [first, last] = read('/proc/sys/net/ipv4/ip_local_port_range').trim().split(/\s+/).map(Number);
84
+ return { sockets, range: { first, last }, timeWaitSec: 60 };
85
+ }
86
+
87
+ /** Listening port → owning pid, through /proc/<pid>/fd socket inodes (this user's processes). */
88
+ export async function listeners(sockets = []) {
89
+ const byInode = new Map(sockets.filter((s) => s.state === 'LISTEN').map((s) => [s.inode, s.local.port]));
90
+ const map = new Map();
91
+ if (!byInode.size) return map;
92
+ for (const name of readdirSync('/proc')) {
93
+ if (!/^\d+$/.test(name)) continue;
94
+ let fds; try { fds = readdirSync(`/proc/${name}/fd`); } catch { continue; }
95
+ for (const fd of fds) {
96
+ let target; try { target = readlinkSync(`/proc/${name}/fd/${fd}`); } catch { continue; }
97
+ const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
98
+ if (inode && byInode.has(inode) && !map.has(byInode.get(inode))) {
99
+ let command = ''; try { command = read(`/proc/${name}/comm`).trim(); } catch { /* exited */ }
100
+ map.set(byInode.get(inode), { pid: Number(name), command });
101
+ }
102
+ }
103
+ }
104
+ return map;
105
+ }
106
+
107
+ export async function files() {
108
+ const [open, , max] = read('/proc/sys/fs/file-nr').trim().split(/\s+/).map(Number);
109
+ return { open, max };
110
+ }
111
+
112
+ const SKIP_FS = new Set(['proc', 'sysfs', 'tmpfs', 'devtmpfs', 'devpts', 'cgroup', 'cgroup2', 'overlay', 'squashfs', 'securityfs', 'debugfs', 'tracefs', 'pstore', 'bpf', 'mqueue', 'hugetlbfs', 'fusectl', 'configfs', 'autofs', 'binfmt_misc', 'nsfs', 'ramfs', 'efivarfs', 'rpc_pipefs', 'fuse.portal', '9p']);
113
+ export async function volumes() {
114
+ const seen = new Set(); const rows = [];
115
+ for (const line of read('/proc/mounts').split('\n')) {
116
+ const [device, mount, type] = line.split(' ');
117
+ if (!mount || SKIP_FS.has(type) || seen.has(device)) continue;
118
+ seen.add(device);
119
+ try { const s = statfsSync(mount.replace(/\\040/g, ' ')); rows.push({ mount, device, totalBytes: s.blocks * s.bsize, freeBytes: s.bavail * s.bsize }); } catch { /* unreadable mount */ }
120
+ }
121
+ return rows;
122
+ }
123
+
124
+ export async function diskIO() {
125
+ const out = {};
126
+ for (const line of read('/proc/diskstats').split('\n')) {
127
+ const f = line.trim().split(/\s+/);
128
+ if (f.length < 14 || /^(loop|ram|dm-)/.test(f[2]) || /\d$/.test(f[2]) && !/nvme\d+n\d+$/.test(f[2])) continue;
129
+ out[f[2]] = { bytes: (Number(f[5]) + Number(f[9])) * 512 };
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /** systemd --user units: name → { pid, lastExit, active }. */
135
+ export async function services(names = []) {
136
+ const map = new Map();
137
+ if (!names.length) return map;
138
+ const r = await run('systemctl', ['--user', 'show', '-p', 'Id,MainPID,ExecMainStatus,ActiveState', ...names], { timeoutMs: 5000 });
139
+ if (!r.ok) throw new Error(r.error);
140
+ for (const block of r.stdout.split('\n\n')) {
141
+ const kv = Object.fromEntries(block.split('\n').filter(Boolean).map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]));
142
+ if (kv.Id) map.set(kv.Id.replace(/\.service$/, ''), { pid: Number(kv.MainPID) || null, lastExit: Number(kv.ExecMainStatus) || 0, active: kv.ActiveState });
143
+ }
144
+ return map;
145
+ }
146
+
147
+ export const isRoot = () => process.getuid?.() === 0;
148
+ export const powermetricsAvailable = () => false;