@bli-cockpit/cli 0.2.237 → 0.2.238

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 (139) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +73 -30
  29. package/dist/commands/cal.js +36 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/heartbeat-post.js +116 -0
  35. package/dist/commands/heartbeat-token-lock.js +46 -33
  36. package/dist/commands/heartbeat.js +43 -125
  37. package/dist/commands/install-update.js +17 -15
  38. package/dist/commands/issue-show.js +27 -28
  39. package/dist/commands/issue-write.js +70 -23
  40. package/dist/commands/issue.js +68 -37
  41. package/dist/commands/jarvis-live-answer.js +99 -0
  42. package/dist/commands/jarvis-render.js +1 -94
  43. package/dist/commands/jarvis-trace.js +30 -22
  44. package/dist/commands/jarvis-turn-report.js +124 -0
  45. package/dist/commands/jarvis-turn.js +108 -182
  46. package/dist/commands/jarvis.js +26 -17
  47. package/dist/commands/local-args-collector-setup.js +82 -84
  48. package/dist/commands/local-args-collector-work.js +37 -38
  49. package/dist/commands/local-args-tower-admin.js +155 -142
  50. package/dist/commands/local-args-tower-cal.js +52 -50
  51. package/dist/commands/local-args-tower-careers.js +73 -54
  52. package/dist/commands/local-args-tower-docs-msg.js +55 -45
  53. package/dist/commands/local-args-tower-github.js +26 -20
  54. package/dist/commands/local-args-tower-hits.js +32 -20
  55. package/dist/commands/local-args-tower-manager.js +85 -4
  56. package/dist/commands/local-args-tower-memory.js +139 -117
  57. package/dist/commands/local-args-tower-notes.js +346 -0
  58. package/dist/commands/local-args-tower-pages.js +100 -378
  59. package/dist/commands/local-args-tower-settings.js +28 -29
  60. package/dist/commands/local-args-tower-work.js +52 -33
  61. package/dist/commands/local-args.js +80 -129
  62. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  63. package/dist/commands/local-help-commands-tower.js +6 -2
  64. package/dist/commands/local-help-commands.js +342 -341
  65. package/dist/commands/local-help.js +87 -83
  66. package/dist/commands/local-run-guards.js +100 -0
  67. package/dist/commands/local.js +137 -206
  68. package/dist/commands/login.js +13 -9
  69. package/dist/commands/logs.js +55 -38
  70. package/dist/commands/mail-attachments.js +44 -26
  71. package/dist/commands/manager-render-home.js +208 -0
  72. package/dist/commands/manager-render.js +7 -3
  73. package/dist/commands/manager-requests.js +33 -0
  74. package/dist/commands/manager.js +39 -5
  75. package/dist/commands/memory-consolidation.js +18 -9
  76. package/dist/commands/memory-daemon-probe.js +37 -28
  77. package/dist/commands/memory-install-config.js +64 -60
  78. package/dist/commands/memory-install.js +21 -41
  79. package/dist/commands/memory-requeue.js +26 -19
  80. package/dist/commands/memory-rules.js +26 -18
  81. package/dist/commands/memory-search-print.js +26 -0
  82. package/dist/commands/memory-store.js +14 -30
  83. package/dist/commands/memory-verb-list.js +49 -48
  84. package/dist/commands/notes-accounts.js +49 -32
  85. package/dist/commands/notes-sync-loop.js +115 -89
  86. package/dist/commands/notes-wisprflow-login.js +44 -23
  87. package/dist/commands/notes-writes.js +115 -100
  88. package/dist/commands/notes.js +33 -55
  89. package/dist/commands/onboard-flows.js +69 -51
  90. package/dist/commands/onboard.js +11 -9
  91. package/dist/commands/ops-render-memory.js +10 -7
  92. package/dist/commands/ops-render.js +33 -30
  93. package/dist/commands/ops.js +3 -0
  94. package/dist/commands/public-root.js +1 -1
  95. package/dist/commands/session-sync-outside-roots.js +40 -36
  96. package/dist/commands/session-sync-plan.js +14 -10
  97. package/dist/commands/session-sync-upload.js +24 -22
  98. package/dist/commands/session-sync.js +19 -8
  99. package/dist/commands/slack.js +25 -16
  100. package/dist/commands/sources-check.js +13 -7
  101. package/dist/commands/sources-discovery.js +25 -17
  102. package/dist/commands/sources-verdict.js +19 -18
  103. package/dist/commands/speed.js +23 -19
  104. package/dist/commands/start.js +20 -16
  105. package/dist/commands/sync-doctor.js +52 -48
  106. package/dist/commands/sync.js +22 -16
  107. package/dist/commands/usage-telemetry-toml.js +71 -45
  108. package/dist/commands/usage.js +91 -67
  109. package/dist/commands/workbook-render.js +10 -7
  110. package/dist/evidence-reconcile-client.js +75 -64
  111. package/dist/evidence-redelivery-plan.js +37 -34
  112. package/dist/evidence-redelivery.js +32 -29
  113. package/dist/evidence-transcripts-client.js +20 -20
  114. package/dist/evidence-upload-object.js +86 -89
  115. package/dist/evidence-upload-plan.js +94 -83
  116. package/dist/evidence-upload-terminal.js +45 -50
  117. package/dist/image-description-session.js +156 -159
  118. package/dist/local-state-pairing-code.js +39 -31
  119. package/dist/local-state-status.js +9 -2
  120. package/dist/onboarding-roots.js +5 -8
  121. package/dist/process-runner.js +8 -8
  122. package/dist/raw-evidence-gc.js +33 -38
  123. package/dist/raw-evidence-stuck-summary.js +7 -14
  124. package/dist/repo-identity.js +26 -23
  125. package/dist/scheduled-self-update.js +11 -15
  126. package/dist/second-install.js +3 -0
  127. package/dist/slim/codex-rules.js +23 -36
  128. package/dist/sync-lock.js +5 -15
  129. package/dist/tower-client.js +36 -53
  130. package/dist/tower-stream.js +7 -24
  131. package/dist/upload-agent-artifacts.js +58 -64
  132. package/dist/upload-envelope-build.js +60 -59
  133. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  134. package/dist/upload-evidence-delivery-summary.js +32 -34
  135. package/dist/upload-ingest-receipt.js +20 -23
  136. package/dist/upload-session-reports-queue.js +4 -1
  137. package/dist/upload-session-reports-wire.js +64 -68
  138. package/dist/upload-sync.js +171 -157
  139. package/package.json +2 -2
@@ -37,82 +37,86 @@ export async function resolveMemoryConfig(command, io, platform, deps) {
37
37
  cliEntryPoint: deps.cliEntryPoint,
38
38
  realpath: deps.realpath,
39
39
  });
40
- if (!found) {
41
- return {
42
- config: null,
43
- source: "none",
44
- binTarget: {
45
- target: "bin",
46
- status: "skipped",
47
- reason: "bin_missing",
48
- detail: `${MEMORY_MCP_BIN} is not installed beside this CLI or on PATH; nothing was written, and the next daily run will try again`,
49
- },
50
- };
51
- }
52
- if (isUnsafeBinPath(found.path)) {
53
- // A hook command is a shell string by the platform's design. A path that
54
- // cannot be expressed safely in one is not escaped cleverly, and it is not
55
- // swapped for a bare name that may resolve to something else either — the
56
- // install refuses and says why. See `shellSafeBinPath` for what CAN be
57
- // expressed, including the Windows separator normalisation of BLI-4136.
58
- return {
59
- config: null,
60
- source: "none",
61
- binTarget: {
62
- target: "bin",
63
- status: "failed",
64
- reason: "bin_path_unsafe",
65
- detail: "the resolved bin path cannot be expressed safely in a hook command; nothing was written",
66
- },
67
- bin_source: found.source,
68
- };
69
- }
40
+ if (!found)
41
+ return binMissing();
42
+ if (isUnsafeBinPath(found.path))
43
+ return binPathUnsafe(found.source);
44
+ return await configFromPresentBin(io, found, platform, dashboardUrl);
45
+ }
46
+ function binMissing() {
47
+ return {
48
+ config: null,
49
+ source: "none",
50
+ binTarget: {
51
+ target: "bin",
52
+ status: "skipped",
53
+ reason: "bin_missing",
54
+ detail: `${MEMORY_MCP_BIN} is not installed beside this CLI or on PATH; nothing was written, and the next daily run will try again`,
55
+ },
56
+ };
57
+ }
58
+ function binPathUnsafe(binSource) {
59
+ // A hook command is a shell string by the platform's design. A path that
60
+ // cannot be expressed safely in one is not escaped cleverly, and it is not
61
+ // swapped for a bare name that may resolve to something else either — the
62
+ // install refuses and says why. See `shellSafeBinPath` for what CAN be
63
+ // expressed, including the Windows separator normalisation of BLI-4136.
64
+ return {
65
+ config: null,
66
+ source: "none",
67
+ binTarget: {
68
+ target: "bin",
69
+ status: "failed",
70
+ reason: "bin_path_unsafe",
71
+ detail: "the resolved bin path cannot be expressed safely in a hook command; nothing was written",
72
+ },
73
+ bin_source: binSource,
74
+ };
75
+ }
76
+ /** The bin exists and its path is safe: take its own printed shape, or the built-in one. */
77
+ async function configFromPresentBin(io, found, platform, dashboardUrl) {
70
78
  const printed = await printedMemoryConfig(io, found.path);
71
- if (printed) {
72
- // The bin prints a BARE command name — it cannot know where it was
73
- // installed, and it is nested inside the CLI's node_modules rather than on
74
- // PATH. Path-qualifying it here is what makes the registration runnable at
75
- // all; see `withResolvedBinPath`.
76
- const qualified = withResolvedBinPath(printed, {
77
- binPath: found.path,
78
- platform,
79
- dashboardUrl,
80
- });
81
- if (qualified) {
82
- return {
83
- config: qualified,
84
- source: "bin",
85
- binTarget: { target: "bin", status: "already", reason: "bin_printed_config" },
86
- bin_source: found.source,
87
- };
88
- }
89
- // A printed hook command this installer cannot re-point. The template is
90
- // always path-qualified, so it is the safe answer — and the reason says
91
- // which of the two fallbacks happened.
79
+ if (!printed) {
92
80
  return {
93
81
  config: builtinMemoryInstallConfig({ binPath: found.path, platform, dashboardUrl }),
94
82
  source: "template",
95
83
  binTarget: {
96
84
  target: "bin",
97
85
  status: "already",
98
- reason: "bin_printed_config_unqualifiable",
99
- detail: "the bin printed a hook command this installer could not re-point at the resolved path; the built-in shape was used",
86
+ reason: "bin_present_template_used",
87
+ detail: "the bin did not print a usable --print-config; the built-in shape was used",
100
88
  },
101
89
  bin_source: found.source,
102
90
  };
103
91
  }
92
+ // The bin prints a BARE command name — it cannot know where it was
93
+ // installed, and it is nested inside the CLI's node_modules rather than on
94
+ // PATH. Path-qualifying it here is what makes the registration runnable at
95
+ // all; see `withResolvedBinPath`.
96
+ const qualified = withResolvedBinPath(printed, {
97
+ binPath: found.path,
98
+ platform,
99
+ dashboardUrl,
100
+ });
101
+ if (qualified) {
102
+ return {
103
+ config: qualified,
104
+ source: "bin",
105
+ binTarget: { target: "bin", status: "already", reason: "bin_printed_config" },
106
+ bin_source: found.source,
107
+ };
108
+ }
109
+ // A printed hook command this installer cannot re-point. The template is
110
+ // always path-qualified, so it is the safe answer — and the reason says
111
+ // which of the two fallbacks happened.
104
112
  return {
105
- config: builtinMemoryInstallConfig({
106
- binPath: found.path,
107
- platform,
108
- dashboardUrl,
109
- }),
113
+ config: builtinMemoryInstallConfig({ binPath: found.path, platform, dashboardUrl }),
110
114
  source: "template",
111
115
  binTarget: {
112
116
  target: "bin",
113
117
  status: "already",
114
- reason: "bin_present_template_used",
115
- detail: "the bin did not print a usable --print-config; the built-in shape was used",
118
+ reason: "bin_printed_config_unqualifiable",
119
+ detail: "the bin printed a hook command this installer could not re-point at the resolved path; the built-in shape was used",
116
120
  },
117
121
  bin_source: found.source,
118
122
  };
@@ -97,18 +97,9 @@ export async function installMemoryIntegration(command, io, deps = {}) {
97
97
  // `no_bin_no_write`: with no resolved server there is nothing safe to
98
98
  // register, so not one file is opened. The bin target carries the reason.
99
99
  if (resolved.config) {
100
- targets.push(...(await installClaudeMemoryIntegration({
101
- homeDir,
102
- config: resolved.config,
103
- dryRun: command.dryRun,
104
- io: fileIo,
105
- })));
106
- targets.push(...(await installCodexMemoryIntegration({
107
- homeDir,
108
- config: resolved.config,
109
- dryRun: command.dryRun,
110
- io: fileIo,
111
- })));
100
+ const host = { homeDir, config: resolved.config, dryRun: command.dryRun, io: fileIo };
101
+ targets.push(...(await installClaudeMemoryIntegration(host)));
102
+ targets.push(...(await installCodexMemoryIntegration(host)));
112
103
  }
113
104
  // BLI-3706: `bli-tower` (docs_*/msg_* over the collector device token)
114
105
  // registers beside `bli-memory` on the SAME command — see
@@ -127,6 +118,20 @@ export async function installMemoryIntegration(command, io, deps = {}) {
127
118
  }, command.dryRun)));
128
119
  const outcome = {
129
120
  action: "install",
121
+ ...(await readBackOutcome(targets, resolved, homeDir, fileIo)),
122
+ };
123
+ logMemoryOutcome(outcome, platform);
124
+ // The heartbeat reads the cache, not the machine — see the receipt module's
125
+ // "Where the receipt lives between runs". A dry run caches nothing: it proves
126
+ // what WOULD happen and nothing about what is on disk.
127
+ if (!command.dryRun) {
128
+ await cacheReceipt(homeDir ?? command.homeDir, outcome.receipt);
129
+ }
130
+ return outcome;
131
+ }
132
+ /** What both `install` and `status` report once the targets are known: their verdict and the receipt. */
133
+ async function readBackOutcome(targets, resolved, homeDir, fileIo) {
134
+ return {
130
135
  ...aggregate(targets),
131
136
  config_source: resolved.source,
132
137
  bin_found: resolved.config !== null,
@@ -139,14 +144,6 @@ export async function installMemoryIntegration(command, io, deps = {}) {
139
144
  expectedTrustRows: resolved.config ? expectedCodexTrustRows(resolved.config) : 0,
140
145
  }),
141
146
  };
142
- logMemoryOutcome(outcome, platform);
143
- // The heartbeat reads the cache, not the machine — see the receipt module's
144
- // "Where the receipt lives between runs". A dry run caches nothing: it proves
145
- // what WOULD happen and nothing about what is on disk.
146
- if (!command.dryRun) {
147
- await cacheReceipt(homeDir ?? command.homeDir, outcome.receipt);
148
- }
149
- return outcome;
150
147
  }
151
148
  /**
152
149
  * Reading the trust state never fails an install. A `config.toml` we cannot
@@ -185,16 +182,9 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
185
182
  // against, so `status` says the one true thing — the server is not here —
186
183
  // rather than reporting four targets against a shape we invented.
187
184
  if (resolved.config) {
188
- targets.push(...(await inspectClaudeMemoryIntegration({
189
- homeDir,
190
- config: resolved.config,
191
- io: fileIo,
192
- })));
193
- targets.push(...(await inspectCodexMemoryIntegration({
194
- homeDir,
195
- config: resolved.config,
196
- io: fileIo,
197
- })));
185
+ const host = { homeDir, config: resolved.config, io: fileIo };
186
+ targets.push(...(await inspectClaudeMemoryIntegration(host)));
187
+ targets.push(...(await inspectCodexMemoryIntegration(host)));
198
188
  }
199
189
  // BLI-3706: see the matching note in `installMemoryIntegration`.
200
190
  targets.push(...(await inspectTowerIntegration({
@@ -213,17 +203,7 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
213
203
  // bounded, it opens nothing but a local socket, and a machine without one
214
204
  // is reported as exactly that rather than as a problem.
215
205
  daemon: await probeMemoryDaemon({ ...(homeDir ? { homeDir } : {}) }),
216
- ...aggregate(targets),
217
- config_source: resolved.source,
218
- bin_found: resolved.config !== null,
219
- ...(resolved.bin_source ? { bin_source: resolved.bin_source } : {}),
220
- targets,
221
- receipt: buildMemoryInstallReceipt({
222
- targets,
223
- binFound: resolved.config !== null,
224
- codexTrust: await codexTrustFor(homeDir, fileIo),
225
- expectedTrustRows: resolved.config ? expectedCodexTrustRows(resolved.config) : 0,
226
- }),
206
+ ...(await readBackOutcome(targets, resolved, homeDir, fileIo)),
227
207
  };
228
208
  // `status` is a pure read and its reading is exactly as good as an install's
229
209
  // — both re-parse the stored files — so it refreshes the cache too. A person
@@ -54,7 +54,19 @@ export async function runMemoryRequeue(command, io) {
54
54
  if (typeof body.matched !== "number") {
55
55
  return failAgentDoor(door, TAG, "requeue_answer_unreadable", "Tower answered 200 with no requeue receipt in it. The usual cause is a dashboard that has not shipped POST /api/memory/requeue yet; nothing was requeued.");
56
56
  }
57
- // Counts and labels only: a receipt's content never enters a log line.
57
+ logRequeueReceipt(door, command, body);
58
+ if (door.json)
59
+ return emitAgentDoor(door, answer.body);
60
+ const matched = body.matched ?? 0;
61
+ if (matched === 0) {
62
+ writeNothingMatched(door, command);
63
+ return 0;
64
+ }
65
+ writeRequeueSummary(door, body, matched);
66
+ return 0;
67
+ }
68
+ /** Counts and labels only: a receipt's content never enters a log line. */
69
+ function logRequeueReceipt(door, command, body) {
58
70
  writeLine(door.io.stderr, `${TAG} ${body.dryRun ? "dry run" : "applied"} ${JSON.stringify({
59
71
  since: command.since ?? "",
60
72
  until: command.until ?? null,
@@ -65,15 +77,15 @@ export async function runMemoryRequeue(command, io) {
65
77
  refused: body.refused ?? 0,
66
78
  coverage: body.coverage ?? "unknown",
67
79
  })}`);
68
- if (door.json)
69
- return emitAgentDoor(door, answer.body);
70
- const matched = body.matched ?? 0;
71
- if (matched === 0) {
72
- // Silence is not an outage, and it is not a wrong filter either — say
73
- // which one this is by naming the window that was asked about.
74
- writeLine(door.io.stdout, `No finished extraction receipt is in that window (${command.since} → ${command.until ?? "now"}${command.source ? `, ${command.source} only` : ""}). Nothing to requeue; the receipts table answered.`);
75
- return 0;
76
- }
80
+ }
81
+ /**
82
+ * Silence is not an outage, and it is not a wrong filter either — say
83
+ * which one this is by naming the window that was asked about.
84
+ */
85
+ function writeNothingMatched(door, command) {
86
+ writeLine(door.io.stdout, `No finished extraction receipt is in that window (${command.since} → ${command.until ?? "now"}${command.source ? `, ${command.source} only` : ""}). Nothing to requeue; the receipts table answered.`);
87
+ }
88
+ function writeRequeueSummary(door, body, matched) {
77
89
  writeLine(door.io.stdout, body.dryRun
78
90
  ? `${matched} receipt(s) would be requeued (${describeSplit(body.bySource)}).`
79
91
  : `${body.requeued ?? 0} of ${matched} receipt(s) requeued (${describeSplit(body.bySource)}).`);
@@ -86,15 +98,10 @@ export async function runMemoryRequeue(command, io) {
86
98
  if (body.coverage === "capped") {
87
99
  writeLine(door.io.stdout, " The page ceiling stopped this run before the window ended. Run it again for the rest; nothing is lost.");
88
100
  }
89
- if (body.dryRun) {
90
- writeLine(door.io.stdout, "");
91
- writeLine(door.io.stdout, "Nothing was written. Add --apply to do it for real.");
92
- }
93
- else {
94
- writeLine(door.io.stdout, "");
95
- writeLine(door.io.stdout, "They are queued again: the hourly sweep reruns the hook receipts and the harvest pass reserves the rest. The previous result is kept on each row under result.requeued_from.");
96
- }
97
- return 0;
101
+ writeLine(door.io.stdout, "");
102
+ writeLine(door.io.stdout, body.dryRun
103
+ ? "Nothing was written. Add --apply to do it for real."
104
+ : "They are queued again: the hourly sweep reruns the hook receipts and the harvest pass reserves the rest. The previous result is kept on each row under result.requeued_from.");
98
105
  }
99
106
  /** `hook:104, harvest:18` — counts and labels, in the order the door sorted them. */
100
107
  function describeSplit(split) {
@@ -46,19 +46,31 @@ function printProjects(groups, machine, write) {
46
46
  for (const group of groups) {
47
47
  write("");
48
48
  write(`${group.project}, ${group.count.toLocaleString("en-US")} memories`);
49
- const topics = group.topics ?? [];
50
- // Topic headings help only when topics rank the ideas; a lone "Other" heading is noise.
51
- const headed = Boolean(group.ranked) && topics.length > 1;
52
- if (topics.length)
53
- for (const topic of topics) {
54
- if (headed)
55
- write(` ${topic.title}`);
56
- for (const idea of topic.memories)
57
- write(`${headed ? " " : " "}${machine ? agentWording(idea) : personWording(idea)}`);
58
- }
59
- else
60
- for (const idea of group.memories ?? [])
61
- write(` ${machine ? agentWording(idea) : personWording(idea)}`);
49
+ printProjectIdeas(group, machine, write);
50
+ }
51
+ }
52
+ function printProjectIdeas(group, machine, write) {
53
+ const wording = machine ? agentWording : personWording;
54
+ const topics = group.topics ?? [];
55
+ if (topics.length === 0) {
56
+ for (const idea of group.memories ?? [])
57
+ write(` ${wording(idea)}`);
58
+ return;
59
+ }
60
+ // Topic headings help only when topics rank the ideas; a lone "Other" heading is noise.
61
+ const headed = Boolean(group.ranked) && topics.length > 1;
62
+ for (const topic of topics) {
63
+ if (headed)
64
+ write(` ${topic.title}`);
65
+ for (const idea of topic.memories)
66
+ write(`${headed ? " " : " "}${wording(idea)}`);
67
+ }
68
+ }
69
+ function printOwnerGroups(groups, machine, write) {
70
+ for (const group of groups) {
71
+ write(group.owner);
72
+ for (const rule of group.rules)
73
+ write(` [${rule.created_at.slice(0, 10)}] ${rule.project ? `[${rule.project}] ` : ""}${machine ? rule.text : personWording(rule)}`);
62
74
  }
63
75
  }
64
76
  export async function runMemoryRules(command, door) {
@@ -87,11 +99,7 @@ export async function runMemoryRules(command, door) {
87
99
  if (projects)
88
100
  printProjects(body.projectGroups ?? [], Boolean(command.machine), write);
89
101
  else
90
- for (const group of body.groups ?? []) {
91
- write(group.owner);
92
- for (const rule of group.rules)
93
- write(` [${rule.created_at.slice(0, 10)}] ${rule.project ? `[${rule.project}] ` : ""}${command.machine ? rule.text : personWording(rule)}`);
94
- }
102
+ printOwnerGroups(body.groups ?? [], Boolean(command.machine), write);
95
103
  if (!body.returned)
96
104
  write(`No ${command.action === "knowledge" ? "project knowledge" : "ground rules"} matched.`);
97
105
  if (body.capped)
@@ -92,4 +92,30 @@ function printOriginals(out, row, alreadyPrinted) {
92
92
  const hidden = (row["originalsHidden"] ?? {});
93
93
  if (Number(hidden["private"] ?? 0) > 0)
94
94
  writeLine(out, ` (${String(hidden["private"])} private original(s) not shown)`);
95
+ }
96
+ export function writeSearchResults(out, results, degraded) {
97
+ if (results.length > 0) {
98
+ printSearchRows(out, results);
99
+ return;
100
+ }
101
+ if (degraded) {
102
+ // BLI-10448: an empty answer from a search that lost a channel is NOT the
103
+ // record being silent. `semantic_timed_out` is the store answering on
104
+ // keywords alone after its vector search ran out of time, and saying
105
+ // "the record is silent" over it sends a person away from memories that
106
+ // exist.
107
+ writeLine(out, `No memories matched on the channels that answered, and one did not (${degraded}). Do not read this as the record being silent; ask again.`);
108
+ return;
109
+ }
110
+ // The distinction the whole store is built on: silence is not an outage.
111
+ // A failure took the `!answer.ok` branch above and named itself there.
112
+ writeLine(out, "No memories matched. The record is silent on this — it does not mean the store is down.");
113
+ }
114
+ export function writeRecentContext(out, recent) {
115
+ writeLine(out, "");
116
+ writeLine(out, "Recent context:");
117
+ for (const row of recent) {
118
+ const author = typeof row["savedBy"] === "string" && row["savedBy"] ? `(from ${row["savedBy"]}) ` : "";
119
+ writeLine(out, ` - ${memoryProjectPrefix(row)}${row["project"] ? "" : author}${oneLine(row["preview"] ?? row["memory"])}`);
120
+ }
95
121
  }
@@ -45,8 +45,7 @@
45
45
  import { runMemoryMap } from "./memory-map.js";
46
46
  import { runMemoryRepeats } from "./memory-repeats.js";
47
47
  import { runMemoryRules } from "./memory-rules.js";
48
- import { memoryProjectPrefix } from "./memory-project-label.js";
49
- import { oneLine, printSearchRows } from "./memory-search-print.js";
48
+ import { oneLine, writeRecentContext, writeSearchResults } from "./memory-search-print.js";
50
49
  import { memorySaveOutsideRoots } from "@bli-cockpit/memory-mcp/dist/recorded-outside-roots.js";
51
50
  import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.js";
52
51
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
@@ -169,9 +168,8 @@ function containerTagsFor(command) {
169
168
  return command.containerTags;
170
169
  return [resolveContainerTag({ cwd: command.workspace ?? process.cwd() }).containerTag];
171
170
  }
172
- async function searchMemory(command, door) {
173
- const containerTags = containerTagsFor(command);
174
- const requestBody = {
171
+ function searchRequestBody(command, containerTags) {
172
+ return {
175
173
  query: command.query,
176
174
  containerTag: containerTags,
177
175
  ...(command.limit === undefined ? {} : { limit: command.limit }),
@@ -181,6 +179,10 @@ async function searchMemory(command, door) {
181
179
  // BLI-19255: absent lets the door decide by the question.
182
180
  ...(command.includeSource === undefined ? {} : { includeSource: command.includeSource }),
183
181
  };
182
+ }
183
+ async function searchMemory(command, door) {
184
+ const containerTags = containerTagsFor(command);
185
+ const requestBody = searchRequestBody(command, containerTags);
184
186
  const answer = await readThroughCache({
185
187
  io: door.io,
186
188
  homeDir: door.homeDir,
@@ -219,30 +221,9 @@ async function searchMemory(command, door) {
219
221
  if (answer.fromCache) {
220
222
  writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
221
223
  }
222
- if (results.length === 0 && body.degraded) {
223
- // BLI-10448: an empty answer from a search that lost a channel is NOT the
224
- // record being silent. `semantic_timed_out` is the store answering on
225
- // keywords alone after its vector search ran out of time, and saying
226
- // "the record is silent" over it sends a person away from memories that
227
- // exist.
228
- writeLine(door.io.stdout, `No memories matched on the channels that answered, and one did not (${body.degraded}). Do not read this as the record being silent; ask again.`);
229
- }
230
- else if (results.length === 0) {
231
- // The distinction the whole store is built on: silence is not an outage.
232
- // A failure took the `!answer.ok` branch above and named itself there.
233
- writeLine(door.io.stdout, "No memories matched. The record is silent on this — it does not mean the store is down.");
234
- }
235
- else {
236
- printSearchRows(door.io.stdout, results);
237
- }
238
- if (recent.length > 0) {
239
- writeLine(door.io.stdout, "");
240
- writeLine(door.io.stdout, "Recent context:");
241
- for (const row of recent) {
242
- const author = typeof row["savedBy"] === "string" && row["savedBy"] ? `(from ${row["savedBy"]}) ` : "";
243
- writeLine(door.io.stdout, ` - ${memoryProjectPrefix(row)}${row["project"] ? "" : author}${oneLine(row["preview"] ?? row["memory"])}`);
244
- }
245
- }
224
+ writeSearchResults(door.io.stdout, results, body.degraded);
225
+ if (recent.length > 0)
226
+ writeRecentContext(door.io.stdout, recent);
246
227
  if (body.degraded) {
247
228
  writeLine(door.io.stdout, "");
248
229
  writeLine(door.io.stdout, `This answer is thinner than usual (${body.degraded}) — one search channel did not run.`);
@@ -289,6 +270,10 @@ async function saveMemory(command, door) {
289
270
  })}`);
290
271
  if (door.json)
291
272
  return emitAgentDoor(door, answer.body);
273
+ writeSavedMemory(door, saved, containerTag);
274
+ return 0;
275
+ }
276
+ function writeSavedMemory(door, saved, containerTag) {
292
277
  // The container the SERVER used, which is the canonical one when this
293
278
  // repository has more than one tag on the shelf; saying the tag we sent
294
279
  // would name a space the row is not in.
@@ -300,7 +285,6 @@ async function saveMemory(command, door) {
300
285
  if (saved["extract"] === "fell_back_verbatim") {
301
286
  writeLine(door.io.stdout, `Saved as written; extraction failed (${String(saved["extractFailureReason"] ?? "unknown")}).`);
302
287
  }
303
- return 0;
304
288
  }
305
289
  async function updateMemory(command, door) {
306
290
  const body = await readBody(command, door, BODY_MAX_CHARS, "A memory");
@@ -11,6 +11,54 @@
11
11
  * Every string moved verbatim.
12
12
  */
13
13
  import { writeLine } from "./cli-io.js";
14
+ /** The human page: what the noun does, one screen. */
15
+ const VERB_LIST_LINES = [
16
+ "cockpit memory — BLI Memory: the store, and this machine's registration.",
17
+ "",
18
+ "The store (Tower, over /api/memory/*):",
19
+ " cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--json]",
20
+ " cockpit memory save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>] [--json]",
21
+ " the memory comes from stdin, or --file; never from the command line",
22
+ " cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
23
+ " cockpit memory forget <id> [--reason <label>] [--json]",
24
+ " cockpit memory forget --content-stdin --container <tag> [--reason <label>] [--json]",
25
+ " you can forget or update only your own memories (a super admin, any)",
26
+ " cockpit memory private <id> [--json] only you and super admins can read this memory",
27
+ " cockpit memory public <id> [--json] every member can read it again (how every memory starts)",
28
+ " cockpit memory map [--json]",
29
+ " cockpit memory repeats [--project <name-or-slug>] [--min 2..50] [--limit 1..200] [--json]",
30
+ " subjects a person corrected an agent about, or ordered with emphasis, more than once",
31
+ " cockpit memory rules [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
32
+ " cockpit memory knowledge [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
33
+ " plain sentences per project; --per-project 1..100 (default 15) is each project's own",
34
+ " quota, --limit 1..1000 caps the ideas in all; --machine prints the agent wording with",
35
+ " scores and spaces; --json is the whole answer, both wordings (--by-project is accepted)",
36
+ " cockpit memory profile [--container <tag>] [--json]",
37
+ " the compiled one-page briefing every session opens with; a read, never a compile",
38
+ " cockpit memory consolidation [--container <tag>] [--json]",
39
+ " what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
40
+ " cockpit memory chain <id> [--json]",
41
+ " cockpit memory exam [--night <YYYY-MM-DD>] [--misses] [--json]",
42
+ " the nightly memory exam: purity, coverage, trend and worst project; yours alone",
43
+ " cockpit memory source <id> [--query \"<words>\"] [--chars 200..6000] [--json]",
44
+ " the memories the nightly pass folded into this one: who, when, what they said",
45
+ " cockpit memory requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply] [--json]",
46
+ " put a window of finished extractions back in the queue after the store refused",
47
+ " their writes; super admins only, and a DRY RUN unless you pass --apply",
48
+ "",
49
+ "This machine:",
50
+ " cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
51
+ " cockpit memory status [--json] read the stored config back",
52
+ "",
53
+ "The experience log:",
54
+ " cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
55
+ "",
56
+ "Search: --limit caps the MEMORIES (default 5, at most 20). Up to 3 Tower pages and",
57
+ "tickets ride beside them whatever --limit says; for more, use `cockpit search`.",
58
+ "Save accepts repeated --alias names (up to 16 names of 64 characters).",
59
+ "Omit --container and this repository's own memory space is used — the same",
60
+ "space the bli-memory MCP server and its hooks read and write.",
61
+ ];
14
62
  /**
15
63
  * The verb list a bare `cockpit memory` prints (BLI-4047).
16
64
  *
@@ -31,54 +79,7 @@ export function runMemoryVerbList(io, json) {
31
79
  }));
32
80
  return 0;
33
81
  }
34
- for (const line of [
35
- "cockpit memory — BLI Memory: the store, and this machine's registration.",
36
- "",
37
- "The store (Tower, over /api/memory/*):",
38
- " cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--json]",
39
- " cockpit memory save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>] [--json]",
40
- " the memory comes from stdin, or --file; never from the command line",
41
- " cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
42
- " cockpit memory forget <id> [--reason <label>] [--json]",
43
- " cockpit memory forget --content-stdin --container <tag> [--reason <label>] [--json]",
44
- " you can forget or update only your own memories (a super admin, any)",
45
- " cockpit memory private <id> [--json] only you and super admins can read this memory",
46
- " cockpit memory public <id> [--json] every member can read it again (how every memory starts)",
47
- " cockpit memory map [--json]",
48
- " cockpit memory repeats [--project <name-or-slug>] [--min 2..50] [--limit 1..200] [--json]",
49
- " subjects a person corrected an agent about, or ordered with emphasis, more than once",
50
- " cockpit memory rules [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
51
- " cockpit memory knowledge [--person <name>] [--project <name-or-slug>] [--limit <n>] [--per-project <n>] [--machine] [--json]",
52
- " plain sentences per project; --per-project 1..100 (default 15) is each project's own",
53
- " quota, --limit 1..1000 caps the ideas in all; --machine prints the agent wording with",
54
- " scores and spaces; --json is the whole answer, both wordings (--by-project is accepted)",
55
- " cockpit memory profile [--container <tag>] [--json]",
56
- " the compiled one-page briefing every session opens with; a read, never a compile",
57
- " cockpit memory consolidation [--container <tag>] [--json]",
58
- " what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
59
- " cockpit memory chain <id> [--json]",
60
- " cockpit memory exam [--night <YYYY-MM-DD>] [--misses] [--json]",
61
- " the nightly memory exam: purity, coverage, trend and worst project; yours alone",
62
- " cockpit memory source <id> [--query \"<words>\"] [--chars 200..6000] [--json]",
63
- " the memories the nightly pass folded into this one: who, when, what they said",
64
- " cockpit memory requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply] [--json]",
65
- " put a window of finished extractions back in the queue after the store refused",
66
- " their writes; super admins only, and a DRY RUN unless you pass --apply",
67
- "",
68
- "This machine:",
69
- " cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
70
- " cockpit memory status [--json] read the stored config back",
71
- "",
72
- "The experience log:",
73
- " cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
74
- "",
75
- "Search: --limit caps the MEMORIES (default 5, at most 20). Up to 3 Tower pages and",
76
- "tickets ride beside them whatever --limit says; for more, use `cockpit search`.",
77
- "Save accepts repeated --alias names (up to 16 names of 64 characters).",
78
- "Omit --container and this repository's own memory space is used — the same",
79
- "space the bli-memory MCP server and its hooks read and write.",
80
- ]) {
82
+ for (const line of VERB_LIST_LINES)
81
83
  writeLine(io.stdout, line);
82
- }
83
84
  return 0;
84
85
  }