@bli-cockpit/cli 0.2.126 → 0.2.128
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-memory.js +6 -0
- package/dist/commands/local-args-tower-settings.js +10 -4
- package/dist/commands/local-discovery.js +80 -26
- package/dist/commands/local-help-commands.js +1 -1
- package/dist/commands/memory-install-skills.js +1 -0
- package/dist/commands/memory-store.js +2 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sessions.js +8 -1
- package/dist/commands/settings.js +1 -0
- package/dist/commands/status.js +17 -2
- package/dist/upload-agent-artifacts.js +33 -13
- package/dist/upload-http.js +27 -7
- package/package.json +3 -3
|
@@ -44,6 +44,7 @@ const ALLOWED_FLAGS = [
|
|
|
44
44
|
"--limit",
|
|
45
45
|
"--no-recent",
|
|
46
46
|
"--custom-id",
|
|
47
|
+
"--alias",
|
|
47
48
|
"--extract",
|
|
48
49
|
"--file",
|
|
49
50
|
"--content-stdin",
|
|
@@ -58,6 +59,7 @@ const VALUE_FLAGS = [
|
|
|
58
59
|
"--container",
|
|
59
60
|
"--limit",
|
|
60
61
|
"--custom-id",
|
|
62
|
+
"--alias",
|
|
61
63
|
"--file",
|
|
62
64
|
"--reason",
|
|
63
65
|
];
|
|
@@ -71,6 +73,9 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
71
73
|
if (action !== "search" && (values.flags.has("--limit") || values.booleans.has("--no-recent"))) {
|
|
72
74
|
throw new Error("--limit and --no-recent only apply to memory search.");
|
|
73
75
|
}
|
|
76
|
+
if (action !== "save" && values.flags.has("--alias")) {
|
|
77
|
+
throw new Error("--alias only applies to memory save.");
|
|
78
|
+
}
|
|
74
79
|
if (action !== "save" && (values.flags.has("--custom-id") || values.booleans.has("--extract"))) {
|
|
75
80
|
throw new Error("--custom-id and --extract only apply to memory save.");
|
|
76
81
|
}
|
|
@@ -108,6 +113,7 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
108
113
|
...command,
|
|
109
114
|
action,
|
|
110
115
|
customId: optionalNonEmpty(values.flags.get("--custom-id")),
|
|
116
|
+
...(values.flagValues.has("--alias") ? { aliases: values.flagValues.get("--alias") } : {}),
|
|
111
117
|
extract: values.booleans.has("--extract"),
|
|
112
118
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
113
119
|
};
|
|
@@ -24,6 +24,7 @@ export function parseSettingsArgs(args) {
|
|
|
24
24
|
"--brief-model",
|
|
25
25
|
"--chat",
|
|
26
26
|
"--memory",
|
|
27
|
+
"--note-reader",
|
|
27
28
|
"--project",
|
|
28
29
|
"--file",
|
|
29
30
|
"--id",
|
|
@@ -37,6 +38,7 @@ export function parseSettingsArgs(args) {
|
|
|
37
38
|
"--brief-model",
|
|
38
39
|
"--chat",
|
|
39
40
|
"--memory",
|
|
41
|
+
"--note-reader",
|
|
40
42
|
"--project",
|
|
41
43
|
"--file",
|
|
42
44
|
"--id",
|
|
@@ -124,18 +126,21 @@ function parseCliFloorSection(rest, common) {
|
|
|
124
126
|
}
|
|
125
127
|
return { kind: "settings", section: "cli-floor", action: "set", floorVersion: version, ...common };
|
|
126
128
|
}
|
|
127
|
-
/** `settings models` or `settings models set --chat <key> / --memory <id>`. */
|
|
129
|
+
/** `settings models` or `settings models set --chat <key> / --memory <id> / --note-reader <id>`. */
|
|
128
130
|
function parseModelsSection(rest, flags, common) {
|
|
129
131
|
if (rest.length === 0) {
|
|
130
132
|
return { kind: "settings", section: "models", action: "show", ...common };
|
|
131
133
|
}
|
|
132
134
|
if (rest[0] !== "set" || rest.length > 1) {
|
|
133
|
-
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
135
|
+
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>` / `set --note-reader <id>`.");
|
|
134
136
|
}
|
|
135
137
|
const orgChatModel = optionalNonEmpty(flags.get("--chat"));
|
|
136
138
|
const orgMemoryModel = optionalNonEmpty(flags.get("--memory"));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
// BLI-4484: the note reader is the third org-wide slot; the web page could
|
|
140
|
+
// set it since BLI-4045 and the CLI could not, which is the twin rule broken.
|
|
141
|
+
const orgNoteReaderModel = optionalNonEmpty(flags.get("--note-reader"));
|
|
142
|
+
if (!orgChatModel && !orgMemoryModel && !orgNoteReaderModel) {
|
|
143
|
+
throw new Error("settings models set needs --chat <key>, --memory <id>, --note-reader <id>, or any of them.");
|
|
139
144
|
}
|
|
140
145
|
return {
|
|
141
146
|
kind: "settings",
|
|
@@ -143,6 +148,7 @@ function parseModelsSection(rest, flags, common) {
|
|
|
143
148
|
action: "set",
|
|
144
149
|
orgChatModel,
|
|
145
150
|
orgMemoryModel,
|
|
151
|
+
orgNoteReaderModel,
|
|
146
152
|
...common,
|
|
147
153
|
};
|
|
148
154
|
}
|
|
@@ -1,37 +1,15 @@
|
|
|
1
1
|
import { writeLine } from "./cli-io.js";
|
|
2
2
|
import { resolveDiscoveryLimits, saveDiscoveryLimits, } from "../discovery-limits.js";
|
|
3
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
3
4
|
import { discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
4
5
|
export async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
5
6
|
// What the operator typed this run, else what they typed some previous run,
|
|
6
7
|
// else the built-in defaults (BLI-2362).
|
|
7
8
|
const limits = await resolveDiscoveryLimits(discovery, discovery.homeDir);
|
|
8
|
-
const
|
|
9
|
-
const roots =
|
|
10
|
-
|
|
11
|
-
: [repoRoot ?? process.cwd()];
|
|
12
|
-
const result = await discoverGitWorktreesInRootsWithStatus(roots, {
|
|
13
|
-
maxDepth: limits.maxDepth,
|
|
14
|
-
maxWorktrees,
|
|
15
|
-
});
|
|
16
|
-
if (io && result.unreadable_dirs.length > 0) {
|
|
17
|
-
// Never silently dropped: anything under these folders is missing from the
|
|
18
|
-
// scan, so say so even when the run otherwise succeeds.
|
|
19
|
-
writeLine(io.stderr, unreadableDirectoriesMessage(result.unreadable_dirs));
|
|
20
|
-
}
|
|
9
|
+
const askedForRoots = repoRoot === undefined ? undefined : Array.isArray(repoRoot) ? repoRoot : [repoRoot];
|
|
10
|
+
const roots = askedForRoots ?? [process.cwd()];
|
|
11
|
+
const result = await scanRootsReportingWhatItMissed(roots, limits, discovery, io);
|
|
21
12
|
const worktrees = result.worktrees;
|
|
22
|
-
if (!result.complete) {
|
|
23
|
-
const message = incompleteDiscoveryMessage({
|
|
24
|
-
result,
|
|
25
|
-
roots,
|
|
26
|
-
maxDepth: limits.maxDepth,
|
|
27
|
-
maxRepos: maxWorktrees,
|
|
28
|
-
found: worktrees.length,
|
|
29
|
-
});
|
|
30
|
-
if (io)
|
|
31
|
-
writeLine(io.stderr, message);
|
|
32
|
-
discovery.onPartial?.(message);
|
|
33
|
-
console.error("[local-discovery] partial collection", JSON.stringify({ reason: result.incomplete_reasons.join(","), roots: roots.length, incomplete_roots: result.incomplete_roots.length, max_depth: limits.maxDepth, found: worktrees.length }));
|
|
34
|
-
}
|
|
35
13
|
if (worktrees.length === 0 && result.complete && !discovery.allowEmpty) {
|
|
36
14
|
// "No git repos found" is only true when the scan could actually SEE
|
|
37
15
|
// everywhere it looked. With unreadable folders in hand, the sentence sent
|
|
@@ -53,10 +31,86 @@ export async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
|
53
31
|
"Fix the permissions on those folders and run the command again, or point --workspace at the project folders you can read.",
|
|
54
32
|
].join("\n"));
|
|
55
33
|
}
|
|
34
|
+
// An explicit --workspace is an instruction about WHERE to look, so an
|
|
35
|
+
// empty answer there is the answer. Without one, the folder the operator
|
|
36
|
+
// stood in was a guess, and this machine already knows a better one — for
|
|
37
|
+
// the commands that asked for it (BLI-4467).
|
|
38
|
+
if (askedForRoots === undefined && discovery.fallbackToSavedRoots) {
|
|
39
|
+
return await discoverInSavedCollectionRoots(limits, discovery, io);
|
|
40
|
+
}
|
|
56
41
|
throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
|
|
57
42
|
}
|
|
58
43
|
return worktrees;
|
|
59
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The second look: this machine's approved collection roots, scanned with the
|
|
47
|
+
* same limits and the same partial/unreadable reporting as the first. Refuses
|
|
48
|
+
* only when there are no approved roots to read, because then the machine
|
|
49
|
+
* genuinely has nothing to point the operator at (BLI-4467).
|
|
50
|
+
*/
|
|
51
|
+
async function discoverInSavedCollectionRoots(limits, discovery, io) {
|
|
52
|
+
const savedRoots = await readSavedCollectionRoots(discovery.homeDir);
|
|
53
|
+
if (savedRoots.length === 0) {
|
|
54
|
+
console.error("[local-discovery] cwd holds no repos and this machine has no saved collection roots", JSON.stringify({ reason: "no_saved_roots" }));
|
|
55
|
+
throw new Error([
|
|
56
|
+
"No git repos found here and this machine has no approved collection roots.",
|
|
57
|
+
"Run `cockpit doctor` to set them up, or `cockpit roots add <dir>`.",
|
|
58
|
+
].join("\n"));
|
|
59
|
+
}
|
|
60
|
+
console.error("[local-discovery] cwd holds no repos, using the saved collection roots", JSON.stringify({ reason: "roots_fallback", roots: savedRoots.length }));
|
|
61
|
+
if (io)
|
|
62
|
+
writeLine(io.stderr, "No git repo here; reading the machine's approved roots.");
|
|
63
|
+
const result = await scanRootsReportingWhatItMissed(savedRoots, limits, discovery, io);
|
|
64
|
+
console.error("[local-discovery] saved collection roots scanned", JSON.stringify({
|
|
65
|
+
reason: "roots_fallback_result",
|
|
66
|
+
roots: savedRoots.length,
|
|
67
|
+
found: result.worktrees.length,
|
|
68
|
+
complete: result.complete,
|
|
69
|
+
}));
|
|
70
|
+
return result.worktrees;
|
|
71
|
+
}
|
|
72
|
+
/** The approved roots this machine already agreed to collect, or none if the config cannot be read. */
|
|
73
|
+
async function readSavedCollectionRoots(homeDir) {
|
|
74
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
75
|
+
const config = await readLocalCollectorConfig(paths).catch((error) => {
|
|
76
|
+
console.error("[local-discovery] could not read the saved collection roots", JSON.stringify({
|
|
77
|
+
reason: error.code ?? "config_unreadable",
|
|
78
|
+
}));
|
|
79
|
+
return null;
|
|
80
|
+
});
|
|
81
|
+
return config?.default_repo_paths ?? [];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One scan plus the two things a scan owes the operator: the folders it could
|
|
85
|
+
* not open, and the coverage it could not finish. Shared so the fallback scan
|
|
86
|
+
* reports exactly what the first one does.
|
|
87
|
+
*/
|
|
88
|
+
async function scanRootsReportingWhatItMissed(roots, limits, discovery, io) {
|
|
89
|
+
const maxWorktrees = limits.maxRepos;
|
|
90
|
+
const result = await discoverGitWorktreesInRootsWithStatus(roots, {
|
|
91
|
+
maxDepth: limits.maxDepth,
|
|
92
|
+
maxWorktrees,
|
|
93
|
+
});
|
|
94
|
+
if (io && result.unreadable_dirs.length > 0) {
|
|
95
|
+
// Never silently dropped: anything under these folders is missing from the
|
|
96
|
+
// scan, so say so even when the run otherwise succeeds.
|
|
97
|
+
writeLine(io.stderr, unreadableDirectoriesMessage(result.unreadable_dirs));
|
|
98
|
+
}
|
|
99
|
+
if (!result.complete) {
|
|
100
|
+
const message = incompleteDiscoveryMessage({
|
|
101
|
+
result,
|
|
102
|
+
roots,
|
|
103
|
+
maxDepth: limits.maxDepth,
|
|
104
|
+
maxRepos: maxWorktrees,
|
|
105
|
+
found: result.worktrees.length,
|
|
106
|
+
});
|
|
107
|
+
if (io)
|
|
108
|
+
writeLine(io.stderr, message);
|
|
109
|
+
discovery.onPartial?.(message);
|
|
110
|
+
console.error("[local-discovery] partial collection", JSON.stringify({ reason: result.incomplete_reasons.join(","), roots: roots.length, incomplete_roots: result.incomplete_roots.length, max_depth: limits.maxDepth, found: result.worktrees.length }));
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
60
114
|
/**
|
|
61
115
|
* Persists `--max-depth` / `--max-repos` when a command carried them, so the
|
|
62
116
|
* number survives into the background sync and the doctor's own sync — neither
|
|
@@ -156,7 +156,7 @@ export function localSubcommandHelp(command) {
|
|
|
156
156
|
" Your teams and your two model choices. No flags = show.",
|
|
157
157
|
" cockpit settings switches [set <key> <value>]",
|
|
158
158
|
" Access and feature switches. Everyone may look; only an admin may set.",
|
|
159
|
-
" cockpit settings models [set --chat <key>] [--memory <id>]",
|
|
159
|
+
" cockpit settings models [set --chat <key>] [--memory <id>] [--note-reader <id>]",
|
|
160
160
|
" The ORG-WIDE model map. Admin only, in both directions.",
|
|
161
161
|
" cockpit settings cli-floor [<version>]",
|
|
162
162
|
" The fleet forced-update floor: every collector below it updates on its",
|
|
@@ -67,6 +67,7 @@ const SAVE_MD = `# save_memory
|
|
|
67
67
|
|
|
68
68
|
\`\`\`
|
|
69
69
|
in : { content: string (1-200000, required),
|
|
70
|
+
aliases?: string[] (up to 16 alternate names of 64 characters),
|
|
70
71
|
containerTag?: string,
|
|
71
72
|
customId?: string, // e.g. the session id — the upsert key
|
|
72
73
|
metadata?: object }
|
|
@@ -100,6 +100,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
100
100
|
"The experience log:",
|
|
101
101
|
" cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
|
|
102
102
|
"",
|
|
103
|
+
"Save accepts repeated --alias names (up to 16 names of 64 characters).",
|
|
103
104
|
"Omit --container and this repository's own memory space is used — the same",
|
|
104
105
|
"space the bli-memory MCP server and its hooks read and write.",
|
|
105
106
|
]) {
|
|
@@ -184,6 +185,7 @@ async function saveMemory(command, door) {
|
|
|
184
185
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
185
186
|
body: {
|
|
186
187
|
content: body.text,
|
|
188
|
+
...(command.aliases === undefined ? {} : { aliases: command.aliases }),
|
|
187
189
|
containerTag,
|
|
188
190
|
...(command.customId ? { customId: command.customId } : {}),
|
|
189
191
|
mode: command.extract ? "extract" : "verbatim",
|
|
@@ -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.128");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -19,7 +19,14 @@ const SESSION_SCAN_OVERRIDE_LIMIT = 10_000;
|
|
|
19
19
|
export async function runSessions(command, io) {
|
|
20
20
|
const now = new Date();
|
|
21
21
|
const homeDir = command.homeDir ?? os.homedir();
|
|
22
|
-
const worktrees = await discoverCommandWorktrees(command.repoRoot, {
|
|
22
|
+
const worktrees = await discoverCommandWorktrees(command.repoRoot, {
|
|
23
|
+
maxDepth: command.maxDepth,
|
|
24
|
+
maxRepos: command.maxRepos,
|
|
25
|
+
homeDir: command.homeDir,
|
|
26
|
+
// BLI-4467: a read-only question about what this machine has observed,
|
|
27
|
+
// so the approved roots answer it from any folder.
|
|
28
|
+
fallbackToSavedRoots: true,
|
|
29
|
+
}, io);
|
|
23
30
|
const window = await sessionsScanWindow(command, now);
|
|
24
31
|
const { codex, claude } = await scanSessionSources(command, homeDir, worktrees, now, window);
|
|
25
32
|
const { codexRows, claudeRows } = sessionRows(codex, claude);
|
|
@@ -187,6 +187,7 @@ async function setModels(command, tower, io) {
|
|
|
187
187
|
body: {
|
|
188
188
|
...(command.orgChatModel ? { chatModel: parseModelKey(command.orgChatModel) } : {}),
|
|
189
189
|
...(command.orgMemoryModel ? { memoryModel: command.orgMemoryModel } : {}),
|
|
190
|
+
...(command.orgNoteReaderModel ? { noteReaderModel: command.orgNoteReaderModel } : {}),
|
|
190
191
|
},
|
|
191
192
|
});
|
|
192
193
|
if (!result.ok)
|
package/dist/commands/status.js
CHANGED
|
@@ -30,11 +30,26 @@ import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
|
30
30
|
export async function runStatus(command, io) {
|
|
31
31
|
const backfillCursor = await inspectBackfillCursor(command.homeDir);
|
|
32
32
|
const setupReceipt = await readMachineSetupReceipt(command.homeDir, io);
|
|
33
|
-
const worktrees = await discoverCommandWorktrees(command.repoRoot, {
|
|
33
|
+
const worktrees = await discoverCommandWorktrees(command.repoRoot, {
|
|
34
|
+
maxDepth: command.maxDepth,
|
|
35
|
+
maxRepos: command.maxRepos,
|
|
36
|
+
homeDir: command.homeDir,
|
|
37
|
+
// BLI-4467: this command answers "what does this machine believe about
|
|
38
|
+
// itself", so standing in a folder with no repo is not a reason to
|
|
39
|
+
// refuse — the approved roots are the better answer.
|
|
40
|
+
fallbackToSavedRoots: true,
|
|
41
|
+
}, io);
|
|
34
42
|
if (worktrees.length > 1) {
|
|
35
43
|
return writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, worktrees);
|
|
36
44
|
}
|
|
37
|
-
|
|
45
|
+
// BLI-4467: with no --workspace, the folder was a guess and discovery is the
|
|
46
|
+
// one that knows which worktree this is about — the fallback above may have
|
|
47
|
+
// found it under an approved root, nowhere near the cwd. Describing the guess
|
|
48
|
+
// printed `repo: <the empty folder>` and `branch: unknown` while exiting 0.
|
|
49
|
+
const single = command.repoRoot === undefined && worktrees.length === 1
|
|
50
|
+
? { ...command, repoRoot: worktrees[0].repo_root }
|
|
51
|
+
: command;
|
|
52
|
+
return writeSingleRepoStatus(single, io, backfillCursor, setupReceipt);
|
|
38
53
|
}
|
|
39
54
|
async function readMachineSetupReceipt(homeDir, io) {
|
|
40
55
|
// BLI-3731: "is this machine actually connected?" belongs beside "is it
|
|
@@ -40,20 +40,26 @@ export async function reportAgentImageArtifacts(options) {
|
|
|
40
40
|
// Both non-2xx branches returned a label and said nothing (BLI-3483). An
|
|
41
41
|
// image that never gets reported is never redacted and never OCR'd, and
|
|
42
42
|
// the only place that fact existed was a return value the sync discards.
|
|
43
|
-
if (response.status === 404) {
|
|
44
|
-
console.error("[agent-artifacts] the dashboard has no agent-artifact route; these images stay unreported", JSON.stringify({
|
|
45
|
-
reason: "agent_artifact_api_unavailable",
|
|
46
|
-
http_status: 404,
|
|
47
|
-
artifact_count: artifacts.length,
|
|
48
|
-
}));
|
|
49
|
-
return {
|
|
50
|
-
posted: false,
|
|
51
|
-
reason: "agent_artifact_api_unavailable",
|
|
52
|
-
recorded_count: 0,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
43
|
if (!response.ok) {
|
|
56
|
-
const
|
|
44
|
+
const errorBody = await readResponseJson(response);
|
|
45
|
+
// A 404 is two different facts. "No such route" is the platform
|
|
46
|
+
// answering with an empty or HTML body; a JSON error envelope means the
|
|
47
|
+
// route answered and refused, and calling that "the dashboard has no
|
|
48
|
+
// agent-artifact route" hid a real refusal behind a shrug for as long as
|
|
49
|
+
// the door rejected ambient tickets.
|
|
50
|
+
if (response.status === 404 && !routeAnswered(errorBody)) {
|
|
51
|
+
console.error("[agent-artifacts] nothing answered at the agent-artifact path; these images stay unreported", JSON.stringify({
|
|
52
|
+
reason: "agent_artifact_api_unavailable",
|
|
53
|
+
http_status: 404,
|
|
54
|
+
artifact_count: artifacts.length,
|
|
55
|
+
}));
|
|
56
|
+
return {
|
|
57
|
+
posted: false,
|
|
58
|
+
reason: "agent_artifact_api_unavailable",
|
|
59
|
+
recorded_count: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const serverReason = serverFailureDetail(errorBody);
|
|
57
63
|
console.error("[agent-artifacts] report refused", JSON.stringify({
|
|
58
64
|
reason: "report_failed",
|
|
59
65
|
http_status: response.status,
|
|
@@ -99,6 +105,20 @@ export async function reportAgentImageArtifacts(options) {
|
|
|
99
105
|
return { posted: false, reason: "report_network_error", recorded_count: 0 };
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Did the route answer, or did something in front of it?
|
|
110
|
+
*
|
|
111
|
+
* The dashboard's refusals are always a JSON envelope carrying a `code` (and
|
|
112
|
+
* sometimes a `reason`); a missing route answers with an empty body or a
|
|
113
|
+
* platform HTML page, which `readResponseJson` hands back as `{ message }`.
|
|
114
|
+
* That difference is the whole reason a 404 can mean two things.
|
|
115
|
+
*/
|
|
116
|
+
function routeAnswered(body) {
|
|
117
|
+
if (!body || typeof body !== "object")
|
|
118
|
+
return false;
|
|
119
|
+
const record = body;
|
|
120
|
+
return (typeof record["code"] === "string" || typeof record["reason"] === "string");
|
|
121
|
+
}
|
|
102
122
|
/**
|
|
103
123
|
* The images this sync made durable, from both directions.
|
|
104
124
|
*
|
package/dist/upload-http.js
CHANGED
|
@@ -39,11 +39,19 @@ export async function readResponseJson(response) {
|
|
|
39
39
|
* The server's own reason for refusing, as something safe to log.
|
|
40
40
|
*
|
|
41
41
|
* The dashboard's error envelopes answer `{ code, message, reason? }`. A
|
|
42
|
-
* `reason`/`code` that already looks like a label
|
|
43
|
-
*
|
|
44
|
-
* MB" reaches an operator instead of "upload failed" (BLI-2528).
|
|
45
|
-
*
|
|
46
|
-
* is
|
|
42
|
+
* `reason`/`code` that already looks like a label is the bucket the server
|
|
43
|
+
* chose and travels verbatim at the head — that is how "storage rejected 116
|
|
44
|
+
* MB" reaches an operator instead of "upload failed" (BLI-2528).
|
|
45
|
+
*
|
|
46
|
+
* The label alone is not the whole fact. Every dashboard envelope carries a
|
|
47
|
+
* `code`, so a label-first helper that stopped at the first match never read
|
|
48
|
+
* `message` on any refusal: four fleet machines showed `ingest_rejected: http
|
|
49
|
+
* 500; server reason BLI-E001` while the server had sent "Collector device
|
|
50
|
+
* lookup failed: <cause>" in the same body. The failure knew its reason and
|
|
51
|
+
* the collector discarded it. So the sentence rides behind the label as
|
|
52
|
+
* `label: message`, redacted and capped — a proxy's error page is unbounded
|
|
53
|
+
* and a route's message can carry a path. A message that merely restates the
|
|
54
|
+
* `reason` is dropped rather than repeated.
|
|
47
55
|
*
|
|
48
56
|
* `null` means the body said nothing usable — which is itself worth logging as
|
|
49
57
|
* `"none"`, since "the server gave no reason" and "we never looked" are
|
|
@@ -53,14 +61,26 @@ export function serverFailureDetail(value) {
|
|
|
53
61
|
if (!value || typeof value !== "object")
|
|
54
62
|
return null;
|
|
55
63
|
const record = value;
|
|
64
|
+
let head = null;
|
|
65
|
+
let headIsReason = false;
|
|
56
66
|
for (const key of ["reason", "code"]) {
|
|
57
67
|
const candidate = record[key];
|
|
58
68
|
if (typeof candidate === "string" && /^[a-z0-9_:.-]{1,80}$/iu.test(candidate)) {
|
|
59
|
-
|
|
69
|
+
head = candidate;
|
|
70
|
+
headIsReason = key === "reason";
|
|
71
|
+
break;
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
const message = record["message"] ?? record["error"];
|
|
63
|
-
|
|
75
|
+
const sentence = typeof message === "string" ? boundedFailureDetail(message) : null;
|
|
76
|
+
if (!head)
|
|
77
|
+
return sentence;
|
|
78
|
+
if (!sentence)
|
|
79
|
+
return head;
|
|
80
|
+
if (headIsReason && sentence.trim().toLowerCase() === head.trim().toLowerCase()) {
|
|
81
|
+
return head;
|
|
82
|
+
}
|
|
83
|
+
return `${head}: ${sentence}`;
|
|
64
84
|
}
|
|
65
85
|
/** The server's own words when it supplied any, otherwise our fallback. */
|
|
66
86
|
export function responseErrorMessage(value, fallback) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.128",
|
|
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-exit-contract.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.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.34",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.55",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.48"
|
|
33
33
|
}
|
|
34
34
|
}
|