@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
@@ -4,6 +4,13 @@ import { runSync } from "./sync.js";
4
4
  import { describeError, isMissingFileFailure } from "../health-detail.js";
5
5
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "../local-state.js";
6
6
  export async function runAnalyze(command, io) {
7
+ const sync = await runAnalysisSync(command, io);
8
+ if (sync.exitCode !== 0 || !isUploadedSyncResult(sync.output)) {
9
+ return writeIncompleteSync(command, io, sync);
10
+ }
11
+ return queueAnalysis(command, io, sync);
12
+ }
13
+ async function runAnalysisSync(command, io) {
7
14
  const syncStdout = [];
8
15
  const syncStderr = [];
9
16
  const syncIo = {
@@ -17,39 +24,33 @@ export async function runAnalyze(command, io) {
17
24
  json: true,
18
25
  };
19
26
  const syncExitCode = await runSync(syncCommand, syncIo);
20
- const syncOutput = parseCapturedJson(syncStdout);
21
- if (syncExitCode !== 0 || !isUploadedSyncResult(syncOutput)) {
22
- if (command.json) {
23
- writeLine(io.stdout, JSON.stringify({
24
- status: syncExitCode === 0 ? "sync_not_completed" : "sync_failed",
25
- sync: syncOutput,
26
- error: syncStderr.join("").trim()
27
- || (syncExitCode === 0
28
- ? "Tower sync did not upload fresh evidence."
29
- : "Tower sync failed."),
30
- }, null, 2));
31
- }
32
- else {
33
- replayCaptured(io.stderr, syncStderr);
34
- writeLine(io.stderr, syncExitCode === 0
35
- ? "Analysis was not queued because a fresh upload did not complete."
36
- : "Analysis was not queued because the latest upload failed.");
37
- }
38
- return syncExitCode === 0 ? 1 : syncExitCode;
27
+ return { exitCode: syncExitCode, output: parseCapturedJson(syncStdout), stderr: syncStderr };
28
+ }
29
+ function writeIncompleteSync(command, io, sync) {
30
+ const error = syncFailureMessage(sync);
31
+ if (command.json) {
32
+ writeLine(io.stdout, JSON.stringify({
33
+ status: sync.exitCode === 0 ? "sync_not_completed" : "sync_failed",
34
+ sync: sync.output,
35
+ error,
36
+ }, null, 2));
39
37
  }
40
- const paths = getCollectorRuntimePaths(command.homeDir);
41
- const session = await readLocalCollectorSessionFile(paths).catch((error) => {
42
- // "Not signed in" is the right sentence for an absent session file and the
43
- // wrong one for a corrupt one, which `cockpit login` will not repair
44
- // (BLI-3238).
45
- if (!isMissingFileFailure(error)) {
46
- console.error("[cockpit-analyze] session file present but unreadable, reporting as not signed in", JSON.stringify({
47
- reason: "session_file_unusable",
48
- ...describeError(error),
49
- }));
50
- }
51
- throw new Error("Tower is not signed in. Run `cockpit onboard` or `cockpit login` first.");
52
- });
38
+ else {
39
+ replayCaptured(io.stderr, sync.stderr);
40
+ writeLine(io.stderr, sync.exitCode === 0
41
+ ? "Analysis was not queued because a fresh upload did not complete."
42
+ : "Analysis was not queued because the latest upload failed.");
43
+ }
44
+ return sync.exitCode === 0 ? 1 : sync.exitCode;
45
+ }
46
+ function syncFailureMessage(sync) {
47
+ return sync.stderr.join("").trim()
48
+ || (sync.exitCode === 0
49
+ ? "Tower sync did not upload fresh evidence."
50
+ : "Tower sync failed.");
51
+ }
52
+ async function queueAnalysis(command, io, sync) {
53
+ const session = await readAnalyzeSession(command.homeDir);
53
54
  const dashboardUrl = normalizeUrl(command.dashboardUrl ?? session.dashboard_url ?? DEFAULT_DASHBOARD_URL);
54
55
  const response = await io.fetch(`${dashboardUrl}/api/ambient/analyze`, {
55
56
  method: "POST",
@@ -60,40 +61,59 @@ export async function runAnalyze(command, io) {
60
61
  body: "{}",
61
62
  });
62
63
  const body = await readAnalyzeApiResponse(response);
63
- if (!response.ok) {
64
- const message = typeof body.message === "string"
65
- ? body.message
66
- : `Analysis request failed with HTTP ${response.status}.`;
67
- if (command.json) {
68
- writeLine(io.stdout, JSON.stringify({
69
- status: "analyze_failed",
70
- sync: syncOutput,
71
- http_status: response.status,
72
- code: typeof body.code === "string" ? body.code : null,
73
- message,
74
- retry_after_seconds: typeof body.retry_after_seconds === "number"
75
- ? body.retry_after_seconds
76
- : null,
77
- }, null, 2));
78
- }
79
- else {
80
- replayCaptured(io.stderr, syncStderr);
81
- writeLine(io.stderr, `Analysis was not queued: ${message}`);
64
+ if (!response.ok)
65
+ return writeFailedAnalysis(command, io, sync, response, body);
66
+ return writeQueuedAnalysis(command, io, sync, dashboardUrl, body);
67
+ }
68
+ async function readAnalyzeSession(homeDir) {
69
+ const paths = getCollectorRuntimePaths(homeDir);
70
+ return readLocalCollectorSessionFile(paths).catch((error) => {
71
+ // "Not signed in" is the right sentence for an absent session file and the
72
+ // wrong one for a corrupt one, which `cockpit login` will not repair
73
+ // (BLI-3238).
74
+ if (!isMissingFileFailure(error)) {
75
+ console.error("[cockpit-analyze] session file present but unreadable, reporting as not signed in", JSON.stringify({
76
+ reason: "session_file_unusable",
77
+ ...describeError(error),
78
+ }));
82
79
  }
83
- return 1;
80
+ throw new Error("Tower is not signed in. Run `cockpit onboard` or `cockpit login` first.");
81
+ });
82
+ }
83
+ function writeFailedAnalysis(command, io, sync, response, body) {
84
+ const message = typeof body.message === "string"
85
+ ? body.message
86
+ : `Analysis request failed with HTTP ${response.status}.`;
87
+ const retryAfterSeconds = typeof body.retry_after_seconds === "number" ? body.retry_after_seconds : null;
88
+ if (command.json) {
89
+ writeLine(io.stdout, JSON.stringify({
90
+ status: "analyze_failed",
91
+ sync: sync.output,
92
+ http_status: response.status,
93
+ code: typeof body.code === "string" ? body.code : null,
94
+ message,
95
+ retry_after_seconds: retryAfterSeconds,
96
+ }, null, 2));
84
97
  }
98
+ else {
99
+ replayCaptured(io.stderr, sync.stderr);
100
+ writeLine(io.stderr, `Analysis was not queued: ${message}`);
101
+ }
102
+ return 1;
103
+ }
104
+ function writeQueuedAnalysis(command, io, sync, dashboardUrl, body) {
85
105
  const jobId = typeof body.job?.id === "string" ? body.job.id : null;
86
106
  const jobStatus = typeof body.job?.status === "string" ? body.job.status : "pending";
87
107
  if (command.json) {
88
108
  writeLine(io.stdout, JSON.stringify({
89
109
  status: "queued",
90
- sync: syncOutput,
110
+ sync: sync.output,
91
111
  job: body.job ?? null,
92
112
  dashboard_url: `${dashboardUrl}/my-work`,
93
113
  }, null, 2));
94
114
  return 0;
95
115
  }
96
- replayCaptured(io.stderr, syncStderr);
116
+ replayCaptured(io.stderr, sync.stderr);
97
117
  writeLine(io.stdout, "Tower uploaded your latest work.");
98
118
  writeLine(io.stdout, "Tower analysis queued.");
99
119
  if (jobId)
@@ -42,23 +42,64 @@ export async function runBriefRewrite(command, io, watchOptions = {}) {
42
42
  const log = (line) => writeLine(io.stderr, line);
43
43
  const styled = colorEnabled(io);
44
44
  const startedAt = Date.now();
45
+ const target = await readRewriteTarget(command, io, session.device_token, dashboardUrl, log);
46
+ if (!target.ok)
47
+ return target.exitCode;
48
+ const queued = await queueBriefRewrite(command, io, session.device_token, dashboardUrl, log, target.personId, target.displayName);
49
+ if (!queued.ok)
50
+ return queued.exitCode;
51
+ const { answer, request, whose } = queued;
52
+ const scheduled = answer.compileScheduled !== false;
53
+ writeQueueReceipt(io, request.requestId, answer, command.wait !== false);
54
+ if (!scheduled) {
55
+ return reportQueueOnly(command, io, request.requestId, target.personId, answer, whose);
56
+ }
57
+ if (command.wait === false) {
58
+ return reportQueuedRewrite(command, io, request.requestId, target.personId, whose, styled);
59
+ }
60
+ return watchQueuedRewrite({
61
+ command,
62
+ io,
63
+ session,
64
+ dashboardUrl,
65
+ log,
66
+ styled,
67
+ startedAt,
68
+ beforePageId: target.beforePageId,
69
+ personId: target.personId,
70
+ requestId: request.requestId,
71
+ whose,
72
+ watchOptions,
73
+ });
74
+ }
75
+ async function readRewriteTarget(command, io, deviceToken, dashboardUrl, log) {
45
76
  // Which page, and what it looks like NOW — the "before" the poll compares
46
77
  // against. Read first for a second reason: it is the only way this command
47
78
  // learns the person id, and it is the same read that decides whether this
48
79
  // caller may see that page at all.
49
- const before = await readPage(command, io, session.device_token, dashboardUrl, log);
80
+ const before = await readPage(command, io, deviceToken, dashboardUrl, log);
50
81
  if (!before.ok)
51
- return fail(command, io, before.reason, before.detail);
82
+ return { ok: false, exitCode: fail(command, io, before.reason, before.detail) };
52
83
  const personId = before.page.page?.personId;
53
84
  if (!personId) {
54
- return fail(command, io, before.page.error ?? "no_page", before.page.reply ??
55
- "There is no page here to write again yet. The first one is compiled by the daily job.");
85
+ return {
86
+ ok: false,
87
+ exitCode: fail(command, io, before.page.error ?? "no_page", before.page.reply ??
88
+ "There is no page here to write again yet. The first one is compiled by the daily job."),
89
+ };
56
90
  }
57
- const beforePageId = before.page.page?.pageId ?? null;
91
+ return {
92
+ ok: true,
93
+ personId,
94
+ beforePageId: before.page.page?.pageId ?? null,
95
+ displayName: before.page.page?.displayName,
96
+ };
97
+ }
98
+ async function queueBriefRewrite(command, io, deviceToken, dashboardUrl, log, personId, displayName) {
58
99
  const queued = await towerJsonRequest({
59
100
  dashboardUrl,
60
101
  path: "/api/jarvis/recompile",
61
- deviceToken: session.device_token,
102
+ deviceToken,
62
103
  fetch: io.fetch,
63
104
  label: "brief:rewrite",
64
105
  timeoutMs: REQUEST_DEADLINE_MS,
@@ -66,91 +107,108 @@ export async function runBriefRewrite(command, io, watchOptions = {}) {
66
107
  body: { personIds: [personId], mode: "queue" },
67
108
  });
68
109
  if (!queued.ok)
69
- return fail(command, io, queued.reason, queued.detail);
110
+ return { ok: false, exitCode: fail(command, io, queued.reason, queued.detail) };
70
111
  const answer = queued.body;
71
112
  const request = (answer.queued ?? [])[0];
72
113
  if (!answer.ok || !request?.requestId) {
73
- return fail(command, io, "not_queued", answer.reply ?? "Tower did not accept that ask.");
114
+ return {
115
+ ok: false,
116
+ exitCode: fail(command, io, "not_queued", answer.reply ?? "Tower did not accept that ask."),
117
+ };
74
118
  }
75
- const whose = request.name ?? before.page.page?.displayName ?? "that page";
76
- const scheduled = answer.compileScheduled !== false;
119
+ return {
120
+ ok: true,
121
+ answer,
122
+ request: { requestId: request.requestId, name: request.name },
123
+ whose: request.name ?? displayName ?? "that page",
124
+ };
125
+ }
126
+ function writeQueueReceipt(io, requestId, answer, waiting) {
77
127
  writeLine(io.stderr, `[brief rewrite] queued ${JSON.stringify({
78
- request_id: request.requestId,
79
- compile_scheduled: scheduled,
128
+ request_id: requestId,
129
+ compile_scheduled: answer.compileScheduled !== false,
80
130
  compile_reason: answer.compileReason ?? null,
81
- waiting: command.wait !== false,
131
+ waiting,
82
132
  })}`);
83
- if (!scheduled) {
84
- // Queue-only, said as queue-only. The ask is a real row; what is missing is
85
- // somebody to run it in the next minute, and pretending otherwise would
86
- // leave a person watching a page that was never going to change.
87
- const sentence = `${whose} is on the list to be written again, but this Tower could not start the work now ` +
88
- `(${answer.compileReason ?? "no reason given"}). The daily compile will reach it. Nothing is lost.`;
89
- if (command.json) {
90
- writeLine(io.stdout, JSON.stringify({
91
- ok: true,
92
- outcome: "queued_only",
93
- requestId: request.requestId,
94
- personId,
95
- compileScheduled: false,
96
- compileReason: answer.compileReason ?? null,
97
- }));
98
- }
99
- else {
100
- writeLine(io.stdout, sentence);
101
- }
102
- return 0;
133
+ }
134
+ function reportQueueOnly(command, io, requestId, personId, answer, whose) {
135
+ // Queue-only, said as queue-only. The ask is a real row; what is missing is
136
+ // somebody to run it in the next minute, and pretending otherwise would
137
+ // leave a person watching a page that was never going to change.
138
+ const sentence = `${whose} is on the list to be written again, but this Tower could not start the work now ` +
139
+ `(${answer.compileReason ?? "no reason given"}). The daily compile will reach it. Nothing is lost.`;
140
+ if (command.json) {
141
+ writeLine(io.stdout, JSON.stringify({
142
+ ok: true,
143
+ outcome: "queued_only",
144
+ requestId,
145
+ personId,
146
+ compileScheduled: false,
147
+ compileReason: answer.compileReason ?? null,
148
+ }));
103
149
  }
104
- if (command.wait === false) {
105
- if (command.json) {
106
- writeLine(io.stdout, JSON.stringify({
107
- ok: true,
108
- outcome: "queued",
109
- requestId: request.requestId,
110
- personId,
111
- compileScheduled: true,
112
- }));
113
- }
114
- else {
115
- writeLine(io.stdout, `Asked. ${whose} is being written again.`);
116
- writeLine(io.stdout, dim("Run `cockpit brief` in a few minutes to read it.", styled));
117
- }
118
- return 0;
150
+ else {
151
+ writeLine(io.stdout, sentence);
152
+ }
153
+ return 0;
154
+ }
155
+ function reportQueuedRewrite(command, io, requestId, personId, whose, styled) {
156
+ if (command.json) {
157
+ writeLine(io.stdout, JSON.stringify({
158
+ ok: true,
159
+ outcome: "queued",
160
+ requestId,
161
+ personId,
162
+ compileScheduled: true,
163
+ }));
164
+ }
165
+ else {
166
+ writeLine(io.stdout, `Asked. ${whose} is being written again.`);
167
+ writeLine(io.stdout, dim("Run `cockpit brief` in a few minutes to read it.", styled));
119
168
  }
120
- writeLine(io.stderr, dim(`Writing ${whose} again. This takes a few minutes; watching for the new version…`, styled));
169
+ return 0;
170
+ }
171
+ async function watchQueuedRewrite(input) {
172
+ writeLine(input.io.stderr, dim(`Writing ${input.whose} again. This takes a few minutes; watching for the new version…`, input.styled));
121
173
  const watched = await watchForNewVersion({
122
- command,
123
- io,
124
- deviceToken: session.device_token,
125
- dashboardUrl,
126
- beforePageId,
127
- log,
128
- pollEveryMs: watchOptions.pollEveryMs ?? POLL_EVERY_MS,
129
- ceilingMs: watchOptions.ceilingMs ?? WATCH_CEILING_MS,
174
+ command: input.command,
175
+ io: input.io,
176
+ deviceToken: input.session.device_token,
177
+ dashboardUrl: input.dashboardUrl,
178
+ beforePageId: input.beforePageId,
179
+ log: input.log,
180
+ pollEveryMs: input.watchOptions.pollEveryMs ?? POLL_EVERY_MS,
181
+ ceilingMs: input.watchOptions.ceilingMs ?? WATCH_CEILING_MS,
130
182
  });
131
183
  if (watched.outcome === "written") {
132
- if (command.json) {
133
- writeLine(io.stdout, JSON.stringify({
134
- ok: true,
135
- outcome: "written",
136
- requestId: request.requestId,
137
- personId,
138
- pageId: watched.pageId,
139
- waitedMs: watched.waitedMs,
140
- }));
141
- }
142
- else {
143
- writeLine(io.stdout, `${whose} has been written again.`);
144
- writeLine(io.stdout, dim("Run `cockpit brief` to read it.", styled));
145
- }
146
- writeLine(io.stderr, `[brief rewrite] written ${JSON.stringify({
147
- request_id: request.requestId,
148
- polls: watched.polls,
149
- waited_ms: watched.waitedMs,
150
- elapsed_ms: Date.now() - startedAt,
151
- })}`);
152
- return 0;
184
+ return reportWrittenRewrite(input, watched);
153
185
  }
186
+ return reportUnconfirmedRewrite(input.command, input.io, input.requestId, input.personId, input.whose, watched);
187
+ }
188
+ function reportWrittenRewrite(input, watched) {
189
+ if (input.command.json) {
190
+ writeLine(input.io.stdout, JSON.stringify({
191
+ ok: true,
192
+ outcome: "written",
193
+ requestId: input.requestId,
194
+ personId: input.personId,
195
+ pageId: watched.pageId,
196
+ waitedMs: watched.waitedMs,
197
+ }));
198
+ }
199
+ else {
200
+ writeLine(input.io.stdout, `${input.whose} has been written again.`);
201
+ writeLine(input.io.stdout, dim("Run `cockpit brief` to read it.", input.styled));
202
+ }
203
+ writeLine(input.io.stderr, `[brief rewrite] written ${JSON.stringify({
204
+ request_id: input.requestId,
205
+ polls: watched.polls,
206
+ waited_ms: watched.waitedMs,
207
+ elapsed_ms: Date.now() - input.startedAt,
208
+ })}`);
209
+ return 0;
210
+ }
211
+ function reportUnconfirmedRewrite(command, io, requestId, personId, whose, watched) {
154
212
  // Not a failure, and named so it cannot be read as one. Nothing observed here
155
213
  // says the compile stopped — only that this command stopped watching.
156
214
  const sentence = watched.outcome === "watch_failed"
@@ -163,7 +221,7 @@ export async function runBriefRewrite(command, io, watchOptions = {}) {
163
221
  writeLine(io.stdout, JSON.stringify({
164
222
  ok: false,
165
223
  outcome: watched.outcome,
166
- requestId: request.requestId,
224
+ requestId,
167
225
  personId,
168
226
  waitedMs: watched.waitedMs,
169
227
  ...(watched.outcome === "watch_failed" ? { detail: watched.detail } : {}),
@@ -173,7 +231,7 @@ export async function runBriefRewrite(command, io, watchOptions = {}) {
173
231
  writeLine(io.stderr, sentence);
174
232
  }
175
233
  writeLine(io.stderr, `[brief rewrite] not confirmed ${JSON.stringify({
176
- request_id: request.requestId,
234
+ request_id: requestId,
177
235
  reason: watched.outcome,
178
236
  polls: watched.polls,
179
237
  waited_ms: watched.waitedMs,
@@ -194,30 +252,35 @@ async function watchForNewVersion(input) {
194
252
  while (Date.now() - startedAt < input.ceilingMs) {
195
253
  await sleep(input.pollEveryMs);
196
254
  polls += 1;
197
- const read = await readPage(input.command, input.io, input.deviceToken, input.dashboardUrl, input.log);
198
- const waitedMs = Date.now() - startedAt;
199
- if (!read.ok) {
200
- // A single blip while waiting is not an answer about the compile. Only a
201
- // refusal is, and that one stops the watch rather than spinning.
202
- if (read.httpStatus === 403 || read.httpStatus === 401) {
203
- return { outcome: "watch_failed", detail: read.detail, polls, waitedMs };
204
- }
205
- input.log(`[brief rewrite] poll failed ${JSON.stringify({ poll: polls, reason: read.reason })}`);
206
- continue;
207
- }
208
- const pageId = read.page.page?.pageId ?? null;
209
- input.log(`[brief rewrite] polled ${JSON.stringify({
210
- poll: polls,
211
- changed: pageId != null && pageId !== input.beforePageId,
212
- compiled_at: read.page.page?.compiledAt ?? null,
213
- waited_ms: waitedMs,
214
- })}`);
215
- if (pageId && pageId !== input.beforePageId) {
216
- return { outcome: "written", pageId, polls, waitedMs };
217
- }
255
+ const result = await readNextPageVersion(input, polls, startedAt);
256
+ if (result)
257
+ return result;
218
258
  }
219
259
  return { outcome: "still_running", polls, waitedMs: Date.now() - startedAt };
220
260
  }
261
+ async function readNextPageVersion(input, polls, startedAt) {
262
+ const read = await readPage(input.command, input.io, input.deviceToken, input.dashboardUrl, input.log);
263
+ const waitedMs = Date.now() - startedAt;
264
+ if (!read.ok) {
265
+ // A single blip while waiting is not an answer about the compile. Only a
266
+ // refusal is, and that one stops the watch rather than spinning.
267
+ if (read.httpStatus === 403 || read.httpStatus === 401) {
268
+ return { outcome: "watch_failed", detail: read.detail, polls, waitedMs };
269
+ }
270
+ input.log(`[brief rewrite] poll failed ${JSON.stringify({ poll: polls, reason: read.reason })}`);
271
+ return null;
272
+ }
273
+ const pageId = read.page.page?.pageId ?? null;
274
+ input.log(`[brief rewrite] polled ${JSON.stringify({
275
+ poll: polls,
276
+ changed: pageId != null && pageId !== input.beforePageId,
277
+ compiled_at: read.page.page?.compiledAt ?? null,
278
+ waited_ms: waitedMs,
279
+ })}`);
280
+ return pageId && pageId !== input.beforePageId
281
+ ? { outcome: "written", pageId, polls, waitedMs }
282
+ : null;
283
+ }
221
284
  async function readPage(command, io, deviceToken, dashboardUrl, log) {
222
285
  const params = new URLSearchParams({ tldr: "1" });
223
286
  if (command.subject)
@@ -23,10 +23,18 @@ export async function runBrief(command, io) {
23
23
  if (command.action === "status") {
24
24
  return runBriefStatus(command, io, dashboardUrl, session.device_token, startedAt);
25
25
  }
26
+ const body = await requestBrief(command, io, dashboardUrl, session.device_token);
27
+ if (!body)
28
+ return 1;
29
+ writeBriefReply(command, io, body);
30
+ logBriefRead(command, io, body, startedAt);
31
+ return 0;
32
+ }
33
+ async function requestBrief(command, io, dashboardUrl, deviceToken) {
26
34
  const result = await towerJsonRequest({
27
35
  dashboardUrl,
28
36
  path: `/api/jarvis/brief${queryFor(command)}`,
29
- deviceToken: session.device_token,
37
+ deviceToken,
30
38
  fetch: io.fetch,
31
39
  method: "GET",
32
40
  label: "brief",
@@ -39,20 +47,25 @@ export async function runBrief(command, io) {
39
47
  // on the page ("Nothing has been written for you yet."). Relayed verbatim
40
48
  // rather than translated into a code.
41
49
  writeFailure(command, io, result.reason, result.detail);
42
- return 1;
50
+ return undefined;
43
51
  }
44
52
  const body = result.body;
45
53
  if (!body.ok || typeof body.text !== "string") {
46
54
  const detail = body.reply ?? body.error ?? "Tower answered without a page.";
47
55
  writeFailure(command, io, body.error ?? "no_page", detail);
48
- return 1;
56
+ return undefined;
49
57
  }
58
+ return body;
59
+ }
60
+ function writeBriefReply(command, io, body) {
50
61
  if (command.json) {
51
62
  writeLine(io.stdout, JSON.stringify(body));
52
63
  }
53
64
  else {
54
65
  writeHuman(io, command, body);
55
66
  }
67
+ }
68
+ function logBriefRead(command, io, body, startedAt) {
56
69
  writeLine(io.stderr, `[brief cli] read ${JSON.stringify({
57
70
  text_length: body.text.length,
58
71
  cadence: body.page?.cadence ?? null,
@@ -70,7 +83,6 @@ export async function runBrief(command, io) {
70
83
  claim_count: body.claims?.length ?? null,
71
84
  elapsed_ms: Date.now() - startedAt,
72
85
  })}`);
73
- return 0;
74
86
  }
75
87
  /**
76
88
  * `cockpit brief status` (BLI-3462) — why a brief was or was not delivered.
@@ -157,15 +169,7 @@ function writeHuman(io, command, body) {
157
169
  // Decided once per page: colour only for a real terminal that has not said
158
170
  // NO_COLOR, so `cockpit brief > page.txt` is text and nothing else (BLI-3482).
159
171
  const styled = colorEnabled(io);
160
- const page = body.page ?? {};
161
- // Whose page, and — when it is not the newest — that it is a record of a
162
- // moment rather than something that failed to update.
163
- const whose = page.displayName ? `${page.displayName}'s page` : "This page";
164
- const pinned = page.olderVersionLabel ? ` · version from ${page.olderVersionLabel}` : "";
165
- // BLI-3484: which day, in the SUBJECT's zone, said with the zone beside it so
166
- // nobody has to work out whose midnight this is.
167
- const day = body.day?.date ? ` · ${body.day.date}${body.day.zone ? ` ${body.day.zone}` : ""}` : "";
168
- writeLine(io.stdout, dim(`${whose}${pinned}${day}`, styled));
172
+ writeBriefHeading(io, body, styled);
169
173
  // `history` lists days and prints no page — a list of dates under a full
170
174
  // brief would bury the thing somebody asked for.
171
175
  if (command.action === "history") {
@@ -176,6 +180,22 @@ function writeHuman(io, command, body) {
176
180
  writeLine(io.stdout, body.text ?? "");
177
181
  if (command.delta)
178
182
  writeDelta(io, body, styled);
183
+ writeBriefClaims(io, body, styled);
184
+ writeBriefVersions(io, body, styled);
185
+ writeOtherBriefPages(io, command, body, styled);
186
+ }
187
+ function writeBriefHeading(io, body, styled) {
188
+ const page = body.page ?? {};
189
+ // Whose page, and — when it is not the newest — that it is a record of a
190
+ // moment rather than something that failed to update.
191
+ const whose = page.displayName ? `${page.displayName}'s page` : "This page";
192
+ const pinned = page.olderVersionLabel ? ` · version from ${page.olderVersionLabel}` : "";
193
+ // BLI-3484: which day, in the SUBJECT's zone, said with the zone beside it so
194
+ // nobody has to work out whose midnight this is.
195
+ const day = body.day?.date ? ` · ${body.day.date}${body.day.zone ? ` ${body.day.zone}` : ""}` : "";
196
+ writeLine(io.stdout, dim(`${whose}${pinned}${day}`, styled));
197
+ }
198
+ function writeBriefClaims(io, body, styled) {
179
199
  if (body.claims && body.claims.length > 0) {
180
200
  writeLine(io.stdout, "");
181
201
  writeLine(io.stdout, dim("Claim ids — pass one to `cockpit correct --claim`:", styled));
@@ -183,6 +203,8 @@ function writeHuman(io, command, body) {
183
203
  writeLine(io.stdout, `${dim(` [${claim.claimId}]`, styled)} ${claim.text ?? ""}`);
184
204
  }
185
205
  }
206
+ }
207
+ function writeBriefVersions(io, body, styled) {
186
208
  if (body.versions) {
187
209
  writeLine(io.stdout, "");
188
210
  if (body.versions.length === 0) {
@@ -198,7 +220,10 @@ function writeHuman(io, command, body) {
198
220
  }
199
221
  }
200
222
  }
223
+ }
224
+ function writeOtherBriefPages(io, command, body, styled) {
201
225
  if (!command.subject && body.people && body.people.length > 1) {
226
+ const page = body.page ?? {};
202
227
  const others = body.people
203
228
  .filter((person) => person.personId !== page.personId)
204
229
  .map((person) => person.slug)