@bli-cockpit/cli 0.2.57 → 0.2.59

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 (53) hide show
  1. package/dist/commands/browser-open.js +88 -0
  2. package/dist/commands/docs.js +27 -6
  3. package/dist/commands/doctor-report.js +17 -1
  4. package/dist/commands/doctor.js +12 -2
  5. package/dist/commands/heartbeat.js +65 -1
  6. package/dist/commands/issue-contracts.js +99 -0
  7. package/dist/commands/issue-write.js +129 -0
  8. package/dist/commands/issue.js +189 -0
  9. package/dist/commands/jarvis-answer-envelope.js +80 -0
  10. package/dist/commands/jarvis-turn.js +16 -1
  11. package/dist/commands/jarvis.js +3 -0
  12. package/dist/commands/local-args-collector-setup.js +17 -0
  13. package/dist/commands/local-args-tower-admin.js +12 -2
  14. package/dist/commands/local-args-tower-docs-msg.js +95 -6
  15. package/dist/commands/local-args-tower-search.js +50 -0
  16. package/dist/commands/local-args-tower-work.js +178 -0
  17. package/dist/commands/local-args-tower.js +7 -1
  18. package/dist/commands/local-args.js +29 -14
  19. package/dist/commands/local-command-shapes.js +12 -0
  20. package/dist/commands/local-help-commands.js +643 -0
  21. package/dist/commands/local-help.js +12 -551
  22. package/dist/commands/local.js +9 -0
  23. package/dist/commands/login.js +91 -8
  24. package/dist/commands/memory-install-claude.js +35 -15
  25. package/dist/commands/memory-install-codex-hooks.js +200 -0
  26. package/dist/commands/memory-install-codex.js +12 -2
  27. package/dist/commands/memory-install-config.js +140 -0
  28. package/dist/commands/memory-install-receipt.js +222 -0
  29. package/dist/commands/memory-install-report.js +113 -0
  30. package/dist/commands/memory-install.js +99 -276
  31. package/dist/commands/msg.js +85 -2
  32. package/dist/commands/notes-door.js +120 -0
  33. package/dist/commands/notes-reads.js +134 -0
  34. package/dist/commands/notes-writes.js +208 -0
  35. package/dist/commands/notes.js +16 -442
  36. package/dist/commands/onboard-completion.js +47 -0
  37. package/dist/commands/onboard-setup.js +82 -2
  38. package/dist/commands/ops-render-memory.js +76 -0
  39. package/dist/commands/ops-render.js +13 -1
  40. package/dist/commands/ops.js +65 -3
  41. package/dist/commands/project.js +38 -0
  42. package/dist/commands/public-root.js +1 -1
  43. package/dist/commands/search.js +122 -0
  44. package/dist/commands/setup-receipt-lines.js +71 -0
  45. package/dist/commands/setup-receipt.js +241 -0
  46. package/dist/commands/status.js +20 -1
  47. package/dist/local-state-pairing-code.js +200 -0
  48. package/dist/local-state.js +6 -0
  49. package/dist/repo-identity-fingerprint.js +88 -0
  50. package/dist/repo-identity-git.js +76 -0
  51. package/dist/repo-identity-linked-worktrees.js +81 -0
  52. package/dist/repo-identity.js +5 -222
  53. package/package.json +7 -7
@@ -0,0 +1,222 @@
1
+ /**
2
+ * THE MEMORY RECEIPT (BLI-3729) — six words this machine can prove.
3
+ *
4
+ * `cockpit memory install` already knew, per target, what it had just done.
5
+ * What nobody could ask was the fleet question: "does Savina's machine have
6
+ * memory hooks?" This module turns the install's own per-target results into
7
+ * the small fixed shape that rides the heartbeat
8
+ * (`@bli-cockpit/telemetry-core`'s `MemoryInstallReceipt`) and that
9
+ * `lib/ops/fleet-liveness.ts` reads back on the board.
10
+ *
11
+ * **It is built from targets that were READ BACK, never from what was sent**
12
+ * (BLI-2541). Every target it consumes has already re-read and re-parsed the
13
+ * stored file the way the host will: `installed` means the bytes on disk parse
14
+ * back to what we wrote, `already` means they already did. This module adds no
15
+ * new claim; it names the six pieces and carries the reason each one is not
16
+ * `ok`.
17
+ *
18
+ * **One module, on purpose.** BLI-3731 folds this receipt into a one-login
19
+ * ceremony, so the shape has exactly one producer here and exactly one reader
20
+ * on the server. Anything that wants to know whether a machine has memory on
21
+ * calls `buildMemoryInstallReceipt` — it does not re-derive six words from a
22
+ * target list of its own.
23
+ *
24
+ * ## The one word that is not about a file
25
+ *
26
+ * `codex.hooks` can be `needs_trust`: installed correctly, and not running,
27
+ * because Codex requires a person to review and trust a non-managed command
28
+ * hook before it fires (`/hooks`; see `memory-install-codex-hooks.ts` for the
29
+ * citation). That state exists in no other piece and is the reason this is six
30
+ * words rather than a boolean.
31
+ */
32
+ import { MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION, } from "@bli-cockpit/telemetry-core";
33
+ /**
34
+ * The one place the six words are decided.
35
+ *
36
+ * A target this run never produced (an older CLI, a half-run that threw) is
37
+ * `unknown`, never `missing`: "we did not look" and "it is not there" are
38
+ * different facts and the board acts on them differently.
39
+ */
40
+ export function buildMemoryInstallReceipt(input) {
41
+ const reasons = {};
42
+ const piece = (id, key) => {
43
+ const target = input.targets.find((entry) => entry.target === id);
44
+ if (!target) {
45
+ reasons[key] = input.binFound ? "target_not_checked" : "bin_missing";
46
+ return "unknown";
47
+ }
48
+ const word = pieceFor(target.status);
49
+ if (word !== "ok")
50
+ reasons[key] = reasonLabel(target.reason);
51
+ return word;
52
+ };
53
+ const codexHooks = judgeCodexHooks(input, reasons);
54
+ const receipt = {
55
+ schema_version: MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION,
56
+ checked_at: (input.now ?? new Date()).toISOString(),
57
+ claude: {
58
+ mcp: piece("claude_mcp", "claude.mcp"),
59
+ hooks: piece("claude_hooks", "claude.hooks"),
60
+ },
61
+ codex: {
62
+ mcp: piece("codex_mcp", "codex.mcp"),
63
+ skill: piece("codex_skills", "codex.skill"),
64
+ hooks: codexHooks,
65
+ },
66
+ bin_found: input.binFound,
67
+ ...(Object.keys(reasons).length > 0 ? { reasons } : {}),
68
+ };
69
+ return receipt;
70
+ }
71
+ /**
72
+ * `codex.hooks` is judged in three steps, in this order, and the order is the
73
+ * point:
74
+ *
75
+ * 1. A person who turned hooks OFF is `unsupported`. That is a decision, and
76
+ * reporting it as a missing install would put a machine on somebody's
77
+ * morning board for doing what its owner asked.
78
+ * 2. A file target that is not installed loses; there is nothing to trust.
79
+ * 3. Only then does trust decide, and it can only demote: a correctly written
80
+ * hooks file with no trust rows is `needs_trust`, which is INSTALLED and
81
+ * NOT RUNNING.
82
+ */
83
+ function judgeCodexHooks(input, reasons) {
84
+ const key = "codex.hooks";
85
+ if (input.codexTrust.feature === "disabled") {
86
+ reasons[key] = "hooks_disabled_in_config";
87
+ return "unsupported";
88
+ }
89
+ const target = input.targets.find((entry) => entry.target === "codex_hooks");
90
+ if (!target) {
91
+ reasons[key] = input.binFound ? "target_not_checked" : "bin_missing";
92
+ return "unknown";
93
+ }
94
+ const word = pieceFor(target.status);
95
+ if (word !== "ok") {
96
+ reasons[key] = reasonLabel(target.reason);
97
+ return word;
98
+ }
99
+ if (input.codexTrust.feature === "config_unreadable") {
100
+ // The hooks are written and correct; whether Codex will run them could not
101
+ // be read. Say which half is unknown rather than claiming either.
102
+ reasons[key] = "trust_unreadable";
103
+ return "unknown";
104
+ }
105
+ if (input.codexTrust.trustedRows >= input.expectedTrustRows)
106
+ return "ok";
107
+ reasons[key] =
108
+ input.codexTrust.disabledRows > 0 ? "trust_rows_disabled" : "no_trust_rows";
109
+ return "needs_trust";
110
+ }
111
+ /**
112
+ * A target's status collapsed to one of the receipt's words.
113
+ *
114
+ * `would_install` (a dry run) is `unknown` on purpose: a dry run proves what
115
+ * WOULD happen and nothing about what is on disk, and a receipt that treated
116
+ * it as a reading would let `--dry-run` publish a state nobody wrote.
117
+ */
118
+ function pieceFor(status) {
119
+ switch (status) {
120
+ case "installed":
121
+ case "already":
122
+ return "ok";
123
+ case "mismatch":
124
+ return "stale";
125
+ case "missing":
126
+ return "missing";
127
+ case "skipped":
128
+ // Nothing was written, on purpose — today only `bin_missing`. The config
129
+ // is untouched, so "missing" is the true reading of the file.
130
+ return "missing";
131
+ case "would_install":
132
+ case "failed":
133
+ default:
134
+ return "unknown";
135
+ }
136
+ }
137
+ /** The receipt's reason labels are charset-bounded; a stray reason is clipped. */
138
+ function reasonLabel(reason) {
139
+ const cleaned = reason.trim().replace(/[^A-Za-z0-9_:.-]/gu, "_");
140
+ if (cleaned.length === 0)
141
+ return "unlabelled";
142
+ return cleaned.slice(0, 60);
143
+ }
144
+ /** `claude ✓ hooks ✓ · codex mcp ✓ skill ✓ hooks needs_trust` — one line. */
145
+ export function memoryReceiptLine(receipt) {
146
+ const mark = (word) => (word === "ok" ? "✓" : word);
147
+ return (`claude mcp ${mark(receipt.claude.mcp)} hooks ${mark(receipt.claude.hooks)} · ` +
148
+ `codex mcp ${mark(receipt.codex.mcp)} skill ${mark(receipt.codex.skill)} ` +
149
+ `hooks ${mark(receipt.codex.hooks)}`);
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Where the receipt lives between runs
153
+ // ---------------------------------------------------------------------------
154
+ /**
155
+ * The receipt is CACHED on disk, and the heartbeat reads the cache.
156
+ *
157
+ * The alternative — recomputing it inside the heartbeat — costs five file
158
+ * reads AND a `bli-memory-mcp --print-config` spawn every fifteen minutes on
159
+ * every machine, to re-prove a state that changes about once a month. The
160
+ * install and status paths already do that work; this is where they leave the
161
+ * answer. `checked_at` travels with it, so a stale reading says how stale it
162
+ * is rather than pretending to be now.
163
+ *
164
+ * A cache that cannot be written is logged and dropped: the install still
165
+ * happened, and a heartbeat with no receipt reads as "not reported", never as
166
+ * "not installed".
167
+ */
168
+ export const MEMORY_RECEIPT_FILE = "memory-install-receipt.json";
169
+ /** How old a cached receipt may be before the heartbeat stops sending it. */
170
+ export const MEMORY_RECEIPT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
171
+ export async function writeMemoryReceiptFile(options) {
172
+ const file = joinState(options.stateDir, MEMORY_RECEIPT_FILE);
173
+ try {
174
+ await options.io.mkdir(options.stateDir);
175
+ await options.io.writeFile(file, `${JSON.stringify(options.receipt, null, 2)}\n`);
176
+ return true;
177
+ }
178
+ catch (error) {
179
+ console.error("[memory-install] the memory receipt could not be cached for the heartbeat", JSON.stringify({
180
+ reason: "receipt_cache_write_failed",
181
+ error_name: error instanceof Error ? error.name : "unknown",
182
+ }));
183
+ return false;
184
+ }
185
+ }
186
+ /**
187
+ * The cached receipt, or a named reason there is none. Never throws: a
188
+ * heartbeat must not fail over a cache file.
189
+ */
190
+ export async function readMemoryReceiptFile(options) {
191
+ let raw;
192
+ try {
193
+ raw = await options.readText(joinState(options.stateDir, MEMORY_RECEIPT_FILE));
194
+ }
195
+ catch {
196
+ return { receipt: null, reason: "receipt_absent" };
197
+ }
198
+ if (raw === null || !raw.trim())
199
+ return { receipt: null, reason: "receipt_absent" };
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(raw);
203
+ }
204
+ catch {
205
+ return { receipt: null, reason: "receipt_unparseable" };
206
+ }
207
+ const receipt = options.parse(parsed);
208
+ if (!receipt)
209
+ return { receipt: null, reason: "receipt_unparseable" };
210
+ const age = (options.now ?? new Date()).getTime() - Date.parse(receipt.checked_at);
211
+ // A reading a week old is not a reading. Better to send nothing and let the
212
+ // board say "not reported" than to publish a claim about a machine that has
213
+ // not been looked at since.
214
+ if (Number.isFinite(age) && age > MEMORY_RECEIPT_MAX_AGE_MS) {
215
+ return { receipt: null, reason: "receipt_expired" };
216
+ }
217
+ return { receipt, reason: "receipt_read" };
218
+ }
219
+ function joinState(stateDir, name) {
220
+ const separator = stateDir.includes("\\") && !stateDir.includes("/") ? "\\" : "/";
221
+ return stateDir.endsWith(separator) ? `${stateDir}${name}` : `${stateDir}${separator}${name}`;
222
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The dashboard URL to register against, aggregating per-target results into
3
+ * one outcome, and rendering that outcome for a log line or a terminal.
4
+ * Sibling of `memory-install.ts`, named in its header.
5
+ */
6
+ import { memoryInstallGaps } from "@bli-cockpit/telemetry-core";
7
+ import { memoryReceiptLine } from "./memory-install-receipt.js";
8
+ import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
9
+ export async function resolveDashboardUrl(command, deps) {
10
+ if (command.dashboardUrl)
11
+ return command.dashboardUrl;
12
+ const paths = getCollectorRuntimePaths(deps.homeDir ?? command.homeDir);
13
+ const config = await readLocalCollectorConfig(paths).catch(() => null);
14
+ return config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
15
+ }
16
+ export function aggregate(targets) {
17
+ const failed = targets.filter((target) => target.status === "failed");
18
+ if (failed.length > 0) {
19
+ return {
20
+ status: "failed",
21
+ // The first named reason, not a count: an operator needs the reason, and
22
+ // the per-target list beside it carries the rest.
23
+ reason: failed[0]?.reason ?? "unknown_failure",
24
+ };
25
+ }
26
+ if (targets.some((target) => target.status === "would_install")) {
27
+ return { status: "would_install", reason: "dry_run" };
28
+ }
29
+ if (targets.some((target) => target.status === "installed")) {
30
+ return { status: "installed", reason: "wrote_entry" };
31
+ }
32
+ if (targets.some((target) => target.status === "mismatch" || target.status === "missing")) {
33
+ return { status: "missing", reason: "entry_absent" };
34
+ }
35
+ // BLI-3706: checked BEFORE `skipped`, not after. `bli-tower` registering
36
+ // beside `bli-memory` on this same command means a machine can be fully
37
+ // "already" registered for one server while the other's bin is not here
38
+ // yet — that machine's OWN state must not read as "skipped" (nothing is
39
+ // happening) when something plainly already is. `skipped` only wins the
40
+ // whole outcome when NOTHING on this machine has ever reached "already"
41
+ // either — the original one-server case (a fresh machine, bin missing,
42
+ // nothing written at all) still returns "skipped" via the fallback below.
43
+ if (targets.some((target) => target.status === "already")) {
44
+ return { status: "already", reason: "already_current" };
45
+ }
46
+ const skipped = targets.find((target) => target.status === "skipped");
47
+ if (skipped) {
48
+ // Not a failure and not a success: nothing was written, on purpose, and
49
+ // the next daily run will try again. `bin_missing` is the only one today.
50
+ return { status: "skipped", reason: skipped.reason };
51
+ }
52
+ return { status: "already", reason: "already_current" };
53
+ }
54
+ /**
55
+ * Both branches log, and neither carries a path — a home directory names a
56
+ * person, and this line ends up in `sync.err.log` on every machine.
57
+ */
58
+ export function logMemoryOutcome(outcome, platform) {
59
+ const fields = {
60
+ status: outcome.status,
61
+ reason: outcome.reason,
62
+ config_source: outcome.config_source,
63
+ bin_found: outcome.bin_found,
64
+ platform,
65
+ installed_count: outcome.targets.filter((target) => target.status === "installed").length,
66
+ already_count: outcome.targets.filter((target) => target.status === "already").length,
67
+ failed: outcome.targets
68
+ .filter((target) => target.status === "failed")
69
+ .map((target) => `${target.target}:${target.reason}`),
70
+ // BLI-3729: the six words, on the same line. `sync.err.log` is where a
71
+ // person looks when the board says their machine is missing a piece, and
72
+ // the log that only named per-target statuses could not answer "is memory
73
+ // ON here?" without the reader collapsing five rows in their head.
74
+ memory_on: memoryInstallGaps(outcome.receipt).length === 0,
75
+ memory_gaps: memoryInstallGaps(outcome.receipt),
76
+ codex_hooks: outcome.receipt.codex.hooks,
77
+ };
78
+ // stderr on both branches: launchd captures it to sync.err.log, and stdout is
79
+ // reserved for `--json`.
80
+ console.error(outcome.status === "failed"
81
+ ? "[memory-install] BLI Memory is not fully registered on this machine"
82
+ : "[memory-install] BLI Memory registration converged", JSON.stringify(fields));
83
+ }
84
+ export function memoryOutcomeLines(outcome) {
85
+ const headline = outcome.status === "installed"
86
+ ? "BLI Memory registered on this machine."
87
+ : outcome.status === "already"
88
+ ? "BLI Memory is already registered on this machine."
89
+ : outcome.status === "would_install"
90
+ ? "BLI Memory would be registered (dry run; nothing was written)."
91
+ : outcome.status === "missing"
92
+ ? "BLI Memory is not registered on this machine."
93
+ : outcome.status === "skipped"
94
+ ? "BLI Memory was not registered and nothing was written: the bli-memory-mcp server is not on this machine yet."
95
+ : `BLI Memory is not fully registered: ${outcome.reason}.`;
96
+ const lines = [headline, ` ${memoryReceiptLine(outcome.receipt)}`];
97
+ // The trust step is the one thing a person has to do by hand, so it is said
98
+ // here in words rather than left as a status word they have to look up.
99
+ if (outcome.receipt.codex.hooks === "needs_trust") {
100
+ lines.push(" Codex will NOT run these hooks until you trust them: open Codex in any folder, " +
101
+ "run /hooks, and trust the three bli-memory entries. One time, per machine.");
102
+ }
103
+ if (outcome.receipt.codex.hooks === "unsupported") {
104
+ lines.push(" Codex hooks are turned off in your ~/.codex/config.toml ([features] hooks = false), " +
105
+ "so the hooks were written and will not run. That is your setting; nothing here changes it.");
106
+ }
107
+ for (const target of outcome.targets) {
108
+ const where = target.path ? ` ${target.path}` : "";
109
+ const detail = target.detail ? ` — ${target.detail}` : "";
110
+ lines.push(` ${target.target}: ${target.status} (${target.reason})${where}${detail}`);
111
+ }
112
+ return lines;
113
+ }