@bli-cockpit/cli 0.2.237 → 0.2.239

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 (148) 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-direct.js +119 -0
  29. package/dist/commands/cal-overlay.js +73 -30
  30. package/dist/commands/cal.js +62 -25
  31. package/dist/commands/clean.js +4 -1
  32. package/dist/commands/docs-read.js +5 -2
  33. package/dist/commands/docs.js +38 -6
  34. package/dist/commands/doctor-backfill.js +88 -71
  35. package/dist/commands/doctor-pipeline.js +59 -46
  36. package/dist/commands/heartbeat-post.js +116 -0
  37. package/dist/commands/heartbeat-token-lock.js +46 -33
  38. package/dist/commands/heartbeat.js +43 -125
  39. package/dist/commands/install-update.js +17 -15
  40. package/dist/commands/issue-show.js +27 -28
  41. package/dist/commands/issue-versions.js +93 -0
  42. package/dist/commands/issue-write.js +70 -23
  43. package/dist/commands/issue.js +73 -37
  44. package/dist/commands/jarvis-live-answer.js +99 -0
  45. package/dist/commands/jarvis-render.js +1 -94
  46. package/dist/commands/jarvis-trace.js +30 -22
  47. package/dist/commands/jarvis-turn-report.js +124 -0
  48. package/dist/commands/jarvis-turn.js +108 -182
  49. package/dist/commands/jarvis.js +26 -17
  50. package/dist/commands/local-args-collector-setup.js +82 -84
  51. package/dist/commands/local-args-collector-work.js +37 -38
  52. package/dist/commands/local-args-tower-admin.js +155 -142
  53. package/dist/commands/local-args-tower-cal.js +69 -54
  54. package/dist/commands/local-args-tower-careers.js +73 -54
  55. package/dist/commands/local-args-tower-docs-msg.js +79 -51
  56. package/dist/commands/local-args-tower-github.js +26 -20
  57. package/dist/commands/local-args-tower-hits.js +32 -20
  58. package/dist/commands/local-args-tower-manager.js +85 -4
  59. package/dist/commands/local-args-tower-memory.js +139 -117
  60. package/dist/commands/local-args-tower-msg.js +24 -6
  61. package/dist/commands/local-args-tower-notes.js +346 -0
  62. package/dist/commands/local-args-tower-pages.js +100 -378
  63. package/dist/commands/local-args-tower-settings.js +28 -29
  64. package/dist/commands/local-args-tower-setup.js +11 -4
  65. package/dist/commands/local-args-tower-work.js +74 -37
  66. package/dist/commands/local-args.js +80 -129
  67. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  68. package/dist/commands/local-help-commands-tower.js +20 -7
  69. package/dist/commands/local-help-commands.js +342 -341
  70. package/dist/commands/local-help.js +87 -83
  71. package/dist/commands/local-run-guards.js +100 -0
  72. package/dist/commands/local.js +137 -206
  73. package/dist/commands/login.js +13 -9
  74. package/dist/commands/logs.js +55 -38
  75. package/dist/commands/mail-attachments.js +44 -26
  76. package/dist/commands/manager-render-home.js +208 -0
  77. package/dist/commands/manager-render.js +7 -3
  78. package/dist/commands/manager-requests.js +33 -0
  79. package/dist/commands/manager.js +39 -5
  80. package/dist/commands/memory-consolidation.js +18 -9
  81. package/dist/commands/memory-daemon-probe.js +37 -28
  82. package/dist/commands/memory-install-config.js +64 -60
  83. package/dist/commands/memory-install.js +21 -41
  84. package/dist/commands/memory-requeue.js +26 -19
  85. package/dist/commands/memory-rules.js +26 -18
  86. package/dist/commands/memory-search-print.js +26 -0
  87. package/dist/commands/memory-store.js +14 -30
  88. package/dist/commands/memory-verb-list.js +49 -48
  89. package/dist/commands/msg-read-line.js +32 -0
  90. package/dist/commands/msg.js +45 -3
  91. package/dist/commands/notes-accounts.js +49 -32
  92. package/dist/commands/notes-sync-loop.js +115 -89
  93. package/dist/commands/notes-wisprflow-login.js +44 -23
  94. package/dist/commands/notes-writes.js +115 -100
  95. package/dist/commands/notes.js +33 -55
  96. package/dist/commands/onboard-flows.js +69 -51
  97. package/dist/commands/onboard.js +11 -9
  98. package/dist/commands/ops-render-memory.js +10 -7
  99. package/dist/commands/ops-render.js +33 -30
  100. package/dist/commands/ops.js +3 -0
  101. package/dist/commands/public-root.js +1 -1
  102. package/dist/commands/session-sync-outside-roots.js +40 -36
  103. package/dist/commands/session-sync-plan.js +14 -10
  104. package/dist/commands/session-sync-upload.js +24 -22
  105. package/dist/commands/session-sync.js +19 -8
  106. package/dist/commands/setup.js +13 -0
  107. package/dist/commands/slack.js +25 -16
  108. package/dist/commands/sources-check.js +13 -7
  109. package/dist/commands/sources-discovery.js +25 -17
  110. package/dist/commands/sources-verdict.js +19 -18
  111. package/dist/commands/speed.js +23 -19
  112. package/dist/commands/start.js +20 -16
  113. package/dist/commands/sync-doctor.js +52 -48
  114. package/dist/commands/sync.js +22 -16
  115. package/dist/commands/usage-telemetry-toml.js +71 -45
  116. package/dist/commands/usage.js +94 -68
  117. package/dist/commands/workbook-render.js +10 -7
  118. package/dist/evidence-reconcile-client.js +75 -64
  119. package/dist/evidence-redelivery-plan.js +37 -34
  120. package/dist/evidence-redelivery.js +32 -29
  121. package/dist/evidence-transcripts-client.js +20 -20
  122. package/dist/evidence-upload-object.js +86 -89
  123. package/dist/evidence-upload-plan.js +94 -83
  124. package/dist/evidence-upload-terminal.js +45 -50
  125. package/dist/image-description-session.js +156 -159
  126. package/dist/local-state-pairing-code.js +39 -31
  127. package/dist/local-state-status.js +9 -2
  128. package/dist/onboarding-roots.js +5 -8
  129. package/dist/process-runner.js +8 -8
  130. package/dist/raw-evidence-gc.js +33 -38
  131. package/dist/raw-evidence-stuck-summary.js +7 -14
  132. package/dist/repo-identity.js +26 -23
  133. package/dist/scheduled-self-update.js +11 -15
  134. package/dist/second-install.js +3 -0
  135. package/dist/slim/codex-rules.js +23 -36
  136. package/dist/spool/local-spool-parse.js +13 -12
  137. package/dist/sync-lock.js +5 -15
  138. package/dist/tower-client.js +36 -53
  139. package/dist/tower-stream.js +7 -24
  140. package/dist/upload-agent-artifacts.js +58 -64
  141. package/dist/upload-envelope-build.js +60 -59
  142. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  143. package/dist/upload-evidence-delivery-summary.js +32 -34
  144. package/dist/upload-ingest-receipt.js +20 -23
  145. package/dist/upload-session-reports-queue.js +4 -1
  146. package/dist/upload-session-reports-wire.js +64 -68
  147. package/dist/upload-sync.js +171 -157
  148. package/package.json +4 -4
@@ -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,119 @@
1
+ /**
2
+ * `cockpit cal duplicate | batch | undo` (BLI-21296, calendar slice 4): the
3
+ * terminal in front of the same doors the browser's grid uses. `batch` reads
4
+ * its changes as JSON on stdin; every verb takes `--idempotency-key` so a retry
5
+ * writes once, and `--json` prints the door's answer untouched.
6
+ */
7
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
8
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
9
+ const TAG = "[cal cli]";
10
+ const WRITE_DEADLINE_MS = 300_000;
11
+ export async function runCalDirect(command, door) {
12
+ if (command.action === "duplicate")
13
+ return duplicate(command, door);
14
+ if (command.action === "batch")
15
+ return batch(command, door);
16
+ return undo(command, door);
17
+ }
18
+ function keyed(command) {
19
+ return {
20
+ ...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
21
+ ...(command.reason ? { reason: command.reason } : {}),
22
+ };
23
+ }
24
+ function writeReceiptLines(door, receipt) {
25
+ if (receipt.event?.account_name)
26
+ writeLine(door.io.stdout, `Account: ${receipt.event.account_name}.`);
27
+ if (receipt.event?.event_id)
28
+ writeLine(door.io.stdout, `Tower event: ${receipt.event.event_id}`);
29
+ if (receipt.change_id)
30
+ writeLine(door.io.stdout, `Change: ${receipt.change_id} (undo with: cockpit cal undo ${receipt.change_id})`);
31
+ else
32
+ writeLine(door.io.stdout, "Google took the change but Tower could not record it in the ledger, so there is no change id to undo.");
33
+ if (receipt.event?.version)
34
+ writeLine(door.io.stdout, `Version: ${receipt.event.version}`);
35
+ if (receipt.replayed)
36
+ writeLine(door.io.stdout, "This key was used before: nothing new was written; this is the first receipt.");
37
+ }
38
+ async function duplicate(command, door) {
39
+ const answer = await askAgentDoor(door, {
40
+ method: "POST",
41
+ path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}/duplicate`,
42
+ label: "cal duplicate",
43
+ timeoutMs: WRITE_DEADLINE_MS,
44
+ body: {
45
+ ...(command.startsAt ? { starts_at: command.startsAt } : {}),
46
+ ...(command.endsAt ? { ends_at: command.endsAt } : {}),
47
+ ...(command.calendarId ? { calendar_id: command.calendarId } : {}),
48
+ ...(command.accountName ? { account: command.accountName } : {}),
49
+ ...(command.keepInvitees ? { keep_invitees: true } : {}),
50
+ ...keyed(command),
51
+ },
52
+ });
53
+ if (!answer.ok)
54
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
55
+ if (door.json)
56
+ return emitAgentDoor(door, { ok: true, ...answer.body });
57
+ const receipt = answer.body;
58
+ writeLine(door.io.stdout, `Duplicated "${receipt.event?.title ?? "the event"}". ${receipt.invitees === "kept" ? "The same guests were invited (Google emailed them)." : "No guests were invited."}`);
59
+ writeReceiptLines(door, receipt);
60
+ return 0;
61
+ }
62
+ async function readBatchBody(door) {
63
+ if (isInteractiveStdin(door.io))
64
+ return null;
65
+ try {
66
+ const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 200_000, overflowMessage: "The batch is too large." }));
67
+ if (Array.isArray(parsed))
68
+ return { changes: parsed };
69
+ return parsed && typeof parsed === "object" ? parsed : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ async function batch(command, door) {
76
+ const body = await readBatchBody(door);
77
+ if (!body || !Array.isArray(body.changes)) {
78
+ return failAgentDoor(door, TAG, "invalid_body", 'Pipe JSON: {"changes":[{"event_id":"…","starts_at":"…","ends_at":"…","expected_version":"…"}]} (or a bare array). Up to 25 changes, each event once.');
79
+ }
80
+ const answer = await askAgentDoor(door, {
81
+ method: "POST",
82
+ path: "/api/cal/events/batch",
83
+ label: "cal batch",
84
+ timeoutMs: WRITE_DEADLINE_MS,
85
+ body: { ...body, ...keyed(command) },
86
+ });
87
+ if (!answer.ok)
88
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
89
+ const result = answer.body;
90
+ // A script must be able to tell "all of it" from "some of it" without parsing: exit 1 unless every item was written.
91
+ const code = result.outcome === "all_committed" ? 0 : 1;
92
+ if (door.json)
93
+ return emitAgentDoor(door, { ok: code === 0, ...result }, code);
94
+ for (const item of result.items) {
95
+ writeLine(door.io.stdout, `#${item.index + 1} ${item.event_id}: ${item.outcome}${item.reason ? ` (${item.reason})` : ""}${item.change_id ? ` change ${item.change_id}` : ""}`);
96
+ }
97
+ writeLine(door.io.stdout, `${result.committed} of ${result.items.length} changed (${result.outcome.replaceAll("_", " ")}).`);
98
+ if (result.unchanged.length > 0) {
99
+ writeLine(door.io.stdout, `Left as they were: ${result.unchanged.map((item) => `#${item.index + 1} ${item.event_id}`).join(", ")}.`);
100
+ }
101
+ return code;
102
+ }
103
+ async function undo(command, door) {
104
+ const answer = await askAgentDoor(door, {
105
+ method: "POST",
106
+ path: "/api/cal/undo",
107
+ label: "cal undo",
108
+ timeoutMs: WRITE_DEADLINE_MS,
109
+ body: { ...(command.subjectId ? { change_id: command.subjectId } : {}), ...keyed(command) },
110
+ });
111
+ if (!answer.ok)
112
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
113
+ if (door.json)
114
+ return emitAgentDoor(door, { ok: true, ...answer.body });
115
+ const receipt = answer.body;
116
+ writeLine(door.io.stdout, `Undid change ${receipt.undoes_change_id}. The earlier values are back (or the created event is gone); guests, if any, were told by Google.`);
117
+ writeReceiptLines(door, receipt);
118
+ return 0;
119
+ }
@@ -1,29 +1,51 @@
1
1
  import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
2
  import { readPipedText, isInteractiveStdin, writeLine } from "./cli-io.js";
3
+ const FAIL_TAG = "[cal cli]";
4
+ const REQUEST_TIMEOUT_MS = 30000;
3
5
  export async function runCalOverlay(command, door) {
4
- if (command.action === "calendar-change") {
5
- let body = command.color ? { color_override: command.color === "provider" ? null : command.color } : {};
6
- if (!command.color && !isInteractiveStdin(door.io)) {
7
- try {
8
- const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 4000, overflowMessage: "Preferences are too large." }));
9
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
10
- throw new Error("invalid_preferences");
11
- body = parsed;
12
- }
13
- catch {
14
- return failAgentDoor(door, "[cal cli]", "invalid_body", "Pipe JSON preferences or use --color '#4285f4' (provider resets).");
15
- }
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
16
  }
17
- if (!Object.keys(body).length)
18
- return failAgentDoor(door, "[cal cli]", "invalid_body", "Use --color '#4285f4', --color provider, or pipe JSON with color_override/selected.");
19
- const answer = await askAgentDoor(door, { method: "PATCH", path: `/api/cal/calendars/${encodeURIComponent(command.subjectId)}`, body, label: "cal calendar-change", timeoutMs: 30000 });
20
- if (!answer.ok)
21
- return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
22
- if (door.json)
23
- return emitAgentDoor(door, answer.body);
24
- writeLine(door.io.stdout, "Calendar preferences saved. Provider sync preserves your colour override.");
25
- return 0;
17
+ body = piped;
26
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) {
27
49
  const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
28
50
  if (command.from)
29
51
  query.set("from", command.from);
@@ -33,31 +55,52 @@ export async function runCalOverlay(command, door) {
33
55
  query.set("offset", String(command.offset));
34
56
  if (command.includeDeselected)
35
57
  query.set("all", "true");
36
- const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs: 30000 });
58
+ const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs: REQUEST_TIMEOUT_MS });
37
59
  if (!answer.ok)
38
- return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
60
+ return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
39
61
  if (door.json)
40
62
  return emitAgentDoor(door, answer.body);
41
63
  const result = answer.body;
42
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) {
43
70
  writeLine(door.io.stdout, `${result.groups.length} double-bookings · own calendars`);
44
71
  for (const group of result.groups) {
45
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));
46
73
  const part = (name) => parts.find(item => item.type === name)?.value;
47
- writeLine(door.io.stdout, `${part("weekday")} ${part("day")} ${part("month")} ${part("hour")}:${part("minute")}: ${group.meetings.map(meeting => `${meeting.event.title} · ${meeting.event.calendar_name}`).join(" / ")}`);
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}`);
48
76
  }
49
- const checked = result.coverage.calendars.filter(calendar => calendar.checked);
77
+ }
78
+ function describeSyncAge(checked) {
50
79
  const oldest = Math.min(...checked.map(calendar => calendar.lastSyncAt ? Date.parse(calendar.lastSyncAt) : NaN));
51
80
  const minutes = Math.max(0, Math.floor((Date.now() - oldest) / 60000));
52
- const age = !checked.length ? "none checked" : !Number.isFinite(oldest) ? "sync age unknown" : minutes < 1 ? "just now" : minutes < 60 ? `${minutes} min ago` : minutes < 1440 ? `${Math.floor(minutes / 60)} h ago` : `${Math.floor(minutes / 1440)} days ago`;
53
- writeLine(door.io.stdout, `Checked ${checked.length} calendar${checked.length === 1 ? "" : "s"} · ${age}`);
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)}`);
54
96
  for (const calendar of result.coverage.calendars.filter(calendar => calendar.hidden || !calendar.checked)) {
55
97
  writeLine(door.io.stdout, `${calendar.name}: ${calendar.hidden ? "hidden; " : ""}${calendar.checked ? "included by request" : `not checked (${(calendar.excludedReason ?? "unavailable").replaceAll("_", " ")})`}`);
56
98
  }
57
- for (const account of result.coverage.accounts ?? [])
99
+ for (const account of result.coverage.accounts ?? []) {
58
100
  if (!account.calendars)
59
101
  writeLine(door.io.stdout, `${account.label}: no discovered calendars (${account.status.replaceAll("_", " ")})`);
60
- if (result.coverage.incomplete || result.coverage.truncated || result.coverage.notes.length)
102
+ }
103
+ if (result.coverage.incomplete || result.coverage.truncated || result.coverage.notes.length) {
61
104
  writeLine(door.io.stdout, "Incomplete window. This check cannot confirm free time.");
62
- return 0;
105
+ }
63
106
  }
@@ -22,8 +22,10 @@
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";
25
26
  import { runCalOverlay } from "./cal-overlay.js";
26
27
  import { runCalDelete } from "./cal-delete.js";
28
+ import { runCalDirect } from "./cal-direct.js";
27
29
  import { runCalSyncStatus } from "./cal-sync-status.js";
28
30
  import { writeLine } from "./cli-io.js";
29
31
  import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
@@ -33,6 +35,13 @@ const WRITE_DEADLINE_MS = 60_000;
33
35
  export async function runCal(command, io) {
34
36
  const door = await openAgentDoor("cal", command, io);
35
37
  switch (command.action) {
38
+ case "event":
39
+ case "export":
40
+ return runCalDetail(command, door);
41
+ case "duplicate":
42
+ case "batch":
43
+ case "undo":
44
+ return runCalDirect(command, door);
36
45
  case "conflicts":
37
46
  case "calendar-change":
38
47
  return runCalOverlay(command, door);
@@ -95,6 +104,19 @@ function windowQuery(command) {
95
104
  return query;
96
105
  }
97
106
  async function window(command, door) {
107
+ const answer = await askForWindow(command, door);
108
+ if (!answer.ok)
109
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
110
+ const body = answer.body;
111
+ if (door.json)
112
+ return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
113
+ if (answer.fromCache) {
114
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
115
+ }
116
+ writeWindow(body, door);
117
+ return 0;
118
+ }
119
+ async function askForWindow(command, door) {
98
120
  // `--from`/`--to` beat a named window, on any of the three verbs: somebody
99
121
  // who named two dates meant them.
100
122
  const path = command.from && command.to ? "events" : command.action;
@@ -109,33 +131,27 @@ async function window(command, door) {
109
131
  timeoutMs: READ_DEADLINE_MS,
110
132
  warmRoute: cached,
111
133
  });
112
- const answer = cached
113
- ? await readThroughCache({
114
- io: door.io,
115
- homeDir: door.homeDir,
116
- deviceToken: door.deviceToken,
117
- surface: "cal",
118
- label: "cal today",
119
- path: apiPath,
120
- json: door.json,
121
- ask,
122
- })
123
- : { ...(await ask()), fromCache: false, ageMs: null };
124
- if (!answer.ok)
125
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
126
- const body = answer.body;
127
- if (door.json)
128
- return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
129
- if (answer.fromCache) {
130
- writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
131
- }
134
+ if (!cached)
135
+ return { ...(await ask()), fromCache: false, ageMs: null };
136
+ return readThroughCache({
137
+ io: door.io,
138
+ homeDir: door.homeDir,
139
+ deviceToken: door.deviceToken,
140
+ surface: "cal",
141
+ label: "cal today",
142
+ path: apiPath,
143
+ json: door.json,
144
+ ask,
145
+ });
146
+ }
147
+ function writeWindow(body, door) {
132
148
  const zone = body.window?.time_zone ?? localTimeZone();
133
149
  const events = body.events ?? [];
134
150
  if (events.length === 0) {
135
151
  writeLine(door.io.stdout, (body.calendars ?? []).length === 0
136
152
  ? "No calendars attached yet. `cockpit cal add-ical` for a personal one, or connect Google in the browser."
137
153
  : `Nothing on ${describeWindow(body, zone)}.`);
138
- return 0;
154
+ return;
139
155
  }
140
156
  writeLine(door.io.stdout, describeWindow(body, zone));
141
157
  writeLine(door.io.stdout, "");
@@ -156,7 +172,6 @@ async function window(command, door) {
156
172
  for (const note of body.notes ?? []) {
157
173
  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.`);
158
174
  }
159
- return 0;
160
175
  }
161
176
  function describeWindow(body, zone) {
162
177
  if (!body.window)
@@ -289,6 +304,8 @@ async function create(command, door) {
289
304
  ...(command.allDay ? { all_day: true } : {}),
290
305
  ...(command.attendees.length > 0 ? { attendees: command.attendees } : {}),
291
306
  ...(command.meet ? { meet: true } : {}),
307
+ ...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
308
+ ...(command.reason ? { reason: command.reason } : {}),
292
309
  time_zone: command.timeZone ?? localTimeZone(),
293
310
  },
294
311
  });
@@ -297,6 +314,10 @@ async function create(command, door) {
297
314
  const event = answer.body.event;
298
315
  if (door.json)
299
316
  return emitAgentDoor(door, { ok: true, event });
317
+ writeCreated(command, door, event);
318
+ return 0;
319
+ }
320
+ function writeCreated(command, door, event) {
300
321
  const zone = command.timeZone ?? localTimeZone();
301
322
  writeLine(door.io.stdout, `Created "${event?.title ?? command.title}" on ${event ? `${dayLabel(event.starts_at, zone)} ${clock(event.starts_at, zone)}` : "that calendar"}.`);
302
323
  if (event?.event_id) {
@@ -317,7 +338,6 @@ async function create(command, door) {
317
338
  if (command.attendees.length > 0) {
318
339
  writeLine(door.io.stdout, `Invitations went to ${command.attendees.join(", ")} from Google.`);
319
340
  }
320
- return 0;
321
341
  }
322
342
  function meetingLinkText(event) {
323
343
  const label = { meet: "Meet link", zoom: "Zoom link", teams: "Teams link" }[event.meeting_kind ?? ""] ?? "Meeting link";
@@ -327,11 +347,28 @@ function meetingLinkText(event) {
327
347
  }
328
348
  async function changeEvent(command, door) {
329
349
  const answer = await askAgentDoor(door, { path: `/api/cal/events/${encodeURIComponent(command.subjectId)}`, method: "PATCH", label: "cal change", timeoutMs: WRITE_DEADLINE_MS,
330
- 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 } : {}) } });
350
+ body: {
351
+ ...(command.accountName ? { account: command.accountName } : {}),
352
+ ...(command.title ? { title: command.title } : {}),
353
+ ...(command.startsAt ? { starts_at: command.startsAt } : {}),
354
+ ...(command.endsAt ? { ends_at: command.endsAt } : {}),
355
+ ...(command.location ? { location: command.location } : {}),
356
+ // BLI-21296: all-day conversion either way, and the write controls.
357
+ ...(command.allDay ? { all_day: true } : command.timed ? { all_day: false } : {}),
358
+ ...(command.timeZone && (command.allDay || command.timed) ? { time_zone: command.timeZone } : {}),
359
+ ...(command.expectedVersion ? { expected_version: command.expectedVersion } : {}),
360
+ ...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
361
+ ...(command.reason ? { reason: command.reason } : {}),
362
+ } });
331
363
  if (!answer.ok)
332
364
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
333
365
  if (door.json)
334
366
  return emitAgentDoor(door, { ok: true, ...answer.body });
335
- writeLine(door.io.stdout, `Changed event in ${answer.body.event?.account_name ?? "its account"}.`);
367
+ const changed = answer.body;
368
+ writeLine(door.io.stdout, `Changed event in ${changed.event?.account_name ?? "its account"}.`);
369
+ if (changed.change_id)
370
+ writeLine(door.io.stdout, `Change: ${changed.change_id} (undo with: cockpit cal undo ${changed.change_id})`);
371
+ if (changed.version)
372
+ writeLine(door.io.stdout, `Version: ${changed.version}`);
336
373
  return 0;
337
374
  }
@@ -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, {