@bli-cockpit/cli 0.2.236 → 0.2.238

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.
Files changed (143) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +106 -0
  29. package/dist/commands/cal.js +40 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/doctor-usage-telemetry.js +2 -1
  35. package/dist/commands/heartbeat-post.js +116 -0
  36. package/dist/commands/heartbeat-token-lock.js +46 -33
  37. package/dist/commands/heartbeat.js +43 -125
  38. package/dist/commands/install-update.js +17 -15
  39. package/dist/commands/issue-show.js +27 -28
  40. package/dist/commands/issue-write.js +70 -23
  41. package/dist/commands/issue.js +68 -37
  42. package/dist/commands/jarvis-live-answer.js +99 -0
  43. package/dist/commands/jarvis-render.js +1 -94
  44. package/dist/commands/jarvis-trace.js +30 -22
  45. package/dist/commands/jarvis-turn-report.js +124 -0
  46. package/dist/commands/jarvis-turn.js +108 -182
  47. package/dist/commands/jarvis.js +26 -17
  48. package/dist/commands/local-args-collector-setup.js +82 -84
  49. package/dist/commands/local-args-collector-work.js +37 -38
  50. package/dist/commands/local-args-tower-admin.js +155 -142
  51. package/dist/commands/local-args-tower-cal.js +54 -48
  52. package/dist/commands/local-args-tower-careers.js +73 -54
  53. package/dist/commands/local-args-tower-docs-msg.js +56 -188
  54. package/dist/commands/local-args-tower-github.js +26 -20
  55. package/dist/commands/local-args-tower-hits.js +32 -20
  56. package/dist/commands/local-args-tower-manager.js +85 -4
  57. package/dist/commands/local-args-tower-memory.js +139 -117
  58. package/dist/commands/local-args-tower-msg.js +149 -0
  59. package/dist/commands/local-args-tower-notes.js +346 -0
  60. package/dist/commands/local-args-tower-pages.js +100 -378
  61. package/dist/commands/local-args-tower-settings.js +28 -29
  62. package/dist/commands/local-args-tower-work.js +52 -33
  63. package/dist/commands/local-args.js +80 -129
  64. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  65. package/dist/commands/local-help-commands-tower.js +11 -4
  66. package/dist/commands/local-help-commands.js +342 -341
  67. package/dist/commands/local-help.js +87 -83
  68. package/dist/commands/local-run-guards.js +100 -0
  69. package/dist/commands/local.js +137 -206
  70. package/dist/commands/login.js +13 -9
  71. package/dist/commands/logs.js +55 -38
  72. package/dist/commands/mail-attachments.js +44 -26
  73. package/dist/commands/mail.js +6 -4
  74. package/dist/commands/manager-render-home.js +208 -0
  75. package/dist/commands/manager-render.js +7 -3
  76. package/dist/commands/manager-requests.js +33 -0
  77. package/dist/commands/manager.js +39 -5
  78. package/dist/commands/memory-consolidation.js +18 -9
  79. package/dist/commands/memory-daemon-probe.js +37 -28
  80. package/dist/commands/memory-install-config.js +64 -60
  81. package/dist/commands/memory-install.js +21 -41
  82. package/dist/commands/memory-requeue.js +26 -19
  83. package/dist/commands/memory-rules.js +26 -18
  84. package/dist/commands/memory-search-print.js +26 -0
  85. package/dist/commands/memory-store.js +14 -30
  86. package/dist/commands/memory-verb-list.js +49 -48
  87. package/dist/commands/msg.js +51 -9
  88. package/dist/commands/notes-accounts.js +49 -32
  89. package/dist/commands/notes-sync-loop.js +115 -89
  90. package/dist/commands/notes-wisprflow-login.js +44 -23
  91. package/dist/commands/notes-writes.js +115 -100
  92. package/dist/commands/notes.js +33 -55
  93. package/dist/commands/onboard-flows.js +69 -51
  94. package/dist/commands/onboard.js +11 -9
  95. package/dist/commands/ops-render-memory.js +10 -7
  96. package/dist/commands/ops-render.js +33 -30
  97. package/dist/commands/ops.js +3 -0
  98. package/dist/commands/public-root.js +1 -1
  99. package/dist/commands/session-sync-outside-roots.js +40 -36
  100. package/dist/commands/session-sync-plan.js +14 -10
  101. package/dist/commands/session-sync-upload.js +24 -22
  102. package/dist/commands/session-sync.js +19 -8
  103. package/dist/commands/slack.js +25 -16
  104. package/dist/commands/sources-check.js +13 -7
  105. package/dist/commands/sources-discovery.js +25 -17
  106. package/dist/commands/sources-verdict.js +19 -18
  107. package/dist/commands/speed.js +23 -19
  108. package/dist/commands/start.js +20 -16
  109. package/dist/commands/sync-doctor.js +52 -48
  110. package/dist/commands/sync.js +22 -16
  111. package/dist/commands/usage-telemetry-toml.js +71 -45
  112. package/dist/commands/usage.js +91 -67
  113. package/dist/commands/workbook-render.js +10 -7
  114. package/dist/evidence-reconcile-client.js +75 -64
  115. package/dist/evidence-redelivery-plan.js +37 -34
  116. package/dist/evidence-redelivery.js +32 -29
  117. package/dist/evidence-transcripts-client.js +20 -20
  118. package/dist/evidence-upload-object.js +86 -89
  119. package/dist/evidence-upload-plan.js +94 -83
  120. package/dist/evidence-upload-terminal.js +45 -50
  121. package/dist/image-description-session.js +156 -159
  122. package/dist/local-state-pairing-code.js +39 -31
  123. package/dist/local-state-status.js +9 -2
  124. package/dist/onboarding-roots.js +5 -8
  125. package/dist/process-runner.js +8 -8
  126. package/dist/raw-evidence-gc.js +33 -38
  127. package/dist/raw-evidence-stuck-summary.js +7 -14
  128. package/dist/repo-identity.js +26 -23
  129. package/dist/scheduled-self-update.js +11 -15
  130. package/dist/second-install.js +3 -0
  131. package/dist/slim/codex-rules.js +23 -36
  132. package/dist/sync-lock.js +5 -15
  133. package/dist/tower-client.js +36 -53
  134. package/dist/tower-stream.js +7 -24
  135. package/dist/upload-agent-artifacts.js +58 -64
  136. package/dist/upload-envelope-build.js +60 -59
  137. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  138. package/dist/upload-evidence-delivery-summary.js +32 -34
  139. package/dist/upload-ingest-receipt.js +20 -23
  140. package/dist/upload-session-reports-queue.js +4 -1
  141. package/dist/upload-session-reports-wire.js +64 -68
  142. package/dist/upload-sync.js +171 -157
  143. package/package.json +2 -2
@@ -41,54 +41,20 @@ export async function runBriefEdit(command, io) {
41
41
  const log = (line) => writeLine(io.stderr, line);
42
42
  const startedAt = Date.now();
43
43
  // ── 1. The page, as a document ────────────────────────────────────────────
44
- const read = await towerJsonRequest({
45
- dashboardUrl,
46
- path: `/api/jarvis/brief${briefQuery(command)}`,
47
- deviceToken: session.device_token,
48
- fetch: io.fetch,
49
- method: "GET",
50
- label: "brief:edit",
51
- timeoutMs: REQUEST_DEADLINE_MS,
52
- log,
53
- });
54
- if (!read.ok)
55
- return fail(command, io, read.reason, read.detail);
56
- const brief = read.body;
57
- const personId = brief.page?.personId;
58
- const original = brief.editable?.document;
59
- const lines = (brief.editable?.lines ?? []).filter((line) => typeof line.claimId === "string" && typeof line.text === "string");
60
- if (!brief.ok || !personId || typeof original !== "string" || lines.length === 0) {
61
- return fail(command, io, brief.error ?? "no_page", brief.reply ?? "There is no page to edit.");
62
- }
44
+ const page = await readEditablePage(command, io, session, dashboardUrl, log);
45
+ if (!page.ok)
46
+ return fail(command, io, page.reason, page.detail);
63
47
  // ── 2. Their editor, or their stdin ───────────────────────────────────────
64
- const edited = await collectEditedDocument(io, original, log);
48
+ const edited = await collectEditedDocument(io, page.original, log);
65
49
  if (!edited.ok)
66
50
  return fail(command, io, edited.reason, edited.detail);
67
51
  // ── 3. The diff, here ─────────────────────────────────────────────────────
68
- const diff = diffDocument(lines, edited.text);
69
- if (diff.invented.length > 0) {
70
- return fail(command, io, "invented_claim", `The page has no line called “${diff.invented[0]}”. Change the words after an id, never the id ` +
71
- "itself — nothing has been sent to Tower.");
72
- }
73
- if (diff.deleted.length > 0) {
74
- return fail(command, io, "deleted_claim", `The line [${diff.deleted[0]}] is missing from what you saved. Put it back — a line you delete ` +
75
- "reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart. " +
76
- "Nothing has been sent.");
77
- }
78
- if (diff.edits.length === 0) {
79
- const styled = colorEnabled(io);
80
- if (command.json) {
81
- writeLine(io.stdout, JSON.stringify({ ok: true, saved: 0, reason: "nothing_changed" }));
82
- }
83
- else {
84
- writeLine(io.stdout, dim("Nothing changed, so nothing was written.", styled));
85
- }
86
- writeLine(io.stderr, `[brief edit] nothing changed ${JSON.stringify({
87
- lines: lines.length,
88
- elapsed_ms: Date.now() - startedAt,
89
- })}`);
90
- return 0;
91
- }
52
+ const diff = diffDocument(page.lines, edited.text);
53
+ const refusal = refuseBadDiff(diff);
54
+ if (refusal)
55
+ return fail(command, io, refusal.reason, refusal.detail);
56
+ if (diff.edits.length === 0)
57
+ return reportNothingChanged(command, io, page.lines.length, startedAt);
92
58
  // ── 4. The same door the browser's save posts to ──────────────────────────
93
59
  const saved = await towerJsonRequest({
94
60
  dashboardUrl,
@@ -99,8 +65,8 @@ export async function runBriefEdit(command, io) {
99
65
  timeoutMs: REQUEST_DEADLINE_MS,
100
66
  log,
101
67
  body: {
102
- personId,
103
- pageId: brief.page?.pageId ?? null,
68
+ personId: page.personId,
69
+ pageId: page.pageId,
104
70
  edits: diff.edits,
105
71
  ...(command.reason ? { reason: command.reason } : {}),
106
72
  },
@@ -122,6 +88,65 @@ export async function runBriefEdit(command, io) {
122
88
  })}`);
123
89
  return 0;
124
90
  }
91
+ async function readEditablePage(command, io, session, dashboardUrl, log) {
92
+ const read = await towerJsonRequest({
93
+ dashboardUrl,
94
+ path: `/api/jarvis/brief${briefQuery(command)}`,
95
+ deviceToken: session.device_token,
96
+ fetch: io.fetch,
97
+ method: "GET",
98
+ label: "brief:edit",
99
+ timeoutMs: REQUEST_DEADLINE_MS,
100
+ log,
101
+ });
102
+ if (!read.ok)
103
+ return { ok: false, reason: read.reason, detail: read.detail };
104
+ const brief = read.body;
105
+ const personId = brief.page?.personId;
106
+ const original = brief.editable?.document;
107
+ const lines = (brief.editable?.lines ?? []).filter((line) => typeof line.claimId === "string" && typeof line.text === "string");
108
+ if (!brief.ok || !personId || typeof original !== "string" || lines.length === 0) {
109
+ return {
110
+ ok: false,
111
+ reason: brief.error ?? "no_page",
112
+ detail: brief.reply ?? "There is no page to edit.",
113
+ };
114
+ }
115
+ return { ok: true, personId, pageId: brief.page?.pageId ?? null, original, lines };
116
+ }
117
+ /** The refusal for a diff that invented or deleted a claim id, or null when the diff may be sent. */
118
+ function refuseBadDiff(diff) {
119
+ if (diff.invented.length > 0) {
120
+ return {
121
+ reason: "invented_claim",
122
+ detail: `The page has no line called “${diff.invented[0]}”. Change the words after an id, never the id ` +
123
+ "itself — nothing has been sent to Tower.",
124
+ };
125
+ }
126
+ if (diff.deleted.length > 0) {
127
+ return {
128
+ reason: "deleted_claim",
129
+ detail: `The line [${diff.deleted[0]}] is missing from what you saved. Put it back — a line you delete ` +
130
+ "reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart. " +
131
+ "Nothing has been sent.",
132
+ };
133
+ }
134
+ return null;
135
+ }
136
+ function reportNothingChanged(command, io, lineCount, startedAt) {
137
+ const styled = colorEnabled(io);
138
+ if (command.json) {
139
+ writeLine(io.stdout, JSON.stringify({ ok: true, saved: 0, reason: "nothing_changed" }));
140
+ }
141
+ else {
142
+ writeLine(io.stdout, dim("Nothing changed, so nothing was written.", styled));
143
+ }
144
+ writeLine(io.stderr, `[brief edit] nothing changed ${JSON.stringify({
145
+ lines: lineCount,
146
+ elapsed_ms: Date.now() - startedAt,
147
+ })}`);
148
+ return 0;
149
+ }
125
150
  function briefQuery(command) {
126
151
  const params = new URLSearchParams({ editable: "1" });
127
152
  if (command.subject)
@@ -0,0 +1,35 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { writeLine } from "./cli-io.js";
3
+ export async function runCalDetail(command, door) {
4
+ const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
5
+ if (command.occurrenceStart)
6
+ query.set("occurrence_start", command.occurrenceStart);
7
+ const path = `/api/cal/events/${encodeURIComponent(command.subjectId)}${command.action === "export" ? "/export" : ""}?${query}`;
8
+ const answer = await askAgentDoor(door, { method: "GET", path, label: `cal ${command.action}`, timeoutMs: 30000 });
9
+ if (!answer.ok)
10
+ return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
11
+ if (door.json)
12
+ return emitAgentDoor(door, answer.body);
13
+ if (command.action === "export") {
14
+ writeLine(door.io.stdout, String(answer.body.content));
15
+ return 0;
16
+ }
17
+ const event = answer.body.event;
18
+ const zone = command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
19
+ const date = new Date(event.starts_at).toLocaleDateString("en-CA", { timeZone: event.all_day ? "UTC" : zone });
20
+ const clock = (value) => new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(value));
21
+ writeLine(door.io.stdout, `${event.title} · ${date} · ${event.all_day ? "All day" : `${clock(event.starts_at)}–${clock(event.ends_at)}`} · ${zone} · ${event.calendar_name}`);
22
+ writeLine(door.io.stdout, event.recurrence_summary);
23
+ if (event.recurrence_coverage_note)
24
+ writeLine(door.io.stdout, event.recurrence_coverage_note);
25
+ writeLine(door.io.stdout, `Organizer: ${event.organizer_detail?.name ?? "Unknown"}`);
26
+ for (const guest of event.guests)
27
+ writeLine(door.io.stdout, `${guest.name} · ${guest.rsvp}${guest.optional ? " · Optional" : ""}`);
28
+ if (event.meeting_url)
29
+ writeLine(door.io.stdout, `Join: ${event.meeting_url}`);
30
+ if (event.location)
31
+ writeLine(door.io.stdout, `Location: ${event.location}`);
32
+ if (event.capabilities.reason)
33
+ writeLine(door.io.stdout, event.capabilities.reason);
34
+ return 0;
35
+ }
@@ -0,0 +1,106 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { readPipedText, isInteractiveStdin, writeLine } from "./cli-io.js";
3
+ const FAIL_TAG = "[cal cli]";
4
+ const REQUEST_TIMEOUT_MS = 30000;
5
+ export async function runCalOverlay(command, door) {
6
+ if (command.action === "calendar-change")
7
+ return changeCalendarPreferences(command, door);
8
+ return showConflicts(command, door);
9
+ }
10
+ async function changeCalendarPreferences(command, door) {
11
+ let body = command.color ? { color_override: command.color === "provider" ? null : command.color } : {};
12
+ if (!command.color && !isInteractiveStdin(door.io)) {
13
+ const piped = await readPipedPreferences(door);
14
+ if (piped === null) {
15
+ return failAgentDoor(door, FAIL_TAG, "invalid_body", "Pipe JSON preferences or use --color '#4285f4' (provider resets).");
16
+ }
17
+ body = piped;
18
+ }
19
+ if (!Object.keys(body).length) {
20
+ return failAgentDoor(door, FAIL_TAG, "invalid_body", "Use --color '#4285f4', --color provider, or pipe JSON with color_override/selected.");
21
+ }
22
+ const answer = await askAgentDoor(door, {
23
+ method: "PATCH",
24
+ path: `/api/cal/calendars/${encodeURIComponent(command.subjectId)}`,
25
+ body,
26
+ label: "cal calendar-change",
27
+ timeoutMs: REQUEST_TIMEOUT_MS,
28
+ });
29
+ if (!answer.ok)
30
+ return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
31
+ if (door.json)
32
+ return emitAgentDoor(door, answer.body);
33
+ writeLine(door.io.stdout, "Calendar preferences saved. Provider sync preserves your colour override.");
34
+ return 0;
35
+ }
36
+ /** Piped JSON preferences, or null when the pipe is unreadable or not a JSON object. */
37
+ async function readPipedPreferences(door) {
38
+ try {
39
+ const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 4000, overflowMessage: "Preferences are too large." }));
40
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
41
+ return null;
42
+ return parsed;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function showConflicts(command, door) {
49
+ const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
50
+ if (command.from)
51
+ query.set("from", command.from);
52
+ if (command.to)
53
+ query.set("to", command.to);
54
+ if (command.offset !== undefined)
55
+ query.set("offset", String(command.offset));
56
+ if (command.includeDeselected)
57
+ query.set("all", "true");
58
+ const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs: REQUEST_TIMEOUT_MS });
59
+ if (!answer.ok)
60
+ return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
61
+ if (door.json)
62
+ return emitAgentDoor(door, answer.body);
63
+ const result = answer.body;
64
+ const zone = result.window?.time_zone ?? command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
65
+ writeConflictGroups(door, result, zone);
66
+ writeCoverage(door, result);
67
+ return 0;
68
+ }
69
+ function writeConflictGroups(door, result, zone) {
70
+ writeLine(door.io.stdout, `${result.groups.length} double-bookings · own calendars`);
71
+ for (const group of result.groups) {
72
+ const parts = new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date(group.from));
73
+ const part = (name) => parts.find(item => item.type === name)?.value;
74
+ const meetingTitles = group.meetings.map(meeting => `${meeting.event.title} · ${meeting.event.calendar_name}`).join(" / ");
75
+ writeLine(door.io.stdout, `${part("weekday")} ${part("day")} ${part("month")} ${part("hour")}:${part("minute")}: ${meetingTitles}`);
76
+ }
77
+ }
78
+ function describeSyncAge(checked) {
79
+ const oldest = Math.min(...checked.map(calendar => calendar.lastSyncAt ? Date.parse(calendar.lastSyncAt) : NaN));
80
+ const minutes = Math.max(0, Math.floor((Date.now() - oldest) / 60000));
81
+ if (!checked.length)
82
+ return "none checked";
83
+ if (!Number.isFinite(oldest))
84
+ return "sync age unknown";
85
+ if (minutes < 1)
86
+ return "just now";
87
+ if (minutes < 60)
88
+ return `${minutes} min ago`;
89
+ if (minutes < 1440)
90
+ return `${Math.floor(minutes / 60)} h ago`;
91
+ return `${Math.floor(minutes / 1440)} days ago`;
92
+ }
93
+ function writeCoverage(door, result) {
94
+ const checked = result.coverage.calendars.filter(calendar => calendar.checked);
95
+ writeLine(door.io.stdout, `Checked ${checked.length} calendar${checked.length === 1 ? "" : "s"} · ${describeSyncAge(checked)}`);
96
+ for (const calendar of result.coverage.calendars.filter(calendar => calendar.hidden || !calendar.checked)) {
97
+ writeLine(door.io.stdout, `${calendar.name}: ${calendar.hidden ? "hidden; " : ""}${calendar.checked ? "included by request" : `not checked (${(calendar.excludedReason ?? "unavailable").replaceAll("_", " ")})`}`);
98
+ }
99
+ for (const account of result.coverage.accounts ?? []) {
100
+ if (!account.calendars)
101
+ writeLine(door.io.stdout, `${account.label}: no discovered calendars (${account.status.replaceAll("_", " ")})`);
102
+ }
103
+ if (result.coverage.incomplete || result.coverage.truncated || result.coverage.notes.length) {
104
+ writeLine(door.io.stdout, "Incomplete window. This check cannot confirm free time.");
105
+ }
106
+ }
@@ -22,6 +22,8 @@
22
22
  */
23
23
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
24
24
  import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-calendars-write.js";
25
+ import { runCalDetail } from "./cal-detail.js";
26
+ import { runCalOverlay } from "./cal-overlay.js";
25
27
  import { runCalDelete } from "./cal-delete.js";
26
28
  import { runCalSyncStatus } from "./cal-sync-status.js";
27
29
  import { writeLine } from "./cli-io.js";
@@ -32,6 +34,12 @@ const WRITE_DEADLINE_MS = 60_000;
32
34
  export async function runCal(command, io) {
33
35
  const door = await openAgentDoor("cal", command, io);
34
36
  switch (command.action) {
37
+ case "event":
38
+ case "export":
39
+ return runCalDetail(command, door);
40
+ case "conflicts":
41
+ case "calendar-change":
42
+ return runCalOverlay(command, door);
35
43
  case "today":
36
44
  case "week":
37
45
  case "next":
@@ -91,6 +99,19 @@ function windowQuery(command) {
91
99
  return query;
92
100
  }
93
101
  async function window(command, door) {
102
+ const answer = await askForWindow(command, door);
103
+ if (!answer.ok)
104
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
105
+ const body = answer.body;
106
+ if (door.json)
107
+ return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
108
+ if (answer.fromCache) {
109
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
110
+ }
111
+ writeWindow(body, door);
112
+ return 0;
113
+ }
114
+ async function askForWindow(command, door) {
94
115
  // `--from`/`--to` beat a named window, on any of the three verbs: somebody
95
116
  // who named two dates meant them.
96
117
  const path = command.from && command.to ? "events" : command.action;
@@ -105,33 +126,27 @@ async function window(command, door) {
105
126
  timeoutMs: READ_DEADLINE_MS,
106
127
  warmRoute: cached,
107
128
  });
108
- const answer = cached
109
- ? await readThroughCache({
110
- io: door.io,
111
- homeDir: door.homeDir,
112
- deviceToken: door.deviceToken,
113
- surface: "cal",
114
- label: "cal today",
115
- path: apiPath,
116
- json: door.json,
117
- ask,
118
- })
119
- : { ...(await ask()), fromCache: false, ageMs: null };
120
- if (!answer.ok)
121
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
122
- const body = answer.body;
123
- if (door.json)
124
- return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
125
- if (answer.fromCache) {
126
- writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
127
- }
129
+ if (!cached)
130
+ return { ...(await ask()), fromCache: false, ageMs: null };
131
+ return readThroughCache({
132
+ io: door.io,
133
+ homeDir: door.homeDir,
134
+ deviceToken: door.deviceToken,
135
+ surface: "cal",
136
+ label: "cal today",
137
+ path: apiPath,
138
+ json: door.json,
139
+ ask,
140
+ });
141
+ }
142
+ function writeWindow(body, door) {
128
143
  const zone = body.window?.time_zone ?? localTimeZone();
129
144
  const events = body.events ?? [];
130
145
  if (events.length === 0) {
131
146
  writeLine(door.io.stdout, (body.calendars ?? []).length === 0
132
147
  ? "No calendars attached yet. `cockpit cal add-ical` for a personal one, or connect Google in the browser."
133
148
  : `Nothing on ${describeWindow(body, zone)}.`);
134
- return 0;
149
+ return;
135
150
  }
136
151
  writeLine(door.io.stdout, describeWindow(body, zone));
137
152
  writeLine(door.io.stdout, "");
@@ -152,7 +167,6 @@ async function window(command, door) {
152
167
  for (const note of body.notes ?? []) {
153
168
  writeLine(door.io.stdout, `Note: "${note.title}" repeats on a rule Tower does not expand (${note.detail.join(", ")}), so only its first occurrence is shown.`);
154
169
  }
155
- return 0;
156
170
  }
157
171
  function describeWindow(body, zone) {
158
172
  if (!body.window)
@@ -293,6 +307,10 @@ async function create(command, door) {
293
307
  const event = answer.body.event;
294
308
  if (door.json)
295
309
  return emitAgentDoor(door, { ok: true, event });
310
+ writeCreated(command, door, event);
311
+ return 0;
312
+ }
313
+ function writeCreated(command, door, event) {
296
314
  const zone = command.timeZone ?? localTimeZone();
297
315
  writeLine(door.io.stdout, `Created "${event?.title ?? command.title}" on ${event ? `${dayLabel(event.starts_at, zone)} ${clock(event.starts_at, zone)}` : "that calendar"}.`);
298
316
  if (event?.event_id) {
@@ -313,7 +331,6 @@ async function create(command, door) {
313
331
  if (command.attendees.length > 0) {
314
332
  writeLine(door.io.stdout, `Invitations went to ${command.attendees.join(", ")} from Google.`);
315
333
  }
316
- return 0;
317
334
  }
318
335
  function meetingLinkText(event) {
319
336
  const label = { meet: "Meet link", zoom: "Zoom link", teams: "Teams link" }[event.meeting_kind ?? ""] ?? "Meeting link";
@@ -83,11 +83,14 @@ export async function runClean(command, io) {
83
83
  writeLine(io.stdout, cleanJson(footprint, plan, vaults, pruned, reconciled));
84
84
  return pruned.status === "fail" ? 1 : 0;
85
85
  }
86
+ writeFreedSummary(io, pruned, removedVaults);
87
+ return pruned.status === "fail" ? 1 : 0;
88
+ }
89
+ function writeFreedSummary(io, pruned, removedVaults) {
86
90
  writeLine(io.stdout, `Freed ${mb(pruned.deleted_bytes)} MB across ${pruned.deleted_files} file(s) and ${pruned.removed_packs} pack(s)${removedVaults > 0 ? `, plus ${removedVaults} evidence vault(s)` : ""}.`);
87
91
  if (pruned.cap_blocked_by_uncommitted) {
88
92
  writeLine(io.stdout, `Still ${mb(pruned.bytes_after)} MB over a ${mb(pruned.cap_bytes)} MB cap, held by ${pruned.cap_blocked_count} object(s) the upload ledger cannot vouch for. Run \`cockpit sync\` to try delivering them; nothing here will delete them.`);
89
93
  }
90
- return pruned.status === "fail" ? 1 : 0;
91
94
  }
92
95
  /** The bucket table a person reads before anything is deleted. */
93
96
  export function cleanLines(footprint, plan, vaults, command, logArchives = []) {
@@ -166,8 +166,11 @@ export async function readDoc(command, door) {
166
166
  writeDocumentBlock(door.io, result.document);
167
167
  return 0;
168
168
  }
169
- // Several refs: `{ ok, documents: [{ ref, ok, document|error }] }` — one bad
170
- // ref never loses the rest of the batch.
169
+ return reportSeveralDocuments(door, results);
170
+ }
171
+ // Several refs: `{ ok, documents: [{ ref, ok, document|error }] }` — one bad
172
+ // ref never loses the rest of the batch.
173
+ function reportSeveralDocuments(door, results) {
171
174
  const failed = results.filter((result) => !result.ok);
172
175
  if (door.json) {
173
176
  return emitAgentDoor(door, {
@@ -36,91 +36,108 @@ export async function checkBackfillState(context) {
36
36
  */
37
37
  export async function fixBackfillState(context) {
38
38
  const started = Date.now();
39
- const deadline = started + doctorBackfillBudgetSeconds(context, started) * 1000;
40
- let caughtUp = 0;
41
- let chunks = 0;
42
- let live = null;
43
- let remaining = null;
44
- const receipt = () => `caught up on ${caughtUp} old sessions in ${chunks} chunk${chunks === 1 ? "" : "s"} over ${clock(Date.now() - started)}`;
45
- const unfinished = (code, reason) => ({
46
- ...needsFix("backfill-complete", code, `${receipt()}, ${remaining ?? "unknown"} still to go; ${reason}; run \`cockpit doctor\` again to continue`),
39
+ const run = {
40
+ started,
41
+ deadline: started + doctorBackfillBudgetSeconds(context, started) * 1000,
42
+ caughtUp: 0,
43
+ chunks: 0,
44
+ live: null,
45
+ remaining: null,
46
+ };
47
+ const stopProgress = startProgress((text) => context.io.stderr.write(text), progressReporter(run));
48
+ try {
49
+ return await catchUpChunkByChunk(context, run);
50
+ }
51
+ catch (error) {
52
+ console.error("[doctor] backfill-complete failed", JSON.stringify({ reason: "backfill_threw", error_name: error instanceof Error ? error.name : "unknown" }));
53
+ return unfinishedRow(run, "backfill_threw", "backfill failed");
54
+ }
55
+ finally {
56
+ stopProgress();
57
+ }
58
+ }
59
+ function describeReceipt(run) {
60
+ return `caught up on ${run.caughtUp} old sessions in ${run.chunks} chunk${run.chunks === 1 ? "" : "s"} over ${clock(Date.now() - run.started)}`;
61
+ }
62
+ function unfinishedRow(run, code, reason) {
63
+ return {
64
+ ...needsFix("backfill-complete", code, `${describeReceipt(run)}, ${run.remaining ?? "unknown"} still to go; ${reason}; run \`cockpit doctor\` again to continue`),
47
65
  nextAction: "cockpit doctor",
48
- });
66
+ };
67
+ }
68
+ function progressReporter(run) {
49
69
  let lastLine = null;
50
- const stopProgress = startProgress((text) => context.io.stderr.write(text), (elapsedMs) => {
70
+ return (elapsedMs) => {
71
+ const { live } = run;
51
72
  const snapshot = {
52
- caughtUp: live === null ? caughtUp : caughtUp + live.uploaded,
53
- remaining: live?.remaining ?? remaining,
73
+ caughtUp: live === null ? run.caughtUp : run.caughtUp + live.uploaded,
74
+ remaining: live?.remaining ?? run.remaining,
54
75
  };
55
76
  console.error("[doctor] backfill-complete progress", JSON.stringify({
56
77
  status: live === null ? "between_chunks" : "running", processed: live?.processed ?? null,
57
- uploaded: live === null ? null : caughtUp + live.uploaded,
58
- caught_up: snapshot.caughtUp, remaining: snapshot.remaining, chunks, elapsed_seconds: Math.floor(elapsedMs / 1000),
78
+ uploaded: live === null ? null : run.caughtUp + live.uploaded,
79
+ caught_up: snapshot.caughtUp, remaining: snapshot.remaining, chunks: run.chunks, elapsed_seconds: Math.floor(elapsedMs / 1000),
59
80
  }));
60
81
  const line = backfillProgressLine(snapshot, lastLine, elapsedMs);
61
82
  lastLine = snapshot;
62
83
  return line;
63
- });
64
- try {
65
- while (Date.now() < deadline) {
66
- const row = await withCollectionGate(context, "backfill-complete", async () => {
67
- if (Date.now() >= deadline)
68
- return unfinished("backfill_budget_timeout", "backfill time budget reached");
69
- const capture = capturedIo(context.io, false);
70
- live = null;
71
- chunks += 1;
72
- const code = await runBackfillCommand({
73
- homeDir: context.command.homeDir, repoRoot: context.command.repoRoot,
74
- all: true, dryRun: false, yes: true, json: true,
75
- deadlineMs: deadline,
76
- // Doctor never waits for the collection lock (BLI-12872): the gate
77
- // above looked at it, and a busy answer here is reported, not retried.
78
- syncLockWaitMs: 0,
79
- onProgress: (progress) => { live = progress; },
80
- }, capture.io);
81
- const parsed = parseDoctorBackfillJson(capture.stdout());
82
- const counts = asRecord(parsed?.counts);
83
- const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
84
- caughtUp += count(counts?.["backfilled"]) ?? 0;
85
- remaining = count(counts?.["remaining"]) ?? remaining;
86
- live = null;
87
- if (code === 0 && parsed?.status === "complete") {
88
- // Reporting totals include permanent caps. Preserve their marker
89
- // note instead of retrying history the collector says is complete.
90
- const checked = await checkBackfillState(context);
91
- return ok("backfill-complete", checked.status === "ok" ? checked.code : "complete", receipt() + (checked.status === "ok" ? `; ${checked.message}` : ""));
92
- }
93
- const verdict = backfillFixVerdict(parsed, jsonField(capture.stderr(), "failure_reason"));
94
- if (parsed?.status === "partial" &&
95
- (parsed.failure_reason === "deferred_budget_exhausted" || !parsed.failure_reason)) {
96
- return needsFix("backfill-complete", "backfill_chunk_pending", "more history remains");
97
- }
98
- return { ...verdict, message: typeof parsed?.failure_reason === "string" ? parsed.failure_reason : "backfill failed without a valid completion receipt" };
99
- });
100
- if (row.status === "ok") {
101
- console.error("[doctor] backfill-complete finished", JSON.stringify({ caught_up: caughtUp, remaining, chunks, elapsed_seconds: Math.floor((Date.now() - started) / 1000) }));
102
- return row;
103
- }
104
- if (row.status === "skipped") {
105
- // A running sync (or another catch-up) owns the lock. Doctor does not
106
- // wait; it says what this run managed before it stood aside.
107
- console.error("[doctor] backfill-complete stood aside", JSON.stringify({ reason: row.code, caught_up: caughtUp, remaining, chunks }));
108
- return caughtUp > 0 ? { ...row, message: `${receipt()}; ${row.message}` } : row;
109
- }
110
- if (row.code === "sync_stuck_not_ended")
111
- return row;
112
- if (row.code !== "backfill_chunk_pending")
113
- return unfinished(row.code, row.message);
84
+ };
85
+ }
86
+ async function catchUpChunkByChunk(context, run) {
87
+ while (Date.now() < run.deadline) {
88
+ const row = await withCollectionGate(context, "backfill-complete", () => runOneChunk(context, run));
89
+ if (row.status === "ok") {
90
+ console.error("[doctor] backfill-complete finished", JSON.stringify({ caught_up: run.caughtUp, remaining: run.remaining, chunks: run.chunks, elapsed_seconds: Math.floor((Date.now() - run.started) / 1000) }));
91
+ return row;
114
92
  }
115
- return unfinished("backfill_budget_timeout", "backfill time budget reached");
93
+ if (row.status === "skipped") {
94
+ // A running sync (or another catch-up) owns the lock. Doctor does not
95
+ // wait; it says what this run managed before it stood aside.
96
+ console.error("[doctor] backfill-complete stood aside", JSON.stringify({ reason: row.code, caught_up: run.caughtUp, remaining: run.remaining, chunks: run.chunks }));
97
+ return run.caughtUp > 0 ? { ...row, message: `${describeReceipt(run)}; ${row.message}` } : row;
98
+ }
99
+ if (row.code === "sync_stuck_not_ended")
100
+ return row;
101
+ if (row.code !== "backfill_chunk_pending")
102
+ return unfinishedRow(run, row.code, row.message);
116
103
  }
117
- catch (error) {
118
- console.error("[doctor] backfill-complete failed", JSON.stringify({ reason: "backfill_threw", error_name: error instanceof Error ? error.name : "unknown" }));
119
- return unfinished("backfill_threw", "backfill failed");
104
+ return unfinishedRow(run, "backfill_budget_timeout", "backfill time budget reached");
105
+ }
106
+ async function runOneChunk(context, run) {
107
+ if (Date.now() >= run.deadline)
108
+ return unfinishedRow(run, "backfill_budget_timeout", "backfill time budget reached");
109
+ const capture = capturedIo(context.io, false);
110
+ run.live = null;
111
+ run.chunks += 1;
112
+ const code = await runBackfillCommand({
113
+ homeDir: context.command.homeDir, repoRoot: context.command.repoRoot,
114
+ all: true, dryRun: false, yes: true, json: true,
115
+ deadlineMs: run.deadline,
116
+ // Doctor never waits for the collection lock (BLI-12872): the gate
117
+ // above looked at it, and a busy answer here is reported, not retried.
118
+ syncLockWaitMs: 0,
119
+ onProgress: (progress) => { run.live = progress; },
120
+ }, capture.io);
121
+ const parsed = parseDoctorBackfillJson(capture.stdout());
122
+ const counts = asRecord(parsed?.counts);
123
+ run.caughtUp += nonNegativeCount(counts?.["backfilled"]) ?? 0;
124
+ run.remaining = nonNegativeCount(counts?.["remaining"]) ?? run.remaining;
125
+ run.live = null;
126
+ if (code === 0 && parsed?.status === "complete") {
127
+ // Reporting totals include permanent caps. Preserve their marker
128
+ // note instead of retrying history the collector says is complete.
129
+ const checked = await checkBackfillState(context);
130
+ return ok("backfill-complete", checked.status === "ok" ? checked.code : "complete", describeReceipt(run) + (checked.status === "ok" ? `; ${checked.message}` : ""));
120
131
  }
121
- finally {
122
- stopProgress();
132
+ const verdict = backfillFixVerdict(parsed, jsonField(capture.stderr(), "failure_reason"));
133
+ if (parsed?.status === "partial" &&
134
+ (parsed.failure_reason === "deferred_budget_exhausted" || !parsed.failure_reason)) {
135
+ return needsFix("backfill-complete", "backfill_chunk_pending", "more history remains");
123
136
  }
137
+ return { ...verdict, message: typeof parsed?.failure_reason === "string" ? parsed.failure_reason : "backfill failed without a valid completion receipt" };
138
+ }
139
+ function nonNegativeCount(value) {
140
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
124
141
  }
125
142
  function capturedIo(io, forward) {
126
143
  const stdoutChunks = [];