@bli-cockpit/cli 0.2.197 → 0.2.199
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-delete.js +3 -1
- package/dist/commands/cal.js +19 -3
- 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-args-tower-cal.js +6 -5
- package/dist/commands/local-args-tower-mail.js +10 -5
- package/dist/commands/local-args-tower-pages.js +1 -0
- package/dist/commands/local-help-commands-tower-ops.js +4 -0
- package/dist/commands/local-help-commands-tower.js +8 -4
- package/dist/commands/mail-account-preferences.js +16 -0
- package/dist/commands/mail-compose.js +2 -2
- package/dist/commands/mail.js +11 -4
- 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-bot-upcoming.js +16 -0
- package/dist/commands/notes-folders.js +2 -0
- package/dist/commands/notes.js +3 -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);
|
|
@@ -4,7 +4,7 @@ const TAG = "[cal cli]";
|
|
|
4
4
|
const DELETE_DEADLINE_MS = 60_000;
|
|
5
5
|
export async function runCalDelete(command, door) {
|
|
6
6
|
const answer = await askAgentDoor(door, {
|
|
7
|
-
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}`,
|
|
7
|
+
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}${command.accountName ? `?account=${encodeURIComponent(command.accountName)}` : ""}`,
|
|
8
8
|
method: "DELETE",
|
|
9
9
|
label: "cal delete",
|
|
10
10
|
timeoutMs: DELETE_DEADLINE_MS,
|
|
@@ -18,6 +18,8 @@ export async function runCalDelete(command, door) {
|
|
|
18
18
|
writeLine(door.io.stdout, "Tower answered without a result. Nothing to report.");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
|
+
if (deleted.account_name)
|
|
22
|
+
writeLine(door.io.stdout, `Account: ${deleted.account_name}.`);
|
|
21
23
|
const alreadyGone = deleted.google === "already_gone_at_google";
|
|
22
24
|
const stored = deleted.local_store === "removed"
|
|
23
25
|
? "Tower's copy is removed."
|
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;
|
|
@@ -52,6 +52,9 @@ export async function runCal(command, io) {
|
|
|
52
52
|
return runCalSync(command, door);
|
|
53
53
|
case "sync-status":
|
|
54
54
|
return runCalSyncStatus(command, door);
|
|
55
|
+
case "change":
|
|
56
|
+
return changeEvent(command, door);
|
|
57
|
+
case "cancel":
|
|
55
58
|
case "delete":
|
|
56
59
|
return runCalDelete(command, door);
|
|
57
60
|
}
|
|
@@ -118,7 +121,7 @@ async function window(command, door) {
|
|
|
118
121
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
119
122
|
const body = answer.body;
|
|
120
123
|
if (door.json)
|
|
121
|
-
return emitAgentDoor(door, { ok: true, ...body });
|
|
124
|
+
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
122
125
|
if (answer.fromCache) {
|
|
123
126
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
124
127
|
}
|
|
@@ -260,7 +263,7 @@ async function listCalendars(door) {
|
|
|
260
263
|
? (calendar.last_sync_at ? `synced ${calendar.last_sync_at.slice(0, 16).replace("T", " ")}` : "never synced")
|
|
261
264
|
: `${calendar.status}: ${calendar.status_reason ?? "no reason recorded"}`;
|
|
262
265
|
const flags = `${calendar.org_visible ? "shared" : "private"}${calendar.selected ? "" : ", hidden"}`;
|
|
263
|
-
writeLine(door.io.stdout, `${calendar.name.slice(0, 32).padEnd(32)} ${calendar.provider.padEnd(12)} ${flags.padEnd(16)} ${health}`);
|
|
266
|
+
writeLine(door.io.stdout, `${calendar.account_label ?? calendar.provider} ${calendar.name.slice(0, 32).padEnd(32)} ${calendar.provider.padEnd(12)} ${flags.padEnd(16)} ${health}`);
|
|
264
267
|
}
|
|
265
268
|
writeLine(door.io.stdout, "");
|
|
266
269
|
writeLine(door.io.stdout, `${calendars.length} calendar(s). ids: ${calendars.map((calendar) => calendar.id).join(", ")}`);
|
|
@@ -274,6 +277,7 @@ async function create(command, door) {
|
|
|
274
277
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
275
278
|
body: {
|
|
276
279
|
calendar_id: command.calendarId,
|
|
280
|
+
...(command.accountName ? { account: command.accountName } : {}),
|
|
277
281
|
title: command.title,
|
|
278
282
|
starts_at: command.startsAt,
|
|
279
283
|
ends_at: command.endsAt,
|
|
@@ -299,6 +303,8 @@ async function create(command, door) {
|
|
|
299
303
|
}
|
|
300
304
|
if (event?.html_link)
|
|
301
305
|
writeLine(door.io.stdout, event.html_link);
|
|
306
|
+
if (event?.account_name)
|
|
307
|
+
writeLine(door.io.stdout, `Account: ${event.account_name}.`);
|
|
302
308
|
if (command.meet) {
|
|
303
309
|
writeLine(door.io.stdout, event?.meet_link
|
|
304
310
|
? `Google Meet: ${event.meet_link}`
|
|
@@ -314,4 +320,14 @@ function meetingLinkText(event) {
|
|
|
314
320
|
const meeting = event.meeting_url ? ` ${label}: ${event.meeting_url}` : "";
|
|
315
321
|
console.error("[cal render] meeting link", JSON.stringify({ reason: event.meeting_url ? "meeting_link_shown" : "no_meeting_link" }));
|
|
316
322
|
return meeting;
|
|
323
|
+
}
|
|
324
|
+
async function changeEvent(command, door) {
|
|
325
|
+
const answer = await askAgentDoor(door, { path: `/api/cal/events/${encodeURIComponent(command.subjectId)}`, method: "PATCH", label: "cal change", timeoutMs: WRITE_DEADLINE_MS,
|
|
326
|
+
body: { ...(command.accountName ? { account: command.accountName } : {}), ...(command.title ? { title: command.title } : {}), ...(command.startsAt ? { starts_at: command.startsAt } : {}), ...(command.endsAt ? { ends_at: command.endsAt } : {}), ...(command.location ? { location: command.location } : {}) } });
|
|
327
|
+
if (!answer.ok)
|
|
328
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
329
|
+
if (door.json)
|
|
330
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
331
|
+
writeLine(door.io.stdout, `Changed event in ${answer.body.event?.account_name ?? "its account"}.`);
|
|
332
|
+
return 0;
|
|
317
333
|
}
|
|
@@ -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")),
|
|
@@ -26,6 +26,7 @@ const CAL_ACTIONS = new Set([
|
|
|
26
26
|
"find",
|
|
27
27
|
"calendars",
|
|
28
28
|
"add-ical",
|
|
29
|
+
"change", "cancel",
|
|
29
30
|
"create",
|
|
30
31
|
"share",
|
|
31
32
|
"detach",
|
|
@@ -34,7 +35,7 @@ const CAL_ACTIONS = new Set([
|
|
|
34
35
|
"delete",
|
|
35
36
|
]);
|
|
36
37
|
/** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
|
|
37
|
-
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share", "delete"]);
|
|
38
|
+
const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share", "delete", "change", "cancel"]);
|
|
38
39
|
/** `sync-status [<calendar id>]` — every calendar, or one's. Ticket 10730. */
|
|
39
40
|
const CAL_ACTIONS_WITH_OPTIONAL_SUBJECT = new Set(["sync-status"]);
|
|
40
41
|
export function parseCalArgs(args) {
|
|
@@ -43,6 +44,7 @@ export function parseCalArgs(args) {
|
|
|
43
44
|
"--home",
|
|
44
45
|
"--dashboard-url",
|
|
45
46
|
"--calendar",
|
|
47
|
+
"--account",
|
|
46
48
|
"--tz",
|
|
47
49
|
"--offset",
|
|
48
50
|
"--hours",
|
|
@@ -68,6 +70,7 @@ export function parseCalArgs(args) {
|
|
|
68
70
|
"--home",
|
|
69
71
|
"--dashboard-url",
|
|
70
72
|
"--calendar",
|
|
73
|
+
"--account",
|
|
71
74
|
"--tz",
|
|
72
75
|
"--offset",
|
|
73
76
|
"--hours",
|
|
@@ -134,9 +137,6 @@ function readCalCreateDetails(action, values) {
|
|
|
134
137
|
const startsAt = optionalNonEmpty(values.flags.get("--at"));
|
|
135
138
|
const endsAt = optionalNonEmpty(values.flags.get("--until"));
|
|
136
139
|
if (action === "create") {
|
|
137
|
-
if (!optionalNonEmpty(values.flags.get("--calendar"))) {
|
|
138
|
-
throw new Error("cal create needs --calendar <id>: with several calendars attached, which one this goes on is yours to say. `cockpit cal calendars` lists them.");
|
|
139
|
-
}
|
|
140
140
|
if (!title)
|
|
141
141
|
throw new Error('cal create needs --title "<what it is>".');
|
|
142
142
|
if (!startsAt)
|
|
@@ -155,7 +155,7 @@ function validateCalSharing(action, values) {
|
|
|
155
155
|
}
|
|
156
156
|
/** `delete` notifies attendees, exactly like `create`'s invitations — never without saying so first. */
|
|
157
157
|
function validateCalDelete(action, values) {
|
|
158
|
-
if (action === "delete" && !values.booleans.has("--yes")) {
|
|
158
|
+
if ((action === "delete" || action === "cancel") && !values.booleans.has("--yes")) {
|
|
159
159
|
throw new Error("cal delete needs --yes: it removes the event at Google and can email everybody on it. Add --yes once you mean it.");
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -175,6 +175,7 @@ function buildCalCommand(values, action, subject, query, title, startsAt, endsAt
|
|
|
175
175
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
176
176
|
...(subject ? { subjectId: subject } : {}),
|
|
177
177
|
...(query ? { query } : {}),
|
|
178
|
+
accountName: optionalNonEmpty(values.flags.get("--account")),
|
|
178
179
|
calendarId: optionalNonEmpty(values.flags.get("--calendar")),
|
|
179
180
|
// An absent `--tz` means "this machine's zone", resolved at run time in
|
|
180
181
|
// `commands/cal.ts` rather than here, so the parser stays pure.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
32
32
|
const MAIL_ACTIONS = new Set([
|
|
33
|
+
"rename", "enable", "disable", "default", "remove",
|
|
33
34
|
"accounts",
|
|
34
35
|
"add-imap",
|
|
35
36
|
"detach",
|
|
@@ -54,6 +55,7 @@ export function parseMailArgs(args) {
|
|
|
54
55
|
"--home",
|
|
55
56
|
"--dashboard-url",
|
|
56
57
|
"--account",
|
|
58
|
+
"--source",
|
|
57
59
|
"--address",
|
|
58
60
|
"--name",
|
|
59
61
|
"--limit",
|
|
@@ -74,6 +76,7 @@ export function parseMailArgs(args) {
|
|
|
74
76
|
"--home",
|
|
75
77
|
"--dashboard-url",
|
|
76
78
|
"--account",
|
|
79
|
+
"--source",
|
|
77
80
|
"--address",
|
|
78
81
|
"--name",
|
|
79
82
|
"--limit",
|
|
@@ -90,6 +93,12 @@ export function parseMailArgs(args) {
|
|
|
90
93
|
});
|
|
91
94
|
const { action, rest } = readMailAction(values);
|
|
92
95
|
const { subject, query } = readMailSubjectOrQuery(action, rest);
|
|
96
|
+
if (["rename", "enable", "disable", "default", "remove"].includes(action) && !values.flags.get("--account"))
|
|
97
|
+
throw new Error("Name the account with --account <name>.");
|
|
98
|
+
if (action === "rename" && !values.flags.get("--name"))
|
|
99
|
+
throw new Error("Rename needs --name <new name>.");
|
|
100
|
+
if (["enable", "disable"].includes(action) && !["mail", "calendar"].includes(values.flags.get("--source") ?? ""))
|
|
101
|
+
throw new Error("Name --source mail or --source calendar.");
|
|
93
102
|
const address = readImapAddress(action, values);
|
|
94
103
|
const { to, cc } = readMailRecipients(action, values);
|
|
95
104
|
const outPath = readAttachmentPath(action, values);
|
|
@@ -146,11 +155,6 @@ function readMailRecipients(action, values) {
|
|
|
146
155
|
if (MAIL_ACTIONS_COMPOSING.has(action) && to.length === 0) {
|
|
147
156
|
throw new Error(`mail ${action} needs --to "someone@example.com" (comma-separated for several).`);
|
|
148
157
|
}
|
|
149
|
-
if (MAIL_ACTIONS_COMPOSING.has(action) && !optionalNonEmpty(values.flags.get("--account"))) {
|
|
150
|
-
throw new Error(action === "draft"
|
|
151
|
-
? "mail draft needs --account <id>: with several mailboxes attached, which one the draft is saved in is yours to say. `cockpit mail accounts` lists them."
|
|
152
|
-
: "mail send needs --account <id>: with several mailboxes attached, which address this goes out from is yours to say. `cockpit mail accounts` lists them.");
|
|
153
|
-
}
|
|
154
158
|
return { to, cc };
|
|
155
159
|
}
|
|
156
160
|
function readAttachmentPath(action, values) {
|
|
@@ -194,6 +198,7 @@ function buildMailCommand(values, action, subject, query, address, to, cc, outPa
|
|
|
194
198
|
...(subject ? { subjectId: subject } : {}),
|
|
195
199
|
...(query ? { query } : {}),
|
|
196
200
|
accountId: optionalNonEmpty(values.flags.get("--account")),
|
|
201
|
+
source: values.flags.get("--source"),
|
|
197
202
|
address,
|
|
198
203
|
displayName: optionalNonEmpty(values.flags.get("--name")),
|
|
199
204
|
...(limit === undefined ? {} : { limit }),
|
|
@@ -215,6 +215,7 @@ const NOTES_ACTIONS = new Set([
|
|
|
215
215
|
"webhook",
|
|
216
216
|
// Ticket 10718: the Granola cron's own last-several-runs health.
|
|
217
217
|
"granola-status",
|
|
218
|
+
"bot-upcoming",
|
|
218
219
|
]);
|
|
219
220
|
/** Actions whose first positional is the note it acts on. */
|
|
220
221
|
const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
@@ -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,",
|
|
@@ -115,7 +115,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
115
115
|
[
|
|
116
116
|
"Usage: cockpit mail [accounts|add-imap|inbox|read <thread>|search \"<words>\"|send|draft|attachment <id>|sync <account>|sync-status [<account>]|detach <account>] [flags]",
|
|
117
117
|
"",
|
|
118
|
-
"Every mailbox you attached, in one place. One person, several addresses
|
|
118
|
+
"Every mailbox you attached, in one place. One person, several addresses. Writes use a named account or your default.",
|
|
119
|
+
"rename --account <name> --name <label>; enable|disable --account <name> --source mail|calendar; default|remove --account <name>. Switches keep stored data; remove disconnects and deletes Tower copies.",
|
|
119
120
|
"accounts — every mailbox, its provider (gmail_oauth or imap), its health and when it last synced. The ids other verbs take are on the last line.",
|
|
120
121
|
"add-imap --address <you@gmail.com> [--name \"<display name>\"] — attaches a personal Google account over IMAP + SMTP. The APP PASSWORD is read from stdin and there is no flag for it:",
|
|
121
122
|
" macOS: printf \"%s\" \"abcd efgh ijkl mnop\" | cockpit mail add-imap --address you@gmail.com",
|
|
@@ -124,8 +125,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
124
125
|
"inbox [--account <id>] [--unread] [--label <l>] [--limit <n>] [--before <iso>] — everything across every mailbox, newest first. * is unread, @ has an attachment. Every row (text or --json) carries the thread id — `[thread <id>]` in text — so `mail read <thread>` never costs a second --json call to learn it (BLI-10450).",
|
|
125
126
|
"read <thread> — one conversation with its bodies. Thread ids come from `cockpit mail inbox` or `cockpit mail search`, in either --json or plain text.",
|
|
126
127
|
"search \"<words>\" [--account <id>] [--limit <n>] — full text over subject and body. Quotes and OR work the way they do in a search box. Every row (text or --json) carries the thread id.",
|
|
127
|
-
"send --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] [--attach <path>]... — the BODY comes from --file or stdin. --account
|
|
128
|
-
"draft --account <id> --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] [--attach <path>]... — saves the message as a DRAFT in that mailbox for a person to read, edit and send themselves. NOTHING IS SENT and nobody is notified, so there is no confirmation step. Same flags and same body rule as send (--file or stdin). Only the mailbox's owner can draft into it. --reply-to threads the draft into that conversation the way send does; Gmail files it there only when the subject matches the original's, and the answer says whether it did. Gmail mailboxes only: an IMAP mailbox is refused as imap_drafts_unsupported. --json answers { ok, draft: { account_id, address, provider_draft_id, provider_thread_id, threaded } } (BLI-12530).",
|
|
128
|
+
"send [--account <name|id>] --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] [--attach <path>]... — the BODY comes from --file or stdin. Name --account to override your default; with several accounts and no default, Tower asks which one.",
|
|
129
|
+
"draft [--account <name|id>] --to <a[,b]> [--cc <c>] --subject \"<s>\" [--reply-to <message id>] [--file <path>] [--attach <path>]... — saves the message as a DRAFT in that mailbox for a person to read, edit and send themselves. NOTHING IS SENT and nobody is notified, so there is no confirmation step. Same flags and same body rule as send (--file or stdin). Only the mailbox's owner can draft into it. --reply-to threads the draft into that conversation the way send does; Gmail files it there only when the subject matches the original's, and the answer says whether it did. Gmail mailboxes only: an IMAP mailbox is refused as imap_drafts_unsupported. --json answers { ok, draft: { account_id, address, provider_draft_id, provider_thread_id, threaded } } (BLI-12530).",
|
|
129
130
|
"--attach <path> (send and draft; repeat it for each file) — a file that rides along with the message: up to 10, and 3,000,000 bytes together (about 3 MB: a request to Tower is capped at 4.5 MB and a file travels as base64, a third bigger than itself). The type comes from the extension (.docx .pdf .png .jpg .jpeg .txt .csv .md .zip and more; anything else goes as application/octet-stream), the name is the file's own. --file is still the BODY and only the body. Refused BEFORE anything is sent, by name and with the numbers: too_many_attachments, attachment_unreadable (a path that is missing, a folder or empty), attachment_too_large. A reply (--reply-to) keeps its thread with files in it. Works on Gmail mailboxes and on IMAP mailboxes for send; a draft is Gmail only. --json adds { attachments: { count, bytes, content_types } } to the answer. The terminal reports from Tower's own answer, not from what it posted: if Tower does not confirm the files (an older Tower), it exits 1 with attachments_not_confirmed and says whether the message went. Names, paths and contents are never logged; a refusal says \"Attachment 2\", not which file (BLI-12661).",
|
|
130
131
|
"attachment <id> --out <path> — downloads one attachment to a file. Attachments are pointers until you ask; nothing is stored in Tower.",
|
|
131
132
|
"sync <account> — reads that mailbox now instead of waiting for the cron. Prints what landed, or the reason it did not.",
|
|
@@ -152,12 +153,14 @@ export const TOWER_COMMAND_HELP = [
|
|
|
152
153
|
' PowerShell: "https://calendar.google.com/calendar/ical/…/basic.ics" | cockpit cal add-ical --name "Personal"',
|
|
153
154
|
" Find it at calendar.google.com -> the calendar's Settings -> \"Secret address in iCal format\". Anybody who has that link can read the calendar, which is why it never goes on a command line.",
|
|
154
155
|
" A work @buildlaunchiterate.ca calendar does not need this: connect Google in the browser and `cockpit cal calendars` will list them.",
|
|
155
|
-
'create --calendar <id> --title "<what>" --at <iso> --until <iso> [--location "<where>"] [--attendee a@x,b@y] [--all-day] [--meet] — creates the event AT GOOGLE, then here. Attendees are invited by Google; --meet asks for a Google Meet room and prints its address (or says meet_not_available and keeps the event).',
|
|
156
|
+
'create [--account <name>|--calendar <id>] --title "<what>" --at <iso> --until <iso> [--location "<where>"] [--attendee a@x,b@y] [--all-day] [--meet] — creates the event AT GOOGLE, then here. Attendees are invited by Google; --meet asks for a Google Meet room and prints its address (or says meet_not_available and keeps the event).',
|
|
156
157
|
" Only a Google calendar can be written to: an iCal secret address is a read address, and `write_not_supported_for_provider` says so by name.",
|
|
157
158
|
"share <id> --org-visible|--private — makes one of YOUR calendars readable by everyone at BLI, or private again. Sharing never lets anybody else change it.",
|
|
158
159
|
"sync <id> [--full] — reads that calendar now instead of waiting for the cron. --full ignores the sync token / ETag and re-reads everything.",
|
|
159
160
|
"sync-status [<id>] — every calendar's health, or one's: last successful sync, the last pass's own reason, how many events it carried, failures in the last 24h, and the lag between an event's own provider clock and when Tower stored it (ticket 10730).",
|
|
160
161
|
"detach <id> — removes the calendar and its stored events from Tower. The calendar itself is untouched at Google.",
|
|
162
|
+
"change <event id> [--account <name>] [--title <title>] [--at <iso> --until <iso>] [--location <place>] — changes an event on its own account.",
|
|
163
|
+
"cancel <event id> --yes [--account <name>] — same door as delete; a named account must own the event.",
|
|
161
164
|
"delete <event id> --yes — removes ONE event, AT GOOGLE first, then here. Refuses without --yes: a delete can email everybody on it, exactly like create's invitations do. Google already having it gone (404/410) is answered as success, not a refusal.",
|
|
162
165
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
163
166
|
"A refusal keeps Tower's own reason label — needs_rls_client, calendar_not_found_or_unreadable, calendar_not_yours, event_not_found_or_unreadable, ical_url_invalid, google_oauth_not_configured, write_not_supported_for_provider, and so on.",
|
|
@@ -392,6 +395,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
392
395
|
"detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
|
|
393
396
|
"sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
|
|
394
397
|
"sync-status [<account-id>] — every notetaker's health, or one's: last successful sync, the last pass's own reason, how many meetings it carried, failures in the last 24h, and the lag between the newest meeting it has read and when it last ran (ticket 10730).",
|
|
398
|
+
"bot-upcoming [--json] — your own Google Meet meetings today that the bot will join, with claim state. Decline or mark private to opt out.",
|
|
395
399
|
"granola-status [--limit <n>] [--json] — the Granola cron's own last several runs (ticket 10718): reason, counts, model, spend and which lookback it used. Granola runs on a shared server-side key, not a connected account, so this is its own read rather than `accounts`.",
|
|
396
400
|
"A large note is read by a model on the server and can take a couple of minutes; the terminal says so before it waits.",
|
|
397
401
|
"--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
export async function runMailAccountPreference(command, door) {
|
|
4
|
+
const removing = command.action === "remove";
|
|
5
|
+
const answer = await askAgentDoor(door, {
|
|
6
|
+
path: removing ? `/api/mail/accounts?account=${encodeURIComponent(command.accountId)}` : "/api/mail/accounts",
|
|
7
|
+
method: removing ? "DELETE" : "PATCH", label: `mail ${command.action}`, timeoutMs: 30_000,
|
|
8
|
+
...(!removing ? { body: { account: command.accountId, ...(command.action === "rename" ? { account_label: command.displayName } : command.action === "default" ? { is_default: true } : { [`${command.source}_enabled`]: command.action === "enable" }) } } : {}),
|
|
9
|
+
});
|
|
10
|
+
if (!answer.ok)
|
|
11
|
+
return failAgentDoor(door, "[accounts cli]", answer.reason, answer.detail);
|
|
12
|
+
if (door.json)
|
|
13
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
14
|
+
writeLine(door.io.stdout, `Account ${command.accountId}: ${command.action} completed.`);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
@@ -63,7 +63,7 @@ export async function readMailBody(command, door) {
|
|
|
63
63
|
*/
|
|
64
64
|
export function composedMessageBody(command, text, attachments = []) {
|
|
65
65
|
return {
|
|
66
|
-
account_id: command.accountId,
|
|
66
|
+
...(command.accountId ? /^[0-9a-f-]{36}$/i.test(command.accountId) ? { account_id: command.accountId } : { account: command.accountId } : {}),
|
|
67
67
|
to: command.to.map((address) => ({ address })),
|
|
68
68
|
...(command.cc.length > 0 ? { cc: command.cc.map((address) => ({ address })) } : {}),
|
|
69
69
|
subject: command.subjectLine ?? "",
|
|
@@ -164,7 +164,7 @@ export async function runMailDraft(command, door) {
|
|
|
164
164
|
return failAttachmentsUnconfirmed(door, "draft", unconfirmed, draft);
|
|
165
165
|
if (door.json)
|
|
166
166
|
return emitAgentDoor(door, { ok: true, draft, ...message.receipt.json });
|
|
167
|
-
writeLine(door.io.stdout, `Saved a draft in ${draft?.address ?? "that mailbox"} for ${command.to.join(", ")}. `
|
|
167
|
+
writeLine(door.io.stdout, `Saved a draft in ${draft?.account_name ?? draft?.address ?? "that mailbox"} for ${command.to.join(", ")}. `
|
|
168
168
|
+ "Nothing was sent: it is in Drafts for a person to read, edit and send.");
|
|
169
169
|
if (message.receipt.sentence)
|
|
170
170
|
writeLine(door.io.stdout, message.receipt.sentence);
|
package/dist/commands/mail.js
CHANGED
|
@@ -26,7 +26,8 @@ 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
|
+
import { runMailAccountPreference } from "./mail-account-preferences.js";
|
|
30
31
|
import { towerRequest } from "../tower-client.js";
|
|
31
32
|
const TAG = "[mail cli]";
|
|
32
33
|
const READ_DEADLINE_MS = 30_000;
|
|
@@ -37,6 +38,12 @@ const PASSWORD_MAX_CHARS = 200;
|
|
|
37
38
|
export async function runMail(command, io) {
|
|
38
39
|
const door = await openAgentDoor("mail", command, io);
|
|
39
40
|
switch (command.action) {
|
|
41
|
+
case "rename":
|
|
42
|
+
case "enable":
|
|
43
|
+
case "disable":
|
|
44
|
+
case "default":
|
|
45
|
+
case "remove":
|
|
46
|
+
return runMailAccountPreference(command, door);
|
|
40
47
|
case "accounts":
|
|
41
48
|
return listAccounts(door);
|
|
42
49
|
case "add-imap":
|
|
@@ -82,7 +89,7 @@ async function listAccounts(door) {
|
|
|
82
89
|
}
|
|
83
90
|
for (const account of accounts) {
|
|
84
91
|
const health = account.status === "active" ? account.backfill_state : `${account.status}: ${account.status_reason ?? "no reason recorded"}`;
|
|
85
|
-
writeLine(door.io.stdout, `${account.address.padEnd(34)} ${account.provider.padEnd(12)} ${health.padEnd(18)} ${account.last_sync_at ?? "never synced"}`);
|
|
92
|
+
writeLine(door.io.stdout, `${(account.account_label ?? account.address).padEnd(20)} ${account.is_default ? "default" : ""} mail=${account.mail_enabled !== false} calendar=${account.calendar_enabled !== false} ${account.address.padEnd(34)} ${account.provider.padEnd(12)} ${health.padEnd(18)} ${account.last_sync_at ?? "never synced"}`);
|
|
86
93
|
}
|
|
87
94
|
writeLine(door.io.stdout, "");
|
|
88
95
|
writeLine(door.io.stdout, `${accounts.length} mailbox(es). ids: ${accounts.map((account) => account.id).join(", ")}`);
|
|
@@ -177,7 +184,7 @@ async function inbox(command, door) {
|
|
|
177
184
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
178
185
|
const body = answer.body;
|
|
179
186
|
if (door.json)
|
|
180
|
-
return emitAgentDoor(door, { ok: true, ...body });
|
|
187
|
+
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
181
188
|
if (answer.fromCache) {
|
|
182
189
|
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
183
190
|
}
|
|
@@ -279,7 +286,7 @@ async function send(command, door) {
|
|
|
279
286
|
return failAttachmentsUnconfirmed(door, "send", unconfirmed, sent);
|
|
280
287
|
if (door.json)
|
|
281
288
|
return emitAgentDoor(door, { ok: true, sent, ...message.receipt.json });
|
|
282
|
-
writeLine(door.io.stdout, `Sent from ${sent?.address ?? "that mailbox"} to ${command.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.`);
|
|
289
|
+
writeLine(door.io.stdout, `Sent from ${sent?.account_name ?? sent?.address ?? "that mailbox"} to ${command.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.`);
|
|
283
290
|
if (message.receipt.sentence)
|
|
284
291
|
writeLine(door.io.stdout, message.receipt.sentence);
|
|
285
292
|
return 0;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ask, emit, fail, READ_DEADLINE_MS } from "./notes-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
export async function showBotUpcoming(_command, door) {
|
|
4
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
5
|
+
const answer = await ask(door, { path: `/api/notetaker/upcoming?tz=${encodeURIComponent(tz)}`, method: "GET", label: "bot upcoming", timeoutMs: READ_DEADLINE_MS });
|
|
6
|
+
if (!answer.ok)
|
|
7
|
+
return fail(door, answer.reason, answer.detail);
|
|
8
|
+
if (door.json)
|
|
9
|
+
return emit(door, answer.body);
|
|
10
|
+
const body = answer.body;
|
|
11
|
+
if (!body.meetings.length)
|
|
12
|
+
writeLine(door.io.stdout, "No meetings for the bot today.");
|
|
13
|
+
for (const meeting of body.meetings)
|
|
14
|
+
writeLine(door.io.stdout, `${meeting.startsAt} ${meeting.claim.status} ${meeting.meetUrl}`);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
@@ -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
|
}
|
package/dist/commands/notes.js
CHANGED
|
@@ -44,6 +44,7 @@ import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
|
44
44
|
import { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
45
45
|
import { runFolderCommand } from "./notes-folders.js";
|
|
46
46
|
import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
47
|
+
import { showBotUpcoming } from "./notes-bot-upcoming.js";
|
|
47
48
|
import { showGranolaStatus } from "./notes-granola-status.js";
|
|
48
49
|
import { listRecordings, showRecording } from "./notes-recordings.js";
|
|
49
50
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
@@ -106,6 +107,8 @@ export async function runNotes(command, io) {
|
|
|
106
107
|
return showRecording(command, door);
|
|
107
108
|
case "webhook":
|
|
108
109
|
return armNotesWebhook(command, door);
|
|
110
|
+
case "bot-upcoming":
|
|
111
|
+
return showBotUpcoming(command, door);
|
|
109
112
|
case "granola-status":
|
|
110
113
|
return showGranolaStatus(command, door);
|
|
111
114
|
}
|
|
@@ -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.199");
|
|
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.199",
|
|
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.111",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.69"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|