@bli-cockpit/cli 0.2.87 → 0.2.89

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.
@@ -68,6 +68,9 @@ export function parseOpsArgs(args) {
68
68
  "--memory-days",
69
69
  // BLI-3798: the coverage half on its own, with the bucket table.
70
70
  "--coverage",
71
+ // BLI-3912: one line per model — latency, tokens/s, today's spend, last
72
+ // probe verdict. A SECOND door like --memory, asked for only when named.
73
+ "--models",
71
74
  "--person",
72
75
  "--dry-run",
73
76
  "--json",
@@ -108,6 +111,7 @@ export function parseOpsArgs(args) {
108
111
  coverage: coverageOnly,
109
112
  skips: values.booleans.has("--skips"),
110
113
  memory: values.booleans.has("--memory") || memoryDays !== undefined,
114
+ models: values.booleans.has("--models"),
111
115
  ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
112
116
  ...base,
113
117
  };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `cockpit models` — the model-card shelf, in the terminal (BLI-3912).
3
+ *
4
+ * cockpit models # every card: documented vs measured
5
+ * cockpit models list --json
6
+ * cockpit models show openai:gpt-5.6-terra
7
+ *
8
+ * Two nouns that look alike and are not: `cockpit model` is the ORG SETTING for
9
+ * which model a slot runs on (`model show`, `model set`); `cockpit models` is
10
+ * the shelf that setting picks FROM — windows, prices, reasoning vocabulary,
11
+ * quirks, probe verdicts and stale marks. The help text for each says so.
12
+ */
13
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
14
+ export function parseModelsArgs(args) {
15
+ const values = parseNamedArgs(args, {
16
+ allowedFlags: ["--home", "--dashboard-url", "--json"],
17
+ valueFlags: ["--home", "--dashboard-url"],
18
+ });
19
+ if (values.positionals.length > 2) {
20
+ throw new Error("models accepts at most an action (list|show) and one model id.");
21
+ }
22
+ const [rawAction, rawId] = values.positionals;
23
+ const base = {
24
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
25
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
26
+ json: values.booleans.has("--json"),
27
+ };
28
+ if (!rawAction || rawAction === "list") {
29
+ if (rawAction === "list" && rawId) {
30
+ throw new Error("models list takes no model id — use `models show <provider:model>`.");
31
+ }
32
+ return { kind: "models", action: "list", ...base };
33
+ }
34
+ if (rawAction !== "show") {
35
+ // A bare id is the shape people type first; treat it as `show <id>` rather
36
+ // than refusing on a technicality.
37
+ return { kind: "models", action: "show", modelId: rawAction, ...base };
38
+ }
39
+ const modelId = optionalNonEmpty(rawId);
40
+ if (!modelId) {
41
+ throw new Error("models show needs a model id, e.g. `models show openai:gpt-5.6-terra`.");
42
+ }
43
+ return { kind: "models", action: "show", modelId, ...base };
44
+ }
@@ -33,5 +33,6 @@ export { SCOUT_MIN_PREFIX_LENGTH, parseScoutArgs, parseOpsArgs, SLACK_WORKSPACE_
33
33
  export { parseDocsArgs, parseMsgArgs, } from "./local-args-tower-docs-msg.js";
34
34
  export { ISSUE_STATES, parseIssueArgs, parseProjectArgs, } from "./local-args-tower-work.js";
35
35
  export { SEARCH_KINDS, parseSearchArgs } from "./local-args-tower-search.js";
36
+ export { parseModelsArgs } from "./local-args-tower-models.js";
36
37
  export { parseMailArgs } from "./local-args-tower-mail.js";
37
38
  export { parseCalArgs } from "./local-args-tower-cal.js";
@@ -16,7 +16,7 @@
16
16
  * verbatim, no logic change.
17
17
  */
18
18
  import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
19
- import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
19
+ import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
20
20
  // `normalizeUrl` has always been part of this module's surface — `local.ts` and
21
21
  // `local-auth.ts` import it from here — so it stays exported from this address
22
22
  // even though it now lives next door. The same goes for the four names the
@@ -63,6 +63,8 @@ export function parseLocalArgs(argv) {
63
63
  return parseJarvisArgs(argv.slice(1));
64
64
  case "model":
65
65
  return parseModelArgs(argv.slice(1));
66
+ case "models":
67
+ return parseModelsArgs(argv.slice(1));
66
68
  case "scout":
67
69
  return parseScoutArgs(argv.slice(1));
68
70
  case "ops":
@@ -273,6 +273,26 @@ export function localSubcommandHelp(command) {
273
273
  "Run `cockpit login` first if this machine is not paired.",
274
274
  ],
275
275
  ],
276
+ [
277
+ "models",
278
+ [
279
+ "Usage: cockpit models [list|show <provider:model>] [--dashboard-url <url>] [--json]",
280
+ "",
281
+ "The model-card shelf: one card per model, with what the PROVIDER documents (context",
282
+ "window, max output, price per million, reasoning modes, tool calling, structured output,",
283
+ "quirks) beside what OUR machine measured (latency p50/p95, observed tokens/s, the last",
284
+ "belt and conformance probe verdicts).",
285
+ "Every value carries the page it was read from; a field the provider does not state reads",
286
+ "unknown with the reason, never a guess.",
287
+ "STALE:<field> means a measurement contradicted the documentation — reasoning-off refused",
288
+ "where the card says it can be turned off, the belt refused where the card says tool",
289
+ "calling. The line under it is the command that re-reads the provider.",
290
+ "`cockpit models` is the SHELF; `cockpit model` is the org setting saying which model a",
291
+ "slot runs on. Different nouns, one letter apart, on purpose.",
292
+ "--json writes one object to stdout; operational lines stay on stderr.",
293
+ "Run `cockpit login` first if this machine is not paired.",
294
+ ],
295
+ ],
276
296
  [
277
297
  "scout",
278
298
  [
@@ -295,7 +315,7 @@ export function localSubcommandHelp(command) {
295
315
  [
296
316
  "ops",
297
317
  [
298
- "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] | recompile --person <email|name|id> [--dry-run]] [--json]",
318
+ "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] | recompile --person <email|name|id> [--dry-run]] [--json]",
299
319
  "",
300
320
  " cockpit ops status",
301
321
  " One line per scheduled job: when it last produced something, and whether that",
@@ -25,6 +25,7 @@ export const rootCommandNames = new Set([
25
25
  "analyze",
26
26
  "jarvis",
27
27
  "model",
28
+ "models",
28
29
  "scout",
29
30
  "ops",
30
31
  "slack",
@@ -70,8 +71,9 @@ export function localCommandHelp(command) {
70
71
  " cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
71
72
  " cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--trace <id|last>] [--dashboard-url <url>] [--json [--show-approval-code]]",
72
73
  " cockpit model [show|set <provider:model>] [--json]",
74
+ " cockpit models [list|show <provider:model>] [--dashboard-url <url>] [--json]",
73
75
  " cockpit scout [start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
74
- " cockpit ops [status [--job <id>] [--skips] [--memory] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
76
+ " cockpit ops [status [--job <id>] [--skips] [--memory] [--models] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
75
77
  " cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] | read [--person <p>] [--channel <c>] [--query <text>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>]] [--json]",
76
78
  " cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | cli-floor [<version>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
77
79
  " cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes] | device [list] | device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]] [--json]",
@@ -16,6 +16,7 @@ import { runDoctor } from "./doctor.js";
16
16
  import { runStatus } from "./status.js";
17
17
  import { runSessions } from "./sessions.js";
18
18
  import { runJarvis } from "./jarvis.js";
19
+ import { runModels } from "./models.js";
19
20
  import { runScout } from "./scout.js";
20
21
  import { runOps } from "./ops.js";
21
22
  import { runSlack } from "./slack.js";
@@ -104,6 +105,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
104
105
  return await runJarvis(command, io);
105
106
  case "model":
106
107
  return await runModel(command, io);
108
+ case "models":
109
+ return await runModels(command, io);
107
110
  case "scout":
108
111
  return await runScout(command, io);
109
112
  case "ops":
@@ -54,6 +54,7 @@ export function readMemoryHookCounts(options) {
54
54
  }
55
55
  : {}),
56
56
  hook_skipped_trivial_24h: window.skippedTrivial,
57
+ hook_billing_exhausted_24h: window.billingExhausted,
57
58
  },
58
59
  reason: "ok",
59
60
  };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `cockpit models` — what do we know about each model? (BLI-3912)
3
+ *
4
+ * Prints the shelf: one line per model with the provider's documented window,
5
+ * price, reasoning mode and tool support beside OUR measured latency, observed
6
+ * tokens/s and last probe verdict, and a `STALE:<field>` mark wherever the two
7
+ * disagree — with the command that fixes it on the next line.
8
+ *
9
+ * **Every line comes from the server** (`/api/models/cards`, rendered by
10
+ * `lib/models/model-lines.ts`). This file decides an exit code and nothing
11
+ * else. A terminal that formatted a card itself would eventually disagree with
12
+ * the board about what "stale" means, and the one that is wrong is always the
13
+ * one somebody is reading.
14
+ *
15
+ * **Exit codes.** Zero when the shelf was read, whatever it says — a stale card
16
+ * is a fact to act on, not a broken command. One when the read failed or when a
17
+ * named card does not exist, because a script asking about a specific model
18
+ * needs to know it asked about nothing.
19
+ */
20
+ import { writeLine } from "./cli-io.js";
21
+ import { loadPairedSession, towerJsonRequest, } from "../tower-client.js";
22
+ /** A card read is checked-in data on the server side; it should never hang a shell. */
23
+ const READ_DEADLINE_MS = 30_000;
24
+ export async function runModels(command, io) {
25
+ const session = await loadPairedSession("models", command.homeDir);
26
+ const dashboardUrl = command.dashboardUrl ?? session.dashboard_url;
27
+ const log = (line) => writeLine(io.stderr, line);
28
+ const path = command.action === "show" && command.modelId
29
+ ? `/api/models/cards?id=${encodeURIComponent(command.modelId)}`
30
+ : "/api/models/cards";
31
+ const result = await towerJsonRequest({
32
+ dashboardUrl,
33
+ path,
34
+ method: "GET",
35
+ deviceToken: session.device_token,
36
+ fetch: io.fetch,
37
+ label: "models",
38
+ timeoutMs: READ_DEADLINE_MS,
39
+ log,
40
+ });
41
+ if (!result.ok) {
42
+ writeFailure(command, io, result);
43
+ return 1;
44
+ }
45
+ const payload = (result.body ?? {});
46
+ if (command.json) {
47
+ writeLine(io.stdout, JSON.stringify({ ok: true, ...payload }));
48
+ }
49
+ else {
50
+ for (const line of payload.lines ?? [])
51
+ writeLine(io.stdout, line);
52
+ }
53
+ // Never silent about a card the server could not read: a shelf quietly one
54
+ // model short is exactly what the card registry exists to prevent.
55
+ for (const unreadable of payload.unreadable ?? []) {
56
+ writeLine(io.stderr, `[models cli] card unreadable ${JSON.stringify({ card: unreadable })}`);
57
+ }
58
+ writeLine(io.stderr, `[models cli] read ${JSON.stringify({
59
+ action: command.action,
60
+ model: command.modelId ?? null,
61
+ cards: Array.isArray(payload.cards) ? payload.cards.length : payload.card ? 1 : 0,
62
+ unreadable: payload.unreadable?.length ?? 0,
63
+ })}`);
64
+ return 0;
65
+ }
66
+ function writeFailure(command, io, failure) {
67
+ // `TowerFailure` carries the server's own words in `detail` — a 404 for a
68
+ // model nobody has carded says exactly which id it was and what to run.
69
+ const message = failure.detail;
70
+ if (command.json) {
71
+ writeLine(io.stdout, JSON.stringify({
72
+ ok: false,
73
+ error: failure.reason,
74
+ message,
75
+ httpStatus: failure.httpStatus ?? null,
76
+ }));
77
+ }
78
+ else {
79
+ writeLine(io.stdout, message);
80
+ }
81
+ writeLine(io.stderr, `[models cli] not read ${JSON.stringify({
82
+ reason: failure.reason,
83
+ http_status: failure.httpStatus ?? null,
84
+ model: command.modelId ?? null,
85
+ })}`);
86
+ }
@@ -96,11 +96,14 @@ export function renderMemoryHooks(hooks, dim) {
96
96
  const rightKnown = typeof right.runs === "number" ? 0 : 1;
97
97
  if (leftKnown !== rightKnown)
98
98
  return leftKnown - rightKnown;
99
- return (right.timeouts ?? 0) - (left.timeouts ?? 0);
99
+ // BLI-3891: a billing refusal ranks beside a missed deadline. Both are a
100
+ // recall the person did not get; only the fix differs.
101
+ return ((right.timeouts ?? 0) + (right.billingExhausted ?? 0)
102
+ - ((left.timeouts ?? 0) + (left.billingExhausted ?? 0)));
100
103
  });
101
104
  for (const device of ranked) {
102
105
  const line = ` ${device.line ?? `${device.displayName ?? "?"}: (no line)`}`;
103
- lines.push((device.timeouts ?? 0) > 0 ? line : dim(line));
106
+ lines.push((device.timeouts ?? 0) > 0 || (device.billingExhausted ?? 0) > 0 ? line : dim(line));
104
107
  if (device.performanceLine)
105
108
  lines.push(` ${device.performanceLine}`);
106
109
  // BLI-3884. Written on the server (`lib/ops/memory-hook-misses.ts`) and
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `cockpit ops` — WHAT TODAY COST, by model (BLI-3909).
3
+ *
4
+ * Split out of `ops-render.ts` the way `ops-render-memory.ts` and
5
+ * `ops-render-coverage.ts` were, when that file crossed the repo's 500-line
6
+ * readability floor.
7
+ *
8
+ * Same rule as the rest of that family: every sentence here was written on the
9
+ * SERVER (`lib/ops/model-spend-today.ts`) and is printed VERBATIM. This module
10
+ * decides order, indentation and column widths and nothing else — a terminal
11
+ * that composed its own total from the same fields would eventually disagree
12
+ * with the page about what a $25 day means, and the one that is wrong is
13
+ * always the one somebody is reading.
14
+ *
15
+ * A CLI too old to know this section simply does not print it. Nothing is lost
16
+ * by that: the section rides the ordinary `/api/ops/status` response, so
17
+ * `cockpit ops --json` carries it on every version.
18
+ */
19
+ /** `$18.65`, `$0.0031`, `—` when the server sent no number at all. */
20
+ function usdColumn(usd) {
21
+ if (typeof usd !== "number")
22
+ return "—";
23
+ return `$${usd.toFixed(usd < 0.01 ? 4 : 2)}`;
24
+ }
25
+ /**
26
+ * The SPEND block: the server's one line, then one row per model. `$314 of a
27
+ * $357 month on one model` is the thing this exists to make visible before the
28
+ * invoice does.
29
+ */
30
+ export function renderSpend(spend, dim) {
31
+ const lines = ["", `SPEND ${spend.summary ?? "(no summary)"}`];
32
+ for (const row of spend.models ?? []) {
33
+ lines.push(dim(` ${usdColumn(row.usd).padStart(9)} ${row.model ?? "?"} · ${row.calls ?? 0} call(s) · ${(row.bases ?? []).join("+") || "?"} · ${(row.sources ?? []).join(",") || "?"}`));
34
+ }
35
+ return lines;
36
+ }
@@ -13,6 +13,10 @@
13
13
  * somebody is about to conclude something from it.
14
14
  */
15
15
  import { renderCoverageBuckets, } from "./ops-render-coverage.js";
16
+ // BLI-3909: today's spend by model, its own renderer for the same reason the
17
+ // coverage table has one — this file is at the repo's readability floor.
18
+ import { renderSpend } from "./ops-render-spend.js";
19
+ export { renderSpend } from "./ops-render-spend.js";
16
20
  export { renderMemoryHooks, renderMemoryUsage } from "./ops-render-memory.js";
17
21
  export { renderCoverageBuckets };
18
22
  /** The word a person reads. Short, fixed width, and never a bare colour. */
@@ -124,6 +128,8 @@ options = {}) {
124
128
  lines.push(...renderCoverageBuckets(payload.coverage.buckets, dim));
125
129
  }
126
130
  }
131
+ if (payload.spend)
132
+ lines.push(...renderSpend(payload.spend, dim));
127
133
  const slack = payload.skips?.slack;
128
134
  const external = payload.skips?.external;
129
135
  if (slack || external) {
@@ -77,6 +77,11 @@ async function runOpsStatus(command, io, tower) {
77
77
  // status board's exit code — "is the org using memory" going unread is not a
78
78
  // pipeline outage.
79
79
  const memory = command.memory ? await readMemoryUsage(command, io, tower) : null;
80
+ // BLI-3912. A THIRD door, asked for only when `--models` was, and never able
81
+ // to fail the board: "what is each model costing and how fast is it" is a
82
+ // different question from "is the pipeline healthy", and a card shelf that
83
+ // could not be read is not a collection outage.
84
+ const models = command.models ? await readModelBoardLines(io, tower) : null;
80
85
  // BLI-3723: `failing` joins the list. An older CLI that has never heard of it
81
86
  // simply does not count it — which is why the server keeps the sentence in
82
87
  // `detail`, so an un-upgraded terminal still PRINTS the outage even when it
@@ -88,7 +93,13 @@ async function runOpsStatus(command, io, tower) {
88
93
  // BLI-3762: a job that ran and wrote less than it owed is not healthy.
89
94
  row.verdict === "degraded");
90
95
  if (command.json) {
91
- writeLine(io.stdout, JSON.stringify(memory ? { ...payload, memory: memory.section, hooks: memory.hooks, experience: memory.experience } : payload));
96
+ writeLine(io.stdout, JSON.stringify({
97
+ ...payload,
98
+ ...(memory
99
+ ? { memory: memory.section, hooks: memory.hooks, experience: memory.experience }
100
+ : {}),
101
+ ...(models ? { models: models.board, modelsReason: models.reason } : {}),
102
+ }));
92
103
  }
93
104
  else {
94
105
  const styled = colorEnabled(io);
@@ -116,6 +127,17 @@ async function runOpsStatus(command, io, tower) {
116
127
  for (const line of memory.experience.lines)
117
128
  writeLine(io.stdout, ` ${line}`);
118
129
  }
130
+ if (models) {
131
+ writeLine(io.stdout, "");
132
+ if (models.lines.length > 0) {
133
+ for (const line of models.lines)
134
+ writeLine(io.stdout, line);
135
+ }
136
+ else {
137
+ // Never a silent gap where a section was asked for.
138
+ writeLine(io.stdout, `MODELS not read (${models.reason})`);
139
+ }
140
+ }
119
141
  if (memory && !memory.section) {
120
142
  // Never a silent gap where a section was asked for: the reason the gauge
121
143
  // could not be read is printed where the gauge would have been.
@@ -152,11 +174,19 @@ async function runOpsStatus(command, io, tower) {
152
174
  // BLI-3881: runs the hook declined to search for. A SUBSET of runs, so
153
175
  // `runs - skipped_trivial` is how many turns actually asked the door.
154
176
  memory_hook_skipped_trivial_24h: memory?.hooks?.totals?.skippedTrivial ?? null,
177
+ // BLI-3891: null means the fleet reported none of these at all, which is
178
+ // "not measured" on a machine older than the count — never "no refusals".
179
+ memory_hook_billing_exhausted_24h: memory?.hooks?.totals?.billingExhausted ?? null,
155
180
  memory_hook_devices_reporting: memory?.hooks?.reporting ?? null,
156
181
  // BLI-3884: which route answered them, fleet-wide. Null while no machine
157
182
  // names one — a rollout fact, not a count of zero.
158
183
  memory_hook_via_daemon_24h: memory?.hooks?.via?.daemon ?? null,
159
184
  memory_hook_via_direct_24h: memory?.hooks?.via?.direct ?? null,
185
+ // BLI-3912: whether the model board was asked for, and what it said.
186
+ models_asked: Boolean(command.models),
187
+ models_reason: models?.reason ?? null,
188
+ models_counted: models?.board?.rows?.length ?? null,
189
+ models_cost_today_usd: models?.board?.total_cost_today_usd ?? null,
160
190
  })}`);
161
191
  return unhealthy.length > 0 ? 1 : 0;
162
192
  }
@@ -192,6 +222,29 @@ async function readMemoryUsage(command, io, tower) {
192
222
  return { section: null, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "memory_section_absent" };
193
223
  return { section: body.memory, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "ok" };
194
224
  }
225
+ /**
226
+ * The per-model board, read through its own door (BLI-3912).
227
+ *
228
+ * Same posture as the adoption gauge next door: it never fails the status
229
+ * board, and when it cannot be read the reason is printed where the section
230
+ * would have been. The LINES come from the server, so a terminal and the
231
+ * dashboard cannot disagree about what a model cost today.
232
+ */
233
+ async function readModelBoardLines(io, tower) {
234
+ const result = await callTower(tower, { path: "/api/ops/models", label: "ops-models" });
235
+ if (!result.ok) {
236
+ writeLine(io.stderr, `[ops cli] model board not read ${JSON.stringify({
237
+ reason: result.reason,
238
+ http_status: result.httpStatus ?? null,
239
+ })}`);
240
+ return { lines: [], board: null, reason: result.reason };
241
+ }
242
+ const body = asRecord(result.body);
243
+ if (!body.lines || body.lines.length === 0) {
244
+ return { lines: [], board: body.board ?? null, reason: "models_section_absent" };
245
+ }
246
+ return { lines: body.lines, board: body.board ?? null, reason: "ok" };
247
+ }
195
248
  async function runOpsRecompile(command, io, tower) {
196
249
  const person = command.person ?? "";
197
250
  if (!command.json) {
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.87");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.89");
19
19
  return 0;
20
20
  }
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.87",
3
+ "version": "0.2.89",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.19",
31
- "@bli-cockpit/mcp": "0.1.20",
32
- "@bli-cockpit/telemetry-core": "0.1.36"
30
+ "@bli-cockpit/memory-mcp": "0.1.20",
31
+ "@bli-cockpit/mcp": "0.1.22",
32
+ "@bli-cockpit/telemetry-core": "0.1.37"
33
33
  }
34
34
  }