@bli-cockpit/cli 0.2.97 → 0.2.99

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/dist/cli.js CHANGED
@@ -1,5 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCockpitCli } from "./commands/public-root.js";
3
+ import {
4
+ EXIT_DIED_MID_RUN,
5
+ installCrashGuard,
6
+ markCommandReturned,
7
+ } from "./crash-guard.js";
8
+
9
+ // BLI-4110. Success is EARNED, not assumed — see crash-guard.js. This
10
+ // entry point is GENERATED, so it must mirror src/cli.ts; the packed CLI
11
+ // is the one the fleet runs.
12
+ process.exitCode = EXIT_DIED_MID_RUN;
13
+ installCrashGuard();
3
14
 
4
15
  const exitCode = await runCockpitCli(process.argv.slice(2));
16
+
17
+ markCommandReturned();
5
18
  process.exitCode = exitCode;
@@ -1,3 +1,4 @@
1
+ import { withStage } from "../crash-guard.js";
1
2
  import { checkSingleInstallState, fixAuthState, fixRootState, readAuthState, readRootState, } from "./doctor-access.js";
2
3
  import { backfillCompletionStepState, checkBackfillState, checkDiskState, checkGcState, checkSyncState, fixBackfillState, fixDiskState, fixGcState, fixSyncState, syncBacklogDrainingVerdict, } from "./doctor-pipeline.js";
3
4
  import { checkMcpAnswersState } from "./doctor-mcp.js";
@@ -14,7 +15,7 @@ export async function runDoctorWithDeps(command, io, deps) {
14
15
  const context = { command, io, deps };
15
16
  const rows = [];
16
17
  for (const invariant of doctorInvariants()) {
17
- const checked = await invariant.check(context);
18
+ const checked = await withStage(`doctor:check:${invariant.id}`, () => invariant.check(context));
18
19
  if (checked.status === "ok" || checked.status === "skipped") {
19
20
  rows.push(checked);
20
21
  continue;
@@ -31,7 +32,10 @@ export async function runDoctorWithDeps(command, io, deps) {
31
32
  break;
32
33
  continue;
33
34
  }
34
- const fixed = await invariant.fix(context, checked);
35
+ // BLI-4110: named so a crash inside a repair says WHICH repair. The
36
+ // 2026-09-09 incident printed a bare `read ENOTCONN` stack and the only
37
+ // way to place it was the log line that happened to precede it.
38
+ const fixed = await withStage(`doctor:fix:${invariant.id}`, () => invariant.fix(context, checked));
35
39
  rows.push({ ...fixed, fixed: fixed.status !== "fail" });
36
40
  if (fixed.hardStop)
37
41
  break;
@@ -11,6 +11,7 @@
11
11
  import { DEFAULT_AUTOSTART_INTERVAL_SECONDS } from "../autostart.js";
12
12
  import { assertNoPositionals, normalizeUrl, optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
13
13
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
14
+ import { MEMORY_STORE_ACTIONS, parseMemoryStoreArgs, } from "./local-args-tower-memory.js";
14
15
  export function parseStatusArgs(args) {
15
16
  const values = parseNamedArgs(args, {
16
17
  allowedFlags: [
@@ -164,11 +165,30 @@ export function parseAgentRulesArgs(args) {
164
165
  };
165
166
  }
166
167
  /**
167
- * BLI-3580. `install` is the default action on purpose: this command exists to
168
- * be run unasked (by `do-everything` and by the sync tick), and a bare
169
- * `cockpit memory` should do the thing rather than print usage.
168
+ * BLI-3580, amended by BLI-4047.
169
+ *
170
+ * `cockpit memory` is now SIX verbs behind one noun: the two that configure
171
+ * this machine (`install`, `status`), the experience log (`log`), and the four
172
+ * that read and write the memory store itself (`search`, `save`, `update`,
173
+ * `forget`, parsed next door in `local-args-tower-memory.ts`).
174
+ *
175
+ * **A bare `cockpit memory` prints the verb list; it no longer installs.**
176
+ * BLI-3580 made `install` the default because the command is run unasked by
177
+ * `do-everything` and by the sync tick — but those callers build
178
+ * `{ kind: "memory", action: "install" }` in code (`doctor-registration.ts`,
179
+ * `sync-followups-memory.ts`, `onboard-completion.ts`) and never pass through
180
+ * this parser. The only caller the default ever served was a PERSON typing the
181
+ * noun to find out what it does, and what they got was an MCP server and three
182
+ * hooks registered on their machine. Installing stays one word away.
170
183
  */
171
184
  export function parseMemoryArgs(args) {
185
+ // The verb word first, always: reading it off `args[0]` rather than "the
186
+ // first token without a leading dash" is what keeps `cockpit memory --home
187
+ // /tmp/x status` from being read as the verb `/tmp/x`.
188
+ const first = args[0];
189
+ if (first !== undefined && MEMORY_STORE_ACTIONS.has(first)) {
190
+ return parseMemoryStoreArgs(first, args);
191
+ }
172
192
  const values = parseNamedArgs(args, {
173
193
  allowedFlags: ["--home", "--dashboard-url", "--dry-run", "--json", "--store", "--reason-stdin"],
174
194
  valueFlags: ["--home", "--dashboard-url", "--store"],
@@ -192,9 +212,10 @@ export function parseMemoryArgs(args) {
192
212
  if (values.positionals.length > 1) {
193
213
  throw new Error("memory accepts at most one action (install|status).");
194
214
  }
195
- const action = values.positionals[0] ?? "install";
196
- if (action !== "install" && action !== "status") {
197
- throw new Error("memory action must be install or status.");
215
+ // BLI-4047: no positional means "tell me what this noun does", not "install".
216
+ const action = values.positionals[0] ?? "help";
217
+ if (action !== "install" && action !== "status" && action !== "help") {
218
+ throw new Error("memory action must be install, status, help, log, search, save, update, or forget.");
198
219
  }
199
220
  return {
200
221
  kind: "memory",
@@ -14,9 +14,17 @@ import { isTeamDeviceRevokeReasonLabel, TEAM_DEVICE_REVOKE_REASON_LABELS, } from
14
14
  /** The shortest prefix `cockpit scout start` will resolve. Below this, ids collide. */
15
15
  export const SCOUT_MIN_PREFIX_LENGTH = 6;
16
16
  /**
17
- * `cockpit scout [--days <n>]` reads the board; `cockpit scout start|dismiss|undo
18
- * <id>` moves one card. Positional shape modelled on `autostart`: an optional
19
- * action word first, then what it acts on.
17
+ * `cockpit scout [board] [--days <n>]` reads the board; `cockpit scout
18
+ * start|dismiss|undo <id>` moves one card. Positional shape modelled on
19
+ * `autostart`: an optional action word first, then what it acts on.
20
+ *
21
+ * `board` is spellable as well as implied (BLI-4048). The parsed command has
22
+ * always carried `action: "board"`, the MCP twin has always been `scout_board`,
23
+ * and three documents printed `cockpit scout board` under a "CLI verb" heading
24
+ * while the parser answered "scout action must be start, dismiss, or undo" —
25
+ * the promise was older than the refusal. Every sibling noun (`ops status`,
26
+ * `slack coverage`, `notes list`) lets a person name the read it defaults to,
27
+ * so scout does too, and the word costs one branch.
20
28
  */
21
29
  export function parseScoutArgs(args) {
22
30
  const values = parseNamedArgs(args, {
@@ -24,7 +32,7 @@ export function parseScoutArgs(args) {
24
32
  valueFlags: ["--home", "--dashboard-url", "--days"],
25
33
  });
26
34
  if (values.positionals.length > 2) {
27
- throw new Error("scout accepts at most an action (start|dismiss|undo) and one experiment id.");
35
+ throw new Error("scout accepts at most an action (board|start|dismiss|undo) and one experiment id.");
28
36
  }
29
37
  const [rawAction, rawRef] = values.positionals;
30
38
  const base = {
@@ -33,10 +41,13 @@ export function parseScoutArgs(args) {
33
41
  days: optionalPositiveInteger(values.flags.get("--days"), "--days"),
34
42
  json: values.booleans.has("--json"),
35
43
  };
36
- if (!rawAction)
44
+ if (!rawAction || rawAction === "board") {
45
+ if (rawRef)
46
+ throw new Error("scout board reads the whole board; it takes no experiment id.");
37
47
  return { kind: "scout", action: "board", ...base };
48
+ }
38
49
  if (rawAction !== "start" && rawAction !== "dismiss" && rawAction !== "undo") {
39
- throw new Error("scout action must be start, dismiss, or undo.");
50
+ throw new Error("scout action must be board, start, dismiss, or undo.");
40
51
  }
41
52
  const experimentRef = optionalNonEmpty(rawRef);
42
53
  if (!experimentRef) {
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `cockpit memory search|save|update|forget` — the argument table (BLI-4047).
3
+ *
4
+ * BLI Memory was the one Tower surface with no command-line door: the browser
5
+ * had one, the `bli-memory` MCP server had one, and a person or a script at a
6
+ * terminal had nothing. These four verbs are the twins of that server's four
7
+ * tools, over the SAME `/api/memory/*` doors.
8
+ *
9
+ * Two rules this table exists to enforce, both of them the same discipline
10
+ * `cockpit docs create` and `cockpit notes paste` already follow:
11
+ *
12
+ * - **A body never travels on the command line.** `save`, `update` and
13
+ * `forget --content` read their text from stdin (or `--file`, which is the
14
+ * safest way to say it on Windows). An argument list is visible in `ps`,
15
+ * lands in a shell history file, and is quoted wrong on the first line that
16
+ * contains a newline. A QUERY is not a body — `search` takes one
17
+ * positional, the same way `cockpit search` and `cockpit docs list --query`
18
+ * do — but the text of a memory always is.
19
+ * - **The container tag is derived, not typed.** Omit `--container` and the
20
+ * tag comes from this repository, resolved by `@bli-cockpit/memory-mcp`'s
21
+ * own resolver, so a memory saved from the terminal lands in the same space
22
+ * the MCP server and the hooks write to. `--container` may be repeated on
23
+ * `search` alone, because searching two spaces is meaningful and saving
24
+ * into two is not.
25
+ *
26
+ * The verbs live in their own `kind` (`memory-store`) rather than widening
27
+ * `kind: "memory"`, because that command acts on THIS MACHINE (it registers an
28
+ * MCP server and three hooks) and these act on Tower. `memory-store.ts` runs
29
+ * them; `parseMemoryArgs` in `local-args-collector-status.ts` routes to here.
30
+ */
31
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
32
+ export const MEMORY_STORE_ACTIONS = new Set([
33
+ "search",
34
+ "save",
35
+ "update",
36
+ "forget",
37
+ ]);
38
+ const ALLOWED_FLAGS = [
39
+ "--home",
40
+ "--dashboard-url",
41
+ "--workspace",
42
+ "--repo",
43
+ "--container",
44
+ "--limit",
45
+ "--no-recent",
46
+ "--custom-id",
47
+ "--extract",
48
+ "--file",
49
+ "--content-stdin",
50
+ "--reason",
51
+ "--json",
52
+ ];
53
+ const VALUE_FLAGS = [
54
+ "--home",
55
+ "--dashboard-url",
56
+ "--workspace",
57
+ "--repo",
58
+ "--container",
59
+ "--limit",
60
+ "--custom-id",
61
+ "--file",
62
+ "--reason",
63
+ ];
64
+ export function parseMemoryStoreArgs(action, args) {
65
+ const values = parseNamedArgs(args, { allowedFlags: ALLOWED_FLAGS, valueFlags: VALUE_FLAGS });
66
+ const rest = values.positionals.slice(1);
67
+ const containers = values.flagValues.get("--container") ?? [];
68
+ if (action !== "search" && containers.length > 1) {
69
+ throw new Error(`memory ${action} takes at most one --container.`);
70
+ }
71
+ if (action !== "search" && (values.flags.has("--limit") || values.booleans.has("--no-recent"))) {
72
+ throw new Error("--limit and --no-recent only apply to memory search.");
73
+ }
74
+ if (action !== "save" && (values.flags.has("--custom-id") || values.booleans.has("--extract"))) {
75
+ throw new Error("--custom-id and --extract only apply to memory save.");
76
+ }
77
+ if (action !== "forget" && (values.flags.has("--reason") || values.booleans.has("--content-stdin"))) {
78
+ throw new Error("--reason and --content-stdin only apply to memory forget.");
79
+ }
80
+ if (values.flags.has("--file") && action !== "save" && action !== "update") {
81
+ throw new Error("--file only applies to memory save and memory update.");
82
+ }
83
+ const command = {
84
+ kind: "memory-store",
85
+ action,
86
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
87
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
88
+ workspace: optionalNonEmpty(workRootFlagValue(values)),
89
+ containerTags: containers
90
+ .map((tag) => tag.trim())
91
+ .filter((tag) => tag.length > 0),
92
+ json: values.booleans.has("--json"),
93
+ };
94
+ if (action === "search") {
95
+ // Every remaining positional joined, so `cockpit memory search what did we
96
+ // decide about pagination` needs no quotes — the same shape `cockpit
97
+ // search` and `cockpit cal find` already have.
98
+ const query = rest.join(" ").trim();
99
+ if (!query)
100
+ throw new Error("memory search needs a question, e.g. `cockpit memory search \"pagination rule\"`.");
101
+ return { ...command, action, query, limit: readLimit(values.flags.get("--limit")), includeRecent: !values.booleans.has("--no-recent") };
102
+ }
103
+ if (action === "save") {
104
+ if (rest.length > 0) {
105
+ throw new Error("memory save reads the memory from stdin or --file, never from the command line: `echo \"...\" | cockpit memory save`.");
106
+ }
107
+ return {
108
+ ...command,
109
+ action,
110
+ customId: optionalNonEmpty(values.flags.get("--custom-id")),
111
+ extract: values.booleans.has("--extract"),
112
+ filePath: optionalNonEmpty(values.flags.get("--file")),
113
+ };
114
+ }
115
+ if (action === "update") {
116
+ const memoryId = optionalNonEmpty(rest[0]);
117
+ if (!memoryId)
118
+ throw new Error("memory update needs the memory id `cockpit memory search` printed.");
119
+ if (rest.length > 1) {
120
+ throw new Error("memory update takes one id; the replacement text comes from stdin or --file, never from the command line.");
121
+ }
122
+ return { ...command, action, memoryId, filePath: optionalNonEmpty(values.flags.get("--file")) };
123
+ }
124
+ const memoryId = optionalNonEmpty(rest[0]);
125
+ if (rest.length > 1)
126
+ throw new Error("memory forget takes one id, or --content-stdin with a --container.");
127
+ const contentStdin = values.booleans.has("--content-stdin");
128
+ if (memoryId && contentStdin) {
129
+ throw new Error("memory forget takes an id OR --content-stdin, never both — they name different rows.");
130
+ }
131
+ if (!memoryId && !contentStdin) {
132
+ throw new Error("memory forget needs an id, or --content-stdin plus --container to forget by exact text.");
133
+ }
134
+ return {
135
+ ...command,
136
+ action,
137
+ memoryId,
138
+ contentStdin,
139
+ reason: optionalNonEmpty(values.flags.get("--reason")),
140
+ };
141
+ }
142
+ /** 1..20, the same window the MCP tool and the door both accept. */
143
+ function readLimit(value) {
144
+ if (value === undefined)
145
+ return undefined;
146
+ const parsed = Number.parseInt(value, 10);
147
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 20) {
148
+ throw new Error("--limit must be between 1 and 20.");
149
+ }
150
+ return parsed;
151
+ }
@@ -162,6 +162,7 @@ const NOTES_ACTIONS = new Set([
162
162
  "share",
163
163
  "unshare",
164
164
  "move",
165
+ "place",
165
166
  ]);
166
167
  /** Actions whose first positional is the note it acts on. */
167
168
  const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
@@ -169,6 +170,7 @@ const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
169
170
  "share",
170
171
  "unshare",
171
172
  "move",
173
+ "place",
172
174
  ]);
173
175
  export function parseNotesArgs(args) {
174
176
  const values = parseNamedArgs(args, {
@@ -180,6 +182,7 @@ export function parseNotesArgs(args) {
180
182
  "--exclude",
181
183
  "--to",
182
184
  "--clear-shelf",
185
+ "--apply",
183
186
  "--series",
184
187
  "--kind",
185
188
  "--since",
@@ -207,7 +210,7 @@ export function parseNotesArgs(args) {
207
210
  const first = values.positionals[0];
208
211
  const action = (first === undefined ? "list" : first);
209
212
  if (!NOTES_ACTIONS.has(action)) {
210
- throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, or move.`);
213
+ throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, move, or place.`);
211
214
  }
212
215
  const rest = values.positionals.slice(first === undefined ? 0 : 1);
213
216
  const json = values.booleans.has("--json");
@@ -259,6 +262,8 @@ export function parseNotesArgs(args) {
259
262
  exclude: optionalNonEmpty(values.flags.get("--exclude")),
260
263
  ...(to ? { to } : {}),
261
264
  ...(clearShelf ? { clearShelf } : {}),
265
+ // BLI-4058. `place` says where a note belongs; only `--apply` moves it.
266
+ ...(values.booleans.has("--apply") ? { apply: true } : {}),
262
267
  series: optionalNonEmpty(values.flags.get("--series")),
263
268
  meetingKind: optionalNonEmpty(values.flags.get("--kind")),
264
269
  since: optionalNonEmpty(values.flags.get("--since")),
@@ -0,0 +1,8 @@
1
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
2
+ export function parseUsageArgs(args) {
3
+ const values = parseNamedArgs(args, { allowedFlags: ["--since", "--until", "--include-automated", "--home", "--dashboard-url", "--json"], valueFlags: ["--since", "--until", "--home", "--dashboard-url"] });
4
+ const action = values.positionals[0] ?? "people";
5
+ if (action !== "people" || values.positionals.length > 1)
6
+ throw new Error("usage takes one verb: people.");
7
+ return { kind: "usage", action: "people", since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
8
+ }
@@ -35,4 +35,5 @@ export { ISSUE_STATES, parseIssueArgs, parseProjectArgs, } from "./local-args-to
35
35
  export { SEARCH_KINDS, parseSearchArgs } from "./local-args-tower-search.js";
36
36
  export { parseModelsArgs } from "./local-args-tower-models.js";
37
37
  export { parseMailArgs } from "./local-args-tower-mail.js";
38
- export { parseCalArgs } from "./local-args-tower-cal.js";
38
+ export { parseCalArgs } from "./local-args-tower-cal.js";
39
+ export { parseUsageArgs } from "./local-args-tower-usage.js";
@@ -16,7 +16,7 @@
16
16
  * verbatim, no logic change.
17
17
  */
18
18
  import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
19
- import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
19
+ import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, } from "./local-args-tower.js";
20
20
  // `normalizeUrl` has always been part of this module's surface — `local.ts` and
21
21
  // `local-auth.ts` import it from here — so it stays exported from this address
22
22
  // even though it now lives next door. The same goes for the four names the
@@ -115,6 +115,8 @@ export function parseLocalArgs(argv) {
115
115
  return parseSearchArgs(argv.slice(1));
116
116
  case "release":
117
117
  return parseReleaseArgs(argv.slice(1));
118
+ case "usage":
119
+ return parseUsageArgs(argv.slice(1));
118
120
  default:
119
121
  throw new Error(`Unknown local command: ${command ?? ""}`);
120
122
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The `cockpit memory` family's command shapes (BLI-4047).
3
+ *
4
+ * Split out of `local-command-shapes.ts` the same way `SearchCommandShape` and
5
+ * the product shapes were: that file crossed the repo's 500-line band floor
6
+ * when the store verbs arrived, and one noun's shapes are exactly the kind of
7
+ * thing that reads better in its own file than as a third of a 540-line union.
8
+ *
9
+ * Three kinds, and the difference between them is the point:
10
+ *
11
+ * `memory` install / status / help — acts on THIS MACHINE, writing an
12
+ * MCP registration and three hooks into a home directory.
13
+ * `memory-store` search / save / update / forget — acts on TOWER, over
14
+ * `/api/memory/*`, the same doors the `bli-memory` MCP
15
+ * server and the browser call.
16
+ * `memory-log` the agent-experience opinion, part local file part Tower.
17
+ *
18
+ * They are separate kinds rather than one widening `action` union because they
19
+ * refuse different things, run on different credentials, and one of them can
20
+ * be run unasked by a scheduler while the others never are.
21
+ */
22
+ export {};
@@ -11,6 +11,15 @@
11
11
  * Every string moved verbatim. Help output is what an intern pastes back when
12
12
  * something breaks, so it is a user-visible contract like any other.
13
13
  */
14
+ import { SEARCH_KINDS } from "./local-args-tower-search.js";
15
+ /**
16
+ * The corpora `cockpit search` can name, spelled the way `--kind` takes them.
17
+ * Read off `SEARCH_KINDS` rather than typed out: the help string said five for
18
+ * the two months `mail` and `cal` were searchable, so a person was told the
19
+ * door was narrower than it is (BLI-4048).
20
+ */
21
+ const SEARCH_KIND_LIST = SEARCH_KINDS.join(",");
22
+ const SEARCH_CORPORA_COUNT = SEARCH_KINDS.length;
14
23
  /** One entry per Tower noun, in the order `cockpit --help` lists them. */
15
24
  export const TOWER_COMMAND_HELP = [
16
25
  [
@@ -130,12 +139,27 @@ export const TOWER_COMMAND_HELP = [
130
139
  "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
131
140
  ],
132
141
  ],
142
+ [
143
+ "usage",
144
+ [
145
+ "Usage: cockpit usage people [--since 30d|<iso>] [--until <iso>] [--include-automated] [--json]",
146
+ "",
147
+ "Claude Code and Codex usage per person: sessions observed and extracted, tokens (total, output,",
148
+ "and the input / cache split when the row carries it), and an API list-price equivalent that is",
149
+ "labelled as such and is never actual spend. A super_admin sees everyone; a member sees their own row.",
150
+ "--since takes 7d, 30d, 90d or an ISO timestamp; --until an ISO timestamp (default now).",
151
+ "--include-automated adds harness, subagent and scheduled sessions, which are excluded by default.",
152
+ "It presses the same door as the /usage page and the usage_people MCP tool (GET /api/usage/people).",
153
+ "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
154
+ ],
155
+ ],
133
156
  [
134
157
  "search",
135
158
  [
136
- 'Usage: cockpit search "<words>" [--kind doc,msg,issue,note,memory] [--limit <n>] [--json]',
159
+ `Usage: cockpit search "<words>" [--kind ${SEARCH_KIND_LIST}] [--limit <n>] [--json]`,
137
160
  "",
138
- "One bar over five corpora: documents, messages, issues, meeting notes and memory.",
161
+ `One bar over ${SEARCH_CORPORA_COUNT} corpora: documents, messages, issues, meeting notes, mail, calendar`,
162
+ "events and memory.",
139
163
  "It presses the SAME door the browser's search bar presses (GET /api/search), so what you",
140
164
  "read here is the same row, the same ranking and the same snippet a person sees in Tower.",
141
165
  "",
@@ -144,12 +168,14 @@ export const TOWER_COMMAND_HELP = [
144
168
  "Quoted phrases and -word work inside the query itself — the query goes to Postgres's",
145
169
  "websearch parser, which shrugs at anything a person can type instead of raising.",
146
170
  "",
147
- "--kind narrows to one or more corpora, comma separated. Omit it and all five are searched.",
171
+ `--kind narrows to one or more corpora, comma separated: ${SEARCH_KIND_LIST}.`,
172
+ `Omit it and all ${SEARCH_CORPORA_COUNT} are searched.`,
148
173
  "--limit caps the number of results (default 20, maximum 50).",
149
174
  "--json writes the door's whole answer to stdout, hits, per-kind counts, failures and all.",
150
175
  "",
151
- "Every result is scoped by what YOU may read: four of the five corpora are searched on your",
152
- "own database session, so a document or a channel you cannot open is not in the list.",
176
+ `Every result is scoped by what YOU may read: ${SEARCH_CORPORA_COUNT - 1} of the ${SEARCH_CORPORA_COUNT} corpora are`,
177
+ "searched on your own database session, so a document or a channel you cannot open is not in",
178
+ "the list; memory runs on the service role behind the memory doors' own gate.",
153
179
  "",
154
180
  "A corpus that could not ANSWER gets its own line, separately from \"nothing matched\" —",
155
181
  "those are different facts and folding them together would let a broken search read as silence.",
@@ -305,12 +305,14 @@ export function localSubcommandHelp(command) {
305
305
  [
306
306
  "scout",
307
307
  [
308
- "Usage: cockpit scout [start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
308
+ "Usage: cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
309
309
  "",
310
310
  "Prints the same Scout board the Tower page shows, in the same words: the standing",
311
311
  "watch line, the cards waiting on a decision, and the raw signal watch underneath.",
312
312
  "This is the board itself. To ask a QUESTION about it — what it means, whether it is",
313
313
  "worth doing here — use `cockpit jarvis`, whose readScout tool reads the same rows.",
314
+ "`cockpit scout board` and a bare `cockpit scout` are the same command; the MCP twin",
315
+ "is `scout_board` on the bli-tower server, over the same GET /api/cockpit/scout door.",
314
316
  "--days <n> widens or narrows the window (1 to 90; the board defaults to 14).",
315
317
  "Reading follows the Scout page audience setting, so whoever can open /scout can run this.",
316
318
  "start, dismiss, and undo move one card and are super_admin actions on every surface;",
@@ -461,6 +463,7 @@ export function localSubcommandHelp(command) {
461
463
  "share <id> [--yes] — let everyone signed in read the statements that are safe to share. Asks first in a terminal; --yes is required without one, and --json implies --yes.",
462
464
  "unshare <id> — take it back. Never asks: it only ever narrows who can read.",
463
465
  "move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
466
+ "place <id> [--apply] — say which shelf this note belongs on and why, from the meeting it is part of, who was in the room and its own name. Moves nothing without --apply, and never overrules a shelf somebody typed.",
464
467
  "A large note is read by a model on the server and can take a couple of minutes; the terminal says so before it waits.",
465
468
  "--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
466
469
  "Sharing and moving need a signed-in session the database can see. If this deployment cannot mint one, they are refused as needs_signed_in_session rather than done with no permission check — and reads say when they came back narrower than the browser's.",
@@ -555,15 +558,32 @@ export function localSubcommandHelp(command) {
555
558
  [
556
559
  "memory",
557
560
  [
558
- "Usage: cockpit memory [install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--dashboard-url <url>] [--dry-run] [--json]",
559
- "",
560
- "Registers BLI Memory on this machine: the `bli-memory` MCP server plus the",
561
+ "Usage: cockpit memory [search|save|update|forget|install|status|log] [flags]",
562
+ "",
563
+ "The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
564
+ " search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
565
+ " repeat --container to search more than one space; omit it for this repository's own.",
566
+ " save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>]",
567
+ " the memory comes from stdin or --file, never from the command line:",
568
+ " `echo \"Edward decided X on 2026-09-10\" | cockpit memory save`.",
569
+ " --custom-id makes a repeat save update instead of duplicating;",
570
+ " --extract distils atomic facts first (one model call) instead of storing as given.",
571
+ " update <id> [--file <path>] replacement text on stdin; the old row is kept as history.",
572
+ " forget <id> [--reason <label>]",
573
+ " forget --content-stdin --container <tag> [--reason <label>] forget by exact text.",
574
+ "Refusals keep the door's own label — needs_rls_client, unknown_memory,",
575
+ "already_superseded, no_match — and --json prints the door's answer unchanged.",
576
+ "",
577
+ "BARE `cockpit memory` PRINTS THIS LIST AND DOES NOTHING ELSE (BLI-4047).",
578
+ "It used to run the installer; installing is now `cockpit memory install`.",
579
+ "",
580
+ "install/status register BLI Memory on this machine: the `bli-memory` MCP server plus the",
561
581
  "recall/save hooks, for Claude Code (~/.claude.json, ~/.claude/settings.json)",
562
582
  "and for Codex (~/.codex/config.toml, ~/.codex/skills/bli-memory/).",
563
583
  "Idempotent: it merges with what is already there, never duplicates its own",
564
584
  "entries, and reads the stored config back before reporting success.",
565
585
  "`do-everything` runs it, and the sync tick re-runs it at most once a day.",
566
- "Action defaults to `install`. See docs/runbooks/bli-memory-install.md.",
586
+ "See docs/runbooks/bli-memory-install.md.",
567
587
  "log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
568
588
  "Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
569
589
  "Reasons: one line, max 500 characters; no prompts, memory bodies or secrets.",
@@ -8,6 +8,7 @@
8
8
  * user-visible contract like any other.
9
9
  */
10
10
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
11
+ import { SEARCH_KINDS } from "./local-args-tower-search.js";
11
12
  import { localSubcommandHelp } from "./local-help-commands.js";
12
13
  export const rootCommandNames = new Set([
13
14
  "onboard",
@@ -51,6 +52,7 @@ export const rootCommandNames = new Set([
51
52
  "project",
52
53
  "search",
53
54
  "release",
55
+ "usage",
54
56
  ]);
55
57
  export function localCommandHelp(command) {
56
58
  if (command)
@@ -72,7 +74,7 @@ export function localCommandHelp(command) {
72
74
  " cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--trace <id|last>] [--dashboard-url <url>] [--json [--show-approval-code]]",
73
75
  " cockpit model [show|set <provider:model>] [--json]",
74
76
  " cockpit models [list|show <provider:model>|compare <provider:model> <provider:model> [...]] [--highlight] [--dashboard-url <url>] [--json]",
75
- " cockpit scout [start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
77
+ " cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
76
78
  " cockpit ops [status [--job <id>] [--skips] [--memory] [--models] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
77
79
  " cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] | read [--person <p>] [--channel <c>] [--query <text>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>]] [--json]",
78
80
  " cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | cli-floor [<version>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
@@ -81,14 +83,14 @@ export function localCommandHelp(command) {
81
83
  " cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
82
84
  " cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
83
85
  " cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
84
- " cockpit notes [list|show <id>|shelf|shelves|upload <paths...>|paste|share <id>|unshare <id>|move <id>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--yes] [--dashboard-url <url>] [--json]",
86
+ " cockpit notes [list|show <id>|shelf|shelves|upload <paths...>|paste|share <id>|unshare <id>|move <id>|place <id>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--apply] [--yes] [--dashboard-url <url>] [--json]",
85
87
  " cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
86
88
  " cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
87
89
  " cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
88
90
  " cockpit serve [--port <port>] [--workspace <path>]",
89
91
  " cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
90
92
  " cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
91
- " cockpit memory [install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--dashboard-url <url>] [--dry-run] [--json]",
93
+ " cockpit memory [search \"<question>\"|save|update <id>|forget <id>|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
92
94
  " cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
93
95
  " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>] [--parent <id>|root|--clear-parent] [--query <text>] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
94
96
  " cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--dashboard-url <url>] [--json]",
@@ -96,8 +98,9 @@ export function localCommandHelp(command) {
96
98
  " cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--dashboard-url <url>] [--json]",
97
99
  " cockpit cal [today|week|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
98
100
  " cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
99
- " cockpit search \"<words>\" [--kind doc,msg,issue,note,memory] [--limit <n>] [--dashboard-url <url>] [--json]",
101
+ ` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
100
102
  " cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
103
+ " cockpit usage people [--since 30d|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
101
104
  "",
102
105
  `Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
103
106
  ].join("\n");