@tpsdev-ai/flair 0.53.0 → 0.54.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.
Files changed (97) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1791 -15648
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1665 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +931 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1628 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/engine-version.js +12 -4
  43. package/dist/fabric-npm-install.js +87 -0
  44. package/dist/fabric-upgrade.js +30 -15
  45. package/dist/federation-verify.js +498 -0
  46. package/dist/fleet-verify.js +144 -21
  47. package/dist/install/clients.js +167 -0
  48. package/dist/lib/auth-resolve.js +76 -1
  49. package/dist/lib/daemon-liveness.js +131 -2
  50. package/dist/lib/doctor-config-path.js +61 -0
  51. package/dist/lib/doctor-federation-driver.js +189 -0
  52. package/dist/lib/doctor-run.js +40 -0
  53. package/dist/lib/entity-vocab-cli.js +3 -3
  54. package/dist/lib/federation-pair-identity.js +47 -0
  55. package/dist/lib/launchd-repair.js +5 -4
  56. package/dist/lib/npm-registry.js +578 -0
  57. package/dist/lib/ops-api-bind.js +115 -0
  58. package/dist/lib/owned-pins.js +219 -0
  59. package/dist/lib/uninstall-purge.js +218 -0
  60. package/dist/rem/restore.js +8 -10
  61. package/dist/resources/AgentReadPosition.js +74 -0
  62. package/dist/resources/Federation.js +8 -2
  63. package/dist/resources/Memory.js +4 -3
  64. package/dist/resources/MemoryBootstrap.js +41 -25
  65. package/dist/resources/MemoryCandidate.js +5 -6
  66. package/dist/resources/OrgEventCatchup.js +126 -47
  67. package/dist/resources/agent-read-position-lib.js +83 -0
  68. package/dist/resources/agent-read-position.js +120 -0
  69. package/dist/resources/embeddings-boot.js +32 -0
  70. package/dist/resources/federation-peer-liveness.js +73 -0
  71. package/dist/resources/health.js +68 -19
  72. package/dist/resources/mcp-tools.js +48 -279
  73. package/dist/resources/memory-visibility.js +3 -3
  74. package/dist/resources/migration-boot.js +59 -18
  75. package/dist/resources/migrations/embedding-stamp.js +20 -1
  76. package/dist/resources/migrations/recheck.js +43 -0
  77. package/dist/resources/migrations/runner.js +6 -1
  78. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  79. package/dist/resources/migrations/visibility-backfill.js +2 -2
  80. package/dist/resources/org-event-catchup-lib.js +47 -0
  81. package/dist/resources/record-owner-guard.js +1 -0
  82. package/dist/resources/tool-descriptors/index.js +669 -0
  83. package/dist/stamp-migration-verify.js +163 -0
  84. package/dist/stamp-outstanding.js +144 -0
  85. package/dist/version-check.js +29 -8
  86. package/docs/api-reference.md +4 -2
  87. package/docs/deploying-on-fabric.md +11 -10
  88. package/docs/deployment.md +3 -1
  89. package/docs/federation.md +19 -0
  90. package/docs/hosted-on-fabric.md +3 -3
  91. package/docs/quickstart.md +2 -1
  92. package/docs/releasing.md +20 -6
  93. package/docs/spoke-bringup.md +10 -5
  94. package/docs/standalone-local.md +3 -1
  95. package/docs/upgrade.md +25 -6
  96. package/package.json +4 -4
  97. package/schemas/agent.graphql +15 -0
@@ -0,0 +1,931 @@
1
+ import { resolveAdminUser } from "../lib/auth-resolve.js";
2
+ import { opsApiBindFinding } from "../lib/ops-api-bind.js";
3
+ import * as render from "../render.js";
4
+ import { checkVersion, formatVersionNudge, FLAIR_PKG_NAME } from "../version-check.js";
5
+ import { resolveRegistryNotice } from "../lib/npm-registry.js";
6
+ let cli;
7
+ /** Bind the cli-locals this module depends on. */
8
+ export function bindCli(fns) {
9
+ cli = fns;
10
+ }
11
+ function fetchHealthDetail(...args) {
12
+ return cli.fetchHealthDetail(...args);
13
+ }
14
+ function humanBytes(...args) {
15
+ return cli.humanBytes(...args);
16
+ }
17
+ function relativeTime(...args) {
18
+ return cli.relativeTime(...args);
19
+ }
20
+ function resolveSigningAgentId(...args) {
21
+ return cli.resolveSigningAgentId(...args);
22
+ }
23
+ function sortSoulKeyEntries(...args) {
24
+ return cli.sortSoulKeyEntries(...args);
25
+ }
26
+ function defaultDataDir(...args) {
27
+ return cli.defaultDataDir(...args);
28
+ }
29
+ function readHarperConfig(...args) {
30
+ return cli.readHarperConfig(...args);
31
+ }
32
+ /**
33
+ * Local install-health warnings that `flair doctor` also checks (flair#852).
34
+ *
35
+ * The ops-API bind is read from the same harper-config.yaml doctor reads.
36
+ * Only judged for a loopback target: the LOCAL install's config says nothing
37
+ * about a remote instance, and warning about the wrong machine is its own lie.
38
+ */
39
+ function localOpsApiWarnings(baseUrl) {
40
+ const out = [];
41
+ if (!isLocalhostUrl(baseUrl))
42
+ return out;
43
+ const finding = opsApiBindFinding(readHarperConfig(defaultDataDir()));
44
+ if (finding?.allInterfaces) {
45
+ // Name where the full explanation + fix live, matching the status warning
46
+ // convention (docs/quickstart.md: a warning names the command to run).
47
+ out.push({ level: "warn", message: `${finding.message} — run \`flair doctor\` for the fix` });
48
+ }
49
+ return out;
50
+ }
51
+ function federationPeerCountParts(peers) {
52
+ const connected = Number(peers?.connected ?? 0);
53
+ const disconnected = Number(peers?.disconnected ?? 0);
54
+ const revoked = Number(peers?.revoked ?? 0);
55
+ const unknown = Number(peers?.unknown ?? 0);
56
+ const parts = [
57
+ render.wrap(connected > 0 ? render.c.green : render.c.dim, `${connected} connected`),
58
+ render.wrap(disconnected > 0 ? render.c.yellow : render.c.dim, `${disconnected} down`),
59
+ render.wrap(revoked > 0 ? render.c.red : render.c.dim, `${revoked} revoked`),
60
+ ];
61
+ if (unknown > 0) {
62
+ parts.push(render.wrap(render.c.dim, `${unknown} unknown`));
63
+ }
64
+ return parts;
65
+ }
66
+ function oauthSummaryLines(o) {
67
+ const clients = Number(o?.clients ?? 0);
68
+ const idps = Number(o?.idpConfigs ?? 0);
69
+ const tokens = Number(o?.activeTokens ?? 0);
70
+ return [
71
+ "\nOAuth:",
72
+ ` Clients: ${clients} IdPs: ${idps} Active tokens: ${tokens}`,
73
+ ];
74
+ }
75
+ function oauthDetailLines(o) {
76
+ const clients = Number(o?.clients ?? 0);
77
+ const idps = Number(o?.idpConfigs ?? 0);
78
+ const tokens = Number(o?.activeTokens ?? 0);
79
+ const out = [
80
+ "OAuth:",
81
+ ` Clients: ${clients}`,
82
+ ` IdP configs: ${idps}`,
83
+ ` Active tokens: ${tokens}`,
84
+ ];
85
+ if (Array.isArray(o?.clientList) && o.clientList.length > 0) {
86
+ out.push("", " Clients:");
87
+ for (const c of o.clientList) {
88
+ const id = String(c?.id ?? "");
89
+ const name = String(c?.name ?? "—");
90
+ const registeredBy = String(c?.registeredBy ?? "—");
91
+ const createdAt = String(c?.createdAt ?? "—");
92
+ out.push(` ${id} ${name} ${registeredBy} ${createdAt}`);
93
+ }
94
+ }
95
+ if (Array.isArray(o?.idpList) && o.idpList.length > 0) {
96
+ out.push("", " IdPs:");
97
+ for (const i of o.idpList) {
98
+ const id = String(i?.id ?? "");
99
+ const name = String(i?.name ?? "—");
100
+ const issuer = String(i?.issuer ?? "—");
101
+ out.push(` ${id} ${name} ${issuer}`);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ // Common localhost ports a running Flair daemon might be on. Used by
107
+ // discoverLocalFlairPort when the configured URL is unreachable, to detect
108
+ // config-vs-daemon port drift. Order is ad-hoc — first hit wins.
109
+ //
110
+ // 9926: original default (long-running early installs predate the bump)
111
+ // 19926: current default (DEFAULT_PORT)
112
+ // 19925: ops-anvil VM secondary
113
+ const LOCAL_FLAIR_PROBE_PORTS = [9926, 19926, 19925];
114
+ export function isLocalhostUrl(url) {
115
+ try {
116
+ const u = new URL(url);
117
+ // URL.hostname keeps brackets around IPv6 (e.g. "[::1]") so match both forms.
118
+ return (u.hostname === "127.0.0.1" ||
119
+ u.hostname === "localhost" ||
120
+ u.hostname === "::1" ||
121
+ u.hostname === "[::1]");
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ /**
128
+ * When a configured-localhost URL is unreachable, probe a small candidate-port
129
+ * set to detect a daemon listening on a different port (config drift). Returns
130
+ * the first responsive port, or null if none. Excludes the original port from
131
+ * the probe set so we don't repeat the failed call.
132
+ *
133
+ * Runs sequentially with a 500ms timeout per probe — typical 3-port sweep
134
+ * completes in <1.5s on a healthy box, faster on an unhealthy one.
135
+ */
136
+ export async function discoverLocalFlairPort(originalUrl) {
137
+ if (!isLocalhostUrl(originalUrl))
138
+ return null;
139
+ let originalPort = null;
140
+ try {
141
+ originalPort = Number(new URL(originalUrl).port) || null;
142
+ }
143
+ catch { /* ignore */ }
144
+ for (const port of LOCAL_FLAIR_PROBE_PORTS) {
145
+ if (port === originalPort)
146
+ continue;
147
+ try {
148
+ const res = await fetch(`http://127.0.0.1:${port}/Health`, { signal: AbortSignal.timeout(500) });
149
+ // Treat 401 (auth required) the same as 200 — the daemon is alive,
150
+ // we just can't see /Health without admin auth. The point is to detect
151
+ // "something is listening" not "we have full access".
152
+ if (res.ok || res.status === 401)
153
+ return port;
154
+ }
155
+ catch { /* port not listening, try next */ }
156
+ }
157
+ return null;
158
+ }
159
+ export function register(program) {
160
+ const __pkgVersion = cli.__pkgVersion;
161
+ // Renders OAuth status lines from non-secret metadata. /HealthDetail never
162
+ // returns clientSecret — only counts and identifying fields (id, name,
163
+ // registeredBy, createdAt, issuer). Inputs are coerced to scalar primitives
164
+ // before formatting to keep the display values clearly separated from the
165
+ // source record.
166
+ /** Colored `N connected · N down · N revoked` (+ unknown when > 0). */
167
+ const statusCmd = program
168
+ .command("status")
169
+ .description("Show Flair instance status, memory stats, and agent info")
170
+ .option("--port <port>", "Harper HTTP port")
171
+ .option("--url <url>", "Flair base URL (overrides --port)")
172
+ .option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET; alias for --url)")
173
+ .option("--json", "Output as JSON")
174
+ .option("--agent <id>", "Agent ID for authenticated detail (or set FLAIR_AGENT_ID)")
175
+ .action(async (opts) => {
176
+ const { agentId: statusAgentId, source: statusSource } = resolveSigningAgentId(opts, "status");
177
+ const { healthy, baseUrl, healthData } = await fetchHealthDetail(opts, statusAgentId, statusSource);
178
+ // Local ops-API bind (flair#852). `flair doctor` reads this from the same
179
+ // harper-config.yaml; the two commands must not disagree. A bare ops port
180
+ // is Harper's all-interfaces default, so reporting "all checks passing"
181
+ // while it is exposed is a lying green.
182
+ const localWarnings = localOpsApiWarnings(baseUrl);
183
+ // When unreachable on a localhost URL, probe candidate ports to detect
184
+ // config-vs-daemon port drift. Surface the actually-listening
185
+ // port with a fix recipe — better UX than just "unreachable."
186
+ let discoveredPort = null;
187
+ if (!healthy && isLocalhostUrl(baseUrl)) {
188
+ discoveredPort = await discoverLocalFlairPort(baseUrl);
189
+ }
190
+ // Version-behind check (flair#587) — offline-tolerant + cached, so this
191
+ // never fails `status` when the registry is unreachable, and costs no
192
+ // network round trip on the common up-to-date path. When a cached answer
193
+ // would print a nudge it spends one short-timeout refetch so the printed
194
+ // fact is current (flair#1341). Independent of Harper health; runs either way.
195
+ const versionCheckResult = await checkVersion(__pkgVersion);
196
+ const versionNudge = formatVersionNudge(versionCheckResult);
197
+ // flair#1692: name the registry (and where it came from) on every status
198
+ // check, so a redirected mirror is never silent — including when the
199
+ // version answer came from cache and no network request was made.
200
+ const registryNotice = await resolveRegistryNotice(FLAIR_PKG_NAME);
201
+ if (opts.json) {
202
+ const out = { healthy, url: baseUrl, flairVersion: __pkgVersion, ...healthData };
203
+ if (localWarnings.length > 0) {
204
+ out.warnings = [
205
+ ...localWarnings,
206
+ ...(Array.isArray(healthData?.warnings) ? healthData.warnings : []),
207
+ ];
208
+ }
209
+ if (discoveredPort != null)
210
+ out.discoveredPort = discoveredPort;
211
+ if (versionCheckResult.latest)
212
+ out.latestVersion = versionCheckResult.latest;
213
+ if (registryNotice.line)
214
+ out.registry = registryNotice.line;
215
+ if (registryNotice.error)
216
+ out.registryError = registryNotice.error;
217
+ console.log(JSON.stringify(out, null, 2));
218
+ if (!healthy)
219
+ process.exit(1);
220
+ return;
221
+ }
222
+ if (!healthy) {
223
+ console.log(`Flair v${__pkgVersion} — 🔴 unreachable`);
224
+ console.log(` URL: ${baseUrl}`);
225
+ if (registryNotice.line)
226
+ console.log(` ${registryNotice.line}`);
227
+ if (registryNotice.error)
228
+ console.log(` ⚠ ${registryNotice.error}`);
229
+ if (discoveredPort != null) {
230
+ const altUrl = `http://127.0.0.1:${discoveredPort}`;
231
+ console.log(`\n ⚠ Found a Flair daemon listening on port ${discoveredPort} (URL: ${altUrl}).`);
232
+ console.log(` Your config points at ${baseUrl} — drift detected.`);
233
+ console.log(`\n Quick fix: FLAIR_URL=${altUrl} flair status`);
234
+ console.log(` Permanent fix: edit ~/.flair/config.yaml to set port: ${discoveredPort}`);
235
+ console.log(` Or: flair doctor (when port-drift detection lands there)`);
236
+ }
237
+ else {
238
+ console.log(`\n Run: flair start or flair doctor`);
239
+ }
240
+ if (versionNudge) {
241
+ const color = versionNudge.severity === "red" ? render.c.red : render.c.yellow;
242
+ console.log(`\n ${render.wrap(color, "⚠")} ${render.wrap(color, versionNudge.message)}`);
243
+ }
244
+ process.exit(1);
245
+ }
246
+ const uptimeSec = healthData?.uptimeSeconds;
247
+ let uptimeStr = "";
248
+ if (uptimeSec != null) {
249
+ const d = Math.floor(uptimeSec / 86400);
250
+ const h = Math.floor((uptimeSec % 86400) / 3600);
251
+ const m = Math.floor((uptimeSec % 3600) / 60);
252
+ uptimeStr = d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
253
+ }
254
+ const pid = healthData?.pid ?? "";
255
+ const agents = healthData?.agents;
256
+ const memories = healthData?.memories;
257
+ const warnings = [
258
+ ...localWarnings,
259
+ ...(Array.isArray(healthData?.warnings) ? healthData.warnings : []),
260
+ ];
261
+ // Scope warnings to the filtered agent if --agent is set
262
+ const scopedWarnings = opts.agent && healthData?.agents?.perAgent
263
+ ? warnings.filter((w) => {
264
+ // Hash-fallback warnings contain agent-specific counts
265
+ if (w.message.includes("hash-fallback")) {
266
+ const match = w.message.match(/\b(\d+)\/(\d+) \((\d+)%\)/);
267
+ if (match) {
268
+ const hashCount = parseInt(match[1]);
269
+ const totalCount = parseInt(match[2]);
270
+ const agentRow = healthData.agents.perAgent.find((r) => r.id === opts.agent);
271
+ if (agentRow && agentRow.hashFallback === hashCount && agentRow.memoryCount === totalCount) {
272
+ return true;
273
+ }
274
+ return false;
275
+ }
276
+ }
277
+ // Mixed-model / outstanding stamp-migration warnings are fleet-wide; keep them
278
+ if (w.message.includes("multiple embedding models"))
279
+ return true;
280
+ if (w.message.includes("embedding-stamp") || w.message.includes("duplicate detection is inactive"))
281
+ return true;
282
+ // Federation warnings are fleet-wide; keep them
283
+ if (w.message.includes("federation"))
284
+ return true;
285
+ // REM warnings are fleet-wide; keep them
286
+ if (w.message.includes("REM") || w.message.includes("nightly"))
287
+ return true;
288
+ // Ops-API bind is a host fact, not per-agent; keep it
289
+ if (w.message.includes("Ops API bound"))
290
+ return true;
291
+ // Default: keep fleet-wide warnings
292
+ return !w.message.includes(opts.agent);
293
+ })
294
+ : warnings;
295
+ const hasWarn = scopedWarnings.some((w) => w.level === "warn");
296
+ const headerIcon = hasWarn ? render.icons.warn : render.icons.ok;
297
+ const versionStr = render.wrap(render.c.bold, `Flair v${__pkgVersion}`);
298
+ const runStatus = `${headerIcon} ${render.wrap(render.c.green, "running")}`;
299
+ const pidPart = pid ? render.wrap(render.c.dim, `PID ${pid}`) : "";
300
+ const uptimePart = uptimeStr ? render.wrap(render.c.dim, `uptime ${uptimeStr}`) : "";
301
+ const metaParts = [pidPart, uptimePart].filter(Boolean).join(render.wrap(render.c.dim, " · "));
302
+ console.log(`${versionStr} ${render.wrap(render.c.dim, "—")} ${runStatus}${metaParts ? ` ${metaParts}` : ""}`);
303
+ console.log(render.kv("URL", baseUrl));
304
+ if (registryNotice.line)
305
+ console.log(render.kv("Registry", registryNotice.line.replace(/^registry:\s*/, "")));
306
+ if (registryNotice.error)
307
+ console.log(` ${render.icons.warn} ${render.wrap(render.c.yellow, registryNotice.error)}`);
308
+ if (versionNudge) {
309
+ const color = versionNudge.severity === "red" ? render.c.red : render.c.yellow;
310
+ console.log(`\n ${render.wrap(color, "⚠")} ${render.wrap(color, versionNudge.message)}`);
311
+ }
312
+ if (scopedWarnings.length > 0) {
313
+ console.log(`\n${render.wrap(render.c.bold, "Warnings")} ${render.wrap(render.c.dim, `(${scopedWarnings.length})`)}`);
314
+ for (const w of scopedWarnings) {
315
+ const icon = w.level === "warn" ? render.icons.warn : render.icons.info;
316
+ console.log(` ${icon} ${w.message}`);
317
+ }
318
+ }
319
+ if (memories) {
320
+ console.log(`\n${render.wrap(render.c.bold, "Memory")}`);
321
+ const embStr = memories.withEmbeddings > 0 ? `${memories.withEmbeddings} embedded` : "";
322
+ const hashStr = memories.hashFallback > 0 ? `${memories.hashFallback} hash` : "";
323
+ const detail = [embStr, hashStr].filter(Boolean).join(", ");
324
+ console.log(render.kv("Total", `${render.wrap(render.c.bold, String(memories.total))}${detail ? ` ${render.wrap(render.c.dim, `(${detail})`)}` : ""}`));
325
+ if (memories.modelCounts && typeof memories.modelCounts === "object") {
326
+ const entries = Object.entries(memories.modelCounts)
327
+ .filter(([, n]) => n > 0)
328
+ .sort((a, b) => b[1] - a[1]);
329
+ if (entries.length > 0) {
330
+ const formatted = entries.map(([k, n]) => `${render.wrap(render.c.cyan, k)}:${n}`).join(render.wrap(render.c.dim, ", "));
331
+ console.log(render.kv("Embeddings", formatted));
332
+ }
333
+ }
334
+ if (memories.byDurability) {
335
+ const d = memories.byDurability;
336
+ const parts = [
337
+ `${render.wrap(render.c.magenta, "permanent")}:${d.permanent ?? 0}`,
338
+ `${render.wrap(render.c.blue, "persistent")}:${d.persistent ?? 0}`,
339
+ `${render.wrap(render.c.cyan, "standard")}:${d.standard ?? 0}`,
340
+ `${render.wrap(render.c.gray, "ephemeral")}:${d.ephemeral ?? 0}`,
341
+ ];
342
+ console.log(render.kv("Durability", parts.join(render.wrap(render.c.dim, " · "))));
343
+ }
344
+ if (typeof memories.archived === "number")
345
+ console.log(render.kv("Archived", String(memories.archived)));
346
+ if (typeof memories.expired === "number" && memories.expired > 0) {
347
+ console.log(render.kv("Expired", `${render.wrap(render.c.yellow, String(memories.expired))}`));
348
+ }
349
+ if (healthData?.lastWrite)
350
+ console.log(render.kv("Last write", render.relativeTime(healthData.lastWrite)));
351
+ }
352
+ if (agents && agents.count > 0) {
353
+ console.log(`\n${render.wrap(render.c.bold, "Agents")}`);
354
+ const nameStr = agents.names?.length > 0 ? ` ${render.wrap(render.c.dim, "—")} ${agents.names.join(render.wrap(render.c.dim, ", "))}` : "";
355
+ console.log(render.kv("Total", `${render.wrap(render.c.bold, String(agents.count))}${nameStr}`));
356
+ if (agents.count > 1 && Array.isArray(agents.perAgent) && agents.perAgent.length > 0) {
357
+ const hasDeep = agents.perAgent.some((r) => typeof r.hashFallback === "number" || typeof r.writes24h === "number");
358
+ const cols = hasDeep
359
+ ? [
360
+ { label: "id", key: "id" },
361
+ { label: "memories", key: "memoryCount", align: "right" },
362
+ { label: "hash_fb", key: "hashFallback", align: "right", format: (v) => (typeof v === "number" ? String(v) : "—") },
363
+ { label: "24h", key: "writes24h", align: "right", format: (v) => (typeof v === "number" ? String(v) : "—") },
364
+ { label: "last_write", key: "lastWriteAt", format: (v) => render.relativeTime(v) },
365
+ ]
366
+ : [
367
+ { label: "id", key: "id" },
368
+ { label: "memories", key: "memoryCount", align: "right" },
369
+ { label: "last_write", key: "lastWriteAt", format: (v) => render.relativeTime(v) },
370
+ ];
371
+ console.log(render.table(cols, agents.perAgent));
372
+ }
373
+ }
374
+ if (healthData?.relationships) {
375
+ const r = healthData.relationships;
376
+ console.log(`\n${render.wrap(render.c.bold, "Relationships")}`);
377
+ console.log(render.kv("Total", `${r.total} ${render.wrap(render.c.dim, `(${r.active} active)`)}`));
378
+ }
379
+ if (healthData?.soul && healthData.soul.total > 0) {
380
+ const s = healthData.soul;
381
+ console.log(`\n${render.wrap(render.c.bold, "Soul")}`);
382
+ const entries = sortSoulKeyEntries(s.byKey ?? {});
383
+ const parts = entries.map(([k, n]) => `${render.wrap(render.c.cyan, k)}:${n}`);
384
+ const suffix = parts.length > 0
385
+ ? ` ${render.wrap(render.c.dim, "—")} ${parts.join(render.wrap(render.c.dim, " · "))}`
386
+ : "";
387
+ console.log(render.kv("Entries", `${render.wrap(render.c.bold, String(s.total))}${suffix}`));
388
+ }
389
+ else if (typeof healthData?.soulEntries === "number" && healthData.soulEntries > 0) {
390
+ console.log(`\n${render.wrap(render.c.bold, "Soul")}`);
391
+ console.log(render.kv("Entries", String(healthData.soulEntries)));
392
+ }
393
+ if (healthData?.rem) {
394
+ const r = healthData.rem;
395
+ console.log(`\n${render.wrap(render.c.bold, "REM")}`);
396
+ if (r.lastLightAt)
397
+ console.log(render.kv("Last light", render.relativeTime(r.lastLightAt)));
398
+ if (r.lastRapidAt)
399
+ console.log(render.kv("Last rapid", render.relativeTime(r.lastRapidAt)));
400
+ if (r.lastRestorativeAt)
401
+ console.log(render.kv("Last restorative", render.relativeTime(r.lastRestorativeAt)));
402
+ const nightlyTxt = r.nightlyEnabled === true
403
+ ? render.wrap(render.c.green, "enabled")
404
+ : r.nightlyEnabled === false
405
+ ? render.wrap(render.c.dim, "disabled")
406
+ : render.wrap(render.c.dim, "unknown");
407
+ console.log(render.kv("Nightly", nightlyTxt));
408
+ if (r.nightlyEnabled && r.lastNightlyAt)
409
+ console.log(render.kv("Last nightly", render.relativeTime(r.lastNightlyAt)));
410
+ if (typeof r.pendingCandidates === "number" && r.pendingCandidates > 0) {
411
+ console.log(render.kv("Pending candidates", render.wrap(render.c.yellow, String(r.pendingCandidates))));
412
+ }
413
+ }
414
+ if (healthData?.federation) {
415
+ const f = healthData.federation;
416
+ console.log(`\n${render.wrap(render.c.bold, "Federation")}`);
417
+ if (f.instance) {
418
+ const statusColor = f.instance.status === "active" ? render.c.green : render.c.yellow;
419
+ console.log(render.kv("Instance", `${f.instance.id} ${render.wrap(render.c.dim, "(")}${f.instance.role ?? "—"}${render.wrap(render.c.dim, ", ")}${render.wrap(statusColor, f.instance.status ?? "—")}${render.wrap(render.c.dim, ")")}`));
420
+ }
421
+ if (f.peers) {
422
+ const parts = federationPeerCountParts(f.peers);
423
+ console.log(render.kv("Peers", `${render.wrap(render.c.bold, String(f.peers.total))} ${render.wrap(render.c.dim, "—")} ${parts.join(render.wrap(render.c.dim, " · "))}`));
424
+ }
425
+ if (f.pendingTokens > 0)
426
+ console.log(render.kv("Pairing", `${render.wrap(render.c.yellow, String(f.pendingTokens))} unconsumed token(s)`));
427
+ }
428
+ else {
429
+ console.log(`\n${render.wrap(render.c.bold, "Federation")} ${render.wrap(render.c.dim, "not configured")}`);
430
+ }
431
+ if (healthData?.oauth) {
432
+ const lines = oauthSummaryLines(healthData.oauth);
433
+ // Tweak the "OAuth:" header to bold; downstream lines are aligned k/v which already look fine
434
+ for (const line of lines) {
435
+ if (line.trim() === "OAuth:")
436
+ console.log(`\n${render.wrap(render.c.bold, "OAuth")}`);
437
+ else
438
+ console.log(line);
439
+ }
440
+ }
441
+ if (healthData?.bridges) {
442
+ const b = healthData.bridges;
443
+ console.log(`\n${render.wrap(render.c.bold, "Bridges")}`);
444
+ if (Array.isArray(b.installed) && b.installed.length > 0)
445
+ console.log(render.kv("Installed", b.installed.join(render.wrap(render.c.dim, ", "))));
446
+ if (b.lastImport)
447
+ console.log(render.kv("Last import", render.relativeTime(b.lastImport)));
448
+ if (b.lastExport)
449
+ console.log(render.kv("Last export", render.relativeTime(b.lastExport)));
450
+ }
451
+ else {
452
+ console.log(`\n${render.wrap(render.c.bold, "Bridges")} ${render.wrap(render.c.dim, "none installed")}`);
453
+ }
454
+ if (healthData?.disk) {
455
+ const d = healthData.disk;
456
+ console.log(`\n${render.wrap(render.c.bold, "Disk")}`);
457
+ console.log(render.kv("Data", `${render.wrap(render.c.dim, d.dataDir)} ${render.wrap(render.c.dim, "—")} ${render.wrap(render.c.bold, render.humanBytes(d.dataBytes ?? 0))}`));
458
+ console.log(render.kv("Snapshots", `${render.wrap(render.c.dim, d.snapshotDir)} ${render.wrap(render.c.dim, "—")} ${render.wrap(render.c.bold, render.humanBytes(d.snapshotBytes ?? 0))}`));
459
+ }
460
+ console.log("");
461
+ if (scopedWarnings.length > 0) {
462
+ console.log(` ${render.icons.warn} ${render.wrap(render.c.yellow, `${scopedWarnings.length} warning${scopedWarnings.length === 1 ? "" : "s"}`)}`);
463
+ }
464
+ else {
465
+ console.log(` ${render.icons.ok} ${render.wrap(render.c.green, "all checks passing")}`);
466
+ }
467
+ });
468
+ statusCmd
469
+ .command("rem")
470
+ .description("Show REM (memory hygiene) subsystem status")
471
+ .action(async function () {
472
+ const opts = this.optsWithGlobals();
473
+ const { healthy, healthData } = await fetchHealthDetail(opts);
474
+ const mode = render.resolveOutputMode(opts);
475
+ if (mode === "json") {
476
+ console.log(render.asJSON({ healthy, rem: healthData?.rem ?? null }));
477
+ if (!healthy)
478
+ process.exit(1);
479
+ return;
480
+ }
481
+ if (!healthy) {
482
+ console.log(`${render.icons.error} ${render.wrap(render.c.red, "unreachable")}`);
483
+ process.exit(1);
484
+ }
485
+ const r = healthData?.rem;
486
+ if (!r) {
487
+ console.log(`${render.wrap(render.c.bold, "REM")} ${render.wrap(render.c.dim, "not configured (no log entries or platform timers found)")}`);
488
+ return;
489
+ }
490
+ console.log(render.wrap(render.c.bold, "REM"));
491
+ console.log(render.kv("Last light", render.relativeTime(r.lastLightAt), 18));
492
+ console.log(render.kv("Last rapid", render.relativeTime(r.lastRapidAt), 18));
493
+ console.log(render.kv("Last restorative", render.relativeTime(r.lastRestorativeAt), 18));
494
+ const nightlyTxt = r.nightlyEnabled === true
495
+ ? render.wrap(render.c.green, "enabled")
496
+ : r.nightlyEnabled === false
497
+ ? render.wrap(render.c.dim, "disabled")
498
+ : render.wrap(render.c.dim, "unknown");
499
+ console.log(render.kv("Nightly", nightlyTxt, 18));
500
+ if (r.lastNightlyAt) {
501
+ console.log(render.kv("Last nightly", `${render.relativeTime(r.lastNightlyAt)} ${render.wrap(render.c.dim, `(${r.lastNightlyAt})`)}`, 18));
502
+ }
503
+ if (typeof r.pendingCandidates === "number") {
504
+ const pendingColor = r.pendingCandidates > 0 ? render.c.yellow : render.c.dim;
505
+ console.log(render.kv("Pending candidates", render.wrap(pendingColor, String(r.pendingCandidates)), 18));
506
+ }
507
+ else {
508
+ console.log(render.kv("Pending candidates", render.wrap(render.c.dim, "— (schema not available)"), 18));
509
+ }
510
+ });
511
+ statusCmd
512
+ .command("federation")
513
+ .description("Show federation subsystem status")
514
+ .action(async function () {
515
+ const opts = this.optsWithGlobals();
516
+ const { healthy, healthData } = await fetchHealthDetail(opts);
517
+ const mode = render.resolveOutputMode(opts);
518
+ if (mode === "json") {
519
+ console.log(render.asJSON({ healthy, federation: healthData?.federation ?? null }));
520
+ if (!healthy)
521
+ process.exit(1);
522
+ return;
523
+ }
524
+ if (!healthy) {
525
+ console.log(`${render.icons.error} ${render.wrap(render.c.red, "unreachable")}`);
526
+ process.exit(1);
527
+ }
528
+ const f = healthData?.federation;
529
+ if (!f) {
530
+ console.log(`${render.wrap(render.c.bold, "Federation")} ${render.wrap(render.c.dim, "not configured")}`);
531
+ return;
532
+ }
533
+ console.log(render.wrap(render.c.bold, "Federation"));
534
+ if (f.instance) {
535
+ const statusColor = f.instance.status === "active" ? render.c.green : render.c.yellow;
536
+ console.log(render.kv("Instance", `${f.instance.id} ${render.wrap(render.c.dim, "(")}${f.instance.role ?? "—"}${render.wrap(render.c.dim, ", ")}${render.wrap(statusColor, f.instance.status ?? "—")}${render.wrap(render.c.dim, ")")}`));
537
+ }
538
+ else {
539
+ console.log(render.kv("Instance", render.wrap(render.c.dim, "—")));
540
+ }
541
+ if (f.peers) {
542
+ const parts = federationPeerCountParts(f.peers);
543
+ console.log(render.kv("Peers", `${render.wrap(render.c.bold, String(f.peers.total))} ${render.wrap(render.c.dim, "—")} ${parts.join(render.wrap(render.c.dim, " · "))}`));
544
+ }
545
+ if (typeof f.pendingTokens === "number" && f.pendingTokens > 0) {
546
+ console.log(render.kv("Pairing", `${render.wrap(render.c.yellow, String(f.pendingTokens))} unconsumed token(s)`));
547
+ }
548
+ if (Array.isArray(f.peerList) && f.peerList.length > 0) {
549
+ console.log();
550
+ const cols = [
551
+ { label: "peer", key: "id" },
552
+ { label: "role", key: "role", format: (v) => String(v ?? "—") },
553
+ {
554
+ label: "status",
555
+ key: "status",
556
+ format: (v) => {
557
+ const s = String(v ?? "—");
558
+ const color = s === "paired" || s === "connected" ? render.c.green : s === "revoked" ? render.c.red : render.c.yellow;
559
+ return render.wrap(color, s);
560
+ },
561
+ },
562
+ {
563
+ label: "liveness",
564
+ key: "liveness",
565
+ format: (v) => {
566
+ const s = String(v ?? "—");
567
+ const color = s === "connected" ? render.c.green : s === "disconnected" ? render.c.yellow : s === "revoked" ? render.c.red : render.c.dim;
568
+ return render.wrap(color, s);
569
+ },
570
+ },
571
+ {
572
+ label: "last_sync",
573
+ key: "lastSyncAt",
574
+ format: (v) => {
575
+ const iso = v;
576
+ if (!iso)
577
+ return render.wrap(render.c.dim, "never");
578
+ return `${render.relativeTime(iso)} ${render.wrap(render.c.dim, `(${iso})`)}`;
579
+ },
580
+ },
581
+ ];
582
+ console.log(render.table(cols, f.peerList));
583
+ }
584
+ });
585
+ statusCmd
586
+ .command("auth")
587
+ .description("Show OAuth / IdP subsystem status")
588
+ .action(async function () {
589
+ const opts = this.optsWithGlobals();
590
+ const { healthy, healthData } = await fetchHealthDetail(opts);
591
+ const mode = render.resolveOutputMode(opts);
592
+ if (mode === "json") {
593
+ console.log(render.asJSON({ healthy, oauth: healthData?.oauth ?? null }));
594
+ if (!healthy)
595
+ process.exit(1);
596
+ return;
597
+ }
598
+ if (!healthy) {
599
+ console.log(`${render.icons.error} ${render.wrap(render.c.red, "unreachable")}`);
600
+ process.exit(1);
601
+ }
602
+ const o = healthData?.oauth;
603
+ if (!o) {
604
+ console.log(`${render.wrap(render.c.bold, "OAuth")} ${render.wrap(render.c.dim, "not configured")}`);
605
+ return;
606
+ }
607
+ console.log(render.wrap(render.c.bold, "OAuth"));
608
+ console.log(render.kv("Clients", render.wrap(render.c.bold, String(Number(o?.clients ?? 0)))));
609
+ console.log(render.kv("IdP configs", String(Number(o?.idpConfigs ?? 0))));
610
+ const tokenColor = Number(o?.activeTokens ?? 0) > 0 ? render.c.green : render.c.dim;
611
+ console.log(render.kv("Active tokens", render.wrap(tokenColor, String(Number(o?.activeTokens ?? 0)))));
612
+ if (Array.isArray(o?.clientList) && o.clientList.length > 0) {
613
+ console.log(`\n ${render.wrap(render.c.dim, "Clients")}`);
614
+ const cols = [
615
+ { label: "id", key: "id" },
616
+ { label: "name", key: "name", format: (v) => String(v ?? "—") },
617
+ { label: "registered_by", key: "registeredBy", format: (v) => String(v ?? "—") },
618
+ { label: "created_at", key: "createdAt", format: (v) => String(v ?? "—") },
619
+ ];
620
+ console.log(render.table(cols, o.clientList));
621
+ }
622
+ if (Array.isArray(o?.idpList) && o.idpList.length > 0) {
623
+ console.log(`\n ${render.wrap(render.c.dim, "IdPs")}`);
624
+ const cols = [
625
+ { label: "id", key: "id" },
626
+ { label: "name", key: "name", format: (v) => String(v ?? "—") },
627
+ { label: "issuer", key: "issuer", format: (v) => String(v ?? "—") },
628
+ ];
629
+ console.log(render.table(cols, o.idpList));
630
+ }
631
+ });
632
+ statusCmd
633
+ .command("bridges")
634
+ .description("Show memory bridges subsystem status")
635
+ .action(async function () {
636
+ const opts = this.optsWithGlobals();
637
+ const { healthy, healthData } = await fetchHealthDetail(opts);
638
+ const mode = render.resolveOutputMode(opts);
639
+ if (mode === "json") {
640
+ console.log(render.asJSON({ healthy, bridges: healthData?.bridges ?? null }));
641
+ if (!healthy)
642
+ process.exit(1);
643
+ return;
644
+ }
645
+ if (!healthy) {
646
+ console.log(`${render.icons.error} ${render.wrap(render.c.red, "unreachable")}`);
647
+ process.exit(1);
648
+ }
649
+ const b = healthData?.bridges;
650
+ if (!b) {
651
+ console.log(`${render.wrap(render.c.bold, "Bridges")} ${render.wrap(render.c.dim, "none installed (no flair-bridge-* packages found)")}`);
652
+ return;
653
+ }
654
+ console.log(render.wrap(render.c.bold, "Bridges"));
655
+ if (Array.isArray(b.installed) && b.installed.length > 0) {
656
+ console.log(render.kv("Installed", b.installed.join(render.wrap(render.c.dim, ", "))));
657
+ }
658
+ if (b.lastImport)
659
+ console.log(render.kv("Last import", render.relativeTime(b.lastImport)));
660
+ if (b.lastExport)
661
+ console.log(render.kv("Last export", render.relativeTime(b.lastExport)));
662
+ });
663
+ // ─── flair status --deep ──────────────────────────────────────────────────────
664
+ //
665
+ // Deeper observability than the default `flair status` summary — full
666
+ // per-section detail with no condensing. Optional --bootstrap measures
667
+ // cold-start context bytes per agent (slow; calls /MemoryBootstrap once per
668
+ // agent, admin-auth required).
669
+ //
670
+ // Addresses ops-yph: Nathan's 2026-04-22 ask for "how much storage does my
671
+ // memory take, how much context are bootstraps pulling, what's the real usage
672
+ // pattern" — questions the default `flair status` summarizes but doesn't
673
+ // surface in full. Agents should be able to self-audit via this too.
674
+ statusCmd
675
+ .command("deep")
676
+ .description("Verbose status + optional bootstrap context size per agent (ops-yph)")
677
+ .option("--bootstrap", "Also measure bootstrap context bytes per agent (slow; admin auth required)")
678
+ .option("--max-tokens <n>", "Bootstrap maxTokens cap when --bootstrap is set", "4000")
679
+ .action(async function () {
680
+ const opts = this.optsWithGlobals();
681
+ const { healthy, baseUrl, healthData } = await fetchHealthDetail(opts);
682
+ // Same local install-health facts the plain `status` renders, so `--deep`
683
+ // cannot end on "✅ no warnings" while the ops API is exposed (flair#852).
684
+ const localWarnings = localOpsApiWarnings(baseUrl);
685
+ if (!healthy) {
686
+ if (opts.json) {
687
+ console.log(JSON.stringify({ healthy: false, url: baseUrl, error: "unreachable" }, null, 2));
688
+ }
689
+ else {
690
+ console.log(`Flair v${__pkgVersion} — 🔴 unreachable`);
691
+ console.log(` URL: ${baseUrl}`);
692
+ console.log(`\n Run: flair start or flair doctor`);
693
+ }
694
+ process.exit(1);
695
+ }
696
+ // Optional: per-agent bootstrap context bytes. Calls /MemoryBootstrap with
697
+ // admin auth (so we can measure on behalf of any agent without holding
698
+ // their keys). 15s timeout per call — bootstrap can be expensive on cold
699
+ // caches or large memory sets.
700
+ const agentList = (Array.isArray(healthData?.agents?.names) && healthData.agents.names.length > 0
701
+ ? healthData.agents.names
702
+ : Array.isArray(healthData?.agents?.perAgent)
703
+ ? healthData.agents.perAgent.map((r) => r.id).filter(Boolean)
704
+ : []);
705
+ const bootstrapBytes = {};
706
+ if (opts.bootstrap && agentList.length > 0) {
707
+ const adminPass = process.env.HDB_ADMIN_PASSWORD || process.env.FLAIR_ADMIN_PASS;
708
+ if (!adminPass) {
709
+ if (!opts.json) {
710
+ console.log("⚠ --bootstrap requires HDB_ADMIN_PASSWORD or FLAIR_ADMIN_PASS env var");
711
+ }
712
+ }
713
+ else {
714
+ const auth = `Basic ${Buffer.from(`${resolveAdminUser(undefined)}:${adminPass}`).toString("base64")}`;
715
+ const maxTokens = Number.parseInt(String(opts.maxTokens ?? "4000"), 10);
716
+ for (const agentId of agentList) {
717
+ try {
718
+ const res = await fetch(`${baseUrl}/BootstrapMemories`, {
719
+ method: "POST",
720
+ headers: { "Content-Type": "application/json", Authorization: auth },
721
+ body: JSON.stringify({ agentId, maxTokens }),
722
+ signal: AbortSignal.timeout(15000),
723
+ });
724
+ const text = await res.text();
725
+ const bytes = Buffer.byteLength(text, "utf8");
726
+ let tokenEstimate;
727
+ let memoriesIncluded;
728
+ try {
729
+ const json = JSON.parse(text);
730
+ tokenEstimate = typeof json.tokenEstimate === "number" ? json.tokenEstimate : undefined;
731
+ memoriesIncluded = typeof json.memoriesIncluded === "number" ? json.memoriesIncluded : undefined;
732
+ }
733
+ catch { /* response wasn't JSON; bytes still valid */ }
734
+ if (!res.ok) {
735
+ bootstrapBytes[agentId] = { bytes, error: `HTTP ${res.status}` };
736
+ }
737
+ else {
738
+ bootstrapBytes[agentId] = { bytes, tokenEstimate, memoriesIncluded };
739
+ }
740
+ }
741
+ catch (e) {
742
+ bootstrapBytes[agentId] = { bytes: 0, error: e?.message ?? String(e) };
743
+ }
744
+ }
745
+ }
746
+ }
747
+ if (opts.json) {
748
+ const out = { healthy, url: baseUrl, flairVersion: __pkgVersion, ...healthData };
749
+ if (localWarnings.length > 0) {
750
+ out.warnings = [
751
+ ...localWarnings,
752
+ ...(Array.isArray(healthData?.warnings) ? healthData.warnings : []),
753
+ ];
754
+ }
755
+ if (opts.bootstrap)
756
+ out.bootstrapBytes = bootstrapBytes;
757
+ console.log(JSON.stringify(out, null, 2));
758
+ return;
759
+ }
760
+ // Human-readable verbose render.
761
+ const uptimeSec = healthData?.uptimeSeconds;
762
+ let uptimeStr = "";
763
+ if (uptimeSec != null) {
764
+ const d = Math.floor(uptimeSec / 86400);
765
+ const h = Math.floor((uptimeSec % 86400) / 3600);
766
+ const m = Math.floor((uptimeSec % 3600) / 60);
767
+ uptimeStr = d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
768
+ }
769
+ const pid = healthData?.pid ?? "?";
770
+ console.log(`Flair v${__pkgVersion} — running (PID ${pid}${uptimeStr ? `, uptime ${uptimeStr}` : ""})`);
771
+ console.log(`URL: ${baseUrl}`);
772
+ const memories = healthData?.memories;
773
+ if (memories) {
774
+ console.log("\n═══ Memory ═══════════════════════════════════════");
775
+ console.log(`Total: ${memories.total}`);
776
+ console.log(`Breakdown: ${memories.withEmbeddings ?? 0} embedded, ${memories.hashFallback ?? 0} hash-fallback`);
777
+ if (memories.modelCounts && memories.total > 0) {
778
+ const entries = Object.entries(memories.modelCounts)
779
+ .filter(([, n]) => n > 0)
780
+ .sort((a, b) => b[1] - a[1]);
781
+ for (const [k, n] of entries) {
782
+ const pct = ((n / memories.total) * 100).toFixed(1);
783
+ console.log(` ${k.padEnd(28)} ${String(n).padStart(6)} (${pct}%)`);
784
+ }
785
+ }
786
+ if (memories.byDurability) {
787
+ const d = memories.byDurability;
788
+ console.log(`Durability: ${d.permanent ?? 0} permanent / ${d.persistent ?? 0} persistent / ${d.standard ?? 0} standard / ${d.ephemeral ?? 0} ephemeral`);
789
+ }
790
+ console.log(`Archived: ${memories.archived ?? 0}`);
791
+ console.log(`Expired: ${memories.expired ?? 0}`);
792
+ if (healthData?.lastWrite)
793
+ console.log(`Last write: ${relativeTime(healthData.lastWrite)} (${healthData.lastWrite})`);
794
+ }
795
+ const agents = healthData?.agents;
796
+ if (agents && agents.count > 0) {
797
+ console.log("\n═══ Agents ═══════════════════════════════════════");
798
+ console.log(`Total: ${agents.count}`);
799
+ if (Array.isArray(agents.names) && agents.names.length > 0) {
800
+ console.log(`Names: ${agents.names.join(", ")}`);
801
+ }
802
+ if (Array.isArray(agents.perAgent) && agents.perAgent.length > 0) {
803
+ const idW = Math.max(2, ...agents.perAgent.map((r) => (r.id ?? "").length));
804
+ console.log(`\n ${"id".padEnd(idW)} memories hash_fb 24h last_write`);
805
+ for (const r of agents.perAgent) {
806
+ const fb = typeof r.hashFallback === "number" ? String(r.hashFallback) : "—";
807
+ const w24 = typeof r.writes24h === "number" ? String(r.writes24h) : "—";
808
+ console.log(` ${(r.id ?? "").padEnd(idW)} ${String(r.memoryCount).padStart(8)} ${fb.padStart(7)} ${w24.padStart(3)} ${relativeTime(r.lastWriteAt)}`);
809
+ }
810
+ }
811
+ }
812
+ // Bootstrap context section — always printed so users see how to opt in.
813
+ console.log("\n═══ Bootstrap context ═══════════════════════════");
814
+ if (!opts.bootstrap) {
815
+ console.log(" (not measured — pass --bootstrap to fetch per-agent context bytes)");
816
+ }
817
+ else if (Object.keys(bootstrapBytes).length === 0) {
818
+ console.log(" (no agents found, or admin pass missing — see HDB_ADMIN_PASSWORD)");
819
+ }
820
+ else {
821
+ const idW = Math.max(5, ...Object.keys(bootstrapBytes).map((id) => id.length));
822
+ console.log(` ${"agent".padEnd(idW)} ${"bytes".padStart(9)} ${"~tokens".padStart(7)} ${"mems".padStart(4)} status`);
823
+ // Sort by bytes desc so heaviest bootstraps surface first.
824
+ const sortedEntries = Object.entries(bootstrapBytes).sort((a, b) => (b[1].bytes ?? 0) - (a[1].bytes ?? 0));
825
+ for (const [agentId, info] of sortedEntries) {
826
+ const bytesStr = info.error ? "error" : humanBytes(info.bytes);
827
+ const tok = info.tokenEstimate != null ? String(info.tokenEstimate) : "—";
828
+ const mems = info.memoriesIncluded != null ? String(info.memoriesIncluded) : "—";
829
+ const status = info.error ? info.error.slice(0, 40) : "ok";
830
+ console.log(` ${agentId.padEnd(idW)} ${bytesStr.padStart(9)} ${tok.padStart(7)} ${mems.padStart(4)} ${status}`);
831
+ }
832
+ }
833
+ if (healthData?.relationships) {
834
+ const r = healthData.relationships;
835
+ console.log("\n═══ Relationships ═══════════════════════════════");
836
+ console.log(`Total: ${r.total} (${r.active} active)`);
837
+ }
838
+ if (healthData?.soul && healthData.soul.total > 0) {
839
+ const s = healthData.soul;
840
+ console.log("\n═══ Soul ═════════════════════════════════════════");
841
+ console.log(`Total: ${s.total} entries`);
842
+ const entries = sortSoulKeyEntries(s.byKey ?? {});
843
+ if (entries.length > 0) {
844
+ console.log(`Keys: ${entries.map(([k, n]) => `${n} ${k}`).join(" / ")}`);
845
+ }
846
+ }
847
+ else if (typeof healthData?.soulEntries === "number" && healthData.soulEntries > 0) {
848
+ console.log("\n═══ Soul ═════════════════════════════════════════");
849
+ console.log(`Total: ${healthData.soulEntries} entries`);
850
+ }
851
+ if (healthData?.rem) {
852
+ const r = healthData.rem;
853
+ console.log("\n═══ REM ══════════════════════════════════════════");
854
+ console.log(`Last light: ${relativeTime(r.lastLightAt)}`);
855
+ console.log(`Last rapid: ${relativeTime(r.lastRapidAt)}`);
856
+ console.log(`Last restorative: ${relativeTime(r.lastRestorativeAt)}`);
857
+ const nightly = r.nightlyEnabled === true ? "enabled" : r.nightlyEnabled === false ? "disabled" : "unknown";
858
+ console.log(`Nightly: ${nightly}`);
859
+ if (r.lastNightlyAt)
860
+ console.log(`Last nightly: ${relativeTime(r.lastNightlyAt)} (${r.lastNightlyAt})`);
861
+ if (typeof r.pendingCandidates === "number")
862
+ console.log(`Pending candidates: ${r.pendingCandidates}`);
863
+ }
864
+ if (healthData?.federation) {
865
+ const f = healthData.federation;
866
+ console.log("\n═══ Federation ═══════════════════════════════════");
867
+ if (f.instance)
868
+ console.log(`Instance: ${f.instance.id} (${f.instance.role ?? "—"}, ${f.instance.status ?? "—"})`);
869
+ if (f.peers) {
870
+ const unknown = Number(f.peers.unknown ?? 0);
871
+ const unknownBit = unknown > 0 ? `, ${unknown} unknown` : "";
872
+ console.log(`Peers: ${f.peers.total} total (${f.peers.connected} connected, ${f.peers.disconnected} down, ${f.peers.revoked} revoked${unknownBit})`);
873
+ }
874
+ if (typeof f.pendingTokens === "number" && f.pendingTokens > 0)
875
+ console.log(`Pairing: ${f.pendingTokens} unconsumed token(s)`);
876
+ if (Array.isArray(f.peerList) && f.peerList.length > 0) {
877
+ const idW = Math.max(4, ...f.peerList.map((p) => (p.id ?? "").length));
878
+ console.log(`\n ${"peer".padEnd(idW)} ${"role".padEnd(5)} ${"status".padEnd(13)} ${"liveness".padEnd(13)} last_sync`);
879
+ for (const p of f.peerList) {
880
+ console.log(` ${(p.id ?? "").padEnd(idW)} ${(p.role ?? "—").padEnd(5)} ${(p.status ?? "—").padEnd(13)} ${(p.liveness ?? "—").padEnd(13)} ${p.lastSyncAt ? `${relativeTime(p.lastSyncAt)} (${p.lastSyncAt})` : "never"}`);
881
+ }
882
+ }
883
+ }
884
+ else {
885
+ console.log("\n═══ Federation ═══════════════════════════════════");
886
+ console.log(" not configured");
887
+ }
888
+ if (healthData?.oauth) {
889
+ const o = healthData.oauth;
890
+ console.log("\n═══ OAuth / IdP ══════════════════════════════════");
891
+ console.log(`Clients: ${o.clients}`);
892
+ console.log(`IdP configs: ${o.idpConfigs}`);
893
+ console.log(`Active tokens: ${o.activeTokens}`);
894
+ if (Array.isArray(o.clientList) && o.clientList.length > 0) {
895
+ console.log(`\n Clients:`);
896
+ for (const c of o.clientList) {
897
+ console.log(` ${c.id} ${c.name ?? "—"} ${c.registeredBy ?? "—"} ${c.createdAt ?? "—"}`);
898
+ }
899
+ }
900
+ }
901
+ if (healthData?.bridges) {
902
+ const b = healthData.bridges;
903
+ console.log("\n═══ Bridges ══════════════════════════════════════");
904
+ if (Array.isArray(b.installed) && b.installed.length > 0)
905
+ console.log(`Installed: ${b.installed.join(", ")}`);
906
+ if (b.lastImport)
907
+ console.log(`Last import: ${relativeTime(b.lastImport)}`);
908
+ if (b.lastExport)
909
+ console.log(`Last export: ${relativeTime(b.lastExport)}`);
910
+ }
911
+ if (healthData?.disk) {
912
+ const d = healthData.disk;
913
+ console.log("\n═══ Disk ═════════════════════════════════════════");
914
+ console.log(`Data: ${d.dataDir} — ${humanBytes(d.dataBytes ?? 0)}`);
915
+ console.log(`Snapshots: ${d.snapshotDir} — ${humanBytes(d.snapshotBytes ?? 0)}`);
916
+ console.log(`Total: ${humanBytes((d.dataBytes ?? 0) + (d.snapshotBytes ?? 0))}`);
917
+ }
918
+ const warnings = [
919
+ ...localWarnings,
920
+ ...(Array.isArray(healthData?.warnings) ? healthData.warnings : []),
921
+ ];
922
+ if (warnings.length > 0) {
923
+ console.log("\n═══ Warnings ═════════════════════════════════════");
924
+ for (const w of warnings)
925
+ console.log(` ${w.level === "warn" ? "⚠" : "ℹ"} ${w.message}`);
926
+ }
927
+ else {
928
+ console.log("\n✅ no warnings");
929
+ }
930
+ });
931
+ }