@bli-cockpit/cli 0.2.140 → 0.2.142

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.
@@ -49,6 +49,8 @@ export async function runDocs(command, io) {
49
49
  return createDoc(command, door);
50
50
  case "update":
51
51
  return updateDoc(command, door);
52
+ case "delete":
53
+ return deleteDocs(command, door);
52
54
  case "link":
53
55
  return linkDoc(command, door);
54
56
  case "sync-status":
@@ -234,6 +236,75 @@ async function updateDoc(command, door) {
234
236
  writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
235
237
  return 0;
236
238
  }
239
+ /**
240
+ * BLI‑10555: soft-deletes one or more documents — `DELETE
241
+ * /api/docs/documents/[id]`, the same door the browser's own delete calls.
242
+ * Recoverable (only a super_admin clearing `deleted_at` brings it back, no
243
+ * self-service undo) and never cascades: a deleted document's children keep
244
+ * their own `parent_id` and stay exactly where they were.
245
+ *
246
+ * Mirrors `readDoc`'s multi-ref shape (BLI-10450): one shared `docs list`
247
+ * resolves every ref, then one `DELETE` per document — one bad ref never
248
+ * loses the rest of the batch.
249
+ */
250
+ async function deleteDocs(command, door) {
251
+ const refs = command.docRefs && command.docRefs.length > 0 ? command.docRefs : [command.docRef ?? ""];
252
+ const resolved = await resolveDocIds(door, refs);
253
+ if (resolved.status === "list_failed")
254
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
255
+ const results = await Promise.all(refs.map((ref) => deleteOneDocument(door, ref, resolved.ids.get(ref))));
256
+ const failed = results.filter((result) => !result.ok);
257
+ writeLine(door.io.stderr, `${TAG} delete ${JSON.stringify({ requested: refs.length, deleted: results.length - failed.length, refused: failed.length })}`);
258
+ if (refs.length === 1) {
259
+ const [result] = results;
260
+ if (!result || !result.ok) {
261
+ return failAgentDoor(door, TAG, result?.reason ?? "document_not_found_or_unreadable", result?.detail ?? "Not found.");
262
+ }
263
+ if (door.json)
264
+ return emitAgentDoor(door, { ok: true, document_id: result.id });
265
+ writeLine(door.io.stdout, `Deleted ${result.id}. Recoverable by a super_admin; its children stay in place.`);
266
+ return 0;
267
+ }
268
+ if (door.json) {
269
+ return emitAgentDoor(door, {
270
+ ok: failed.length === 0,
271
+ documents: results.map((result) => result.ok
272
+ ? { ref: result.ref, ok: true, document_id: result.id }
273
+ : { ref: result.ref, ok: false, error: result.reason, detail: result.detail }),
274
+ }, failed.length === 0 ? 0 : 1);
275
+ }
276
+ results.forEach((result) => {
277
+ if (result.ok) {
278
+ writeLine(door.io.stdout, `Deleted ${result.ref} (${result.id}).`);
279
+ }
280
+ else {
281
+ writeLine(door.io.stderr, `${TAG} refused ${JSON.stringify({ ref: result.ref, reason: result.reason })}`);
282
+ writeLine(door.io.stdout, `${result.ref}: ${result.detail}`);
283
+ }
284
+ });
285
+ writeLine(door.io.stdout, "");
286
+ writeLine(door.io.stdout, `${results.length - failed.length}/${results.length} document(s) deleted. Recoverable by a super_admin; children stay in place.`);
287
+ return failed.length === 0 ? 0 : 1;
288
+ }
289
+ async function deleteOneDocument(door, ref, id) {
290
+ if (!id) {
291
+ return {
292
+ ref,
293
+ ok: false,
294
+ reason: "document_not_found_or_unreadable",
295
+ detail: `That document does not exist, or you cannot delete it: ${ref}`,
296
+ };
297
+ }
298
+ const answer = await askAgentDoor(door, {
299
+ path: `/api/docs/documents/${encodeURIComponent(id)}`,
300
+ method: "DELETE",
301
+ label: "docs delete",
302
+ timeoutMs: WRITE_DEADLINE_MS,
303
+ });
304
+ if (!answer.ok)
305
+ return { ref, ok: false, reason: answer.reason, detail: answer.detail };
306
+ return { ref, ok: true, id };
307
+ }
237
308
  async function updateDocument(command, door, documentId, bodyMarkdown) {
238
309
  return askAgentDoor(door, {
239
310
  path: `/api/docs/documents/${encodeURIComponent(documentId)}`,
@@ -7,10 +7,12 @@
7
7
  * added both without stretching either existing family's own doc comment.
8
8
  */
9
9
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
10
- const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "link", "sync-status"]);
10
+ const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status"]);
11
11
  // BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
12
12
  // it takes SEVERAL references now, so it no longer shares the "exactly one"
13
- // rule `update`/`link` still enforce.
13
+ // rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
14
+ // (the same door, called once per ref), so it joined `read`'s branch rather
15
+ // than this set.
14
16
  const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link"]);
15
17
  export function parseDocsArgs(args) {
16
18
  const values = parseNamedArgs(args, {
@@ -52,6 +54,11 @@ export function parseDocsArgs(args) {
52
54
  const { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
53
55
  if (action === "read")
54
56
  validateDocsReadTargets(docRefs, parentId, bodies);
57
+ // BLI‑10555: `delete` needs no `--parent`/`--bodies` shape, only at least
58
+ // one reference — `read`'s emptier validation does not apply.
59
+ if (action === "delete" && (!docRefs || docRefs.length === 0)) {
60
+ throw new Error("docs delete needs one or more document ids/slugs.");
61
+ }
55
62
  return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link);
56
63
  }
57
64
  /**
@@ -104,7 +111,7 @@ function parseDocsAction(positionals) {
104
111
  const first = positionals[0];
105
112
  const action = (first === undefined ? "list" : first);
106
113
  if (!DOCS_ACTIONS.has(action)) {
107
- throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, link, or sync-status.`);
114
+ throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, or sync-status.`);
108
115
  }
109
116
  return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
110
117
  }
@@ -112,8 +119,10 @@ function parseDocsDocumentReference(action, rest) {
112
119
  // BLI-10450: `read` alone takes several references in one call — an agent
113
120
  // reading a folder's worth of documents used to pay a resolve-then-read
114
121
  // pair PER document. Emptiness is validated later, once `--parent`/
115
- // `--bodies` are known (`validateDocsReadTargets`).
116
- if (action === "read") {
122
+ // `--bodies` are known (`validateDocsReadTargets`). BLI‑10555: `delete`
123
+ // shares this shape — several refs, one shared resolve, one DELETE each —
124
+ // its own emptiness check runs right after this function returns.
125
+ if (action === "read" || action === "delete") {
117
126
  const refs = rest.map((token) => optionalNonEmpty(token)).filter((token) => Boolean(token));
118
127
  return { docRef: refs[0], docRefs: refs.length > 0 ? refs : undefined };
119
128
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The long-form `cockpit <command> --help` text for JARVIS, the model cards,
3
+ * Scout and the ops doors.
4
+ *
5
+ * Split out of `local-help-commands.ts` (BLI-10445) when that file crossed the
6
+ * 500-line readability ratchet, along the same seam BLI-3709 drew: these are
7
+ * Tower surfaces, not the collector's own commands.
8
+ *
9
+ * Every string moved verbatim. Help output is what an intern pastes back when
10
+ * something breaks, so it is a user-visible contract like any other.
11
+ */
12
+ export const TOWER_OPS_COMMAND_HELP = [
13
+ [
14
+ "jarvis",
15
+ [
16
+ "Usage: 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]]",
17
+ "",
18
+ "Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
19
+ "--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
20
+ "--date <YYYY-MM-DD> binds the page that was live on one of that person's days (`today` and `yesterday` work too), so the turn is about that day's page instead of the newest one. A day with nothing compiled binds no page and JARVIS says so rather than answering from a different day.",
21
+ "--model <key> requests one provider:model pair (e.g. openai:gpt-5.6-terra); an unrecognised key is refused by the dashboard, not this command.",
22
+ "--image <path> (alias --file) attaches one local PNG, JPEG, or WEBP under 10 MB with the question, the same one-image gate the JARVIS panel uses. A bad path, an unsupported type, or an oversized file is refused with its own plain sentence — never a stack trace.",
23
+ "Run with no question for an interactive terminal conversation.",
24
+ "Agents can pass --prompt, positional text, or pipe one question on stdin.",
25
+ "Each answer prints a per-tool trace line and, only on a fallback or model mismatch, one model receipt line.",
26
+ "Trace lines arrive live while JARVIS works; pass --no-stream to wait for the whole answer in one piece instead.",
27
+ "--json writes one machine-readable response (including trace and model) to stdout; operational metadata stays on stderr. A --json run never streams fragments, whichever mode it is in.",
28
+ "A --json turn is treated as an AGENT surface: if it proposes a coding task, JARVIS answers with proposal_id and NOT the eight-character approval code, because a code a program can read is a code it could approve with. The person fetches the code themself — a Tower browser tab, their Slack DM, or an interactive `cockpit jarvis` — and hands it back. Pass --show-approval-code when a script really is driving a dispatch for a person; the run says on stderr that it did.",
29
+ "That object carries answer, sources (the Source: lines inside it), turn_id (pass it to --trace), thread_id (pass it to --thread), proposal_id, next_action, degraded and degraded_reasons — the SAME shape the bli-tower MCP server's jarvis_ask tool returns, so a script and an agent read one contract. reply/thread/traceId/model/trace/latency are still there beside it and still mean what they always meant.",
30
+ "proposal_id carries the coding-arm proposal a turn made, and is null when it made none — including when the arm refused. next_action is the sentence that says which of those it was and what a PERSON has to do about it: fetch the approval code for that proposal (a Tower tab, their Slack DM, or their own interactive `cockpit jarvis`), or approve a refused one where a person reads. A null proposal_id is never on its own. Do not send a proposal_id back on a --json turn: this is the surface the code was withheld from, and reading it back is refused here.",
31
+ "A turn that outlives the dashboard's own ceiling is reported as turn_timed_out — an incomplete answer is never printed as a finished one.",
32
+ "The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
33
+ "A thread is SINGLE-WRITER: two turns in the same conversation at the same time can read each other, and one can answer the other's question. Your own terminal is one conversation, named main, and stays that way. A --json turn is a program's, so it gets a FRESH conversation per call unless --thread names one, and the answer's thread_id is the name it used — pass that back with --thread to continue it deliberately. On a --json turn, --thread main asks for the shared default nobody chose, so it is treated as naming nothing and a fresh conversation is minted anyway.",
34
+ "--threads lists the terminal conversations on this account; --thread <name> --history replays one (the newest --limit <n> messages). The per-call conversations a --json turn minted are counted rather than listed, and any one of them still replays by name. Neither takes a question, and neither can reach anybody else's conversations.",
35
+ "--trace <id|last> prints the step tree of one turn — every model step, tool call and server-side memory step, with how long each took, which model ran it, the tokens it spent and what failed. `last` opens the turn this machine asked most recently. Like --threads it takes no question and reaches no model.",
36
+ "Run `cockpit login` first if this machine is not paired.",
37
+ ],
38
+ ],
39
+ [
40
+ "models",
41
+ [
42
+ "Usage: cockpit models [list|show <provider:model>|compare <id> <id> [...]] [--highlight] [--dashboard-url <url>] [--json]",
43
+ "",
44
+ "The model-card shelf: one card per model, with what the PROVIDER documents (context",
45
+ "window, max output, price per million, reasoning modes, tool calling, structured output,",
46
+ "quirks) beside what OUR machine measured (latency p50/p95, observed tokens/s, the last",
47
+ "belt and conformance probe verdicts).",
48
+ "Every value carries the page it was read from; a field the provider does not state reads",
49
+ "unknown with the reason, never a guess.",
50
+ "STALE:<field> means a measurement contradicted the documentation — reasoning-off refused",
51
+ "where the card says it can be turned off, the belt refused where the card says tool",
52
+ "calling. The line under it is the command that re-reads the provider.",
53
+ "compare puts two to four cards side by side: the same rows the page at /settings/models/",
54
+ "compare renders, including `$ per 100 JARVIS turns` (our own measured tokens a turn times",
55
+ "the provider's price, with the window it was measured over beside it — `not measured`",
56
+ "where no profile exists, never a per-million price relabelled) and the bench row, which",
57
+ "appears only when every model was judged in the SAME run against the SAME baseline;",
58
+ "otherwise it says `benched separately — not a head-to-head` rather than subtracting two",
59
+ "win rates. --highlight marks the winner in each scored row (lowest wins on price and",
60
+ "latency, highest on tokens/s, most wins on the bench); a row where any column is `—` or",
61
+ "`not measured` is not scored at all, and says why.",
62
+ "`cockpit models` is the SHELF; `cockpit model` is the org setting saying which model a",
63
+ "slot runs on. Different nouns, one letter apart, on purpose.",
64
+ "--json writes one object to stdout; operational lines stay on stderr.",
65
+ "Run `cockpit login` first if this machine is not paired.",
66
+ ],
67
+ ],
68
+ [
69
+ "scout",
70
+ [
71
+ "Usage: cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
72
+ "",
73
+ "Prints the same Scout board the Tower page shows, in the same words: the standing",
74
+ "watch line, the cards waiting on a decision, and the raw signal watch underneath.",
75
+ "This is the board itself. To ask a QUESTION about it — what it means, whether it is",
76
+ "worth doing here — use `cockpit jarvis`, whose readScout tool reads the same rows.",
77
+ "`cockpit scout board` and a bare `cockpit scout` are the same command; the MCP twin",
78
+ "is `scout_board` on the bli-tower server, over the same GET /api/cockpit/scout door.",
79
+ "--days <n> widens or narrows the window (1 to 90; the board defaults to 14).",
80
+ "Reading follows the Scout page audience setting, so whoever can open /scout can run this.",
81
+ "start, dismiss, and undo move one card and are super_admin actions on every surface;",
82
+ "a refusal is printed as Tower worded it and the board stays readable underneath.",
83
+ "The id can be the first 6 or more characters printed beside each card; an ambiguous",
84
+ "prefix is refused with the candidates rather than acting on the first match.",
85
+ "--json writes one object to stdout; operational lines stay on stderr.",
86
+ "Run `cockpit login` first if this machine is not paired.",
87
+ ],
88
+ ],
89
+ [
90
+ "ops",
91
+ [
92
+ "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
93
+ "",
94
+ " cockpit ops status",
95
+ " One line per scheduled job: when it last produced something, and whether that",
96
+ " is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
97
+ " quarter-hourly one does not. Every line names the schedule it is judged against.",
98
+ " --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
99
+ " A job that is not ok also prints what its artifact does and does not prove —",
100
+ " several only write a row when there is something new, so quiet can mean a quiet",
101
+ " week rather than a broken cron.",
102
+ " --job <id> asks about one; --skips adds the open Slack and external-sync skips,",
103
+ " grouped by reason. Exits 1 if anything is stale, empty or unreadable.",
104
+ " --coverage is the collection-coverage row on its own, with the BUCKET table:",
105
+ " which bucket the missing sessions are in (staged-not-committed with the upload",
106
+ " ledger's own reason, still pending upload, deliberately withheld, or no row at",
107
+ " all), whose machines they are on, which of their own days, and what to run for",
108
+ " each. That table also prints on the ordinary board whenever anything is missing.",
109
+ " Every machine's line also carries its BLI Memory column — whether the MCP",
110
+ " servers and the recall/save hooks are registered for Claude Code and Codex,",
111
+ " and whether Codex has been told to TRUST its hooks. That column never colours",
112
+ " a machine: a laptop with no memory hooks is still collecting sessions.",
113
+ " --memory adds the adoption gauge: memories saved per person per day for the",
114
+ " last 7 days, split by source (an agent calling save_memory, the Stop hook, or",
115
+ " the vendor import, which is a backfill and never counted as use).",
116
+ " --memory-days N changes the window (1-30) and implies --memory. Searches are",
117
+ " NOT counted — nothing in Tower records one, and the gauge says so rather than",
118
+ " printing a zero you would read as nobody searching.",
119
+ " It also shows ordinary prompt-hook observations: outcome counts, 100 ms",
120
+ " duration bounds, actual vector-cache reuse and named coverage gaps. These",
121
+ " are hook-body timings, not full host wait or retrieval-quality scores.",
122
+ " cockpit ops recompile --person <email|name|id> [--dry-run]",
123
+ " Writes that person's page again, now. Your own page is always yours to",
124
+ " recompile; somebody else's is for the people who read across the team.",
125
+ " --dry-run resolves the person and says which page is being served, compiling",
126
+ " nothing. A compile only ever ADDS a version — nothing is overwritten.",
127
+ " It can take minutes, and if Tower's 800-second budget runs out first you are",
128
+ " told exactly that: the compile may still have finished. Check with",
129
+ " `cockpit brief --for <person>`.",
130
+ " There is no --cadence: the compile path behind this writes a daily, and a flag",
131
+ " that was accepted and dropped would be worse than no flag.",
132
+ ],
133
+ ],
134
+ ];
@@ -42,7 +42,7 @@ export const TOWER_COMMAND_HELP = [
42
42
  [
43
43
  "docs",
44
44
  [
45
- "Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|link <id|slug> <notion-page>|sync-status [<id|slug>]] [flags]",
45
+ "Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|delete <id|slug> [<id|slug>...]|link <id|slug> <notion-page>|sync-status [<id|slug>]] [flags]",
46
46
  "",
47
47
  "The Tower document library, typed. Bare `cockpit docs` lists every document you may read.",
48
48
  "list [--parent <id>|root] [--query <text> [--word]] [--limit <n>] — every document you may see: id, visibility, body size, slug, title. Metadata only — a list never carries a body, so it stays cheap enough for an agent to call (--json was 604 KB for 155 documents before BLI-3737, and is now a few tens of KB). --json shape: { documents: [{ id, slug, title, visibility, parent_id, source, updated_at, body_chars }] }.",
@@ -59,6 +59,7 @@ export const TOWER_COMMAND_HELP = [
59
59
  "create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default. --json shape: { document }.",
60
60
  "update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document }.",
61
61
  " --allow-empty clears the page on purpose. A body that would empty a document that holds text is refused (refused_empty_body) unless you say so, because far more often it is a surface that lost the content than a person who meant it.",
62
+ "delete <id|slug> [<id|slug>...] — soft-deletes one or more documents (BLI-10555): the browser's own delete, sets deleted_at, never a real SQL DELETE. Recoverable — only a super_admin clearing deleted_at brings it back, there is no self-service undo. Deleting a parent does NOT delete its children; they keep their own parent_id and stay exactly where they were. Several refs resolve against ONE shared list call, then one DELETE each — one bad ref never loses the rest. --json shape: one ref answers { ok, document_id }; several answer { documents: [{ ref, ok, document_id } | { ref, ok: false, error, detail }] }.",
62
63
  "link <id|slug> <notion-url|page-id> — ties the document to a Notion page so the two-way sync moves it both ways. The page is the truth from then on: the next sync pulls it over this copy and keeps the copy as a version. `link <id|slug> --clear` unties it. Refusals: invalid_notion_page, notion_page_already_linked, document_not_writable.",
63
64
  "sync-status [<id|slug>] — every linked document's Notion sync state, or one's: synced (the body is what both sides last agreed on), tower_changed (edited here since; the next write-back pushes it), never_synced (linked, no baseline yet). Tower's half only: a Notion edit shows up at the next pull, not here. Metadata only, never a body.",
64
65
  "A document body is never accepted on the command line — --file (safest on Windows) or a pipe only, same discipline as `cockpit notes paste`.",
@@ -216,4 +217,185 @@ export const TOWER_COMMAND_HELP = [
216
217
  "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
217
218
  ],
218
219
  ],
220
+ [
221
+ "slack",
222
+ [
223
+ "Usage: cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [filters] [--format compact|full]] [--json]",
224
+ "",
225
+ " cockpit slack coverage",
226
+ " What Cockpit's Slack bot can see: how many channels it is in, what each",
227
+ " visibility state means, which cursors have not moved, and which ingest skips",
228
+ " are open. No message text at all, so anybody signed in can run it.",
229
+ " This is the command that tells an empty search apart from a coverage gap.",
230
+ " --stale-only drops the per-state breakdown and shows just what is behind.",
231
+ " --list names every channel and its readable state — counts alone could not",
232
+ " answer \"is there a Kaiben channel?\". --json's shape: { coverage: { workspaces: [",
233
+ " { workspace, channelsKnown, covered, byVisibility, staleChannels, openSkips,",
234
+ " channels? } ] } } — channels is present only with --list.",
235
+ " cockpit slack read --person <p> | --channel <c> | --query <text> [--word]",
236
+ " The messages themselves, through the same reader JARVIS uses. At least one",
237
+ " filter is required. --since/--until are YYYY-MM-DD; --limit caps the rows.",
238
+ " --query matches a substring; add --word to match whole words, so `excel`",
239
+ " stops matching \"Excellent\".",
240
+ " Reading Slack reads across the team, so this is for the people who do that",
241
+ " job; being refused it never costs you `cockpit slack coverage`.",
242
+ " An empty or partial answer prints Tower's own sentence about WHY, word for",
243
+ " word — none of those reasons means nothing was said.",
244
+ " --format full (default): each message as its own block (channel/author/time,",
245
+ " text, permalink). --format compact: one line per message. Always one rendering,",
246
+ " never both — a result used to print as a summary AND as blocks (BLI-10450).",
247
+ " --json's shape: { result: { status, reason, messages: [...], coverage, note } }.",
248
+ ],
249
+ ],
250
+ [
251
+ "workbook",
252
+ [
253
+ "Usage: cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
254
+ "",
255
+ "Reads the Tower workbook library: written project documents — workbooks, runbooks —",
256
+ "each with an author and a date. With no arguments it lists every compartment and the",
257
+ "command that opens each document.",
258
+ "`cockpit workbook tower` lists one project's shelf; `cockpit workbook tower workbook`",
259
+ "prints that document as wrapped plain text.",
260
+ "--section <id> prints one section, named by the anchors listed with the document.",
261
+ "--markdown prints the server's Markdown unchanged, for piping into anything else.",
262
+ "--width <n> sets the wrap column; otherwise the terminal's width is used, held between 60 and 100.",
263
+ "There is no pager: pipe it into one if you want paging.",
264
+ "Sign-in is the only gate, exactly as on the website page.",
265
+ "Run `cockpit login` first if this machine is not paired.",
266
+ ],
267
+ ],
268
+ [
269
+ "brief",
270
+ [
271
+ "Usage: cockpit brief [done <item>|undone <item>|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]",
272
+ "",
273
+ "Prints the TODAY page — the same page the Tower website shows, rendered for a terminal.",
274
+ "--for opens somebody else's page; the website's own rule decides whether you may, and it refuses in plain words when you may not.",
275
+ "--tldr prints the ten-second version; the whole page is the default.",
276
+ "--version <pageId> steps back to one exact earlier version; --versions lists them with their ids.",
277
+ "--claims prints the [claimId] beside every line, which is what `cockpit correct --claim` takes.",
278
+ "Reading it here counts as opening it, exactly as opening it in a browser does.",
279
+ "",
280
+ "--date <YYYY-MM-DD> reads the page that was live on one day — `today` and `yesterday` work too. Days are that person's own days, in their own timezone, so a page always sits on the date its own masthead says.",
281
+ " A day with nothing compiled says so by name, and says which day is nearest. It never quietly hands you a different day's page.",
282
+ "--delta says what changed between that day and the day before it, in the same words the website's version history uses. --against <day> compares against a day you name instead.",
283
+ " Comparisons that skip versions are worked out fresh each time and say `(not kept)` — a day with several compiles is one of those.",
284
+ "",
285
+ "history — the days there are, newest first, each with its masthead line and how many times it compiled that day. --days <n> asks for more than the last 14.",
286
+ " Pass any date it prints back to `--date`.",
287
+ "",
288
+ " cockpit brief status [--who <person>] [--render]",
289
+ " Why your brief did or did not arrive this morning — one reason, from the same",
290
+ " chain the Slack DM cron itself follows: not_due, not_monday, not_the_1st,",
291
+ " no_page, page_stale, no_slack_identity, engagement_write_failed, delivered.",
292
+ " It shows your delivery window, which cadence owns this morning, whether a",
293
+ " Slack identity is on record, and the delivered/opened/clicked receipts.",
294
+ " --render adds the TLDR the DM would have carried.",
295
+ " --who <person> asks about somebody else and is for the people who read across",
296
+ " the team. Exits 0 only on `delivered`.",
297
+ "edit — opens the page in $VISUAL/$EDITOR as a document: one [claimId] line per sentence you may rewrite, everything else a # comment. Change the words after an id, save, close. Only the lines that really changed are sent, so an accidental save writes nothing.",
298
+ " An id you delete or invent is refused by name BEFORE anything is sent: a missing line reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart.",
299
+ " --reason \"<why>\" rides on every row, exactly like a commit message. Optional; the before-and-after already teaches on its own.",
300
+ " With no editor, or with something piped in, the edited document is read from stdin instead — `cockpit brief edit < page.md`.",
301
+ " The edit IS the correction: each changed line is filed in the ledger before the page is republished, so a publish that falls over never costs you the edit.",
302
+ "",
303
+ "done <item> / undone <item>, tick one DO THIS FIRST row off, or put it back. The same checkbox the website's board has, on the same record.",
304
+ " <item> is the ticket exactly as the board writes it (BLI-3511), or a claim id from `cockpit brief --claims` for a row with no ticket behind it.",
305
+ " A ticked row prints as [x] and says `done`; everything still open prints as [ ]. A ticket keeps its tick across every recompile, so what you finished stays finished.",
306
+ " An item that is not on your page is named before anything is sent, and nothing is recorded.",
307
+ " Your own board only: a tick is your working state, so there is no --for here.",
308
+ "",
309
+ "rewrite — asks Tower to compile the page again and waits for the new version. Writing a page takes minutes, so this queues the work and watches your page rather than holding one long request open.",
310
+ " --no-wait returns as soon as the ask is on the record. Waiting is the default.",
311
+ " Running out of patience is not a failure and says so: the work is still going, and `cockpit brief` will show it when it lands.",
312
+ " Whose page you may ask about is the website's own rule — your own always, anybody's if you are an admin.",
313
+ "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
314
+ ],
315
+ ],
316
+ [
317
+ "notes",
318
+ [
319
+ "Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>] [flags]",
320
+ "Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 4 MB. --wait polls transcription for at most 330 seconds.",
321
+ "",
322
+ "folders [--tree] [--json]: list folders and visible note counts.",
323
+ "mkdir <path> [--json]: create a folder and any missing ancestors.",
324
+ "rename <path> --name <name> [--json]: rename a folder and its descendant paths.",
325
+ "rmdir <path> [--json]: delete an empty folder; refuses notes or subfolders.",
326
+ "upload, paste and move accept --folder <path>; use / for Root. Shelf placement stays independent.",
327
+ "The /meeting-notes surface, typed. Bare `cockpit notes` lists the library.",
328
+ "list [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] — every note you may open, grouped by shelf, newest meeting first. Never a word of a note.",
329
+ "show <id> — one note with its text, its shelf, and why you are allowed to see it.",
330
+ "shelf [--limit <n>] — the notes YOU have put in, and what became of each.",
331
+ "shelves — the shelves in use, with how many notes are on each.",
332
+ "upload <paths...> [--exclude \"<sentence>\"] — put one or more local files in. Explicit paths only; your shell does any globbing, and a file that does not exist is named rather than skipped.",
333
+ "paste [--file <path>] [--name <n>] [--exclude \"<sentence>\"] — put text in. --file <path> is safest (any editor encoding is decoded). Stdin works too: `pbpaste | cockpit notes paste` on macOS; on Windows use PowerShell 7 (`Get-Clipboard | cockpit notes paste`) — PowerShell 5.1 turns non-ASCII into `?` on pipes.",
334
+ "--exclude carries your own sentence about what to leave out, exactly as the browser's box does.",
335
+ "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.",
336
+ "unshare <id> — take it back. Never asks: it only ever narrows who can read.",
337
+ "move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
338
+ "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.",
339
+ "",
340
+ "Your own notetaker (BLI-4383). Fellow and Circleback today; Otter needs an Enterprise plan and Granola still runs on its own cron.",
341
+ "accounts [--json] — the notetakers you have connected, with health and when each was last read.",
342
+ "accounts --show-webhook <account-id> — that account's webhook URL, where to paste it at the vendor, and whether a signing secret is stored (BLI-4394). Until one is, every request to that URL is refused, which is the right way to be off.",
343
+ "webhook <account-id> --secret-stdin — store the VENDOR's webhook signing secret. THE SECRET IS READ FROM STDIN, never a flag: printf \"%s\" \"<signing secret>\" | cockpit notes webhook <account-id> --secret-stdin",
344
+ "connect --provider fellow|circleback [--workspace <subdomain>] --key-stdin [--name <n>] — connect one. Fellow needs --workspace, its API is per workspace. THE API KEY IS READ FROM STDIN, never a flag: printf \"%s\" \"<key>\" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin",
345
+ "detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
346
+ "sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
347
+ "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.",
348
+ "--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
349
+ "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.",
350
+ "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
351
+ ],
352
+ ],
353
+ [
354
+ "memory",
355
+ [
356
+ "Usage: cockpit memory [search|save|update|forget|profile|consolidation|requeue|install|status|log] [flags]",
357
+ "",
358
+ "The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
359
+ " search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
360
+ " repeat --container to search more than one space; omit it for this repository's own.",
361
+ " --limit caps the memories (default 5); up to 3 Tower pages and tickets ride beside",
362
+ " them whatever it says. For more Tower rows, use `cockpit search`.",
363
+ " save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>]",
364
+ " the memory comes from stdin or --file, never from the command line:",
365
+ " `echo \"Edward decided X on 2026-09-10\" | cockpit memory save`.",
366
+ " --custom-id makes a repeat save update instead of duplicating;",
367
+ " --extract distils atomic facts first (one model call) instead of storing as given.",
368
+ " update <id> [--file <path>] replacement text on stdin; the old row is kept as history.",
369
+ " forget <id> [--reason <label>]",
370
+ " forget --content-stdin --container <tag> [--reason <label>] forget by exact text.",
371
+ " profile [--container <tag>] the compiled one-page briefing; a read, never a compile.",
372
+ " consolidation [--container <tag>] what the nightly tidy-up pass did.",
373
+ " Clusters looked at, edits by verb, refusals by reason, model and cost.",
374
+ " Read-only: the pass is a nightly cron, it never deletes a memory, and",
375
+ " report mode changes nothing on the shelf at all.",
376
+ " requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply]",
377
+ " puts finished extractions back in the queue after the store refused their",
378
+ " writes. Super admins only. A DRY RUN unless you pass --apply, because an",
379
+ " apply re-runs one model call per receipt. Nothing is ever deleted: each",
380
+ " row keeps its previous outcome under result.requeued_from.",
381
+ "Refusals keep the door's own label — needs_rls_client, unknown_memory,",
382
+ "already_superseded, no_match — and --json prints the door's answer unchanged.",
383
+ "",
384
+ "BARE `cockpit memory` PRINTS THIS LIST AND DOES NOTHING ELSE (BLI-4047).",
385
+ "It used to run the installer; installing is now `cockpit memory install`.",
386
+ "",
387
+ "install/status register BLI Memory on this machine: the `bli-memory` MCP server plus the",
388
+ "recall/save hooks, for Claude Code (~/.claude.json, ~/.claude/settings.json)",
389
+ "and for Codex (~/.codex/config.toml, ~/.codex/skills/bli-memory/).",
390
+ "Idempotent: it merges with what is already there, never duplicates its own",
391
+ "entries, and reads the stored config back before reporting success.",
392
+ "`cockpit doctor` runs it, and the sync tick re-runs it at most once a day.",
393
+ "See docs/runbooks/bli-memory-install.md.",
394
+ "log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
395
+ "Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
396
+ "Reasons: one line, max 500 characters; no prompts, memory bodies or secrets.",
397
+ "Markers: [bli] = BLI Memory, ◪ = Supermemory, [bli]+◪ = both.",
398
+ "Offline entries stay queued; the next collector sync retries with the same ID.",
399
+ ],
400
+ ],
219
401
  ];
@@ -19,6 +19,7 @@
19
19
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
20
20
  import { localCommandHelp, SURFACE_ALIASES } from "./local-help.js";
21
21
  import { TOWER_COMMAND_HELP } from "./local-help-commands-tower.js";
22
+ import { TOWER_OPS_COMMAND_HELP } from "./local-help-commands-tower-ops.js";
22
23
  export function localSubcommandHelp(command) {
23
24
  const helpByCommand = new Map([
24
25
  ["roots", ["Usage: cockpit roots add <dir> [--allow-home-root] [--json]", "Approves a work directory, saves an approval receipt, and adds it to collection. Run cockpit doctor afterward."]],
@@ -248,260 +249,9 @@ export function localSubcommandHelp(command) {
248
249
  "`--repo <path>` remains supported as a backward-compatible alias.",
249
250
  ],
250
251
  ],
251
- [
252
- "jarvis",
253
- [
254
- "Usage: 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]]",
255
- "",
256
- "Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
257
- "--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
258
- "--date <YYYY-MM-DD> binds the page that was live on one of that person's days (`today` and `yesterday` work too), so the turn is about that day's page instead of the newest one. A day with nothing compiled binds no page and JARVIS says so rather than answering from a different day.",
259
- "--model <key> requests one provider:model pair (e.g. openai:gpt-5.6-terra); an unrecognised key is refused by the dashboard, not this command.",
260
- "--image <path> (alias --file) attaches one local PNG, JPEG, or WEBP under 10 MB with the question, the same one-image gate the JARVIS panel uses. A bad path, an unsupported type, or an oversized file is refused with its own plain sentence — never a stack trace.",
261
- "Run with no question for an interactive terminal conversation.",
262
- "Agents can pass --prompt, positional text, or pipe one question on stdin.",
263
- "Each answer prints a per-tool trace line and, only on a fallback or model mismatch, one model receipt line.",
264
- "Trace lines arrive live while JARVIS works; pass --no-stream to wait for the whole answer in one piece instead.",
265
- "--json writes one machine-readable response (including trace and model) to stdout; operational metadata stays on stderr. A --json run never streams fragments, whichever mode it is in.",
266
- "A --json turn is treated as an AGENT surface: if it proposes a coding task, JARVIS answers with proposal_id and NOT the eight-character approval code, because a code a program can read is a code it could approve with. The person fetches the code themself — a Tower browser tab, their Slack DM, or an interactive `cockpit jarvis` — and hands it back. Pass --show-approval-code when a script really is driving a dispatch for a person; the run says on stderr that it did.",
267
- "That object carries answer, sources (the Source: lines inside it), turn_id (pass it to --trace), thread_id (pass it to --thread), proposal_id, next_action, degraded and degraded_reasons — the SAME shape the bli-tower MCP server's jarvis_ask tool returns, so a script and an agent read one contract. reply/thread/traceId/model/trace/latency are still there beside it and still mean what they always meant.",
268
- "proposal_id carries the coding-arm proposal a turn made, and is null when it made none — including when the arm refused. next_action is the sentence that says which of those it was and what a PERSON has to do about it: fetch the approval code for that proposal (a Tower tab, their Slack DM, or their own interactive `cockpit jarvis`), or approve a refused one where a person reads. A null proposal_id is never on its own. Do not send a proposal_id back on a --json turn: this is the surface the code was withheld from, and reading it back is refused here.",
269
- "A turn that outlives the dashboard's own ceiling is reported as turn_timed_out — an incomplete answer is never printed as a finished one.",
270
- "The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
271
- "A thread is SINGLE-WRITER: two turns in the same conversation at the same time can read each other, and one can answer the other's question. Your own terminal is one conversation, named main, and stays that way. A --json turn is a program's, so it gets a FRESH conversation per call unless --thread names one, and the answer's thread_id is the name it used — pass that back with --thread to continue it deliberately. On a --json turn, --thread main asks for the shared default nobody chose, so it is treated as naming nothing and a fresh conversation is minted anyway.",
272
- "--threads lists the terminal conversations on this account; --thread <name> --history replays one (the newest --limit <n> messages). The per-call conversations a --json turn minted are counted rather than listed, and any one of them still replays by name. Neither takes a question, and neither can reach anybody else's conversations.",
273
- "--trace <id|last> prints the step tree of one turn — every model step, tool call and server-side memory step, with how long each took, which model ran it, the tokens it spent and what failed. `last` opens the turn this machine asked most recently. Like --threads it takes no question and reaches no model.",
274
- "Run `cockpit login` first if this machine is not paired.",
275
- ],
276
- ],
277
- [
278
- "models",
279
- [
280
- "Usage: cockpit models [list|show <provider:model>|compare <id> <id> [...]] [--highlight] [--dashboard-url <url>] [--json]",
281
- "",
282
- "The model-card shelf: one card per model, with what the PROVIDER documents (context",
283
- "window, max output, price per million, reasoning modes, tool calling, structured output,",
284
- "quirks) beside what OUR machine measured (latency p50/p95, observed tokens/s, the last",
285
- "belt and conformance probe verdicts).",
286
- "Every value carries the page it was read from; a field the provider does not state reads",
287
- "unknown with the reason, never a guess.",
288
- "STALE:<field> means a measurement contradicted the documentation — reasoning-off refused",
289
- "where the card says it can be turned off, the belt refused where the card says tool",
290
- "calling. The line under it is the command that re-reads the provider.",
291
- "compare puts two to four cards side by side: the same rows the page at /settings/models/",
292
- "compare renders, including `$ per 100 JARVIS turns` (our own measured tokens a turn times",
293
- "the provider's price, with the window it was measured over beside it — `not measured`",
294
- "where no profile exists, never a per-million price relabelled) and the bench row, which",
295
- "appears only when every model was judged in the SAME run against the SAME baseline;",
296
- "otherwise it says `benched separately — not a head-to-head` rather than subtracting two",
297
- "win rates. --highlight marks the winner in each scored row (lowest wins on price and",
298
- "latency, highest on tokens/s, most wins on the bench); a row where any column is `—` or",
299
- "`not measured` is not scored at all, and says why.",
300
- "`cockpit models` is the SHELF; `cockpit model` is the org setting saying which model a",
301
- "slot runs on. Different nouns, one letter apart, on purpose.",
302
- "--json writes one object to stdout; operational lines stay on stderr.",
303
- "Run `cockpit login` first if this machine is not paired.",
304
- ],
305
- ],
306
- [
307
- "scout",
308
- [
309
- "Usage: cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
310
- "",
311
- "Prints the same Scout board the Tower page shows, in the same words: the standing",
312
- "watch line, the cards waiting on a decision, and the raw signal watch underneath.",
313
- "This is the board itself. To ask a QUESTION about it — what it means, whether it is",
314
- "worth doing here — use `cockpit jarvis`, whose readScout tool reads the same rows.",
315
- "`cockpit scout board` and a bare `cockpit scout` are the same command; the MCP twin",
316
- "is `scout_board` on the bli-tower server, over the same GET /api/cockpit/scout door.",
317
- "--days <n> widens or narrows the window (1 to 90; the board defaults to 14).",
318
- "Reading follows the Scout page audience setting, so whoever can open /scout can run this.",
319
- "start, dismiss, and undo move one card and are super_admin actions on every surface;",
320
- "a refusal is printed as Tower worded it and the board stays readable underneath.",
321
- "The id can be the first 6 or more characters printed beside each card; an ambiguous",
322
- "prefix is refused with the candidates rather than acting on the first match.",
323
- "--json writes one object to stdout; operational lines stay on stderr.",
324
- "Run `cockpit login` first if this machine is not paired.",
325
- ],
326
- ],
327
- [
328
- "ops",
329
- [
330
- "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
331
- "",
332
- " cockpit ops status",
333
- " One line per scheduled job: when it last produced something, and whether that",
334
- " is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
335
- " quarter-hourly one does not. Every line names the schedule it is judged against.",
336
- " --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
337
- " A job that is not ok also prints what its artifact does and does not prove —",
338
- " several only write a row when there is something new, so quiet can mean a quiet",
339
- " week rather than a broken cron.",
340
- " --job <id> asks about one; --skips adds the open Slack and external-sync skips,",
341
- " grouped by reason. Exits 1 if anything is stale, empty or unreadable.",
342
- " --coverage is the collection-coverage row on its own, with the BUCKET table:",
343
- " which bucket the missing sessions are in (staged-not-committed with the upload",
344
- " ledger's own reason, still pending upload, deliberately withheld, or no row at",
345
- " all), whose machines they are on, which of their own days, and what to run for",
346
- " each. That table also prints on the ordinary board whenever anything is missing.",
347
- " Every machine's line also carries its BLI Memory column — whether the MCP",
348
- " servers and the recall/save hooks are registered for Claude Code and Codex,",
349
- " and whether Codex has been told to TRUST its hooks. That column never colours",
350
- " a machine: a laptop with no memory hooks is still collecting sessions.",
351
- " --memory adds the adoption gauge: memories saved per person per day for the",
352
- " last 7 days, split by source (an agent calling save_memory, the Stop hook, or",
353
- " the vendor import, which is a backfill and never counted as use).",
354
- " --memory-days N changes the window (1-30) and implies --memory. Searches are",
355
- " NOT counted — nothing in Tower records one, and the gauge says so rather than",
356
- " printing a zero you would read as nobody searching.",
357
- " It also shows ordinary prompt-hook observations: outcome counts, 100 ms",
358
- " duration bounds, actual vector-cache reuse and named coverage gaps. These",
359
- " are hook-body timings, not full host wait or retrieval-quality scores.",
360
- " cockpit ops recompile --person <email|name|id> [--dry-run]",
361
- " Writes that person's page again, now. Your own page is always yours to",
362
- " recompile; somebody else's is for the people who read across the team.",
363
- " --dry-run resolves the person and says which page is being served, compiling",
364
- " nothing. A compile only ever ADDS a version — nothing is overwritten.",
365
- " It can take minutes, and if Tower's 800-second budget runs out first you are",
366
- " told exactly that: the compile may still have finished. Check with",
367
- " `cockpit brief --for <person>`.",
368
- " There is no --cadence: the compile path behind this writes a daily, and a flag",
369
- " that was accepted and dropped would be worse than no flag.",
370
- ],
371
- ],
372
- [
373
- "slack",
374
- [
375
- "Usage: cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [filters] [--format compact|full]] [--json]",
376
- "",
377
- " cockpit slack coverage",
378
- " What Cockpit's Slack bot can see: how many channels it is in, what each",
379
- " visibility state means, which cursors have not moved, and which ingest skips",
380
- " are open. No message text at all, so anybody signed in can run it.",
381
- " This is the command that tells an empty search apart from a coverage gap.",
382
- " --stale-only drops the per-state breakdown and shows just what is behind.",
383
- " --list names every channel and its readable state — counts alone could not",
384
- " answer \"is there a Kaiben channel?\". --json's shape: { coverage: { workspaces: [",
385
- " { workspace, channelsKnown, covered, byVisibility, staleChannels, openSkips,",
386
- " channels? } ] } } — channels is present only with --list.",
387
- " cockpit slack read --person <p> | --channel <c> | --query <text> [--word]",
388
- " The messages themselves, through the same reader JARVIS uses. At least one",
389
- " filter is required. --since/--until are YYYY-MM-DD; --limit caps the rows.",
390
- " --query matches a substring; add --word to match whole words, so `excel`",
391
- " stops matching \"Excellent\".",
392
- " Reading Slack reads across the team, so this is for the people who do that",
393
- " job; being refused it never costs you `cockpit slack coverage`.",
394
- " An empty or partial answer prints Tower's own sentence about WHY, word for",
395
- " word — none of those reasons means nothing was said.",
396
- " --format full (default): each message as its own block (channel/author/time,",
397
- " text, permalink). --format compact: one line per message. Always one rendering,",
398
- " never both — a result used to print as a summary AND as blocks (BLI-10450).",
399
- " --json's shape: { result: { status, reason, messages: [...], coverage, note } }.",
400
- ],
401
- ],
402
- [
403
- "workbook",
404
- [
405
- "Usage: cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
406
- "",
407
- "Reads the Tower workbook library: written project documents — workbooks, runbooks —",
408
- "each with an author and a date. With no arguments it lists every compartment and the",
409
- "command that opens each document.",
410
- "`cockpit workbook tower` lists one project's shelf; `cockpit workbook tower workbook`",
411
- "prints that document as wrapped plain text.",
412
- "--section <id> prints one section, named by the anchors listed with the document.",
413
- "--markdown prints the server's Markdown unchanged, for piping into anything else.",
414
- "--width <n> sets the wrap column; otherwise the terminal's width is used, held between 60 and 100.",
415
- "There is no pager: pipe it into one if you want paging.",
416
- "Sign-in is the only gate, exactly as on the website page.",
417
- "Run `cockpit login` first if this machine is not paired.",
418
- ],
419
- ],
420
- [
421
- "brief",
422
- [
423
- "Usage: cockpit brief [done <item>|undone <item>|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]",
424
- "",
425
- "Prints the TODAY page — the same page the Tower website shows, rendered for a terminal.",
426
- "--for opens somebody else's page; the website's own rule decides whether you may, and it refuses in plain words when you may not.",
427
- "--tldr prints the ten-second version; the whole page is the default.",
428
- "--version <pageId> steps back to one exact earlier version; --versions lists them with their ids.",
429
- "--claims prints the [claimId] beside every line, which is what `cockpit correct --claim` takes.",
430
- "Reading it here counts as opening it, exactly as opening it in a browser does.",
431
- "",
432
- "--date <YYYY-MM-DD> reads the page that was live on one day — `today` and `yesterday` work too. Days are that person's own days, in their own timezone, so a page always sits on the date its own masthead says.",
433
- " A day with nothing compiled says so by name, and says which day is nearest. It never quietly hands you a different day's page.",
434
- "--delta says what changed between that day and the day before it, in the same words the website's version history uses. --against <day> compares against a day you name instead.",
435
- " Comparisons that skip versions are worked out fresh each time and say `(not kept)` — a day with several compiles is one of those.",
436
- "",
437
- "history — the days there are, newest first, each with its masthead line and how many times it compiled that day. --days <n> asks for more than the last 14.",
438
- " Pass any date it prints back to `--date`.",
439
- "",
440
- " cockpit brief status [--who <person>] [--render]",
441
- " Why your brief did or did not arrive this morning — one reason, from the same",
442
- " chain the Slack DM cron itself follows: not_due, not_monday, not_the_1st,",
443
- " no_page, page_stale, no_slack_identity, engagement_write_failed, delivered.",
444
- " It shows your delivery window, which cadence owns this morning, whether a",
445
- " Slack identity is on record, and the delivered/opened/clicked receipts.",
446
- " --render adds the TLDR the DM would have carried.",
447
- " --who <person> asks about somebody else and is for the people who read across",
448
- " the team. Exits 0 only on `delivered`.",
449
- "edit — opens the page in $VISUAL/$EDITOR as a document: one [claimId] line per sentence you may rewrite, everything else a # comment. Change the words after an id, save, close. Only the lines that really changed are sent, so an accidental save writes nothing.",
450
- " An id you delete or invent is refused by name BEFORE anything is sent: a missing line reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart.",
451
- " --reason \"<why>\" rides on every row, exactly like a commit message. Optional; the before-and-after already teaches on its own.",
452
- " With no editor, or with something piped in, the edited document is read from stdin instead — `cockpit brief edit < page.md`.",
453
- " The edit IS the correction: each changed line is filed in the ledger before the page is republished, so a publish that falls over never costs you the edit.",
454
- "",
455
- "done <item> / undone <item>, tick one DO THIS FIRST row off, or put it back. The same checkbox the website's board has, on the same record.",
456
- " <item> is the ticket exactly as the board writes it (BLI-3511), or a claim id from `cockpit brief --claims` for a row with no ticket behind it.",
457
- " A ticked row prints as [x] and says `done`; everything still open prints as [ ]. A ticket keeps its tick across every recompile, so what you finished stays finished.",
458
- " An item that is not on your page is named before anything is sent, and nothing is recorded.",
459
- " Your own board only: a tick is your working state, so there is no --for here.",
460
- "",
461
- "rewrite — asks Tower to compile the page again and waits for the new version. Writing a page takes minutes, so this queues the work and watches your page rather than holding one long request open.",
462
- " --no-wait returns as soon as the ask is on the record. Waiting is the default.",
463
- " Running out of patience is not a failure and says so: the work is still going, and `cockpit brief` will show it when it lands.",
464
- " Whose page you may ask about is the website's own rule — your own always, anybody's if you are an admin.",
465
- "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
466
- ],
467
- ],
468
- [
469
- "notes",
470
- [
471
- "Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>] [flags]",
472
- "Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 4 MB. --wait polls transcription for at most 330 seconds.",
473
- "",
474
- "folders [--tree] [--json]: list folders and visible note counts.",
475
- "mkdir <path> [--json]: create a folder and any missing ancestors.",
476
- "rename <path> --name <name> [--json]: rename a folder and its descendant paths.",
477
- "rmdir <path> [--json]: delete an empty folder; refuses notes or subfolders.",
478
- "upload, paste and move accept --folder <path>; use / for Root. Shelf placement stays independent.",
479
- "The /meeting-notes surface, typed. Bare `cockpit notes` lists the library.",
480
- "list [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] — every note you may open, grouped by shelf, newest meeting first. Never a word of a note.",
481
- "show <id> — one note with its text, its shelf, and why you are allowed to see it.",
482
- "shelf [--limit <n>] — the notes YOU have put in, and what became of each.",
483
- "shelves — the shelves in use, with how many notes are on each.",
484
- "upload <paths...> [--exclude \"<sentence>\"] — put one or more local files in. Explicit paths only; your shell does any globbing, and a file that does not exist is named rather than skipped.",
485
- "paste [--file <path>] [--name <n>] [--exclude \"<sentence>\"] — put text in. --file <path> is safest (any editor encoding is decoded). Stdin works too: `pbpaste | cockpit notes paste` on macOS; on Windows use PowerShell 7 (`Get-Clipboard | cockpit notes paste`) — PowerShell 5.1 turns non-ASCII into `?` on pipes.",
486
- "--exclude carries your own sentence about what to leave out, exactly as the browser's box does.",
487
- "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.",
488
- "unshare <id> — take it back. Never asks: it only ever narrows who can read.",
489
- "move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
490
- "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.",
491
- "",
492
- "Your own notetaker (BLI-4383). Fellow and Circleback today; Otter needs an Enterprise plan and Granola still runs on its own cron.",
493
- "accounts [--json] — the notetakers you have connected, with health and when each was last read.",
494
- "accounts --show-webhook <account-id> — that account's webhook URL, where to paste it at the vendor, and whether a signing secret is stored (BLI-4394). Until one is, every request to that URL is refused, which is the right way to be off.",
495
- "webhook <account-id> --secret-stdin — store the VENDOR's webhook signing secret. THE SECRET IS READ FROM STDIN, never a flag: printf \"%s\" \"<signing secret>\" | cockpit notes webhook <account-id> --secret-stdin",
496
- "connect --provider fellow|circleback [--workspace <subdomain>] --key-stdin [--name <n>] — connect one. Fellow needs --workspace, its API is per workspace. THE API KEY IS READ FROM STDIN, never a flag: printf \"%s\" \"<key>\" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin",
497
- "detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
498
- "sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
499
- "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.",
500
- "--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
501
- "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.",
502
- "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
503
- ],
504
- ],
252
+ // BLI-10445: JARVIS, models, Scout and ops live in `local-help-commands-tower-ops.ts`;
253
+ // slack, workbook, brief, notes and memory joined `local-help-commands-tower.ts`.
254
+ ...TOWER_OPS_COMMAND_HELP,
505
255
  [
506
256
  "correct",
507
257
  [
@@ -587,54 +337,6 @@ export function localSubcommandHelp(command) {
587
337
  "Action defaults to `install`.",
588
338
  ],
589
339
  ],
590
- [
591
- "memory",
592
- [
593
- "Usage: cockpit memory [search|save|update|forget|profile|consolidation|requeue|install|status|log] [flags]",
594
- "",
595
- "The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
596
- " search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
597
- " repeat --container to search more than one space; omit it for this repository's own.",
598
- " --limit caps the memories (default 5); up to 3 Tower pages and tickets ride beside",
599
- " them whatever it says. For more Tower rows, use `cockpit search`.",
600
- " save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>]",
601
- " the memory comes from stdin or --file, never from the command line:",
602
- " `echo \"Edward decided X on 2026-09-10\" | cockpit memory save`.",
603
- " --custom-id makes a repeat save update instead of duplicating;",
604
- " --extract distils atomic facts first (one model call) instead of storing as given.",
605
- " update <id> [--file <path>] replacement text on stdin; the old row is kept as history.",
606
- " forget <id> [--reason <label>]",
607
- " forget --content-stdin --container <tag> [--reason <label>] forget by exact text.",
608
- " profile [--container <tag>] the compiled one-page briefing; a read, never a compile.",
609
- " consolidation [--container <tag>] what the nightly tidy-up pass did.",
610
- " Clusters looked at, edits by verb, refusals by reason, model and cost.",
611
- " Read-only: the pass is a nightly cron, it never deletes a memory, and",
612
- " report mode changes nothing on the shelf at all.",
613
- " requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply]",
614
- " puts finished extractions back in the queue after the store refused their",
615
- " writes. Super admins only. A DRY RUN unless you pass --apply, because an",
616
- " apply re-runs one model call per receipt. Nothing is ever deleted: each",
617
- " row keeps its previous outcome under result.requeued_from.",
618
- "Refusals keep the door's own label — needs_rls_client, unknown_memory,",
619
- "already_superseded, no_match — and --json prints the door's answer unchanged.",
620
- "",
621
- "BARE `cockpit memory` PRINTS THIS LIST AND DOES NOTHING ELSE (BLI-4047).",
622
- "It used to run the installer; installing is now `cockpit memory install`.",
623
- "",
624
- "install/status register BLI Memory on this machine: the `bli-memory` MCP server plus the",
625
- "recall/save hooks, for Claude Code (~/.claude.json, ~/.claude/settings.json)",
626
- "and for Codex (~/.codex/config.toml, ~/.codex/skills/bli-memory/).",
627
- "Idempotent: it merges with what is already there, never duplicates its own",
628
- "entries, and reads the stored config back before reporting success.",
629
- "`cockpit doctor` runs it, and the sync tick re-runs it at most once a day.",
630
- "See docs/runbooks/bli-memory-install.md.",
631
- "log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
632
- "Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
633
- "Reasons: one line, max 500 characters; no prompts, memory bodies or secrets.",
634
- "Markers: [bli] = BLI Memory, ◪ = Supermemory, [bli]+◪ = both.",
635
- "Offline entries stay queued; the next collector sync retries with the same ID.",
636
- ],
637
- ],
638
340
  [
639
341
  "clean",
640
342
  [
@@ -117,7 +117,7 @@ export function localCommandHelp(command) {
117
117
  " cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
118
118
  " cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|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]",
119
119
  " cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
120
- " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
120
+ " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
121
121
  " 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]",
122
122
  " cockpit issue [list|show <BLI-id>|create --title <t>|update <BLI-id>|move <BLI-id> <state>|comment <BLI-id>|history <BLI-id>] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] [--priority 0-4] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
123
123
  " 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]",
@@ -48,6 +48,7 @@ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
48
48
  import { runMemoryConsolidation } from "./memory-consolidation.js";
49
49
  import { runMemoryRequeue } from "./memory-requeue.js";
50
50
  import { readNoteFile } from "./notes-file.js";
51
+ export { runMemoryVerbList } from "./memory-verb-list.js";
51
52
  const TAG = "[memory cli]";
52
53
  const SEARCH_DEADLINE_MS = 60_000;
53
54
  /** `--extract` is one model call, sometimes two; the door waits for it when asked to. */
@@ -130,61 +131,6 @@ async function profileMemory(command, door) {
130
131
  writeLine(door.io.stdout, `Compiled ${String(profile.compiledAt ?? "—")} from ${String(profile.sourceRows ?? 0)} memories by ${String(profile.model ?? "—")}.`);
131
132
  return 0;
132
133
  }
133
- /**
134
- * The verb list a bare `cockpit memory` prints (BLI-4047).
135
- *
136
- * It used to run the installer. Typing a noun to find out what it does is the
137
- * single most common thing a person does with a new CLI, and this was the one
138
- * command in the collector that answered that by writing five files in a home
139
- * directory. Installing is still one word away, and every unattended caller
140
- * asks for `install` by name in code.
141
- */
142
- export function runMemoryVerbList(io, json) {
143
- if (json) {
144
- writeLine(io.stdout, JSON.stringify({
145
- ok: true,
146
- noun: "memory",
147
- machine_verbs: ["install", "status"],
148
- store_verbs: ["search", "save", "update", "forget", "profile", "consolidation", "requeue"],
149
- log_verbs: ["log"],
150
- }));
151
- return 0;
152
- }
153
- for (const line of [
154
- "cockpit memory — BLI Memory: the store, and this machine's registration.",
155
- "",
156
- "The store (Tower, over /api/memory/*):",
157
- " cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--json]",
158
- " cockpit memory save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>] [--json]",
159
- " the memory comes from stdin, or --file; never from the command line",
160
- " cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
161
- " cockpit memory forget <id> [--reason <label>] [--json]",
162
- " cockpit memory forget --content-stdin --container <tag> [--reason <label>] [--json]",
163
- " cockpit memory profile [--container <tag>] [--json]",
164
- " the compiled one-page briefing every session opens with; a read, never a compile",
165
- " cockpit memory consolidation [--container <tag>] [--json]",
166
- " what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
167
- " cockpit memory requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply] [--json]",
168
- " put a window of finished extractions back in the queue after the store refused",
169
- " their writes; super admins only, and a DRY RUN unless you pass --apply",
170
- "",
171
- "This machine:",
172
- " cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
173
- " cockpit memory status [--json] read the stored config back",
174
- "",
175
- "The experience log:",
176
- " cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
177
- "",
178
- "Search: --limit caps the MEMORIES (default 5, at most 20). Up to 3 Tower pages and",
179
- "tickets ride beside them whatever --limit says; for more, use `cockpit search`.",
180
- "Save accepts repeated --alias names (up to 16 names of 64 characters).",
181
- "Omit --container and this repository's own memory space is used — the same",
182
- "space the bli-memory MCP server and its hooks read and write.",
183
- ]) {
184
- writeLine(io.stdout, line);
185
- }
186
- return 0;
187
- }
188
134
  /**
189
135
  * This repository's memory space, resolved by the MCP server's own resolver.
190
136
  * `--container` wins; `--workspace` says which directory to derive it from.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The verb list a bare `cockpit memory` prints: what the noun does, never a
3
+ * side effect.
4
+ *
5
+ * Split out of `memory-store.ts` (BLI-10445) when that file crossed the
6
+ * 500-line readability ratchet. The seam: `memory-store.ts` talks to the
7
+ * `/api/memory/*` doors; this module talks to nobody and only describes them,
8
+ * so it changes when a verb is added, not when a door's contract moves.
9
+ * `memory-store.ts` re-exports it so the router and tests import one name.
10
+ *
11
+ * Every string moved verbatim.
12
+ */
13
+ import { writeLine } from "./cli-io.js";
14
+ /**
15
+ * The verb list a bare `cockpit memory` prints (BLI-4047).
16
+ *
17
+ * It used to run the installer. Typing a noun to find out what it does is the
18
+ * single most common thing a person does with a new CLI, and this was the one
19
+ * command in the collector that answered that by writing five files in a home
20
+ * directory. Installing is still one word away, and every unattended caller
21
+ * asks for `install` by name in code.
22
+ */
23
+ export function runMemoryVerbList(io, json) {
24
+ if (json) {
25
+ writeLine(io.stdout, JSON.stringify({
26
+ ok: true,
27
+ noun: "memory",
28
+ machine_verbs: ["install", "status"],
29
+ store_verbs: ["search", "save", "update", "forget", "profile", "consolidation", "requeue"],
30
+ log_verbs: ["log"],
31
+ }));
32
+ return 0;
33
+ }
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] [--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
+ " cockpit memory profile [--container <tag>] [--json]",
45
+ " the compiled one-page briefing every session opens with; a read, never a compile",
46
+ " cockpit memory consolidation [--container <tag>] [--json]",
47
+ " what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
48
+ " cockpit memory requeue --since <iso> [--until <iso>] [--source hook|harvest] [--reason <label>] [--apply] [--json]",
49
+ " put a window of finished extractions back in the queue after the store refused",
50
+ " their writes; super admins only, and a DRY RUN unless you pass --apply",
51
+ "",
52
+ "This machine:",
53
+ " cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
54
+ " cockpit memory status [--json] read the stored config back",
55
+ "",
56
+ "The experience log:",
57
+ " cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
58
+ "",
59
+ "Search: --limit caps the MEMORIES (default 5, at most 20). Up to 3 Tower pages and",
60
+ "tickets ride beside them whatever --limit says; for more, use `cockpit search`.",
61
+ "Save accepts repeated --alias names (up to 16 names of 64 characters).",
62
+ "Omit --container and this repository's own memory space is used — the same",
63
+ "space the bli-memory MCP server and its hooks read and write.",
64
+ ]) {
65
+ writeLine(io.stdout, line);
66
+ }
67
+ return 0;
68
+ }
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.140");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.142");
25
25
  return 0;
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.140",
3
+ "version": "0.2.142",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@bli-cockpit/memory-mcp": "0.1.36",
31
- "@bli-cockpit/mcp": "0.1.65",
31
+ "@bli-cockpit/mcp": "0.1.67",
32
32
  "@bli-cockpit/telemetry-core": "0.1.48"
33
33
  }
34
34
  }