@bli-cockpit/cli 0.2.117 → 0.2.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/commands/analyze.js +74 -54
  2. package/dist/commands/brief-rewrite.js +164 -101
  3. package/dist/commands/brief.js +38 -13
  4. package/dist/commands/correct.js +38 -21
  5. package/dist/commands/docs.js +13 -10
  6. package/dist/commands/editor.js +59 -30
  7. package/dist/commands/install-receipts.js +106 -91
  8. package/dist/commands/local-args-tower-admin.js +4 -0
  9. package/dist/commands/local-args-tower-cal.js +28 -3
  10. package/dist/commands/local-args-tower-chat.js +55 -28
  11. package/dist/commands/local-args-tower-docs-msg.js +39 -8
  12. package/dist/commands/local-args-tower-mail.js +27 -1
  13. package/dist/commands/local-args-tower-models.js +14 -17
  14. package/dist/commands/local-args-tower-pages.js +62 -5
  15. package/dist/commands/local-args-tower-work.js +37 -6
  16. package/dist/commands/local-help-commands.js +6 -3
  17. package/dist/commands/local-help.js +1 -1
  18. package/dist/commands/mcp-stdio-probe.js +92 -73
  19. package/dist/commands/memory-hook-performance.js +135 -101
  20. package/dist/commands/memory-install-claude.js +15 -14
  21. package/dist/commands/memory-install-codex.js +10 -6
  22. package/dist/commands/memory-install-config.js +5 -4
  23. package/dist/commands/memory-install-contract.js +56 -10
  24. package/dist/commands/memory-install-report.js +16 -11
  25. package/dist/commands/memory-install-skills.js +11 -11
  26. package/dist/commands/memory-log.js +22 -5
  27. package/dist/commands/msg.js +11 -5
  28. package/dist/commands/notes-accounts.js +96 -5
  29. package/dist/commands/notes.js +10 -3
  30. package/dist/commands/onboard-setup.js +16 -1
  31. package/dist/commands/ops-sections.js +89 -0
  32. package/dist/commands/ops.js +117 -120
  33. package/dist/commands/public-root.js +1 -1
  34. package/dist/commands/scout.js +90 -68
  35. package/dist/commands/session-sync-failures.js +19 -13
  36. package/dist/commands/session-sync-record.js +53 -52
  37. package/dist/commands/session-sync-upload.js +15 -11
  38. package/dist/commands/sessions.js +61 -51
  39. package/dist/commands/slack.js +90 -61
  40. package/dist/commands/status.js +53 -41
  41. package/dist/commands/workbook.js +23 -20
  42. package/package.json +2 -2
@@ -86,17 +86,7 @@ async function syncOneWorktree(options) {
86
86
  const attributedSyntheticTarget = options.isDiscoveredWorktree
87
87
  ? null
88
88
  : worktree;
89
- const contextOptions = {
90
- homeDir: run.homeDir,
91
- repoRoot: worktree.repo_root,
92
- activeTicketId: run.activeTicketId,
93
- operatorId: run.operatorId,
94
- sessionId: run.sessionId,
95
- ...(attributedSyntheticTarget ? {} : { branch: run.branch }),
96
- };
97
- const ensureContext = () => attributedSyntheticTarget
98
- ? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
99
- : startLocalWorkContext(contextOptions);
89
+ const ensureContext = () => startWorktreeContext(options, attributedSyntheticTarget);
100
90
  let context = null;
101
91
  if (run.startContexts || attributedSyntheticTarget) {
102
92
  context = await ensureContext();
@@ -118,6 +108,20 @@ async function syncOneWorktree(options) {
118
108
  }
119
109
  return { worktree, context, sync };
120
110
  }
111
+ function startWorktreeContext(options, attributedSyntheticTarget) {
112
+ const { run, worktree } = options;
113
+ const contextOptions = {
114
+ homeDir: run.homeDir,
115
+ repoRoot: worktree.repo_root,
116
+ activeTicketId: run.activeTicketId,
117
+ operatorId: run.operatorId,
118
+ sessionId: run.sessionId,
119
+ ...(attributedSyntheticTarget ? {} : { branch: run.branch }),
120
+ };
121
+ return attributedSyntheticTarget
122
+ ? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
123
+ : startLocalWorkContext(contextOptions);
124
+ }
121
125
  /**
122
126
  * Assemble what one worktree is about to upload: its sibling worktree
123
127
  * inventory, its Codex mains, its Claude mains and sidecars, and its share of
@@ -21,9 +21,45 @@ export async function runSessions(command, io) {
21
21
  const homeDir = command.homeDir ?? os.homedir();
22
22
  const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
23
23
  const window = await sessionsScanWindow(command, now);
24
- const wantCodex = command.source !== "claude";
25
- const wantClaude = command.source !== "codex";
26
- const codex = wantCodex
24
+ const { codex, claude } = await scanSessionSources(command, homeDir, worktrees, now, window);
25
+ const { codexRows, claudeRows } = sessionRows(codex, claude);
26
+ // BLI-3483: the scanners have always counted the folders and files they
27
+ // could not read, and this command has always thrown those counts away
28
+ // except for one field in `--json`. So "No sessions observed" was printed on
29
+ // a machine where the scan had been blocked from looking — the operator's
30
+ // one diagnostic tool answering the question with the failure removed.
31
+ const readFailures = countReadFailures(codex, claude);
32
+ if (command.json) {
33
+ return writeSessionJson(io, window, readFailures, codex, claude, codexRows, claudeRows);
34
+ }
35
+ return writeSessionText(io, window, readFailures, codexRows, claudeRows);
36
+ }
37
+ function writeSessionText(io, window, readFailures, codexRows, claudeRows) {
38
+ writeLine(io.stdout, "Tower sessions (read-only attribution)");
39
+ writeLine(io.stdout, `window: ${sessionsWindowLine(window)}`);
40
+ for (const row of codexRows) {
41
+ writeLine(io.stdout, sessionRowLine(row));
42
+ }
43
+ for (const row of claudeRows) {
44
+ writeLine(io.stdout, sessionRowLine(row));
45
+ for (const sidecar of row.sidecar_skips) {
46
+ writeLine(io.stdout, ` sidecar ${sidecar.file_name}: ${sidecar.reason}`);
47
+ }
48
+ }
49
+ if (codexRows.length === 0 && claudeRows.length === 0) {
50
+ writeLine(io.stdout, readFailures.total > 0
51
+ ? `No sessions listed, but ${readFailures.total} read failure(s) mean the scan could not see everywhere — this is not proof there were none.`
52
+ : "No sessions observed in the scan window.");
53
+ }
54
+ if (readFailures.total > 0) {
55
+ for (const line of readFailureLines(readFailures)) {
56
+ writeLine(io.stdout, line);
57
+ }
58
+ }
59
+ return 0;
60
+ }
61
+ async function scanSessionSources(command, homeDir, worktrees, now, window) {
62
+ const codex = command.source !== "claude"
27
63
  ? await scanAndAttributeCodexSessions({
28
64
  sessionsDirs: defaultCodexSessionDirs(homeDir),
29
65
  worktrees,
@@ -32,7 +68,7 @@ export async function runSessions(command, io) {
32
68
  limit: window.limit,
33
69
  })
34
70
  : null;
35
- const claude = wantClaude
71
+ const claude = command.source !== "codex"
36
72
  ? await scanAndAttributeClaudeSessions({
37
73
  projectsDir: path.join(homeDir, ".claude", "projects"),
38
74
  worktrees,
@@ -41,6 +77,26 @@ export async function runSessions(command, io) {
41
77
  limit: window.mode === "default" ? undefined : window.limit,
42
78
  })
43
79
  : null;
80
+ return { codex, claude };
81
+ }
82
+ function writeSessionJson(io, window, readFailures, codex, claude, codexRows, claudeRows) {
83
+ writeLine(io.stdout, JSON.stringify({
84
+ window,
85
+ read_failures: readFailures,
86
+ ...(codex ? { codex: { counts: codex.counts, sessions: codexRows } } : {}),
87
+ ...(claude
88
+ ? {
89
+ claude: {
90
+ counts: claude.counts,
91
+ project_dirs_skipped: claude.project_dirs_skipped,
92
+ sessions: claudeRows,
93
+ },
94
+ }
95
+ : {}),
96
+ }, null, 2));
97
+ return 0;
98
+ }
99
+ function sessionRows(codex, claude) {
44
100
  // Safe output contract (B.4 §6): id, state, reason, scores, signals, sidecar
45
101
  // skip reasons, plus the repo_label basename. Branch is intentionally omitted
46
102
  // — branch names can carry operator-authored task/customer text.
@@ -71,53 +127,7 @@ export async function runSessions(command, io) {
71
127
  reason: sidecar.skipped_reason,
72
128
  })),
73
129
  }));
74
- // BLI-3483: the scanners have always counted the folders and files they
75
- // could not read, and this command has always thrown those counts away
76
- // except for one field in `--json`. So "No sessions observed" was printed on
77
- // a machine where the scan had been blocked from looking — the operator's
78
- // one diagnostic tool answering the question with the failure removed.
79
- const readFailures = countReadFailures(codex, claude);
80
- if (command.json) {
81
- writeLine(io.stdout, JSON.stringify({
82
- window,
83
- read_failures: readFailures,
84
- ...(codex
85
- ? { codex: { counts: codex.counts, sessions: codexRows } }
86
- : {}),
87
- ...(claude
88
- ? {
89
- claude: {
90
- counts: claude.counts,
91
- project_dirs_skipped: claude.project_dirs_skipped,
92
- sessions: claudeRows,
93
- },
94
- }
95
- : {}),
96
- }, null, 2));
97
- return 0;
98
- }
99
- writeLine(io.stdout, "Tower sessions (read-only attribution)");
100
- writeLine(io.stdout, `window: ${sessionsWindowLine(window)}`);
101
- for (const row of codexRows) {
102
- writeLine(io.stdout, sessionRowLine(row));
103
- }
104
- for (const row of claudeRows) {
105
- writeLine(io.stdout, sessionRowLine(row));
106
- for (const sidecar of row.sidecar_skips) {
107
- writeLine(io.stdout, ` sidecar ${sidecar.file_name}: ${sidecar.reason}`);
108
- }
109
- }
110
- if (codexRows.length === 0 && claudeRows.length === 0) {
111
- writeLine(io.stdout, readFailures.total > 0
112
- ? `No sessions listed, but ${readFailures.total} read failure(s) mean the scan could not see everywhere — this is not proof there were none.`
113
- : "No sessions observed in the scan window.");
114
- }
115
- if (readFailures.total > 0) {
116
- for (const line of readFailureLines(readFailures)) {
117
- writeLine(io.stdout, line);
118
- }
119
- }
120
- return 0;
130
+ return { codexRows, claudeRows };
121
131
  }
122
132
  /**
123
133
  * Everything the two scanners looked at and could not read. Counts only — the
@@ -46,15 +46,23 @@ async function runCoverageAction(command, io, tower) {
46
46
  const workspaces = Array.isArray(coverage["workspaces"])
47
47
  ? coverage["workspaces"]
48
48
  : [];
49
+ writeCoverageReply(command, io, body, coverage, workspaces);
50
+ logCoverageRead(command, io, workspaces);
51
+ // A stale cursor is a finding, not a failure of this command: exit 0 so a
52
+ // person reading coverage is not told their own read broke.
53
+ return 0;
54
+ }
55
+ function writeCoverageReply(command, io, body, coverage, workspaces) {
49
56
  if (command.json) {
50
57
  writeLine(io.stdout, JSON.stringify(body));
58
+ return;
51
59
  }
52
- else {
53
- const styled = colorEnabled(io);
54
- for (const line of coverageLines(coverage, workspaces, command.staleOnly === true, (text) => dim(text, styled))) {
55
- writeLine(io.stdout, line);
56
- }
60
+ const styled = colorEnabled(io);
61
+ for (const line of coverageLines(coverage, workspaces, command.staleOnly === true, (text) => dim(text, styled))) {
62
+ writeLine(io.stdout, line);
57
63
  }
64
+ }
65
+ function logCoverageRead(command, io, workspaces) {
58
66
  const staleTotal = workspaces.reduce((sum, row) => sum + numberOf(row["staleChannelsTotal"]), 0);
59
67
  writeLine(io.stderr, `[slack cli] coverage read ${JSON.stringify({
60
68
  workspace: command.workspace ?? "all",
@@ -64,75 +72,74 @@ async function runCoverageAction(command, io, tower) {
64
72
  open_skips: workspaces.reduce((sum, row) => sum + numberOf(row["openSkipsTotal"]), 0),
65
73
  stale_only: command.staleOnly === true,
66
74
  })}`);
67
- // A stale cursor is a finding, not a failure of this command: exit 0 so a
68
- // person reading coverage is not told their own read broke.
69
- return 0;
70
75
  }
71
76
  function coverageLines(coverage, workspaces, staleOnly, paint) {
72
77
  const lines = [];
73
78
  const staleAfter = numberOf(coverage["staleAfterHours"]);
74
79
  for (const workspace of workspaces) {
75
- const key = String(workspace["workspace"] ?? "?");
76
- const known = numberOf(workspace["channelsKnown"]);
77
- const covered = numberOf(workspace["covered"]);
78
- lines.push(`${key.toUpperCase()} ${covered} of ${known} channels readable`);
79
- if (!staleOnly) {
80
- const byVisibility = Array.isArray(workspace["byVisibility"])
81
- ? workspace["byVisibility"]
82
- : [];
83
- for (const entry of byVisibility) {
84
- lines.push(paint(` ${String(numberOf(entry["count"])).padStart(4)} ${String(entry["state"] ?? "?")} — ${String(entry["meaning"] ?? "?")}`));
85
- }
86
- lines.push(paint(` newest cursor: ${String(workspace["newestCursorIso"] ?? "never")}`));
87
- }
88
- const stale = Array.isArray(workspace["staleChannels"])
89
- ? workspace["staleChannels"]
90
- : [];
91
- const staleTotal = numberOf(workspace["staleChannelsTotal"]);
92
- if (staleTotal === 0) {
93
- lines.push(paint(` no channel is over ${staleAfter}h since its last sync`));
94
- }
95
- else {
96
- lines.push(` ${staleTotal} channel${staleTotal === 1 ? "" : "s"} over ${staleAfter}h since last sync:`);
97
- for (const entry of stale) {
98
- lines.push(paint(` ${String(entry["name"] ?? "?")} last ${String(entry["lastSyncedAt"] ?? "never")}`));
99
- }
100
- if (stale.length < staleTotal)
101
- lines.push(paint(` …and ${staleTotal - stale.length} more`));
102
- }
103
- const skips = Array.isArray(workspace["openSkips"])
104
- ? workspace["openSkips"]
105
- : [];
106
- if (skips.length > 0) {
107
- lines.push(` open ingest skips:`);
108
- for (const skip of skips) {
109
- lines.push(paint(` ${String(numberOf(skip["count"])).padStart(4)} ${String(skip["reason"] ?? "?")}`));
110
- }
111
- }
112
- lines.push("");
80
+ writeWorkspaceCoverageLines(lines, workspace, staleAfter, staleOnly, paint);
113
81
  }
82
+ writeCoverageFooter(lines, coverage, paint);
83
+ return lines;
84
+ }
85
+ function writeWorkspaceCoverageLines(lines, workspace, staleAfter, staleOnly, paint) {
86
+ const key = String(workspace["workspace"] ?? "?");
87
+ const known = numberOf(workspace["channelsKnown"]);
88
+ const covered = numberOf(workspace["covered"]);
89
+ lines.push(`${key.toUpperCase()} ${covered} of ${known} channels readable`);
90
+ writeVisibilityLines(lines, workspace, staleOnly, paint);
91
+ writeStaleChannelLines(lines, workspace, staleAfter, paint);
92
+ writeOpenSkipLines(lines, workspace, paint);
93
+ lines.push("");
94
+ }
95
+ function writeCoverageFooter(lines, coverage, paint) {
114
96
  const orphanChannels = numberOf(coverage["orphanChannels"]);
115
97
  if (orphanChannels > 0) {
116
98
  lines.push(paint(`${orphanChannels} channel(s) belong to no known workspace and are not counted above.`));
117
99
  }
118
100
  if (typeof coverage["note"] === "string")
119
101
  lines.push(paint(coverage["note"]));
120
- return lines;
102
+ }
103
+ function writeVisibilityLines(lines, workspace, staleOnly, paint) {
104
+ if (staleOnly)
105
+ return;
106
+ const byVisibility = Array.isArray(workspace["byVisibility"])
107
+ ? workspace["byVisibility"]
108
+ : [];
109
+ for (const entry of byVisibility) {
110
+ lines.push(paint(` ${String(numberOf(entry["count"])).padStart(4)} ${String(entry["state"] ?? "?")} — ${String(entry["meaning"] ?? "?")}`));
111
+ }
112
+ lines.push(paint(` newest cursor: ${String(workspace["newestCursorIso"] ?? "never")}`));
113
+ }
114
+ function writeStaleChannelLines(lines, workspace, staleAfter, paint) {
115
+ const stale = Array.isArray(workspace["staleChannels"])
116
+ ? workspace["staleChannels"]
117
+ : [];
118
+ const staleTotal = numberOf(workspace["staleChannelsTotal"]);
119
+ if (staleTotal === 0) {
120
+ lines.push(paint(` no channel is over ${staleAfter}h since its last sync`));
121
+ return;
122
+ }
123
+ lines.push(` ${staleTotal} channel${staleTotal === 1 ? "" : "s"} over ${staleAfter}h since last sync:`);
124
+ for (const entry of stale) {
125
+ lines.push(paint(` ${String(entry["name"] ?? "?")} last ${String(entry["lastSyncedAt"] ?? "never")}`));
126
+ }
127
+ if (stale.length < staleTotal)
128
+ lines.push(paint(` …and ${staleTotal - stale.length} more`));
129
+ }
130
+ function writeOpenSkipLines(lines, workspace, paint) {
131
+ const skips = Array.isArray(workspace["openSkips"])
132
+ ? workspace["openSkips"]
133
+ : [];
134
+ if (skips.length === 0)
135
+ return;
136
+ lines.push(` open ingest skips:`);
137
+ for (const skip of skips) {
138
+ lines.push(paint(` ${String(numberOf(skip["count"])).padStart(4)} ${String(skip["reason"] ?? "?")}`));
139
+ }
121
140
  }
122
141
  async function runReadAction(command, io, tower) {
123
- const body = {};
124
- if (command.person)
125
- body["person"] = command.person;
126
- if (command.channel)
127
- body["channel"] = command.channel;
128
- if (command.query)
129
- body["query"] = command.query;
130
- if (command.since)
131
- body["since"] = command.since;
132
- if (command.until)
133
- body["until"] = command.until;
134
- if (command.limit !== undefined)
135
- body["limit"] = command.limit;
142
+ const body = buildReadBody(command);
136
143
  const result = await callTower(tower, {
137
144
  path: "/api/ops/slack/read",
138
145
  method: "POST",
@@ -156,6 +163,27 @@ async function runReadAction(command, io, tower) {
156
163
  const messages = Array.isArray(read["messages"])
157
164
  ? read["messages"]
158
165
  : [];
166
+ writeReadReply(command, io, payload, read, messages);
167
+ logReadResult(command, io, read, messages);
168
+ return 0;
169
+ }
170
+ function buildReadBody(command) {
171
+ const body = {};
172
+ if (command.person)
173
+ body["person"] = command.person;
174
+ if (command.channel)
175
+ body["channel"] = command.channel;
176
+ if (command.query)
177
+ body["query"] = command.query;
178
+ if (command.since)
179
+ body["since"] = command.since;
180
+ if (command.until)
181
+ body["until"] = command.until;
182
+ if (command.limit !== undefined)
183
+ body["limit"] = command.limit;
184
+ return body;
185
+ }
186
+ function writeReadReply(command, io, payload, read, messages) {
159
187
  if (command.json) {
160
188
  writeLine(io.stdout, JSON.stringify(payload));
161
189
  }
@@ -179,6 +207,8 @@ async function runReadAction(command, io, tower) {
179
207
  writeLine(io.stdout, dim(read["note"], styled));
180
208
  }
181
209
  }
210
+ }
211
+ function logReadResult(command, io, read, messages) {
182
212
  const coverage = asRecord(read["coverage"]);
183
213
  writeLine(io.stderr, `[slack cli] read ${JSON.stringify({
184
214
  status: read["status"] ?? null,
@@ -191,7 +221,6 @@ async function runReadAction(command, io, tower) {
191
221
  has_channel: Boolean(command.channel),
192
222
  has_query: Boolean(command.query),
193
223
  })}`);
194
- return 0;
195
224
  }
196
225
  function numberOf(value) {
197
226
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -29,6 +29,14 @@ import { getCollectorRuntimePaths, inspectLocalCollectorStatus, readLocalCollect
29
29
  import { normalizeCollectionRoots } from "../root-normalization.js";
30
30
  export async function runStatus(command, io) {
31
31
  const backfillCursor = await inspectBackfillCursor(command.homeDir);
32
+ const setupReceipt = await readMachineSetupReceipt(command.homeDir, io);
33
+ const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
34
+ if (worktrees.length > 1) {
35
+ return writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, worktrees);
36
+ }
37
+ return writeSingleRepoStatus(command, io, backfillCursor, setupReceipt);
38
+ }
39
+ async function readMachineSetupReceipt(homeDir, io) {
32
40
  // BLI-3731: "is this machine actually connected?" belongs beside "is it
33
41
  // collecting?", and it is read back off the host rather than remembered.
34
42
  // Refreshing here also keeps the heartbeat's cache warm without the 15-minute
@@ -38,40 +46,43 @@ export async function runStatus(command, io) {
38
46
  // about the MACHINE, so it cannot depend on whether the folder you happen to
39
47
  // stand in holds one repo or six — and the multi-repo branch, which every
40
48
  // agent machine takes, used to return before the receipt was ever built.
41
- const setupReceipt = await refreshSetupReceipt(io, {
42
- ...(command.homeDir ? { homeDir: command.homeDir } : {}),
49
+ return refreshSetupReceipt(io, {
50
+ ...(homeDir ? { homeDir } : {}),
43
51
  });
44
- const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
45
- if (worktrees.length > 1) {
46
- const statuses = await Promise.all(worktrees.map(async (worktree) => ({
47
- ...(await inspectLocalCollectorStatus({
48
- homeDir: command.homeDir,
49
- repoRoot: worktree.repo_root,
50
- })),
51
- head_sha: worktree.head_sha,
52
- })));
53
- if (command.json) {
54
- writeLine(io.stdout, JSON.stringify({
55
- mode: "multi_repo",
56
- statuses,
57
- backfill_cursor: backfillCursor,
58
- // Beside `backfill_cursor`, and for the same reason: both are facts
59
- // about this machine, so both sit at the top of the payload rather
60
- // than once per repo row. Null means this run could not read the
61
- // host — `refreshSetupReceipt` named the reason on stderr — never
62
- // "this machine is not connected".
63
- setup_receipt: setupReceipt?.receipt ?? null,
64
- }, null, 2));
65
- return 0;
66
- }
67
- writeLine(io.stdout, "Tower status — every repo below this folder");
68
- writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
69
- for (const status of statuses) {
70
- writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
71
- }
72
- writeConnectedBlock(io, setupReceipt);
73
- return 0;
52
+ }
53
+ async function writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, worktrees) {
54
+ const statuses = await Promise.all(worktrees.map(async (worktree) => ({
55
+ ...(await inspectLocalCollectorStatus({
56
+ homeDir: command.homeDir,
57
+ repoRoot: worktree.repo_root,
58
+ })),
59
+ head_sha: worktree.head_sha,
60
+ })));
61
+ if (command.json)
62
+ return writeMultiRepoJson(io, backfillCursor, setupReceipt, statuses);
63
+ writeLine(io.stdout, "Tower status — every repo below this folder");
64
+ writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
65
+ for (const status of statuses) {
66
+ writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
74
67
  }
68
+ writeConnectedBlock(io, setupReceipt);
69
+ return 0;
70
+ }
71
+ function writeMultiRepoJson(io, backfillCursor, setupReceipt, statuses) {
72
+ writeLine(io.stdout, JSON.stringify({
73
+ mode: "multi_repo",
74
+ statuses,
75
+ backfill_cursor: backfillCursor,
76
+ // Beside `backfill_cursor`, and for the same reason: both are facts
77
+ // about this machine, so both sit at the top of the payload rather
78
+ // than once per repo row. Null means this run could not read the
79
+ // host — `refreshSetupReceipt` named the reason on stderr — never
80
+ // "this machine is not connected".
81
+ setup_receipt: setupReceipt?.receipt ?? null,
82
+ }, null, 2));
83
+ return 0;
84
+ }
85
+ async function writeSingleRepoStatus(command, io, backfillCursor, setupReceipt) {
75
86
  // The staging walk is opt-in (BLI-3797) and this is the surface that pays for
76
87
  // it: one person asking one question. The multi-repo branch above deliberately
77
88
  // does not — the backlog is a MACHINE fact, so N repos would walk one tree N
@@ -80,14 +91,16 @@ export async function runStatus(command, io) {
80
91
  ...command,
81
92
  includeStagingBacklog: true,
82
93
  });
83
- if (command.json) {
84
- writeLine(io.stdout, JSON.stringify({
85
- ...status,
86
- backfill_cursor: backfillCursor,
87
- setup_receipt: setupReceipt?.receipt ?? null,
88
- }, null, 2));
89
- return 0;
90
- }
94
+ if (command.json)
95
+ return writeSingleRepoJson(io, status, backfillCursor, setupReceipt);
96
+ writeSingleRepoLines(io, status, backfillCursor, setupReceipt);
97
+ return 0;
98
+ }
99
+ function writeSingleRepoJson(io, status, backfillCursor, setupReceipt) {
100
+ writeLine(io.stdout, JSON.stringify({ ...status, backfill_cursor: backfillCursor, setup_receipt: setupReceipt?.receipt ?? null }, null, 2));
101
+ return 0;
102
+ }
103
+ function writeSingleRepoLines(io, status, backfillCursor, setupReceipt) {
91
104
  writeLine(io.stdout, "Tower status");
92
105
  writeLine(io.stdout, `Installed: ${status.installed ? "yes" : "no"}`);
93
106
  writeLine(io.stdout, `Signed in: ${signedInLine(status.session_state)}`);
@@ -117,7 +130,6 @@ export async function runStatus(command, io) {
117
130
  for (const detail of status.details)
118
131
  writeLine(io.stdout, `- ${detail}`);
119
132
  writeConnectedBlock(io, setupReceipt);
120
- return 0;
121
133
  }
122
134
  /**
123
135
  * The receipt block, in the words `cockpit doctor` prints, from the one reader
@@ -100,26 +100,9 @@ function logIndexRead(io, projects) {
100
100
  function writeDoc(command, io, payload, width) {
101
101
  const markdown = payload.markdown ?? "";
102
102
  const sections = payload.sections ?? [];
103
- let selected = markdown;
104
- if (command.section) {
105
- const slice = sectionSlice(markdown, sections, command.section);
106
- if (slice === null) {
107
- const known = sections.map((section) => section.id ?? "").filter(Boolean);
108
- const sentence = `No section "${command.section}" in this document (${known.join(", ") || "none"}).`;
109
- writeLine(io.stderr, `[workbook cli] section missed ${JSON.stringify({
110
- reason: "workbook_section_not_found",
111
- sections: sections.length,
112
- })}`);
113
- if (command.json) {
114
- writeLine(io.stdout, JSON.stringify({ ok: false, error: "workbook_section_not_found", detail: sentence }));
115
- }
116
- else {
117
- writeLine(io.stderr, sentence);
118
- }
119
- return 1;
120
- }
121
- selected = slice;
122
- }
103
+ const selected = selectDocumentSection(command, io, markdown, sections);
104
+ if (selected === null)
105
+ return 1;
123
106
  if (command.json) {
124
107
  writeLine(io.stdout, JSON.stringify({
125
108
  ok: true,
@@ -150,6 +133,26 @@ function writeDoc(command, io, payload, width) {
150
133
  })}`);
151
134
  return 0;
152
135
  }
136
+ function selectDocumentSection(command, io, markdown, sections) {
137
+ if (!command.section)
138
+ return markdown;
139
+ const slice = sectionSlice(markdown, sections, command.section);
140
+ if (slice !== null)
141
+ return slice;
142
+ const known = sections.map((section) => section.id ?? "").filter(Boolean);
143
+ const sentence = `No section "${command.section}" in this document (${known.join(", ") || "none"}).`;
144
+ writeLine(io.stderr, `[workbook cli] section missed ${JSON.stringify({
145
+ reason: "workbook_section_not_found",
146
+ sections: sections.length,
147
+ })}`);
148
+ if (command.json) {
149
+ writeLine(io.stdout, JSON.stringify({ ok: false, error: "workbook_section_not_found", detail: sentence }));
150
+ }
151
+ else {
152
+ writeLine(io.stderr, sentence);
153
+ }
154
+ return null;
155
+ }
153
156
  // ----------------------------------------------------------------- small parts
154
157
  /**
155
158
  * The wrap column: an explicit `--width` wins, otherwise the terminal's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.117",
3
+ "version": "0.2.121",
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.32",
31
- "@bli-cockpit/mcp": "0.1.45",
31
+ "@bli-cockpit/mcp": "0.1.48",
32
32
  "@bli-cockpit/telemetry-core": "0.1.48"
33
33
  }
34
34
  }