@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.
- package/dist/commands/browser-open.js +88 -0
- package/dist/commands/docs.js +27 -6
- package/dist/commands/doctor-report.js +17 -1
- package/dist/commands/doctor.js +12 -2
- package/dist/commands/heartbeat.js +65 -1
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/jarvis-answer-envelope.js +80 -0
- package/dist/commands/jarvis-turn.js +16 -1
- package/dist/commands/jarvis.js +3 -0
- package/dist/commands/local-args-collector-setup.js +17 -0
- package/dist/commands/local-args-tower-admin.js +12 -2
- package/dist/commands/local-args-tower-docs-msg.js +95 -6
- package/dist/commands/local-args-tower-search.js +50 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +29 -14
- package/dist/commands/local-command-shapes.js +12 -0
- package/dist/commands/local-help-commands.js +643 -0
- package/dist/commands/local-help.js +12 -551
- package/dist/commands/local.js +9 -0
- package/dist/commands/login.js +91 -8
- package/dist/commands/memory-install-claude.js +35 -15
- package/dist/commands/memory-install-codex-hooks.js +200 -0
- package/dist/commands/memory-install-codex.js +12 -2
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-receipt.js +222 -0
- package/dist/commands/memory-install-report.js +113 -0
- package/dist/commands/memory-install.js +99 -276
- package/dist/commands/msg.js +85 -2
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/onboard-completion.js +47 -0
- package/dist/commands/onboard-setup.js +82 -2
- package/dist/commands/ops-render-memory.js +76 -0
- package/dist/commands/ops-render.js +13 -1
- package/dist/commands/ops.js +65 -3
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +122 -0
- package/dist/commands/setup-receipt-lines.js +71 -0
- package/dist/commands/setup-receipt.js +241 -0
- package/dist/commands/status.js +20 -1
- package/dist/local-state-pairing-code.js +200 -0
- package/dist/local-state.js +6 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- 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
|
+
}
|