@volter/supercode-health 0.1.0 → 0.1.2

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/lib/darwin.mjs CHANGED
@@ -156,14 +156,20 @@ export async function diskIO() {
156
156
  }
157
157
 
158
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);
159
+ /** The user's launchd jobs: label → { pid, lastExit, runs }. `runs` counts every spawn since the job was loaded,
160
+ * so a KeepAlive job that crashes and respawns between two readings shows as runs rising with a failed last exit,
161
+ * where `launchctl list` alone shows a pid and looks running (12 ms per `launchctl print`). */
162
+ export async function services(labels = []) {
162
163
  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
- }
164
+ const uid = process.getuid?.();
165
+ await Promise.all(labels.map(async (label) => {
166
+ const r = await run('/bin/launchctl', ['print', `gui/${uid}/${label}`], { timeoutMs: 5000 });
167
+ if (!r.ok) return; // not loaded
168
+ const field = (name) => new RegExp(`^\\t${name} = (.+)$`, 'm').exec(r.stdout)?.[1];
169
+ const pid = Number(field('pid')) || null;
170
+ const lastExit = field('last exit code');
171
+ map.set(label, { pid, lastExit: lastExit == null || /never exited/.test(lastExit) ? 0 : Number.parseInt(lastExit, 10), runs: Number(field('runs')) || 0 });
172
+ }));
167
173
  return map;
168
174
  }
169
175
 
package/lib/format.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The snapshot as a person reads it: alarms first, then each reading, then which features are off and why.
2
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}` : '');
3
+ const who = (p) => (p?.session?.address ? ` ← ${p.session.address}` : p?.session?.harness ? ` ← ${p.session.harness} pid ${p.session.agentPid}` : p?.session?.note ? ` ← (${p.session.note})` : p?.session?.inherited ? ` ← (no agent above; inherited ${p.session.inherited})` : '');
4
4
 
5
5
  export function formatStatus(s, { all = false } = {}) {
6
6
  if (!s) return 'machine health: no reading yet (is the probe running? `supercode health run`)';
@@ -25,6 +25,10 @@ export function formatStatus(s, { all = false } = {}) {
25
25
  if (r.renderers) {
26
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
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
+ // Chrome names no renderer per tab (CDP's TargetInfo has no pid): a renderer that holds no storage bucket
29
+ // cannot be matched, so the tabs no renderer was matched to are listed with their owners instead.
30
+ const unmatched = (r.browserTabs?.tabs ?? []).filter((t) => !t.renderers.length);
31
+ if (r.renderers.top.slice(0, 3).some((p) => !p.tab) && unmatched.length) out.push(` unmatched tabs (one may be served by an unmatched renderer above): ${unmatched.map((t) => `${t.url.slice(0, 50)} · ${t.owner}${who(t.server)}`).join(' | ')}`);
28
32
  }
29
33
  if (r.browserTabs) {
30
34
  out.push(`tabs ${r.browserTabs.count} open${Object.keys(r.browserTabs.byHolder).length ? ` (${Object.entries(r.browserTabs.byHolder).map(([h, c]) => `${h} ${c}`).join(', ')})` : ''}`);
@@ -41,7 +45,7 @@ export function formatStatus(s, { all = false } = {}) {
41
45
  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
46
  }
43
47
  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` : ''}`);
48
+ if (r.services) for (const sv of r.services) out.push(`service ${sv.name ?? sv.label}: ${sv.crashing ? `CRASH-LOOPING: ${sv.restarts} restarts since the last reading, last exit ${sv.lastExit}, ${sv.runs} runs` : sv.running ? `running pid ${sv.pid}` : `NOT running (last exit ${sv.lastExit})`}${sv.logAgeMin != null ? ` · log written ${sv.logAgeMin} min ago` : ''}`);
45
49
  if (r.supercode) {
46
50
  const c = r.supercode;
47
51
  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})`}`);
package/lib/linux.mjs CHANGED
@@ -135,11 +135,11 @@ export async function diskIO() {
135
135
  export async function services(names = []) {
136
136
  const map = new Map();
137
137
  if (!names.length) return map;
138
- const r = await run('systemctl', ['--user', 'show', '-p', 'Id,MainPID,ExecMainStatus,ActiveState', ...names], { timeoutMs: 5000 });
138
+ const r = await run('systemctl', ['--user', 'show', '-p', 'Id,MainPID,ExecMainStatus,ActiveState,NRestarts', ...names], { timeoutMs: 5000 });
139
139
  if (!r.ok) throw new Error(r.error);
140
140
  for (const block of r.stdout.split('\n\n')) {
141
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 });
142
+ if (kv.Id) map.set(kv.Id.replace(/\.service$/, ''), { pid: Number(kv.MainPID) || null, lastExit: Number(kv.ExecMainStatus) || 0, active: kv.ActiveState, runs: (Number(kv.NRestarts) || 0) + 1 });
143
143
  }
144
144
  return map;
145
145
  }
package/lib/mail.mjs CHANGED
@@ -44,7 +44,7 @@ export class Outbox {
44
44
  const LEVEL = { warn: 'WARN', crit: 'CRIT', none: 'clear' };
45
45
  function line(t) {
46
46
  const d = t.detail;
47
- const who = d?.session?.address ? `; session ${d.session.address}` : d?.session?.harness ? `; under ${d.session.harness} pid ${d.session.agentPid}` : d?.pid ? '; no agent session above it' : '';
47
+ const who = d?.session?.address ? `; session ${d.session.address}` : d?.session?.harness ? `; under ${d.session.harness} pid ${d.session.agentPid}` : d?.session?.note ? `; ${d.session.note}` : d?.session?.inherited ? `; no agent above it (environment inherited from ${d.session.inherited}, unverified)` : d?.pid ? '; no agent session above it' : '';
48
48
  const proc = d?.pid ? ` [pid ${d.pid}: ${d.command}${who}]` : d?.listener ? ` [listener pid ${d.listener.pid}: ${d.listener.command}${d.listener.session?.address ? `; session ${d.listener.session.address}` : ''}]` : '';
49
49
  return `${LEVEL[t.to] ?? t.to}${t.from !== 'none' && t.to !== 'none' ? ` (was ${t.from})` : ''} ${t.key}: ${t.summary}${proc}`;
50
50
  }
package/lib/sample.mjs CHANGED
@@ -18,6 +18,11 @@ const HARNESS = { claude: 'claude-code', codex: 'codex', gemini: 'gemini', goose
18
18
  const RENDERER = /Helper \(Renderer\)|--type=renderer/;
19
19
  const BROWSER_MAIN = /(Google Chrome|Chromium|chrome-headless-shell|chrome)(\s|$)/i;
20
20
  const LOOPBACK = /^(127\.|::1$|localhost$)/;
21
+ /** The session an agent names on its own command line: `--resume <id>`, `--session-id <id>`, `-r <id>`, `codex resume <id>`. */
22
+ function argvSession(command) {
23
+ return /(?:--resume|--session-id|\s-r)[=\s]+([0-9a-f]{8}-[0-9a-f-]{27,})/.exec(command)?.[1]
24
+ ?? /\bresume\s+([0-9a-zA-Z][0-9a-zA-Z-]{7,})/.exec(command)?.[1] ?? null;
25
+ }
21
26
  // The slow set's readings and alarm families, by the feature that produces them.
22
27
  const SLOW_FEATURE = { disk: 'disk', services: 'services', service: 'services', supercode: 'supercode', browserTabs: 'browser-tabs', tabs: 'browser-tabs', power: 'power' };
23
28
  // Every alarm family, by the feature that observes it.
@@ -32,6 +37,8 @@ export class Sampler {
32
37
  this.swapRing = []; // [[at, swapouts]] for the 30-minute sum
33
38
  this.diskRing = new Map(); // mount → [[at, freeBytes]] for the fill rate
34
39
  this.sessionCache = new Map(); // pid:command → session
40
+ this.serviceRuns = new Map(); // service label → spawn count at the last reading
41
+ this.recentListeners = new Map(); // port → when it was last seen listening (its TIME_WAITs outlive it)
35
42
  this.topFoot = { at: 0, map: new Map() };
36
43
  this.slow = { at: 0, readings: {}, observations: [], families: new Set(), errors: {} };
37
44
  this.slowStarted = 0;
@@ -110,8 +117,11 @@ export class Sampler {
110
117
  r.footprintSource = ru ? 'footprint' : top != null ? 'footprint (top)' : 'rss';
111
118
  r.writeBps = same && dt && ru && prev.written != null && ru.diskWrittenBytes >= prev.written ? (ru.diskWrittenBytes - prev.written) / dt : null;
112
119
  nextPrev.set(r.pid, { cpuSec: r.cpuSec, command: r.command, written: ru?.diskWrittenBytes ?? null });
113
- const hist = same ? (this.footHistory.get(r.pid) ?? []) : [];
114
- hist.push([now, r.footprintBytes]);
120
+ // Growth compares like with like: a footprint read through a different instrument than the last reading
121
+ // (another user's process alternates between top's 5-minute map and RSS) starts a new history.
122
+ let hist = same ? (this.footHistory.get(r.pid) ?? []) : [];
123
+ if (hist.length && hist.at(-1)[2] !== r.footprintSource) hist = [];
124
+ hist.push([now, r.footprintBytes, r.footprintSource]);
115
125
  while (hist.length && now - hist[0][0] > 130_000) hist.shift();
116
126
  this.footHistory.set(r.pid, hist);
117
127
  const ago = hist.findLast?.(([at]) => now - at >= 55_000) ?? [...hist].reverse().find(([at]) => now - at >= 55_000);
@@ -225,6 +235,10 @@ export class Sampler {
225
235
  if (!['*', '0.0.0.0', '::', '[::]'].includes(s.local.addr)) local.add(s.local.addr);
226
236
  }
227
237
  const inRange = (port) => port >= range.first && port <= range.last;
238
+ for (const port of listening) this.recentListeners.set(port, now);
239
+ const hold = Math.max(4 * timeWaitSec * 1000, 120_000);
240
+ for (const [port, at] of this.recentListeners) if (now - at > hold) this.recentListeners.delete(port);
241
+ const wasListening = (port) => this.recentListeners.has(port);
228
242
  const wildcard = new Set();
229
243
  const used = new Map();
230
244
  const add = (addr, port) => { if (!used.has(addr)) used.set(addr, new Set()); used.get(addr).add(port); };
@@ -236,9 +250,11 @@ export class Sampler {
236
250
  if (local.has(s.foreign.addr) && inRange(s.foreign.port)) add(s.foreign.addr, s.foreign.port);
237
251
  if (s.state !== 'TIME_WAIT') continue;
238
252
  // Group each TIME_WAIT by the endpoint it churned against: a listener here (either side), else the remote.
253
+ // A listener that exited still owns its TIME_WAITs until they drain, so a port counts as a listener for
254
+ // a while after it was last seen listening; otherwise its alarm would clear while the sockets remain.
239
255
  let target;
240
- if (listening.has(s.local.port)) target = `:${s.local.port}`;
241
- else if ((local.has(s.foreign.addr) || LOOPBACK.test(s.foreign.addr)) && listening.has(s.foreign.port)) target = `:${s.foreign.port}`;
256
+ if (wasListening(s.local.port)) target = `:${s.local.port}`;
257
+ else if ((local.has(s.foreign.addr) || LOOPBACK.test(s.foreign.addr)) && wasListening(s.foreign.port)) target = `:${s.foreign.port}`;
242
258
  else target = `${s.foreign.addr}:${s.foreign.port}`;
243
259
  targets.set(target, (targets.get(target) ?? 0) + 1);
244
260
  }
@@ -255,12 +271,13 @@ export class Sampler {
255
271
  } else owners = this.owners ?? new Map();
256
272
  const timeWaitByTarget = top.map(([target, count]) => {
257
273
  const owner = target.startsWith(':') ? owners.get(Number(target.slice(1))) : null;
258
- return { target, count, pct: round((count / size) * 100), ...(owner ? { listener: { pid: owner.pid, command: short(owner.command) } } : {}) };
274
+ const exited = target.startsWith(':') && !listening.has(Number(target.slice(1)));
275
+ return { target, count, pct: round((count / size) * 100), ...(exited ? { exited: true } : {}), ...(owner ? { listener: { pid: owner.pid, command: short(owner.command) } } : {}) };
259
276
  });
260
277
  readings.tcp = { states, timeWait: states.TIME_WAIT ?? 0, range: { ...range, size }, timeWaitSec, ephemeral, timeWaitByTarget };
261
278
  families.add('tcp');
262
279
  for (const e of ephemeral) observations.push({ key: `tcp.ephemeral:${e.addr}`, family: 'tcp', def: alarm('tcp.ephemeral'), value: e.pct, unit: '% of ports', summary: `${e.used} of ${size} ephemeral ports in use on ${e.addr}` });
263
- for (const t of timeWaitByTarget) observations.push({ key: `tcp.timeWaitTarget:${t.target}`, family: 'tcp', def, value: t.pct, unit: '% of ports', summary: `${t.count} TIME_WAIT against ${t.target.startsWith(':') ? `listener ${t.target}` : t.target}${t.listener ? ` (pid ${t.listener.pid})` : ''}; range ${size}, TIME_WAIT lasts ${timeWaitSec} s`, detail: t });
280
+ for (const t of timeWaitByTarget) observations.push({ key: `tcp.timeWaitTarget:${t.target}`, family: 'tcp', def, value: t.pct, unit: '% of ports', summary: `${t.count} TIME_WAIT against ${t.target.startsWith(':') ? `listener ${t.target}${t.exited ? ' (listener has exited; its TIME_WAITs are draining)' : ''}` : t.target}${t.listener ? ` (pid ${t.listener.pid})` : ''}; range ${size}, TIME_WAIT lasts ${timeWaitSec} s`, detail: t });
264
281
  });
265
282
 
266
283
  // ---- the slow set ---------------------------------------------------------------------------------
@@ -327,12 +344,18 @@ export class Sampler {
327
344
  const log = s.log?.replace(/^~(?=\/)/, homedir());
328
345
  if (log) try { logAgeMin = Math.max(0, round((now - statSync(log).mtimeMs) / 60_000)); } catch { logAgeMin = null; }
329
346
  const running = Boolean(job?.pid);
330
- const down = !job || (!running && (job.lastExit !== 0 || s.keepAlive !== false));
331
- const why = !job ? 'not loaded' : !running ? `not running (last exit ${job.lastExit})` : null;
347
+ // Down (ADR 0006): not loaded, not running, or failing. A KeepAlive job that crashes and is respawned
348
+ // always has a pid when read, so failing is runs rising since the last reading with a non-zero last exit.
349
+ const before = this.serviceRuns.get(s.label);
350
+ const restarts = job && before != null && job.runs >= before ? job.runs - before : 0;
351
+ if (job) this.serviceRuns.set(s.label, job.runs);
352
+ const crashing = Boolean(job && restarts > 0 && job.lastExit !== 0);
353
+ const down = !job || (!running && (job.lastExit !== 0 || s.keepAlive !== false)) || crashing;
354
+ const why = !job ? 'not loaded' : !running ? `not running (last exit ${job.lastExit})` : crashing ? `crash-looping: ${restarts} restart${restarts === 1 ? '' : 's'} since the last reading, last exit ${job.lastExit} (${job.runs} runs since loaded)` : null;
332
355
  if (down) slow.observations.push({ key: `service.down:${s.label}`, family: 'service', def: alarm('service.down'), value: 1, met: { warn: true, crit: false, clears: false }, summary: `service ${s.name ?? s.label} is ${why}` });
333
356
  else slow.observations.push({ key: `service.down:${s.label}`, family: 'service', def: alarm('service.down'), value: 0, met: { warn: false, crit: false, clears: true }, summary: `service ${s.name ?? s.label} running` });
334
357
  if (s.log && s.maxSilentMin) slow.observations.push({ key: `service.deaf:${s.label}`, family: 'service', def: { warn: s.maxSilentMin, forSec: 0 }, value: logAgeMin ?? Infinity, unit: 'min silent', summary: logAgeMin == null ? `service ${s.name ?? s.label}: its log ${log} is missing` : `service ${s.name ?? s.label} has written nothing to its log for ${logAgeMin} min` });
335
- return { label: s.label, name: s.name ?? null, running, pid: job?.pid ?? null, lastExit: job?.lastExit ?? null, ...(s.log ? { logAgeMin } : {}) };
358
+ return { label: s.label, name: s.name ?? null, running, pid: job?.pid ?? null, lastExit: job?.lastExit ?? null, runs: job?.runs ?? null, restarts, crashing, ...(s.log ? { logAgeMin } : {}) };
336
359
  });
337
360
  });
338
361
 
@@ -435,27 +458,44 @@ export class Sampler {
435
458
  for (let i = 0; i < 40 && p > 1; i += 1) { const r = byPid.get(p); if (!r) break; out.push(r); p = r.ppid; }
436
459
  return out;
437
460
  };
461
+ // Who owns a process is the agent above it, named by the agent itself: the session id on its own command
462
+ // line (`--resume`/`--session-id`, `codex resume`), else the id it sets in its own children's environment.
463
+ // An environment is never trusted across a tmux server: every pane inherits the variables of whichever
464
+ // session started that server (measured 2026-10-02: one tmux server credited one Claude session with
465
+ // dozens of unrelated panes). Above no agent, an inherited id is reported as inherited, never as owner.
438
466
  const need = new Set();
439
467
  const plan = new Map();
468
+ const childOf = (pid) => this.procs.rows.find((x) => x.ppid === pid && !AGENT.test(base(x.command)));
440
469
  for (const pid of wanted) {
441
470
  const r = byPid.get(pid);
442
471
  if (!r) continue;
443
472
  const cacheKey = `${pid}:${r.command}`;
444
473
  if (this.sessionCache.has(cacheKey)) { plan.set(pid, { cacheKey }); continue; }
445
474
  const up = chain(pid);
446
- const agentIndex = up.findIndex((x) => AGENT.test(base(x.command)));
447
- const lineage = agentIndex >= 0 ? up.slice(0, agentIndex + 1) : up.slice(0, 3);
448
- for (const x of lineage) need.add(x.pid);
449
- plan.set(pid, { cacheKey, lineage, agent: agentIndex >= 0 ? up[agentIndex] : null });
475
+ const stop = up.findIndex((x) => AGENT.test(base(x.command)) || base(x.command) === 'tmux');
476
+ const top = stop >= 0 ? up[stop] : null;
477
+ const agent = top && AGENT.test(base(top.command)) ? top : null;
478
+ // The agent's direct child on this lineage carries the variables the agent set; for the agent itself, any child.
479
+ const witness = agent ? (stop > 0 ? up[stop - 1] : childOf(agent.pid)) : (top ? null : r);
480
+ if (witness) need.add(witness.pid);
481
+ plan.set(pid, { cacheKey, agent, witness, pane: Boolean(top && !agent) });
450
482
  }
451
483
  const env = need.size ? await this.#environments([...need]) : new Map();
452
484
  for (const [pid, p] of plan) {
453
485
  if (!this.sessionCache.has(p.cacheKey)) {
454
- let found = null;
455
- for (const x of p.lineage) { found = env.get(x.pid); if (found) break; }
456
- const harness = p.agent ? (HARNESS[base(p.agent.command)] ?? base(p.agent.command)) : found?.harness ?? null;
457
- const session = found ? { harness: found.harness ?? harness, id: found.id, address: found.address ?? `sc:${this.machine}:${found.harness ?? harness}:${found.id}`, agentPid: p.agent?.pid ?? null }
458
- : p.agent ? { harness, id: null, address: null, agentPid: p.agent.pid } : null;
486
+ let session = null;
487
+ if (p.agent) {
488
+ const harness = HARNESS[base(p.agent.command)] ?? base(p.agent.command);
489
+ const seen = p.witness ? env.get(p.witness.pid) : null;
490
+ const id = argvSession(p.agent.command) ?? (harness === 'codex' ? seen?.codex : harness === 'claude-code' ? seen?.claude : null) ?? null;
491
+ session = { harness, id, address: id ? `sc:${this.machine}:${harness}:${id}` : null, agentPid: p.agent.pid };
492
+ } else if (p.pane) {
493
+ session = { harness: null, id: null, address: null, agentPid: null, note: 'in a tmux pane with no agent above it' };
494
+ } else {
495
+ const seen = p.witness ? env.get(p.witness.pid) : null;
496
+ const inherited = seen?.supercode ?? (seen?.claude ? `sc:${this.machine}:claude-code:${seen.claude}` : seen?.codex ? `sc:${this.machine}:codex:${seen.codex}` : null);
497
+ if (inherited) session = { harness: null, id: null, address: null, agentPid: null, inherited };
498
+ }
459
499
  this.sessionCache.set(p.cacheKey, session);
460
500
  if (this.sessionCache.size > 5000) this.sessionCache.delete(this.sessionCache.keys().next().value);
461
501
  }
@@ -471,10 +511,7 @@ export class Sampler {
471
511
  const supercode = /(?:^|\s|\0)SUPERCODE_CALLER=(sc:[^\s\0]+)/.exec(text)?.[1];
472
512
  const claude = /(?:^|\s|\0)CLAUDE_CODE_SESSION_ID=([0-9a-f-]{36})/.exec(text)?.[1];
473
513
  const codex = /(?:^|\s|\0)CODEX_THREAD_ID=([0-9a-zA-Z-]{8,})/.exec(text)?.[1];
474
- if (claude) return { harness: 'claude-code', id: claude };
475
- if (codex) return { harness: 'codex', id: codex };
476
- if (supercode) { const [, , harness, id] = supercode.split(':'); return { harness, id, address: supercode }; }
477
- return null;
514
+ return supercode || claude || codex ? { supercode, claude, codex } : null;
478
515
  };
479
516
  if (this.platform === 'linux') {
480
517
  for (const pid of pids) { try { const found = pick(readFileSync(`/proc/${pid}/environ`, 'utf8')); if (found) map.set(pid, found); } catch { /* another user's */ } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-health",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Optional machine pack: a cheap, deterministic probe of a machine's vitals that mails the machine's maintainer when a threshold is crossed. Each feature sits behind its own permission barrier.",
6
6
  "license": "MIT",
@@ -10,8 +10,17 @@
10
10
  "./format": "./lib/format.mjs",
11
11
  "./paths": "./lib/paths.mjs"
12
12
  },
13
- "bin": { "supercode-health": "./bin/health.mjs" },
14
- "files": ["index.mjs", "lib", "bin", "README.md"],
15
- "engines": { "node": ">=20" },
13
+ "bin": {
14
+ "supercode-health": "./bin/health.mjs"
15
+ },
16
+ "files": [
17
+ "index.mjs",
18
+ "lib",
19
+ "bin",
20
+ "README.md"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
16
25
  "dependencies": {}
17
26
  }