@volter/supercode-health 0.1.13 → 0.1.15

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/drift.mjs CHANGED
@@ -71,7 +71,18 @@ export async function staleSupercode({ rows = null } = {}) {
71
71
  } else if (Number.isNaN(started) || installed <= started + 1000) continue; // ps says a start to the second
72
72
  stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
73
73
  }
74
- return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
74
+ return moved(stale).sort((a, b) => b.installedAt.localeCompare(a.installedAt));
75
+ }
76
+
77
+ /** Less the processes that answer through the release on disk: an MCP server's pump says so in `<home>/run/mcp/<pid>`
78
+ * once a child of the release an install put there answers for it (lib/mcp.mjs), at or after that install. */
79
+ function moved(stale) {
80
+ const home = process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(homedir(), '.config', 'supercode'));
81
+ return stale.filter((row) => {
82
+ let said = null;
83
+ try { said = JSON.parse(readFileSync(join(home, 'run', 'mcp', String(row.pid)), 'utf8')); } catch { return true; }
84
+ return !(said?.pid === row.pid && Date.parse(said.at) >= Date.parse(row.installedAt));
85
+ });
75
86
  }
76
87
 
77
88
  /**
package/lib/features.mjs CHANGED
@@ -13,6 +13,7 @@ export const FEATURES = [
13
13
  { id: 'disk-io', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'device throughput; top writers per process' },
14
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
15
  { id: 'files', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'open files against the system limit' },
16
+ { id: 'recorded', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'every command supercode recorded outside itself (harness configs, hooks baked into live sessions), one of each program and verb run as its consumer runs it' },
16
17
  { id: 'drift', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'running supercode processes whose install was replaced after they started (one ps)' },
17
18
  { id: 'services', barrier: 'none', platforms: ['darwin', 'linux'], reads: "configured launchd/systemd user jobs: running, last exit, log silence" },
18
19
  { id: 'supercode', barrier: 'integration:machine-daemon', platforms: ['darwin', 'linux', 'win32'], reads: "this machine's supercode connector: answering, server link, stuck launches" },
package/lib/probe.mjs CHANGED
@@ -51,6 +51,9 @@ export class Probe {
51
51
  this.f = healthFiles(dir);
52
52
  this.machine = machineName(machine || env.SUPERCODE_HEALTH_MACHINE || hostname());
53
53
  this.state = {};
54
+ // what the last mail said of each alarm (key → its level, summary, detail): a mail says what changed since, never each
55
+ // flip in between (t_be880329: disk and memory flapping mailed the maintainer ~1,200 times a day on yuerans)
56
+ this.said = new Map(); this.changesMailAt = 0; this.raisedLastAt = new Map(); // key → when it was last seen raised
54
57
  this.unknowns = {}; this.unknownMailAt = {}; // unknownMailAt: a kind of unknown (unknownKind: its key's prefix) → last mailed
55
58
  this.lastHistory = 0;
56
59
  this.sampleMs = null;
@@ -99,6 +102,7 @@ export class Probe {
99
102
  // A raised alarm whose reading was unknown keeps its unknown since across the restart, as its unknowns entry does.
100
103
  for (const a of (previous?.alarms ?? []).filter((x) => !x.detail?.pid || alive(x.detail.pid))) this.state[a.key] = { key: a.key, family: a.family, level: a.level, warnSince: null, critSince: null, raisedAt: a.since ? Date.parse(a.since) : Date.now(), lastMailAt: this.droppedOutbox ? 0 : Date.now(), restoredUntil: Date.now() + 2 * 60_000, value: a.value, unit: a.unit, summary: a.summary, detail: a.detail,
101
104
  ...(a.unknown?.since ? { unread: a.unknown.why, unreadAt: Date.parse(a.unknown.since) } : {}) };
105
+ for (const a of (previous?.alarms ?? [])) this.said.set(a.key, { level: a.level, summary: a.summary, detail: a.detail ?? null });
102
106
  }
103
107
 
104
108
  /** Remove an earlier pack's outbox; answers how many mails it held, logged by recipient. */
@@ -138,7 +142,26 @@ export class Probe {
138
142
  const repeatMs = this.config.repeatMin * 60_000;
139
143
  // A raised alarm is reminded each repeatMin after it was last said: in a mail of its own transition or reminder, never
140
144
  // pushed back by a mail about something else.
141
- const raisedNow = new Set(transitions.filter((t) => t.to !== 'none').map((t) => t.key));
145
+ // What changed since the last mail said it (net of every raise and clear between): said at most once each quietMin,
146
+ // a new crit at once, and with any mail going anyway (a reminder, an unknown due). A key that raised and cleared
147
+ // between two mails is never said.
148
+ const quietMs = (this.config.quietMin ?? 15) * 60_000;
149
+ const levels = new Map(alarms.map((a) => [a.key, a]));
150
+ const lastTransition = new Map(transitions.map((t) => [t.key, t]));
151
+ const net = [];
152
+ for (const a of alarms) {
153
+ const was = this.said.get(a.key)?.level ?? 'none';
154
+ if (was !== a.level) net.push({ key: a.key, from: was, to: a.level, value: a.value, unit: a.unit ?? null, summary: a.summary, detail: a.detail ?? null });
155
+ }
156
+ for (const a of alarms) this.raisedLastAt.set(a.key, now);
157
+ for (const [key, was] of this.said) {
158
+ if (levels.has(key)) continue;
159
+ // a clear is said once it has held a whole quiet window: an alarm that flaps is said raised once, never each clear
160
+ if (now - (this.raisedLastAt.get(key) ?? 0) < quietMs) continue;
161
+ const t = lastTransition.get(key);
162
+ net.push({ key, from: was.level, to: 'none', value: null, summary: t?.to === 'none' ? t.summary : `${was.summary} — cleared`, detail: t?.detail ?? was.detail });
163
+ }
164
+ const raisedNow = new Set(net.filter((t) => t.to !== 'none').map((t) => t.key));
142
165
  const reminders = alarms.filter((a) => !raisedNow.has(a.key) && now - (this.state[a.key].lastMailAt ?? 0) >= repeatMs);
143
166
  // A reading unknown for unknownMin is the maintainers' to know (raised or not), and again each repeatMin until read.
144
167
  // Timed per kind (a key's prefix; a family's whole key): a kind is due once its earliest key has been unknown
@@ -159,14 +182,25 @@ export class Probe {
159
182
  const to = subscribers(this.f, this.config);
160
183
  // after dropping an earlier pack's undelivered mail, the first mail states the whole current state, clears included
161
184
  const composedState = Boolean(this.droppedOutbox && to.length);
162
- const mail = composedState ? composeState({ machine: this.machine, raised: alarms }) : compose({ machine: this.machine, transitions, reminders, raised: alarms, unknowns: unknownsDue });
185
+ const changesDue = net.length > 0 && (now - this.changesMailAt >= quietMs || net.some((t) => t.to === 'crit' && t.from !== 'crit') || reminders.length > 0 || unknownsDue.length > 0);
186
+ const changes = changesDue ? net : [];
187
+ const mail = composedState ? composeState({ machine: this.machine, raised: alarms }) : compose({ machine: this.machine, transitions: changes, reminders: reminders.filter((a) => !changes.some((t) => t.key === a.key)), raised: alarms, unknowns: unknownsDue });
163
188
  if (composedState) this.droppedOutbox = 0;
164
189
  if (mail) {
165
190
  // the kinds of unknown this mail said (the whole-state mail says none: they stay due)
166
191
  if (!composedState) for (const [kind] of dueKinds) this.unknownMailAt[kind] = now;
167
192
  // only the alarms this mail said: raised by it, or reminded (the whole-state mail says every one)
168
- const said = composedState ? alarms : alarms.filter((a) => raisedNow.has(a.key) || reminders.includes(a));
193
+ const said = composedState ? alarms : alarms.filter((a) => (changes.length && raisedNow.has(a.key)) || reminders.includes(a));
169
194
  for (const a of said) this.state[a.key].lastMailAt = now;
195
+ // the state this mail said is what the next mail is measured from: the whole state, or the last one said with this
196
+ // mail's changes applied. A clear still in its hold was not said, so its alarm stays said raised until a mail
197
+ // says the clear (#1268 review P1: replacing it with the alarms raised now lost that clear for good)
198
+ if (composedState) this.said = new Map(alarms.map((a) => [a.key, { level: a.level, summary: a.summary, detail: a.detail ?? null }]));
199
+ else for (const t of changes) {
200
+ if (t.to === 'none') this.said.delete(t.key);
201
+ else this.said.set(t.key, { level: t.to, summary: t.summary, detail: t.detail ?? null });
202
+ }
203
+ if (composedState || changes.length) this.changesMailAt = now;
170
204
  this.log(`${mail.subject}${to.length ? '' : ' (no maintainer subscribed)'}\n${mail.body}`);
171
205
  }
172
206
  const snapshot = {
@@ -0,0 +1,136 @@
1
+ // Recorded commands (t_4e879316, ADR 0022): every command this machine holds for supercode outside supercode itself
2
+ // still runs. Supercode writes commands into things it does not own: a harness's config (Codex's hooks.json and
3
+ // config.toml, Claude's settings and .claude.json) and the hooks baked into a session's launch flags, which the session
4
+ // holds until it ends. A release that moved a verb out of the native left those answering `unrecognized subcommand`
5
+ // (2026-10-06: Codex failed every tool call, Claude's AskUserQuestion was blocked). This reads them all, groups them by
6
+ // what they run (program and verb), and runs one of each as its consumer runs it: a hook with {} on stdin, an MCP server
7
+ // sent initialize. A hook may refuse by its own logic; what is said is a command that cannot run at all.
8
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
9
+ import { spawn } from 'node:child_process';
10
+ import { homedir, tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { run } from './run.mjs';
13
+
14
+ const json = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
15
+ const text = (path) => { try { return readFileSync(path, 'utf8'); } catch { return ''; } };
16
+ const mentions = (command) => /supercode/.test(command);
17
+ const unquote = (value) => { try { return JSON.parse(`"${value}"`); } catch { return value.replace(/\\"/g, '"'); } };
18
+ /** The `command` values in a JSON or TOML text: a hook's, an MCP server's. */
19
+ const commandsIn = (source) => [...source.matchAll(/"command"\s*[:=]\s*"((?:\\.|[^"\\])*)"|\bcommand\s*=\s*'([^']*)'/g)].map((m) => (m[1] !== undefined ? unquote(m[1]) : m[2]));
20
+
21
+ /** What a command runs, for grouping: its program (the first path in it) and the supercode verb after it. */
22
+ function shape(command) {
23
+ const words = command.match(/'[^']*'|"[^"]*"|\S+/g)?.map((word) => word.replace(/^['"]|['"]$/g, '')) ?? [];
24
+ const at = words.findIndex((word) => /supercode/.test(word) && !word.includes('='));
25
+ if (at < 0) return null;
26
+ return `${words[at]} ${words.slice(at + 1).filter((word) => !word.startsWith('/') && !word.startsWith("'")).slice(0, 2).join(' ')}`.trim();
27
+ }
28
+
29
+ /** Every recorded command naming supercode: `{ command, kind: 'hook' | 'mcp', where }`. */
30
+ export async function recordedCommands({ home = homedir(), processes = true } = {}) {
31
+ const found = [];
32
+ const hooks = (config, where) => {
33
+ for (const [event, groups] of Object.entries(config?.hooks ?? {})) for (const group of groups ?? []) for (const hook of group.hooks ?? []) {
34
+ if (typeof hook.command === 'string' && mentions(hook.command)) found.push({ command: hook.command, kind: 'hook', where: `${where} ${event}` });
35
+ }
36
+ };
37
+ hooks(json(join(home, '.codex', 'hooks.json')), 'codex hooks.json');
38
+ for (const file of ['settings.json', 'settings.local.json']) hooks(json(join(home, '.claude', file)), `claude ${file}`);
39
+ for (const [name, server] of Object.entries(json(join(home, '.claude.json'))?.mcpServers ?? {})) {
40
+ const command = [server.command, ...(server.args ?? [])].map((word) => `'${String(word).replace(/'/g, `'\\''`)}'`).join(' ');
41
+ if (mentions(command)) found.push({ command, kind: 'mcp', where: `claude mcp ${name}` });
42
+ }
43
+ // a section runs to the next header line (`[name]` opening a line), not to the next `[` (an `args = [...]` array)
44
+ for (const m of text(join(home, '.codex', 'config.toml')).matchAll(/^\[mcp_servers\.([^\]\n]+)\]\s*\n((?:(?!\[)[^\n]*\n?)*)/gm)) {
45
+ const program = /command\s*=\s*"([^"]+)"/.exec(m[2])?.[1];
46
+ const args = [...(/args\s*=\s*\[([^\]]*)\]/.exec(m[2])?.[1] ?? '').matchAll(/"([^"]*)"/g)].map((a) => a[1]);
47
+ const command = [program, ...args].map((word) => `'${String(word).replace(/'/g, `'\\''`)}'`).join(' ');
48
+ if (program && mentions(command)) found.push({ command, kind: 'mcp', where: `codex mcp ${m[1]}` });
49
+ }
50
+ // the hooks baked into running sessions: Claude's --settings (a file, or inline JSON) and Codex's -c values
51
+ if (processes) {
52
+ const table = String((await run('ps', ['-A', '-ww', '-o', 'pid=,command='], { timeoutMs: 10_000 })).stdout ?? '');
53
+ for (const line of table.split('\n')) {
54
+ const m = /^\s*(\d+)\s+\S*?(?:^|\/)(claude|codex)(\s.*)$/.exec(line);
55
+ if (!m) continue;
56
+ const sources = [m[3], ...[...m[3].matchAll(/--settings\s+(\/\S+\.json)/g)].map((f) => text(f[1]))];
57
+ for (const source of sources) for (const command of commandsIn(source)) if (mentions(command)) found.push({ command, kind: 'hook', where: `${m[2]} session ${m[1]}` });
58
+ }
59
+ }
60
+ return found;
61
+ }
62
+
63
+ /**
64
+ * The command as it can be run with no effect on this machine's live state (#1255 review P1: run as recorded, a relay's
65
+ * gate settled its queued send and relay-inbound overwrote its turn): its program and verb exactly as recorded, every
66
+ * data path after them (a folder, a binding; never a script an interpreter runs) a path that does not exist inside
67
+ * `root`, and no leading `env NAME=…` (which would name the live
68
+ * home again). Run under `root`'s own HOME and supercode homes, it reaches no live daemon, relay, binding or mailbox:
69
+ * what it shows is whether its program runs that verb at all.
70
+ */
71
+ export function isolated(command, root) {
72
+ const words = command.match(/'(?:[^']|'\\'')*'|"(?:\\.|[^"\\])*"|\S+/g) ?? [];
73
+ const bare = (word) => word.replace(/^'(.*)'$/s, '$1').replace(/^"(.*)"$/s, '$1');
74
+ let at = 0;
75
+ if (bare(words[0] ?? '') === 'env') { at = 1; while (at < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(bare(words[at]))) at += 1; }
76
+ const [program, ...rest] = words.slice(at);
77
+ if (!program) return null;
78
+ const quote = (word) => `'${word.replace(/'/g, `'\\''`)}'`;
79
+ // a script an interpreter runs (`node …/guard.mjs`) is what runs, kept; a data path (a relay's folder, a binding) is not
80
+ const data = (word) => word.startsWith('/') && !/\.(?:m?js|cjs)$/.test(word);
81
+ return [quote(bare(program)), ...rest.map((word, index) => (data(bare(word)) ? quote(join(root, 'absent', String(index))) : quote(bare(word))))].join(' ');
82
+ }
83
+
84
+ /** The environment a probe runs in: `root`'s own homes, nothing of this machine's. */
85
+ function isolatedEnv(root) {
86
+ for (const dir of ['home', 'supercode', 'xdg', 'tmp']) mkdirSync(join(root, dir), { recursive: true });
87
+ return { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: join(root, 'home'), SUPERCODE_HOME: join(root, 'supercode'), XDG_CONFIG_HOME: join(root, 'xdg'), TMPDIR: join(root, 'tmp'), CODEX_HOME: join(root, 'home', '.codex'), CLAUDE_CONFIG_DIR: join(root, 'home', '.claude') };
88
+ }
89
+
90
+ /** Run `command` as its consumer would, isolated (above); `{ code, stdout, stderr }`. */
91
+ function attempt(command, { mcp, root }) {
92
+ return new Promise((done) => {
93
+ const safe = isolated(command, root);
94
+ if (!safe) { done({ code: 127, stdout: '', stderr: 'no program in the command' }); return; }
95
+ const child = spawn('/bin/sh', ['-c', safe], { stdio: ['pipe', 'pipe', 'pipe'], env: isolatedEnv(root), cwd: root });
96
+ let stdout = '', stderr = '', settled = false;
97
+ const finish = (answer) => { if (settled) return; settled = true; clearTimeout(timer); try { child.kill('SIGKILL'); } catch { /* gone */ } done(answer); };
98
+ const timer = setTimeout(() => finish({ code: null, stdout, stderr: `${stderr}\n(no answer within 15 s)` }), 15_000);
99
+ child.on('error', (error) => finish({ code: 127, stdout, stderr: error.message }));
100
+ child.stdout.on('data', (data) => { stdout += data; if (mcp && /"result"/.test(stdout)) finish({ code: 0, stdout, stderr }); });
101
+ child.stderr.on('data', (data) => { stderr += data; });
102
+ child.on('close', (code) => finish({ code, stdout, stderr }));
103
+ child.stdin.end(mcp ? `${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'supercode-health', version: '1' } } })}\n` : '{}');
104
+ });
105
+ }
106
+
107
+ const cannotRun = ({ code, stderr }) => code === null || code === 126 || code === 127
108
+ || /unrecognized subcommand|unexpected argument|command not found|No such file|Cannot find module|Exec format/i.test(stderr);
109
+
110
+ /**
111
+ * The recorded commands that cannot run, one per shape (program and verb):
112
+ * `[{ shape, command, kind, where: [..], count, code, error }]`. `commands` (for a reading without the machine's) are
113
+ * `recordedCommands()`'s.
114
+ */
115
+ export async function brokenRecorded({ commands = null } = {}) {
116
+ const all = commands ?? (await recordedCommands());
117
+ const groups = new Map();
118
+ for (const entry of all) {
119
+ const key = `${entry.kind} ${shape(entry.command) ?? entry.command}`;
120
+ const group = groups.get(key) ?? { key, ...entry, where: [], count: 0 };
121
+ group.where.push(entry.where);
122
+ group.count += 1;
123
+ groups.set(key, group);
124
+ }
125
+ const broken = [];
126
+ const root = mkdtempSync(join(tmpdir(), 'supercode-recorded-'));
127
+ try {
128
+ await Promise.all([...groups.values()].map(async (group) => {
129
+ const answer = await attempt(group.command, { mcp: group.kind === 'mcp', root });
130
+ const failed = group.kind === 'mcp' ? !/"result"/.test(answer.stdout) : cannotRun(answer);
131
+ if (failed) broken.push({ shape: group.key, command: group.command.slice(0, 300), kind: group.kind, where: [...new Set(group.where)].slice(0, 10), count: group.count, code: answer.code, error: answer.stderr.trim().split('\n')[0]?.slice(0, 200) ?? '' });
132
+ }));
133
+ } finally { rmSync(root, { recursive: true, force: true }); }
134
+ return broken.sort((a, b) => b.count - a.count);
135
+ }
136
+
package/lib/sample.mjs CHANGED
@@ -8,6 +8,7 @@ import { rendererOrigins, originOf, localPort } from './chrome.mjs';
8
8
  import { expiredFailures, failureSummary, keyFor, Ledger, runningRelease, sayOnChanges, slowFailures } from './failures.mjs';
9
9
  import { healthDir } from './paths.mjs';
10
10
  import { pinnedHomes, staleSupercode } from './drift.mjs';
11
+ import { brokenRecorded } from './recorded.mjs';
11
12
  import { join } from 'node:path';
12
13
 
13
14
  const MB = 1024 * 1024;
@@ -399,6 +400,14 @@ export class Sampler {
399
400
  slow.readings.pinned = pinned;
400
401
  for (const p of pinned) slow.observations.push({ key: `release.pinned:${p.home}`, family: 'pinned', def: alarm('release.pinned'), value: 1, met: { warn: true, crit: false, clears: false }, summary: `board home ${p.home} is ${p.why} (orchestrator ${p.version ?? 'unknown'})`, detail: p });
401
402
  });
403
+ if (on.has('recorded')) await attempt('recorded', async () => {
404
+ const broken = await brokenRecorded();
405
+ slow.families.add('recorded');
406
+ // every broken shape this pass found; one that runs again is cleared
407
+ slow.observations.push({ enumerated: 'recorded', family: 'recorded', by: 'suffix', entities: new Set(broken.map((b) => b.shape)) });
408
+ slow.readings.recorded = broken;
409
+ for (const b of broken) slow.observations.push({ key: `recorded.broken:${b.shape}`, family: 'recorded', def: alarm('recorded.broken'), value: b.count, met: { warn: true, crit: true, clears: false }, summary: `${b.count} recorded command(s) cannot run (${b.where.slice(0, 3).join(', ')}): ${b.error}; ${b.command.slice(0, 120)}`, detail: b });
410
+ });
402
411
  if (on.has('services')) await attempt('services', async () => {
403
412
  const list = cfg.services ?? [];
404
413
  const jobs = await this.reader.services(list.map((s) => s.label));
@@ -5,6 +5,7 @@ export const DEFAULTS = {
5
5
  slowIntervalSec: 60, // disk space, services, the connector, tabs
6
6
  footprintTopEverySec: 300, // macOS: every user's largest footprints through top (about 2 s of top per run)
7
7
  repeatMin: 60, // a raised alarm mails again this often (netdata's repeat cap)
8
+ quietMin: 15, // what changed is mailed at most this often (a new crit at once): a flapping alarm is no stream
8
9
  unknownMin: 15, // a reading unknown this long (raised or not) is mailed to the maintainers, and again each repeatMin
9
10
  historyHours: 24,
10
11
  daemons: {
@@ -36,6 +37,8 @@ export const DEFAULTS = {
36
37
  'release.drift': { forSec: 300 },
37
38
  // a board home its record holds to another install's orchestrator: said once it has stayed so 5 min
38
39
  'release.pinned': { forSec: 300 },
40
+ // a recorded command that cannot run fails its harness on every use (a hook on every tool call): said at once
41
+ 'recorded.broken': { forSec: 0 },
39
42
  'supercode.connector': { forSec: 60 },
40
43
  'supercode.launch': { stuckMin: 5, failedWithinMin: 30 },
41
44
  // one failure that keeps happening (the same command or door with the same outcome, the same mail expiry) in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-health",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
@@ -9,7 +9,8 @@
9
9
  "./bin": "./bin/health.mjs",
10
10
  "./format": "./lib/format.mjs",
11
11
  "./paths": "./lib/paths.mjs",
12
- "./drift": "./lib/drift.mjs"
12
+ "./drift": "./lib/drift.mjs",
13
+ "./recorded": "./lib/recorded.mjs"
13
14
  },
14
15
  "bin": {
15
16
  "supercode-health": "./bin/health.mjs"