@bli-cockpit/cli 0.2.119 → 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 (38) 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-work.js +37 -6
  15. package/dist/commands/local-help-commands.js +2 -1
  16. package/dist/commands/mcp-stdio-probe.js +92 -73
  17. package/dist/commands/memory-hook-performance.js +135 -101
  18. package/dist/commands/memory-install-claude.js +15 -14
  19. package/dist/commands/memory-install-codex.js +10 -6
  20. package/dist/commands/memory-install-config.js +5 -4
  21. package/dist/commands/memory-install-contract.js +56 -10
  22. package/dist/commands/memory-install-report.js +16 -11
  23. package/dist/commands/memory-install-skills.js +11 -11
  24. package/dist/commands/memory-log.js +22 -5
  25. package/dist/commands/msg.js +11 -5
  26. package/dist/commands/onboard-setup.js +16 -1
  27. package/dist/commands/ops-sections.js +89 -0
  28. package/dist/commands/ops.js +117 -120
  29. package/dist/commands/public-root.js +1 -1
  30. package/dist/commands/scout.js +90 -68
  31. package/dist/commands/session-sync-failures.js +19 -13
  32. package/dist/commands/session-sync-record.js +53 -52
  33. package/dist/commands/session-sync-upload.js +15 -11
  34. package/dist/commands/sessions.js +61 -51
  35. package/dist/commands/slack.js +90 -61
  36. package/dist/commands/status.js +53 -41
  37. package/dist/commands/workbook.js +23 -20
  38. package/package.json +2 -2
@@ -37,11 +37,22 @@ export async function runCorrect(command, io) {
37
37
  writeFailure(command, io, "no_correction_text", 'Say what is wrong: `cockpit correct --claim <id> --text "..."`, or pipe it in on stdin.');
38
38
  return 1;
39
39
  }
40
+ const selection = await readBriefClaim(command, io, dashboardUrl, session.device_token, log);
41
+ if (!selection)
42
+ return 1;
43
+ const filed = await fileCorrection(command, io, dashboardUrl, session.device_token, log, selection, text);
44
+ if (!filed)
45
+ return 1;
46
+ printFiledCorrection(command, io, filed);
47
+ logFiledCorrection(command, io, filed, selection.claim, text, startedAt);
48
+ return 0;
49
+ }
50
+ async function readBriefClaim(command, io, dashboardUrl, deviceToken, log) {
40
51
  // Step one: which page, whose, and does that line exist.
41
52
  const read = await towerJsonRequest({
42
53
  dashboardUrl,
43
54
  path: `/api/jarvis/brief${briefQuery(command)}`,
44
- deviceToken: session.device_token,
55
+ deviceToken,
45
56
  fetch: io.fetch,
46
57
  method: "GET",
47
58
  label: "correct:brief",
@@ -50,62 +61,69 @@ export async function runCorrect(command, io) {
50
61
  });
51
62
  if (!read.ok) {
52
63
  writeFailure(command, io, read.reason, read.detail);
53
- return 1;
64
+ return null;
54
65
  }
55
66
  const brief = read.body;
56
67
  const personId = brief.page?.personId;
57
68
  if (!brief.ok || !personId) {
58
69
  writeFailure(command, io, brief.error ?? "no_page", brief.reply ?? "There is no page to correct.");
59
- return 1;
70
+ return null;
60
71
  }
61
72
  const claim = (brief.claims ?? []).find((candidate) => candidate.claimId === command.claimId);
62
73
  if (!claim) {
63
74
  writeFailure(command, io, "unknown_claim", `That page has no line called “${command.claimId}”. Run \`cockpit brief --claims\` to see the ids.`);
64
- return 1;
75
+ return null;
65
76
  }
77
+ return { brief, personId, claim };
78
+ }
79
+ async function fileCorrection(command, io, dashboardUrl, deviceToken, log, selection, text) {
66
80
  // Step two: the same door the panel's form posts to.
67
81
  const written = await towerJsonRequest({
68
82
  dashboardUrl,
69
83
  path: "/api/jarvis/corrections",
70
- deviceToken: session.device_token,
84
+ deviceToken,
71
85
  fetch: io.fetch,
72
86
  label: "correct",
73
87
  timeoutMs: REQUEST_DEADLINE_MS,
74
88
  log,
75
89
  body: {
76
- personId,
77
- pageId: brief.page?.pageId ?? null,
90
+ personId: selection.personId,
91
+ pageId: selection.brief.page?.pageId ?? null,
78
92
  claimId: command.claimId,
79
93
  // The line as it reads now, so the ledger records what was disputed and
80
94
  // the live check has the sentence to work from.
81
- quotedText: claim.text ?? null,
95
+ quotedText: selection.claim.text ?? null,
82
96
  correctionText: text,
83
- contextLinks: claim.links ?? [],
97
+ contextLinks: selection.claim.links ?? [],
84
98
  clauseOnPage: true,
85
- clauseIsObserved: Boolean(claim.observed),
99
+ clauseIsObserved: Boolean(selection.claim.observed),
86
100
  ...(command.supersedes ? { supersedes: command.supersedes } : {}),
87
101
  },
88
102
  });
89
103
  if (!written.ok) {
90
104
  writeFailure(command, io, written.reason, written.detail);
91
- return 1;
105
+ return null;
92
106
  }
93
107
  const filed = written.body;
94
108
  if (!filed.correctionId) {
95
109
  writeFailure(command, io, "not_filed", filed.error ?? "Tower did not record that correction.");
96
- return 1;
110
+ return null;
97
111
  }
112
+ return filed;
113
+ }
114
+ function printFiledCorrection(command, io, filed) {
98
115
  if (command.json) {
99
116
  writeLine(io.stdout, JSON.stringify({ ok: true, ...filed }));
117
+ return;
100
118
  }
101
- else {
102
- const styled = colorEnabled(io);
103
- writeLine(io.stdout, filed.reply ?? "Filed.");
104
- if (filed.finding)
105
- writeLine(io.stdout, dim(`The record says: ${filed.finding}`, styled));
106
- if (filed.link)
107
- writeLine(io.stdout, dim(filed.link, styled));
108
- }
119
+ const styled = colorEnabled(io);
120
+ writeLine(io.stdout, filed.reply ?? "Filed.");
121
+ if (filed.finding)
122
+ writeLine(io.stdout, dim(`The record says: ${filed.finding}`, styled));
123
+ if (filed.link)
124
+ writeLine(io.stdout, dim(filed.link, styled));
125
+ }
126
+ function logFiledCorrection(command, io, filed, claim, text, startedAt) {
109
127
  writeLine(io.stderr, `[correct cli] filed ${JSON.stringify({
110
128
  tier: filed.tier ?? null,
111
129
  outcome: filed.outcome ?? null,
@@ -117,7 +135,6 @@ export async function runCorrect(command, io) {
117
135
  text_length: text.length,
118
136
  elapsed_ms: Date.now() - startedAt,
119
137
  })}`);
120
- return 0;
121
138
  }
122
139
  function briefQuery(command) {
123
140
  const params = new URLSearchParams({ claims: "1" });
@@ -225,8 +225,19 @@ async function updateDoc(command, door) {
225
225
  && !command.clearParent) {
226
226
  return failAgentDoor(door, TAG, "invalid_body", "docs update needs at least one of --title, --visibility, --parent/--clear-parent, or a body on --body-stdin/--file.");
227
227
  }
228
- const answer = await askAgentDoor(door, {
229
- path: `/api/docs/documents/${encodeURIComponent(resolved.id)}`,
228
+ const answer = await updateDocument(command, door, resolved.id, bodyMarkdown);
229
+ if (!answer.ok)
230
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
231
+ const document = answer.body.document ?? {};
232
+ writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
233
+ if (door.json)
234
+ return emitAgentDoor(door, { ok: true, document });
235
+ writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
236
+ return 0;
237
+ }
238
+ async function updateDocument(command, door, documentId, bodyMarkdown) {
239
+ return askAgentDoor(door, {
240
+ path: `/api/docs/documents/${encodeURIComponent(documentId)}`,
230
241
  method: "PATCH",
231
242
  label: "docs update",
232
243
  timeoutMs: WRITE_DEADLINE_MS,
@@ -242,12 +253,4 @@ async function updateDoc(command, door) {
242
253
  ...(command.allowEmpty ? { allow_empty: true } : {}),
243
254
  },
244
255
  });
245
- if (!answer.ok)
246
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
247
- const document = answer.body.document ?? {};
248
- writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
249
- if (door.json)
250
- return emitAgentDoor(door, { ok: true, document });
251
- writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
252
- return 0;
253
256
  }
@@ -105,14 +105,32 @@ export async function editInEditor(options) {
105
105
  const platform = options.platform ?? process.platform;
106
106
  const log = options.log ?? ((line) => console.error(line));
107
107
  const chosen = resolveEditorCommand(options.env, platform);
108
- if (!chosen) {
109
- return {
110
- ok: false,
111
- reason: "no_editor_configured",
112
- detail: "No editor is configured. Set EDITOR (or VISUAL) — for example `export EDITOR=nano` — " +
113
- "or pipe the edited page in on stdin instead.",
114
- };
108
+ if (!chosen)
109
+ return noEditorConfigured();
110
+ const unavailable = invalidWindowsEditorReason(chosen, platform);
111
+ if (unavailable)
112
+ return unavailable;
113
+ const scratch = await writeEditorScratchFile(options);
114
+ if (!scratch.ok)
115
+ return scratch;
116
+ try {
117
+ return await runEditorSession(options, chosen, scratch.file, log);
115
118
  }
119
+ finally {
120
+ // Whatever happened. The page is not a secret, but it is somebody's writing
121
+ // and it does not belong in /tmp after the command returns.
122
+ await removeEditorScratchDirectory(scratch.directory);
123
+ }
124
+ }
125
+ function noEditorConfigured() {
126
+ return {
127
+ ok: false,
128
+ reason: "no_editor_configured",
129
+ detail: "No editor is configured. Set EDITOR (or VISUAL) — for example `export EDITOR=nano` — " +
130
+ "or pipe the edited page in on stdin instead.",
131
+ };
132
+ }
133
+ function invalidWindowsEditorReason(chosen, platform) {
116
134
  // A `.cmd` or `.bat` is a batch script; Node cannot execute one without
117
135
  // handing the whole line to cmd.exe, and that is the one thing this module
118
136
  // will not do with a person's own path in it.
@@ -125,12 +143,14 @@ export async function editInEditor(options) {
125
143
  "notepad, or the full path to Code.exe.",
126
144
  };
127
145
  }
128
- let directory;
129
- let file;
146
+ return null;
147
+ }
148
+ async function writeEditorScratchFile(options) {
130
149
  try {
131
- directory = await mkdtemp(join(tmpdir(), "cockpit-brief-"));
132
- file = join(directory, `brief-${randomBytes(4).toString("hex")}${options.suffix ?? ".md"}`);
150
+ const directory = await mkdtemp(join(tmpdir(), "cockpit-brief-"));
151
+ const file = join(directory, `brief-${randomBytes(4).toString("hex")}${options.suffix ?? ".md"}`);
133
152
  await writeFile(file, options.contents, "utf8");
153
+ return { ok: true, directory, file };
134
154
  }
135
155
  catch (error) {
136
156
  return {
@@ -139,13 +159,11 @@ export async function editInEditor(options) {
139
159
  detail: `The page could not be written to a scratch file (${messageOf(error)}).`,
140
160
  };
141
161
  }
162
+ }
163
+ async function runEditorSession(options, chosen, file, log) {
142
164
  try {
143
165
  const spawn = options.spawn ?? spawnSync;
144
- log(`[brief edit] editor opening ${JSON.stringify({
145
- program: basenameOf(chosen.program),
146
- extra_args: chosen.args.length,
147
- bytes: Buffer.byteLength(options.contents, "utf8"),
148
- })}`);
166
+ reportEditorOpening(log, chosen, options.contents);
149
167
  // `stdio: "inherit"` hands the terminal over: a full-screen editor needs the
150
168
  // real tty. `shell: false` is the default and is stated to make the rule
151
169
  // above impossible to lose in a refactor.
@@ -170,12 +188,8 @@ export async function editInEditor(options) {
170
188
  "Nothing has been sent to Tower.",
171
189
  };
172
190
  }
173
- const text = stripLeadingBom(await readFile(file, "utf8"));
174
- log(`[brief edit] editor closed ${JSON.stringify({
175
- program: basenameOf(chosen.program),
176
- status: result.status ?? null,
177
- bytes: Buffer.byteLength(text, "utf8"),
178
- })}`);
191
+ const text = await readEditedText(file);
192
+ reportEditorClosed(log, chosen, result.status, text);
179
193
  return { ok: true, text, program: basenameOf(chosen.program) };
180
194
  }
181
195
  catch (error) {
@@ -185,14 +199,29 @@ export async function editInEditor(options) {
185
199
  detail: `The edited page could not be read back (${messageOf(error)}).`,
186
200
  };
187
201
  }
188
- finally {
189
- // Whatever happened. The page is not a secret, but it is somebody's writing
190
- // and it does not belong in /tmp after the command returns.
191
- await rm(directory, { recursive: true, force: true }).catch(() => {
192
- // Deliberately silent: the temp directory is the OS's to reap, and a
193
- // failure to remove it must not turn a saved edit into a failed command.
194
- });
195
- }
202
+ }
203
+ function reportEditorOpening(log, chosen, contents) {
204
+ log(`[brief edit] editor opening ${JSON.stringify({
205
+ program: basenameOf(chosen.program),
206
+ extra_args: chosen.args.length,
207
+ bytes: Buffer.byteLength(contents, "utf8"),
208
+ })}`);
209
+ }
210
+ async function readEditedText(file) {
211
+ return stripLeadingBom(await readFile(file, "utf8"));
212
+ }
213
+ function reportEditorClosed(log, chosen, status, text) {
214
+ log(`[brief edit] editor closed ${JSON.stringify({
215
+ program: basenameOf(chosen.program),
216
+ status,
217
+ bytes: Buffer.byteLength(text, "utf8"),
218
+ })}`);
219
+ }
220
+ async function removeEditorScratchDirectory(directory) {
221
+ await rm(directory, { recursive: true, force: true }).catch(() => {
222
+ // Deliberately silent: the temp directory is the OS's to reap, and a
223
+ // failure to remove it must not turn a saved edit into a failed command.
224
+ });
196
225
  }
197
226
  /** The program's own name, never the directories around it. */
198
227
  function basenameOf(program) {
@@ -48,6 +48,17 @@ export function sanitizeInstallErrorCode(value) {
48
48
  export async function reportInstallEventsBestEffort(options) {
49
49
  if (options.events.length === 0)
50
50
  return null;
51
+ if (shouldWithholdDevBuildReceipts(options))
52
+ return null;
53
+ const paths = getCollectorRuntimePaths(options.homeDir);
54
+ if (!(await queueInstallEvents(options, paths)))
55
+ return null;
56
+ const session = await readLocalCollectorSessionFile(paths).catch(() => null);
57
+ if (!hasValidInstallEventSession(session))
58
+ return null;
59
+ return deliverPendingInstallEvents(options, paths, session.device_token);
60
+ }
61
+ function shouldWithholdDevBuildReceipts(options) {
51
62
  // BLI-3554. Withheld BEFORE the outbox, not before the POST: an entry queued
52
63
  // by a checkout survives in `~/.cockpit` and the next real scheduled tick
53
64
  // would deliver it under the workspace version, which is exactly how 47
@@ -65,32 +76,18 @@ export async function reportInstallEventsBestEffort(options) {
65
76
  cli_version: LOCAL_COLLECTOR_VERSION,
66
77
  fix: "set COCKPIT_DEV=0 to post receipts from a checkout deliberately",
67
78
  }));
68
- return null;
79
+ return true;
69
80
  }
70
- const paths = getCollectorRuntimePaths(options.homeDir);
81
+ return suppression.suppressed;
82
+ }
83
+ async function queueInstallEvents(options, paths) {
71
84
  try {
72
85
  await enqueueInstallEventEntry(paths, {
73
86
  dashboardUrl: options.dashboardUrl,
74
87
  cliVersion: LOCAL_COLLECTOR_VERSION,
75
88
  command: options.command,
76
89
  osPlatform: os.platform(),
77
- events: options.events.map((event) => ({
78
- step: event.step.trim().slice(0, 120),
79
- status: event.status,
80
- ...(event.error_code
81
- ? { error_code: sanitizeInstallErrorCode(event.error_code) }
82
- : {}),
83
- // Already redacted and capped at the point it was produced; bounded
84
- // again here because this mapping is what the server contract sees.
85
- ...(event.error_detail
86
- ? {
87
- error_detail: event.error_detail
88
- .trim()
89
- .slice(0, SYNC_ERROR_DETAIL_MAX_CHARS),
90
- }
91
- : {}),
92
- ...(event.at ? { at: event.at } : {}),
93
- })),
90
+ events: options.events.map(normalizeInstallEvent),
94
91
  });
95
92
  }
96
93
  catch (error) {
@@ -107,90 +104,44 @@ export async function reportInstallEventsBestEffort(options) {
107
104
  if (options.json) {
108
105
  writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
109
106
  }
110
- return null;
107
+ return false;
111
108
  }
112
- const session = await readLocalCollectorSessionFile(paths).catch(() => null);
109
+ return true;
110
+ }
111
+ function normalizeInstallEvent(event) {
112
+ // Already redacted and capped at the point it was produced; bounded
113
+ // again here because this mapping is what the server contract sees.
114
+ const detail = event.error_detail?.trim().slice(0, SYNC_ERROR_DETAIL_MAX_CHARS);
115
+ const code = event.error_code ? sanitizeInstallErrorCode(event.error_code) : undefined;
116
+ return {
117
+ step: event.step.trim().slice(0, 120),
118
+ status: event.status,
119
+ ...(code ? { error_code: code } : {}),
120
+ ...(detail ? { error_detail: detail } : {}),
121
+ ...(event.at ? { at: event.at } : {}),
122
+ };
123
+ }
124
+ function hasValidInstallEventSession(session) {
113
125
  if (!session ||
114
126
  session.session_state !== "valid" ||
115
127
  typeof session.device_token !== "string" ||
116
128
  !session.device_token) {
117
- return null;
129
+ return false;
118
130
  }
131
+ return true;
132
+ }
133
+ async function deliverPendingInstallEvents(options, paths, deviceToken) {
119
134
  const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
120
135
  const failures = [];
121
136
  let delivered = 0;
122
137
  let observedMinCliVersion = null;
123
138
  for (let offset = 0; offset < pending.length; offset += 5) {
124
- await Promise.all(pending.slice(offset, offset + 5).map(async (entry) => {
125
- const controller = new AbortController();
126
- const timeout = setTimeout(() => controller.abort(), 5_000);
127
- try {
128
- const response = await options.io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- Authorization: `Bearer ${session.device_token}`,
133
- },
134
- body: JSON.stringify({
135
- cli_version: entry.cli_version,
136
- command: entry.command,
137
- os_platform: entry.os_platform,
138
- events: entry.events,
139
- }),
140
- signal: controller.signal,
141
- });
142
- if (!response.ok) {
143
- throw new Error(`http_${response.status}`);
144
- }
145
- const receipt = (await response.json().catch((error) => {
146
- // This reply carries the server-published `min_cli_version` floor
147
- // (BLI-2678). A body that will not parse means the floor is not
148
- // observed on this tick and the forced-update path silently does
149
- // nothing — while the 2xx above says the receipt landed fine.
150
- console.error("[install-receipts] receipt body unreadable; no min_cli_version observed", JSON.stringify({
151
- reason: "receipt_body_unreadable",
152
- http_status: response.status,
153
- ...describeError(error),
154
- }));
155
- return null;
156
- }));
157
- if (typeof receipt?.min_cli_version === "string" &&
158
- receipt.min_cli_version.trim()) {
159
- observedMinCliVersion = receipt.min_cli_version.trim();
160
- }
161
- await removeInstallEventEntry(paths, entry.outbox_id);
139
+ await Promise.all(pending.slice(offset, offset + 5).map((entry) => deliverInstallEventEntry(options.io, paths, entry, deviceToken, failures).then((floor) => {
140
+ if (floor != null)
141
+ observedMinCliVersion = floor;
142
+ if (floor !== undefined)
162
143
  delivered += 1;
163
- }
164
- catch (error) {
165
- const failureReason = classifyInstallTelemetryError(error);
166
- failures.push(failureReason);
167
- // The classified reason is the coarse bucket the outbox row keeps;
168
- // beside it, what actually happened. `network_error` covers DNS,
169
- // TLS, timeout and abort, and only one of those is worth waking up
170
- // for (BLI-3238).
171
- console.error("[install-receipts] install event delivery failed, entry kept for retry", JSON.stringify({
172
- reason: failureReason,
173
- outbox_id: entry.outbox_id,
174
- ...describeError(error),
175
- }));
176
- await recordInstallEventAttemptFailure(paths, entry, {
177
- attemptedAt: new Date().toISOString(),
178
- failureReason,
179
- }).catch((writeError) => {
180
- // Double failure: delivery failed AND the retry bookkeeping did.
181
- // The entry stays queued, so nothing is lost, but the attempt
182
- // count stops advancing and the outbox looks stuck for no reason.
183
- console.error("[install-receipts] could not record the delivery failure against the entry", JSON.stringify({
184
- reason: "attempt_bookkeeping_failed",
185
- outbox_id: entry.outbox_id,
186
- ...describeError(writeError),
187
- }));
188
- });
189
- }
190
- finally {
191
- clearTimeout(timeout);
192
- }
193
- }));
144
+ })));
194
145
  }
195
146
  if (delivered > 0) {
196
147
  // The success branch says so too (BLI-3554 / the logging contract): a log
@@ -209,6 +160,70 @@ export async function reportInstallEventsBestEffort(options) {
209
160
  }
210
161
  return observedMinCliVersion;
211
162
  }
163
+ async function deliverInstallEventEntry(io, paths, entry, deviceToken, failures) {
164
+ const controller = new AbortController();
165
+ const timeout = setTimeout(() => controller.abort(), 5_000);
166
+ try {
167
+ const response = await postInstallEventEntry(io, entry, deviceToken, controller.signal);
168
+ const floor = await readObservedMinCliVersion(response);
169
+ await removeInstallEventEntry(paths, entry.outbox_id);
170
+ return floor;
171
+ }
172
+ catch (error) {
173
+ await keepFailedInstallEventEntry(paths, entry, error, failures);
174
+ return undefined;
175
+ }
176
+ finally {
177
+ clearTimeout(timeout);
178
+ }
179
+ }
180
+ async function postInstallEventEntry(io, entry, deviceToken, signal) {
181
+ const response = await io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}` },
184
+ body: JSON.stringify({
185
+ cli_version: entry.cli_version,
186
+ command: entry.command,
187
+ os_platform: entry.os_platform,
188
+ events: entry.events,
189
+ }),
190
+ signal,
191
+ });
192
+ if (!response.ok)
193
+ throw new Error(`http_${response.status}`);
194
+ return response;
195
+ }
196
+ async function readObservedMinCliVersion(response) {
197
+ const receipt = (await response.json().catch((error) => {
198
+ // This reply carries the server-published `min_cli_version` floor
199
+ // (BLI-2678). A body that will not parse means the floor is not
200
+ // observed on this tick and the forced-update path silently does
201
+ // nothing — while the 2xx above says the receipt landed fine.
202
+ console.error("[install-receipts] receipt body unreadable; no min_cli_version observed", JSON.stringify({ reason: "receipt_body_unreadable", http_status: response.status, ...describeError(error) }));
203
+ return null;
204
+ }));
205
+ return typeof receipt?.min_cli_version === "string" && receipt.min_cli_version.trim()
206
+ ? receipt.min_cli_version.trim()
207
+ : null;
208
+ }
209
+ async function keepFailedInstallEventEntry(paths, entry, error, failures) {
210
+ const failureReason = classifyInstallTelemetryError(error);
211
+ failures.push(failureReason);
212
+ // The classified reason is the coarse bucket the outbox row keeps;
213
+ // beside it, what actually happened. `network_error` covers DNS,
214
+ // TLS, timeout and abort, and only one of those is worth waking up
215
+ // for (BLI-3238).
216
+ console.error("[install-receipts] install event delivery failed, entry kept for retry", JSON.stringify({ reason: failureReason, outbox_id: entry.outbox_id, ...describeError(error) }));
217
+ await recordInstallEventAttemptFailure(paths, entry, {
218
+ attemptedAt: new Date().toISOString(),
219
+ failureReason,
220
+ }).catch((writeError) => {
221
+ // Double failure: delivery failed AND the retry bookkeeping did.
222
+ // The entry stays queued, so nothing is lost, but the attempt
223
+ // count stops advancing and the outbox looks stuck for no reason.
224
+ console.error("[install-receipts] could not record the delivery failure against the entry", JSON.stringify({ reason: "attempt_bookkeeping_failed", outbox_id: entry.outbox_id, ...describeError(writeError) }));
225
+ });
226
+ }
212
227
  function classifyInstallTelemetryError(error) {
213
228
  if (error instanceof Error && error.name === "AbortError") {
214
229
  return "timeout";
@@ -89,6 +89,8 @@ export function parseOpsArgs(args) {
89
89
  // BLI-3912: one line per model — latency, tokens/s, today's spend, last
90
90
  // probe verdict. A SECOND door like --memory, asked for only when named.
91
91
  "--models",
92
+ "--turns",
93
+ "--tool-router",
92
94
  "--person",
93
95
  "--dry-run",
94
96
  "--json",
@@ -130,6 +132,8 @@ export function parseOpsArgs(args) {
130
132
  skips: values.booleans.has("--skips"),
131
133
  memory: values.booleans.has("--memory") || memoryDays !== undefined,
132
134
  models: values.booleans.has("--models"),
135
+ turns: values.booleans.has("--turns"),
136
+ toolRouter: values.booleans.has("--tool-router"),
133
137
  ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
134
138
  ...base,
135
139
  };
@@ -77,12 +77,25 @@ export function parseCalArgs(args) {
77
77
  "--attendee",
78
78
  ],
79
79
  });
80
+ const { action, rest } = readCalAction(values);
81
+ const { subject, query } = readCalSubjectOrQuery(action, rest);
82
+ const { title, startsAt, endsAt } = readCalCreateDetails(action, values);
83
+ validateCalSharing(action, values);
84
+ // Read in the order the flags have always been checked, so a caller who got
85
+ // two of them wrong is told about --limit first, as before.
86
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
87
+ const offset = readCalOffset(values);
88
+ return buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit);
89
+ }
90
+ function readCalAction(values) {
80
91
  const first = values.positionals[0];
81
92
  const action = (first === undefined ? "today" : first);
82
93
  if (!CAL_ACTIONS.has(action)) {
83
94
  throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, or sync.`);
84
95
  }
85
- const rest = values.positionals.slice(first === undefined ? 0 : 1);
96
+ return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
97
+ }
98
+ function readCalSubjectOrQuery(action, rest) {
86
99
  let subject;
87
100
  let query;
88
101
  if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
@@ -100,6 +113,9 @@ export function parseCalArgs(args) {
100
113
  else if (rest.length > 0) {
101
114
  throw new Error(`cal ${action} does not take "${rest[0]}".`);
102
115
  }
116
+ return { subject, query };
117
+ }
118
+ function readCalCreateDetails(action, values) {
103
119
  const title = optionalNonEmpty(values.flags.get("--title"));
104
120
  const startsAt = optionalNonEmpty(values.flags.get("--at"));
105
121
  const endsAt = optionalNonEmpty(values.flags.get("--until"));
@@ -114,15 +130,24 @@ export function parseCalArgs(args) {
114
130
  if (!endsAt)
115
131
  throw new Error("cal create needs --until <when it ends>.");
116
132
  }
117
- if (action === "share" && !values.booleans.has("--org-visible") && !values.booleans.has("--private")) {
133
+ return { title, startsAt, endsAt };
134
+ }
135
+ function validateCalSharing(action, values) {
136
+ if (action === "share" &&
137
+ !values.booleans.has("--org-visible") &&
138
+ !values.booleans.has("--private")) {
118
139
  throw new Error("cal share needs --org-visible (every member may read it) or --private (only you). Sharing is a decision, not a default.");
119
140
  }
120
- const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
141
+ }
142
+ function readCalOffset(values) {
121
143
  const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
122
144
  const offset = offsetRaw === undefined ? undefined : Number.parseInt(offsetRaw, 10);
123
145
  if (offset !== undefined && !Number.isFinite(offset)) {
124
146
  throw new Error("--offset takes a whole number of days (today/next) or weeks (week).");
125
147
  }
148
+ return offset;
149
+ }
150
+ function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
126
151
  return {
127
152
  kind: "cal",
128
153
  action,