@bli-cockpit/cli 0.2.119 → 0.2.122
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/analyze.js +74 -54
- package/dist/commands/brief-rewrite.js +164 -101
- package/dist/commands/brief.js +38 -13
- package/dist/commands/careers.js +81 -9
- package/dist/commands/correct.js +38 -21
- package/dist/commands/docs.js +13 -10
- package/dist/commands/editor.js +59 -30
- package/dist/commands/install-receipts.js +106 -91
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-cal.js +28 -3
- package/dist/commands/local-args-tower-careers.js +56 -9
- package/dist/commands/local-args-tower-chat.js +55 -28
- package/dist/commands/local-args-tower-docs-msg.js +39 -8
- package/dist/commands/local-args-tower-mail.js +27 -1
- package/dist/commands/local-args-tower-models.js +14 -17
- package/dist/commands/local-args-tower-work.js +37 -6
- package/dist/commands/local-help-commands-tower.js +15 -1
- package/dist/commands/local-help-commands.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mcp-stdio-probe.js +92 -73
- package/dist/commands/memory-hook-performance.js +135 -101
- package/dist/commands/memory-install-claude.js +15 -14
- package/dist/commands/memory-install-codex.js +10 -6
- package/dist/commands/memory-install-config.js +5 -4
- package/dist/commands/memory-install-contract.js +56 -10
- package/dist/commands/memory-install-report.js +16 -11
- package/dist/commands/memory-install-skills.js +11 -11
- package/dist/commands/memory-log.js +22 -5
- package/dist/commands/msg.js +11 -5
- package/dist/commands/onboard-setup.js +16 -1
- package/dist/commands/ops-sections.js +89 -0
- package/dist/commands/ops.js +117 -120
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout.js +90 -68
- package/dist/commands/session-sync-failures.js +19 -13
- package/dist/commands/session-sync-record.js +53 -52
- package/dist/commands/session-sync-upload.js +15 -11
- package/dist/commands/sessions.js +61 -51
- package/dist/commands/slack.js +90 -61
- package/dist/commands/status.js +53 -41
- package/dist/commands/workbook.js +23 -20
- package/package.json +2 -2
|
@@ -49,6 +49,12 @@
|
|
|
49
49
|
* machine resolved before anything is written. Skipping that step registered
|
|
50
50
|
* three hooks that answered `command not found` on every turn.
|
|
51
51
|
*
|
|
52
|
+
* **And the path it is re-pointed at is written for a SHELL, not for the
|
|
53
|
+
* filesystem** — `shellSafeBinPath`. A hook command is a shell string, Windows
|
|
54
|
+
* runs it through bash, and a native path's backslashes are eaten as escapes:
|
|
55
|
+
* the same three hooks, the same `command not found`, arrived at from the other
|
|
56
|
+
* direction (BLI-4136).
|
|
57
|
+
*
|
|
52
58
|
* Nothing here touches the filesystem. The halves that do are
|
|
53
59
|
* `memory-install-claude.ts` and `memory-install-codex.ts`.
|
|
54
60
|
*/
|
|
@@ -96,16 +102,56 @@ export const MEMORY_AUTO_APPROVE_TOOLS = [
|
|
|
96
102
|
];
|
|
97
103
|
/** Characters that would let a resolved path change the meaning of a hook command string. */
|
|
98
104
|
const UNSAFE_PATH_CHARACTERS = /["'`$;&|<>\r\n]/u;
|
|
105
|
+
/**
|
|
106
|
+
* Windows device paths — `\\?\C:\…` (extended length) and `\\.\…` (device
|
|
107
|
+
* namespace). Both prefixes are DEFINED in terms of backslashes and stop being
|
|
108
|
+
* that prefix the moment `shellSafeBinPath` turns them into forward slashes, so
|
|
109
|
+
* a hook command cannot carry one without silently naming a different file.
|
|
110
|
+
* Refused with a name rather than rewritten into something plausible.
|
|
111
|
+
*/
|
|
112
|
+
const WINDOWS_DEVICE_PATH = /^\\\\[?.]\\/u;
|
|
99
113
|
export function isUnsafeBinPath(binPath) {
|
|
100
|
-
return UNSAFE_PATH_CHARACTERS.test(binPath);
|
|
114
|
+
return UNSAFE_PATH_CHARACTERS.test(binPath) || WINDOWS_DEVICE_PATH.test(binPath);
|
|
101
115
|
}
|
|
102
116
|
/**
|
|
103
|
-
* A
|
|
104
|
-
*
|
|
105
|
-
*
|
|
117
|
+
* A path that no shell gives a meaning to, so it needs no quoting. Deliberately
|
|
118
|
+
* a tight allow-list rather than a list of known-bad characters: `(` and `)` in
|
|
119
|
+
* `C:/Program Files (x86)/…` are a bash syntax error, and the earlier
|
|
120
|
+
* whitespace-only trigger caught them only by the accident of the space.
|
|
121
|
+
*/
|
|
122
|
+
const PLAINLY_SAFE_PATH = /^[A-Za-z0-9_@:./+,-]+$/u;
|
|
123
|
+
/**
|
|
124
|
+
* The resolved bin path, as it must appear INSIDE a hook command string.
|
|
125
|
+
*
|
|
126
|
+
* A Claude Code hook `command` is a shell string by the platform's design, and
|
|
127
|
+
* on Windows the host runs it through **bash** — which eats every backslash as
|
|
128
|
+
* an escape. So the native path this installer resolved,
|
|
129
|
+
* `C:\Users\…\.bin\bli-memory-mcp.cmd`, reaches the shell as
|
|
130
|
+
* `C:Users….binbli-memory-mcp.cmd` and answers `command not found` on every
|
|
131
|
+
* SessionStart, every prompt and every Stop. Non-blocking and therefore silent:
|
|
132
|
+
* memory recall simply degrades, and nothing says so (BLI-4136, fixed by hand
|
|
133
|
+
* twice on the same machine before the installer was the thing that changed).
|
|
134
|
+
*
|
|
135
|
+
* Two independent defences, because either one alone is a thin edge:
|
|
136
|
+
*
|
|
137
|
+
* 1. **Separators are normalised to `/` on Windows.** Win32 accepts forward
|
|
138
|
+
* slashes in every path it resolves, and after this there is no escape
|
|
139
|
+
* character left in the string for a shell to consume. This is also what
|
|
140
|
+
* survives a `\\`: double-quoting ALONE would collapse the UNC prefix of
|
|
141
|
+
* `\\server\share\…` back to one backslash and name the wrong path.
|
|
142
|
+
* 2. **Anything not plainly safe is double-quoted**, which covers spaces and
|
|
143
|
+
* the bracket characters in `Program Files (x86)`.
|
|
144
|
+
*
|
|
145
|
+
* A path that still could not be expressed safely is refused upstream
|
|
146
|
+
* (`isUnsafeBinPath`, checked in `resolveMemoryConfig`) rather than escaped
|
|
147
|
+
* cleverly.
|
|
148
|
+
*
|
|
149
|
+
* POSIX keeps its separators untouched — a backslash there is a legal
|
|
150
|
+
* character in a filename, and rewriting it would name a different file.
|
|
106
151
|
*/
|
|
107
|
-
export function
|
|
108
|
-
|
|
152
|
+
export function shellSafeBinPath(binPath, platform) {
|
|
153
|
+
const forShell = platform === "win32" ? binPath.replace(/\\/gu, "/") : binPath;
|
|
154
|
+
return PLAINLY_SAFE_PATH.test(forShell) ? forShell : `"${forShell}"`;
|
|
109
155
|
}
|
|
110
156
|
/**
|
|
111
157
|
* Windows cannot spawn an npm `.cmd` shim directly — Node refuses it without a
|
|
@@ -125,13 +171,13 @@ export function memoryMcpServerEntry(options) {
|
|
|
125
171
|
return { command: options.binPath, args: [], env };
|
|
126
172
|
}
|
|
127
173
|
export function builtinMemoryInstallConfig(options) {
|
|
128
|
-
const
|
|
174
|
+
const binPath = shellSafeBinPath(options.binPath, options.platform);
|
|
129
175
|
return {
|
|
130
176
|
server_id: MEMORY_MCP_SERVER_ID,
|
|
131
177
|
mcp_server: memoryMcpServerEntry(options),
|
|
132
178
|
hooks: MEMORY_HOOK_EVENTS.map((event) => ({
|
|
133
179
|
event,
|
|
134
|
-
command: `${
|
|
180
|
+
command: `${binPath} ${MEMORY_HOOK_SUBCOMMAND[event]}`,
|
|
135
181
|
timeout_seconds: MEMORY_HOOK_TIMEOUT_SECONDS[event],
|
|
136
182
|
})),
|
|
137
183
|
permissions_allow: [...MEMORY_AUTO_APPROVE_TOOLS],
|
|
@@ -237,13 +283,13 @@ export function parsePrintedMemoryInstallConfig(stdout) {
|
|
|
237
283
|
* hook that runs something else.
|
|
238
284
|
*/
|
|
239
285
|
export function withResolvedBinPath(config, options) {
|
|
240
|
-
const
|
|
286
|
+
const binPath = shellSafeBinPath(options.binPath, options.platform);
|
|
241
287
|
const hooks = [];
|
|
242
288
|
for (const hook of config.hooks) {
|
|
243
289
|
const tail = hookSubcommandTail(hook.command);
|
|
244
290
|
if (!tail)
|
|
245
291
|
return null;
|
|
246
|
-
hooks.push({ ...hook, command: `${
|
|
292
|
+
hooks.push({ ...hook, command: `${binPath} ${tail}` });
|
|
247
293
|
}
|
|
248
294
|
const entry = memoryMcpServerEntry(options);
|
|
249
295
|
return {
|
|
@@ -83,17 +83,7 @@ export function logMemoryOutcome(outcome, platform) {
|
|
|
83
83
|
: "[memory-install] BLI Memory registration converged", JSON.stringify(fields));
|
|
84
84
|
}
|
|
85
85
|
export function memoryOutcomeLines(outcome) {
|
|
86
|
-
const headline = outcome
|
|
87
|
-
? "BLI Memory registered on this machine."
|
|
88
|
-
: outcome.status === "already"
|
|
89
|
-
? "BLI Memory is already registered on this machine."
|
|
90
|
-
: outcome.status === "would_install"
|
|
91
|
-
? "BLI Memory would be registered (dry run; nothing was written)."
|
|
92
|
-
: outcome.status === "missing"
|
|
93
|
-
? "BLI Memory is not registered on this machine."
|
|
94
|
-
: outcome.status === "skipped"
|
|
95
|
-
? "BLI Memory was not registered and nothing was written: the bli-memory-mcp server is not on this machine yet."
|
|
96
|
-
: `BLI Memory is not fully registered: ${outcome.reason}.`;
|
|
86
|
+
const headline = memoryOutcomeHeadline(outcome);
|
|
97
87
|
const lines = [headline, ` ${memoryReceiptLine(outcome.receipt)}`];
|
|
98
88
|
// BLI-3884. Only `status` asks; an absent reading is not printed as "no
|
|
99
89
|
// daemon", because nothing looked.
|
|
@@ -115,4 +105,19 @@ export function memoryOutcomeLines(outcome) {
|
|
|
115
105
|
lines.push(` ${target.target}: ${target.status} (${target.reason})${where}${detail}`);
|
|
116
106
|
}
|
|
117
107
|
return lines;
|
|
108
|
+
}
|
|
109
|
+
function memoryOutcomeHeadline(outcome) {
|
|
110
|
+
if (outcome.status === "installed")
|
|
111
|
+
return "BLI Memory registered on this machine.";
|
|
112
|
+
if (outcome.status === "already")
|
|
113
|
+
return "BLI Memory is already registered on this machine.";
|
|
114
|
+
if (outcome.status === "would_install") {
|
|
115
|
+
return "BLI Memory would be registered (dry run; nothing was written).";
|
|
116
|
+
}
|
|
117
|
+
if (outcome.status === "missing")
|
|
118
|
+
return "BLI Memory is not registered on this machine.";
|
|
119
|
+
if (outcome.status === "skipped") {
|
|
120
|
+
return "BLI Memory was not registered and nothing was written: the bli-memory-mcp server is not on this machine yet.";
|
|
121
|
+
}
|
|
122
|
+
return `BLI Memory is not fully registered: ${outcome.reason}.`;
|
|
118
123
|
}
|
|
@@ -13,16 +13,6 @@
|
|
|
13
13
|
* install is idempotent to the byte and a drifted copy is replaced rather than
|
|
14
14
|
* merged — the same deal `cockpit agent-rules` offers for its managed block.
|
|
15
15
|
*/
|
|
16
|
-
/** Relative path → exact file contents. The map IS the install. */
|
|
17
|
-
export function memoryCodexSkillFiles() {
|
|
18
|
-
return {
|
|
19
|
-
"SKILL.md": SKILL_MD,
|
|
20
|
-
"references/search.md": SEARCH_MD,
|
|
21
|
-
"references/save.md": SAVE_MD,
|
|
22
|
-
"references/update.md": UPDATE_MD,
|
|
23
|
-
"references/forget.md": FORGET_MD,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
16
|
const SKILL_MD = `---
|
|
27
17
|
name: bli-memory
|
|
28
18
|
description: BLI Memory — durable memory for this machine. Use when you need to recall what was decided before, or when a session produced a durable decision, preference or correction worth keeping.
|
|
@@ -118,4 +108,14 @@ again. Matching by content is exact after whitespace normalisation — never
|
|
|
118
108
|
fuzzy, and never widened to every container.
|
|
119
109
|
|
|
120
110
|
It returns what it removed. A bare "done" is not an answer.
|
|
121
|
-
`;
|
|
111
|
+
`;
|
|
112
|
+
/** Relative path → exact file contents. The map IS the install. */
|
|
113
|
+
export function memoryCodexSkillFiles() {
|
|
114
|
+
return {
|
|
115
|
+
"SKILL.md": SKILL_MD,
|
|
116
|
+
"references/search.md": SEARCH_MD,
|
|
117
|
+
"references/save.md": SAVE_MD,
|
|
118
|
+
"references/update.md": UPDATE_MD,
|
|
119
|
+
"references/forget.md": FORGET_MD,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -5,20 +5,37 @@ import { callTower, openTower } from "./tower-command.js";
|
|
|
5
5
|
function sender(command, io) {
|
|
6
6
|
return async (entry) => {
|
|
7
7
|
const tower = await openTower("memory log", command, io);
|
|
8
|
-
const result = await callTower(tower, {
|
|
9
|
-
|
|
8
|
+
const result = await callTower(tower, {
|
|
9
|
+
path: "/api/memory/experience",
|
|
10
|
+
method: "POST",
|
|
11
|
+
body: entry,
|
|
12
|
+
label: "memory experience",
|
|
13
|
+
timeoutMs: 5000,
|
|
14
|
+
});
|
|
15
|
+
return result.ok
|
|
16
|
+
? {
|
|
17
|
+
ok: result.body?.ok === true,
|
|
18
|
+
reason: "experience_not_acknowledged",
|
|
19
|
+
}
|
|
20
|
+
: result;
|
|
10
21
|
};
|
|
11
22
|
}
|
|
12
23
|
export async function runMemoryLog(command, io) {
|
|
13
|
-
const reason = command.reasonStdin
|
|
24
|
+
const reason = command.reasonStdin
|
|
25
|
+
? (await readPipedText(io.stdin, { maxChars: 1002 })).trim()
|
|
26
|
+
: command.reason;
|
|
14
27
|
validateExperience(command.store, command.verdict, reason);
|
|
15
28
|
const entry = await appendExperience({ store: command.store, verdict: command.verdict, reason }, {
|
|
16
|
-
homeDir: command.homeDir,
|
|
29
|
+
homeDir: command.homeDir,
|
|
30
|
+
agent: "cockpit",
|
|
31
|
+
project: path.basename(process.cwd()),
|
|
17
32
|
});
|
|
18
33
|
const delivery = await shipExperience(entry, sender(command, io), command.homeDir);
|
|
19
34
|
const receipt = { ok: true, id: entry.id, local: true, ...delivery };
|
|
20
35
|
writeLine(io.stderr, `[memory experience] recorded ${JSON.stringify(receipt)}`);
|
|
21
|
-
writeLine(io.stdout, command.json
|
|
36
|
+
writeLine(io.stdout, command.json
|
|
37
|
+
? JSON.stringify(receipt)
|
|
38
|
+
: `Experience appended; shipped: ${delivery.shipped} (${delivery.reason}).`);
|
|
22
39
|
return 0;
|
|
23
40
|
}
|
|
24
41
|
export async function runMemoryExperienceAfterSync(command, io) {
|
package/dist/commands/msg.js
CHANGED
|
@@ -220,16 +220,23 @@ async function createChannel(command, door) {
|
|
|
220
220
|
const membersAdded = body.members_added ?? [];
|
|
221
221
|
const membersFailed = body.members_failed ?? [];
|
|
222
222
|
const requestedMembers = command.memberEmails?.length ?? 0;
|
|
223
|
+
logChannelCreation(door, channel, command.isPrivate, requestedMembers, membersAdded, membersFailed);
|
|
224
|
+
if (door.json) {
|
|
225
|
+
return emitAgentDoor(door, { ok: true, channel, membersAdded, membersFailed });
|
|
226
|
+
}
|
|
227
|
+
reportChannelCreationToHuman(door, channel, name, requestedMembers, membersAdded, membersFailed);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
function logChannelCreation(door, channel, isPrivate, requestedMembers, membersAdded, membersFailed) {
|
|
223
231
|
writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({
|
|
224
232
|
channel_id: channel?.id ?? null,
|
|
225
|
-
is_private:
|
|
233
|
+
is_private: isPrivate,
|
|
226
234
|
members_requested: requestedMembers,
|
|
227
235
|
members_added: membersAdded.length,
|
|
228
236
|
members_failed: membersFailed.length,
|
|
229
237
|
})}`);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
238
|
+
}
|
|
239
|
+
function reportChannelCreationToHuman(door, channel, name, requestedMembers, membersAdded, membersFailed) {
|
|
233
240
|
writeLine(door.io.stdout, `Created #${channel?.name ?? name} (${channel?.id ?? "?"}).`);
|
|
234
241
|
if (membersAdded.length > 0) {
|
|
235
242
|
writeLine(door.io.stdout, `${membersAdded.length} member(s) added.`);
|
|
@@ -246,7 +253,6 @@ async function createChannel(command, door) {
|
|
|
246
253
|
for (const failure of membersFailed) {
|
|
247
254
|
writeLine(door.io.stdout, `Not added (${failure.reason}): ${failure.userId}`);
|
|
248
255
|
}
|
|
249
|
-
return 0;
|
|
250
256
|
}
|
|
251
257
|
async function openDm(command, door) {
|
|
252
258
|
const email = command.dmEmail ?? "";
|
|
@@ -57,6 +57,13 @@ export async function pairForOnboarding(command, input, installEvents, io) {
|
|
|
57
57
|
repoRoot: input.primaryRoot,
|
|
58
58
|
branch: command.branch,
|
|
59
59
|
});
|
|
60
|
+
if (await reuseInstalledOnboardSession(command, input, installedStatus, installEvents, io)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
await logOutMismatchedOnboardSession(command, installedStatus, io);
|
|
64
|
+
return await pairOnboardDevice(command, input, installEvents, io);
|
|
65
|
+
}
|
|
66
|
+
async function reuseInstalledOnboardSession(command, input, installedStatus, installEvents, io) {
|
|
60
67
|
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
61
68
|
const canReuseInstalledSession = canReuseOnboardSession(installedSession, input.claimedOwnerEmail, command.dashboardUrl);
|
|
62
69
|
if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
|
|
@@ -65,14 +72,19 @@ export async function pairForOnboarding(command, input, installEvents, io) {
|
|
|
65
72
|
if (!command.json) {
|
|
66
73
|
writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
|
|
67
74
|
}
|
|
68
|
-
return
|
|
75
|
+
return true;
|
|
69
76
|
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
async function logOutMismatchedOnboardSession(command, installedStatus, io) {
|
|
70
80
|
if (installedStatus.session_state === "valid") {
|
|
71
81
|
await logoutLocalCollector({ homeDir: command.homeDir });
|
|
72
82
|
if (!command.json) {
|
|
73
83
|
writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
|
|
74
84
|
}
|
|
75
85
|
}
|
|
86
|
+
}
|
|
87
|
+
async function pairOnboardDevice(command, input, installEvents, io) {
|
|
76
88
|
// BLI-3731: one sign-in first. The link both signs the browser in and
|
|
77
89
|
// finishes the pairing, so nobody reads a second email. Every other arm
|
|
78
90
|
// below is the fallback, and it runs unchanged when this one cannot finish.
|
|
@@ -89,6 +101,9 @@ export async function pairForOnboarding(command, input, installEvents, io) {
|
|
|
89
101
|
return linked;
|
|
90
102
|
}
|
|
91
103
|
}
|
|
104
|
+
return await pairOnboardDeviceWithAuthFallback(command, input, installEvents, io);
|
|
105
|
+
}
|
|
106
|
+
async function pairOnboardDeviceWithAuthFallback(command, input, installEvents, io) {
|
|
92
107
|
const authResult = await requestPairingAccessTokenDetailed({
|
|
93
108
|
dashboardUrl: command.dashboardUrl,
|
|
94
109
|
email: input.claimedOwnerEmail,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { asRecord, callTower } from "./tower-command.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
export function turnAggregateSection(payload) {
|
|
4
|
+
const section = payload.turns ?? null;
|
|
5
|
+
return { section, reason: section ? "ok" : "turns_section_absent" };
|
|
6
|
+
}
|
|
7
|
+
export async function readToolRouterBoard(io, tower) {
|
|
8
|
+
const result = await callTower(tower, { path: "/api/ops/tool-router", label: "ops-tool-router" });
|
|
9
|
+
if (!result.ok) {
|
|
10
|
+
writeLine(io.stderr, `[ops cli] tool router not read ${JSON.stringify({ reason: result.reason, http_status: result.httpStatus ?? null })}`);
|
|
11
|
+
return { board: null, reason: result.reason };
|
|
12
|
+
}
|
|
13
|
+
const board = asRecord(result.body).board ?? null;
|
|
14
|
+
return { board, reason: board ? "ok" : "tool_router_section_absent" };
|
|
15
|
+
}
|
|
16
|
+
export function turnAggregateLine(section) {
|
|
17
|
+
return `TURNS ${section.turns ?? 0} turns / ${section.windowDays ?? 30}d · wall p50 ${section.wallTimeMs?.p50 ?? "n/a"}ms · p95 ${section.wallTimeMs?.p95 ?? "n/a"}ms · ${section.completion ?? "unknown"}`;
|
|
18
|
+
}
|
|
19
|
+
export function toolRouterLine(board) {
|
|
20
|
+
const rate = board.agreementRate === null || board.agreementRate === undefined
|
|
21
|
+
? "n/a"
|
|
22
|
+
: `${(board.agreementRate * 100).toFixed(1)}%`;
|
|
23
|
+
return `TOOL ROUTER ${board.turnsRouted ?? 0} routed · agreement ${rate} · p50 ${board.p50LatencyMs ?? "n/a"}ms${board.capped ? " · capped" : ""}${board.readError ? ` · ${board.readError}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The adoption gauge, read through its own door.
|
|
27
|
+
*
|
|
28
|
+
* Never fails the board: a `cockpit ops --memory` whose memory half is
|
|
29
|
+
* unreadable still prints the pipelines, the fleet and the coverage, and says
|
|
30
|
+
* in one line which half is missing and why. The status board's exit code is
|
|
31
|
+
* about collection health, and adoption is not that.
|
|
32
|
+
*/
|
|
33
|
+
export async function readMemoryUsage(command, io, tower) {
|
|
34
|
+
const params = new URLSearchParams();
|
|
35
|
+
if (command.memoryDays !== undefined)
|
|
36
|
+
params.set("days", String(command.memoryDays));
|
|
37
|
+
const query = params.toString();
|
|
38
|
+
const result = await callTower(tower, {
|
|
39
|
+
path: `/api/ops/memory-usage${query ? `?${query}` : ""}`,
|
|
40
|
+
label: "ops-memory-usage",
|
|
41
|
+
});
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
writeLine(io.stderr, `[ops cli] memory usage not read ${JSON.stringify({
|
|
44
|
+
reason: result.reason,
|
|
45
|
+
http_status: result.httpStatus ?? null,
|
|
46
|
+
})}`);
|
|
47
|
+
return { section: null, hooks: null, experience: null, reason: result.reason };
|
|
48
|
+
}
|
|
49
|
+
const body = asRecord(result.body);
|
|
50
|
+
// BLI-3788: the hook counts ride the same answer, and their absence is a
|
|
51
|
+
// fact about the SERVER's version rather than about this fleet — an older
|
|
52
|
+
// dashboard sends no `hooks` key, and printing nothing is right there.
|
|
53
|
+
if (!body.memory)
|
|
54
|
+
return {
|
|
55
|
+
section: null,
|
|
56
|
+
hooks: body.hooks ?? null,
|
|
57
|
+
experience: body.experience ?? null,
|
|
58
|
+
reason: "memory_section_absent",
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
section: body.memory,
|
|
62
|
+
hooks: body.hooks ?? null,
|
|
63
|
+
experience: body.experience ?? null,
|
|
64
|
+
reason: "ok",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The per-model board, read through its own door (BLI-3912).
|
|
69
|
+
*
|
|
70
|
+
* Same posture as the adoption gauge next door: it never fails the status
|
|
71
|
+
* board, and when it cannot be read the reason is printed where the section
|
|
72
|
+
* would have been. The LINES come from the server, so a terminal and the
|
|
73
|
+
* dashboard cannot disagree about what a model cost today.
|
|
74
|
+
*/
|
|
75
|
+
export async function readModelBoardLines(io, tower) {
|
|
76
|
+
const result = await callTower(tower, { path: "/api/ops/models", label: "ops-models" });
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
writeLine(io.stderr, `[ops cli] model board not read ${JSON.stringify({
|
|
79
|
+
reason: result.reason,
|
|
80
|
+
http_status: result.httpStatus ?? null,
|
|
81
|
+
})}`);
|
|
82
|
+
return { lines: [], board: null, reason: result.reason };
|
|
83
|
+
}
|
|
84
|
+
const body = asRecord(result.body);
|
|
85
|
+
if (!body.lines || body.lines.length === 0) {
|
|
86
|
+
return { lines: [], board: body.board ?? null, reason: "models_section_absent" };
|
|
87
|
+
}
|
|
88
|
+
return { lines: body.lines, board: body.board ?? null, reason: "ok" };
|
|
89
|
+
}
|