@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
|
@@ -78,6 +78,8 @@ export function reexecDoctor(command, io) {
|
|
|
78
78
|
const args = ["doctor"];
|
|
79
79
|
if (command.noUsageTelemetry)
|
|
80
80
|
args.push("--no-usage-telemetry");
|
|
81
|
+
if (command.noMemory)
|
|
82
|
+
args.push("--no-memory");
|
|
81
83
|
if (command.homeDir)
|
|
82
84
|
args.push("--home", command.homeDir);
|
|
83
85
|
if (command.backfillBudgetSeconds)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getCollectorRuntimePaths, readLocalCollectorSessionFile } from "../local-state.js";
|
|
3
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, readLocalCollectorSessionFile } from "../local-state.js";
|
|
4
|
+
import { founderRoutingOn } from "./doctor-shelf.js";
|
|
4
5
|
import { claudeSettingsFile } from "./memory-install-claude.js";
|
|
5
6
|
import { codexConfigFile } from "./tower-mcp-codex.js";
|
|
6
7
|
import { defaultMemoryFileIo } from "./memory-install-files.js";
|
|
@@ -83,7 +84,7 @@ async function usageTelemetryStep(context, checkOnly) {
|
|
|
83
84
|
if (!session?.device_token)
|
|
84
85
|
return needsFix("usage-telemetry", "device_token_missing", "usage telemetry: needs Tower sign-in");
|
|
85
86
|
const result = await configureUsageTelemetry({ homeDir: home, origin: context.command.dashboardUrl, token: session.device_token,
|
|
86
|
-
optOut: context.command.noUsageTelemetry, checkOnly, io: defaultMemoryFileIo({ mode: 0o600 }) });
|
|
87
|
+
optOut: context.command.noUsageTelemetry || (await founderWithholdsModels(home, context.io.fetch)), checkOnly, io: defaultMemoryFileIo({ mode: 0o600 }) });
|
|
87
88
|
const message = result.off ? "usage telemetry: off (explicit opt-out)" : "usage telemetry: on (claude code, codex)";
|
|
88
89
|
return checkOnly && result.changed ? needsFix("usage-telemetry", "settings_differ", message) : ok("usage-telemetry", result.off ? "explicit_opt_out" : "already_current", message);
|
|
89
90
|
}
|
|
@@ -93,5 +94,20 @@ async function usageTelemetryStep(context, checkOnly) {
|
|
|
93
94
|
return fail("usage-telemetry", reason, "usage telemetry: settings could not be safely merged");
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* BLI-23478 (three shelves, design 3.2 category C): usage telemetry names the model and tool of every session, so
|
|
99
|
+
* on a founder's laptop, once founder routing is on, it is opted out unless the founder allowed `model` with
|
|
100
|
+
* `cockpit shelf metrics --allow`. The opt-out persists like any other (the marker), so allowing `model` later
|
|
101
|
+
* needs the marker removed by hand: deliberate, since turning a channel back on is a person's call.
|
|
102
|
+
*/
|
|
103
|
+
async function founderWithholdsModels(homeDir, fetchImpl) {
|
|
104
|
+
if (!(await founderRoutingOn(homeDir, fetchImpl)))
|
|
105
|
+
return false;
|
|
106
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
107
|
+
const withheld = !(config?.shelf_metrics_allow ?? []).includes("model");
|
|
108
|
+
if (withheld)
|
|
109
|
+
console.error("[usage telemetry] founder opt-out", JSON.stringify({ reason: "shelf_model_category_off" }));
|
|
110
|
+
return withheld;
|
|
111
|
+
}
|
|
96
112
|
export const checkUsageTelemetry = (context) => usageTelemetryStep(context, true);
|
|
97
113
|
export const fixUsageTelemetry = (context) => usageTelemetryStep(context, false);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { checkSlackConnected } from "./doctor-slack.js";
|
|
2
2
|
import { checkUsageTelemetry, fixUsageTelemetry } from "./doctor-usage-telemetry.js";
|
|
3
|
+
import { settleDoctorMemoryChoice } from "./memory-capture-prompt.js";
|
|
4
|
+
import { reportIgnoredOptOuts } from "../capture-opt-outs.js";
|
|
5
|
+
import { checkShelfRouting, fixShelfRouting } from "./doctor-shelf.js";
|
|
3
6
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
4
7
|
import { withCommandGuard } from "../command-guard.js";
|
|
5
8
|
import { installHttp1GlobalFetch } from "../http1-fetch.js";
|
|
@@ -31,6 +34,10 @@ export async function runDoctorWithDeps(command, io, deps) {
|
|
|
31
34
|
const deadlineMs = doctorRunDeadline(command, runStarted);
|
|
32
35
|
const context = { command, io, deps, deadlineMs };
|
|
33
36
|
console.error("[doctor] run started", JSON.stringify({ json: command.json, check_only: Boolean(command.dryRun || command.checkOnly), budget_seconds: deadlineMs === null ? null : DOCTOR_JSON_BUDGET_SECONDS }));
|
|
37
|
+
// BLI-22640: `--no-memory`, or the one-time question, before any row reads the choice.
|
|
38
|
+
await settleDoctorMemoryChoice(command, io);
|
|
39
|
+
// Founders only: a non-founder's opt-outs are ignored, and doctor says so in one line.
|
|
40
|
+
await reportIgnoredOptOuts({ ...(command.homeDir ? { homeDir: command.homeDir } : {}), json: command.json }, io);
|
|
34
41
|
const rows = [];
|
|
35
42
|
const repairs = [];
|
|
36
43
|
const diagnoseOnly = command.dryRun || command.checkOnly;
|
|
@@ -178,6 +185,8 @@ function doctorInvariants() {
|
|
|
178
185
|
// no fix because a session outside an approved root is not a defect
|
|
179
186
|
// doctor can repair; it names the counts and moves on.
|
|
180
187
|
{ id: "roots-cover-work", check: context => context.deps.readRootsCoverWork(context) },
|
|
188
|
+
// BLI-23478: a founder's drawer default once Tower's lock is on (`doctor-shelf.ts`); everyone else: one ok line.
|
|
189
|
+
{ id: "shelf-routing", check: (c) => (c.deps.checkShelfRouting ?? checkShelfRouting)(c), fix: (c) => (c.deps.fixShelfRouting ?? fixShelfRouting)(c) },
|
|
181
190
|
// Deliberately has NO fix: uninstalling software the operator did not ask
|
|
182
191
|
// to have uninstalled is not a repair (BLI-3218/BLI-3553). Doctor names the
|
|
183
192
|
// other install and the exact command; the person decides.
|
|
@@ -11,6 +11,10 @@ import { describeError } from "../health-detail.js";
|
|
|
11
11
|
import { classifySyncHealthError } from "../sync-health-class.js";
|
|
12
12
|
import { noteHeartbeatRefusal, recordHeartbeatPendingToken, recordHeartbeatTokenExpiry, TOKEN_ROTATION_HEADER, } from "./heartbeat-token.js";
|
|
13
13
|
import { noteDoctorRequest } from "./sync-doctor-request.js";
|
|
14
|
+
import { recordFounderStanding } from "../founder-standing-file.js";
|
|
15
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
16
|
+
import { collectorShelfHeaders } from "../shelf-headers.js";
|
|
17
|
+
import { recordShelfState } from "../shelf-state.js";
|
|
14
18
|
const HEARTBEAT_TIMEOUT_MS = 5_000;
|
|
15
19
|
export async function postHeartbeat(options, deviceToken, heartbeat, memory) {
|
|
16
20
|
const controller = new AbortController();
|
|
@@ -21,6 +25,8 @@ export async function postHeartbeat(options, deviceToken, heartbeat, memory) {
|
|
|
21
25
|
headers: {
|
|
22
26
|
"Content-Type": "application/json",
|
|
23
27
|
Authorization: `Bearer ${deviceToken}`,
|
|
28
|
+
// BLI-23478: the CLI release, so Tower records `cli_too_old` on an owner's old laptop.
|
|
29
|
+
...collectorShelfHeaders(),
|
|
24
30
|
// BLI-17994. "I store a renewed token if you send one": the dashboard
|
|
25
31
|
// only rotates for a collector that says so.
|
|
26
32
|
[TOKEN_ROTATION_HEADER]: "1",
|
|
@@ -84,6 +90,19 @@ async function recordAcceptedHeartbeat(options, response, heartbeat, memory) {
|
|
|
84
90
|
// door reports, which moves only when this request promoted a token.
|
|
85
91
|
await recordHeartbeatPendingToken(options.homeDir, reply);
|
|
86
92
|
await recordHeartbeatTokenExpiry(options.homeDir, response);
|
|
93
|
+
// BLI-22640: Tower's answer to "is this machine's person a founder?", kept
|
|
94
|
+
// for DISPLAY (doctor, support). No decision reads this file: every reader
|
|
95
|
+
// that decides what leaves the machine asks Tower live. Only a Tower reply
|
|
96
|
+
// writes it. A reply that could not read the owners list says null, and the
|
|
97
|
+
// last displayed answer stands.
|
|
98
|
+
if (typeof reply?.["is_owner"] === "boolean") {
|
|
99
|
+
await recordFounderStanding(getCollectorRuntimePaths(options.homeDir), reply["is_owner"], "heartbeat");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.error("[heartbeat] no founder answer in this reply; the last one stands", JSON.stringify({ reason: "is_owner_absent" }));
|
|
103
|
+
}
|
|
104
|
+
// BLI-23478 (three shelves): what the lock is for this laptop's person, for the hooks, the MCP server and doctor.
|
|
105
|
+
await recordShelfState(options.homeDir, reply);
|
|
87
106
|
console.error("[heartbeat] checked in", JSON.stringify({
|
|
88
107
|
reason: "heartbeat_recorded",
|
|
89
108
|
http_status: response.status,
|
|
@@ -24,7 +24,7 @@ import { collectAgentSetup } from "./agent-setup.js";
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
-
import { COLLECTOR_HEARTBEAT_SCHEMA_VERSION, MemoryInstallReceiptSchema, setupReceiptGaps, } from "@bli-cockpit/telemetry-core";
|
|
27
|
+
import { COLLECTOR_HEARTBEAT_MAX_ROOTS, COLLECTOR_HEARTBEAT_SCHEMA_VERSION, MemoryInstallReceiptSchema, setupReceiptGaps, } from "@bli-cockpit/telemetry-core";
|
|
28
28
|
import { readCachedSetupReceipt } from "./setup-receipt.js";
|
|
29
29
|
import { readStagingInventory } from "../disk-usage.js";
|
|
30
30
|
import { readStagingGate } from "../staging-gate.js";
|
|
@@ -32,6 +32,8 @@ import { describeError } from "../health-detail.js";
|
|
|
32
32
|
import { shouldSuppressFleetReceipts, } from "../dev-build.js";
|
|
33
33
|
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, } from "../local-state.js";
|
|
34
34
|
import { postHeartbeat } from "./heartbeat-post.js";
|
|
35
|
+
import { readCaptureOptOuts } from "../capture-opt-outs.js";
|
|
36
|
+
import { teamRootsForHeartbeat } from "../shelf-state.js";
|
|
35
37
|
import { readHeartbeatSession } from "./heartbeat-token.js";
|
|
36
38
|
import { withTokenRotationLock } from "./heartbeat-token-lock.js";
|
|
37
39
|
import { readMemoryReceiptFile } from "./memory-install-receipt.js";
|
|
@@ -103,6 +105,9 @@ export function buildCollectorHeartbeat(options) {
|
|
|
103
105
|
collector_version: options.collectorVersion ?? LOCAL_COLLECTOR_VERSION,
|
|
104
106
|
os_platform: platform,
|
|
105
107
|
roots: collectionRootLabels(options.roots, platform),
|
|
108
|
+
...(options.shelvedRoots
|
|
109
|
+
? { shelved_roots: Array.from({ length: Math.min(options.shelvedRoots, COLLECTOR_HEARTBEAT_MAX_ROOTS) }, () => ({ shelf: "drawer" })) }
|
|
110
|
+
: {}),
|
|
106
111
|
last_sync_status: options.facts.status,
|
|
107
112
|
...(options.facts.outsideRootDirectories ? { outside_root_directories: options.facts.outsideRootDirectories } : {}),
|
|
108
113
|
...(options.facts.reason ? { last_sync_reason: options.facts.reason } : {}),
|
|
@@ -118,6 +123,7 @@ export function buildCollectorHeartbeat(options) {
|
|
|
118
123
|
...(typeof options.facts.sessionsPendingUpload === "number"
|
|
119
124
|
? { sessions_pending_upload: options.facts.sessionsPendingUpload }
|
|
120
125
|
: {}),
|
|
126
|
+
...(options.facts.collectionScope ? { collection_scope: options.facts.collectionScope } : {}),
|
|
121
127
|
...(typeof options.facts.stagingUncommittedBytes === "number"
|
|
122
128
|
? { staging_uncommitted_bytes: options.facts.stagingUncommittedBytes }
|
|
123
129
|
: {}),
|
|
@@ -127,6 +133,7 @@ export function buildCollectorHeartbeat(options) {
|
|
|
127
133
|
...(options.facts.disk ? { disk: options.facts.disk } : {}),
|
|
128
134
|
...(options.memoryInstall ? { memory_install: options.memoryInstall } : {}),
|
|
129
135
|
...(options.setupReceipt ? { setup_receipt: options.setupReceipt } : {}),
|
|
136
|
+
...(options.captureOptOuts ? { capture_opt_outs: options.captureOptOuts } : {}),
|
|
130
137
|
};
|
|
131
138
|
}
|
|
132
139
|
/**
|
|
@@ -280,9 +287,13 @@ async function sendCollectorHeartbeatLocked(options) {
|
|
|
280
287
|
}).catch(() => ({ receipt: null, reason: "receipt_read_threw" }));
|
|
281
288
|
const setupReceipt = await readCachedSetupReceipt(options.homeDir, options.now ?? new Date());
|
|
282
289
|
logSetupReceiptCarried(setupReceipt);
|
|
290
|
+
// BLI-23478: a drawer-labelled folder's name and path hash never travel; only its shelf, counted.
|
|
291
|
+
const shelfRoots = await teamRootsForHeartbeat(options.roots, options.homeDir, options.io.fetch);
|
|
283
292
|
const heartbeat = buildCollectorHeartbeat({
|
|
284
|
-
roots:
|
|
293
|
+
roots: shelfRoots.team,
|
|
294
|
+
shelvedRoots: shelfRoots.shelved,
|
|
285
295
|
facts: options.facts,
|
|
296
|
+
captureOptOuts: await readCaptureOptOuts(options.homeDir),
|
|
286
297
|
setupReceipt: setupReceipt ? { ...setupReceipt, agent_setup: await collectAgentSetup(options.homeDir ?? os.homedir()) } : null,
|
|
287
298
|
...(options.now ? { now: options.now } : {}),
|
|
288
299
|
memoryInstall: memory.receipt,
|
|
@@ -144,6 +144,9 @@ async function deliverPendingInstallEvents(options, paths, deviceToken) {
|
|
|
144
144
|
if (receipt.imageHandlingMode !== undefined && options.observed) {
|
|
145
145
|
options.observed.imageHandlingMode = receipt.imageHandlingMode;
|
|
146
146
|
}
|
|
147
|
+
if (receipt.collectionScope !== undefined && options.observed) {
|
|
148
|
+
options.observed.collectionScope = receipt.collectionScope;
|
|
149
|
+
}
|
|
147
150
|
delivered += 1;
|
|
148
151
|
})));
|
|
149
152
|
}
|
|
@@ -215,6 +218,7 @@ async function readReceiptReply(response) {
|
|
|
215
218
|
? receipt.min_cli_version.trim()
|
|
216
219
|
: null,
|
|
217
220
|
imageHandlingMode: receipt?.image_handling_mode,
|
|
221
|
+
collectionScope: receipt?.collection_scope,
|
|
218
222
|
};
|
|
219
223
|
}
|
|
220
224
|
async function keepFailedInstallEventEntry(paths, entry, error, failures) {
|
|
@@ -95,6 +95,7 @@ const DOCTOR_ALLOWED_FLAGS = [
|
|
|
95
95
|
"--check",
|
|
96
96
|
"--no-repair",
|
|
97
97
|
"--no-usage-telemetry",
|
|
98
|
+
"--no-memory",
|
|
98
99
|
"--lock-wait",
|
|
99
100
|
"--backfill-budget",
|
|
100
101
|
"--json",
|
|
@@ -141,6 +142,7 @@ export function parseDoctorArgs(alias, args) {
|
|
|
141
142
|
return {
|
|
142
143
|
kind: "doctor",
|
|
143
144
|
noUsageTelemetry: values.booleans.has("--no-usage-telemetry"),
|
|
145
|
+
noMemory: values.booleans.has("--no-memory"),
|
|
144
146
|
checkOnly: values.booleans.has("--check") || values.booleans.has("--no-repair"),
|
|
145
147
|
// Absent stays absent (BLI-11101): `doctor-budget.ts` owns the defaults,
|
|
146
148
|
// and `--json` without either flag runs under one bounded budget.
|
|
@@ -168,23 +168,9 @@ export function parseAgentRulesArgs(args) {
|
|
|
168
168
|
json: values.booleans.has("--json"),
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* `cockpit memory` is now SIX verbs behind one noun: the two that configure
|
|
175
|
-
* this machine (`install`, `status`), the experience log (`log`), and the four
|
|
176
|
-
* that read and write the memory store itself (`search`, `save`, `update`,
|
|
177
|
-
* `forget`, parsed next door in `local-args-tower-memory.ts`).
|
|
178
|
-
*
|
|
179
|
-
* **A bare `cockpit memory` prints the verb list; it no longer installs.**
|
|
180
|
-
* BLI-3580 made `install` the default because the command is run unasked by
|
|
181
|
-
* `doctor` and by the sync tick, but those callers build
|
|
182
|
-
* `{ kind: "memory", action: "install" }` in code (`doctor-registration.ts`,
|
|
183
|
-
* `sync-followups-memory.ts`, `onboard-completion.ts`) and never pass through
|
|
184
|
-
* this parser. The only caller the default ever served was a PERSON typing the
|
|
185
|
-
* noun to find out what it does, and what they got was an MCP server and three
|
|
186
|
-
* hooks registered on their machine. Installing stays one word away.
|
|
187
|
-
*/
|
|
171
|
+
const MEMORY_MACHINE_ACTIONS = new Set([
|
|
172
|
+
"install", "uninstall", "on", "off", "status", "help",
|
|
173
|
+
]);
|
|
188
174
|
export function parseMemoryArgs(args) {
|
|
189
175
|
// The verb word first, always: reading it off `args[0]` rather than "the
|
|
190
176
|
// first token without a leading dash" is what keeps `cockpit memory --home
|
|
@@ -214,16 +200,16 @@ export function parseMemoryArgs(args) {
|
|
|
214
200
|
if (values.flags.has("--store") || values.booleans.has("--reason-stdin"))
|
|
215
201
|
throw new Error("--store and --reason-stdin require memory log");
|
|
216
202
|
if (values.positionals.length > 1) {
|
|
217
|
-
throw new Error("memory accepts at most one action (install|status).");
|
|
203
|
+
throw new Error("memory accepts at most one action (on|off|install|uninstall|status).");
|
|
218
204
|
}
|
|
219
205
|
// BLI-4047: no positional means "tell me what this noun does", not "install".
|
|
220
206
|
const action = values.positionals[0] ?? "help";
|
|
221
|
-
if (action
|
|
222
|
-
throw new Error("memory action must be install, status, help, log, search, save, update, or forget.");
|
|
207
|
+
if (!MEMORY_MACHINE_ACTIONS.has(action)) {
|
|
208
|
+
throw new Error("memory action must be on, off, install, uninstall, status, help, log, search, save, update, or forget.");
|
|
223
209
|
}
|
|
224
210
|
return {
|
|
225
211
|
kind: "memory",
|
|
226
|
-
action,
|
|
212
|
+
action: action,
|
|
227
213
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
228
214
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
229
215
|
dryRun: values.booleans.has("--dry-run"),
|
|
@@ -20,6 +20,7 @@ import { parseSkillsArgs } from "./skills.js";
|
|
|
20
20
|
import { parseSpendArgs } from "./spend.js";
|
|
21
21
|
import { parsePushArgs } from "./push.js";
|
|
22
22
|
import { parseRootsArgs } from "./roots.js";
|
|
23
|
+
import { parseShelfArgs } from "./shelf.js";
|
|
23
24
|
import { parseSlackPrivacyArgs } from "./slack-privacy-args.js";
|
|
24
25
|
import { parseMilestoneArgs } from "./local-args-tower-milestone.js";
|
|
25
26
|
import { SURFACE_ALIASES } from "./local-help.js";
|
|
@@ -42,6 +43,7 @@ export { SCOUT_MIN_PREFIX_LENGTH, SLACK_WORKSPACE_KEYS, WORKBOOK_MIN_WIDTH, ISSU
|
|
|
42
43
|
*/
|
|
43
44
|
const COMMAND_PARSERS = new Map([
|
|
44
45
|
["roots", (_command, rest) => parseRootsArgs(rest)],
|
|
46
|
+
["shelf", (_command, rest) => parseShelfArgs(rest)],
|
|
45
47
|
["onboard", (_command, rest) => parseOnboardArgs(rest)],
|
|
46
48
|
["update", (command, rest) => parseUpdateArgs(command, rest)],
|
|
47
49
|
["upgrade", (command, rest) => parseUpdateArgs(command, rest)],
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* binding a ticket from a scratch folder to every approved root on the machine
|
|
17
17
|
* is far wider than the operator asked for, so it keeps the old refusal.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { readSourceSwitches } from "../source-opt-out.js";
|
|
20
20
|
import { writeLine } from "./cli-io.js";
|
|
21
21
|
import { resolveDiscoveryLimits, saveDiscoveryLimits, } from "../discovery-limits.js";
|
|
22
22
|
import { getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
@@ -196,7 +196,7 @@ export async function scanWorkDirectories(homeDir, persist = true) {
|
|
|
196
196
|
return null; throw error; });
|
|
197
197
|
const now = new Date();
|
|
198
198
|
const options = { worktrees: [], collectionRoots: config?.default_repo_paths ?? [], now, sinceMinutes: ROOT_WORK_SCAN_WINDOW_DAYS * 24 * 60, limit: Number.MAX_SAFE_INTEGER };
|
|
199
|
-
const switches =
|
|
199
|
+
const switches = await readSourceSwitches(config, paths, "discovery");
|
|
200
200
|
if (!switches.codex)
|
|
201
201
|
console.error("[discovery] codex scan withheld by operator opt-out", JSON.stringify({ reason: switches.codexReason }));
|
|
202
202
|
const codex = switches.codex ? await scanAndAttributeCodexSessions({ ...options, sessionsDirs: defaultCodexSessionDirs(homeDir) }) : null;
|
|
@@ -412,7 +412,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
412
412
|
[
|
|
413
413
|
"memory",
|
|
414
414
|
[
|
|
415
|
-
"Usage: cockpit memory [map|knowledge|rules|search|save|update|forget|private|public|profile|consolidation|chain|source|exam|requeue|install|status|log] [flags]",
|
|
415
|
+
"Usage: cockpit memory [map|knowledge|rules|search|save|update|forget|private|public|profile|consolidation|chain|source|exam|requeue|on|off|install|uninstall|status|log] [flags]",
|
|
416
416
|
"",
|
|
417
417
|
" map [--json] — projects, topics, memory counts and importance from the shared map.",
|
|
418
418
|
" knowledge [--person <name>] [--project <name>] [--by-project] [--limit <n>] [--json] — ranked project ideas.",
|
|
@@ -469,6 +469,11 @@ export const TOWER_COMMAND_HELP = [
|
|
|
469
469
|
"Idempotent: it merges with what is already there, never duplicates its own",
|
|
470
470
|
"entries, and reads the stored config back before reporting success.",
|
|
471
471
|
"`cockpit doctor` runs it, and the sync tick re-runs it at most once a day.",
|
|
472
|
+
"off (alias uninstall) turns BLI Memory off on THIS machine: it records the choice, removes",
|
|
473
|
+
"our hooks and MCP entry from both hosts (your other hooks stay), and doctor, onboard,",
|
|
474
|
+
"update and the sync tick never add them back; the Stop and PreCompact hooks send nothing.",
|
|
475
|
+
"on (alias install) records the opposite choice and registers them again. Each prints",
|
|
476
|
+
"one line saying what changed and how to undo it. See docs/architecture/agent-memory.md.",
|
|
472
477
|
"See docs/runbooks/bli-memory-install.md.",
|
|
473
478
|
"log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
|
|
474
479
|
"Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
|
|
@@ -26,7 +26,18 @@ const COMMAND_HELP = new Map([
|
|
|
26
26
|
["skills", ["Usage: cockpit skills library [--weeks <1-52>] [--json] | cockpit skills refresh [--weeks <1-104>] [--json]", "The Skill Library: every agent skill and command our GitHub repos define (name, description, repo, path, versions) with weekly use by person, from the sessions Tower already collects. Uses that match no scanned skill are listed as unmatched, never dropped. The first line is the coverage: repos scanned, skills, invocations matched of total. A member sees their own usage; a super_admin sees everyone's except an owner's. refresh scans the repos and recomputes the weekly totals now (super_admin only; a stale library also refreshes itself when read). MCP twins: skills_library, skills_refresh. Web: TEAM > Skills."]],
|
|
27
27
|
["push", ["Usage: cockpit push status | test [--dashboard-url <url>] [--json]", "Phone notifications. status lists your saved devices and whether the server has its push keys; test sends a test push to every saved device. Turn a phone on from Settings on the phone."]],
|
|
28
28
|
["spend", ["Usage: cockpit spend memory [--days 1|7|30] [--dashboard-url <url>] [--json]", "Spend by memory operation (write, recall, dream, update_forget, backfill, label, exam, predictor, people, who, intentions, salience, chat, other), ranked top first with list and real dollars, share, trend against the previous window, and how much of the real OpenAI bill the ledger explains. What it does not explain is shown as untagged. A super_admin reads it."]],
|
|
29
|
-
["roots", [
|
|
29
|
+
["roots", [
|
|
30
|
+
"Usage: cockpit roots add <dir> [--allow-home-root] [--json] | label <dir> team|drawer [--json] | list [--json]",
|
|
31
|
+
"add approves a work directory, saves an approval receipt, and adds it to collection. Run cockpit doctor afterward.",
|
|
32
|
+
"label (founders' laptops): team sends that folder's sessions and memories to Tower as today; drawer keeps them on this laptop and sends only bare numbers. safe is refused until the safe is live.",
|
|
33
|
+
"list shows each approved or labelled folder and the shelf it resolves to.",
|
|
34
|
+
]],
|
|
35
|
+
["shelf", [
|
|
36
|
+
"Usage: cockpit shelf status | default team|drawer | metrics --allow <day,tool,model,spawn|none> | drawer purge --older-than <days> [--json]",
|
|
37
|
+
"A founder's shelves on THIS laptop. status: Tower's founder lock, the default, and what is in the drawer here (not backed up).",
|
|
38
|
+
"default: where work outside every labelled folder goes. metrics: the opt-in categories a drawer session's bare record may carry (all off by default).",
|
|
39
|
+
"drawer purge: the only way drawer items are removed.",
|
|
40
|
+
]],
|
|
30
41
|
// BLI-3709: the Tower nouns live next door, split along the same seam the
|
|
31
42
|
// argument parsers already use — see `local-help-commands-tower.ts`.
|
|
32
43
|
...TOWER_COMMAND_HELP,
|
|
@@ -92,12 +103,14 @@ const COMMAND_HELP = new Map([
|
|
|
92
103
|
[
|
|
93
104
|
"doctor",
|
|
94
105
|
[
|
|
95
|
-
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
106
|
+
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--no-memory] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
96
107
|
"",
|
|
97
108
|
"Diagnose, repair, and verify this machine.",
|
|
98
109
|
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
99
110
|
"roots-cover-work is informational: it names how many sessions are inside vs. outside saved roots (all already uploaded, outside ones labelled) and gives cockpit roots add commands for attributing a folder's sessions to a repo (outside-root transcripts upload either way). It never needs a fix.",
|
|
100
111
|
"--no-usage-telemetry switches off Claude Code and Codex usage export, including later doctor runs.",
|
|
112
|
+
"--no-memory turns BLI Memory off on this machine (same as `cockpit memory off`), including later runs and the background sync.",
|
|
113
|
+
"In a terminal, the first doctor or onboard asks once whether BLI Memory may save your sessions; `cockpit memory on|off` changes the answer.",
|
|
101
114
|
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
102
115
|
"--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
|
|
103
116
|
"Doctor never waits for the background sync: a running sync is reported as running (with how long ago it started) and doctor finishes; a sync that stopped responding or made no progress for 15 minutes is ended and doctor carries on. --lock-wait is accepted and changes nothing. Progress prints at most every 30 seconds and says what changed.",
|
|
@@ -36,6 +36,7 @@ export const SURFACE_ALIASES = new Map([
|
|
|
36
36
|
]);
|
|
37
37
|
export const rootCommandNames = new Set([
|
|
38
38
|
"roots",
|
|
39
|
+
"shelf",
|
|
39
40
|
"onboard",
|
|
40
41
|
"update",
|
|
41
42
|
"upgrade",
|
|
@@ -110,7 +111,8 @@ export const rootCommandNames = new Set([
|
|
|
110
111
|
export const publicCommandNames = new Set([...rootCommandNames].filter((name) => name !== "do-everything" && name !== "fix"));
|
|
111
112
|
/** The one-screen summary `cockpit --help` prints: one line per command, then the shared footnotes. */
|
|
112
113
|
const COMMAND_SUMMARY_LINES = [
|
|
113
|
-
" cockpit roots add <dir> [--allow-home-root] [--json]",
|
|
114
|
+
" cockpit roots add <dir> [--allow-home-root] [--json] | label <dir> team|drawer | list",
|
|
115
|
+
" cockpit shelf status | default team|drawer | metrics --allow <categories|none> | drawer purge --older-than <days> [--json]",
|
|
114
116
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
115
117
|
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
116
118
|
" cockpit upgrade [same flags as update]",
|
|
@@ -148,7 +150,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
148
150
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
149
151
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
150
152
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
151
|
-
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
153
|
+
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|on|off|install|uninstall|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
152
154
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
153
155
|
" cockpit sources check [--reconcile] [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
154
156
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>|trash|versions <id|slug>|restore <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
package/dist/commands/local.js
CHANGED
|
@@ -9,6 +9,8 @@ export { rootCommandNames, localCommandHelp, SURFACE_ALIASES, unknownCommandJson
|
|
|
9
9
|
/** Commands that act on THIS machine; the rest talk to Tower. */
|
|
10
10
|
const MACHINE_COMMAND_KINDS = [
|
|
11
11
|
"roots",
|
|
12
|
+
// BLI-23478 (three shelves): acts on this laptop only.
|
|
13
|
+
"shelf",
|
|
12
14
|
"install",
|
|
13
15
|
"onboard",
|
|
14
16
|
"update",
|
|
@@ -81,6 +83,7 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
81
83
|
async function runMachineCommand(command, io) {
|
|
82
84
|
switch (command.kind) {
|
|
83
85
|
case "roots": return await (await import("./roots.js")).runRoots(command, io);
|
|
86
|
+
case "shelf": return await (await import("./shelf.js")).runShelf(command, io);
|
|
84
87
|
case "install": return await (await import("./install-update.js")).runInstall(command, io);
|
|
85
88
|
case "onboard": return await (await import("./onboard.js")).runOnboard(command, io);
|
|
86
89
|
case "update": return await (await import("./install-update.js")).runUpdate(command, io);
|
|
@@ -109,6 +112,10 @@ async function runMachineCommand(command, io) {
|
|
|
109
112
|
// caller asks for it by name in code.
|
|
110
113
|
if (command.action === "help")
|
|
111
114
|
return (await import("./memory-store.js")).runMemoryVerbList(io, command.json);
|
|
115
|
+
// BLI-22640: the per-machine switch. `install`/`status` keep their page.
|
|
116
|
+
if (command.action === "on" || command.action === "off" || command.action === "uninstall") {
|
|
117
|
+
return await (await import("./memory-switch.js")).runMemorySwitch({ ...command, action: command.action }, io);
|
|
118
|
+
}
|
|
112
119
|
return await (await import("./memory-install.js")).runMemoryInstall(command, io);
|
|
113
120
|
case "memory-store": return await (await import("./memory-store.js")).runMemoryStore(command, io);
|
|
114
121
|
case "clean": return await (await import("./clean.js")).runClean(command, io);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-time BLI Memory question (BLI-22640).
|
|
3
|
+
*
|
|
4
|
+
* An interactive `cockpit doctor` or `cockpit onboard` on a machine that has
|
|
5
|
+
* never chosen asks once, and the answer becomes this machine's choice
|
|
6
|
+
* (`memory-capture-choice.ts`). Once answered, never asked again: `cockpit
|
|
7
|
+
* memory on|off` is how a person changes their mind.
|
|
8
|
+
*
|
|
9
|
+
* Asked only when a person is plainly there: stdin AND stdout are terminals,
|
|
10
|
+
* `--json` is off, and this is not the scheduler's tick, a dry run or a
|
|
11
|
+
* `--check`. launchd, Task Scheduler, CI and every piped run keep today's
|
|
12
|
+
* default (on) without a question, and say why in the log.
|
|
13
|
+
*
|
|
14
|
+
* Founders only (Jeetesh's review of 2026-10-25): the question is shown only
|
|
15
|
+
* when Tower answers, live, that this machine's person is a founder. Anybody
|
|
16
|
+
* else never sees it and keeps BLI Memory on; the log names why
|
|
17
|
+
* (`not_founder`, or `founder_check_unavailable` when Tower could not be
|
|
18
|
+
* asked). A cached "not a founder" skips the question without asking Tower.
|
|
19
|
+
*
|
|
20
|
+
* An answer that is neither yes nor no is asked once more; a second unclear
|
|
21
|
+
* answer records NOTHING, so the default holds and the next interactive run
|
|
22
|
+
* asks again. A choice this important is never inferred from a typo.
|
|
23
|
+
*/
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import { readLine, writeLine } from "./cli-io.js";
|
|
26
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
27
|
+
import { readMemoryCaptureChoice, writeMemoryCaptureChoice } from "../memory-capture-choice.js";
|
|
28
|
+
import { mayRecordOff } from "../founder-standing.js";
|
|
29
|
+
import { parseYesNo } from "../yes-no.js";
|
|
30
|
+
export const MEMORY_CHOICE_QUESTION = "BLI Memory: save what you work on so agents remember it across sessions? [Y/n] ";
|
|
31
|
+
export const MEMORY_CHOICE_EXPLAINER = "Answering n keeps BLI Memory's hooks off on this machine, so nothing from your sessions is sent to memory; `cockpit memory on` or `cockpit memory off` changes it later.";
|
|
32
|
+
/** Why this run may not ask, or null when it may. */
|
|
33
|
+
export function memoryChoicePromptBlocker(io, options) {
|
|
34
|
+
if (options.json)
|
|
35
|
+
return "json";
|
|
36
|
+
if (options.automaticTick)
|
|
37
|
+
return "scheduled_tick";
|
|
38
|
+
if (options.dryRun || options.checkOnly)
|
|
39
|
+
return "diagnose_only";
|
|
40
|
+
if (((io.env ?? process.env)["CI"] ?? "") !== "")
|
|
41
|
+
return "ci";
|
|
42
|
+
const stdinTty = Boolean(io.stdin.isTTY);
|
|
43
|
+
const stdoutTty = Boolean(io.stdout.isTTY);
|
|
44
|
+
return stdinTty && stdoutTty ? null : "not_a_tty";
|
|
45
|
+
}
|
|
46
|
+
export async function askMemoryChoiceOnce(io, options) {
|
|
47
|
+
const blocker = memoryChoicePromptBlocker(io, options);
|
|
48
|
+
if (blocker) {
|
|
49
|
+
console.error("[memory-capture] question not asked; the default stands", JSON.stringify({ reason: blocker }));
|
|
50
|
+
return "not_interactive";
|
|
51
|
+
}
|
|
52
|
+
const paths = getCollectorRuntimePaths(options.homeDir ?? os.homedir());
|
|
53
|
+
const current = await readMemoryCaptureChoice(paths);
|
|
54
|
+
if (current.state !== "unchosen") {
|
|
55
|
+
console.error("[memory-capture] question not asked; already answered", JSON.stringify({ reason: "already_chosen", state: current.state }));
|
|
56
|
+
return "already_chosen";
|
|
57
|
+
}
|
|
58
|
+
const founder = await founderMayBeAsked(io, options);
|
|
59
|
+
if (founder !== null)
|
|
60
|
+
return founder;
|
|
61
|
+
writeLine(io.stdout, MEMORY_CHOICE_EXPLAINER);
|
|
62
|
+
let verdict = parseYesNo(await readLine(io, MEMORY_CHOICE_QUESTION), { emptyIsYes: true });
|
|
63
|
+
if (verdict === "unclear") {
|
|
64
|
+
verdict = parseYesNo(await readLine(io, `Please answer y or n. ${MEMORY_CHOICE_QUESTION}`), { emptyIsYes: true });
|
|
65
|
+
}
|
|
66
|
+
if (verdict === "unclear") {
|
|
67
|
+
console.error("[memory-capture] answer unclear twice; nothing recorded", JSON.stringify({ reason: "unclear_answer" }));
|
|
68
|
+
writeLine(io.stdout, "No answer recorded; BLI Memory stays on for now, and the next `cockpit doctor` asks again.");
|
|
69
|
+
return "unclear_answer";
|
|
70
|
+
}
|
|
71
|
+
const on = verdict === "yes";
|
|
72
|
+
try {
|
|
73
|
+
await writeMemoryCaptureChoice(paths, on, "prompt");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error("[memory-capture] answer could not be saved", JSON.stringify({ reason: "choice_write_failed", error_name: error instanceof Error ? error.name : typeof error }));
|
|
77
|
+
writeLine(io.stdout, "Your answer could not be saved; run `cockpit memory " + (on ? "on" : "off") + "` to record it.");
|
|
78
|
+
return "choice_write_failed";
|
|
79
|
+
}
|
|
80
|
+
writeLine(io.stdout, on
|
|
81
|
+
? "BLI Memory is on for this machine. Undo: cockpit memory off"
|
|
82
|
+
: "BLI Memory stays off on this machine; its hooks are removed and not added back. Undo: cockpit memory on");
|
|
83
|
+
return on ? "chose_on" : "chose_off";
|
|
84
|
+
}
|
|
85
|
+
/** Null when the question may be shown; otherwise why not. Logs both branches. */
|
|
86
|
+
async function founderMayBeAsked(io, options) {
|
|
87
|
+
// Only Tower's live "founder" is enough to ask, because the answer can
|
|
88
|
+
// record an off; the saved copy decides nothing (BLI-22640).
|
|
89
|
+
const permission = await mayRecordOff({
|
|
90
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
91
|
+
...(options.dashboardUrl ? { dashboardUrl: options.dashboardUrl } : {}),
|
|
92
|
+
fetch: io.fetch,
|
|
93
|
+
door: "prompt",
|
|
94
|
+
});
|
|
95
|
+
if (permission.allowed)
|
|
96
|
+
return null;
|
|
97
|
+
const reason = permission.reason === "memory_off_founders_only" ? "not_founder" : "founder_check_unavailable";
|
|
98
|
+
console.error("[memory-capture] question not asked; founders only", JSON.stringify({ reason, source: "live", detail: permission.detail }));
|
|
99
|
+
return reason;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What `cockpit doctor` does with the choice before its first row: honour
|
|
103
|
+
* `--no-memory`, otherwise ask once when a person is there. Never throws; a
|
|
104
|
+
* doctor run is never lost to this question.
|
|
105
|
+
*/
|
|
106
|
+
export async function settleDoctorMemoryChoice(command, io) {
|
|
107
|
+
try {
|
|
108
|
+
if (command.noMemory && !command.dryRun && !command.checkOnly) {
|
|
109
|
+
// Founders only, on Tower's live answer; anybody else is told why and doctor carries on.
|
|
110
|
+
const permission = await mayRecordOff({
|
|
111
|
+
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
112
|
+
...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}),
|
|
113
|
+
fetch: io.fetch,
|
|
114
|
+
door: "doctor_no_memory",
|
|
115
|
+
});
|
|
116
|
+
if (!permission.allowed) {
|
|
117
|
+
// stdout belongs to `--json`; the refusal is on stderr there, by name.
|
|
118
|
+
writeLine(command.json ? io.stderr : io.stdout, `--no-memory: ${permission.line}`);
|
|
119
|
+
return "flag_refused";
|
|
120
|
+
}
|
|
121
|
+
await writeMemoryCaptureChoice(getCollectorRuntimePaths(command.homeDir ?? os.homedir()), false, "doctor_flag");
|
|
122
|
+
return "flag_off";
|
|
123
|
+
}
|
|
124
|
+
return await askMemoryChoiceOnce(io, command);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error("[memory-capture] choice step failed; doctor carries on with what is recorded", JSON.stringify({ reason: "choice_step_threw", error_name: error instanceof Error ? error.name : typeof error }));
|
|
128
|
+
return "choice_write_failed";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -200,8 +200,10 @@ export function applyHookGroups(root, hookSpecs) {
|
|
|
200
200
|
? hooks[spec.event].slice()
|
|
201
201
|
: [];
|
|
202
202
|
// Somebody else's Stop hook survives untouched; only OUR entries — matched
|
|
203
|
-
// by bin name plus `hook`, wherever the bin ended up — are replaced.
|
|
204
|
-
|
|
203
|
+
// by bin name plus `hook`, wherever the bin ended up — are replaced. A group
|
|
204
|
+
// a person shares with ours keeps their commands (BLI-22640, the Codex
|
|
205
|
+
// review point): only our command comes out of it.
|
|
206
|
+
const survivors = stripOurHooks(groups).groups;
|
|
205
207
|
survivors.push({
|
|
206
208
|
hooks: [
|
|
207
209
|
{
|
|
@@ -268,9 +270,54 @@ export function ourHookCount(root) {
|
|
|
268
270
|
}
|
|
269
271
|
return count;
|
|
270
272
|
}
|
|
271
|
-
|
|
273
|
+
/** Exported for `memory-uninstall.ts` (BLI-22640): the off-switch removes exactly what the installer would replace. */
|
|
274
|
+
export function groupIsOurs(group) {
|
|
272
275
|
return hookCommands(group).some(isMemoryHookCommand);
|
|
273
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* BLI-22640 (the Codex review point on PR 2615). Takes OUR commands out of
|
|
279
|
+
* each hook group and keeps everything else: a group a person shares with
|
|
280
|
+
* ours keeps their own commands (and its matcher and any other key), and only
|
|
281
|
+
* a group left with no hooks at all is dropped. A group that holds none of
|
|
282
|
+
* ours comes back as the same object, byte for byte once serialised.
|
|
283
|
+
*
|
|
284
|
+
* `removed` counts our commands taken out. Used by the installer (before it
|
|
285
|
+
* appends our fresh group) and by `cockpit memory off`.
|
|
286
|
+
*/
|
|
287
|
+
export function stripOurHooks(groups) {
|
|
288
|
+
const kept = [];
|
|
289
|
+
let removed = 0;
|
|
290
|
+
for (const group of groups) {
|
|
291
|
+
const record = asRecord(group);
|
|
292
|
+
const entries = record && Array.isArray(record["hooks"]) ? record["hooks"] : null;
|
|
293
|
+
if (!record || !entries) {
|
|
294
|
+
kept.push(group);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const others = entries.filter((entry) => {
|
|
298
|
+
const command = asRecord(entry)?.["command"];
|
|
299
|
+
return !(typeof command === "string" && isMemoryHookCommand(command));
|
|
300
|
+
});
|
|
301
|
+
removed += entries.length - others.length;
|
|
302
|
+
if (others.length === entries.length)
|
|
303
|
+
kept.push(group);
|
|
304
|
+
else if (others.length > 0)
|
|
305
|
+
kept.push({ ...record, hooks: others });
|
|
306
|
+
}
|
|
307
|
+
return { groups: kept, removed };
|
|
308
|
+
}
|
|
309
|
+
/** How many of OUR hook commands a settings or hooks.json root holds, across every event and group. */
|
|
310
|
+
export function ourHookCommandCount(root) {
|
|
311
|
+
const hooks = asRecord(root["hooks"]);
|
|
312
|
+
if (!hooks)
|
|
313
|
+
return 0;
|
|
314
|
+
let count = 0;
|
|
315
|
+
for (const groups of Object.values(hooks)) {
|
|
316
|
+
if (Array.isArray(groups))
|
|
317
|
+
count += stripOurHooks(groups).removed;
|
|
318
|
+
}
|
|
319
|
+
return count;
|
|
320
|
+
}
|
|
274
321
|
function hookCommands(group) {
|
|
275
322
|
const record = asRecord(group);
|
|
276
323
|
if (!record || !Array.isArray(record["hooks"]))
|
|
@@ -310,7 +357,7 @@ function sameStringArray(value, expected) {
|
|
|
310
357
|
* itself, so this is cosmetic — but a diff that reindents a person's whole
|
|
311
358
|
* configuration reads like damage even when it is not.
|
|
312
359
|
*/
|
|
313
|
-
function serializeJson(root, previous) {
|
|
360
|
+
export function serializeJson(root, previous) {
|
|
314
361
|
const indent = detectIndent(previous);
|
|
315
362
|
return `${JSON.stringify(root, null, indent)}\n`;
|
|
316
363
|
}
|