@bli-cockpit/cli 0.2.149 → 0.2.150

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.
@@ -23,6 +23,7 @@ export async function openAgentDoor(commandName, command, io) {
23
23
  deviceToken: session.device_token,
24
24
  io,
25
25
  json: command.json,
26
+ homeDir: command.homeDir,
26
27
  };
27
28
  }
28
29
  export async function askAgentDoor(door, options) {
@@ -35,6 +36,8 @@ export async function askAgentDoor(door, options) {
35
36
  label: options.label,
36
37
  timeoutMs: options.timeoutMs,
37
38
  ...(options.body === undefined ? {} : { body: options.body }),
39
+ ...(options.warmRoute === undefined ? {} : { warmRoute: options.warmRoute }),
40
+ ...(door.homeDir === undefined ? {} : { homeDir: door.homeDir }),
38
41
  log: (line) => writeLine(door.io.stderr, line),
39
42
  });
40
43
  if (!result.ok) {
@@ -0,0 +1,107 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
3
+ const TAG = "[cal cli]";
4
+ const WRITE_DEADLINE_MS = 60_000;
5
+ /** A first read of a crowded calendar can take a while. */
6
+ const SYNC_DEADLINE_MS = 120_000;
7
+ const URL_MAX_CHARS = 2_000;
8
+ export async function runCalAddIcal(command, door) {
9
+ // The address comes from stdin and nowhere else. An interactive terminal
10
+ // with nothing piped in is told how, rather than left waiting on a prompt
11
+ // that would echo a permanent credential into the scrollback.
12
+ if (isInteractiveStdin(door.io)) {
13
+ return failAgentDoor(door, TAG, "address_required_on_stdin", 'The calendar\'s secret address is read from stdin, never from a flag. Try: printf "%s" "https://calendar.google.com/calendar/ical/…/basic.ics" | cockpit cal add-ical --name "Personal"');
14
+ }
15
+ let icalUrl;
16
+ try {
17
+ icalUrl = (await readPipedText(door.io.stdin, {
18
+ maxChars: URL_MAX_CHARS,
19
+ overflowMessage: "That is longer than any calendar address; nothing was attached.",
20
+ })).trim();
21
+ }
22
+ catch (error) {
23
+ return failAgentDoor(door, TAG, "address_unreadable", error instanceof Error ? error.message : String(error));
24
+ }
25
+ if (icalUrl === "") {
26
+ return failAgentDoor(door, TAG, "address_required_on_stdin", "Nothing arrived on stdin, so nothing was attached.");
27
+ }
28
+ const answer = await askAgentDoor(door, {
29
+ path: "/api/cal/calendars",
30
+ method: "POST",
31
+ label: "cal add-ical",
32
+ timeoutMs: WRITE_DEADLINE_MS,
33
+ body: {
34
+ ical_url: icalUrl,
35
+ name: command.name ?? null,
36
+ ...(command.orgVisible ? { org_visible: true } : {}),
37
+ },
38
+ });
39
+ if (!answer.ok)
40
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
41
+ const calendar = answer.body.calendar;
42
+ if (door.json)
43
+ return emitAgentDoor(door, { ok: true, calendar });
44
+ writeLine(door.io.stdout, `Attached (${calendar?.id ?? "no id returned"}).`);
45
+ writeLine(door.io.stdout, `Nothing is read until a sync runs: \`cockpit cal sync ${calendar?.id ?? "<id>"}\`, or wait for the cron.`);
46
+ return 0;
47
+ }
48
+ export async function runCalShare(command, door) {
49
+ const orgVisible = command.orgVisible && !command.makePrivate;
50
+ const answer = await askAgentDoor(door, {
51
+ path: `/api/cal/calendars?calendar_id=${encodeURIComponent(command.subjectId ?? "")}`,
52
+ method: "PATCH",
53
+ label: "cal share",
54
+ timeoutMs: WRITE_DEADLINE_MS,
55
+ body: { org_visible: orgVisible },
56
+ });
57
+ if (!answer.ok)
58
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
59
+ const calendar = answer.body.calendar;
60
+ if (door.json)
61
+ return emitAgentDoor(door, { ok: true, calendar });
62
+ writeLine(door.io.stdout, orgVisible
63
+ ? `"${calendar?.name ?? command.subjectId}" is now readable by everyone at BLI. Nobody but you can change it.`
64
+ : `"${calendar?.name ?? command.subjectId}" is private again — only you can read it.`);
65
+ return 0;
66
+ }
67
+ export async function runCalDetach(command, door) {
68
+ const answer = await askAgentDoor(door, {
69
+ path: `/api/cal/calendars?calendar_id=${encodeURIComponent(command.subjectId ?? "")}`,
70
+ method: "DELETE",
71
+ label: "cal detach",
72
+ timeoutMs: WRITE_DEADLINE_MS,
73
+ });
74
+ if (!answer.ok)
75
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
76
+ if (door.json)
77
+ return emitAgentDoor(door, { ok: true, detached: command.subjectId });
78
+ writeLine(door.io.stdout, `Detached ${command.subjectId}. Its stored events went with it; the calendar itself is untouched at Google.`);
79
+ return 0;
80
+ }
81
+ export async function runCalSync(command, door) {
82
+ const answer = await askAgentDoor(door, {
83
+ path: `/api/cal/calendars/${encodeURIComponent(command.subjectId ?? "")}/sync${command.full ? "?full=1" : ""}`,
84
+ method: "POST",
85
+ label: "cal sync",
86
+ timeoutMs: SYNC_DEADLINE_MS,
87
+ });
88
+ if (!answer.ok)
89
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
90
+ const run = answer.body.run;
91
+ if (door.json)
92
+ return emitAgentDoor(door, { ok: true, run });
93
+ if (!run) {
94
+ writeLine(door.io.stdout, "Tower answered without a run. Nothing to report.");
95
+ return 0;
96
+ }
97
+ if (run.reason === "ok") {
98
+ writeLine(door.io.stdout, run.detail === "feed_unchanged"
99
+ ? "Nothing has changed on that calendar since the last read."
100
+ : `Synced: ${run.eventsAdded} new, ${run.eventsUpdated} updated, ${run.eventsDeleted} gone.`);
101
+ return 0;
102
+ }
103
+ // A failure names itself; the exit code says it failed.
104
+ writeLine(door.io.stderr, `${TAG} sync ${JSON.stringify({ reason: run.reason, detail: run.detail })}`);
105
+ writeLine(door.io.stdout, `That calendar did not sync: ${run.reason}${run.detail ? ` (${run.detail})` : ""}.`);
106
+ return 1;
107
+ }
@@ -21,15 +21,14 @@
21
21
  * computed in UTC and "today" would end at 5pm in Vancouver.
22
22
  */
23
23
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
24
+ import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-calendars-write.js";
24
25
  import { runCalDelete } from "./cal-delete.js";
25
26
  import { runCalSyncStatus } from "./cal-sync-status.js";
26
- import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
27
+ import { writeLine } from "./cli-io.js";
28
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
27
29
  const TAG = "[cal cli]";
28
30
  const READ_DEADLINE_MS = 30_000;
29
31
  const WRITE_DEADLINE_MS = 60_000;
30
- /** A first read of a crowded calendar can take a while. */
31
- const SYNC_DEADLINE_MS = 120_000;
32
- const URL_MAX_CHARS = 2_000;
33
32
  export async function runCal(command, io) {
34
33
  const door = await openAgentDoor("cal", command, io);
35
34
  switch (command.action) {
@@ -42,15 +41,15 @@ export async function runCal(command, io) {
42
41
  case "calendars":
43
42
  return listCalendars(door);
44
43
  case "add-ical":
45
- return addIcal(command, door);
44
+ return runCalAddIcal(command, door);
46
45
  case "create":
47
46
  return create(command, door);
48
47
  case "share":
49
- return share(command, door);
48
+ return runCalShare(command, door);
50
49
  case "detach":
51
- return detach(command, door);
50
+ return runCalDetach(command, door);
52
51
  case "sync":
53
- return sync(command, door);
52
+ return runCalSync(command, door);
54
53
  case "sync-status":
55
54
  return runCalSyncStatus(command, door);
56
55
  case "delete":
@@ -92,17 +91,37 @@ async function window(command, door) {
92
91
  // `--from`/`--to` beat a named window, on any of the three verbs: somebody
93
92
  // who named two dates meant them.
94
93
  const path = command.from && command.to ? "events" : command.action;
95
- const answer = await askAgentDoor(door, {
96
- path: `/api/cal/${path}?${windowQuery(command).toString()}`,
94
+ const apiPath = `/api/cal/${path}?${windowQuery(command).toString()}`;
95
+ // BLI-10746: `cal today` is the one read this ticket measured; the warm
96
+ // route and the cache both apply to it (and only it).
97
+ const cached = command.action === "today";
98
+ const ask = () => askAgentDoor(door, {
99
+ path: apiPath,
97
100
  method: "GET",
98
101
  label: `cal ${command.action}`,
99
102
  timeoutMs: READ_DEADLINE_MS,
103
+ warmRoute: cached,
100
104
  });
105
+ const answer = cached
106
+ ? await readThroughCache({
107
+ io: door.io,
108
+ homeDir: door.homeDir,
109
+ deviceToken: door.deviceToken,
110
+ surface: "cal",
111
+ label: "cal today",
112
+ path: apiPath,
113
+ json: door.json,
114
+ ask,
115
+ })
116
+ : { ...(await ask()), fromCache: false, ageMs: null };
101
117
  if (!answer.ok)
102
118
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
103
119
  const body = answer.body;
104
120
  if (door.json)
105
121
  return emitAgentDoor(door, { ok: true, ...body });
122
+ if (answer.fromCache) {
123
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
124
+ }
106
125
  const zone = body.window?.time_zone ?? localTimeZone();
107
126
  const events = body.events ?? [];
108
127
  if (events.length === 0) {
@@ -246,46 +265,6 @@ async function listCalendars(door) {
246
265
  writeLine(door.io.stdout, `${calendars.length} calendar(s). ids: ${calendars.map((calendar) => calendar.id).join(", ")}`);
247
266
  return 0;
248
267
  }
249
- async function addIcal(command, door) {
250
- // The address comes from stdin and nowhere else. An interactive terminal
251
- // with nothing piped in is told how, rather than left waiting on a prompt
252
- // that would echo a permanent credential into the scrollback.
253
- if (isInteractiveStdin(door.io)) {
254
- return failAgentDoor(door, TAG, "address_required_on_stdin", 'The calendar\'s secret address is read from stdin, never from a flag. Try: printf "%s" "https://calendar.google.com/calendar/ical/…/basic.ics" | cockpit cal add-ical --name "Personal"');
255
- }
256
- let icalUrl;
257
- try {
258
- icalUrl = (await readPipedText(door.io.stdin, {
259
- maxChars: URL_MAX_CHARS,
260
- overflowMessage: "That is longer than any calendar address; nothing was attached.",
261
- })).trim();
262
- }
263
- catch (error) {
264
- return failAgentDoor(door, TAG, "address_unreadable", error instanceof Error ? error.message : String(error));
265
- }
266
- if (icalUrl === "") {
267
- return failAgentDoor(door, TAG, "address_required_on_stdin", "Nothing arrived on stdin, so nothing was attached.");
268
- }
269
- const answer = await askAgentDoor(door, {
270
- path: "/api/cal/calendars",
271
- method: "POST",
272
- label: "cal add-ical",
273
- timeoutMs: WRITE_DEADLINE_MS,
274
- body: {
275
- ical_url: icalUrl,
276
- name: command.name ?? null,
277
- ...(command.orgVisible ? { org_visible: true } : {}),
278
- },
279
- });
280
- if (!answer.ok)
281
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
282
- const calendar = answer.body.calendar;
283
- if (door.json)
284
- return emitAgentDoor(door, { ok: true, calendar });
285
- writeLine(door.io.stdout, `Attached (${calendar?.id ?? "no id returned"}).`);
286
- writeLine(door.io.stdout, `Nothing is read until a sync runs: \`cockpit cal sync ${calendar?.id ?? "<id>"}\`, or wait for the cron.`);
287
- return 0;
288
- }
289
268
  async function create(command, door) {
290
269
  const answer = await askAgentDoor(door, {
291
270
  path: "/api/cal/events",
@@ -328,64 +307,4 @@ async function create(command, door) {
328
307
  writeLine(door.io.stdout, `Invitations went to ${command.attendees.join(", ")} from Google.`);
329
308
  }
330
309
  return 0;
331
- }
332
- async function share(command, door) {
333
- const orgVisible = command.orgVisible && !command.makePrivate;
334
- const answer = await askAgentDoor(door, {
335
- path: `/api/cal/calendars?calendar_id=${encodeURIComponent(command.subjectId ?? "")}`,
336
- method: "PATCH",
337
- label: "cal share",
338
- timeoutMs: WRITE_DEADLINE_MS,
339
- body: { org_visible: orgVisible },
340
- });
341
- if (!answer.ok)
342
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
343
- const calendar = answer.body.calendar;
344
- if (door.json)
345
- return emitAgentDoor(door, { ok: true, calendar });
346
- writeLine(door.io.stdout, orgVisible
347
- ? `"${calendar?.name ?? command.subjectId}" is now readable by everyone at BLI. Nobody but you can change it.`
348
- : `"${calendar?.name ?? command.subjectId}" is private again — only you can read it.`);
349
- return 0;
350
- }
351
- async function detach(command, door) {
352
- const answer = await askAgentDoor(door, {
353
- path: `/api/cal/calendars?calendar_id=${encodeURIComponent(command.subjectId ?? "")}`,
354
- method: "DELETE",
355
- label: "cal detach",
356
- timeoutMs: WRITE_DEADLINE_MS,
357
- });
358
- if (!answer.ok)
359
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
360
- if (door.json)
361
- return emitAgentDoor(door, { ok: true, detached: command.subjectId });
362
- writeLine(door.io.stdout, `Detached ${command.subjectId}. Its stored events went with it; the calendar itself is untouched at Google.`);
363
- return 0;
364
- }
365
- async function sync(command, door) {
366
- const answer = await askAgentDoor(door, {
367
- path: `/api/cal/calendars/${encodeURIComponent(command.subjectId ?? "")}/sync${command.full ? "?full=1" : ""}`,
368
- method: "POST",
369
- label: "cal sync",
370
- timeoutMs: SYNC_DEADLINE_MS,
371
- });
372
- if (!answer.ok)
373
- return failAgentDoor(door, TAG, answer.reason, answer.detail);
374
- const run = answer.body.run;
375
- if (door.json)
376
- return emitAgentDoor(door, { ok: true, run });
377
- if (!run) {
378
- writeLine(door.io.stdout, "Tower answered without a run. Nothing to report.");
379
- return 0;
380
- }
381
- if (run.reason === "ok") {
382
- writeLine(door.io.stdout, run.detail === "feed_unchanged"
383
- ? "Nothing has changed on that calendar since the last read."
384
- : `Synced: ${run.eventsAdded} new, ${run.eventsUpdated} updated, ${run.eventsDeleted} gone.`);
385
- return 0;
386
- }
387
- // A failure names itself; the exit code says it failed.
388
- writeLine(door.io.stderr, `${TAG} sync ${JSON.stringify({ reason: run.reason, detail: run.detail })}`);
389
- writeLine(door.io.stdout, `That calendar did not sync: ${run.reason}${run.detail ? ` (${run.detail})` : ""}.`);
390
- return 1;
391
310
  }
@@ -32,6 +32,7 @@ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
32
32
  import { readDoc } from "./docs-read.js";
33
33
  import { linkDoc, syncStatusDocs } from "./docs-notion.js";
34
34
  import { readNoteFile } from "./notes-file.js";
35
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
35
36
  const TAG = "[docs cli]";
36
37
  const READ_DEADLINE_MS = 30_000;
37
38
  const WRITE_DEADLINE_MS = 60_000;
@@ -74,17 +75,32 @@ export function docsListPath(command) {
74
75
  return search.length > 0 ? `/api/docs/documents?${search}` : "/api/docs/documents";
75
76
  }
76
77
  async function listDocs(command, door) {
77
- const answer = await askAgentDoor(door, {
78
- path: docsListPath(command),
79
- method: "GET",
78
+ const listPath = docsListPath(command);
79
+ const answer = await readThroughCache({
80
+ io: door.io,
81
+ homeDir: door.homeDir,
82
+ deviceToken: door.deviceToken,
83
+ surface: "docs",
80
84
  label: "docs list",
81
- timeoutMs: READ_DEADLINE_MS,
85
+ path: listPath,
86
+ json: door.json,
87
+ ask: () => askAgentDoor(door, {
88
+ path: listPath,
89
+ method: "GET",
90
+ label: "docs list",
91
+ timeoutMs: READ_DEADLINE_MS,
92
+ // BLI-10746: route through the resident memory daemon's warm connection.
93
+ warmRoute: true,
94
+ }),
82
95
  });
83
96
  if (!answer.ok)
84
97
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
85
98
  const documents = (answer.body.documents ?? []);
86
99
  if (door.json)
87
100
  return emitAgentDoor(door, { ok: true, documents });
101
+ if (answer.fromCache) {
102
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
103
+ }
88
104
  if (documents.length === 0) {
89
105
  writeLine(door.io.stdout, command.query ? `No document matches "${command.query}".` : "No documents.");
90
106
  return 0;
@@ -22,6 +22,13 @@ async function fetchTeamDirectory(door) {
22
22
  method: "GET",
23
23
  label: "issue assignee directory",
24
24
  timeoutMs: TEAM_DIRECTORY_TIMEOUT_MS,
25
+ // BLI-10746: this runs IN PARALLEL with `work list`'s own issues fetch
26
+ // (BLI-10579) — measured live, an un-warmed directory call was the
27
+ // reason `work list`'s end-to-end time did not show the daemon's own
28
+ // ~300ms handshake saving (its own fetch dropped from 749ms cold to
29
+ // ~420-500ms warm; the command's total didn't, because this parallel
30
+ // call was still paying a cold connection every time).
31
+ warmRoute: true,
25
32
  });
26
33
  if (!answer.ok)
27
34
  return { ok: false, people: [], reason: answer.reason };
@@ -28,6 +28,7 @@
28
28
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
29
29
  import { writeLine } from "./cli-io.js";
30
30
  import { assigneeLabel, beginAssigneeDirectoryFetch, finishAssigneeDirectory, withAssigneeIdentity } from "./issue-directory.js";
31
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
31
32
  import { showIssue } from "./issue-show.js";
32
33
  import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
33
34
  import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
@@ -111,11 +112,26 @@ async function listIssues(command, door) {
111
112
  // instead of waiting for that answer first (measured ~580ms of the ~580ms
112
113
  // `work list` p50 was exactly this serial second call).
113
114
  const directoryFetch = beginAssigneeDirectoryFetch(door);
114
- const answer = await askAgentDoor(door, {
115
- path: `/api/work/issues${suffix ? `?${suffix}` : ""}`,
116
- method: "GET",
115
+ const listPath = `/api/work/issues${suffix ? `?${suffix}` : ""}`;
116
+ const answer = await readThroughCache({
117
+ io: door.io,
118
+ homeDir: door.homeDir,
119
+ deviceToken: door.deviceToken,
120
+ surface: "work",
117
121
  label: "issue list",
118
- timeoutMs: READ_DEADLINE_MS,
122
+ path: listPath,
123
+ json: door.json,
124
+ ask: () => askAgentDoor(door, {
125
+ path: listPath,
126
+ method: "GET",
127
+ label: "issue list",
128
+ timeoutMs: READ_DEADLINE_MS,
129
+ // BLI-10746: `work list` was the slowest of the six reads measured
130
+ // from Vietnam (1,682 ms p50) — route it through the resident memory
131
+ // daemon's warm connection so this command skips a fresh TCP+TLS
132
+ // handshake.
133
+ warmRoute: true,
134
+ }),
119
135
  });
120
136
  if (!answer.ok) {
121
137
  // Not awaited for its own sake — `finishAssigneeDirectory` always awaits
@@ -123,6 +139,9 @@ async function listIssues(command, door) {
123
139
  await finishAssigneeDirectory(door, directoryFetch, []);
124
140
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
125
141
  }
142
+ if (answer.fromCache && !door.json) {
143
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
144
+ }
126
145
  const body = answer.body;
127
146
  const issues = body.issues ?? [];
128
147
  // BLI-10447: the gaps go to stderr in both modes, so `--json` stdout stays one object.
@@ -65,6 +65,20 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
65
65
  argv = argv.filter((token) => token !== "--quiet");
66
66
  process.env.COCKPIT_QUIET = "1";
67
67
  }
68
+ // BLI-10746: `--fresh`/`--allow-stale` are global toggles for the
69
+ // stale-while-revalidate read cache, same reason and same shape as
70
+ // `--quiet` above — stripped here so none of the seven cached read
71
+ // commands' own `allowedFlags` needs to learn them, and mirrored onto
72
+ // env vars `read-cache.ts` reads. One `cockpit` process makes at most one
73
+ // cacheable request, so a process-wide toggle is exactly a per-request one.
74
+ if (argv.includes("--fresh")) {
75
+ argv = argv.filter((token) => token !== "--fresh");
76
+ process.env.COCKPIT_READ_FRESH = "1";
77
+ }
78
+ if (argv.includes("--allow-stale")) {
79
+ argv = argv.filter((token) => token !== "--allow-stale");
80
+ process.env.COCKPIT_READ_ALLOW_STALE = "1";
81
+ }
68
82
  if (isLocalHelpRequest(argv)) {
69
83
  writeLine(io.stdout, localCommandHelp(argv[0]));
70
84
  return 0;
@@ -24,6 +24,7 @@ import { writeFile } from "node:fs/promises";
24
24
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
25
25
  import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
26
26
  import { readNoteFile } from "./notes-file.js";
27
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
27
28
  import { towerRequest } from "../tower-client.js";
28
29
  const TAG = "[mail cli]";
29
30
  const READ_DEADLINE_MS = 30_000;
@@ -151,17 +152,32 @@ async function inbox(command, door) {
151
152
  if (command.label)
152
153
  query.set("label", command.label);
153
154
  const suffix = query.toString();
154
- const answer = await askAgentDoor(door, {
155
- path: `/api/mail/inbox${suffix ? `?${suffix}` : ""}`,
156
- method: "GET",
155
+ const inboxPath = `/api/mail/inbox${suffix ? `?${suffix}` : ""}`;
156
+ const answer = await readThroughCache({
157
+ io: door.io,
158
+ homeDir: door.homeDir,
159
+ deviceToken: door.deviceToken,
160
+ surface: "mail",
157
161
  label: "mail inbox",
158
- timeoutMs: READ_DEADLINE_MS,
162
+ path: inboxPath,
163
+ json: door.json,
164
+ ask: () => askAgentDoor(door, {
165
+ path: inboxPath,
166
+ method: "GET",
167
+ label: "mail inbox",
168
+ timeoutMs: READ_DEADLINE_MS,
169
+ // BLI-10746: route through the resident memory daemon's warm connection.
170
+ warmRoute: true,
171
+ }),
159
172
  });
160
173
  if (!answer.ok)
161
174
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
162
175
  const body = answer.body;
163
176
  if (door.json)
164
177
  return emitAgentDoor(door, { ok: true, ...body });
178
+ if (answer.fromCache) {
179
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
180
+ }
165
181
  const messages = body.messages ?? [];
166
182
  if (messages.length === 0) {
167
183
  writeLine(door.io.stdout, (body.accounts ?? []).length === 0 ? "No mailboxes attached yet." : "No mail matches that.");
@@ -46,6 +46,7 @@ import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.
46
46
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
47
47
  import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
48
48
  import { runMemoryConsolidation } from "./memory-consolidation.js";
49
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
49
50
  import { runMemoryRequeue } from "./memory-requeue.js";
50
51
  import { readNoteFile } from "./notes-file.js";
51
52
  export { runMemoryVerbList } from "./memory-verb-list.js";
@@ -142,17 +143,30 @@ function containerTagsFor(command) {
142
143
  }
143
144
  async function searchMemory(command, door) {
144
145
  const containerTags = containerTagsFor(command);
145
- const answer = await askAgentDoor(door, {
146
- path: "/api/memory/search",
147
- method: "POST",
146
+ const requestBody = {
147
+ query: command.query,
148
+ containerTag: containerTags,
149
+ ...(command.limit === undefined ? {} : { limit: command.limit }),
150
+ includeProfile: command.includeRecent !== false,
151
+ };
152
+ const answer = await readThroughCache({
153
+ io: door.io,
154
+ homeDir: door.homeDir,
155
+ deviceToken: door.deviceToken,
156
+ surface: "memory",
148
157
  label: "memory search",
149
- timeoutMs: SEARCH_DEADLINE_MS,
150
- body: {
151
- query: command.query,
152
- containerTag: containerTags,
153
- ...(command.limit === undefined ? {} : { limit: command.limit }),
154
- includeProfile: command.includeRecent !== false,
155
- },
158
+ path: "/api/memory/search",
159
+ requestBody,
160
+ json: door.json,
161
+ ask: () => askAgentDoor(door, {
162
+ path: "/api/memory/search",
163
+ method: "POST",
164
+ label: "memory search",
165
+ timeoutMs: SEARCH_DEADLINE_MS,
166
+ // BLI-10746: the one POST this repo treats as a warm-routable read.
167
+ warmRoute: true,
168
+ body: requestBody,
169
+ }),
156
170
  });
157
171
  if (!answer.ok)
158
172
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
@@ -170,6 +184,9 @@ async function searchMemory(command, door) {
170
184
  })}`);
171
185
  if (door.json)
172
186
  return emitAgentDoor(door, answer.body);
187
+ if (answer.fromCache) {
188
+ writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
189
+ }
173
190
  if (results.length === 0 && body.degraded) {
174
191
  // BLI-10448: an empty answer from a search that lost a channel is NOT the
175
192
  // record being silent. `semantic_timed_out` is the store answering on
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
19
19
  import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
20
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
20
21
  const TAG = "[msg cli]";
21
22
  const READ_DEADLINE_MS = 30_000;
22
23
  const WRITE_DEADLINE_MS = 30_000;
@@ -39,15 +40,31 @@ export async function runMsg(command, io) {
39
40
  }
40
41
  }
41
42
  async function fetchChannels(door) {
42
- const answer = await askAgentDoor(door, {
43
- path: "/api/msg/channels",
44
- method: "GET",
43
+ const answer = await readThroughCache({
44
+ io: door.io,
45
+ homeDir: door.homeDir,
46
+ deviceToken: door.deviceToken,
47
+ surface: "msg",
45
48
  label: "msg channels",
46
- timeoutMs: READ_DEADLINE_MS,
49
+ path: "/api/msg/channels",
50
+ json: door.json,
51
+ ask: () => askAgentDoor(door, {
52
+ path: "/api/msg/channels",
53
+ method: "GET",
54
+ label: "msg channels",
55
+ timeoutMs: READ_DEADLINE_MS,
56
+ // BLI-10746: route through the resident memory daemon's warm connection.
57
+ warmRoute: true,
58
+ }),
47
59
  });
48
60
  if (!answer.ok)
49
61
  return { ok: false, reason: answer.reason, detail: answer.detail };
50
- return { ok: true, channels: answer.body.channels ?? [] };
62
+ return {
63
+ ok: true,
64
+ channels: answer.body.channels ?? [],
65
+ fromCache: answer.fromCache,
66
+ ageMs: answer.ageMs,
67
+ };
51
68
  }
52
69
  async function listChannels(door) {
53
70
  const fetched = await fetchChannels(door);
@@ -55,6 +72,9 @@ async function listChannels(door) {
55
72
  return failAgentDoor(door, TAG, fetched.reason, fetched.detail);
56
73
  if (door.json)
57
74
  return emitAgentDoor(door, { ok: true, channels: fetched.channels });
75
+ if (fetched.fromCache) {
76
+ writeLine(door.io.stderr, `(${cacheAgeLabel(fetched.ageMs ?? 0)}, refreshing in the background)`);
77
+ }
58
78
  if (fetched.channels.length === 0) {
59
79
  writeLine(door.io.stdout, "No channels.");
60
80
  return 0;
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.149");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.150");
25
25
  return 0;
26
26
  }
27
27
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * STALE-WHILE-REVALIDATE FOR THE SEVEN READ COMMANDS BLI-10746 MEASURED
3
+ * (`work list`, `docs list`, `cal today`, `mail inbox`, `msg channels`,
4
+ * `settings personal`, `memory search`).
5
+ *
6
+ * Text mode: a cache hit prints INSTANTLY (its age travels with it), then a
7
+ * background refresh is spawned so the next call is live — never blocking
8
+ * this process on a network round trip it already has a recent answer for.
9
+ * `--json` (an agent, not a person) trusts cache only under
10
+ * `JSON_FRESH_BOUND_MS`, unless `--allow-stale` says otherwise; older than
11
+ * that runs the live request inline, same as a miss. `--fresh` skips the
12
+ * read entirely — both flags are process-wide (BLI-10450's `--quiet`
13
+ * pattern), stripped from argv and mirrored onto `COCKPIT_READ_FRESH` /
14
+ * `COCKPIT_READ_ALLOW_STALE` by `local.ts`, so none of these seven parsers'
15
+ * own `allowedFlags` needed to learn a new flag.
16
+ *
17
+ * **The background refresh is a whole second `cockpit` process, detached.**
18
+ * Not a lighter internal-only command: it re-runs THIS SAME argv with
19
+ * `--fresh` appended, `stdio: "ignore"`, unref'd. That is deliberately the
20
+ * simplest correct thing — it reuses every bit of behaviour the live path
21
+ * already has (auth, rendering, the warm route) with zero drift risk, at
22
+ * the cost of a process this ticket's "abundant compute" call already
23
+ * accepts (`docs/architecture/repo-map.md`'s standing orders). The PARENT
24
+ * never waits on it, so the terminal returns control the instant the cached
25
+ * answer is printed — a detached child is the only shape that lets a write
26
+ * to disk keep happening after THIS process has already exited (an
27
+ * unref'd but still-open socket dies with the process; a separate process
28
+ * does not).
29
+ *
30
+ * **Any write invalidates.** `tower-client.ts`'s `towerRequest` calls
31
+ * `invalidateReadCacheSurface` (from `@bli-cockpit/telemetry-core`'s
32
+ * `./node-read-cache`, the same module this reads and writes through) on
33
+ * every successful non-GET response whose path names one of the seven
34
+ * surfaces — coarse (the whole surface, not just the one changed row) and
35
+ * correct: a stale row is never possible, only one avoidable extra request.
36
+ */
37
+ import { spawn } from "node:child_process";
38
+ import { readCacheEntryPath, readCacheKey, readReadCacheEntry, writeReadCacheEntryAtomic, } from "@bli-cockpit/telemetry-core/node-read-cache";
39
+ import { writeLine } from "./cli-io.js";
40
+ import { getCollectorRuntimePaths } from "../local-state-paths.js";
41
+ /** An agent's `--json` read trusts cache only this fresh, unless `--allow-stale`. */
42
+ export const JSON_FRESH_BOUND_MS = 30_000;
43
+ /** Never spawns a second refresh for an answer this recently written — avoids a pile-up under rapid repeated calls. */
44
+ export const MIN_REFRESH_INTERVAL_MS = 3_000;
45
+ /** `"cached 3s ago"` / `"cached 4m ago"` — the one phrasing every cached read command prints. */
46
+ export function cacheAgeLabel(ageMs) {
47
+ const seconds = Math.max(0, Math.round(ageMs / 1000));
48
+ if (seconds < 60)
49
+ return `cached ${seconds}s ago`;
50
+ const minutes = Math.round(seconds / 60);
51
+ return `cached ${minutes}m ago`;
52
+ }
53
+ export async function readThroughCache(options) {
54
+ const log = (line) => writeLine(options.io.stderr, line);
55
+ const stateDir = getCollectorRuntimePaths(options.homeDir).state_dir;
56
+ const key = readCacheKey(options.label, options.path, options.requestBody);
57
+ const entryPath = readCacheEntryPath(stateDir, options.deviceToken, options.surface, key);
58
+ const fresh = options.io.env["COCKPIT_READ_FRESH"] === "1";
59
+ const allowStale = options.io.env["COCKPIT_READ_ALLOW_STALE"] === "1";
60
+ if (fresh) {
61
+ log(`[read-cache] bypassed ${JSON.stringify({ surface: options.surface, label: options.label, reason: "fresh_flag" })}`);
62
+ }
63
+ else {
64
+ const cached = readReadCacheEntry(entryPath);
65
+ if (cached) {
66
+ const ageMs = Date.now() - cached.storedAt;
67
+ const trusted = !options.json || ageMs < JSON_FRESH_BOUND_MS || allowStale;
68
+ if (trusted) {
69
+ log(`[read-cache] served ${JSON.stringify({ surface: options.surface, label: options.label, age_ms: ageMs, json: options.json })}`);
70
+ if (ageMs > MIN_REFRESH_INTERVAL_MS) {
71
+ spawnBackgroundRefresh(options.io, log, options.spawnImpl);
72
+ }
73
+ else {
74
+ log(`[read-cache] refresh skipped ${JSON.stringify({ reason: "too_recent", age_ms: ageMs })}`);
75
+ }
76
+ return { ok: true, httpStatus: cached.status, body: cached.body, fromCache: true, ageMs };
77
+ }
78
+ log(`[read-cache] too stale for --json ${JSON.stringify({ surface: options.surface, label: options.label, age_ms: ageMs })}`);
79
+ }
80
+ else {
81
+ log(`[read-cache] miss ${JSON.stringify({ surface: options.surface, label: options.label })}`);
82
+ }
83
+ }
84
+ const live = await options.ask();
85
+ if (live.ok) {
86
+ writeReadCacheEntryAtomic(entryPath, { status: live.httpStatus, body: live.body });
87
+ log(`[read-cache] refreshed ${JSON.stringify({ surface: options.surface, label: options.label })}`);
88
+ }
89
+ return { ...live, fromCache: false, ageMs: null };
90
+ }
91
+ /**
92
+ * Re-runs THIS process's own argv with `--fresh` appended, detached and
93
+ * discarded. Never awaited, never throws — a refresh that could not be
94
+ * spawned just means the next call finds the same cache age it would have
95
+ * found anyway; it is never this call's failure to report.
96
+ */
97
+ export function spawnBackgroundRefresh(io, log, spawnImpl = spawn) {
98
+ try {
99
+ const args = process.argv
100
+ .slice(2)
101
+ .filter((token) => token !== "--fresh" && token !== "--allow-stale");
102
+ args.push("--fresh");
103
+ const child = spawnImpl(process.execPath, [process.argv[1] ?? "", ...args], {
104
+ detached: true,
105
+ stdio: "ignore",
106
+ windowsHide: true,
107
+ env: io.env,
108
+ });
109
+ child.unref();
110
+ log(`[read-cache] background refresh spawned ${JSON.stringify({ argv_len: args.length })}`);
111
+ }
112
+ catch (error) {
113
+ log(`[read-cache] background refresh failed to spawn ${JSON.stringify({ reason: error instanceof Error ? error.name : "unknown" })}`);
114
+ }
115
+ }
@@ -20,6 +20,7 @@
20
20
  * being written to stdout, stderr, or an argument vector.
21
21
  */
22
22
  import { isInteractiveStdin, readLine, writeLine, yesByDefault } from "./cli-io.js";
23
+ import { cacheAgeLabel, readThroughCache } from "./read-cache.js";
23
24
  import { asList, asRecord, callTower, isForbidden, openTower, parseModelKey, readAllStdin, writeCommandFailure, } from "./tower-command.js";
24
25
  import { showOverview } from "./settings-overview.js";
25
26
  import { renderCliFloor, renderEnvBlobs, renderModelRouting, renderPersonal, renderSwitches, } from "./settings-render.js";
@@ -54,12 +55,26 @@ export async function runSettings(command, io) {
54
55
  }
55
56
  // ── personal ──────────────────────────────────────────────────────────
56
57
  async function showPersonal(command, tower, io) {
57
- const result = await callTower(tower, {
58
- path: "/api/settings/personal",
58
+ const result = await readThroughCache({
59
+ io,
60
+ homeDir: tower.homeDir,
61
+ deviceToken: tower.deviceToken,
62
+ surface: "settings",
59
63
  label: "settings personal",
64
+ path: "/api/settings/personal",
65
+ json: command.json,
66
+ ask: () => callTower(tower, {
67
+ path: "/api/settings/personal",
68
+ label: "settings personal",
69
+ // BLI-10746: route through the resident memory daemon's warm connection.
70
+ warmRoute: true,
71
+ }),
60
72
  });
61
73
  if (!result.ok)
62
74
  return writeCommandFailure(io, command.json, result);
75
+ if (result.fromCache && !command.json) {
76
+ writeLine(io.stderr, `(${cacheAgeLabel(result.ageMs ?? 0)}, refreshing in the background)`);
77
+ }
63
78
  return writeBody(io, command.json, result.body, () => renderPersonal(asRecord(result.body)));
64
79
  }
65
80
  async function setPersonal(command, tower, io) {
@@ -33,6 +33,8 @@ export async function openTower(commandName, command, io) {
33
33
  deviceToken: session.device_token,
34
34
  fetch: io.fetch,
35
35
  log: (line) => writeLine(io.stderr, line),
36
+ io,
37
+ homeDir: command.homeDir,
36
38
  };
37
39
  }
38
40
  export async function callTower(context, options) {
@@ -45,6 +47,8 @@ export async function callTower(context, options) {
45
47
  ...(options.body === undefined ? {} : { body: options.body }),
46
48
  timeoutMs: options.timeoutMs ?? REQUEST_DEADLINE_MS,
47
49
  label: options.label,
50
+ ...(options.warmRoute === undefined ? {} : { warmRoute: options.warmRoute }),
51
+ ...(context.homeDir === undefined ? {} : { homeDir: context.homeDir }),
48
52
  log: context.log,
49
53
  });
50
54
  if (result.ok)
@@ -23,9 +23,11 @@
23
23
  * `responseErrorMessage`) is reused from `upload-http.ts` rather than copied —
24
24
  * the upload path and the conversation path answer to the same rules.
25
25
  */
26
+ import { invalidateReadCacheSurface, readCacheSurfaceForPath, } from "@bli-cockpit/telemetry-core/node-read-cache";
26
27
  import { errorMessage } from "./commands/cli-io.js";
27
28
  import { getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "./local-state.js";
28
29
  import { normalizeDashboardUrl, readResponseJson, responseErrorMessage, } from "./upload-http.js";
30
+ import { isWarmRoutableRead, tryWarmTowerFetch } from "./warm-tower-fetch.js";
29
31
  const defaultLog = (line) => {
30
32
  console.error(line);
31
33
  };
@@ -77,6 +79,7 @@ export async function towerRequest(options) {
77
79
  const startedAt = Date.now();
78
80
  const url = `${normalizeDashboardUrl(options.dashboardUrl)}${options.path}`;
79
81
  const isForm = typeof FormData !== "undefined" && options.body instanceof FormData;
82
+ const method = options.method ?? "POST";
80
83
  const headers = {
81
84
  ...(options.headers ?? {}),
82
85
  authorization: `Bearer ${options.deviceToken}`,
@@ -85,16 +88,26 @@ export async function towerRequest(options) {
85
88
  // content-type by hand would strip it and the server would parse nothing.
86
89
  if (options.body !== undefined && !isForm)
87
90
  headers["content-type"] = "application/json";
91
+ const body = options.body === undefined || isForm ? undefined : JSON.stringify(options.body);
88
92
  const init = {
89
- method: options.method ?? "POST",
93
+ method,
90
94
  headers,
91
95
  ...(options.body === undefined
92
96
  ? {}
93
- : { body: (isForm ? options.body : JSON.stringify(options.body)) }),
97
+ : { body: (isForm ? options.body : body) }),
94
98
  ...(timeoutSignal(options.timeoutMs) ?? {}),
95
99
  };
96
100
  try {
97
- const response = await options.fetch(url, init);
101
+ // BLI-10746: a read (GET, or the one POST read this repo has —
102
+ // `memory search`) skips the cold TCP+TLS connect by asking the resident
103
+ // memory daemon to replay it on its own warm dispatcher. `isForm` never
104
+ // holds for a read this repo makes, but the check is here so a future
105
+ // read that DOES carry a form body never gets silently mis-proxied.
106
+ const warmEligible = options.warmRoute === true && !isForm && isWarmRoutableRead(method, options.label);
107
+ const warmResponse = warmEligible
108
+ ? await tryWarmTowerFetch(url, { method, headers, body }, log, { homeDir: options.homeDir })
109
+ : null;
110
+ const response = warmResponse ?? (await options.fetch(url, init));
98
111
  const respondedMs = Date.now() - startedAt;
99
112
  // The success branch always decided something (BLI-10450 kept the
100
113
  // "log the success branch too" rule intact) — it is just that under
@@ -111,6 +124,21 @@ export async function towerRequest(options) {
111
124
  elapsed_ms: respondedMs,
112
125
  })}`);
113
126
  }
127
+ // BLI-10746: "any write through the CLI or MCP invalidates the affected
128
+ // cache". A successful (2xx) write to a covered surface throws away
129
+ // every cached read for that surface — coarse, but a stale row is never
130
+ // possible this way, only one avoidable extra request.
131
+ if (method !== "GET" && response.ok) {
132
+ const surface = readCacheSurfaceForPath(options.path);
133
+ if (surface) {
134
+ const stateDir = getCollectorRuntimePaths(options.homeDir).state_dir;
135
+ const invalidated = invalidateReadCacheSurface(stateDir, options.deviceToken, surface);
136
+ log(`[tower client] read cache invalidated ${JSON.stringify({
137
+ surface,
138
+ ...(invalidated.ok ? { removed: invalidated.removed } : { reason: invalidated.reason }),
139
+ })}`);
140
+ }
141
+ }
114
142
  return { ok: true, response, startedAt, respondedMs };
115
143
  }
116
144
  catch (error) {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A CLI READ, ROUTED THROUGH THE RESIDENT MEMORY DAEMON'S WARM CONNECTION
3
+ * (BLI-10746).
4
+ *
5
+ * Every `cockpit` process today opens its own TCP+TLS connection to Tower —
6
+ * measured from Vietnam, about 420 ms of the ~1,700 ms `work list` takes is
7
+ * exactly that setup, paid fresh on every single command. `@bli-cockpit/
8
+ * memory-mcp`'s resident daemon already holds one connection open across
9
+ * turns for recall (BLI-10741); this module is "reuse that for every other
10
+ * read too" — a GET (or the one POST read this repo has, `memory search`)
11
+ * asks the daemon to replay it on its own dispatcher instead of opening a new
12
+ * socket.
13
+ *
14
+ * **Fallback is the whole safety argument.** A daemon that is absent, wedged,
15
+ * refuses, times out, or answers something this cannot parse means exactly
16
+ * one thing here: go the way this command always went. `isWarmRoutableRead`
17
+ * decides which requests are even eligible — writes NEVER take this path, so
18
+ * there is no daemon-retry-of-a-write hazard to reason about.
19
+ *
20
+ * **Auth never changes hands.** The caller's own `authorization` header
21
+ * travels inside `init.headers` exactly as `towerRequest` already built it;
22
+ * the daemon forwards it unread (see `@bli-cockpit/memory-mcp`'s
23
+ * `daemon/http-proxy.ts`). This module never reads the device token either.
24
+ */
25
+ import os from "node:os";
26
+ import { memoryDaemonEndpoint } from "@bli-cockpit/telemetry-core";
27
+ import { askMemoryDaemonHttp, } from "@bli-cockpit/telemetry-core/node-daemon-http-client";
28
+ /**
29
+ * The only POST this repo treats as a read for warm-routing purposes
30
+ * (`cockpit memory search`, `POST /api/memory/search`). Every other read is a
31
+ * GET; a caller names itself with the same `label` it already logs under.
32
+ */
33
+ const WARM_ROUTABLE_POST_LABELS = new Set(["memory search"]);
34
+ /** GET is always eligible; a small named allowlist covers the one read that is not. */
35
+ export function isWarmRoutableRead(method, label) {
36
+ return method === "GET" || WARM_ROUTABLE_POST_LABELS.has(label);
37
+ }
38
+ /** The whole local-socket-plus-daemon's-own-fetch budget. Comfortably under any `READ_DEADLINE_MS` a command sets. */
39
+ export const WARM_FETCH_DEADLINE_MS = 10_000;
40
+ /**
41
+ * Tries the daemon; returns `null` on ANY reason it could not, so the caller
42
+ * falls back to a direct request. Never throws.
43
+ */
44
+ export async function tryWarmTowerFetch(url, init, log, deps = {}) {
45
+ const endpoint = memoryDaemonEndpoint({
46
+ homeDir: deps.homeDir ?? os.homedir(),
47
+ ...(deps.platform ? { platform: deps.platform } : {}),
48
+ username: deps.username ?? safeUsername(),
49
+ });
50
+ if (endpoint.kind === "unix" && endpoint.addressTooLong) {
51
+ log(`[tower client] warm route skipped ${JSON.stringify({ reason: "socket_path_too_long" })}`);
52
+ return null;
53
+ }
54
+ const result = await askMemoryDaemonHttp({
55
+ address: endpoint.address,
56
+ deadlineMs: deps.deadlineMs ?? WARM_FETCH_DEADLINE_MS,
57
+ ...(deps.connectImpl ? { connectImpl: deps.connectImpl } : {}),
58
+ ...(deps.now ? { now: deps.now } : {}),
59
+ request: {
60
+ method: init.method,
61
+ url,
62
+ headers: init.headers,
63
+ ...(init.body === undefined ? {} : { body: init.body }),
64
+ },
65
+ });
66
+ if (!result.ok) {
67
+ log(`[tower client] warm route missed ${JSON.stringify({ reason: result.reason, elapsed_ms: result.elapsedMs })}`);
68
+ return null;
69
+ }
70
+ log(`[tower client] warm route used ${JSON.stringify({ elapsed_ms: result.elapsedMs })}`);
71
+ return new Response(result.answer.body, {
72
+ status: result.answer.status,
73
+ headers: result.answer.headers,
74
+ });
75
+ }
76
+ function safeUsername() {
77
+ try {
78
+ return os.userInfo().username;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.149",
3
+ "version": "0.2.150",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.38",
31
- "@bli-cockpit/mcp": "0.1.72",
32
- "@bli-cockpit/telemetry-core": "0.1.50"
30
+ "@bli-cockpit/memory-mcp": "0.1.39",
31
+ "@bli-cockpit/mcp": "0.1.73",
32
+ "@bli-cockpit/telemetry-core": "0.1.51"
33
33
  }
34
34
  }