@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
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The three commands that read a page instead of asking a question: the daily
3
- * page (`cockpit brief`), meeting notes (`cockpit notes`), and the document
4
- * library (`cockpit workbook`). Split out of local-args-tower.ts (BLI-3636).
2
+ * The commands that read a page instead of asking a question: the daily
3
+ * page (`cockpit brief`) and the document library (`cockpit workbook`); meeting
4
+ * notes (`cockpit notes`) live in `local-args-tower-notes.ts`. Split out of local-args-tower.ts (BLI-3636).
5
5
  *
6
6
  * Every parser moved verbatim: same flags, same defaults, same refusal
7
7
  * sentences. The three names `local-args.ts` re-exports from this family
@@ -9,83 +9,109 @@
9
9
  * `./local-args-tower.js`, the address they have always had.
10
10
  */
11
11
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
12
- /**
13
- * `cockpit brief` (BLI-3458) — the TODAY page, in the terminal.
14
- *
15
- * `--for` is the terminal's `?p=`, and it follows the same rule the website
16
- * does: the dashboard resolves it against the roster and the database (or the
17
- * route's own mirror of the database's rule) decides whether the page opens.
18
- * Nothing is decided here.
19
- *
20
- * `--date` (BLI-3484) is the terminal's `?d=`, and the same sentence applies to
21
- * it twice over: what counts as a date, where that person's day starts, and
22
- * whether anything was compiled for it are all the dashboard's answer. A local
23
- * copy of "when does Tuesday begin" would be a second answer to a question this
24
- * product already answers per person, per zone.
25
- */
12
+ const BRIEF_ACTIONS = ["read", "edit", "rewrite", "history", "status", "done", "undone"];
13
+ const BRIEF_ALLOWED_FLAGS = [
14
+ "--home",
15
+ "--dashboard-url",
16
+ "--for",
17
+ "--as",
18
+ "--who",
19
+ "--version",
20
+ "--date",
21
+ "--delta",
22
+ "--against",
23
+ "--days",
24
+ "--tldr",
25
+ "--full",
26
+ "--versions",
27
+ "--claims",
28
+ "--render",
29
+ "--missed",
30
+ "--reason",
31
+ "--wait",
32
+ "--no-wait",
33
+ "--json",
34
+ ];
35
+ const BRIEF_VALUE_FLAGS = [
36
+ "--home",
37
+ "--dashboard-url",
38
+ "--for",
39
+ "--as",
40
+ "--who",
41
+ "--version",
42
+ "--date",
43
+ "--against",
44
+ "--days",
45
+ "--reason",
46
+ ];
26
47
  export function parseBriefArgs(args) {
27
- const values = parseNamedArgs(args, {
28
- allowedFlags: [
29
- "--home",
30
- "--dashboard-url",
31
- "--for",
32
- "--as",
33
- "--who",
34
- "--version",
35
- "--date",
36
- "--delta",
37
- "--against",
38
- "--days",
39
- "--tldr",
40
- "--full",
41
- "--versions",
42
- "--claims",
43
- "--render",
44
- "--missed",
45
- "--reason",
46
- "--wait",
47
- "--no-wait",
48
- "--json",
49
- ],
50
- valueFlags: [
51
- "--home",
52
- "--dashboard-url",
53
- "--for",
54
- "--as",
55
- "--who",
56
- "--version",
57
- "--date",
58
- "--against",
59
- "--days",
60
- "--reason",
61
- ],
62
- });
48
+ const values = parseNamedArgs(args, { allowedFlags: BRIEF_ALLOWED_FLAGS, valueFlags: BRIEF_VALUE_FLAGS });
49
+ const action = readBriefAction(values.positionals[0]);
50
+ const item = readBriefItem(action, values.positionals);
51
+ const tldr = readBriefTldr(values);
52
+ const { reason, noWait } = readBriefRewriteAndReasonFlags(values, action);
53
+ const { date, against, delta, days } = readBriefPastDayFlags(values, action);
54
+ const people = readBriefPeople(values);
55
+ const { render, missed } = readBriefStatusFlags(values, action, people.named);
56
+ return {
57
+ kind: "brief",
58
+ action,
59
+ ...(item ? { item } : {}),
60
+ render,
61
+ missed,
62
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
63
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
64
+ subject: people.forPerson ?? people.asPerson ?? people.whoPerson,
65
+ version: optionalNonEmpty(values.flags.get("--version")),
66
+ ...(date ? { date } : {}),
67
+ ...(delta ? { delta: true } : {}),
68
+ ...(against ? { against } : {}),
69
+ ...(days !== undefined ? { days } : {}),
70
+ tldr,
71
+ versions: values.booleans.has("--versions"),
72
+ claims: values.booleans.has("--claims"),
73
+ ...(reason ? { reason } : {}),
74
+ // Waiting is the default; only an explicit `--no-wait` turns it off. A
75
+ // person who typed `rewrite` wants the page, not a receipt.
76
+ ...(noWait ? { wait: false } : {}),
77
+ json: values.booleans.has("--json"),
78
+ };
79
+ }
80
+ function readBriefAction(first) {
63
81
  // Bare `cockpit brief` reads the page, which is what somebody typing it almost
64
82
  // always wants — the same shape `cockpit notes` and `cockpit scout` have.
65
83
  // `status` (BLI-3462) answers why a brief was or was not delivered.
66
- const first = values.positionals[0];
67
84
  const action = (first === undefined ? "read" : first);
68
- if (!["read", "edit", "rewrite", "history", "status", "done", "undone"].includes(action)) {
85
+ if (!BRIEF_ACTIONS.includes(action)) {
69
86
  throw new Error(`Unknown brief command: ${first}. Try done, undone, edit, rewrite, history or status, or nothing to read it.`);
70
87
  }
88
+ return action;
89
+ }
90
+ /** The item `done`/`undone` tick off, and a refusal for any positional the verb does not take. */
91
+ function readBriefItem(action, positionals) {
71
92
  // BLI-3605: `done` and `undone` are the only brief verbs that take a
72
93
  // positional, and they REQUIRE one, "which item" is the whole command, and
73
94
  // a bare `cockpit brief done` that guessed at the top row would tick off
74
95
  // somebody's work for them.
75
96
  const ticksAnItem = action === "done" || action === "undone";
76
- const item = ticksAnItem ? values.positionals[1] : undefined;
97
+ const item = ticksAnItem ? positionals[1] : undefined;
77
98
  if (ticksAnItem && !item) {
78
99
  throw new Error(`brief ${action} needs the item: a ticket like BLI-3511, or a claim id from \`cockpit brief --claims\`.`);
79
100
  }
80
- const positionalsAllowed = first === undefined ? 0 : ticksAnItem ? 2 : 1;
81
- if (values.positionals.length > positionalsAllowed) {
82
- throw new Error(`brief ${action} does not take "${values.positionals[positionalsAllowed]}".`);
101
+ const positionalsAllowed = positionals[0] === undefined ? 0 : ticksAnItem ? 2 : 1;
102
+ if (positionals.length > positionalsAllowed) {
103
+ throw new Error(`brief ${action} does not take "${positionals[positionalsAllowed]}".`);
83
104
  }
105
+ return item;
106
+ }
107
+ function readBriefTldr(values) {
84
108
  const tldr = values.booleans.has("--tldr");
85
- const full = values.booleans.has("--full");
86
- if (tldr && full) {
109
+ if (tldr && values.booleans.has("--full")) {
87
110
  throw new Error("brief accepts either --tldr or --full, not both.");
88
111
  }
112
+ return tldr;
113
+ }
114
+ function readBriefRewriteAndReasonFlags(values, action) {
89
115
  const wait = values.booleans.has("--wait");
90
116
  const noWait = values.booleans.has("--no-wait");
91
117
  if (wait && noWait) {
@@ -103,7 +129,10 @@ export function parseBriefArgs(args) {
103
129
  if (action !== "rewrite" && (wait || noWait)) {
104
130
  throw new Error("--wait and --no-wait belong to `cockpit brief rewrite`.");
105
131
  }
106
- // ── Reading a past day (BLI-3484) ────────────────────────────────────────
132
+ return { reason, noWait };
133
+ }
134
+ /** Reading a past day (BLI-3484). */
135
+ function readBriefPastDayFlags(values, action) {
107
136
  const date = optionalNonEmpty(values.flags.get("--date"));
108
137
  const against = optionalNonEmpty(values.flags.get("--against"));
109
138
  const delta = values.booleans.has("--delta");
@@ -126,6 +155,9 @@ export function parseBriefArgs(args) {
126
155
  if (action === "history" && (values.booleans.has("--tldr") || values.booleans.has("--claims"))) {
127
156
  throw new Error("brief history lists days; --tldr and --claims belong to reading a page.");
128
157
  }
158
+ return { date, against, delta, days };
159
+ }
160
+ function readBriefPeople(values) {
129
161
  // `--as` is accepted as an alias so the two conversational commands read the
130
162
  // same way; `cockpit jarvis --as <person>` has meant this since BLI-3380.
131
163
  // `--who` is the third spelling, and it exists because `cockpit brief status
@@ -137,6 +169,9 @@ export function parseBriefArgs(args) {
137
169
  if (new Set(named).size > 1) {
138
170
  throw new Error("brief --for, --as and --who must name the same person.");
139
171
  }
172
+ return { forPerson, asPerson, whoPerson, named };
173
+ }
174
+ function readBriefStatusFlags(values, action, named) {
140
175
  const render = values.booleans.has("--render");
141
176
  if (render && action !== "status") {
142
177
  throw new Error("brief --render belongs to `cockpit brief status`; the page is printed by default.");
@@ -148,323 +183,10 @@ export function parseBriefArgs(args) {
148
183
  if (missed && named.length > 0) {
149
184
  throw new Error("brief status --missed answers for everybody; it does not take --for, --as or --who.");
150
185
  }
151
- return {
152
- kind: "brief",
153
- action,
154
- ...(item ? { item } : {}),
155
- render,
156
- missed,
157
- homeDir: optionalNonEmpty(values.flags.get("--home")),
158
- dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
159
- subject: forPerson ?? asPerson ?? whoPerson,
160
- version: optionalNonEmpty(values.flags.get("--version")),
161
- ...(date ? { date } : {}),
162
- ...(delta ? { delta: true } : {}),
163
- ...(against ? { against } : {}),
164
- ...(days !== undefined ? { days } : {}),
165
- tldr,
166
- versions: values.booleans.has("--versions"),
167
- claims: values.booleans.has("--claims"),
168
- ...(reason ? { reason } : {}),
169
- // Waiting is the default; only an explicit `--no-wait` turns it off. A
170
- // person who typed `rewrite` wants the page, not a receipt.
171
- ...(noWait ? { wait: false } : {}),
172
- json: values.booleans.has("--json"),
173
- };
174
- }
175
- /**
176
- * The notetakers `cockpit notes connect` will actually seal a key for
177
- * (BLI-4391). It mirrors `CONNECTABLE_NOTES_PROVIDERS` in the dashboard, and
178
- * the door is still the authority — this list only saves a round trip and a
179
- * sealed key for a provider with no reader.
180
- *
181
- * `otter` and `granola` are deliberately absent: Otter's public API is
182
- * Enterprise-only and Granola still runs on its own server-side cron.
183
- */
184
- const CONNECTABLE_PROVIDERS = ["fellow", "circleback", "wisprflow"];
185
- /**
186
- * BLI-12926. Providers with no API key at all: the person signs in through
187
- * their browser and `connect` runs that sign-in itself. `--key-stdin` is
188
- * optional for these, and carries a pre-built sign-in bundle when given.
189
- */
190
- const PROVIDERS_SIGNED_IN_BY_BROWSER = ["wisprflow"];
191
- /** The provider whose API is per workspace, so its account needs a subdomain. */
192
- const PROVIDERS_NEEDING_A_WORKSPACE = ["fellow"];
193
- const NOTES_ACTIONS = new Set([
194
- "folders", "mkdir", "rmdir", "rename",
195
- "list",
196
- "show",
197
- "shelf",
198
- "shelves",
199
- "upload",
200
- "paste",
201
- "share",
202
- "unshare",
203
- "summarize",
204
- "move",
205
- "place",
206
- // BLI-4383: the notetaker a person connects, and the meetings it records.
207
- "accounts",
208
- "connect",
209
- "detach",
210
- "sync",
211
- "sync-status",
212
- // BLI-12926: the alternates of a meeting several notetakers recorded.
213
- "recordings",
214
- "recording",
215
- // BLI-4394: arm that notetaker's webhook with the vendor's signing secret.
216
- "webhook",
217
- // Ticket 10718: the Granola cron's own last-several-runs health.
218
- "granola-status",
219
- "transcription-stats",
220
- "bot-upcoming",
221
- ]);
222
- /** Actions whose first positional is the note it acts on. */
223
- const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
224
- "show",
225
- "share",
226
- "unshare",
227
- "summarize",
228
- "move",
229
- "place",
230
- // BLI-12926: `recordings` names the note, `recording` names the recording.
231
- "recordings",
232
- "recording",
233
- ]);
234
- /**
235
- * BLI-4383. Actions whose first positional is a NOTETAKER ACCOUNT id, not a
236
- * note id. `sync` takes one too, and is handled beside these rather than in
237
- * this set because its id is checked at the door instead of here.
238
- */
239
- const NOTES_ACTIONS_NEEDING_AN_ACCOUNT = new Set(["detach", "webhook"]);
240
- export function parseNotesArgs(args) {
241
- const values = parseNamedArgs(args, {
242
- allowedFlags: [
243
- "--wait",
244
- "--home",
245
- "--dashboard-url",
246
- "--file",
247
- "--name",
248
- "--exclude",
249
- "--to",
250
- "--folder",
251
- "--clear-shelf",
252
- "--apply",
253
- "--series",
254
- "--kind",
255
- "--since",
256
- "--until",
257
- "--limit",
258
- "--month",
259
- "--offset",
260
- "--yes",
261
- "--json",
262
- "--tree",
263
- // BLI-4383. `--key-stdin` is a FLAG and not a value: the key itself is
264
- // read from stdin and never from argv.
265
- "--provider",
266
- "--key-stdin",
267
- // BLI-4391. Fellow's API is per WORKSPACE, and a subdomain is a label,
268
- // not a secret, so this one carries a value while the key never does.
269
- "--workspace",
270
- // BLI-4394. `--secret-stdin` is a FLAG and not a value, for the same
271
- // reason `--key-stdin` is: the signing secret never touches argv.
272
- "--secret-stdin",
273
- "--show-webhook",
274
- // BLI-18006. `sync <id> --repair-truncated` re-reads Wispr Flow notes whose
275
- // transcript was stored cut short. A FLAG, not a verb: it is the same sync
276
- // door with a different request.
277
- "--repair-truncated",
278
- ],
279
- valueFlags: [
280
- "--home",
281
- "--dashboard-url",
282
- "--file",
283
- "--name",
284
- "--exclude",
285
- "--to",
286
- "--folder",
287
- "--series",
288
- "--kind",
289
- "--since",
290
- "--until",
291
- "--limit",
292
- "--month",
293
- "--offset",
294
- "--provider",
295
- "--workspace",
296
- "--show-webhook",
297
- ],
298
- });
299
- // Bare `cockpit notes` is the list, which is the thing a person typing it
300
- // almost always wants — same reasoning as bare `cockpit` running convergence.
301
- const first = values.positionals[0];
302
- const action = (first === undefined ? "list" : first);
303
- if (!NOTES_ACTIONS.has(action)) {
304
- throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status, or transcription-stats.`);
305
- }
306
- const rest = values.positionals.slice(first === undefined ? 0 : 1);
307
- const json = values.booleans.has("--json");
308
- // `--json` implies `--yes`: a machine-readable run has nobody to prompt, and
309
- // failing it for want of a confirmation flag it cannot see is a worse
310
- // surprise than an explicit share it already asked for by name.
311
- const yes = values.booleans.has("--yes") || json;
312
- let noteId;
313
- if (NOTES_ACTIONS_NEEDING_A_NOTE.has(action)) {
314
- noteId = optionalNonEmpty(rest[0]);
315
- if (!noteId)
316
- throw new Error(`notes ${action} needs a note id.`);
317
- if (rest.length > 1) {
318
- throw new Error(`notes ${action} takes one note id, not ${rest.length}.`);
319
- }
320
- }
321
- let accountId;
322
- if (NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action) || action === "sync" || action === "sync-status") {
323
- accountId = optionalNonEmpty(rest[0]);
324
- if (!accountId && NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action)) {
325
- throw new Error(`notes ${action} needs a notetaker account id.`);
326
- }
327
- if (rest.length > 1) {
328
- throw new Error(`notes ${action} takes one account id, not ${rest.length}.`);
329
- }
330
- }
331
- // BLI-4394. `--show-webhook <id>` is a flag on `accounts` rather than a verb
332
- // of its own, because "what is this one's webhook URL?" is the question a
333
- // person asks WHILE looking at the list.
334
- const showWebhook = optionalNonEmpty(values.flags.get("--show-webhook"));
335
- if (showWebhook !== undefined) {
336
- if (action !== "accounts") {
337
- throw new Error("--show-webhook belongs to `cockpit notes accounts --show-webhook <account-id>`.");
338
- }
339
- accountId = showWebhook;
340
- }
341
- if (action === "webhook") {
342
- if (!values.booleans.has("--secret-stdin")) {
343
- // Naming the flag is the point: it says out loud that the signing secret
344
- // is not an argument, so nobody goes looking for a `--secret` that will
345
- // never exist.
346
- throw new Error('notes webhook reads the signing secret from stdin. Say so: printf "%s" "<secret>" | cockpit notes webhook <account-id> --secret-stdin');
347
- }
348
- }
349
- else if (values.booleans.has("--secret-stdin")) {
350
- throw new Error("--secret-stdin belongs to `cockpit notes webhook <account-id> --secret-stdin`.");
351
- }
352
- const provider = optionalNonEmpty(values.flags.get("--provider"));
353
- const workspace = optionalNonEmpty(values.flags.get("--workspace"));
354
- if (action === "connect") {
355
- if (!provider) {
356
- throw new Error(`notes connect needs --provider ${CONNECTABLE_PROVIDERS.join("|")}.`);
357
- }
358
- if (!CONNECTABLE_PROVIDERS.includes(provider)) {
359
- throw new Error(`notes connect does not know the provider "${provider}". Tower reads ${CONNECTABLE_PROVIDERS.join(" and ")}.`);
360
- }
361
- if (PROVIDERS_NEEDING_A_WORKSPACE.includes(provider) && !workspace) {
362
- throw new Error(`notes connect --provider ${provider} needs --workspace <subdomain>: its API lives at https://<subdomain>.fellow.app, so a key alone cannot address anything.`);
363
- }
364
- if (workspace !== undefined && !/^[a-z0-9-]{1,63}$/.test(workspace)) {
365
- throw new Error("--workspace is a workspace subdomain: lowercase letters, digits and hyphens, 1 to 63 characters.");
366
- }
367
- if (!values.booleans.has("--key-stdin") && !PROVIDERS_SIGNED_IN_BY_BROWSER.includes(provider)) {
368
- // Naming the flag is the point: it says out loud that the key is not an
369
- // argument, so nobody goes looking for a `--key` that will never exist.
370
- throw new Error('notes connect reads the key from stdin. Say so: printf "%s" "<key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
371
- }
372
- if (rest.length > 0)
373
- throw new Error(`notes connect does not take "${rest[0]}".`);
374
- }
375
- let paths;
376
- if (action === "upload") {
377
- // Explicit paths only. No globbing happens here — a shell that expands
378
- // `*.md` hands us the names it found, and a shell that does not would have
379
- // this command silently uploading a file literally called `*.md`.
380
- paths = rest.filter((value) => value.trim() !== "");
381
- if (paths.length === 0)
382
- throw new Error("notes upload needs at least one file path.");
383
- }
384
- else if (action === "mkdir" || action === "rmdir" || action === "rename") {
385
- if (rest.length !== 1 || !rest[0]?.trim())
386
- throw new Error(`notes ${action} needs one folder path.`);
387
- }
388
- else if (action !== "paste"
389
- && action !== "connect"
390
- && action !== "sync"
391
- && action !== "sync-status"
392
- && !NOTES_ACTIONS_NEEDING_A_NOTE.has(action)
393
- && !NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action)
394
- && rest.length > 0) {
395
- throw new Error(`notes ${action} does not take "${rest[0]}".`);
396
- }
397
- if (action === "rename" && !optionalNonEmpty(values.flags.get("--name")))
398
- throw new Error("notes rename needs --name <name>.");
399
- const to = optionalNonEmpty(values.flags.get("--to"));
400
- const clearShelf = values.booleans.has("--clear-shelf");
401
- if (action === "move") {
402
- if (to && clearShelf) {
403
- throw new Error("notes move accepts either --to or --clear-shelf, not both.");
404
- }
405
- if (!to && !clearShelf && !values.flags.get("--folder")) {
406
- throw new Error('notes move needs --to "<shelf>", --clear-shelf, or --folder "<path>".');
407
- }
408
- }
409
- const repairTruncated = values.booleans.has("--repair-truncated");
410
- if (repairTruncated && action !== "sync") {
411
- throw new Error("--repair-truncated belongs to `cockpit notes sync <account-id> --repair-truncated`.");
412
- }
413
- const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
414
- const month = optionalNonEmpty(values.flags.get("--month"));
415
- if (month !== undefined) {
416
- if (action !== "transcription-stats") {
417
- throw new Error("--month belongs to `cockpit notes transcription-stats --month YYYY-MM`.");
418
- }
419
- if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(month)) {
420
- throw new Error("--month is a calendar month like 2026-10.");
421
- }
422
- }
423
- // BLI-18006: a long note is read in parts, like `cockpit docs read --offset`.
424
- const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
425
- let offset;
426
- if (offsetRaw !== undefined) {
427
- if (action !== "show")
428
- throw new Error("--offset belongs to `cockpit notes show <id> --offset <characters> --limit <characters>`.");
429
- offset = Number(offsetRaw);
430
- if (!Number.isInteger(offset) || offset < 0)
431
- throw new Error("--offset must be a whole number 0 or greater.");
432
- }
433
- return {
434
- kind: "notes",
435
- ...(values.booleans.has("--wait") ? { wait: true } : {}),
436
- action,
437
- ...(values.flags.has("--folder") ? { folder: values.flags.get("--folder") } : {}),
438
- ...(action === "mkdir" || action === "rmdir" || action === "rename" ? { folder: rest[0] } : {}),
439
- ...(values.booleans.has("--tree") ? { tree: true } : {}),
440
- homeDir: optionalNonEmpty(values.flags.get("--home")),
441
- dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
442
- ...(noteId ? { noteId } : {}),
443
- ...(accountId ? { accountId } : {}),
444
- ...(provider ? { provider } : {}),
445
- ...(workspace ? { workspace } : {}),
446
- ...(showWebhook ? { showWebhook: true } : {}),
447
- ...(action === "connect" && values.booleans.has("--key-stdin") ? { keyStdin: true } : {}),
448
- ...(paths ? { paths } : {}),
449
- filePath: optionalNonEmpty(values.flags.get("--file")),
450
- name: optionalNonEmpty(values.flags.get("--name")),
451
- exclude: optionalNonEmpty(values.flags.get("--exclude")),
452
- ...(to ? { to } : {}),
453
- ...(clearShelf ? { clearShelf } : {}),
454
- // BLI-4058. `place` says where a note belongs; only `--apply` moves it.
455
- ...(values.booleans.has("--apply") ? { apply: true } : {}),
456
- series: optionalNonEmpty(values.flags.get("--series")),
457
- meetingKind: optionalNonEmpty(values.flags.get("--kind")),
458
- since: optionalNonEmpty(values.flags.get("--since")),
459
- until: optionalNonEmpty(values.flags.get("--until")),
460
- ...(limit === undefined ? {} : { limit }),
461
- ...(month ? { month } : {}),
462
- ...(offset === undefined ? {} : { offset }),
463
- ...(repairTruncated ? { repairTruncated: true } : {}),
464
- yes,
465
- json,
466
- };
186
+ return { render, missed };
467
187
  }
188
+ // Meeting notes have their own argument table; these names keep the address they have always had.
189
+ export { parseNotesArgs } from "./local-args-tower-notes.js";
468
190
  /** The narrowest width worth wrapping to; below it every line is one word. */
469
191
  export const WORKBOOK_MIN_WIDTH = 20;
470
192
  /**
@@ -13,37 +13,36 @@
13
13
  */
14
14
  import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
15
15
  const SETTINGS_SECTIONS = ["personal", "switches", "models", "env", "cli-floor"];
16
+ const SETTINGS_ALLOWED_FLAGS = [
17
+ "--home",
18
+ "--dashboard-url",
19
+ "--json",
20
+ "--chat-model",
21
+ "--brief-model",
22
+ "--chat",
23
+ "--memory",
24
+ "--note-reader",
25
+ "--project",
26
+ "--file",
27
+ "--id",
28
+ "--content-stdin",
29
+ "--yes",
30
+ ];
31
+ const SETTINGS_VALUE_FLAGS = [
32
+ "--home",
33
+ "--dashboard-url",
34
+ "--chat-model",
35
+ "--brief-model",
36
+ "--chat",
37
+ "--memory",
38
+ "--note-reader",
39
+ "--project",
40
+ "--file",
41
+ "--id",
42
+ ];
16
43
  /** `cockpit settings [section] [verb] [args]`: the section picker. */
17
44
  export function parseSettingsArgs(args) {
18
- const values = parseNamedArgs(args, {
19
- allowedFlags: [
20
- "--home",
21
- "--dashboard-url",
22
- "--json",
23
- "--chat-model",
24
- "--brief-model",
25
- "--chat",
26
- "--memory",
27
- "--note-reader",
28
- "--project",
29
- "--file",
30
- "--id",
31
- "--content-stdin",
32
- "--yes",
33
- ],
34
- valueFlags: [
35
- "--home",
36
- "--dashboard-url",
37
- "--chat-model",
38
- "--brief-model",
39
- "--chat",
40
- "--memory",
41
- "--note-reader",
42
- "--project",
43
- "--file",
44
- "--id",
45
- ],
46
- });
45
+ const values = parseNamedArgs(args, { allowedFlags: SETTINGS_ALLOWED_FLAGS, valueFlags: SETTINGS_VALUE_FLAGS });
47
46
  const [rawSection, ...rest] = values.positionals;
48
47
  const common = {
49
48
  homeDir: optionalNonEmpty(values.flags.get("--home")),