@bli-cockpit/cli 0.2.286 → 0.2.287
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/capture-opt-outs.js +129 -0
- package/dist/collection-scope.js +201 -0
- package/dist/commands/agent-session-report.js +3 -0
- package/dist/commands/analyze.js +2 -0
- package/dist/commands/backfill-scan.js +16 -11
- package/dist/commands/backfill-scope.js +16 -4
- package/dist/commands/backfill-session-report.js +2 -0
- package/dist/commands/cli-io.js +1 -3
- package/dist/commands/doctor-access.js +7 -0
- package/dist/commands/doctor-registration.js +39 -0
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor-shelf.js +62 -0
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor-usage-telemetry.js +18 -2
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/heartbeat-post.js +19 -0
- package/dist/commands/heartbeat.js +13 -2
- package/dist/commands/install-receipts.js +4 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-args-collector-status.js +7 -21
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help-commands-tower.js +6 -1
- package/dist/commands/local-help-commands.js +15 -2
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +7 -0
- package/dist/commands/memory-capture-prompt.js +130 -0
- package/dist/commands/memory-install-claude.js +51 -4
- package/dist/commands/memory-install-report.js +19 -3
- package/dist/commands/memory-install-toml.js +22 -0
- package/dist/commands/memory-install.js +110 -11
- package/dist/commands/memory-store.js +5 -0
- package/dist/commands/memory-switch.js +165 -0
- package/dist/commands/memory-uninstall.js +187 -0
- package/dist/commands/memory-verb-list.js +6 -2
- package/dist/commands/onboard.js +5 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/roots-label.js +85 -0
- package/dist/commands/roots.js +28 -10
- package/dist/commands/session-sync-plan.js +8 -4
- package/dist/commands/session-sync-scan.js +25 -3
- package/dist/commands/session-sync-shelf.js +143 -0
- package/dist/commands/session-sync-upload.js +6 -2
- package/dist/commands/session-sync.js +68 -8
- package/dist/commands/shelf.js +165 -0
- package/dist/commands/sources-check.js +2 -2
- package/dist/commands/sources-reconciliation.js +2 -1
- package/dist/commands/sync-doctor.js +2 -1
- package/dist/commands/sync-receipt.js +1 -0
- package/dist/commands/sync-report.js +1 -0
- package/dist/commands/sync-stub-heal.js +2 -2
- package/dist/commands/sync.js +12 -0
- package/dist/error-message.js +11 -0
- package/dist/evidence-reconcile-client.js +2 -0
- package/dist/evidence-redelivery-plan.js +6 -0
- package/dist/evidence-redelivery.js +6 -1
- package/dist/evidence-stubs-client.js +2 -0
- package/dist/evidence-transcripts-client.js +2 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/dist/founder-standing-file.js +52 -0
- package/dist/founder-standing.js +117 -0
- package/dist/image-describe-client.js +2 -1
- package/dist/live-founder-memo.js +57 -0
- package/dist/local-state-config.js +13 -7
- package/dist/memory-capture-choice.js +73 -0
- package/dist/root-work.js +55 -7
- package/dist/shelf-decision.js +52 -0
- package/dist/shelf-headers.js +15 -0
- package/dist/shelf-reports.js +167 -0
- package/dist/shelf-state.js +112 -0
- package/dist/source-opt-out.js +83 -6
- package/dist/state-dir-audit.js +2 -0
- package/dist/tower-client.js +4 -1
- package/dist/upload-agent-artifacts.js +2 -0
- package/dist/upload-envelope-build.js +2 -2
- package/dist/upload-ingest-receipt.js +2 -0
- package/dist/upload-session-reports-wire.js +2 -0
- package/package.json +4 -4
|
@@ -75,12 +75,17 @@ export function logMemoryOutcome(outcome, platform) {
|
|
|
75
75
|
memory_on: memoryInstallGaps(outcome.receipt).length === 0,
|
|
76
76
|
memory_gaps: memoryInstallGaps(outcome.receipt),
|
|
77
77
|
codex_hooks: outcome.receipt.codex.hooks,
|
|
78
|
+
...(outcome.opted_out ? { opted_out: true, removed_count: outcome.removed_count ?? 0 } : {}),
|
|
78
79
|
};
|
|
79
80
|
// stderr on both branches: launchd captures it to sync.err.log, and stdout is
|
|
80
81
|
// reserved for `--json`.
|
|
81
|
-
console.error(outcome.
|
|
82
|
-
?
|
|
83
|
-
|
|
82
|
+
console.error(outcome.opted_out
|
|
83
|
+
? outcome.status === "failed"
|
|
84
|
+
? "[memory-install] BLI Memory is off on this machine and an entry could not be removed"
|
|
85
|
+
: "[memory-install] BLI Memory is off on this machine; nothing of it was registered"
|
|
86
|
+
: outcome.status === "failed"
|
|
87
|
+
? "[memory-install] BLI Memory is not fully registered on this machine"
|
|
88
|
+
: "[memory-install] BLI Memory registration converged", JSON.stringify(fields));
|
|
84
89
|
}
|
|
85
90
|
export function memoryOutcomeLines(outcome) {
|
|
86
91
|
const headline = memoryOutcomeHeadline(outcome);
|
|
@@ -89,6 +94,11 @@ export function memoryOutcomeLines(outcome) {
|
|
|
89
94
|
// daemon", because nothing looked.
|
|
90
95
|
if (outcome.daemon)
|
|
91
96
|
lines.push(` ${memoryDaemonLine(outcome.daemon)}`);
|
|
97
|
+
// BLI-22640: a non-founder's "off" is ignored, and said so where they read.
|
|
98
|
+
if (outcome.opt_out_ignored) {
|
|
99
|
+
lines.push(" This machine's BLI Memory \"off\" is ignored: only Tower's founders can turn it off, " +
|
|
100
|
+
"because work on BLI machines goes to Tower (opt_out_ignored_not_founder).");
|
|
101
|
+
}
|
|
92
102
|
// The trust step is the one thing a person has to do by hand, so it is said
|
|
93
103
|
// here in words rather than left as a status word they have to look up.
|
|
94
104
|
if (outcome.receipt.codex.hooks === "needs_trust") {
|
|
@@ -108,6 +118,12 @@ export function memoryOutcomeLines(outcome) {
|
|
|
108
118
|
return lines;
|
|
109
119
|
}
|
|
110
120
|
function memoryOutcomeHeadline(outcome) {
|
|
121
|
+
// BLI-22640: the person's choice is the headline, whatever the files say.
|
|
122
|
+
if (outcome.opted_out) {
|
|
123
|
+
return outcome.action === "status"
|
|
124
|
+
? "BLI Memory is off on this machine (your choice). `cockpit memory on` turns it back on."
|
|
125
|
+
: "BLI Memory is off on this machine (your choice); none of it was registered. `cockpit memory on` turns it back on.";
|
|
126
|
+
}
|
|
111
127
|
if (outcome.status === "installed")
|
|
112
128
|
return "BLI Memory registered on this machine.";
|
|
113
129
|
if (outcome.status === "already")
|
|
@@ -148,6 +148,28 @@ export function removeTomlTable(text, tablePath) {
|
|
|
148
148
|
const lines = text.split("\n");
|
|
149
149
|
return [...lines.slice(0, span.start_line), ...lines.slice(span.end_line)].join("\n");
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* BLI-22640. Removes our table AND every sub-table under it (`[a.b.env]`,
|
|
153
|
+
* `[a.b.tools.x]`), each with the lines it owns; every other table survives
|
|
154
|
+
* byte for byte. `cockpit memory off` uses it, so no half of our entry is left
|
|
155
|
+
* for a host to start a server from.
|
|
156
|
+
*/
|
|
157
|
+
export function removeTomlTableTree(text, tablePath) {
|
|
158
|
+
const lines = text.split("\n");
|
|
159
|
+
const kept = [];
|
|
160
|
+
let dropping = false;
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const header = parseTableHeader(line);
|
|
163
|
+
if (header || isArrayTableHeader(line)) {
|
|
164
|
+
dropping = header !== null &&
|
|
165
|
+
header.length >= tablePath.length &&
|
|
166
|
+
samePath(header.slice(0, tablePath.length), tablePath);
|
|
167
|
+
}
|
|
168
|
+
if (!dropping)
|
|
169
|
+
kept.push(line);
|
|
170
|
+
}
|
|
171
|
+
return kept.join("\n");
|
|
172
|
+
}
|
|
151
173
|
export function renderTomlTable(tablePath, entries) {
|
|
152
174
|
const header = `[${tablePath.map(renderKey).join(".")}]`;
|
|
153
175
|
const body = entries.map(([key, value]) => `${renderKey(key)} = ${renderValue(value)}`);
|
|
@@ -47,7 +47,10 @@ import { installTowerIntegration, inspectTowerIntegration } from "./tower-mcp-in
|
|
|
47
47
|
import { defaultMemoryFileIo, } from "./memory-install-files.js";
|
|
48
48
|
import { installClaudeMemoryIntegration, inspectClaudeMemoryIntegration, } from "./memory-install-claude.js";
|
|
49
49
|
import { installCodexMemoryIntegration, inspectCodexMemoryIntegration, } from "./memory-install-codex.js";
|
|
50
|
-
import { resolveMemoryConfig } from "./memory-install-config.js";
|
|
50
|
+
import { resolveMemoryConfig, resolveMemoryMcpBin } from "./memory-install-config.js";
|
|
51
|
+
import { removeMemoryIntegration } from "./memory-uninstall.js";
|
|
52
|
+
import { readEffectiveMemoryCapture, readMemoryCaptureChoice, writeMemoryCaptureChoice } from "../memory-capture-choice.js";
|
|
53
|
+
import { envWithNodeRuntimeOnPath } from "../scheduled-self-update.js";
|
|
51
54
|
import { probeMemoryDaemon } from "./memory-daemon-probe.js";
|
|
52
55
|
import { expectedCodexTrustRows, readCodexHookTrustFromDisk, } from "./memory-install-codex-hooks.js";
|
|
53
56
|
import { buildMemoryInstallReceipt, writeMemoryReceiptFile, } from "./memory-install-receipt.js";
|
|
@@ -60,6 +63,12 @@ export { memoryOutcomeLines, resolveDashboardUrl } from "./memory-install-report
|
|
|
60
63
|
export { buildMemoryInstallReceipt, memoryReceiptLine, readMemoryReceiptFile, writeMemoryReceiptFile, MEMORY_RECEIPT_FILE, } from "./memory-install-receipt.js";
|
|
61
64
|
export { codexHooksFile, readCodexHookTrust, } from "./memory-install-codex-hooks.js";
|
|
62
65
|
export async function runMemoryInstall(command, io, deps = {}) {
|
|
66
|
+
// BLI-22640: a person typing `install` is choosing BLI Memory for this
|
|
67
|
+
// machine, which clears an earlier `off`. Unattended callers never come
|
|
68
|
+
// through here; they call `installMemoryIntegration` and only read the choice.
|
|
69
|
+
const wasOff = command.action === "install" && !command.dryRun
|
|
70
|
+
? await recordInstallChoice(command, deps)
|
|
71
|
+
: false;
|
|
63
72
|
const outcome = command.action === "status"
|
|
64
73
|
? await inspectMemoryIntegration(command, io, deps)
|
|
65
74
|
: await installMemoryIntegration(command, io, deps);
|
|
@@ -77,6 +86,8 @@ export async function runMemoryInstall(command, io, deps = {}) {
|
|
|
77
86
|
writeLine(io.stdout, JSON.stringify({ ...outcome, setup_receipt: receipt?.receipt ?? null }, null, 2));
|
|
78
87
|
}
|
|
79
88
|
else {
|
|
89
|
+
if (wasOff)
|
|
90
|
+
writeLine(io.stdout, "BLI Memory is back on for this machine (it was off). Undo: cockpit memory off");
|
|
80
91
|
for (const line of memoryOutcomeLines(outcome))
|
|
81
92
|
writeLine(io.stdout, line);
|
|
82
93
|
if (receipt) {
|
|
@@ -88,10 +99,31 @@ export async function runMemoryInstall(command, io, deps = {}) {
|
|
|
88
99
|
}
|
|
89
100
|
return outcome.status === "failed" ? 1 : 0;
|
|
90
101
|
}
|
|
102
|
+
/** Records "on" for a person-typed `install`; true when it replaced an "off". A write that fails is logged and the install still runs. */
|
|
103
|
+
async function recordInstallChoice(command, deps) {
|
|
104
|
+
const paths = getCollectorRuntimePaths(deps.homeDir ?? command.homeDir ?? os.homedir());
|
|
105
|
+
const wasOff = (await readMemoryCaptureChoice(paths)).state === "off";
|
|
106
|
+
try {
|
|
107
|
+
await writeMemoryCaptureChoice(paths, true, "memory_install");
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("[memory-install] choice not saved; an earlier off still stands", JSON.stringify({ reason: "choice_write_failed", error_name: error instanceof Error ? error.name : typeof error }));
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return wasOff;
|
|
114
|
+
}
|
|
91
115
|
export async function installMemoryIntegration(command, io, deps = {}) {
|
|
92
116
|
const homeDir = deps.homeDir ?? command.homeDir ?? os.homedir();
|
|
93
117
|
const platform = deps.platform ?? process.platform;
|
|
94
118
|
const fileIo = deps.io ?? defaultMemoryFileIo();
|
|
119
|
+
// BLI-22640: the person's "off" outranks every unattended caller (doctor,
|
|
120
|
+
// onboard, update, the sync tick), because they all come through here.
|
|
121
|
+
// Only a founder's "off" is obeyed (Jeetesh's review of 2026-10-25); for
|
|
122
|
+
// anybody else the install runs as usual and the outcome names what it ignored.
|
|
123
|
+
const capture = await readEffectiveMemoryCapture(getCollectorRuntimePaths(homeDir), { fetch: io.fetch, ...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}) });
|
|
124
|
+
if (capture.off && capture.reading.state === "off") {
|
|
125
|
+
return installWhileOptedOut(command, io, deps, { homeDir, platform, fileIo, reason: capture.reading.reason });
|
|
126
|
+
}
|
|
95
127
|
const resolved = await resolveMemoryConfig(command, io, platform, deps);
|
|
96
128
|
const targets = [resolved.binTarget];
|
|
97
129
|
// `no_bin_no_write`: with no resolved server there is nothing safe to
|
|
@@ -106,18 +138,10 @@ export async function installMemoryIntegration(command, io, deps = {}) {
|
|
|
106
138
|
// tower-mcp-install.ts's header for why this rides here rather than a
|
|
107
139
|
// separate, un-invoked verb. Independent of the memory outcome above: a
|
|
108
140
|
// machine with one server but not the other is a real machine.
|
|
109
|
-
targets.push(...(await
|
|
110
|
-
io: fileIo,
|
|
111
|
-
platform,
|
|
112
|
-
homeDir,
|
|
113
|
-
dashboardUrl: await resolveDashboardUrl(command, deps),
|
|
114
|
-
env: io.env ?? process.env,
|
|
115
|
-
fileExists: deps.fileExists,
|
|
116
|
-
cliEntryPoint: deps.cliEntryPoint,
|
|
117
|
-
realpath: deps.realpath,
|
|
118
|
-
}, command.dryRun)));
|
|
141
|
+
targets.push(...(await towerTargets(command, io, deps, { homeDir, platform, fileIo })));
|
|
119
142
|
const outcome = {
|
|
120
143
|
action: "install",
|
|
144
|
+
...(capture.ignored ? { opt_out_ignored: "opt_out_ignored_not_founder" } : {}),
|
|
121
145
|
...(await readBackOutcome(targets, resolved, homeDir, fileIo)),
|
|
122
146
|
};
|
|
123
147
|
logMemoryOutcome(outcome, platform);
|
|
@@ -129,6 +153,75 @@ export async function installMemoryIntegration(command, io, deps = {}) {
|
|
|
129
153
|
}
|
|
130
154
|
return outcome;
|
|
131
155
|
}
|
|
156
|
+
/** BLI-3706: `bli-tower` registers beside `bli-memory`, opted out or not. */
|
|
157
|
+
async function towerTargets(command, io, deps, scope) {
|
|
158
|
+
return installTowerIntegration({
|
|
159
|
+
io: scope.fileIo,
|
|
160
|
+
platform: scope.platform,
|
|
161
|
+
homeDir: scope.homeDir,
|
|
162
|
+
dashboardUrl: await resolveDashboardUrl(command, deps),
|
|
163
|
+
env: io.env ?? process.env,
|
|
164
|
+
fileExists: deps.fileExists,
|
|
165
|
+
cliEntryPoint: deps.cliEntryPoint,
|
|
166
|
+
realpath: deps.realpath,
|
|
167
|
+
}, command.dryRun);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* BLI-22640. The install on a machine that turned BLI Memory off: write none
|
|
171
|
+
* of it, take back any of our entries that are still there (an older CLI, a
|
|
172
|
+
* restored backup, a hand edit), and keep `bli-tower`. Every BLI Memory target
|
|
173
|
+
* reads `skipped explicit_opt_out` (or `choice_unreadable`), so the sync
|
|
174
|
+
* tick's receipt, the heartbeat and the doctor row all name the choice.
|
|
175
|
+
*
|
|
176
|
+
* The bin is LOOKED UP but never run: `bin_found` stays a true fact about the
|
|
177
|
+
* machine, and no `--print-config` child is spawned for a server we will not
|
|
178
|
+
* register.
|
|
179
|
+
*/
|
|
180
|
+
async function installWhileOptedOut(command, io, deps, scope) {
|
|
181
|
+
const removal = await removeMemoryIntegration({ homeDir: scope.homeDir, io: scope.fileIo, dryRun: command.dryRun });
|
|
182
|
+
const targets = removal.map((result) => result.status === "failed"
|
|
183
|
+
? { target: result.target, status: "failed", reason: result.reason, path: result.path, ...(result.detail ? { detail: result.detail } : {}) }
|
|
184
|
+
: {
|
|
185
|
+
target: result.target,
|
|
186
|
+
status: "skipped",
|
|
187
|
+
reason: scope.reason,
|
|
188
|
+
path: result.path,
|
|
189
|
+
...(result.status === "removed" ? { detail: `removed ${result.removed ?? 0} BLI Memory entry(ies)` } : {}),
|
|
190
|
+
...(result.status === "would_remove" ? { detail: `would remove ${result.removed ?? 0} BLI Memory entry(ies)` } : {}),
|
|
191
|
+
});
|
|
192
|
+
targets.push({ target: "codex_skills", status: "skipped", reason: scope.reason });
|
|
193
|
+
targets.push(...(await towerTargets(command, io, deps, scope)));
|
|
194
|
+
const bin = await resolveMemoryMcpBin({
|
|
195
|
+
env: envWithNodeRuntimeOnPath(io.env ?? process.env),
|
|
196
|
+
platform: scope.platform,
|
|
197
|
+
fileExists: deps.fileExists,
|
|
198
|
+
cliEntryPoint: deps.cliEntryPoint,
|
|
199
|
+
realpath: deps.realpath,
|
|
200
|
+
}).catch(() => null);
|
|
201
|
+
const failed = targets.find((target) => target.status === "failed");
|
|
202
|
+
const outcome = {
|
|
203
|
+
action: "install",
|
|
204
|
+
status: failed ? "failed" : "skipped",
|
|
205
|
+
reason: failed ? failed.reason : scope.reason,
|
|
206
|
+
opted_out: true,
|
|
207
|
+
// A dry run counts what it WOULD remove, so doctor's check can say "entries left".
|
|
208
|
+
removed_count: removal.reduce((sum, result) => sum + (result.status === "removed" || result.status === "would_remove" ? result.removed ?? 0 : 0), 0),
|
|
209
|
+
config_source: "none",
|
|
210
|
+
bin_found: bin !== null,
|
|
211
|
+
...(bin ? { bin_source: bin.source } : {}),
|
|
212
|
+
targets,
|
|
213
|
+
receipt: buildMemoryInstallReceipt({
|
|
214
|
+
targets,
|
|
215
|
+
binFound: bin !== null,
|
|
216
|
+
codexTrust: await codexTrustFor(scope.homeDir, scope.fileIo),
|
|
217
|
+
expectedTrustRows: 0,
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
logMemoryOutcome(outcome, scope.platform);
|
|
221
|
+
if (!command.dryRun)
|
|
222
|
+
await cacheReceipt(scope.homeDir, outcome.receipt);
|
|
223
|
+
return outcome;
|
|
224
|
+
}
|
|
132
225
|
/** What both `install` and `status` report once the targets are known: their verdict and the receipt. */
|
|
133
226
|
async function readBackOutcome(targets, resolved, homeDir, fileIo) {
|
|
134
227
|
return {
|
|
@@ -197,8 +290,14 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
|
|
|
197
290
|
cliEntryPoint: deps.cliEntryPoint,
|
|
198
291
|
realpath: deps.realpath,
|
|
199
292
|
})));
|
|
293
|
+
// BLI-22640: `status` reads what is on disk either way, and says when the
|
|
294
|
+
// person turned BLI Memory off, so "not registered" is not read as broken.
|
|
295
|
+
const capture = await readEffectiveMemoryCapture(getCollectorRuntimePaths(homeDir), { fetch: io.fetch, ...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}) });
|
|
296
|
+
const optedOut = capture.off;
|
|
200
297
|
const outcome = {
|
|
201
298
|
action: "status",
|
|
299
|
+
...(optedOut ? { opted_out: true } : {}),
|
|
300
|
+
...(capture.ignored ? { opt_out_ignored: "opt_out_ignored_not_founder" } : {}),
|
|
202
301
|
// The one live question on this surface: is the daemon answering? It is
|
|
203
302
|
// bounded, it opens nothing but a local socket, and a machine without one
|
|
204
303
|
// is reported as exactly that rather than as a problem.
|
|
@@ -58,6 +58,7 @@ import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js"
|
|
|
58
58
|
import { runMemoryRequeue } from "./memory-requeue.js";
|
|
59
59
|
import { runMemoryVisibility } from "./memory-visibility.js";
|
|
60
60
|
import { readNoteFile } from "./notes-file.js";
|
|
61
|
+
import { FOUNDER_TEXT_VERBS, founderMemoryVerbRefusal } from "../shelf-state.js";
|
|
61
62
|
export { runMemoryVerbList } from "./memory-verb-list.js";
|
|
62
63
|
const TAG = "[memory cli]";
|
|
63
64
|
const SEARCH_DEADLINE_MS = 60_000;
|
|
@@ -69,6 +70,10 @@ const BODY_MAX_CHARS = 200_000;
|
|
|
69
70
|
const FORGET_MAX_CHARS = 10_000;
|
|
70
71
|
export async function runMemoryStore(command, io) {
|
|
71
72
|
const door = await openAgentDoor("memory", command, io);
|
|
73
|
+
// BLI-23478: on a founder's laptop the text-carrying verbs refuse locally, as the Memory MCP does (`shelf-state.ts`).
|
|
74
|
+
const founderRefusal = FOUNDER_TEXT_VERBS.has(command.action) ? await founderMemoryVerbRefusal(door.homeDir, io.fetch) : null;
|
|
75
|
+
if (founderRefusal)
|
|
76
|
+
return failAgentDoor(door, TAG, founderRefusal.reason, founderRefusal.detail);
|
|
72
77
|
switch (command.action) {
|
|
73
78
|
case "map":
|
|
74
79
|
return runMemoryMap(door);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory on|off` and their long names `install|uninstall`
|
|
3
|
+
* (BLI-22640): the per-machine BLI Memory switch a person flips by hand.
|
|
4
|
+
*
|
|
5
|
+
* `off` records this machine's "no" (`memory-capture-choice.ts`) and then runs
|
|
6
|
+
* the ordinary install, which, on an opted-out machine, registers nothing of
|
|
7
|
+
* BLI Memory and takes back any of our hooks and MCP entries it finds
|
|
8
|
+
* (`installWhileOptedOut`, memory-install.ts). `on` records "yes" and runs the
|
|
9
|
+
* same install, which registers them. Going through the one install keeps the
|
|
10
|
+
* receipt, the heartbeat and the doctor row reading the same thing the switch
|
|
11
|
+
* just did.
|
|
12
|
+
*
|
|
13
|
+
* The choice is written FIRST. If it cannot be written, nothing else happens
|
|
14
|
+
* and the command fails: a removal without the recorded "no" would be undone
|
|
15
|
+
* by the next sync tick, which is the bug this switch exists to end.
|
|
16
|
+
*
|
|
17
|
+
* `off` and `uninstall` are founders only (Jeetesh's review of 2026-10-25):
|
|
18
|
+
* before anything is written, Tower is asked live whether this machine's
|
|
19
|
+
* person is a founder (`mayRecordOff`, founder-standing.ts). Anybody else is
|
|
20
|
+
* refused with `memory_off_founders_only`, and a Tower that cannot be asked
|
|
21
|
+
* refuses with `founder_check_unavailable`; either way nothing is written and
|
|
22
|
+
* BLI Memory stays on. `on` is open to everyone.
|
|
23
|
+
*
|
|
24
|
+
* Each verb prints one line: what changed, and the command that undoes it.
|
|
25
|
+
* `uninstall` (and `install`, in memory-install.ts) add the per-target detail
|
|
26
|
+
* under it.
|
|
27
|
+
*/
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import { writeLine } from "./cli-io.js";
|
|
30
|
+
import { installMemoryIntegration, memoryOutcomeLines, } from "./memory-install.js";
|
|
31
|
+
import { removeMemoryIntegration } from "./memory-uninstall.js";
|
|
32
|
+
import { defaultMemoryFileIo } from "./memory-install-files.js";
|
|
33
|
+
import { refreshSetupReceipt } from "./setup-receipt.js";
|
|
34
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
35
|
+
import { readMemoryCaptureChoice, writeMemoryCaptureChoice, } from "../memory-capture-choice.js";
|
|
36
|
+
import { mayRecordOff } from "../founder-standing.js";
|
|
37
|
+
const VIA = {
|
|
38
|
+
on: "memory_on",
|
|
39
|
+
off: "memory_off",
|
|
40
|
+
uninstall: "memory_uninstall",
|
|
41
|
+
};
|
|
42
|
+
export async function runMemorySwitch(command, io, deps = {}) {
|
|
43
|
+
const homeDir = deps.homeDir ?? command.homeDir ?? os.homedir();
|
|
44
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
45
|
+
const turnOn = command.action === "on";
|
|
46
|
+
const before = await readMemoryCaptureChoice(paths);
|
|
47
|
+
if (!turnOn) {
|
|
48
|
+
const permission = await mayRecordOff({
|
|
49
|
+
homeDir,
|
|
50
|
+
...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}),
|
|
51
|
+
fetch: io.fetch,
|
|
52
|
+
door: `memory_${command.action}`,
|
|
53
|
+
});
|
|
54
|
+
if (!permission.allowed) {
|
|
55
|
+
print(command, io, {
|
|
56
|
+
line: permission.line,
|
|
57
|
+
// A non-founder's "off" is never obeyed, so for them memory is on whatever the file says.
|
|
58
|
+
memory_capture: permission.reason === "memory_off_founders_only" || before.state !== "off",
|
|
59
|
+
changed: false,
|
|
60
|
+
status: "refused",
|
|
61
|
+
reason: permission.reason,
|
|
62
|
+
});
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (command.dryRun)
|
|
67
|
+
return dryRunSwitch(command, io, deps, { homeDir, turnOn });
|
|
68
|
+
try {
|
|
69
|
+
await writeMemoryCaptureChoice(paths, turnOn, VIA[command.action]);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const reason = error instanceof Error && error.message.startsWith("memory_capture") ? error.message : "choice_write_failed";
|
|
73
|
+
console.error("[memory-switch] choice not saved; nothing was changed", JSON.stringify({ reason, action: command.action }));
|
|
74
|
+
const line = `BLI Memory could not be turned ${turnOn ? "on" : "off"}: this machine's choice could not be saved (${reason}). Nothing was changed.`;
|
|
75
|
+
print(command, io, { line, memory_capture: before.state !== "off", changed: false, status: "failed", reason });
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
const outcome = await installMemoryIntegration({ ...command, action: "install" }, io, deps);
|
|
79
|
+
const line = turnOn ? onLine(before, outcome) : offLine(before, outcome);
|
|
80
|
+
const receipt = await refreshSetupReceipt(io, {
|
|
81
|
+
homeDir,
|
|
82
|
+
...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}),
|
|
83
|
+
});
|
|
84
|
+
const changed = turnOn ? before.state !== "on" || outcome.status === "installed" : before.state !== "off" || (outcome.removed_count ?? 0) > 0;
|
|
85
|
+
console.error("[memory-switch] BLI Memory switched", JSON.stringify({ action: command.action, memory_capture: turnOn, changed, status: outcome.status, reason: outcome.reason, removed_count: outcome.removed_count ?? 0 }));
|
|
86
|
+
print(command, io, {
|
|
87
|
+
line,
|
|
88
|
+
memory_capture: turnOn,
|
|
89
|
+
changed,
|
|
90
|
+
status: outcome.status,
|
|
91
|
+
reason: outcome.reason,
|
|
92
|
+
outcome,
|
|
93
|
+
setup_receipt: receipt?.receipt ?? null,
|
|
94
|
+
});
|
|
95
|
+
return outcome.status === "failed" ? 1 : 0;
|
|
96
|
+
}
|
|
97
|
+
/** Writes nothing at all: not the choice, not a host file. */
|
|
98
|
+
async function dryRunSwitch(command, io, deps, scope) {
|
|
99
|
+
if (scope.turnOn) {
|
|
100
|
+
const outcome = await installMemoryIntegration({ ...command, action: "install", dryRun: true }, io, deps);
|
|
101
|
+
const line = "Dry run: BLI Memory would be turned on for this machine and registered with Claude Code and Codex. Nothing was written.";
|
|
102
|
+
print(command, io, { line, memory_capture: true, changed: false, status: outcome.status, reason: "dry_run", outcome });
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const removal = await removeMemoryIntegration({ homeDir: scope.homeDir, io: deps.io ?? defaultMemoryFileIo(), dryRun: true });
|
|
106
|
+
const pending = removal.reduce((sum, result) => sum + (result.status === "would_remove" ? result.removed ?? 0 : 0), 0);
|
|
107
|
+
const line = `Dry run: BLI Memory would be turned off for this machine and ${pending} of its entries removed from Claude Code and Codex. Nothing was written.`;
|
|
108
|
+
print(command, io, { line, memory_capture: false, changed: false, status: "would_remove", reason: "dry_run", removal });
|
|
109
|
+
return removal.some((result) => result.status === "failed") ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
export function offLine(before, outcome) {
|
|
112
|
+
const undo = " Undo: cockpit memory on";
|
|
113
|
+
if (outcome.status === "failed") {
|
|
114
|
+
const failed = outcome.targets.find((target) => target.status === "failed");
|
|
115
|
+
return `BLI Memory is off for this machine, but ${failed?.target ?? "an entry"} could not be cleaned (${outcome.reason}); `
|
|
116
|
+
+ "its save hooks send nothing while it is off, and `cockpit memory off` tries again." + undo;
|
|
117
|
+
}
|
|
118
|
+
const removed = outcome.removed_count ?? 0;
|
|
119
|
+
if (removed > 0) {
|
|
120
|
+
return `BLI Memory is off for this machine: removed its ${removed} hook and MCP entries from Claude Code and Codex, `
|
|
121
|
+
+ "and doctor, onboard, update and the background sync will not add them back." + undo;
|
|
122
|
+
}
|
|
123
|
+
if (before.state === "off")
|
|
124
|
+
return "BLI Memory was already off for this machine; nothing changed." + undo;
|
|
125
|
+
return "BLI Memory is off for this machine: nothing of it was registered, and doctor, onboard, update and the background sync will not add it." + undo;
|
|
126
|
+
}
|
|
127
|
+
export function onLine(before, outcome) {
|
|
128
|
+
const undo = " Undo: cockpit memory off";
|
|
129
|
+
if (outcome.status === "failed") {
|
|
130
|
+
return `BLI Memory is on for this machine, but registering it failed (${outcome.reason}); \`cockpit doctor\` repairs it.` + undo;
|
|
131
|
+
}
|
|
132
|
+
if (outcome.status === "skipped") {
|
|
133
|
+
return "BLI Memory is on for this machine; its server is not installed yet, so nothing was registered, "
|
|
134
|
+
+ "and the next `cockpit doctor` or background sync registers it." + undo;
|
|
135
|
+
}
|
|
136
|
+
if (outcome.status === "installed") {
|
|
137
|
+
return "BLI Memory is on for this machine: registered its hooks and MCP server with Claude Code and Codex, "
|
|
138
|
+
+ "so each session is saved and agents remember it." + undo;
|
|
139
|
+
}
|
|
140
|
+
if (before.state === "on")
|
|
141
|
+
return "BLI Memory was already on for this machine; nothing changed." + undo;
|
|
142
|
+
return "BLI Memory is on for this machine (it was already registered); your choice is saved, so doctor will not ask again." + undo;
|
|
143
|
+
}
|
|
144
|
+
function print(command, io, result) {
|
|
145
|
+
if (command.json) {
|
|
146
|
+
writeLine(io.stdout, JSON.stringify({
|
|
147
|
+
action: command.action,
|
|
148
|
+
memory_capture: result.memory_capture,
|
|
149
|
+
changed: result.changed,
|
|
150
|
+
status: result.status,
|
|
151
|
+
reason: result.reason,
|
|
152
|
+
message: result.line,
|
|
153
|
+
...(result.outcome ? { targets: result.outcome.targets, receipt: result.outcome.receipt } : {}),
|
|
154
|
+
...(result.removal ? { removal: result.removal } : {}),
|
|
155
|
+
...(result.setup_receipt !== undefined ? { setup_receipt: result.setup_receipt } : {}),
|
|
156
|
+
}, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
writeLine(io.stdout, result.line);
|
|
160
|
+
// `uninstall` is the long name: the same line, then what each file says.
|
|
161
|
+
if (command.action === "uninstall" && result.outcome) {
|
|
162
|
+
for (const line of memoryOutcomeLines(result.outcome).slice(1))
|
|
163
|
+
writeLine(io.stdout, line);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory off` / `cockpit memory uninstall` (BLI-22640): take BLI
|
|
3
|
+
* Memory's registration back off this machine.
|
|
4
|
+
*
|
|
5
|
+
* Removes exactly what `cockpit memory install` writes that can send or
|
|
6
|
+
* receive conversation text, and nothing a person wrote themselves:
|
|
7
|
+
*
|
|
8
|
+
* claude_mcp ~/.claude.json `mcpServers["bli-memory"]`
|
|
9
|
+
* claude_hooks ~/.claude/settings.json our hook commands, by bin name (`stripOurHooks`)
|
|
10
|
+
* codex_mcp ~/.codex/config.toml `[mcp_servers.bli-memory]` and its sub-tables
|
|
11
|
+
* codex_hooks ~/.codex/hooks.json our hook commands, by bin name
|
|
12
|
+
*
|
|
13
|
+
* Only OUR commands come out of a hook group. A group a person shares with
|
|
14
|
+
* ours keeps their commands and its matcher; a group left empty is dropped
|
|
15
|
+
* (the Codex review point on PR 2615).
|
|
16
|
+
*
|
|
17
|
+
* Left in place on purpose: the read-only `permissions.allow` entry (it names
|
|
18
|
+
* a tool that no longer exists, so it allows nothing) and the Codex skill
|
|
19
|
+
* files (instructions to an agent, not a hook; they send nothing). `bli-tower`
|
|
20
|
+
* stays registered: it carries Tower's docs and messages tools, not memory.
|
|
21
|
+
*
|
|
22
|
+
* Same three rules as the installer: merge, never clobber (another server or
|
|
23
|
+
* another hook comes out untouched; a file that will not parse is refused,
|
|
24
|
+
* never rewritten); idempotent (a second run finds nothing and says so); and
|
|
25
|
+
* read back what the disk stored (BLI-2541): a removal is only `removed` once
|
|
26
|
+
* the re-read file holds none of ours.
|
|
27
|
+
*
|
|
28
|
+
* Never throws at the caller. The sync tick calls this to keep an opted-out
|
|
29
|
+
* machine clean, and a failure is a named result, not a lost tick.
|
|
30
|
+
*/
|
|
31
|
+
import { MEMORY_MCP_SERVER_ID, } from "./memory-install-contract.js";
|
|
32
|
+
import { claudeMcpConfigFile, claudeSettingsFile, ourHookCommandCount, parseJsonRecord, stripOurHooks, serializeJson, } from "./memory-install-claude.js";
|
|
33
|
+
import { codexConfigFile } from "./memory-install-codex.js";
|
|
34
|
+
import { codexHooksFile } from "./memory-install-codex-hooks.js";
|
|
35
|
+
import { removeTomlTableTree } from "./memory-install-toml.js";
|
|
36
|
+
const MEMORY_TOML_PATH = ["mcp_servers", MEMORY_MCP_SERVER_ID];
|
|
37
|
+
export async function removeMemoryIntegration(options) {
|
|
38
|
+
const results = [
|
|
39
|
+
await removeFromJson("claude_mcp", claudeMcpConfigFile(options.homeDir), options, countOurServer, dropOurServer),
|
|
40
|
+
await removeFromJson("claude_hooks", claudeSettingsFile(options.homeDir), options, countOurHookGroups, dropOurHookGroups),
|
|
41
|
+
await removeFromCodexToml(options),
|
|
42
|
+
await removeFromJson("codex_hooks", codexHooksFile(options.homeDir), options, countOurHookGroups, dropOurHookGroups),
|
|
43
|
+
];
|
|
44
|
+
console.error("[memory-uninstall] BLI Memory registration removal finished", JSON.stringify({
|
|
45
|
+
dry_run: options.dryRun,
|
|
46
|
+
removed_count: results.reduce((sum, result) => sum + (result.status === "removed" ? result.removed ?? 0 : 0), 0),
|
|
47
|
+
targets: results.map((result) => `${result.target}:${result.status}/${result.reason}`),
|
|
48
|
+
}));
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
/** True when every target is clean (or would be, on a dry run). */
|
|
52
|
+
export function removalSucceeded(results) {
|
|
53
|
+
return results.every((result) => result.status !== "failed");
|
|
54
|
+
}
|
|
55
|
+
/** Whether anything of ours is still on disk, for a check that must not write. */
|
|
56
|
+
export function removalPending(results) {
|
|
57
|
+
return results.some((result) => result.status === "would_remove");
|
|
58
|
+
}
|
|
59
|
+
async function removeFromJson(target, file, options, count, drop) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await options.io.readText(file);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return failed(target, file, "read_failed", error);
|
|
66
|
+
}
|
|
67
|
+
if (raw === null || !raw.trim())
|
|
68
|
+
return { target, status: "absent", reason: "file_absent", path: file };
|
|
69
|
+
const root = parseJsonRecord(raw);
|
|
70
|
+
if (!root) {
|
|
71
|
+
return {
|
|
72
|
+
target,
|
|
73
|
+
status: "failed",
|
|
74
|
+
reason: "config_unreadable",
|
|
75
|
+
path: file,
|
|
76
|
+
detail: "the file is not valid JSON; nothing was changed",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const ours = count(root);
|
|
80
|
+
if (ours === 0)
|
|
81
|
+
return { target, status: "absent", reason: "entry_absent", path: file };
|
|
82
|
+
if (options.dryRun)
|
|
83
|
+
return { target, status: "would_remove", reason: "dry_run", path: file, removed: ours };
|
|
84
|
+
try {
|
|
85
|
+
await options.io.writeText(file, serializeJson(drop(structuredClone(root)), raw));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return failed(target, file, "write_failed", error);
|
|
89
|
+
}
|
|
90
|
+
const storedRaw = await options.io.readText(file).catch(() => null);
|
|
91
|
+
const stored = storedRaw === null ? null : parseJsonRecord(storedRaw);
|
|
92
|
+
if (!stored || count(stored) !== 0) {
|
|
93
|
+
return {
|
|
94
|
+
target,
|
|
95
|
+
status: "failed",
|
|
96
|
+
reason: "read_back_mismatch",
|
|
97
|
+
path: file,
|
|
98
|
+
detail: "the file on disk still holds a BLI Memory entry",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { target, status: "removed", reason: "removed_entry", path: file, removed: ours };
|
|
102
|
+
}
|
|
103
|
+
async function removeFromCodexToml(options) {
|
|
104
|
+
const target = "codex_mcp";
|
|
105
|
+
const file = codexConfigFile(options.homeDir);
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = await options.io.readText(file);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return failed(target, file, "read_failed", error);
|
|
112
|
+
}
|
|
113
|
+
if (raw === null)
|
|
114
|
+
return { target, status: "absent", reason: "file_absent", path: file };
|
|
115
|
+
const next = removeTomlTableTree(raw, MEMORY_TOML_PATH);
|
|
116
|
+
if (next === raw)
|
|
117
|
+
return { target, status: "absent", reason: "entry_absent", path: file };
|
|
118
|
+
if (options.dryRun)
|
|
119
|
+
return { target, status: "would_remove", reason: "dry_run", path: file, removed: 1 };
|
|
120
|
+
try {
|
|
121
|
+
await options.io.writeText(file, next);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return failed(target, file, "write_failed", error);
|
|
125
|
+
}
|
|
126
|
+
const stored = await options.io.readText(file).catch(() => null);
|
|
127
|
+
if (stored === null || removeTomlTableTree(stored, MEMORY_TOML_PATH) !== stored) {
|
|
128
|
+
return {
|
|
129
|
+
target,
|
|
130
|
+
status: "failed",
|
|
131
|
+
reason: "read_back_mismatch",
|
|
132
|
+
path: file,
|
|
133
|
+
detail: "the table on disk still holds the bli-memory server",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { target, status: "removed", reason: "removed_entry", path: file, removed: 1 };
|
|
137
|
+
}
|
|
138
|
+
function countOurServer(root) {
|
|
139
|
+
const servers = asRecord(root["mcpServers"]);
|
|
140
|
+
return servers && MEMORY_MCP_SERVER_ID in servers ? 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
function dropOurServer(root) {
|
|
143
|
+
const servers = asRecord(root["mcpServers"]);
|
|
144
|
+
if (servers)
|
|
145
|
+
delete servers[MEMORY_MCP_SERVER_ID];
|
|
146
|
+
return root;
|
|
147
|
+
}
|
|
148
|
+
function countOurHookGroups(root) {
|
|
149
|
+
return ourHookCommandCount(root);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Takes our commands out and keeps everything else: a person's command in a
|
|
153
|
+
* group shared with ours stays, with the group's matcher. A group left with no
|
|
154
|
+
* hooks is dropped, and an event left with no groups at all is dropped, not
|
|
155
|
+
* left as `[]`.
|
|
156
|
+
*/
|
|
157
|
+
function dropOurHookGroups(root) {
|
|
158
|
+
const hooks = asRecord(root["hooks"]);
|
|
159
|
+
if (!hooks)
|
|
160
|
+
return root;
|
|
161
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
162
|
+
if (!Array.isArray(groups))
|
|
163
|
+
continue;
|
|
164
|
+
const stripped = stripOurHooks(groups);
|
|
165
|
+
if (stripped.removed === 0)
|
|
166
|
+
continue;
|
|
167
|
+
if (stripped.groups.length === 0)
|
|
168
|
+
delete hooks[event];
|
|
169
|
+
else
|
|
170
|
+
hooks[event] = stripped.groups;
|
|
171
|
+
}
|
|
172
|
+
return root;
|
|
173
|
+
}
|
|
174
|
+
function failed(target, file, reason, error) {
|
|
175
|
+
return {
|
|
176
|
+
target,
|
|
177
|
+
status: "failed",
|
|
178
|
+
reason,
|
|
179
|
+
path: file,
|
|
180
|
+
detail: error instanceof Error ? error.name : "unknown",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function asRecord(value) {
|
|
184
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
185
|
+
return null;
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
@@ -47,7 +47,11 @@ const VERB_LIST_LINES = [
|
|
|
47
47
|
" their writes; super admins only, and a DRY RUN unless you pass --apply",
|
|
48
48
|
"",
|
|
49
49
|
"This machine:",
|
|
50
|
-
" cockpit memory
|
|
50
|
+
" cockpit memory on [--dry-run] [--json] turn BLI Memory on here and register its hooks",
|
|
51
|
+
" cockpit memory off [--dry-run] [--json] turn it off here: remove its hooks and MCP entry,",
|
|
52
|
+
" and doctor, onboard, update and the background sync never add them back",
|
|
53
|
+
" cockpit memory install [--dry-run] [--json] on, printing every file it checked",
|
|
54
|
+
" cockpit memory uninstall [--dry-run] [--json] off, printing every file it checked",
|
|
51
55
|
" cockpit memory status [--json] read the stored config back",
|
|
52
56
|
"",
|
|
53
57
|
"The experience log:",
|
|
@@ -73,7 +77,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
73
77
|
writeLine(io.stdout, JSON.stringify({
|
|
74
78
|
ok: true,
|
|
75
79
|
noun: "memory",
|
|
76
|
-
machine_verbs: ["install", "status"],
|
|
80
|
+
machine_verbs: ["on", "off", "install", "uninstall", "status"],
|
|
77
81
|
store_verbs: ["rules", "knowledge", "search", "save", "update", "forget", "private", "public", "profile", "consolidation", "chain", "source", "exam", "requeue"],
|
|
78
82
|
log_verbs: ["log"],
|
|
79
83
|
}));
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveOnboardEmail } from "./local-auth.js";
|
|
2
2
|
import { discoverCommandWorktrees } from "./local-discovery.js";
|
|
3
3
|
import { handleOnboardFailure, } from "./onboard-completion.js";
|
|
4
|
+
import { askMemoryChoiceOnce } from "./memory-capture-prompt.js";
|
|
4
5
|
import { runOnboardMultiRepoFlow, runOnboardSingleRepoFlow, } from "./onboard-flows.js";
|
|
5
6
|
import { writeOnboardBanner } from "./onboard-report.js";
|
|
6
7
|
import { prepareOnboardRoots, installOnboardConfig, pairForOnboarding } from "./onboard-setup.js";
|
|
@@ -28,6 +29,10 @@ export async function runOnboard(command, io) {
|
|
|
28
29
|
const claimedOwnerEmail = await resolveOnboardEmail(roots.resolvedCommand, roots.collectionRoots, roots.existingConfig, io);
|
|
29
30
|
install = await installOnboardConfig(command, roots.resolvedRoots, installEvents, io);
|
|
30
31
|
pair = await pairForOnboarding(command, { primaryRoot: roots.primaryRoot, claimedOwnerEmail }, installEvents, io);
|
|
32
|
+
// BLI-22640: asked once, before the flows below can register BLI Memory,
|
|
33
|
+
// and after pairing, because only a founder is asked and Tower is the one
|
|
34
|
+
// who says who is a founder.
|
|
35
|
+
await askMemoryChoiceOnce(io, { ...(command.homeDir ? { homeDir: command.homeDir } : {}), json: command.json });
|
|
31
36
|
const worktrees = await discoverOnboardWorktrees(command, roots, io);
|
|
32
37
|
const ctx = {
|
|
33
38
|
command,
|