@bli-cockpit/cli 0.2.197 → 0.2.198
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/brain.js +12 -0
- package/dist/commands/cal.js +2 -2
- package/dist/commands/docs-notion.js +3 -1
- package/dist/commands/docs.js +5 -2
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/heartbeat.js +1 -1
- package/dist/commands/issue.js +5 -1
- package/dist/commands/local-args-tower-brain.js +21 -5
- package/dist/commands/local-help-commands-tower-ops.js +4 -0
- package/dist/commands/mail.js +2 -2
- package/dist/commands/memory-install-receipt.js +2 -4
- package/dist/commands/memory-store.js +2 -2
- package/dist/commands/msg.js +3 -2
- package/dist/commands/notes-folders.js +2 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/read-cache.js +18 -2
- package/dist/commands/settings.js +2 -2
- package/dist/commands/setup-receipt-lines.js +14 -2
- package/dist/commands/setup-receipt.js +20 -8
- package/dist/commands/setup.js +2 -0
- package/dist/commands/status.js +9 -8
- package/dist/commands/usage-format.js +1 -0
- package/dist/commands/usage.js +2 -0
- package/dist/local-state-status.js +4 -0
- package/package.json +2 -2
package/dist/commands/brain.js
CHANGED
|
@@ -16,6 +16,18 @@ export async function runBrain(command, io) {
|
|
|
16
16
|
writeLine(io.stderr, detail);
|
|
17
17
|
return 1;
|
|
18
18
|
}
|
|
19
|
+
if (command.action === "retry") {
|
|
20
|
+
const answer = await askAgentDoor(door, { path: "/api/brain/incidents/retry", method: "POST", label: "brain retry", timeoutMs: READ_DEADLINE_MS,
|
|
21
|
+
body: { ids: command.ids, all_needs_human: command.allNeedsHuman, project: command.project, reason: command.reason } });
|
|
22
|
+
if (!answer.ok)
|
|
23
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
24
|
+
const body = answer.body;
|
|
25
|
+
writeLine(io.stderr, `${TAG} retry answered ${JSON.stringify({ reason: body.reason, retried: body.retried.length, skipped: body.skipped.length, missing: body.missing.length, failures: body.failures.length })}`);
|
|
26
|
+
if (command.json)
|
|
27
|
+
return emitAgentDoor(door, body, body.ok ? 0 : 1);
|
|
28
|
+
writeLine(io.stdout, body.text);
|
|
29
|
+
return body.ok ? 0 : 1;
|
|
30
|
+
}
|
|
19
31
|
const query = new URLSearchParams();
|
|
20
32
|
if (command.project)
|
|
21
33
|
query.set("project", command.project);
|
package/dist/commands/cal.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-cale
|
|
|
25
25
|
import { runCalDelete } from "./cal-delete.js";
|
|
26
26
|
import { runCalSyncStatus } from "./cal-sync-status.js";
|
|
27
27
|
import { writeLine } from "./cli-io.js";
|
|
28
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
28
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
29
29
|
const TAG = "[cal cli]";
|
|
30
30
|
const READ_DEADLINE_MS = 30_000;
|
|
31
31
|
const WRITE_DEADLINE_MS = 60_000;
|
|
@@ -118,7 +118,7 @@ async function window(command, door) {
|
|
|
118
118
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
119
119
|
const body = answer.body;
|
|
120
120
|
if (door.json)
|
|
121
|
-
return emitAgentDoor(door, { ok: true, ...body });
|
|
121
|
+
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
122
122
|
if (answer.fromCache) {
|
|
123
123
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
124
124
|
}
|
|
@@ -47,8 +47,8 @@ export async function syncStatusDocs(command, door) {
|
|
|
47
47
|
const body = answer.body;
|
|
48
48
|
if (door.json) {
|
|
49
49
|
return emitAgentDoor(door, {
|
|
50
|
+
...answer.body,
|
|
50
51
|
ok: true,
|
|
51
|
-
counts: body.counts,
|
|
52
52
|
documents: body.documents ?? [],
|
|
53
53
|
last_run: body.last_run ?? { push: null, pull: null },
|
|
54
54
|
});
|
|
@@ -57,6 +57,8 @@ export async function syncStatusDocs(command, door) {
|
|
|
57
57
|
for (const doc of documents) {
|
|
58
58
|
writeLine(door.io.stdout, `${doc.id} ${doc.state.padEnd(13)} ${(doc.notion_synced_at ?? "never").padEnd(25)} ${doc.slug ?? "(no slug)"} ${doc.title}`);
|
|
59
59
|
}
|
|
60
|
+
if (body.coverage)
|
|
61
|
+
writeLine(door.io.stdout, body.coverage);
|
|
60
62
|
const byState = body.counts?.by_state ?? {};
|
|
61
63
|
writeLine(door.io.stdout, "");
|
|
62
64
|
writeLine(door.io.stdout, `${body.counts?.linked ?? documents.length} linked: ${Object.entries(byState)
|
package/dist/commands/docs.js
CHANGED
|
@@ -32,7 +32,7 @@ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
|
32
32
|
import { readDoc } from "./docs-read.js";
|
|
33
33
|
import { linkDoc, syncStatusDocs } from "./docs-notion.js";
|
|
34
34
|
import { readNoteFile } from "./notes-file.js";
|
|
35
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
35
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
36
36
|
const TAG = "[docs cli]";
|
|
37
37
|
const READ_DEADLINE_MS = 30_000;
|
|
38
38
|
const WRITE_DEADLINE_MS = 60_000;
|
|
@@ -97,10 +97,13 @@ async function listDocs(command, door) {
|
|
|
97
97
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
98
98
|
const documents = (answer.body.documents ?? []);
|
|
99
99
|
if (door.json)
|
|
100
|
-
return emitAgentDoor(door, { ok: true, documents });
|
|
100
|
+
return emitAgentDoor(door, { ok: true, ...answer.body, documents, ...cacheCoverage(answer) });
|
|
101
101
|
if (answer.fromCache) {
|
|
102
102
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
103
103
|
}
|
|
104
|
+
const counts = answer.body.counts;
|
|
105
|
+
if (counts)
|
|
106
|
+
writeLine(door.io.stdout, `${counts.total} documents match in all. ${counts.coverage}`);
|
|
104
107
|
if (documents.length === 0) {
|
|
105
108
|
writeLine(door.io.stdout, command.query ? `No document matches "${command.query}".` : "No documents.");
|
|
106
109
|
return 0;
|
|
@@ -53,6 +53,7 @@ setupReceipt, repairs = []) {
|
|
|
53
53
|
repairs,
|
|
54
54
|
needs_person: needsPerson,
|
|
55
55
|
setup_receipt: setupReceipt?.receipt ?? null,
|
|
56
|
+
setup_receipt_coverage: setupReceipt ? setupReceiptBlock(setupReceipt).join("; ") : "no data yet: no cached receipt",
|
|
56
57
|
memory_install: setupReceipt?.memory ?? null,
|
|
57
58
|
}, null, 2));
|
|
58
59
|
return;
|
|
@@ -270,7 +270,7 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
270
270
|
const heartbeat = buildCollectorHeartbeat({
|
|
271
271
|
roots: options.roots,
|
|
272
272
|
facts: options.facts,
|
|
273
|
-
setupReceipt:
|
|
273
|
+
setupReceipt: setupReceipt ? { ...setupReceipt, agent_setup: await collectAgentSetup(options.homeDir ?? os.homedir()) } : null,
|
|
274
274
|
...(options.now ? { now: options.now } : {}),
|
|
275
275
|
memoryInstall: memory.receipt,
|
|
276
276
|
});
|
package/dist/commands/issue.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
29
29
|
import { writeLine } from "./cli-io.js";
|
|
30
30
|
import { assigneeLabel, beginAssigneeDirectoryFetch, finishAssigneeDirectory, withAssigneeIdentity } from "./issue-directory.js";
|
|
31
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
31
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
32
32
|
import { showIssue } from "./issue-show.js";
|
|
33
33
|
import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
|
|
34
34
|
import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
|
|
@@ -154,11 +154,15 @@ async function listIssues(command, door) {
|
|
|
154
154
|
if (door.json) {
|
|
155
155
|
return emitAgentDoor(door, {
|
|
156
156
|
ok: true,
|
|
157
|
+
...(body.counts ? { counts: body.counts } : {}),
|
|
158
|
+
...cacheCoverage(answer),
|
|
157
159
|
issues: issues.map((issue) => withSizeLabel(withAssigneeIdentity(issue, directory))),
|
|
158
160
|
...(body.completeness ? { completeness: body.completeness } : {}),
|
|
159
161
|
...(body.outside_project ? { outside_project: body.outside_project } : {}),
|
|
160
162
|
});
|
|
161
163
|
}
|
|
164
|
+
if (body.counts)
|
|
165
|
+
writeLine(door.io.stdout, `${body.counts.total} match in all. ${body.counts.coverage}`);
|
|
162
166
|
if (issues.length === 0) {
|
|
163
167
|
writeLine(door.io.stdout, "No issues match that.");
|
|
164
168
|
return 0;
|
|
@@ -13,12 +13,12 @@ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-value
|
|
|
13
13
|
export const BRAIN_SHOW_STATUSES = ["open", "running", "needs_human", "fixed", "not_actionable", "cleared"];
|
|
14
14
|
export function parseBrainArgs(args) {
|
|
15
15
|
const values = parseNamedArgs(args, {
|
|
16
|
-
allowedFlags: ["--project", "--show", "--home", "--dashboard-url", "--json"],
|
|
17
|
-
valueFlags: ["--project", "--show", "--home", "--dashboard-url"],
|
|
16
|
+
allowedFlags: ["--project", "--show", "--home", "--dashboard-url", "--json", "--all-needs-human", "--reason"],
|
|
17
|
+
valueFlags: ["--project", "--show", "--home", "--dashboard-url", "--reason"],
|
|
18
18
|
});
|
|
19
19
|
const action = values.positionals[0];
|
|
20
|
-
if (action !== "status" || values.positionals.length > 1) {
|
|
21
|
-
throw new Error("brain takes one verb: `status`. Example: `cockpit brain status` or `cockpit brain status --project pearl-portal`.");
|
|
20
|
+
if ((action !== "status" && action !== "retry") || (action === "status" && values.positionals.length > 1)) {
|
|
21
|
+
throw new Error("brain takes one verb: `status` or `retry`. Example: `cockpit brain status` or `cockpit brain status --project pearl-portal`.");
|
|
22
22
|
}
|
|
23
23
|
const project = optionalNonEmpty(values.flags.get("--project"));
|
|
24
24
|
if (project && !LOG_PROJECT_PATTERN.test(project))
|
|
@@ -26,9 +26,25 @@ export function parseBrainArgs(args) {
|
|
|
26
26
|
const show = optionalNonEmpty(values.flags.get("--show"));
|
|
27
27
|
if (show && !BRAIN_SHOW_STATUSES.includes(show))
|
|
28
28
|
throw new Error(`--show wants one of ${BRAIN_SHOW_STATUSES.join(", ")}.`);
|
|
29
|
+
const ids = values.positionals.slice(1);
|
|
30
|
+
const reason = optionalNonEmpty(values.flags.get("--reason"))?.trim();
|
|
31
|
+
const allNeedsHuman = values.booleans.has("--all-needs-human");
|
|
32
|
+
if (action === "retry") {
|
|
33
|
+
if (!reason || reason.length > 4000)
|
|
34
|
+
throw new Error("--reason requires text (at most 4000 characters).");
|
|
35
|
+
if (!ids.length && !allNeedsHuman)
|
|
36
|
+
throw new Error("retry requires incident ids or --all-needs-human.");
|
|
37
|
+
if (ids.length > 100 || ids.some(id => !/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|BLI-[0-9]+)$/i.test(id)))
|
|
38
|
+
throw new Error("retry wants incident UUIDs or BLI issue identifiers (at most 100).");
|
|
39
|
+
if (show)
|
|
40
|
+
throw new Error("--show belongs to brain status.");
|
|
41
|
+
}
|
|
42
|
+
else if (reason || allNeedsHuman)
|
|
43
|
+
throw new Error("--reason and --all-needs-human belong to brain retry.");
|
|
29
44
|
return {
|
|
30
45
|
kind: "brain",
|
|
31
|
-
action
|
|
46
|
+
action,
|
|
47
|
+
...(action === "retry" ? { ids, reason, allNeedsHuman } : {}),
|
|
32
48
|
...(project ? { project } : {}),
|
|
33
49
|
...(show ? { show } : {}),
|
|
34
50
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
@@ -49,6 +49,10 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
49
49
|
[
|
|
50
50
|
"Usage: cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
51
51
|
"",
|
|
52
|
+
"Retry: cockpit brain retry <id...> [--all-needs-human] [--project <p>] --reason <text> [--json]",
|
|
53
|
+
"Ids are incident UUIDs or BLI issue identifiers. Retry reopens needs_human or not_actionable rows, resets attempts and claims,",
|
|
54
|
+
"and records who, why and when. --all-needs-human selects only needs_human; --project narrows every selection. Other states are skipped.",
|
|
55
|
+
"Only members and super admins can retry. One comment is posted per incident issue. MCP twin: brain_retry.",
|
|
52
56
|
"Tower's incident queue. Every 15 minutes the log-digest cron turns each recurring problem in a project's logs (classes",
|
|
53
57
|
"our_bug, config_drift, capacity, unknown) into ONE incident with ONE Tower issue; a repeat adds to its count, never a twin.",
|
|
54
58
|
"A template quiet for 24 hours is marked cleared. This prints, per project, how many incidents are open, being worked,",
|
package/dist/commands/mail.js
CHANGED
|
@@ -26,7 +26,7 @@ import { writeFile } from "node:fs/promises";
|
|
|
26
26
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
27
27
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
28
28
|
import { composeMessage, failAttachmentsUnconfirmed, readMailBody, runMailDraft, unconfirmedAttachments, } from "./mail-compose.js";
|
|
29
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
29
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
30
30
|
import { towerRequest } from "../tower-client.js";
|
|
31
31
|
const TAG = "[mail cli]";
|
|
32
32
|
const READ_DEADLINE_MS = 30_000;
|
|
@@ -177,7 +177,7 @@ async function inbox(command, door) {
|
|
|
177
177
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
178
178
|
const body = answer.body;
|
|
179
179
|
if (door.json)
|
|
180
|
-
return emitAgentDoor(door, { ok: true, ...body });
|
|
180
|
+
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
181
181
|
if (answer.fromCache) {
|
|
182
182
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
183
183
|
}
|
|
@@ -208,11 +208,9 @@ export async function readMemoryReceiptFile(options) {
|
|
|
208
208
|
if (!receipt)
|
|
209
209
|
return { receipt: null, reason: "receipt_unparseable" };
|
|
210
210
|
const age = (options.now ?? new Date()).getTime() - Date.parse(receipt.checked_at);
|
|
211
|
-
//
|
|
212
|
-
// board say "not reported" than to publish a claim about a machine that has
|
|
213
|
-
// not been looked at since.
|
|
211
|
+
// Keep last-known host facts, with the original checked_at and reason.
|
|
214
212
|
if (Number.isFinite(age) && age > MEMORY_RECEIPT_MAX_AGE_MS) {
|
|
215
|
-
return { receipt
|
|
213
|
+
return { receipt, reason: "receipt_expired" };
|
|
216
214
|
}
|
|
217
215
|
return { receipt, reason: "receipt_read" };
|
|
218
216
|
}
|
|
@@ -49,7 +49,7 @@ import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.
|
|
|
49
49
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
50
50
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
51
51
|
import { runMemoryConsolidation } from "./memory-consolidation.js";
|
|
52
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
52
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
53
53
|
import { runMemoryRequeue } from "./memory-requeue.js";
|
|
54
54
|
import { runMemoryVisibility } from "./memory-visibility.js";
|
|
55
55
|
import { readNoteFile } from "./notes-file.js";
|
|
@@ -194,7 +194,7 @@ async function searchMemory(command, door) {
|
|
|
194
194
|
degraded: body.degraded ?? null,
|
|
195
195
|
})}`);
|
|
196
196
|
if (door.json)
|
|
197
|
-
return emitAgentDoor(door, answer.body);
|
|
197
|
+
return emitAgentDoor(door, { ...answer.body, ...cacheCoverage(answer) });
|
|
198
198
|
if (answer.fromCache) {
|
|
199
199
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
200
200
|
}
|
package/dist/commands/msg.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
19
19
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
20
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
20
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
21
21
|
const TAG = "[msg cli]";
|
|
22
22
|
const READ_DEADLINE_MS = 30_000;
|
|
23
23
|
const WRITE_DEADLINE_MS = 30_000;
|
|
@@ -64,6 +64,7 @@ async function fetchChannels(door) {
|
|
|
64
64
|
channels: answer.body.channels ?? [],
|
|
65
65
|
fromCache: answer.fromCache,
|
|
66
66
|
ageMs: answer.ageMs,
|
|
67
|
+
coverage: answer.coverage,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
async function listChannels(door) {
|
|
@@ -71,7 +72,7 @@ async function listChannels(door) {
|
|
|
71
72
|
if (!fetched.ok)
|
|
72
73
|
return failAgentDoor(door, TAG, fetched.reason, fetched.detail);
|
|
73
74
|
if (door.json)
|
|
74
|
-
return emitAgentDoor(door, { ok: true, channels: fetched.channels });
|
|
75
|
+
return emitAgentDoor(door, { ok: true, channels: fetched.channels, ...cacheCoverage(fetched) });
|
|
75
76
|
if (fetched.fromCache) {
|
|
76
77
|
writeLine(door.io.stderr, `(${cacheAgeLabel(fetched.ageMs ?? 0)}, refreshing in the background)`);
|
|
77
78
|
}
|
|
@@ -31,5 +31,7 @@ export async function runFolderCommand(command, door) {
|
|
|
31
31
|
const label = command.tree ? `${" ".repeat(folder.path.split("/").length - 1)}${folder.name}` : folder.path;
|
|
32
32
|
writeLine(door.io.stdout, `${label} (${folder.note_count})`);
|
|
33
33
|
}
|
|
34
|
+
if (body.coverage)
|
|
35
|
+
writeLine(door.io.stdout, body.coverage);
|
|
34
36
|
return 0;
|
|
35
37
|
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.198");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Text mode: a cache hit prints INSTANTLY (its age travels with it), then a
|
|
7
7
|
* background refresh is spawned so the next call is live — never blocking
|
|
8
8
|
* this process on a network round trip it already has a recent answer for.
|
|
9
|
-
* `--json`
|
|
9
|
+
* `--json` attempts live first when the cache is older than
|
|
10
10
|
* `JSON_FRESH_BOUND_MS`, unless `--allow-stale` says otherwise; older than
|
|
11
11
|
* that runs the live request inline, same as a miss. `--fresh` skips the
|
|
12
12
|
* read entirely — both flags are process-wide (BLI-10450's `--quiet`
|
|
@@ -73,7 +73,7 @@ export async function readThroughCache(options) {
|
|
|
73
73
|
else {
|
|
74
74
|
log(`[read-cache] refresh skipped ${JSON.stringify({ reason: "too_recent", age_ms: ageMs })}`);
|
|
75
75
|
}
|
|
76
|
-
return { ok: true, httpStatus: cached.status, body: cached.body, fromCache: true, ageMs };
|
|
76
|
+
return { ok: true, httpStatus: cached.status, body: cached.body, fromCache: true, ageMs, coverage: cacheAgeLabel(ageMs) };
|
|
77
77
|
}
|
|
78
78
|
log(`[read-cache] too stale for --json ${JSON.stringify({ surface: options.surface, label: options.label, age_ms: ageMs })}`);
|
|
79
79
|
}
|
|
@@ -86,6 +86,18 @@ export async function readThroughCache(options) {
|
|
|
86
86
|
writeReadCacheEntryAtomic(entryPath, { status: live.httpStatus, body: live.body });
|
|
87
87
|
log(`[read-cache] refreshed ${JSON.stringify({ surface: options.surface, label: options.label })}`);
|
|
88
88
|
}
|
|
89
|
+
else if (!fresh && live.httpStatus !== 401 && live.httpStatus !== 403) {
|
|
90
|
+
const cached = readReadCacheEntry(entryPath);
|
|
91
|
+
if (cached) {
|
|
92
|
+
const ageMs = Date.now() - cached.storedAt;
|
|
93
|
+
const coverage = `served stale: ${cacheAgeLabel(ageMs)}, live failed: ${live.reason}`;
|
|
94
|
+
log(`[read-cache] ${coverage} ${JSON.stringify({ reason: live.reason, age_ms: ageMs })}`);
|
|
95
|
+
const body = cached.body && typeof cached.body === "object" && !Array.isArray(cached.body)
|
|
96
|
+
? { ...cached.body, cache_coverage: coverage, cache_age_ms: ageMs, live_failure_reason: live.reason }
|
|
97
|
+
: cached.body;
|
|
98
|
+
return { ok: true, httpStatus: cached.status, body, fromCache: true, ageMs, coverage };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
89
101
|
return { ...live, fromCache: false, ageMs: null };
|
|
90
102
|
}
|
|
91
103
|
/**
|
|
@@ -112,4 +124,8 @@ export function spawnBackgroundRefresh(io, log, spawnImpl = spawn) {
|
|
|
112
124
|
catch (error) {
|
|
113
125
|
log(`[read-cache] background refresh failed to spawn ${JSON.stringify({ reason: error instanceof Error ? error.name : "unknown" })}`);
|
|
114
126
|
}
|
|
127
|
+
}
|
|
128
|
+
/** JSON readers carry the same age sentence as the terminal. */
|
|
129
|
+
export function cacheCoverage(answer) {
|
|
130
|
+
return answer.fromCache ? { fromCache: true, ageMs: answer.ageMs, cache_coverage: answer.coverage ?? cacheAgeLabel(answer.ageMs ?? 0) } : {};
|
|
115
131
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* being written to stdout, stderr, or an argument vector.
|
|
21
21
|
*/
|
|
22
22
|
import { isInteractiveStdin, readLine, settleYesNo, writeLine } from "./cli-io.js";
|
|
23
|
-
import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
|
|
23
|
+
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
24
24
|
import { asList, asRecord, callTower, isForbidden, openTower, parseModelKey, readAllStdin, writeCommandFailure, } from "./tower-command.js";
|
|
25
25
|
import { showOverview } from "./settings-overview.js";
|
|
26
26
|
import { renderCliFloor, renderEnvBlobs, renderModelRouting, renderPersonal, renderSwitches, } from "./settings-render.js";
|
|
@@ -75,7 +75,7 @@ async function showPersonal(command, tower, io) {
|
|
|
75
75
|
if (result.fromCache && !command.json) {
|
|
76
76
|
writeLine(io.stderr, `(${cacheAgeLabel(result.ageMs ?? 0)}, refreshing in the background)`);
|
|
77
77
|
}
|
|
78
|
-
return writeBody(io, command.json, result.body, () => renderPersonal(asRecord(result.body)));
|
|
78
|
+
return writeBody(io, command.json, { ...asRecord(result.body), ...cacheCoverage(result) }, () => renderPersonal(asRecord(result.body)));
|
|
79
79
|
}
|
|
80
80
|
async function setPersonal(command, tower, io) {
|
|
81
81
|
const result = await callTower(tower, {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* gets ONE command or ONE click, never a paragraph and never "see the docs".
|
|
11
11
|
*/
|
|
12
12
|
import { setupPieceMark, setupReceiptLine, setupReceiptPieces, } from "@bli-cockpit/telemetry-core";
|
|
13
|
+
/** Cached host facts are old after the same seven-day window as their readers. */
|
|
14
|
+
const OLD_READING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
15
|
/** The one fix for each piece, by key. Present for every key the block prints. */
|
|
14
16
|
const FIXES = {
|
|
15
17
|
browser: "Run `cockpit login` and open the link it prints.",
|
|
@@ -57,8 +59,18 @@ export function setupReceiptBlock(reading, options = {}) {
|
|
|
57
59
|
continue;
|
|
58
60
|
lines.push(`${indent} ${label} ${setupPieceMark(piece.status)}${reasonSuffix(piece)} — ${fixFor(key, piece)}`);
|
|
59
61
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const now = reading.readAt ? Date.parse(reading.readAt) : Date.now();
|
|
63
|
+
const ageMs = now - Date.parse(receipt.checked_at);
|
|
64
|
+
if (options.showCheckedAt || reading.refreshFailure || !Number.isFinite(ageMs) || ageMs > OLD_READING_MS) {
|
|
65
|
+
const age = Number.isFinite(ageMs) ? `${Math.max(0, Math.floor(ageMs / 3_600_000))}h ago` : "age unavailable";
|
|
66
|
+
lines.push(`${indent}As of ${receipt.checked_at} (${age})${reading.refreshFailure ? `; this run could not re-read the machine: ${reading.refreshFailure}` : ""}`);
|
|
67
|
+
}
|
|
68
|
+
if (memory) {
|
|
69
|
+
const memoryAgeMs = now - Date.parse(memory.checked_at);
|
|
70
|
+
if (!Number.isFinite(memoryAgeMs) || memoryAgeMs > OLD_READING_MS) {
|
|
71
|
+
const age = Number.isFinite(memoryAgeMs) ? `${Math.max(0, Math.floor(memoryAgeMs / 3_600_000))}h ago` : "age unavailable";
|
|
72
|
+
lines.push(`${indent}Agent hosts as of ${memory.checked_at} (${age})`);
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
return lines;
|
|
64
76
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* owns the target vocabulary this translates from.
|
|
40
40
|
*/
|
|
41
41
|
import os from "node:os";
|
|
42
|
-
import { SETUP_RECEIPT_SCHEMA_VERSION, setupReceiptGaps, setupReceiptLine, } from "@bli-cockpit/telemetry-core";
|
|
42
|
+
import { SETUP_RECEIPT_SCHEMA_VERSION, SetupReceiptSchema, setupReceiptGaps, setupReceiptLine, } from "@bli-cockpit/telemetry-core";
|
|
43
43
|
import { readHeartbeatMemoryReceipt } from "./heartbeat.js";
|
|
44
44
|
import { autostartSetupPiece, readAutostartRegistration, } from "./autostart-reading.js";
|
|
45
45
|
import { readLocalSessionReference } from "../local-state.js";
|
|
@@ -89,7 +89,7 @@ export async function buildSetupReceipt(io, options = {}) {
|
|
|
89
89
|
gaps,
|
|
90
90
|
memory_receipt: memory ? "read" : "absent",
|
|
91
91
|
}));
|
|
92
|
-
return { receipt, memory };
|
|
92
|
+
return { receipt, memory, readAt: checkedAt };
|
|
93
93
|
}
|
|
94
94
|
function withDefaultProbes(io, homeDir, options) {
|
|
95
95
|
return {
|
|
@@ -167,8 +167,8 @@ export async function cacheSetupReceipt(receipt, homeDir) {
|
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
169
|
* The cached receipt, or null with a named reason. A receipt older than
|
|
170
|
-
* `SETUP_RECEIPT_MAX_AGE_MS` is
|
|
171
|
-
*
|
|
170
|
+
* `SETUP_RECEIPT_MAX_AGE_MS` is logged as stale and sent with its original
|
|
171
|
+
* checked_at; readers show its age rather than discarding the last reading.
|
|
172
172
|
*/
|
|
173
173
|
export async function readCachedSetupReceipt(homeDir, now = new Date()) {
|
|
174
174
|
let raw;
|
|
@@ -187,8 +187,7 @@ export async function readCachedSetupReceipt(homeDir, now = new Date()) {
|
|
|
187
187
|
}
|
|
188
188
|
const age = now.getTime() - Date.parse(parsed.checked_at);
|
|
189
189
|
if (!Number.isFinite(age) || age > SETUP_RECEIPT_MAX_AGE_MS) {
|
|
190
|
-
console.error(`${TAG}
|
|
191
|
-
return null;
|
|
190
|
+
console.error(`${TAG} serving old cache`, JSON.stringify({ reason: "cache_stale", age_days: Math.round(age / 86_400_000) }));
|
|
192
191
|
}
|
|
193
192
|
return parsed;
|
|
194
193
|
}
|
|
@@ -196,7 +195,8 @@ function safeParseReceipt(raw) {
|
|
|
196
195
|
const record = raw;
|
|
197
196
|
if (!record || record.schema_version !== SETUP_RECEIPT_SCHEMA_VERSION)
|
|
198
197
|
return null;
|
|
199
|
-
|
|
198
|
+
const parsed = SetupReceiptSchema.safeParse(raw);
|
|
199
|
+
return parsed.success ? parsed.data : null;
|
|
200
200
|
}
|
|
201
201
|
/** The one line, plus the one fix per gap. Rendering lives in `setup-receipt-lines.ts`. */
|
|
202
202
|
export { setupReceiptLine };
|
|
@@ -212,11 +212,23 @@ export { setupReceiptLine };
|
|
|
212
212
|
export async function refreshSetupReceipt(io, options = {}) {
|
|
213
213
|
try {
|
|
214
214
|
const reading = await buildSetupReceipt(io, options);
|
|
215
|
+
const probeFailed = [reading.receipt.browser, reading.receipt.device, reading.receipt.collector.autostart].some(piece => piece.reason?.endsWith("probe_failed"));
|
|
216
|
+
if (probeFailed) {
|
|
217
|
+
const cached = await readCachedSetupReceipt(options.homeDir, options.now);
|
|
218
|
+
if (cached) {
|
|
219
|
+
console.error(`${TAG} could not be computed`, JSON.stringify({ reason: "receipt_build_failed" }));
|
|
220
|
+
return { receipt: cached, memory: reading.memory, readAt: reading.readAt, refreshFailure: "receipt_build_failed" };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
215
223
|
await cacheSetupReceipt(reading.receipt, options.homeDir);
|
|
216
224
|
return reading;
|
|
217
225
|
}
|
|
218
226
|
catch (error) {
|
|
219
227
|
console.error(`${TAG} could not be computed`, JSON.stringify({ reason: "receipt_build_failed", error_name: errorName(error) }));
|
|
220
|
-
|
|
228
|
+
const receipt = await readCachedSetupReceipt(options.homeDir, options.now);
|
|
229
|
+
if (!receipt)
|
|
230
|
+
return null;
|
|
231
|
+
const memory = await readHeartbeatMemoryReceipt({ homeDir: options.homeDir, now: options.now });
|
|
232
|
+
return { receipt, memory: memory.receipt, readAt: (options.now ?? new Date()).toISOString(), refreshFailure: "receipt_build_failed" };
|
|
221
233
|
}
|
|
222
234
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -48,6 +48,8 @@ export async function runSetup(command, io) {
|
|
|
48
48
|
if (pair.use?.[side])
|
|
49
49
|
writeLine(io.stdout, ` ${side === "first" ? pair.first : pair.second}: tokens by model: ${pair.use[side].models ?? "not available right now."} Skills used: ${pair.use[side].skills ?? "not available right now."}`);
|
|
50
50
|
}
|
|
51
|
+
for (const line of body.read_coverage ?? [])
|
|
52
|
+
writeLine(io.stdout, line);
|
|
51
53
|
if (body.note)
|
|
52
54
|
writeLine(io.stdout, body.note);
|
|
53
55
|
return 0;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cockpit status` — what this machine believes about itself: install, pairing,
|
|
3
|
-
* active work, upload state, retry backlog, and how far historical backfill
|
|
4
|
-
* has got.
|
|
5
2
|
*
|
|
6
|
-
* Read-only. Split out of commands/local.ts (BLI-3104). BLI-3194 rewrote the
|
|
7
|
-
* human block into plain English, because these lines are what an intern
|
|
8
|
-
* pastes into Slack when something looks wrong. The `--json` payload is the
|
|
9
3
|
* machine contract and is byte-for-byte unchanged — `status-json-shape.test.ts`
|
|
10
4
|
* holds it to that.
|
|
11
5
|
*
|
|
@@ -131,7 +125,7 @@ function writeSingleRepoLines(io, status, backfillCursor, setupReceipt, disk) {
|
|
|
131
125
|
if (status.linear_ticket_id)
|
|
132
126
|
writeLine(io.stdout, `Linear: ${status.linear_ticket_id}`);
|
|
133
127
|
writeLine(io.stdout, `Work: ${displayWorkLabel(status)}`);
|
|
134
|
-
writeLine(io.stdout, `Last collected: ${lastCollectedLine(status.collector_freshness)}`);
|
|
128
|
+
writeLine(io.stdout, `Last collected: ${lastCollectedLine(status.collector_freshness, status.last_collected_at)}`);
|
|
135
129
|
writeLine(io.stdout, `Version: ${status.collector_version}`);
|
|
136
130
|
writeLine(io.stdout, `Uploads: ${uploadsLine(status.upload_state)}`);
|
|
137
131
|
writeLine(io.stdout, `Last upload tried: ${status.last_upload_attempt_at ?? "never"}`);
|
|
@@ -239,7 +233,14 @@ function signedInLine(sessionState) {
|
|
|
239
233
|
return "cannot tell";
|
|
240
234
|
}
|
|
241
235
|
}
|
|
242
|
-
function lastCollectedLine(freshness) {
|
|
236
|
+
export function lastCollectedLine(freshness, timestamp) {
|
|
237
|
+
if (timestamp) {
|
|
238
|
+
const ageMs = Date.now() - Date.parse(timestamp);
|
|
239
|
+
const minutes = Math.max(0, Math.floor(ageMs / 60_000));
|
|
240
|
+
const age = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h` : `${Math.floor(minutes / 1440)}d`;
|
|
241
|
+
const label = lastCollectedLine(freshness);
|
|
242
|
+
return `${label} (${timestamp}; ${age} ago)`;
|
|
243
|
+
}
|
|
243
244
|
switch (freshness) {
|
|
244
245
|
case "fresh":
|
|
245
246
|
return "recently";
|
|
@@ -41,6 +41,7 @@ export function githubWords(github, people) {
|
|
|
41
41
|
`GitHub: ${noun(sum("pull_requests_opened"), "pull request")} opened, ${sum("pull_requests_merged")} merged, ${noun(sum("commits"), "commit")}, ` +
|
|
42
42
|
`from ${noun((github.repositories ?? []).length, "mirrored repository", "mirrored repositories")}${range}.`,
|
|
43
43
|
];
|
|
44
|
+
lines.push(...(github.read_coverage ?? []));
|
|
44
45
|
const unmatched = github.unattributed;
|
|
45
46
|
if (unmatched && unmatched.pull_requests_opened + unmatched.pull_requests_merged + unmatched.commits > 0) {
|
|
46
47
|
lines.push(`Not matched to a person: ${noun(unmatched.pull_requests_opened, "pull request")} opened, ${unmatched.pull_requests_merged} merged, ${noun(unmatched.commits, "commit")}.`);
|
package/dist/commands/usage.js
CHANGED
|
@@ -116,6 +116,8 @@ export async function runUsage(command, io) {
|
|
|
116
116
|
// date it cannot know.
|
|
117
117
|
writeLine(io.stdout, "");
|
|
118
118
|
writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent, not actual spend (this dashboard sent no price-sheet version)");
|
|
119
|
+
for (const line of body.read_coverage ?? [])
|
|
120
|
+
writeLine(io.stdout, line);
|
|
119
121
|
writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
|
|
120
122
|
// BLI-4356: the gap says why. A session observed with no tokens is either
|
|
121
123
|
// waiting on a count that has not run yet, or an automated one this view
|
|
@@ -43,6 +43,10 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
43
43
|
worktree_fingerprint: context?.worktree_fingerprint ?? identity.worktree_fingerprint,
|
|
44
44
|
worktree_is_primary: context?.worktree_is_primary ?? identity.worktree_is_primary,
|
|
45
45
|
collector_freshness: freshness,
|
|
46
|
+
last_collected_at: (() => {
|
|
47
|
+
const timestamp = context ? latestTimestamp([context.updated_at ?? context.started_at, uploadSpool.last_upload_success_at]) : null;
|
|
48
|
+
return timestamp === null ? null : new Date(timestamp).toISOString();
|
|
49
|
+
})(),
|
|
46
50
|
upload_state: uploadState,
|
|
47
51
|
last_upload_attempt_at: uploadSpool.last_upload_attempt_at,
|
|
48
52
|
last_upload_success_at: uploadSpool.last_upload_success_at,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.198",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.60",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.110",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.69"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|