@bli-cockpit/cli 0.2.45 → 0.2.46

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.
@@ -59,6 +59,11 @@ export async function runBrief(command, io) {
59
59
  visible_because: body.visibleBecause ?? null,
60
60
  tldr: Boolean(command.tldr),
61
61
  pinned_version: command.version != null,
62
+ asked_day: command.date != null,
63
+ day: body.day?.date ?? null,
64
+ history_count: body.history?.length ?? null,
65
+ history_truncated: body.historyTruncated ?? null,
66
+ delta: command.delta ? (body.delta ? "answered" : (body.deltaReason ?? "missing")) : null,
62
67
  subject: command.subject ? "selected" : "caller",
63
68
  version_count: body.versions?.length ?? null,
64
69
  versions_reason: body.versionsReason ?? null,
@@ -127,12 +132,24 @@ function queryFor(command) {
127
132
  params.set("p", command.subject);
128
133
  if (command.version)
129
134
  params.set("v", command.version);
135
+ if (command.date)
136
+ params.set("d", command.date);
130
137
  if (command.tldr)
131
138
  params.set("tldr", "1");
132
139
  if (command.versions)
133
140
  params.set("versions", "1");
134
141
  if (command.claims)
135
142
  params.set("claims", "1");
143
+ if (command.action === "history") {
144
+ params.set("history", "1");
145
+ if (command.days !== undefined)
146
+ params.set("days", String(command.days));
147
+ }
148
+ if (command.delta) {
149
+ params.set("delta", "1");
150
+ if (command.against)
151
+ params.set("against", command.against);
152
+ }
136
153
  const query = params.toString();
137
154
  return query ? `?${query}` : "";
138
155
  }
@@ -145,9 +162,20 @@ function writeHuman(io, command, body) {
145
162
  // moment rather than something that failed to update.
146
163
  const whose = page.displayName ? `${page.displayName}'s page` : "This page";
147
164
  const pinned = page.olderVersionLabel ? ` · version from ${page.olderVersionLabel}` : "";
148
- writeLine(io.stdout, dim(`${whose}${pinned}`, styled));
165
+ // BLI-3484: which day, in the SUBJECT's zone, said with the zone beside it so
166
+ // nobody has to work out whose midnight this is.
167
+ const day = body.day?.date ? ` · ${body.day.date}${body.day.zone ? ` ${body.day.zone}` : ""}` : "";
168
+ writeLine(io.stdout, dim(`${whose}${pinned}${day}`, styled));
169
+ // `history` lists days and prints no page — a list of dates under a full
170
+ // brief would bury the thing somebody asked for.
171
+ if (command.action === "history") {
172
+ writeHistory(io, body, styled);
173
+ return;
174
+ }
149
175
  writeLine(io.stdout, "");
150
176
  writeLine(io.stdout, body.text ?? "");
177
+ if (command.delta)
178
+ writeDelta(io, body, styled);
151
179
  if (body.claims && body.claims.length > 0) {
152
180
  writeLine(io.stdout, "");
153
181
  writeLine(io.stdout, dim("Claim ids — pass one to `cockpit correct --claim`:", styled));
@@ -181,6 +209,72 @@ function writeHuman(io, command, body) {
181
209
  }
182
210
  }
183
211
  }
212
+ /**
213
+ * `cockpit brief history` — the days there are, newest first (BLI-3484).
214
+ *
215
+ * The masthead line beside each date is what makes the list browsable: a column
216
+ * of dates is a filing cabinet, a column of dates with "Two reviews waiting,
217
+ * one shipped" beside them is a history somebody can find their way around.
218
+ *
219
+ * A short window says so. The oldest date printed is not the oldest date there
220
+ * is when the index hit its cap, and a person picking through history has to
221
+ * know which of those they are looking at.
222
+ */
223
+ function writeHistory(io, body, styled) {
224
+ const history = body.history ?? [];
225
+ writeLine(io.stdout, "");
226
+ if (history.length === 0) {
227
+ writeLine(io.stdout, dim("No days with a page yet.", styled));
228
+ return;
229
+ }
230
+ writeLine(io.stdout, dim("Days — pass one to `cockpit brief --date`:", styled));
231
+ for (const entry of history) {
232
+ const compiles = typeof entry.versionCount === "number" && entry.versionCount > 1
233
+ ? ` (${entry.versionCount} compiles)`
234
+ : "";
235
+ const heading = entry.heading ? ` — ${entry.heading}` : "";
236
+ writeLine(io.stdout, `${dim(` ${entry.date ?? "?"}`, styled)}${heading}${dim(compiles, styled)}`);
237
+ }
238
+ if (body.historyTruncated) {
239
+ writeLine(io.stdout, "");
240
+ writeLine(io.stdout, dim("There are older days than these. Ask for more with `--days <n>`.", styled));
241
+ }
242
+ }
243
+ /**
244
+ * `--delta` — what changed between this day and the one before it.
245
+ *
246
+ * The headline first, then the lines that moved, because the headline is the
247
+ * answer and the detail is the evidence for it. A missing delta prints its
248
+ * reason: silence here would read as "nothing changed", which is a completely
249
+ * different and much worse claim than "I could not work it out".
250
+ */
251
+ function writeDelta(io, body, styled) {
252
+ writeLine(io.stdout, "");
253
+ if (!body.delta) {
254
+ writeLine(io.stdout, dim(`No day-over-day comparison (${body.deltaReason ?? "no reason given"}).`, styled));
255
+ return;
256
+ }
257
+ const delta = body.delta;
258
+ const from = delta.from?.date ?? "the day before";
259
+ const to = delta.to?.date ?? "this day";
260
+ writeLine(io.stdout, dim(`What changed, ${from} → ${to}:`, styled));
261
+ writeLine(io.stdout, ` ${delta.headline ?? "(no headline)"}`);
262
+ for (const item of delta.detail ?? []) {
263
+ writeLine(io.stdout, "");
264
+ if (item.before)
265
+ writeLine(io.stdout, `${dim(" was:", styled)} ${item.before}`);
266
+ if (item.after)
267
+ writeLine(io.stdout, `${dim(" now:", styled)} ${item.after}`);
268
+ if (item.because)
269
+ writeLine(io.stdout, `${dim(" why:", styled)} ${item.because}`);
270
+ }
271
+ if (delta.storedReason) {
272
+ // Not a failure and not hidden either: this comparison skips versions, so
273
+ // it is worked out fresh every time somebody asks for it.
274
+ writeLine(io.stdout, "");
275
+ writeLine(io.stdout, dim(`(not kept: ${delta.storedReason})`, styled));
276
+ }
277
+ }
184
278
  function writeFailure(command, io, reason, detail) {
185
279
  if (command.json) {
186
280
  writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
@@ -174,6 +174,9 @@ async function sendOneTurn(context, prompt, io) {
174
174
  thread: context.command.thread,
175
175
  subject: context.command.subject,
176
176
  model: context.command.model,
177
+ // BLI-3484: which day's page this turn is about. Sent verbatim — the
178
+ // dashboard decides what counts as a date and whose day it is.
179
+ date: context.command.date,
177
180
  },
178
181
  log,
179
182
  });
@@ -263,6 +266,8 @@ function buildAttachmentForm(command, prompt, attachment) {
263
266
  form.set("subject", command.subject);
264
267
  if (command.model)
265
268
  form.set("model", command.model);
269
+ if (command.date)
270
+ form.set("date", command.date);
266
271
  form.set("image", new File([attachment.bytes], attachment.fileName, { type: attachment.mimeType }));
267
272
  return form;
268
273
  }
@@ -842,6 +842,8 @@ function parseJarvisArgs(args) {
842
842
  "--image",
843
843
  "--file",
844
844
  "--no-stream",
845
+ // BLI-3484: which day's page this turn is about.
846
+ "--date",
845
847
  // BLI-3458: reading back what was already said, rather than saying
846
848
  // something new. Neither takes a turn or reaches the model.
847
849
  "--threads",
@@ -858,6 +860,7 @@ function parseJarvisArgs(args) {
858
860
  "--model",
859
861
  "--image",
860
862
  "--file",
863
+ "--date",
861
864
  "--limit",
862
865
  ],
863
866
  });
@@ -892,6 +895,14 @@ function parseJarvisArgs(args) {
892
895
  if ((threads || history) && image) {
893
896
  throw new Error("jarvis --threads and --history do not take an attachment.");
894
897
  }
898
+ // BLI-3484. `--date` binds the page that was live on one of the subject's
899
+ // days, so a turn can be about Sunday's page. Refused on the two reading
900
+ // commands for the same reason an attachment is: they replay what was said
901
+ // and bind no page at all.
902
+ const date = optionalNonEmpty(values.flags.get("--date"));
903
+ if ((threads || history) && date) {
904
+ throw new Error("jarvis --threads and --history replay what was said; they bind no page.");
905
+ }
895
906
  return {
896
907
  kind: "jarvis",
897
908
  homeDir: optionalNonEmpty(values.flags.get("--home")),
@@ -905,6 +916,7 @@ function parseJarvisArgs(args) {
905
916
  // BLI-3381: no client-side allowlist — the dashboard forwards this key
906
917
  // to the inference server's own allowlist and relays its refusal.
907
918
  model: optionalNonEmpty(values.flags.get("--model")),
919
+ ...(date ? { date } : {}),
908
920
  imagePath: image ?? file,
909
921
  // BLI-3457: streaming is on unless a caller opts out. A dashboard that
910
922
  // does not stream yet still answers plain JSON, so this flag is for
@@ -1137,6 +1149,12 @@ function parseWorkbookArgs(args) {
1137
1149
  * does: the dashboard resolves it against the roster and the database (or the
1138
1150
  * route's own mirror of the database's rule) decides whether the page opens.
1139
1151
  * Nothing is decided here.
1152
+ *
1153
+ * `--date` (BLI-3484) is the terminal's `?d=`, and the same sentence applies to
1154
+ * it twice over: what counts as a date, where that person's day starts, and
1155
+ * whether anything was compiled for it are all the dashboard's answer. A local
1156
+ * copy of "when does Tuesday begin" would be a second answer to a question this
1157
+ * product already answers per person, per zone.
1140
1158
  */
1141
1159
  function parseBriefArgs(args) {
1142
1160
  const values = parseNamedArgs(args, {
@@ -1147,6 +1165,10 @@ function parseBriefArgs(args) {
1147
1165
  "--as",
1148
1166
  "--who",
1149
1167
  "--version",
1168
+ "--date",
1169
+ "--delta",
1170
+ "--against",
1171
+ "--days",
1150
1172
  "--tldr",
1151
1173
  "--full",
1152
1174
  "--versions",
@@ -1157,15 +1179,26 @@ function parseBriefArgs(args) {
1157
1179
  "--no-wait",
1158
1180
  "--json",
1159
1181
  ],
1160
- valueFlags: ["--home", "--dashboard-url", "--for", "--as", "--who", "--version", "--reason"],
1182
+ valueFlags: [
1183
+ "--home",
1184
+ "--dashboard-url",
1185
+ "--for",
1186
+ "--as",
1187
+ "--who",
1188
+ "--version",
1189
+ "--date",
1190
+ "--against",
1191
+ "--days",
1192
+ "--reason",
1193
+ ],
1161
1194
  });
1162
1195
  // Bare `cockpit brief` reads the page, which is what somebody typing it almost
1163
1196
  // always wants — the same shape `cockpit notes` and `cockpit scout` have.
1164
1197
  // `status` (BLI-3462) answers why a brief was or was not delivered.
1165
1198
  const first = values.positionals[0];
1166
1199
  const action = (first === undefined ? "read" : first);
1167
- if (!["read", "edit", "rewrite", "status"].includes(action)) {
1168
- throw new Error(`Unknown brief command: ${first}. Try edit, rewrite or status, or nothing to read it.`);
1200
+ if (!["read", "edit", "rewrite", "history", "status"].includes(action)) {
1201
+ throw new Error(`Unknown brief command: ${first}. Try edit, rewrite, history or status, or nothing to read it.`);
1169
1202
  }
1170
1203
  if (values.positionals.length > (first === undefined ? 0 : 1)) {
1171
1204
  throw new Error(`brief ${action} does not take "${values.positionals[1]}".`);
@@ -1192,6 +1225,29 @@ function parseBriefArgs(args) {
1192
1225
  if (action !== "rewrite" && (wait || noWait)) {
1193
1226
  throw new Error("--wait and --no-wait belong to `cockpit brief rewrite`.");
1194
1227
  }
1228
+ // ── Reading a past day (BLI-3484) ────────────────────────────────────────
1229
+ const date = optionalNonEmpty(values.flags.get("--date"));
1230
+ const against = optionalNonEmpty(values.flags.get("--against"));
1231
+ const delta = values.booleans.has("--delta");
1232
+ const days = optionalPositiveInteger(values.flags.get("--days"), "--days");
1233
+ if (date && action !== "read") {
1234
+ throw new Error("--date belongs to `cockpit brief` on its own — it reads one day's page.");
1235
+ }
1236
+ if (delta && action !== "read") {
1237
+ throw new Error("--delta belongs to `cockpit brief` on its own.");
1238
+ }
1239
+ if (against && !delta) {
1240
+ // Said rather than silently ignored: somebody who typed `--against` asked
1241
+ // for a comparison, and running the plain read would answer a different
1242
+ // question without saying so.
1243
+ throw new Error("--against needs --delta: it names the day to compare against.");
1244
+ }
1245
+ if (days !== undefined && action !== "history") {
1246
+ throw new Error("--days belongs to `cockpit brief history`.");
1247
+ }
1248
+ if (action === "history" && (values.booleans.has("--tldr") || values.booleans.has("--claims"))) {
1249
+ throw new Error("brief history lists days; --tldr and --claims belong to reading a page.");
1250
+ }
1195
1251
  // `--as` is accepted as an alias so the two conversational commands read the
1196
1252
  // same way; `cockpit jarvis --as <person>` has meant this since BLI-3380.
1197
1253
  // `--who` is the third spelling, and it exists because `cockpit brief status
@@ -1215,6 +1271,10 @@ function parseBriefArgs(args) {
1215
1271
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
1216
1272
  subject: forPerson ?? asPerson ?? whoPerson,
1217
1273
  version: optionalNonEmpty(values.flags.get("--version")),
1274
+ ...(date ? { date } : {}),
1275
+ ...(delta ? { delta: true } : {}),
1276
+ ...(against ? { against } : {}),
1277
+ ...(days !== undefined ? { days } : {}),
1218
1278
  tldr,
1219
1279
  versions: values.booleans.has("--versions"),
1220
1280
  claims: values.booleans.has("--claims"),
@@ -56,7 +56,7 @@ export function localCommandHelp(command) {
56
56
  " cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
57
57
  " cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
58
58
  " cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
59
- " cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
59
+ " cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
60
60
  " cockpit model [show|set <provider:model>] [--json]",
61
61
  " cockpit scout [start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
62
62
  " cockpit ops [status [--job <id>] [--skips] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
@@ -64,7 +64,7 @@ export function localCommandHelp(command) {
64
64
  " cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
65
65
  " cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes]] [--json]",
66
66
  " cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
67
- " cockpit brief [edit|rewrite] [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
67
+ " cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
68
68
  " cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
69
69
  " cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
70
70
  " cockpit notes [list|show <id>|shelf|shelves|upload <paths...>|paste|share <id>|unshare <id>|move <id>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--yes] [--dashboard-url <url>] [--json]",
@@ -274,10 +274,11 @@ function localSubcommandHelp(command) {
274
274
  [
275
275
  "jarvis",
276
276
  [
277
- "Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
277
+ "Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
278
278
  "",
279
279
  "Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
280
280
  "--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
281
+ "--date <YYYY-MM-DD> binds the page that was live on one of that person's days (`today` and `yesterday` work too), so the turn is about that day's page instead of the newest one. A day with nothing compiled binds no page and JARVIS says so rather than answering from a different day.",
281
282
  "--model <key> requests one provider:model pair (e.g. openai:gpt-5.6-terra); an unrecognised key is refused by the dashboard, not this command.",
282
283
  "--image <path> (alias --file) attaches one local PNG, JPEG, or WEBP under 10 MB with the question, the same one-image gate the JARVIS panel uses. A bad path, an unsupported type, or an oversized file is refused with its own plain sentence — never a stack trace.",
283
284
  "Run with no question for an interactive terminal conversation.",
@@ -377,7 +378,7 @@ function localSubcommandHelp(command) {
377
378
  [
378
379
  "brief",
379
380
  [
380
- "Usage: cockpit brief [edit|rewrite] [--for <person>] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
381
+ "Usage: cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
381
382
  "",
382
383
  "Prints the TODAY page — the same page the Tower website shows, rendered for a terminal.",
383
384
  "--for opens somebody else's page; the website's own rule decides whether you may, and it refuses in plain words when you may not.",
@@ -386,6 +387,14 @@ function localSubcommandHelp(command) {
386
387
  "--claims prints the [claimId] beside every line, which is what `cockpit correct --claim` takes.",
387
388
  "Reading it here counts as opening it, exactly as opening it in a browser does.",
388
389
  "",
390
+ "--date <YYYY-MM-DD> reads the page that was live on one day — `today` and `yesterday` work too. Days are that person's own days, in their own timezone, so a page always sits on the date its own masthead says.",
391
+ " A day with nothing compiled says so by name, and says which day is nearest. It never quietly hands you a different day's page.",
392
+ "--delta says what changed between that day and the day before it, in the same words the website's version history uses. --against <day> compares against a day you name instead.",
393
+ " Comparisons that skip versions are worked out fresh each time and say `(not kept)` — a day with several compiles is one of those.",
394
+ "",
395
+ "history — the days there are, newest first, each with its masthead line and how many times it compiled that day. --days <n> asks for more than the last 14.",
396
+ " Pass any date it prints back to `--date`.",
397
+ "",
389
398
  " cockpit brief status [--who <person>] [--render]",
390
399
  " Why your brief did or did not arrive this morning — one reason, from the same",
391
400
  " chain the Slack DM cron itself follows: not_due, not_monday, not_the_1st,",
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.45");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.46");
19
19
  return 0;
20
20
  }
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {