@bli-cockpit/cli 0.2.164 → 0.2.166

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.
@@ -91,6 +91,8 @@ export function parseOpsArgs(args) {
91
91
  "--models",
92
92
  "--turns",
93
93
  "--tool-router",
94
+ // BLI-12507: the MCP's `people: true`. Per-device and per-person rows in --json.
95
+ "--people",
94
96
  "--person",
95
97
  "--dry-run",
96
98
  "--json",
@@ -134,6 +136,7 @@ export function parseOpsArgs(args) {
134
136
  models: values.booleans.has("--models"),
135
137
  turns: values.booleans.has("--turns"),
136
138
  toolRouter: values.booleans.has("--tool-router"),
139
+ people: values.booleans.has("--people"),
137
140
  ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
138
141
  ...base,
139
142
  };
@@ -14,9 +14,14 @@
14
14
  * captured by this very product's own session harvester. That is the
15
15
  * sourcing-secrets verdict applied to the one credential this product
16
16
  * stores.
17
- * - **The message body.** `cockpit mail send` takes it from stdin or
18
- * `--file`, the same discipline `cockpit issue comment` and `cockpit docs
19
- * update` keep.
17
+ * - **The message body.** `cockpit mail send` and `cockpit mail draft` take it
18
+ * from stdin or `--file`, the same discipline `cockpit issue comment` and
19
+ * `cockpit docs update` keep.
20
+ *
21
+ * `draft` (BLI-12530) takes `send`'s flags exactly — `--account`, `--to`,
22
+ * `--cc`, `--subject`, `--reply-to`, `--file` — and is refused the same two
23
+ * ways when `--account` or `--to` is missing: which mailbox a message is saved
24
+ * in is as much the caller's to say as which one it goes out from.
20
25
  */
21
26
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
22
27
  const MAIL_ACTIONS = new Set([
@@ -27,10 +32,13 @@ const MAIL_ACTIONS = new Set([
27
32
  "read",
28
33
  "search",
29
34
  "send",
35
+ "draft",
30
36
  "attachment",
31
37
  "sync",
32
38
  "sync-status",
33
39
  ]);
40
+ /** The two verbs that put a message in a mailbox: one sends it, one saves it unsent. */
41
+ const MAIL_ACTIONS_COMPOSING = new Set(["send", "draft"]);
34
42
  /** Every verb that names ONE thing first: a thread, an attachment, a mailbox. */
35
43
  const MAIL_ACTIONS_NEEDING_A_SUBJECT = new Set(["read", "attachment", "detach", "sync"]);
36
44
  /** `sync-status [<account id>]` — every mailbox, or one's. Ticket 10730. */
@@ -85,7 +93,7 @@ function readMailAction(values) {
85
93
  const first = values.positionals[0];
86
94
  const action = (first === undefined ? "inbox" : first);
87
95
  if (!MAIL_ACTIONS.has(action)) {
88
- throw new Error(`Unknown mail command: ${first}. Try accounts, add-imap, detach, inbox, read, search, send, attachment, sync, or sync-status.`);
96
+ throw new Error(`Unknown mail command: ${first}. Try accounts, add-imap, detach, inbox, read, search, send, draft, attachment, sync, or sync-status.`);
89
97
  }
90
98
  return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
91
99
  }
@@ -127,11 +135,13 @@ function readImapAddress(action, values) {
127
135
  function readMailRecipients(action, values) {
128
136
  const to = splitAddresses(values.flags.get("--to"));
129
137
  const cc = splitAddresses(values.flags.get("--cc"));
130
- if (action === "send" && to.length === 0) {
131
- throw new Error('mail send needs --to "someone@example.com" (comma-separated for several).');
138
+ if (MAIL_ACTIONS_COMPOSING.has(action) && to.length === 0) {
139
+ throw new Error(`mail ${action} needs --to "someone@example.com" (comma-separated for several).`);
132
140
  }
133
- if (action === "send" && !optionalNonEmpty(values.flags.get("--account"))) {
134
- throw new Error("mail send needs --account <id>: with several mailboxes attached, which address this goes out from is yours to say. `cockpit mail accounts` lists them.");
141
+ if (MAIL_ACTIONS_COMPOSING.has(action) && !optionalNonEmpty(values.flags.get("--account"))) {
142
+ throw new Error(action === "draft"
143
+ ? "mail draft needs --account <id>: with several mailboxes attached, which one the draft is saved in is yours to say. `cockpit mail accounts` lists them."
144
+ : "mail send needs --account <id>: with several mailboxes attached, which address this goes out from is yours to say. `cockpit mail accounts` lists them.");
135
145
  }
136
146
  return { to, cc };
137
147
  }
@@ -115,13 +115,15 @@ export const TOWER_OPS_COMMAND_HELP = [
115
115
  [
116
116
  "ops",
117
117
  [
118
- "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
118
+ "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] [--people] | recompile --person <email|name|id> [--dry-run]] [--json]",
119
119
  "",
120
120
  " cockpit ops status",
121
121
  " One line per scheduled job: when it last produced something, and whether that",
122
122
  " is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
123
123
  " quarter-hourly one does not. Every line names the schedule it is judged against.",
124
124
  " --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
125
+ " --json carries the fleet and coverage SUMMARIES only (about 150 KB lighter);",
126
+ " --people --json adds every per-device and per-person row. The text board always prints them.",
125
127
  " A job that is not ok also prints what its artifact does and does not prove —",
126
128
  " several only write a row when there is something new, so quiet can mean a quiet",
127
129
  " week rather than a broken cron.",
@@ -122,7 +122,7 @@ export const TOWER_COMMAND_HELP = [
122
122
  [
123
123
  "mail",
124
124
  [
125
- "Usage: cockpit mail [accounts|add-imap|inbox|read <thread>|search \"<words>\"|send|attachment <id>|sync <account>|sync-status [<account>]|detach <account>] [flags]",
125
+ "Usage: cockpit mail [accounts|add-imap|inbox|read <thread>|search \"<words>\"|send|draft|attachment <id>|sync <account>|sync-status [<account>]|detach <account>] [flags]",
126
126
  "",
127
127
  "Every mailbox you attached, in one place. One person, several addresses — nothing here has a \"current\" mailbox.",
128
128
  "accounts — every mailbox, its provider (gmail_oauth or imap), its health and when it last synced. The ids other verbs take are on the last line.",
@@ -134,12 +134,13 @@ export const TOWER_COMMAND_HELP = [
134
134
  "read <thread> — one conversation with its bodies. Thread ids come from `cockpit mail inbox` or `cockpit mail search`, in either --json or plain text.",
135
135
  "search \"<words>\" [--account <id>] [--limit <n>] — full text over subject and body. Quotes and OR work the way they do in a search box. Every row (text or --json) carries the thread id.",
136
136
  "send --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] — the BODY comes from --file or stdin. --account is required: which address this goes out from is yours to say.",
137
+ "draft --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] — saves the message as a DRAFT in that mailbox for a person to read, edit and send themselves. NOTHING IS SENT and nobody is notified, so there is no confirmation step. Same flags and same body rule as send (--file or stdin). Only the mailbox's owner can draft into it. --reply-to threads the draft into that conversation the way send does; Gmail files it there only when the subject matches the original's, and the answer says whether it did. Gmail mailboxes only: an IMAP mailbox is refused as imap_drafts_unsupported. --json answers { ok, draft: { account_id, address, provider_draft_id, provider_thread_id, threaded } } (BLI-12530).",
137
138
  "attachment <id> --out <path> — downloads one attachment to a file. Attachments are pointers until you ask; nothing is stored in Tower.",
138
139
  "sync <account> — reads that mailbox now instead of waiting for the cron. Prints what landed, or the reason it did not.",
139
140
  "sync-status [<account>] — every mailbox's health, or one's: last successful sync, the last pass's own reason, how many messages it carried, failures in the last 24h, and the lag between a message's own clock and when Tower stored it (ticket 10730).",
140
141
  "detach <account> — removes the mailbox, its stored mail and its credential.",
141
142
  "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr. Shapes differ by verb: inbox/read answer { messages: [...] }, search answers { matches: [...] } — same MessageRow shape either name, always carrying thread_id.",
142
- "A refusal keeps Tower's own reason label — needs_rls_client, account_not_found_or_unreadable, account_not_active, google_oauth_not_configured, credential_unavailable, and so on.",
143
+ "A refusal keeps Tower's own reason label — needs_rls_client, account_not_found_or_unreadable, account_not_active, account_not_yours, google_oauth_not_configured, credential_unavailable, and so on.",
143
144
  "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
144
145
  ],
145
146
  ],
@@ -124,7 +124,7 @@ export function localCommandHelp(command) {
124
124
  " 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]",
125
125
  " 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]",
126
126
  " 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] [--size S|M|L] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
127
- " 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]",
127
+ " cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|draft --account <id> --to <a> --subject <s> [--reply-to <message id>]|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--dashboard-url <url>] [--json]",
128
128
  " cockpit cal [today|week|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
129
129
  " cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
130
130
  " cockpit github <pr|merge|retitle|rerun> <owner/repo> <n> [--claim <state>] [--method squash|merge|rebase] [--title \"<text>\"] [--run <id>] [--confirm <code>] [--dashboard-url <url>] [--json]",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The compose half of `cockpit mail` (BLI-12530): `draft`, and the two things
3
+ * it shares with `send` — where a message body comes from, and the JSON both
4
+ * verbs post.
5
+ *
6
+ * `cockpit mail draft` saves a message as a DRAFT in one of your own mailboxes
7
+ * (`POST /api/mail/drafts`) so a person can read it, edit it and press send
8
+ * themselves. It is the verb for an agent that prepares mail on somebody's
9
+ * behalf: nothing is sent, nobody is notified, and so — unlike `send` — there
10
+ * is nothing to confirm.
11
+ *
12
+ * Kept beside `mail.ts` rather than in it because `mail.ts` sits at the edge of
13
+ * the readability ceiling, and because the two verbs genuinely share one shape:
14
+ * the request body is built once here, so a field added to `send` cannot be
15
+ * forgotten by `draft` (both doors accept exactly the same fields).
16
+ *
17
+ * THE BODY NEVER TOUCHES ARGV. It comes from `--file` or stdin, the discipline
18
+ * `send`, `issue comment` and `docs update` already keep — argv is world-
19
+ * readable on a shared machine and is captured by this product's own session
20
+ * harvester.
21
+ */
22
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, } from "./agent-door.js";
23
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
24
+ import { readNoteFile } from "./notes-file.js";
25
+ // One spelling per surface: the same tag `mail.ts` uses.
26
+ const TAG = "[mail cli]";
27
+ const WRITE_DEADLINE_MS = 60_000;
28
+ const BODY_MAX_CHARS = 200_000;
29
+ /** Body on stdin, never argv — `--file` is the safest way on Windows. */
30
+ export async function readMailBody(command, door) {
31
+ if (command.filePath) {
32
+ const read = await readNoteFile(command.filePath);
33
+ if (!read.ok) {
34
+ return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
35
+ }
36
+ return { ok: true, text: read.bytes.toString("utf8") };
37
+ }
38
+ if (isInteractiveStdin(door.io))
39
+ return { ok: true, text: "" };
40
+ try {
41
+ const text = await readPipedText(door.io.stdin, {
42
+ maxChars: BODY_MAX_CHARS,
43
+ overflowMessage: `A message body is limited to ${BODY_MAX_CHARS} characters here.`,
44
+ });
45
+ return { ok: true, text };
46
+ }
47
+ catch (error) {
48
+ return { ok: false, reason: "body_too_long", detail: error instanceof Error ? error.message : String(error) };
49
+ }
50
+ }
51
+ /**
52
+ * The JSON that `send` and `draft` post. One shape on purpose: the two doors
53
+ * take the same fields, and a message that can be sent can be saved unsent.
54
+ */
55
+ export function composedMessageBody(command, text) {
56
+ return {
57
+ account_id: command.accountId,
58
+ to: command.to.map((address) => ({ address })),
59
+ ...(command.cc.length > 0 ? { cc: command.cc.map((address) => ({ address })) } : {}),
60
+ subject: command.subjectLine ?? "",
61
+ text,
62
+ ...(command.replyTo ? { in_reply_to_message_id: command.replyTo } : {}),
63
+ };
64
+ }
65
+ /** `draft --account <id> --to <a[,b]> [--cc <c>] --subject "<s>" [--reply-to <message id>] [--file <path>]` */
66
+ export async function runMailDraft(command, door) {
67
+ const body = await readMailBody(command, door);
68
+ if (!body.ok)
69
+ return failAgentDoor(door, TAG, body.reason, body.detail);
70
+ if (body.text.trim() === "") {
71
+ return failAgentDoor(door, TAG, "body_required", 'A draft needs a body: `echo "Tuesday works for me." | cockpit mail draft --account <id> --to someone@example.com --subject "Re: lunch"`.');
72
+ }
73
+ const answer = await askAgentDoor(door, {
74
+ path: "/api/mail/drafts",
75
+ method: "POST",
76
+ label: "mail draft",
77
+ timeoutMs: WRITE_DEADLINE_MS,
78
+ body: composedMessageBody(command, body.text),
79
+ });
80
+ if (!answer.ok)
81
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
82
+ const draft = answer.body.draft;
83
+ if (door.json)
84
+ return emitAgentDoor(door, { ok: true, draft });
85
+ writeLine(door.io.stdout, `Saved a draft in ${draft?.address ?? "that mailbox"} for ${command.to.join(", ")}. `
86
+ + "Nothing was sent: it is in Drafts for a person to read, edit and send.");
87
+ if (command.replyTo) {
88
+ // `threaded` is what Gmail answered, not what was asked: a reply is only
89
+ // filed in its conversation when the subject still matches the original's.
90
+ writeLine(door.io.stdout, draft?.threaded
91
+ ? "It sits in the reply's conversation."
92
+ : "Gmail did not file it in that conversation, so it is a new one. A reply joins its thread only when the subject matches the original's.");
93
+ }
94
+ writeLine(door.io.stdout, `draft ${draft?.provider_draft_id ?? "(no id returned)"} thread ${draft?.provider_thread_id ?? "(none)"} account ${draft?.account_id ?? command.accountId}`);
95
+ return 0;
96
+ }
@@ -2,10 +2,12 @@
2
2
  * `cockpit mail` — the mailboxes a person attached, from a terminal
3
3
  * (BLI-3708).
4
4
  *
5
- * Nine verbs over `/api/mail/**`, the same doors the browser will use when the
5
+ * Ten verbs over `/api/mail/**`, the same doors the browser will use when the
6
6
  * UI lands. Every one of those routes already accepts the collector device
7
7
  * token (`resolveCaller({ allowDeviceToken: true })`), so this command is a
8
8
  * terminal in front of an existing door and adds no authority of its own.
9
+ * `draft` (BLI-12530) lives in `mail-compose.ts` beside the body reader it
10
+ * shares with `send`.
9
11
  *
10
12
  * The CLI is the FIRST surface for this wave, deliberately (AGENTS.md, "CLI is
11
13
  * king"): a mailbox is easier to reason about as `--json` than as a rendered
@@ -23,7 +25,7 @@
23
25
  import { writeFile } from "node:fs/promises";
24
26
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
25
27
  import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
26
- import { readNoteFile } from "./notes-file.js";
28
+ import { composedMessageBody, readMailBody, runMailDraft } from "./mail-compose.js";
27
29
  import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
28
30
  import { towerRequest } from "../tower-client.js";
29
31
  const TAG = "[mail cli]";
@@ -31,7 +33,6 @@ const READ_DEADLINE_MS = 30_000;
31
33
  const WRITE_DEADLINE_MS = 60_000;
32
34
  /** A sync pass reads a bounded page, but a cold mailbox can still take a while. */
33
35
  const SYNC_DEADLINE_MS = 120_000;
34
- const BODY_MAX_CHARS = 200_000;
35
36
  const PASSWORD_MAX_CHARS = 200;
36
37
  export async function runMail(command, io) {
37
38
  const door = await openAgentDoor("mail", command, io);
@@ -50,6 +51,8 @@ export async function runMail(command, io) {
50
51
  return search(command, door);
51
52
  case "send":
52
53
  return send(command, door);
54
+ case "draft":
55
+ return runMailDraft(command, door);
53
56
  case "attachment":
54
57
  return attachment(command, door);
55
58
  case "sync":
@@ -250,7 +253,7 @@ async function search(command, door) {
250
253
  return 0;
251
254
  }
252
255
  async function send(command, door) {
253
- const body = await readBody(command, door);
256
+ const body = await readMailBody(command, door);
254
257
  if (!body.ok)
255
258
  return failAgentDoor(door, TAG, body.reason, body.detail);
256
259
  if (body.text.trim() === "") {
@@ -261,14 +264,7 @@ async function send(command, door) {
261
264
  method: "POST",
262
265
  label: "mail send",
263
266
  timeoutMs: WRITE_DEADLINE_MS,
264
- body: {
265
- account_id: command.accountId,
266
- to: command.to.map((address) => ({ address })),
267
- ...(command.cc.length > 0 ? { cc: command.cc.map((address) => ({ address })) } : {}),
268
- subject: command.subjectLine ?? "",
269
- text: body.text,
270
- ...(command.replyTo ? { in_reply_to_message_id: command.replyTo } : {}),
271
- },
267
+ body: composedMessageBody(command, body.text),
272
268
  });
273
269
  if (!answer.ok)
274
270
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
@@ -361,26 +357,4 @@ async function syncStatus(command, door) {
361
357
  writeLine(door.io.stdout, `${account.address} last_success=${account.last_success_at ?? "never"} last_run=${lastRun} failures_24h=${account.failures_24h} lag=${lag}${account.data_incomplete ? " (partial read)" : ""}`);
362
358
  }
363
359
  return 0;
364
- }
365
- /** Body on stdin, never argv — `--file` is the safest way on Windows. */
366
- async function readBody(command, door) {
367
- if (command.filePath) {
368
- const read = await readNoteFile(command.filePath);
369
- if (!read.ok) {
370
- return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
371
- }
372
- return { ok: true, text: read.bytes.toString("utf8") };
373
- }
374
- if (isInteractiveStdin(door.io))
375
- return { ok: true, text: "" };
376
- try {
377
- const text = await readPipedText(door.io.stdin, {
378
- maxChars: BODY_MAX_CHARS,
379
- overflowMessage: `A message body is limited to ${BODY_MAX_CHARS} characters here.`,
380
- });
381
- return { ok: true, text };
382
- }
383
- catch (error) {
384
- return { ok: false, reason: "body_too_long", detail: error instanceof Error ? error.message : String(error) };
385
- }
386
360
  }
@@ -111,14 +111,24 @@ function isUnhealthyPipeline(row) {
111
111
  }
112
112
  function writeOpsStatusOutput(command, io, payload, memory, models, turns, toolRouter) {
113
113
  if (command.json) {
114
- writeJsonOpsStatus(io, payload, memory, models, turns, toolRouter);
114
+ writeJsonOpsStatus(command.people === true, io, payload, memory, models, turns, toolRouter);
115
115
  return;
116
116
  }
117
117
  writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRouter);
118
118
  }
119
- function writeJsonOpsStatus(io, payload, memory, models, turns, toolRouter) {
119
+ /** The block without its per-device / per-person rows unless `--people` asked for them. */
120
+ function summaryOnly(block, withPeople) {
121
+ if (withPeople)
122
+ return block;
123
+ const { devices: _devices, people: _people, ...rest } = block;
124
+ return rest;
125
+ }
126
+ function writeJsonOpsStatus(withPeople, io, payload, memory, models, turns, toolRouter) {
120
127
  writeLine(io.stdout, JSON.stringify({
121
128
  ...payload,
129
+ // BLI-12507: the MCP's rule, so one board reads the same on both doors.
130
+ ...(payload.fleet ? { fleet: summaryOnly(payload.fleet, withPeople) } : {}),
131
+ ...(payload.coverage ? { coverage: summaryOnly(payload.coverage, withPeople) } : {}),
122
132
  ...(memory
123
133
  ? { memory: memory.section, hooks: memory.hooks, experience: memory.experience }
124
134
  : {}),
@@ -134,6 +144,13 @@ function writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRo
134
144
  })) {
135
145
  writeLine(io.stdout, line);
136
146
  }
147
+ // BLI-12507: a section the server could not read inside its budget is named
148
+ // here, never a silent gap in the board.
149
+ const unreadable = asRecord(payload)["unreadableSections"];
150
+ if (Array.isArray(unreadable) && unreadable.length > 0) {
151
+ writeLine(io.stdout, "");
152
+ writeLine(io.stdout, `NOT READ ${unreadable.join(", ")} (reader_timeout or read failure; see the Tower log)`);
153
+ }
137
154
  if (memory?.section) {
138
155
  for (const line of renderMemoryUsage(memory.section, (text) => dim(text, styled))) {
139
156
  writeLine(io.stdout, line);
@@ -195,6 +212,10 @@ function createOpsStatusReadLog(command, payload, rows, unhealthy, memory, model
195
212
  unhealthy: unhealthy.length,
196
213
  unhealthy_ids: unhealthy.map((row) => row.id ?? "?"),
197
214
  with_skips: Boolean(command.skips),
215
+ people_rows: Boolean(command.people) || !command.json ? "included" : "summary_only",
216
+ unreadable_sections: Array.isArray(asRecord(payload)["unreadableSections"])
217
+ ? asRecord(payload)["unreadableSections"]
218
+ : [],
198
219
  // The laptops (BLI-3550), counted on the same line: a run that shows a
199
220
  // green board and says nothing about the fleet cannot answer "was
200
221
  // anybody's machine dead this morning?".
@@ -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.164");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.166");
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.164",
3
+ "version": "0.2.166",
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.45",
31
- "@bli-cockpit/mcp": "0.1.82",
31
+ "@bli-cockpit/mcp": "0.1.84",
32
32
  "@bli-cockpit/telemetry-core": "0.1.57"
33
33
  },
34
34
  "repository": {