@bli-cockpit/cli 0.2.273 → 0.2.275

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.
@@ -1,4 +1,4 @@
1
- import { setupReceiptPieces } from "@bli-cockpit/telemetry-core";
1
+ import { CODEX_HOOKS_TRUST_GUIDE_URL, setupReceiptPieces } from "@bli-cockpit/telemetry-core";
2
2
  import { redactedHealthDetail } from "../health-detail.js";
3
3
  import { setupReceiptBlock } from "./setup-receipt-lines.js";
4
4
  export function ok(id, code, message) {
@@ -193,7 +193,7 @@ export function doctorNeedsPerson(rows, receipt) {
193
193
  const step = key === "device" || key === "browser" ? "authed" : key === "collector.autostart" ? "autostart-alive" : "memory-registered";
194
194
  if (piece.status !== "needs_trust" && items.some((item) => item.step === step))
195
195
  continue;
196
- items.push({ step: key, reason: piece.reason ?? piece.status, action: piece.status === "needs_trust" ? "Open Codex and run /hooks to trust the installed hooks." : defaults[step] });
196
+ items.push({ step: key, reason: piece.reason ?? piece.status, action: piece.status === "needs_trust" ? `Open Codex and run /hooks to trust the installed hooks. Step by step: ${CODEX_HOOKS_TRUST_GUIDE_URL}` : defaults[step] });
197
197
  }
198
198
  }
199
199
  return items;
@@ -1,78 +1,11 @@
1
- import { runOpsCommand } from "./local-run-ops.js";
2
- import { runGlossary } from "./glossary.js";
3
- import { runSpend } from "./spend.js";
4
- import { runSessionsForget } from "./sessions-forget.js";
5
- import { runPush } from "./push.js";
6
1
  import { defaultIo, writeLine } from "./cli-io.js";
7
2
  import { isLocalHelpRequest, localCommandHelp } from "./local-help.js";
8
3
  import { beginStage, endStage } from "../crash-guard.js";
9
4
  import { reportCommandFailure, refuseArguments, stripGlobalToggles, watchWrites } from "./local-run-guards.js";
10
- import { reportInstallEventsBestEffort } from "./install-receipts.js";
11
- import { persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
12
5
  import { rememberDiscoveryLimits } from "./local-discovery.js";
13
- import { runInstall, runRelease, runSelfUpdate, runUpdate } from "./install-update.js";
14
- import { runOnboard } from "./onboard.js";
15
- import { runLogin } from "./login.js";
16
- import { runLogout } from "./logout.js";
17
- import { runStart } from "./start.js";
18
- import { runSync } from "./sync.js";
19
- import { runDoctorAfterSync } from "./sync-doctor.js";
20
- import { runAnalyze } from "./analyze.js";
21
- import { runBackfillCommand } from "./backfill.js";
22
- import { runDoctor } from "./doctor.js";
23
- import { runStatus } from "./status.js";
24
- import { runSessions } from "./sessions.js";
25
- import { runJarvis } from "./jarvis.js";
26
- import { runModels } from "./models.js";
27
- import { runSlack } from "./slack.js";
28
- import { runModel, runSettings } from "./settings.js";
29
- import { runTeam } from "./team.js";
30
- import { runWorkbook } from "./workbook.js";
31
- import { runBrief } from "./brief.js";
32
- import { runBriefEdit } from "./brief-edit.js";
33
- import { runBriefRewrite } from "./brief-rewrite.js";
34
- import { runBriefDone } from "./brief-done.js";
35
- import { runCorrect } from "./correct.js";
36
- import { runNotes } from "./notes.js";
37
- import { runServe } from "./serve.js";
38
- import { runAutostart } from "./autostart-command.js";
39
- import { runAgentRules } from "./agent-rules-command.js";
40
- import { runMemoryLog } from "./memory-log.js";
41
- import { runMemoryInstall } from "./memory-install.js";
42
- import { runMemoryStore, runMemoryVerbList } from "./memory-store.js";
43
- import { runClean } from "./clean.js";
44
- import { runSourcesCheck } from "./sources-check.js";
45
- import { runDocs } from "./docs.js";
46
- import { runMsg } from "./msg.js";
47
- import { runIssue } from "./issue.js";
48
- import { runCal } from "./cal.js";
49
- import { runMail } from "./mail.js";
50
- import { runProject } from "./project.js";
51
- import { runMilestone } from "./milestone.js";
52
- import { runGithub } from "./github.js";
53
- import { runSearch } from "./search.js";
54
- import { runCareers } from "./careers.js";
55
- import { runRename } from "./rename-scope.js";
56
- import { runSetup } from "./setup.js";
57
- import { runUsage } from "./usage.js";
58
- import { runSpeed } from "./speed.js";
59
- import { runLogs } from "./logs.js";
60
- import { runApprovals, runDrills } from "./approvals.js";
61
- import { runManager } from "./manager.js";
62
- import { runImage } from "./image.js";
63
- import { runBrain } from "./brain.js";
64
- import { runHits } from "./hits.js";
65
- import { runIntentions } from "./intentions.js";
66
- import { runWho } from "./who.js";
67
- import { runPeople } from "./people.js";
68
6
  import { parseLocalArgs } from "./local-args.js";
69
- // `./local.js` is the published entry point (the public CLI root, root.ts, doctor.ts, tests): a split must not move a name off it.
7
+ // `./local.js` is the published entry point (the public CLI root, root.ts): it keeps the help registry names and loads each command module only when that command runs (BLI-23349).
70
8
  export { rootCommandNames, localCommandHelp, SURFACE_ALIASES, unknownCommandJson, unknownCommandLine, } from "./local-help.js";
71
- export { buildCollectorHeartbeat, collectionRootLabel, collectionRootLabels, sendCollectorHeartbeatBestEffort, } from "./heartbeat.js";
72
- export { classifySyncFailureRecords, classifySyncHealthError, redactedSyncErrorDetail, reportInstallEventsBestEffort, SYNC_ERROR_DETAIL_MAX_CHARS, } from "./install-receipts.js";
73
- export { assertCollectionRootPersisted } from "./collection-roots.js";
74
- export { runSelfUpdate, SelfUpdateError, } from "./install-update.js";
75
- import { runRoots } from "./roots.js";
76
9
  /** Commands that act on THIS machine; the rest talk to Tower. */
77
10
  const MACHINE_COMMAND_KINDS = [
78
11
  "roots",
@@ -101,7 +34,9 @@ const MACHINE_COMMAND_KINDS = [
101
34
  function isMachineCommand(command) {
102
35
  return MACHINE_COMMAND_KINDS.includes(command.kind);
103
36
  }
104
- export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorAfterSync }) {
37
+ /** `sync` is the only caller of this hook; the module loads when it is first called, not at startup. */
38
+ const lazyRunDoctorAfterSync = async (...args) => (await import("./sync-doctor.js")).runDoctorAfterSync(...args);
39
+ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorAfterSync: lazyRunDoctorAfterSync }) {
105
40
  argv = stripGlobalToggles(argv);
106
41
  if (isLocalHelpRequest(argv)) {
107
42
  writeLine(io.stdout, localCommandHelp(argv[0]));
@@ -131,7 +66,7 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
131
66
  try {
132
67
  // Not awaited, as it never was: a `cal` rejection bypasses the net below.
133
68
  if (command.kind === "cal")
134
- return runCal(command, io);
69
+ return (await import("./cal.js")).runCal(command, io);
135
70
  if (isMachineCommand(command))
136
71
  return await runMachineCommand(command, io);
137
72
  return await runTowerCommand(command, io);
@@ -145,86 +80,86 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
145
80
  }
146
81
  async function runMachineCommand(command, io) {
147
82
  switch (command.kind) {
148
- case "roots": return await runRoots(command, io);
149
- case "install": return await runInstall(command, io);
150
- case "onboard": return await runOnboard(command, io);
151
- case "update": return await runUpdate(command, io);
83
+ case "roots": return await (await import("./roots.js")).runRoots(command, io);
84
+ case "install": return await (await import("./install-update.js")).runInstall(command, io);
85
+ case "onboard": return await (await import("./onboard.js")).runOnboard(command, io);
86
+ case "update": return await (await import("./install-update.js")).runUpdate(command, io);
152
87
  case "doctor":
153
- return await runDoctor(command, io, {
154
- reportInstallEvents: reportInstallEventsBestEffort,
155
- selfUpdate: runSelfUpdate,
88
+ return await (await import("./doctor.js")).runDoctor(command, io, {
89
+ reportInstallEvents: (await import("./install-receipts.js")).reportInstallEventsBestEffort,
90
+ selfUpdate: (await import("./install-update.js")).runSelfUpdate,
156
91
  runLogin: runDoctorLogin,
157
92
  resolveAndSaveRoots: resolveAndSaveDoctorRoots,
158
93
  });
159
- case "login": return await runLogin(command, io);
160
- case "logout": return await runLogout(command, io);
161
- case "start": return await runStart(command, io);
162
- case "sync": return await runSync(command, io);
163
- case "analyze": return await runAnalyze(command, io);
164
- case "backfill": return await runBackfillCommand(command, io);
165
- case "status": return await runStatus(command, io);
166
- case "sessions": return await runSessions(command, io);
167
- case "serve": return await runServe(command, io);
168
- case "autostart": return await runAutostart(command, io);
169
- case "agent-rules": return await runAgentRules(command, io);
170
- case "memory-log": return await runMemoryLog(command, io);
94
+ case "login": return await (await import("./login.js")).runLogin(command, io);
95
+ case "logout": return await (await import("./logout.js")).runLogout(command, io);
96
+ case "start": return await (await import("./start.js")).runStart(command, io);
97
+ case "sync": return await (await import("./sync.js")).runSync(command, io);
98
+ case "analyze": return await (await import("./analyze.js")).runAnalyze(command, io);
99
+ case "backfill": return await (await import("./backfill.js")).runBackfillCommand(command, io);
100
+ case "status": return await (await import("./status.js")).runStatus(command, io);
101
+ case "sessions": return await (await import("./sessions.js")).runSessions(command, io);
102
+ case "serve": return await (await import("./serve.js")).runServe(command, io);
103
+ case "autostart": return await (await import("./autostart-command.js")).runAutostart(command, io);
104
+ case "agent-rules": return await (await import("./agent-rules-command.js")).runAgentRules(command, io);
105
+ case "memory-log": return await (await import("./memory-log.js")).runMemoryLog(command, io);
171
106
  case "memory":
172
107
  // BLI-4047: a bare `cockpit memory` says what the noun does. It used
173
108
  // to install; `install` is still one word away and every unattended
174
109
  // caller asks for it by name in code.
175
110
  if (command.action === "help")
176
- return runMemoryVerbList(io, command.json);
177
- return await runMemoryInstall(command, io);
178
- case "memory-store": return await runMemoryStore(command, io);
179
- case "clean": return await runClean(command, io);
180
- case "sources": return await runSourcesCheck(command, io);
181
- case "release": return await runRelease(command, io);
111
+ return (await import("./memory-store.js")).runMemoryVerbList(io, command.json);
112
+ return await (await import("./memory-install.js")).runMemoryInstall(command, io);
113
+ case "memory-store": return await (await import("./memory-store.js")).runMemoryStore(command, io);
114
+ case "clean": return await (await import("./clean.js")).runClean(command, io);
115
+ case "sources": return await (await import("./sources-check.js")).runSourcesCheck(command, io);
116
+ case "release": return await (await import("./install-update.js")).runRelease(command, io);
182
117
  }
183
118
  }
184
119
  async function runTowerCommand(command, io) {
185
120
  switch (command.kind) {
186
- case "jarvis": return await runJarvis(command, io);
187
- case "model": return await runModel(command, io);
188
- case "models": return await runModels(command, io);
121
+ case "jarvis": return await (await import("./jarvis.js")).runJarvis(command, io);
122
+ case "model": return await (await import("./settings.js")).runModel(command, io);
123
+ case "models": return await (await import("./models.js")).runModels(command, io);
189
124
  case "scout":
190
125
  case "ci":
191
126
  case "delivery":
192
127
  case "ops":
193
- return await runOpsCommand(command, io);
194
- case "slack": return await runSlack(command, io);
195
- case "settings": return await runSettings(command, io);
196
- case "team": return await runTeam(command, io);
197
- case "workbook": return await runWorkbook(command, io);
128
+ return await (await import("./local-run-ops.js")).runOpsCommand(command, io);
129
+ case "slack": return await (await import("./slack.js")).runSlack(command, io);
130
+ case "settings": return await (await import("./settings.js")).runSettings(command, io);
131
+ case "team": return await (await import("./team.js")).runTeam(command, io);
132
+ case "workbook": return await (await import("./workbook.js")).runWorkbook(command, io);
198
133
  case "brief": return await runBriefVerb(command, io);
199
- case "correct": return await runCorrect(command, io);
200
- case "notes": return await runNotes(command, io);
201
- case "glossary": return await runGlossary(command, io);
202
- case "sessions-forget": return await runSessionsForget(command, io);
203
- case "spend": return await runSpend(command, io);
204
- case "push": return await runPush(command, io);
205
- case "docs": return await runDocs(command, io);
206
- case "msg": return await runMsg(command, io);
207
- case "issue": return await runIssue(command, io);
208
- case "mail": return await runMail(command, io);
209
- case "project": return await runProject(command, io);
210
- case "milestone": return await runMilestone(command, io);
211
- case "search": return await runSearch(command, io);
212
- case "github": return await runGithub(command, io);
213
- case "careers": return await runCareers(command, io);
214
- case "rename": return await runRename(command, io);
215
- case "setup": return await runSetup(command, io);
216
- case "usage": return await runUsage(command, io);
217
- case "speed": return await runSpeed(command, io);
218
- case "logs": return await runLogs(command, io);
219
- case "brain": return await runBrain(command, io);
220
- case "hits": return await runHits(command, io);
221
- case "intentions": return await runIntentions(command, io);
222
- case "who": return await runWho(command, io);
223
- case "people": return await runPeople(command, io);
224
- case "approvals": return await runApprovals(command, io);
225
- case "drills": return await runDrills(command, io);
226
- case "manager": return await runManager(command, io);
227
- case "image": return await runImage(command, io);
134
+ case "correct": return await (await import("./correct.js")).runCorrect(command, io);
135
+ case "notes": return await (await import("./notes.js")).runNotes(command, io);
136
+ case "glossary": return await (await import("./glossary.js")).runGlossary(command, io);
137
+ case "sessions-forget": return await (await import("./sessions-forget.js")).runSessionsForget(command, io);
138
+ case "spend": return await (await import("./spend.js")).runSpend(command, io);
139
+ case "push": return await (await import("./push.js")).runPush(command, io);
140
+ case "docs": return await (await import("./docs.js")).runDocs(command, io);
141
+ case "msg": return await (await import("./msg.js")).runMsg(command, io);
142
+ case "issue": return await (await import("./issue.js")).runIssue(command, io);
143
+ case "mail": return await (await import("./mail.js")).runMail(command, io);
144
+ case "project": return await (await import("./project.js")).runProject(command, io);
145
+ case "milestone": return await (await import("./milestone.js")).runMilestone(command, io);
146
+ case "search": return await (await import("./search.js")).runSearch(command, io);
147
+ case "github": return await (await import("./github.js")).runGithub(command, io);
148
+ case "careers": return await (await import("./careers.js")).runCareers(command, io);
149
+ case "rename": return await (await import("./rename-scope.js")).runRename(command, io);
150
+ case "setup": return await (await import("./setup.js")).runSetup(command, io);
151
+ case "usage": return await (await import("./usage.js")).runUsage(command, io);
152
+ case "speed": return await (await import("./speed.js")).runSpeed(command, io);
153
+ case "logs": return await (await import("./logs.js")).runLogs(command, io);
154
+ case "brain": return await (await import("./brain.js")).runBrain(command, io);
155
+ case "hits": return await (await import("./hits.js")).runHits(command, io);
156
+ case "intentions": return await (await import("./intentions.js")).runIntentions(command, io);
157
+ case "who": return await (await import("./who.js")).runWho(command, io);
158
+ case "people": return await (await import("./people.js")).runPeople(command, io);
159
+ case "approvals": return await (await import("./approvals.js")).runApprovals(command, io);
160
+ case "drills": return await (await import("./approvals.js")).runDrills(command, io);
161
+ case "manager": return await (await import("./manager.js")).runManager(command, io);
162
+ case "image": return await (await import("./image.js")).runImage(command, io);
228
163
  }
229
164
  }
230
165
  /**
@@ -235,17 +170,17 @@ async function runTowerCommand(command, io) {
235
170
  */
236
171
  async function runBriefVerb(command, io) {
237
172
  if (command.action === "edit")
238
- return await runBriefEdit(command, io);
173
+ return await (await import("./brief-edit.js")).runBriefEdit(command, io);
239
174
  if (command.action === "rewrite")
240
- return await runBriefRewrite(command, io);
175
+ return await (await import("./brief-rewrite.js")).runBriefRewrite(command, io);
241
176
  // BLI-3605: the board's done checkbox, at a terminal.
242
177
  if (command.action === "done" || command.action === "undone") {
243
- return await runBriefDone(command, io);
178
+ return await (await import("./brief-done.js")).runBriefDone(command, io);
244
179
  }
245
- return await runBrief(command, io);
180
+ return await (await import("./brief.js")).runBrief(command, io);
246
181
  }
247
182
  async function runDoctorLogin(command, io) {
248
- return runLogin({
183
+ return (await import("./login.js")).runLogin({
249
184
  kind: "login",
250
185
  homeDir: command.homeDir,
251
186
  dashboardUrl: command.dashboardUrl,
@@ -265,8 +200,9 @@ async function resolveAndSaveDoctorRoots(command, io) {
265
200
  // headless equivalent for scripted and scheduled runs.
266
201
  allowHomeRoot: command.allowHomeRoot,
267
202
  };
268
- const resolution = await resolveOnboardingRootsForCommand(rootCommand, io);
269
- await persistOnboardingRootConfig(rootCommand, resolution);
203
+ const roots = await import("./collection-roots.js");
204
+ const resolution = await roots.resolveOnboardingRootsForCommand(rootCommand, io);
205
+ await roots.persistOnboardingRootConfig(rootCommand, resolution);
270
206
  if (!command.json) {
271
207
  writeLine(io.stdout, `Saved collection roots: ${resolution.collectionRoots.join(", ")}`);
272
208
  }
@@ -3,7 +3,7 @@
3
3
  * one outcome, and rendering that outcome for a log line or a terminal.
4
4
  * Sibling of `memory-install.ts`, named in its header.
5
5
  */
6
- import { memoryInstallGaps } from "@bli-cockpit/telemetry-core";
6
+ import { CODEX_HOOKS_TRUST_GUIDE_URL, memoryInstallGaps } from "@bli-cockpit/telemetry-core";
7
7
  import { memoryDaemonLine } from "./memory-daemon-probe.js";
8
8
  import { memoryReceiptLine } from "./memory-install-receipt.js";
9
9
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
@@ -93,7 +93,8 @@ export function memoryOutcomeLines(outcome) {
93
93
  // here in words rather than left as a status word they have to look up.
94
94
  if (outcome.receipt.codex.hooks === "needs_trust") {
95
95
  lines.push(" Codex will NOT run these hooks until you trust them: open Codex in any folder, " +
96
- "run /hooks, and trust the three bli-memory entries. One time, per machine.");
96
+ "run /hooks, and trust every bli-memory entry. One time, per machine. " +
97
+ `Step by step, with pictures: ${CODEX_HOOKS_TRUST_GUIDE_URL}`);
97
98
  }
98
99
  if (outcome.receipt.codex.hooks === "unsupported") {
99
100
  lines.push(" Codex hooks are turned off in your ~/.codex/config.toml ([features] hooks = false), " +
@@ -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.273");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.275");
25
25
  return 0;
26
26
  }
27
27
 
@@ -9,7 +9,7 @@
9
9
  * refusal that does not say what to do next is half a message. Every gap here
10
10
  * gets ONE command or ONE click, never a paragraph and never "see the docs".
11
11
  */
12
- import { setupPieceMark, setupReceiptLine, setupReceiptPieces, } from "@bli-cockpit/telemetry-core";
12
+ import { CODEX_HOOKS_TRUST_GUIDE_URL, setupPieceMark, setupReceiptLine, setupReceiptPieces, } from "@bli-cockpit/telemetry-core";
13
13
  /** Cached host facts are old after the same seven-day window as their readers. */
14
14
  const OLD_READING_MS = 7 * 24 * 60 * 60 * 1000;
15
15
  /** The one fix for each piece, by key. Present for every key the block prints. */
@@ -45,7 +45,7 @@ const UNSUPPORTED_FIX = "Switched off in your own config; nothing to do.";
45
45
  * and waiting to be trusted are finished by a PERSON, in the Codex UI, and no
46
46
  * command we could print would do it.
47
47
  */
48
- const NEEDS_TRUST_FIX = "Open Codex and press `/hooks` to trust them (once, ~15s).";
48
+ const NEEDS_TRUST_FIX = `Open Codex and press \`/hooks\` to trust them (once, ~15s). Step by step: ${CODEX_HOOKS_TRUST_GUIDE_URL}`;
49
49
  /**
50
50
  * The block a terminal prints: the one line, then one fix per gap, then
51
51
  * nothing. A fully connected machine gets one line and no advice — a receipt
@@ -1,185 +1,2 @@
1
- /**
2
- * Find the embedded images of one transcript line without ever building the base64 as a JS string
3
- * (BLI-23317, stage D).
4
- *
5
- * `findEmbeddedImages` (telemetry-core/transcript-images.ts) is the RULE and stays the test oracle: it
6
- * feeds `evidenceRulesFingerprint`, so its regex and its results do not change. This module is the same
7
- * rule run on the line's BYTES, which is what the streamed prepare path calls:
8
- *
9
- * - a whole-string payload: a base64 run that starts right after a `"`, starts with one of the four
10
- * image starts (`iVBOR`, `/9j/`, `R0lGO`, `UklGR`), is at least 495 characters past that start
11
- * (so 499 for a JPEG, 500 for the others, exactly as the regex counts) and is closed by `"` or `\"`;
12
- * - a data URL: `data:image/<[a-z0-9.+-]+>;base64,` and then a run of at least 500 characters, no closing
13
- * quote needed. The whole URL is the replaced range.
14
- *
15
- * A match is accepted only when the decoded bytes sniff as PNG, JPEG, GIF or WebP; either way the scan
16
- * resumes after the run, as `matchAll` does. All matched characters are ASCII, so a byte offset here is
17
- * the character offset there on ASCII text, and the replaced ranges are the same everywhere else.
18
- *
19
- * Decoding is incremental: the run is decoded in blocks of 48 Ki characters (a multiple of 4, so each
20
- * block decodes on its own) straight into a sha256, with the first block sniffed. At no point does a
21
- * string of the run, or a Buffer of the whole decoded image, exist. The one exception is a run whose
22
- * `=` characters are not plain trailing padding: Node's decoder rules for those are version specific,
23
- * so such a run takes the whole-run decode the oracle takes (never seen in real transcripts).
24
- *
25
- * `byteSize` is the length Node's `Buffer.from(run, "base64")` gives, because every block, including a
26
- * short last one, is decoded by that very function.
27
- */
28
- import { createHash } from "node:crypto";
29
- import { IMAGE_MIN_BASE64_CHARS, sniffImageMediaType, } from "@bli-cockpit/telemetry-core";
30
- const QUOTE = 0x22;
31
- const BACKSLASH = 0x5c;
32
- const EQUALS = 0x3d;
33
- /** Characters per decoded block. A multiple of 4: each block is a whole number of base64 quanta. */
34
- const BLOCK_CHARS = 48 * 1024;
35
- const BASE64 = new Uint8Array(256);
36
- for (const range of ["AZ", "az", "09"]) {
37
- for (let code = range.charCodeAt(0); code <= range.charCodeAt(1); code += 1)
38
- BASE64[code] = 1;
39
- }
40
- for (const char of "+/=")
41
- BASE64[char.charCodeAt(0)] = 1;
42
- const DATA_URL_LOWER = new Uint8Array(256);
43
- for (const range of ["az", "09"]) {
44
- for (let code = range.charCodeAt(0); code <= range.charCodeAt(1); code += 1)
45
- DATA_URL_LOWER[code] = 1;
46
- }
47
- for (const char of ".+-")
48
- DATA_URL_LOWER[char.charCodeAt(0)] = 1;
49
- /** The four image starts of a whole-string payload, and the data URL anchor. */
50
- const STARTS = ["iVBOR", "/9j/", "R0lGO", "UklGR"].map((text) => Buffer.from(text, "latin1"));
51
- const DATA_URL = Buffer.from("data:image/", "latin1");
52
- const BASE64_TAG = Buffer.from(";base64,", "latin1");
53
- /** Is there any chance this line holds an image? Same anchors as the string check, on bytes. */
54
- export function bytesMayHoldImage(line) {
55
- return line.includes(DATA_URL) || STARTS.some((start) => line.includes(start));
56
- }
57
- /** The end (exclusive) of the base64-alphabet run that starts at `from`. */
58
- function runEnd(line, from) {
59
- let at = from;
60
- while (at < line.length && BASE64[line[at]] === 1)
61
- at += 1;
62
- return at;
63
- }
64
- /**
65
- * sha256, size and type of the image `line[from, to)` encodes, or null when it is not a PNG, JPEG, GIF
66
- * or WebP. Blocks of the run are decoded one at a time.
67
- */
68
- export function digestBase64Run(line, from, to) {
69
- const length = to - from;
70
- const inRun = line.subarray(from, to).indexOf(EQUALS);
71
- const firstPad = inRun === -1 ? -1 : from + inRun;
72
- const padCount = firstPad === -1 ? 0 : to - firstPad;
73
- const plainPadding = padCount === 0
74
- || (padCount <= 2 && length % 4 === 0 && onlyPaddingFrom(line, firstPad, to));
75
- if (!plainPadding) {
76
- const bytes = Buffer.from(line.toString("latin1", from, to), "base64");
77
- const mediaType = sniffImageMediaType(bytes);
78
- return mediaType ? { sha256: createHash("sha256").update(bytes).digest("hex"), mediaType, byteSize: bytes.byteLength } : null;
79
- }
80
- const hash = createHash("sha256");
81
- let byteSize = 0;
82
- let mediaType = null;
83
- for (let at = from; at < to; at += BLOCK_CHARS) {
84
- const block = Buffer.from(line.toString("latin1", at, Math.min(to, at + BLOCK_CHARS)), "base64");
85
- if (at === from) {
86
- mediaType = sniffImageMediaType(block);
87
- if (!mediaType)
88
- return null;
89
- }
90
- hash.update(block);
91
- byteSize += block.byteLength;
92
- }
93
- return mediaType ? { sha256: hash.digest("hex"), mediaType, byteSize } : null;
94
- }
95
- /** True when everything from `firstPad` to `to` is `=` (padding at the very end of the run, nothing after it). */
96
- function onlyPaddingFrom(line, firstPad, to) {
97
- for (let at = firstPad; at < to; at += 1)
98
- if (line[at] !== EQUALS)
99
- return false;
100
- return true;
101
- }
102
- /** The sha256 of an image's decoded bytes, or null when `line[from, to)` does not decode to an image (used to re-check a payload before it is sent). */
103
- export function sha256OfBase64Run(line, from, to) {
104
- return digestBase64Run(line, from, to)?.sha256 ?? null;
105
- }
106
- /**
107
- * The next index at or after `from` where `anchor` sits right after a `"`, or -1. `current` is the answer
108
- * of the last call: -2 not looked for yet, -1 none left, else still good while it is at or after `from`.
109
- */
110
- function nextQuotedStart(line, anchor, from, current) {
111
- if (current === -1 || current >= from)
112
- return current;
113
- let at = line.indexOf(anchor, from);
114
- while (at !== -1 && !(at > 0 && line[at - 1] === QUOTE))
115
- at = line.indexOf(anchor, at + 1);
116
- return at;
117
- }
118
- /**
119
- * Every embedded image in `line` (one physical line, no `\n` inside), in order, with byte offsets inside
120
- * the line. Equal to `findEmbeddedImages` on the decoded text in everything but the unit of the offsets.
121
- */
122
- export function scanImagesInLine(line) {
123
- const found = [];
124
- if (line.length < IMAGE_MIN_BASE64_CHARS - 1)
125
- return found;
126
- // For each whole-string start, the next quoted occurrence (-2: not looked for yet, -1: none left).
127
- const nextStart = STARTS.map(() => -2);
128
- let nextData = -2;
129
- let position = 0;
130
- while (position < line.length) {
131
- let candidate = -1;
132
- let kind = -1;
133
- for (let index = 0; index < STARTS.length; index += 1) {
134
- nextStart[index] = nextQuotedStart(line, STARTS[index], position, nextStart[index]);
135
- const at = nextStart[index];
136
- if (at !== -1 && (candidate === -1 || at < candidate)) {
137
- candidate = at;
138
- kind = index;
139
- }
140
- }
141
- if (nextData !== -1) {
142
- nextData = nextData >= position ? nextData : line.indexOf(DATA_URL, position);
143
- if (nextData !== -1 && (candidate === -1 || nextData < candidate)) {
144
- candidate = nextData;
145
- kind = STARTS.length;
146
- }
147
- }
148
- if (candidate === -1)
149
- break;
150
- if (kind < STARTS.length) {
151
- const startLength = STARTS[kind].length;
152
- const end = runEnd(line, candidate);
153
- const closed = end < line.length && (line[end] === QUOTE || (line[end] === BACKSLASH && line[end + 1] === QUOTE));
154
- if (!closed || end - candidate < startLength + (IMAGE_MIN_BASE64_CHARS - 5)) {
155
- position = candidate + 1;
156
- continue;
157
- }
158
- position = end;
159
- const digest = digestBase64Run(line, candidate, end);
160
- if (digest)
161
- found.push({ replaceStart: candidate, replaceEnd: end, base64Start: candidate, base64End: end, ...digest });
162
- continue;
163
- }
164
- // data:image/<type>;base64,<run of at least 500>
165
- let cursor = candidate + DATA_URL.length;
166
- const typeStart = cursor;
167
- while (cursor < line.length && DATA_URL_LOWER[line[cursor]] === 1)
168
- cursor += 1;
169
- if (cursor === typeStart || line.compare(BASE64_TAG, 0, BASE64_TAG.length, cursor, cursor + BASE64_TAG.length) !== 0) {
170
- position = candidate + 1;
171
- continue;
172
- }
173
- const base64Start = cursor + BASE64_TAG.length;
174
- const end = runEnd(line, base64Start);
175
- if (end - base64Start < IMAGE_MIN_BASE64_CHARS) {
176
- position = candidate + 1;
177
- continue;
178
- }
179
- position = end;
180
- const digest = digestBase64Run(line, base64Start, end);
181
- if (digest)
182
- found.push({ replaceStart: candidate, replaceEnd: end, base64Start, base64End: end, ...digest });
183
- }
184
- return found;
185
- }
1
+ /** The byte scanner lives in telemetry-core (BLI-23349) so the memory hook shares it; this keeps the collector's import path. */
2
+ export { bytesMayHoldImage, digestBase64Run, scanImagesInLine, sha256OfBase64Run, } from "@bli-cockpit/telemetry-core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.273",
3
+ "version": "0.2.275",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,9 +27,9 @@
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-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.82",
31
- "@bli-cockpit/mcp": "0.1.166",
32
- "@bli-cockpit/telemetry-core": "0.1.89",
30
+ "@bli-cockpit/memory-mcp": "0.1.84",
31
+ "@bli-cockpit/mcp": "0.1.168",
32
+ "@bli-cockpit/telemetry-core": "0.1.91",
33
33
  "unpdf": "1.8.1"
34
34
  },
35
35
  "repository": {