@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.
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-models.js +44 -0
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help-commands.js +21 -1
- package/dist/commands/local-help.js +3 -1
- package/dist/commands/local.js +3 -0
- package/dist/commands/memory-hook-counts.js +1 -0
- package/dist/commands/models.js +86 -0
- package/dist/commands/ops-render-memory.js +5 -2
- package/dist/commands/ops-render-spend.js +36 -0
- package/dist/commands/ops-render.js +6 -0
- package/dist/commands/ops.js +54 -1
- package/dist/commands/public-root.js +1 -1
- package/package.json +4 -4
|
@@ -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]",
|
package/dist/commands/local.js
CHANGED
|
@@ -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":
|
|
@@ -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
|
-
|
|
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) {
|
package/dist/commands/ops.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
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.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
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
|
}
|