@bli-cockpit/cli 0.2.195 → 0.2.197
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/doctor-report.js +1 -0
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor-usage-telemetry.js +96 -0
- package/dist/commands/doctor.js +4 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-help-commands.js +2 -1
- package/dist/commands/memory-install-files.js +9 -6
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup.js +10 -2
- package/dist/commands/usage-telemetry-toml.js +116 -0
- package/package.json +4 -4
|
@@ -169,6 +169,7 @@ export function doctorNeedsPerson(rows, receipt) {
|
|
|
169
169
|
"roots-cover-work": "cockpit doctor",
|
|
170
170
|
"single-install": "Run npm uninstall -g @bli-cockpit/cli with the Node installation that owns the extra CLI listed above.",
|
|
171
171
|
"autostart-alive": "cockpit doctor",
|
|
172
|
+
"usage-telemetry": "cockpit doctor",
|
|
172
173
|
"memory-registered": "cockpit doctor",
|
|
173
174
|
"agent-rules": "cockpit doctor",
|
|
174
175
|
"mcp-answers": "cockpit doctor",
|
|
@@ -73,6 +73,8 @@ function parseNpmVersion(stdout) {
|
|
|
73
73
|
}
|
|
74
74
|
export function reexecDoctor(command, io) {
|
|
75
75
|
const args = ["doctor"];
|
|
76
|
+
if (command.noUsageTelemetry)
|
|
77
|
+
args.push("--no-usage-telemetry");
|
|
76
78
|
if (command.homeDir)
|
|
77
79
|
args.push("--home", command.homeDir);
|
|
78
80
|
if (command.backfillBudgetSeconds)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getCollectorRuntimePaths, readLocalCollectorSessionFile } from "../local-state.js";
|
|
4
|
+
import { claudeSettingsFile } from "./memory-install-claude.js";
|
|
5
|
+
import { codexConfigFile } from "./tower-mcp-codex.js";
|
|
6
|
+
import { defaultMemoryFileIo } from "./memory-install-files.js";
|
|
7
|
+
import { mergeUsageToml } from "./usage-telemetry-toml.js";
|
|
8
|
+
import { fail, needsFix, ok } from "./doctor-report.js";
|
|
9
|
+
export function mergeClaudeUsageSettings(raw, origin, token, off) {
|
|
10
|
+
const root = JSON.parse(raw || "{}");
|
|
11
|
+
if (!root || typeof root !== "object" || Array.isArray(root) || (root.env !== undefined && (!root.env || typeof root.env !== "object" || Array.isArray(root.env))))
|
|
12
|
+
throw new Error("config_unreadable");
|
|
13
|
+
root.env = { ...root.env, CLAUDE_CODE_ENABLE_TELEMETRY: off ? "0" : "1",
|
|
14
|
+
OTEL_METRICS_EXPORTER: off ? "none" : "otlp", OTEL_LOGS_EXPORTER: off ? "none" : "otlp",
|
|
15
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json", OTEL_EXPORTER_OTLP_ENDPOINT: `${new URL(origin).origin}/api/otel`,
|
|
16
|
+
OTEL_EXPORTER_OTLP_HEADERS: `Authorization=Bearer ${token}` };
|
|
17
|
+
// OTEL_LOG_USER_PROMPTS is never added or changed. Existing person keys survive.
|
|
18
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
export function mergeCodexUsageSettings(raw, origin, token, off) {
|
|
21
|
+
// Disabled exporter options remain as comments, including person-set TLS and
|
|
22
|
+
// headers. Restore them before enabling, rather than discard configuration.
|
|
23
|
+
const prefix = "# TOWER usage telemetry opt-out: ";
|
|
24
|
+
let table = "";
|
|
25
|
+
raw = raw.split("\n").flatMap(line => {
|
|
26
|
+
if (!off && line.startsWith(prefix))
|
|
27
|
+
line = line.slice(prefix.length);
|
|
28
|
+
const header = /^\s*\[([^\]]+)\]/.exec(line);
|
|
29
|
+
if (header)
|
|
30
|
+
table = header[1].replace(/["']/g, "").replace(/\s/g, "");
|
|
31
|
+
const exporterTable = /^otel\.(?:exporter|metrics_exporter)(?:\.|$)/.test(table);
|
|
32
|
+
const exporterAssignment = table === "otel" && /^\s*(?:exporter|metrics_exporter)\s*=/.test(line);
|
|
33
|
+
if (off && (exporterTable || exporterAssignment) && !/^\s*(?:exporter|metrics_exporter)\s*=\s*["']none["']/.test(line))
|
|
34
|
+
return [prefix + line];
|
|
35
|
+
if (!off && exporterAssignment && /^\s*(?:exporter|metrics_exporter)\s*=\s*["']none["']/.test(line))
|
|
36
|
+
return [];
|
|
37
|
+
return [line];
|
|
38
|
+
}).join("\n");
|
|
39
|
+
const exporter = off ? '"none"' : { "otlp-http": {
|
|
40
|
+
endpoint: JSON.stringify(`${new URL(origin).origin}/api/otel/v1/logs`), protocol: '"json"', headers: { Authorization: JSON.stringify(`Bearer ${token}`) },
|
|
41
|
+
} };
|
|
42
|
+
const metrics = off ? '"none"' : { "otlp-http": {
|
|
43
|
+
endpoint: JSON.stringify(`${new URL(origin).origin}/api/otel/v1/metrics`), protocol: '"json"', headers: { Authorization: JSON.stringify(`Bearer ${token}`) },
|
|
44
|
+
} };
|
|
45
|
+
return mergeUsageToml(raw, ["otel"], { log_user_prompt: "false", exporter, metrics_exporter: metrics });
|
|
46
|
+
}
|
|
47
|
+
export async function configureUsageTelemetry(options) {
|
|
48
|
+
const marker = path.join(getCollectorRuntimePaths(options.homeDir).state_dir, "usage-telemetry-opt-out.json");
|
|
49
|
+
const persisted = await options.io.readText(marker) !== null;
|
|
50
|
+
const off = persisted || Boolean(options.optOut);
|
|
51
|
+
let changed = Boolean(options.optOut && !persisted);
|
|
52
|
+
const files = [
|
|
53
|
+
[claudeSettingsFile(options.homeDir), mergeClaudeUsageSettings],
|
|
54
|
+
[codexConfigFile(options.homeDir), mergeCodexUsageSettings],
|
|
55
|
+
];
|
|
56
|
+
const updates = [];
|
|
57
|
+
for (const [file, merge] of files) {
|
|
58
|
+
const raw = await options.io.readText(file) ?? "";
|
|
59
|
+
const next = merge(raw, options.origin, options.token, off);
|
|
60
|
+
updates.push({ file, merge, raw, next });
|
|
61
|
+
}
|
|
62
|
+
if (options.optOut && !persisted && !options.checkOnly)
|
|
63
|
+
await options.io.writeText(marker, '{"opt_out":true}\n');
|
|
64
|
+
for (const { file, merge, raw, next } of updates) {
|
|
65
|
+
if (raw === next)
|
|
66
|
+
continue;
|
|
67
|
+
changed = true;
|
|
68
|
+
if (options.checkOnly)
|
|
69
|
+
continue;
|
|
70
|
+
await options.io.writeText(file, next);
|
|
71
|
+
const stored = await options.io.readText(file);
|
|
72
|
+
if (stored === null || merge(stored, options.origin, options.token, off) !== stored)
|
|
73
|
+
throw new Error("read_back_mismatch");
|
|
74
|
+
}
|
|
75
|
+
console.error("[usage telemetry] configured", JSON.stringify({ reason: off ? "explicit_opt_out" : changed ? "settings_changed" : "already_current", check_only: Boolean(options.checkOnly) }));
|
|
76
|
+
return { off, changed };
|
|
77
|
+
}
|
|
78
|
+
async function usageTelemetryStep(context, checkOnly) {
|
|
79
|
+
try {
|
|
80
|
+
const home = context.command.homeDir ?? os.homedir();
|
|
81
|
+
const session = await readLocalCollectorSessionFile(getCollectorRuntimePaths(home));
|
|
82
|
+
if (!session?.device_token)
|
|
83
|
+
return needsFix("usage-telemetry", "device_token_missing", "usage telemetry: needs Tower sign-in");
|
|
84
|
+
const result = await configureUsageTelemetry({ homeDir: home, origin: context.command.dashboardUrl, token: session.device_token,
|
|
85
|
+
optOut: context.command.noUsageTelemetry, checkOnly, io: defaultMemoryFileIo({ mode: 0o600 }) });
|
|
86
|
+
const message = result.off ? "usage telemetry: off (explicit opt-out)" : "usage telemetry: on (claude code, codex)";
|
|
87
|
+
return checkOnly && result.changed ? needsFix("usage-telemetry", "settings_differ", message) : ok("usage-telemetry", result.off ? "explicit_opt_out" : "already_current", message);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const reason = error instanceof SyntaxError ? "config_unreadable" : error instanceof Error && ["config_unreadable", "read_back_mismatch"].includes(error.message) ? error.message : "settings_io_failed";
|
|
91
|
+
console.error("[usage telemetry] refused", JSON.stringify({ reason }));
|
|
92
|
+
return fail("usage-telemetry", reason, "usage telemetry: settings could not be safely merged");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export const checkUsageTelemetry = (context) => usageTelemetryStep(context, true);
|
|
96
|
+
export const fixUsageTelemetry = (context) => usageTelemetryStep(context, false);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkUsageTelemetry, fixUsageTelemetry } from "./doctor-usage-telemetry.js";
|
|
1
2
|
import { LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
2
3
|
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
3
4
|
import { describeError } from "../health-detail.js";
|
|
@@ -184,6 +185,7 @@ function doctorInvariants() {
|
|
|
184
185
|
check: (context) => context.deps.checkMemory(context),
|
|
185
186
|
fix: (context, _state) => context.deps.fixMemory(context),
|
|
186
187
|
},
|
|
188
|
+
{ id: "usage-telemetry", check: context => context.deps.checkUsageTelemetry(context), fix: context => context.deps.fixUsageTelemetry(context) },
|
|
187
189
|
{ id: "agent-rules", check: (context) => context.deps.checkAgentRules(context), fix: (context) => context.deps.fixAgentRules(context) },
|
|
188
190
|
// BLI-3804. Right after the registration row, because it asks the second
|
|
189
191
|
// half of the same question: `memory-registered` proves the entry exists
|
|
@@ -248,6 +250,8 @@ function defaultDoctorDeps(hooks) {
|
|
|
248
250
|
checkSingleInstall: checkSingleInstallState,
|
|
249
251
|
checkAutostart: checkAutostartState,
|
|
250
252
|
fixAutostart: fixAutostartState,
|
|
253
|
+
checkUsageTelemetry,
|
|
254
|
+
fixUsageTelemetry,
|
|
251
255
|
checkMemory: checkMemoryState,
|
|
252
256
|
fixMemory: fixMemoryState,
|
|
253
257
|
checkMcpAnswers: (context) => checkMcpAnswersState(context),
|
|
@@ -100,6 +100,7 @@ export function parseDoctorArgs(alias, args) {
|
|
|
100
100
|
"--dry-run",
|
|
101
101
|
"--check",
|
|
102
102
|
"--no-repair",
|
|
103
|
+
"--no-usage-telemetry",
|
|
103
104
|
"--lock-wait",
|
|
104
105
|
"--backfill-budget",
|
|
105
106
|
"--json",
|
|
@@ -141,6 +142,7 @@ export function parseDoctorArgs(alias, args) {
|
|
|
141
142
|
}
|
|
142
143
|
return {
|
|
143
144
|
kind: "doctor",
|
|
145
|
+
noUsageTelemetry: values.booleans.has("--no-usage-telemetry"),
|
|
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.
|
|
@@ -89,11 +89,12 @@ export function localSubcommandHelp(command) {
|
|
|
89
89
|
[
|
|
90
90
|
"doctor",
|
|
91
91
|
[
|
|
92
|
-
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
92
|
+
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
93
93
|
"",
|
|
94
94
|
"Diagnose, repair, and verify this machine.",
|
|
95
95
|
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
96
96
|
"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 widening a root's redacted transcript content. It never needs a fix.",
|
|
97
|
+
"--no-usage-telemetry switches off Claude Code and Codex usage export, including later doctor runs.",
|
|
97
98
|
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
98
99
|
"--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
|
|
99
100
|
"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.",
|
|
@@ -16,7 +16,7 @@ import { chmod, mkdir, readFile, rename, stat, unlink, writeFile, } from "node:f
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { describeError, isMissingFileFailure } from "../health-detail.js";
|
|
19
|
-
export function defaultMemoryFileIo() {
|
|
19
|
+
export function defaultMemoryFileIo(options = {}) {
|
|
20
20
|
return {
|
|
21
21
|
async readText(file) {
|
|
22
22
|
try {
|
|
@@ -40,9 +40,10 @@ export function defaultMemoryFileIo() {
|
|
|
40
40
|
.then((info) => info.mode & 0o777)
|
|
41
41
|
.catch(() => null);
|
|
42
42
|
try {
|
|
43
|
-
await writeFile(staged, contents, "utf8");
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
await writeFile(staged, contents, { encoding: "utf8", ...(options.mode !== undefined ? { mode: options.mode } : {}) });
|
|
44
|
+
const targetMode = options.mode ?? existingMode;
|
|
45
|
+
if (targetMode !== null)
|
|
46
|
+
await chmod(staged, targetMode);
|
|
46
47
|
await rename(staged, file);
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
@@ -53,10 +54,12 @@ export function defaultMemoryFileIo() {
|
|
|
53
54
|
console.error("[memory-install] staged write could not be renamed; writing in place", JSON.stringify({
|
|
54
55
|
reason: "staged_rename_failed",
|
|
55
56
|
byte_size: contents.length,
|
|
56
|
-
...describeError(error),
|
|
57
|
+
...(options.mode !== undefined ? { error_name: error instanceof Error ? error.name : "unknown" } : describeError(error)),
|
|
57
58
|
}));
|
|
58
59
|
await unlink(staged).catch(() => undefined);
|
|
59
|
-
await writeFile(file, contents, "utf8");
|
|
60
|
+
await writeFile(file, contents, { encoding: "utf8", ...(options.mode !== undefined ? { mode: options.mode } : {}) });
|
|
61
|
+
if (options.mode !== undefined)
|
|
62
|
+
await chmod(file, options.mode);
|
|
60
63
|
}
|
|
61
64
|
},
|
|
62
65
|
};
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.197");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/commands/setup.js
CHANGED
|
@@ -15,11 +15,16 @@ export async function runSetup(command, io) {
|
|
|
15
15
|
if (door.json)
|
|
16
16
|
return emitAgentDoor(door, answer.body);
|
|
17
17
|
const body = answer.body;
|
|
18
|
-
const number = (value) => value === null ? "
|
|
18
|
+
const number = (value) => value === null ? "n/a" : value.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
|
19
|
+
// BLI-13066: the server writes what each number rests on (the share of sessions counted, the sessions judged), so the CLI, the web page and JARVIS say the same words. An older server sends none.
|
|
20
|
+
const coverage = (words) => words ? ` (${words})` : "";
|
|
19
21
|
for (const person of body.people ?? []) {
|
|
20
|
-
writeLine(io.stdout, `${person.rank ? `#${person.rank} ` : "Unranked · "}${person.display_name ?? person.email ?? "Unknown person"}: ${number(person.prs_per_week)} merged PRs/week · ${number(person.tokens_per_pr)} tokens/merged PR · ${person.yield === null ? "
|
|
22
|
+
writeLine(io.stdout, `${person.rank ? `#${person.rank} ` : "Unranked · "}${person.display_name ?? person.email ?? "Unknown person"}: ${number(person.prs_per_week)} merged PRs/week · ${number(person.tokens_per_pr)} tokens/merged PR${coverage(person.coverage_words?.tokens)} · ${person.yield === null ? "no session judged yet" : `${number(person.yield * 100)}% session yield`}${coverage(person.coverage_words?.yield)}`);
|
|
21
23
|
if (person.tokens_basis)
|
|
22
24
|
writeLine(io.stdout, ` Token basis: ${person.tokens_basis === "total_fallback" ? "total (input/cache breakdown unavailable)" : "input + output, excluding cache reads"}`);
|
|
25
|
+
// What they really ran and used over the window (BLI-13067); a read that did not answer says so.
|
|
26
|
+
writeLine(io.stdout, ` Tokens by model: ${person.model_use?.words ?? "not available right now."}`);
|
|
27
|
+
writeLine(io.stdout, ` Skills used: ${person.skill_use?.words ?? "not available right now."}`);
|
|
23
28
|
if (!person.machines.length)
|
|
24
29
|
writeLine(io.stdout, " No setup received.");
|
|
25
30
|
for (const machine of person.machines) {
|
|
@@ -39,6 +44,9 @@ export async function runSetup(command, io) {
|
|
|
39
44
|
writeLine(io.stdout, `${pair.first ?? "Unknown person"} / ${pair.second ?? "Unknown person"}`);
|
|
40
45
|
for (const difference of command.details ? [...pair.differences, ...(pair.details ?? [])] : pair.differences)
|
|
41
46
|
writeLine(io.stdout, ` ${difference}`);
|
|
47
|
+
for (const side of ["first", "second"])
|
|
48
|
+
if (pair.use?.[side])
|
|
49
|
+
writeLine(io.stdout, ` ${side === "first" ? pair.first : pair.second}: tokens by model: ${pair.use[side].models ?? "not available right now."} Skills used: ${pair.use[side].skills ?? "not available right now."}`);
|
|
42
50
|
}
|
|
43
51
|
if (body.note)
|
|
44
52
|
writeLine(io.stdout, body.note);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { findTomlTableSpan, upsertTomlTable } from "./memory-install-toml.js";
|
|
2
|
+
const render = (value) => typeof value === "string" ? value : `{ ${Object.entries(value).map(([k, v]) => `${k} = ${render(v)}`).join(", ")} }`;
|
|
3
|
+
/** Split inline TOML at commas outside strings/nested arrays/tables. Keep unknown values verbatim. */
|
|
4
|
+
function splitEntries(text) {
|
|
5
|
+
const entries = [];
|
|
6
|
+
let start = 0, depth = 0, quote = "", escaped = false;
|
|
7
|
+
for (let i = 0; i < text.length; i++) {
|
|
8
|
+
const c = text[i];
|
|
9
|
+
if (quote) {
|
|
10
|
+
if (escaped)
|
|
11
|
+
escaped = false;
|
|
12
|
+
else if (c === "\\" && quote === '"')
|
|
13
|
+
escaped = true;
|
|
14
|
+
else if (c === quote)
|
|
15
|
+
quote = "";
|
|
16
|
+
}
|
|
17
|
+
else if (c === '"' || c === "'")
|
|
18
|
+
quote = c;
|
|
19
|
+
else if (c === "{" || c === "[")
|
|
20
|
+
depth++;
|
|
21
|
+
else if (c === "}" || c === "]")
|
|
22
|
+
depth--;
|
|
23
|
+
else if (c === "," && depth === 0) {
|
|
24
|
+
entries.push(text.slice(start, i).trim());
|
|
25
|
+
start = i + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (quote || depth !== 0)
|
|
29
|
+
throw new Error("config_unreadable");
|
|
30
|
+
entries.push(text.slice(start).trim());
|
|
31
|
+
return entries.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
function mergeInline(previous, values) {
|
|
34
|
+
const raw = previous.trim();
|
|
35
|
+
if (!raw.startsWith("{"))
|
|
36
|
+
return render(values);
|
|
37
|
+
if (!raw.endsWith("}"))
|
|
38
|
+
throw new Error("config_unreadable");
|
|
39
|
+
const remaining = { ...values };
|
|
40
|
+
const entries = splitEntries(raw.slice(1, -1)).map(entry => {
|
|
41
|
+
const match = /^\s*([\w-]+|"[^"\n]+"|'[^'\n]+')\s*=\s*([\s\S]*)$/.exec(entry);
|
|
42
|
+
if (!match)
|
|
43
|
+
throw new Error("config_unreadable");
|
|
44
|
+
const key = match[1].replace(/^["']|["']$/g, "");
|
|
45
|
+
const value = remaining[key];
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return entry;
|
|
48
|
+
delete remaining[key];
|
|
49
|
+
return `${match[1]} = ${typeof value === "string" ? value : mergeInline(match[2], value)}`;
|
|
50
|
+
});
|
|
51
|
+
for (const [key, value] of Object.entries(remaining))
|
|
52
|
+
entries.push(`${key} = ${render(value)}`);
|
|
53
|
+
return `{ ${entries.join(", ")} }`;
|
|
54
|
+
}
|
|
55
|
+
/** Update only managed assignments; recurse through existing explicit subtables. */
|
|
56
|
+
export function mergeUsageToml(text, tablePath, values) {
|
|
57
|
+
let next = text;
|
|
58
|
+
const local = {};
|
|
59
|
+
for (const [key, value] of Object.entries(values)) {
|
|
60
|
+
const childPath = [...tablePath, key].join(".");
|
|
61
|
+
const childExists = next.split("\n").some(line => {
|
|
62
|
+
const header = /^\s*\[([^\]]+)\]/.exec(line)?.[1].replace(/["']/g, "").replace(/\s/g, "");
|
|
63
|
+
return header === childPath || header?.startsWith(childPath + ".");
|
|
64
|
+
});
|
|
65
|
+
if (typeof value !== "string" && childExists)
|
|
66
|
+
next = mergeUsageToml(next, [...tablePath, key], value);
|
|
67
|
+
else
|
|
68
|
+
local[key] = value;
|
|
69
|
+
}
|
|
70
|
+
const span = findTomlTableSpan(next, tablePath);
|
|
71
|
+
if (!span)
|
|
72
|
+
return Object.keys(local).length ? upsertTomlTable(next, tablePath, `[${tablePath.join(".")}]\n${Object.entries(local).map(([k, v]) => `${k} = ${render(v)}`).join("\n")}`) : next;
|
|
73
|
+
const lines = next.split("\n");
|
|
74
|
+
const remaining = { ...local };
|
|
75
|
+
const body = lines.slice(span.start_line + 1, span.end_line).map(line => {
|
|
76
|
+
const match = /^(\s*)([\w-]+|"[^"\n]+"|'[^'\n]+')\s*=\s*(.*)$/.exec(line);
|
|
77
|
+
if (!match)
|
|
78
|
+
return line;
|
|
79
|
+
const key = match[2].replace(/^["']|["']$/g, "");
|
|
80
|
+
const value = remaining[key];
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
return line;
|
|
83
|
+
delete remaining[key];
|
|
84
|
+
// Strip a trailing comment only outside a string or inline table.
|
|
85
|
+
const raw = match[3];
|
|
86
|
+
let quote = "", depth = 0, comment = raw.length, escaped = false;
|
|
87
|
+
for (let i = 0; i < raw.length; i++) {
|
|
88
|
+
const c = raw[i];
|
|
89
|
+
if (quote) {
|
|
90
|
+
if (escaped)
|
|
91
|
+
escaped = false;
|
|
92
|
+
else if (c === "\\" && quote === '"')
|
|
93
|
+
escaped = true;
|
|
94
|
+
else if (c === quote)
|
|
95
|
+
quote = "";
|
|
96
|
+
}
|
|
97
|
+
else if (c === '"' || c === "'")
|
|
98
|
+
quote = c;
|
|
99
|
+
else if (c === "{" || c === "[")
|
|
100
|
+
depth++;
|
|
101
|
+
else if (c === "}" || c === "]")
|
|
102
|
+
depth--;
|
|
103
|
+
else if (c === "#" && depth === 0) {
|
|
104
|
+
comment = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (quote || depth !== 0)
|
|
109
|
+
throw new Error("config_unreadable");
|
|
110
|
+
const updated = typeof value === "string" ? value : mergeInline(raw.slice(0, comment), value);
|
|
111
|
+
return `${match[1]}${match[2]} = ${updated}${comment < raw.length ? ` ${raw.slice(comment)}` : ""}`;
|
|
112
|
+
});
|
|
113
|
+
for (const [key, value] of Object.entries(remaining))
|
|
114
|
+
body.push(`${key} = ${render(value)}`);
|
|
115
|
+
return [...lines.slice(0, span.start_line + 1), ...body, ...lines.slice(span.end_line)].join("\n");
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.197",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.60",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.109",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.69"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|