@bli-cockpit/cli 0.2.48 → 0.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/adapters/raw-evidence-attribution-gaps.js +133 -0
  2. package/dist/adapters/raw-evidence-claude-reader.js +108 -0
  3. package/dist/adapters/raw-evidence-codex-reader.js +147 -0
  4. package/dist/adapters/raw-evidence-collection-state.js +199 -0
  5. package/dist/adapters/raw-evidence-facts.js +338 -0
  6. package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
  7. package/dist/adapters/raw-evidence-image-reader.js +107 -0
  8. package/dist/adapters/raw-evidence-sanitize.js +56 -0
  9. package/dist/adapters/raw-evidence-transcript-file.js +182 -0
  10. package/dist/adapters/raw-evidence.js +94 -1203
  11. package/dist/autostart-contract.js +79 -0
  12. package/dist/autostart-darwin-plist.js +265 -0
  13. package/dist/autostart-darwin.js +171 -0
  14. package/dist/autostart-windows-scripts.js +310 -0
  15. package/dist/autostart-windows-task-xml.js +260 -0
  16. package/dist/autostart-windows.js +237 -0
  17. package/dist/autostart-xml.js +23 -0
  18. package/dist/autostart.js +35 -1148
  19. package/dist/commands/agent-rules-command.js +55 -0
  20. package/dist/commands/agent-session-report.js +290 -0
  21. package/dist/commands/analyze.js +131 -0
  22. package/dist/commands/autostart-command.js +105 -0
  23. package/dist/commands/backfill-batches.js +34 -0
  24. package/dist/commands/backfill-candidates.js +54 -0
  25. package/dist/commands/backfill-checkpoint.js +101 -0
  26. package/dist/commands/backfill-command-line.js +70 -0
  27. package/dist/commands/backfill-evidence-outcomes.js +104 -0
  28. package/dist/commands/backfill-issues.js +265 -0
  29. package/dist/commands/backfill-output.js +75 -0
  30. package/dist/commands/backfill-plan.js +71 -0
  31. package/dist/commands/backfill-reasons.js +107 -0
  32. package/dist/commands/backfill-report.js +298 -0
  33. package/dist/commands/backfill-result.js +150 -0
  34. package/dist/commands/backfill-scan.js +274 -0
  35. package/dist/commands/backfill-scope.js +114 -0
  36. package/dist/commands/backfill-session-report.js +145 -0
  37. package/dist/commands/backfill-types.js +1 -0
  38. package/dist/commands/backfill-upload.js +212 -0
  39. package/dist/commands/backfill.js +58 -1705
  40. package/dist/commands/cli-io.js +13 -0
  41. package/dist/commands/doctor.js +57 -0
  42. package/dist/commands/jarvis-trace.js +184 -0
  43. package/dist/commands/jarvis.js +323 -7
  44. package/dist/commands/local-arg-values.js +169 -0
  45. package/dist/commands/local-args-collector.js +604 -0
  46. package/dist/commands/local-args-tower.js +891 -0
  47. package/dist/commands/local-args.js +10 -1549
  48. package/dist/commands/local-help.js +30 -5
  49. package/dist/commands/local.js +21 -1786
  50. package/dist/commands/login.js +53 -0
  51. package/dist/commands/logout.js +66 -0
  52. package/dist/commands/memory-install-claude.js +294 -0
  53. package/dist/commands/memory-install-codex.js +205 -0
  54. package/dist/commands/memory-install-contract.js +231 -0
  55. package/dist/commands/memory-install-files.js +63 -0
  56. package/dist/commands/memory-install-skills.js +121 -0
  57. package/dist/commands/memory-install-toml.js +265 -0
  58. package/dist/commands/memory-install.js +378 -0
  59. package/dist/commands/onboard-receipts.js +66 -0
  60. package/dist/commands/onboard-report.js +274 -0
  61. package/dist/commands/onboard.js +449 -0
  62. package/dist/commands/ops-render.js +36 -0
  63. package/dist/commands/public-root.js +1 -1
  64. package/dist/commands/serve.js +13 -0
  65. package/dist/commands/session-sync.js +513 -534
  66. package/dist/commands/settings-render.js +28 -0
  67. package/dist/commands/settings.js +66 -2
  68. package/dist/commands/start.js +47 -0
  69. package/dist/commands/sync-followups.js +308 -0
  70. package/dist/commands/sync.js +387 -0
  71. package/dist/local-state-attributed-target.js +75 -0
  72. package/dist/local-state-config.js +147 -0
  73. package/dist/local-state-files.js +59 -0
  74. package/dist/local-state-identity.js +73 -0
  75. package/dist/local-state-pairing.js +263 -0
  76. package/dist/local-state-paths.js +61 -0
  77. package/dist/local-state-session.js +68 -0
  78. package/dist/local-state-status.js +163 -0
  79. package/dist/local-state-work-context.js +190 -0
  80. package/dist/local-state.js +34 -848
  81. package/dist/tower-client.js +3 -2
  82. package/dist/tower-stream.js +76 -6
  83. package/package.json +2 -1
@@ -10,6 +10,19 @@ export function writeRaw(stream, text) {
10
10
  if (!text.endsWith("\n"))
11
11
  stream.write("\n");
12
12
  }
13
+ /**
14
+ * Part of a line, and nothing else (BLI-3517).
15
+ *
16
+ * `writeRaw` finishes the line for you, which is right for a whole block of
17
+ * captured output and wrong for a sentence still being written: a streamed
18
+ * answer arrives as a dozen fragments and each one would land on its own row.
19
+ * This adds nothing at all; the caller ends the line when the thought ends.
20
+ */
21
+ export function writeFragment(stream, text) {
22
+ if (!text)
23
+ return;
24
+ stream.write(text);
25
+ }
13
26
  export function errorMessage(error) {
14
27
  return error instanceof Error ? error.message : String(error);
15
28
  }
@@ -10,6 +10,7 @@ import { normalizeCollectionRoots } from "../root-normalization.js";
10
10
  import { detectSecondCockpitInstall } from "../second-install.js";
11
11
  import { runRawEvidenceLocalGc, rawEvidenceGcSummary } from "../raw-evidence-gc.js";
12
12
  import { runBackfillCommand } from "./backfill.js";
13
+ import { installMemoryIntegration, inspectMemoryIntegration, } from "./memory-install.js";
13
14
  import { createInteractiveExecRunner } from "../process-runner.js";
14
15
  const GC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
15
16
  export async function runDoctor(command, io, hooks, overrides = {}) {
@@ -81,6 +82,14 @@ function doctorInvariants() {
81
82
  check: (context) => context.deps.checkAutostart(context),
82
83
  fix: (context, _state) => context.deps.fixAutostart(context),
83
84
  },
85
+ // BLI-3580. Runs right after autostart because it is the same kind of
86
+ // thing: a registration on this machine that nobody should have to be
87
+ // asked about. Its fix writes host configs, never installs software.
88
+ {
89
+ id: "memory-registered",
90
+ check: (context) => context.deps.checkMemory(context),
91
+ fix: (context, _state) => context.deps.fixMemory(context),
92
+ },
84
93
  {
85
94
  id: "backfill-complete",
86
95
  check: (context) => context.deps.checkBackfill(context),
@@ -111,6 +120,8 @@ function defaultDoctorDeps(hooks) {
111
120
  checkSingleInstall: checkSingleInstallState,
112
121
  checkAutostart: checkAutostartState,
113
122
  fixAutostart: fixAutostartState,
123
+ checkMemory: checkMemoryState,
124
+ fixMemory: fixMemoryState,
114
125
  checkBackfill: checkBackfillState,
115
126
  fixBackfill: fixBackfillState,
116
127
  checkGc: checkGcState,
@@ -307,6 +318,52 @@ async function fixAutostartState(context) {
307
318
  }
308
319
  return ok("autostart-alive", "installed", "background sync installed and running");
309
320
  }
321
+ /**
322
+ * BLI Memory's registration on this machine (BLI-3580).
323
+ *
324
+ * The step owns the CONFIG, never the software: it writes the MCP entry, the
325
+ * hooks and the Codex table only when `bli-memory-mcp` actually resolves. With
326
+ * no server it writes NOTHING and reads `skipped bin_missing` — registering
327
+ * hooks that point at an absent binary would make every Claude Code turn print
328
+ * a hook failure, which is worse than waiting a day.
329
+ */
330
+ async function checkMemoryState(context) {
331
+ const outcome = await inspectMemoryIntegration(memoryCommandFor(context), context.io);
332
+ return memoryStepState(outcome, "check");
333
+ }
334
+ async function fixMemoryState(context) {
335
+ const outcome = await installMemoryIntegration(memoryCommandFor(context), context.io);
336
+ return memoryStepState(outcome, "fix");
337
+ }
338
+ function memoryCommandFor(context) {
339
+ return {
340
+ kind: "memory",
341
+ action: "install",
342
+ homeDir: context.command.homeDir,
343
+ dashboardUrl: context.command.dashboardUrl,
344
+ dryRun: false,
345
+ json: context.command.json,
346
+ };
347
+ }
348
+ function memoryStepState(outcome, phase) {
349
+ const broken = outcome.targets.filter((target) => target.target !== "bin" && target.status === "failed");
350
+ if (broken.length > 0) {
351
+ const first = broken[0];
352
+ const message = `${first?.target ?? "memory"}: ${first?.reason ?? "failed"}`;
353
+ return phase === "fix"
354
+ ? fail("memory-registered", first?.reason ?? "memory_install_failed", message)
355
+ : needsFix("memory-registered", first?.reason ?? "memory_install_failed", message);
356
+ }
357
+ const pending = outcome.targets.filter((target) => target.target !== "bin" &&
358
+ (target.status === "missing" || target.status === "mismatch"));
359
+ if (pending.length > 0) {
360
+ return needsFix("memory-registered", "registration_incomplete", `BLI Memory is not registered with ${pending.map((target) => target.target).join(", ")}`);
361
+ }
362
+ if (!outcome.bin_found) {
363
+ return skipped("memory-registered", "bin_missing", "bli-memory-mcp is not on this machine yet; nothing was written and the next run will try again");
364
+ }
365
+ return ok("memory-registered", phase === "fix" ? "installed" : "already_installed", "BLI Memory is registered with both agent hosts");
366
+ }
310
367
  /**
311
368
  * Pure so it can be unit-tested without touching the real machine's home
312
369
  * directory (`getCollectorRuntimePaths()` defaults to `os.homedir()` and
@@ -0,0 +1,184 @@
1
+ /**
2
+ * `cockpit jarvis --trace <id|last>` — the step tree of one turn (BLI-3560).
3
+ *
4
+ * The one thing anybody here ever opened Langfuse for, in the terminal where
5
+ * the turn was asked. Every model step, every tool call, every server-side
6
+ * memory step (BLI-3416's doubled answer was one of those and no other surface
7
+ * showed it), with how long each took, which model ran it, what it spent and
8
+ * whether it failed.
9
+ *
10
+ * ## `last` and the remembered receipt
11
+ *
12
+ * A turn now hands back its own trace id, and this module writes that id — with
13
+ * the conversation it belonged to — to one small file beside the collector's
14
+ * other state. `--trace last` reads it, so the loop is: ask a question, watch
15
+ * it come back slow, type `cockpit jarvis --trace last`. If the file is not
16
+ * there (a fresh shell, a machine that has not asked anything since the
17
+ * release), the file's remembered THREAD is used to ask the server for that
18
+ * conversation's newest turn instead — and when there is neither, the refusal
19
+ * says so in words rather than printing nothing.
20
+ *
21
+ * The file holds two opaque ids and a timestamp. No question, no answer, no
22
+ * name; it is a bookmark, not a transcript.
23
+ *
24
+ * ## The lines
25
+ *
26
+ * Rendered SERVER-SIDE and printed verbatim. That is deliberate: the ops door,
27
+ * this terminal and JARVIS's own `readTurnTrace` tool all read the same
28
+ * sentences from `lib/ops/turn-trace-lines.ts`, so an older CLI against a newer
29
+ * dashboard still prints the current tree rather than its own stale idea of one.
30
+ */
31
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
32
+ import os from "node:os";
33
+ import path from "node:path";
34
+ import { colorEnabled, dim, writeLine } from "./cli-io.js";
35
+ import { getCollectorRuntimePaths } from "../local-state.js";
36
+ import { towerFailureDetail, towerJsonRequest } from "../tower-client.js";
37
+ /** The bookmark file's name, inside the collector's own state directory. */
38
+ const LAST_TURN_FILE = "last-turn-trace.json";
39
+ function bookmarkPath(homeDir) {
40
+ const paths = getCollectorRuntimePaths(homeDir ?? os.homedir());
41
+ return path.join(paths.state_dir, LAST_TURN_FILE);
42
+ }
43
+ /**
44
+ * Remember the turn that just happened. Never throws: a bookmark that could not
45
+ * be written must not fail the answer a person already received, so the failure
46
+ * is named on stderr and the turn stands.
47
+ */
48
+ export async function rememberTurnTrace(turn, io, homeDir) {
49
+ if (!turn.traceId && !turn.threadId)
50
+ return;
51
+ const file = bookmarkPath(homeDir);
52
+ const payload = {
53
+ traceId: turn.traceId ?? null,
54
+ threadId: turn.threadId ?? null,
55
+ at: new Date().toISOString(),
56
+ };
57
+ try {
58
+ await mkdir(path.dirname(file), { recursive: true });
59
+ await writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
60
+ }
61
+ catch (error) {
62
+ writeLine(io.stderr, `[jarvis cli] could not remember this turn's trace ${JSON.stringify({
63
+ reason: "bookmark_write_failed",
64
+ error: error instanceof Error ? error.name : "unknown",
65
+ })}`);
66
+ }
67
+ }
68
+ /** The remembered turn, or null when there is not one. Never throws. */
69
+ export async function readRememberedTurn(homeDir) {
70
+ try {
71
+ const raw = await readFile(bookmarkPath(homeDir), "utf8");
72
+ const parsed = JSON.parse(raw);
73
+ const traceId = typeof parsed.traceId === "string" ? parsed.traceId : null;
74
+ const threadId = typeof parsed.threadId === "string" ? parsed.threadId : null;
75
+ if (!traceId && !threadId)
76
+ return null;
77
+ return { traceId, threadId, at: typeof parsed.at === "string" ? parsed.at : "" };
78
+ }
79
+ catch {
80
+ // A missing or unreadable bookmark is "nothing is remembered", which is the
81
+ // exact answer asked for. The caller says so out loud in a sentence.
82
+ return null;
83
+ }
84
+ }
85
+ export async function runJarvisTrace(context, io) {
86
+ const log = (line) => writeLine(io.stderr, line);
87
+ let traceId = context.requested;
88
+ let resolvedFrom = "argument";
89
+ if (context.requested === "last") {
90
+ const remembered = await readRememberedTurn(context.homeDir);
91
+ if (!remembered) {
92
+ return refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt.");
93
+ }
94
+ if (remembered.traceId) {
95
+ traceId = remembered.traceId;
96
+ resolvedFrom = "bookmark";
97
+ }
98
+ else if (remembered.threadId) {
99
+ // The bookmark knew the conversation but not the turn (an older receipt).
100
+ // Ask the server for that conversation's newest turn rather than nothing.
101
+ const newest = await newestTraceForThread(context, remembered.threadId, io);
102
+ if (!newest) {
103
+ return refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new.");
104
+ }
105
+ traceId = newest;
106
+ resolvedFrom = "thread";
107
+ }
108
+ }
109
+ const result = await towerJsonRequest({
110
+ dashboardUrl: context.dashboardUrl,
111
+ path: `/api/ops/trace/${encodeURIComponent(traceId)}`,
112
+ deviceToken: context.deviceToken,
113
+ fetch: io.fetch,
114
+ method: "GET",
115
+ label: "jarvis:trace",
116
+ timeoutMs: 30_000,
117
+ log,
118
+ });
119
+ if (!result.ok) {
120
+ return refuse(context, io, result.reason, towerFailureDetail(result.reason, result.detail));
121
+ }
122
+ const body = result.body;
123
+ if (body.ok === false) {
124
+ return refuse(context, io, body.error ?? "trace_unavailable", body.message ?? "Tower had nothing to show for that turn.");
125
+ }
126
+ if (context.json) {
127
+ writeLine(io.stdout, JSON.stringify(body));
128
+ }
129
+ else {
130
+ writeTraceTree(io, traceId, body);
131
+ }
132
+ writeLine(io.stderr, `[jarvis cli] trace read ${JSON.stringify({
133
+ resolved_from: resolvedFrom,
134
+ spans: body.trace?.spanCount ?? null,
135
+ errors: body.trace?.errorCount ?? null,
136
+ running: body.trace?.runningCount ?? null,
137
+ duration_ms: body.trace?.durationMs ?? null,
138
+ truncated: body.truncated ?? null,
139
+ })}`);
140
+ return 0;
141
+ }
142
+ async function newestTraceForThread(context, threadId, io) {
143
+ const result = await towerJsonRequest({
144
+ dashboardUrl: context.dashboardUrl,
145
+ path: `/api/ops/trace?thread=${encodeURIComponent(threadId)}&limit=1`,
146
+ deviceToken: context.deviceToken,
147
+ fetch: io.fetch,
148
+ method: "GET",
149
+ label: "jarvis:trace-thread",
150
+ timeoutMs: 30_000,
151
+ log: (line) => writeLine(io.stderr, line),
152
+ });
153
+ if (!result.ok)
154
+ return null;
155
+ const body = result.body;
156
+ return body.traces?.[0]?.traceId ?? null;
157
+ }
158
+ function writeTraceTree(io, traceId, body) {
159
+ const styled = colorEnabled(io);
160
+ writeLine(io.stdout, `Turn ${traceId}`);
161
+ if (body.headline)
162
+ writeLine(io.stdout, dim(body.headline, styled));
163
+ writeLine(io.stdout, "");
164
+ for (const line of body.lines ?? [])
165
+ writeLine(io.stdout, line);
166
+ if ((body.lines ?? []).length === 0) {
167
+ // Never a blank screen: an empty tree is a fact and it gets a sentence.
168
+ writeLine(io.stdout, "That turn recorded no steps.");
169
+ }
170
+ if (body.truncated) {
171
+ writeLine(io.stdout, "");
172
+ writeLine(io.stdout, dim("Only the first 1000 spans of this turn are shown.", styled));
173
+ }
174
+ }
175
+ function refuse(context, io, reason, detail) {
176
+ if (context.json) {
177
+ writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
178
+ }
179
+ else {
180
+ writeLine(io.stderr, detail);
181
+ }
182
+ writeLine(io.stderr, `[jarvis cli] trace unavailable ${JSON.stringify({ reason })}`);
183
+ return 1;
184
+ }