@bli-cockpit/cli 0.2.236 → 0.2.238
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/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +106 -0
- package/dist/commands/cal.js +40 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +54 -48
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +56 -188
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +149 -0
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +11 -4
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/mail.js +6 -4
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg.js +51 -9
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
|
@@ -49,54 +49,7 @@ export async function runDoctorAfterSync(command, io, dashboardUrl, overrides =
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
const requestId = requestedDoctorId();
|
|
52
|
-
const
|
|
53
|
-
let receipt = null;
|
|
54
|
-
const quiet = new Writable({ write(_chunk, _encoding, done) { done(); } });
|
|
55
|
-
const doctorIo = { ...io, stdout: quiet };
|
|
56
|
-
await runDoctor({ kind: "doctor", alias: "doctor", automaticTick: true, homeDir: command.homeDir, dashboardUrl, json: true, dryRun: false }, doctorIo, {
|
|
57
|
-
selfUpdate: (updateIo, options) => runSelfUpdate({ ...updateIo, stdout: quiet }, options),
|
|
58
|
-
runLogin: async () => { throw new Error("automatic_doctor_cannot_prompt"); },
|
|
59
|
-
resolveAndSaveRoots: async () => { throw new Error("automatic_doctor_cannot_pick_folders"); },
|
|
60
|
-
reportInstallEvents: (options) => reportInstallEventsBestEffort({ ...options, homeDir: command.homeDir,
|
|
61
|
-
// Automatic receipts carry labels, never terminal instructions or paths.
|
|
62
|
-
events: options.events.map((event) => ({ step: event.step, status: event.status, ...(event.error_code ? { error_code: event.error_code } : {}) })),
|
|
63
|
-
}),
|
|
64
|
-
onReceipt: (rows, repairs, setup) => {
|
|
65
|
-
const actions = new Set();
|
|
66
|
-
for (const item of doctorNeedsPerson(rows, setup)) {
|
|
67
|
-
if (item.step === "authed" && (!session?.device_token || [401, 403].includes(heartbeatRefusalStatus() ?? 0)))
|
|
68
|
-
actions.add("browser_sign_in");
|
|
69
|
-
if (item.step === "roots-ok")
|
|
70
|
-
actions.add("folder_selection");
|
|
71
|
-
if (item.step === "single-install")
|
|
72
|
-
actions.add("duplicate_install");
|
|
73
|
-
if (item.reason === "needs_trust" || item.action?.includes("/hooks"))
|
|
74
|
-
actions.add("hook_trust");
|
|
75
|
-
if (/permission|eacces|eperm|access_denied/u.test(item.reason))
|
|
76
|
-
actions.add("os_permission");
|
|
77
|
-
}
|
|
78
|
-
receipt = {
|
|
79
|
-
request_id: requestId, completed_at: new Date().toISOString(),
|
|
80
|
-
status: rows.some((row) => row.status === "fail" || row.status === "needs_fix") || actions.size ? "attention" : "pass",
|
|
81
|
-
repairs: repairs.map((repair) => ({ step: repair.step, before: safeVerdict(repair.before), after: safeVerdict(repair.after), fix_attempted: repair.fix_attempted ?? false })),
|
|
82
|
-
person_actions: [...actions],
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
}, {
|
|
86
|
-
// The collection phase already ran. Never recurse through another sync.
|
|
87
|
-
checkSync: async () => ({ id: "sync-fresh", status: facts?.status === "fail" ? "needs_fix" : "skipped", code: facts?.status === "fail" ? "tick_collection_failed" : "tick_collection_reported", message: "This tick already reported its collection outcome; it retries on the next tick." }),
|
|
88
|
-
fixSync: async (context) => context.deps.checkSync(context),
|
|
89
|
-
checkAutostart: async (context) => await isAutostartOptedOut(paths)
|
|
90
|
-
? { id: "autostart-alive", status: "skipped", code: "operator_uninstalled", message: "Background sync was deliberately turned off." }
|
|
91
|
-
: checkAutostartState(context),
|
|
92
|
-
fixAutostart: async (context) => {
|
|
93
|
-
// macOS uses the detached healer; Windows repairs in place. Neither
|
|
94
|
-
// overrides an explicit opt-out nor unloads this tick's macOS job.
|
|
95
|
-
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
96
|
-
return checkAutostartState(context);
|
|
97
|
-
},
|
|
98
|
-
...overrides,
|
|
99
|
-
});
|
|
52
|
+
const receipt = await collectDoctorReceipt(command, io, dashboardUrl, overrides, facts, paths, requestId);
|
|
100
53
|
if (!receipt)
|
|
101
54
|
throw new Error("doctor_receipt_missing");
|
|
102
55
|
await fs.mkdir(paths.state_dir, { recursive: true, mode: 0o700 });
|
|
@@ -116,6 +69,57 @@ export async function runDoctorAfterSync(command, io, dashboardUrl, overrides =
|
|
|
116
69
|
await lock.handle.release();
|
|
117
70
|
}
|
|
118
71
|
}
|
|
72
|
+
async function collectDoctorReceipt(command, io, dashboardUrl, overrides, facts, paths, requestId) {
|
|
73
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
74
|
+
let receipt = null;
|
|
75
|
+
const quiet = new Writable({ write(_chunk, _encoding, done) { done(); } });
|
|
76
|
+
const doctorIo = { ...io, stdout: quiet };
|
|
77
|
+
await runDoctor({ kind: "doctor", alias: "doctor", automaticTick: true, homeDir: command.homeDir, dashboardUrl, json: true, dryRun: false }, doctorIo, {
|
|
78
|
+
selfUpdate: (updateIo, options) => runSelfUpdate({ ...updateIo, stdout: quiet }, options),
|
|
79
|
+
runLogin: async () => { throw new Error("automatic_doctor_cannot_prompt"); },
|
|
80
|
+
resolveAndSaveRoots: async () => { throw new Error("automatic_doctor_cannot_pick_folders"); },
|
|
81
|
+
reportInstallEvents: (options) => reportInstallEventsBestEffort({ ...options, homeDir: command.homeDir,
|
|
82
|
+
// Automatic receipts carry labels, never terminal instructions or paths.
|
|
83
|
+
events: options.events.map((event) => ({ step: event.step, status: event.status, ...(event.error_code ? { error_code: event.error_code } : {}) })),
|
|
84
|
+
}),
|
|
85
|
+
onReceipt: (rows, repairs, setup) => {
|
|
86
|
+
const actions = new Set();
|
|
87
|
+
for (const item of doctorNeedsPerson(rows, setup)) {
|
|
88
|
+
if (item.step === "authed" && (!session?.device_token || [401, 403].includes(heartbeatRefusalStatus() ?? 0)))
|
|
89
|
+
actions.add("browser_sign_in");
|
|
90
|
+
if (item.step === "roots-ok")
|
|
91
|
+
actions.add("folder_selection");
|
|
92
|
+
if (item.step === "single-install")
|
|
93
|
+
actions.add("duplicate_install");
|
|
94
|
+
if (item.reason === "needs_trust" || item.action?.includes("/hooks"))
|
|
95
|
+
actions.add("hook_trust");
|
|
96
|
+
if (/permission|eacces|eperm|access_denied/u.test(item.reason))
|
|
97
|
+
actions.add("os_permission");
|
|
98
|
+
}
|
|
99
|
+
receipt = {
|
|
100
|
+
request_id: requestId, completed_at: new Date().toISOString(),
|
|
101
|
+
status: rows.some((row) => row.status === "fail" || row.status === "needs_fix") || actions.size ? "attention" : "pass",
|
|
102
|
+
repairs: repairs.map((repair) => ({ step: repair.step, before: safeVerdict(repair.before), after: safeVerdict(repair.after), fix_attempted: repair.fix_attempted ?? false })),
|
|
103
|
+
person_actions: [...actions],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
}, {
|
|
107
|
+
// The collection phase already ran. Never recurse through another sync.
|
|
108
|
+
checkSync: async () => ({ id: "sync-fresh", status: facts?.status === "fail" ? "needs_fix" : "skipped", code: facts?.status === "fail" ? "tick_collection_failed" : "tick_collection_reported", message: "This tick already reported its collection outcome; it retries on the next tick." }),
|
|
109
|
+
fixSync: async (context) => context.deps.checkSync(context),
|
|
110
|
+
checkAutostart: async (context) => await isAutostartOptedOut(paths)
|
|
111
|
+
? { id: "autostart-alive", status: "skipped", code: "operator_uninstalled", message: "Background sync was deliberately turned off." }
|
|
112
|
+
: checkAutostartState(context),
|
|
113
|
+
fixAutostart: async (context) => {
|
|
114
|
+
// macOS uses the detached healer; Windows repairs in place. Neither
|
|
115
|
+
// overrides an explicit opt-out nor unloads this tick's macOS job.
|
|
116
|
+
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
117
|
+
return checkAutostartState(context);
|
|
118
|
+
},
|
|
119
|
+
...overrides,
|
|
120
|
+
});
|
|
121
|
+
return receipt;
|
|
122
|
+
}
|
|
119
123
|
function safeVerdict(value) {
|
|
120
124
|
const [status, code] = (value ?? "fail:receipt_missing").split(":");
|
|
121
125
|
return `${status}:${sanitizeEventCode(code ?? "receipt_missing")}`;
|
package/dist/commands/sync.js
CHANGED
|
@@ -80,23 +80,29 @@ async function runSyncTick(command, io, tick) {
|
|
|
80
80
|
const minCliVersionAtStart = await reportTickStarted(command, io, dashboardUrl);
|
|
81
81
|
const updated = await tick.selfUpdate(() => runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersionAtStart));
|
|
82
82
|
if (updated && io.exec) {
|
|
83
|
-
|
|
84
|
-
if (command.homeDir)
|
|
85
|
-
args.push("--home", command.homeDir);
|
|
86
|
-
if (command.repoRoot)
|
|
87
|
-
args.push("--workspace", command.repoRoot);
|
|
88
|
-
if (command.dashboardUrl)
|
|
89
|
-
args.push("--dashboard-url", command.dashboardUrl);
|
|
90
|
-
if (command.maxDepth)
|
|
91
|
-
args.push("--max-depth", String(command.maxDepth));
|
|
92
|
-
if (command.maxRepos)
|
|
93
|
-
args.push("--max-repos", String(command.maxRepos));
|
|
94
|
-
if (command.json)
|
|
95
|
-
args.push("--json");
|
|
96
|
-
const exec = io.interactiveExec ?? createInteractiveExecRunner();
|
|
97
|
-
const result = await exec("cockpit", args, { env: envWithNodeRuntimeOnPath({ ...process.env, ...io.env, COCKPIT_SYNC_REEXEC: "1" }) });
|
|
98
|
-
return result.code;
|
|
83
|
+
return reexecuteSync(command, io);
|
|
99
84
|
}
|
|
85
|
+
return collectAndReportTick(command, io, tick, dashboardUrl);
|
|
86
|
+
}
|
|
87
|
+
async function reexecuteSync(command, io) {
|
|
88
|
+
const args = ["sync"];
|
|
89
|
+
if (command.homeDir)
|
|
90
|
+
args.push("--home", command.homeDir);
|
|
91
|
+
if (command.repoRoot)
|
|
92
|
+
args.push("--workspace", command.repoRoot);
|
|
93
|
+
if (command.dashboardUrl)
|
|
94
|
+
args.push("--dashboard-url", command.dashboardUrl);
|
|
95
|
+
if (command.maxDepth)
|
|
96
|
+
args.push("--max-depth", String(command.maxDepth));
|
|
97
|
+
if (command.maxRepos)
|
|
98
|
+
args.push("--max-repos", String(command.maxRepos));
|
|
99
|
+
if (command.json)
|
|
100
|
+
args.push("--json");
|
|
101
|
+
const exec = io.interactiveExec ?? createInteractiveExecRunner();
|
|
102
|
+
const result = await exec("cockpit", args, { env: envWithNodeRuntimeOnPath({ ...process.env, ...io.env, COCKPIT_SYNC_REEXEC: "1" }) });
|
|
103
|
+
return result.code;
|
|
104
|
+
}
|
|
105
|
+
async function collectAndReportTick(command, io, tick, dashboardUrl) {
|
|
100
106
|
try {
|
|
101
107
|
const result = await tick.collect(() => runSyncWithHealthReceipt(command, io));
|
|
102
108
|
// BLI-3551: every tick checks in, including one that collected nothing.
|
|
@@ -7,20 +7,31 @@ function splitEntries(text) {
|
|
|
7
7
|
for (let i = 0; i < text.length; i++) {
|
|
8
8
|
const c = text[i];
|
|
9
9
|
if (quote) {
|
|
10
|
-
if (escaped)
|
|
10
|
+
if (escaped) {
|
|
11
11
|
escaped = false;
|
|
12
|
-
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (c === "\\" && quote === '"') {
|
|
13
15
|
escaped = true;
|
|
14
|
-
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (c === quote)
|
|
15
19
|
quote = "";
|
|
20
|
+
continue;
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
if (c === '"' || c === "'") {
|
|
18
23
|
quote = c;
|
|
19
|
-
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (c === "{" || c === "[") {
|
|
20
27
|
depth++;
|
|
21
|
-
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === "}" || c === "]") {
|
|
22
31
|
depth--;
|
|
23
|
-
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (c === "," && depth === 0) {
|
|
24
35
|
entries.push(text.slice(start, i).trim());
|
|
25
36
|
start = i + 1;
|
|
26
37
|
}
|
|
@@ -72,45 +83,60 @@ export function mergeUsageToml(text, tablePath, values) {
|
|
|
72
83
|
return Object.keys(local).length ? upsertTomlTable(next, tablePath, `[${tablePath.join(".")}]\n${Object.entries(local).map(([k, v]) => `${k} = ${render(v)}`).join("\n")}`) : next;
|
|
73
84
|
const lines = next.split("\n");
|
|
74
85
|
const remaining = { ...local };
|
|
75
|
-
const body = lines.slice(span.start_line + 1, span.end_line)
|
|
76
|
-
|
|
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
|
-
});
|
|
86
|
+
const body = lines.slice(span.start_line + 1, span.end_line)
|
|
87
|
+
.map(line => mergeUsageAssignment(line, remaining));
|
|
113
88
|
for (const [key, value] of Object.entries(remaining))
|
|
114
89
|
body.push(`${key} = ${render(value)}`);
|
|
115
90
|
return [...lines.slice(0, span.start_line + 1), ...body, ...lines.slice(span.end_line)].join("\n");
|
|
91
|
+
}
|
|
92
|
+
function mergeUsageAssignment(line, remaining) {
|
|
93
|
+
const match = /^(\s*)([\w-]+|"[^"\n]+"|'[^'\n]+')\s*=\s*(.*)$/.exec(line);
|
|
94
|
+
if (!match)
|
|
95
|
+
return line;
|
|
96
|
+
const key = match[2].replace(/^["']|["']$/g, "");
|
|
97
|
+
const value = remaining[key];
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return line;
|
|
100
|
+
delete remaining[key];
|
|
101
|
+
// Strip a trailing comment only outside a string or inline table.
|
|
102
|
+
const raw = match[3];
|
|
103
|
+
const comment = findTrailingComment(raw);
|
|
104
|
+
const updated = typeof value === "string" ? value : mergeInline(raw.slice(0, comment), value);
|
|
105
|
+
return `${match[1]}${match[2]} = ${updated}${comment < raw.length ? ` ${raw.slice(comment)}` : ""}`;
|
|
106
|
+
}
|
|
107
|
+
function findTrailingComment(raw) {
|
|
108
|
+
let quote = "", depth = 0, escaped = false;
|
|
109
|
+
for (let i = 0; i < raw.length; i++) {
|
|
110
|
+
const c = raw[i];
|
|
111
|
+
if (quote) {
|
|
112
|
+
if (escaped) {
|
|
113
|
+
escaped = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (c === "\\" && quote === '"') {
|
|
117
|
+
escaped = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c === quote)
|
|
121
|
+
quote = "";
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === '"' || c === "'") {
|
|
125
|
+
quote = c;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (c === "{" || c === "[") {
|
|
129
|
+
depth++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (c === "}" || c === "]") {
|
|
133
|
+
depth--;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === "#" && depth === 0)
|
|
137
|
+
return i;
|
|
138
|
+
}
|
|
139
|
+
if (quote || depth !== 0)
|
|
140
|
+
throw new Error("config_unreadable");
|
|
141
|
+
return raw.length;
|
|
116
142
|
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -5,42 +5,9 @@ import { writeLine } from "./cli-io.js";
|
|
|
5
5
|
export async function runUsage(command, io) {
|
|
6
6
|
const door = await openAgentDoor("usage", command, io);
|
|
7
7
|
const query = new URLSearchParams({ since: command.since });
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
query.set("groupBy", "topic");
|
|
12
|
-
if (command.minConfidence)
|
|
13
|
-
query.set("minConfidence", command.minConfidence);
|
|
14
|
-
if (command.byTopic)
|
|
15
|
-
query.set("groupBy", "task_type");
|
|
16
|
-
if (command.byRepo)
|
|
17
|
-
query.set("groupBy", "repo");
|
|
18
|
-
if (command.byClient)
|
|
19
|
-
query.set("groupBy", "client");
|
|
20
|
-
if (command.until)
|
|
21
|
-
query.set("until", command.until);
|
|
22
|
-
if (command.includeAutomated)
|
|
23
|
-
query.set("include_automated", "1");
|
|
24
|
-
if (command.action === "sessions") {
|
|
25
|
-
for (const [key, value] of [["person", command.person], ["tool", command.tool], ["cursor", command.cursor], ["limit", command.limit], ["task", command.task], ["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
26
|
-
if (value)
|
|
27
|
-
query.set(key, value);
|
|
28
|
-
const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
|
|
29
|
-
if (!response.ok)
|
|
30
|
-
return failAgentDoor(door, "[usage]", response.reason, response.detail);
|
|
31
|
-
if (door.json || command.session)
|
|
32
|
-
return emitAgentDoor(door, response.body);
|
|
33
|
-
const sessions = response.body.sessions ?? [];
|
|
34
|
-
for (const session of sessions)
|
|
35
|
-
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.start_time ?? "start unknown"} ${session.tool ?? session.task_type} ${session.model ?? "model unknown"} ${session.title ?? "no title yet"}`);
|
|
36
|
-
writeLine(io.stdout, `${sessions.length} sessions`);
|
|
37
|
-
const page = response.body;
|
|
38
|
-
for (const line of page.read_coverage ?? [])
|
|
39
|
-
writeLine(io.stdout, line);
|
|
40
|
-
if (page.next_cursor)
|
|
41
|
-
writeLine(io.stdout, `Next page: ${page.window ? `--since ${page.window.since} --until ${page.window.until} ` : ""}--cursor ${page.next_cursor}`);
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
8
|
+
addUsageGroupingFilters(command, query);
|
|
9
|
+
if (command.action === "sessions")
|
|
10
|
+
return runUsageSessions(command, io, door, query);
|
|
44
11
|
const answer = await askAgentDoor(door, { path: `/api/usage/people?${query}`, method: "GET", label: "usage people", timeoutMs: 30_000 });
|
|
45
12
|
if (!answer.ok)
|
|
46
13
|
return failAgentDoor(door, "[usage]", answer.reason, answer.detail);
|
|
@@ -56,8 +23,7 @@ export async function runUsage(command, io) {
|
|
|
56
23
|
const email = command.person === "me" ? (await loadPairedSession("usage", command.homeDir)).email : command.person;
|
|
57
24
|
if (!email)
|
|
58
25
|
return failAgentDoor(door, "[usage]", "caller_email_unavailable", "The paired session has no email. Sign in again or pass --person <email>.");
|
|
59
|
-
|
|
60
|
-
body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0), sessions_counted_at_upload: body.people.reduce((total, person) => total + (person.sessions_counted_at_upload ?? 0), 0), sessions_counted_by_server: body.people.reduce((total, person) => total + (person.sessions_counted_by_server ?? 0), 0), sessions_counted_by_ladder: body.people.reduce((total, person) => total + (person.sessions_counted_by_ladder ?? 0), 0), sessions_awaiting_count: body.people.reduce((total, person) => total + (person.sessions_awaiting_count ?? 0), 0), sessions_automated_excluded: body.people.reduce((total, person) => total + (person.sessions_automated_excluded ?? 0), 0) };
|
|
26
|
+
filterUsageToPerson(body, email);
|
|
61
27
|
}
|
|
62
28
|
if (command.byRepo && body.people?.some((person) => !Array.isArray(person.repos))) {
|
|
63
29
|
return failAgentDoor(door, "[usage]", "repo_grouping_unavailable", "The dashboard returned person totals without project rows. Deploy the dashboard project split before using --by-repo.");
|
|
@@ -70,21 +36,52 @@ export async function runUsage(command, io) {
|
|
|
70
36
|
return failAgentDoor(door, "[usage]", "ticket_grouping_unavailable", "Deploy the dashboard task split before using --by-task.");
|
|
71
37
|
if (door.json)
|
|
72
38
|
return emitAgentDoor(door, body);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
nameWidth = Math.max(nameWidth, repo.repo_label.length + 2);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
39
|
+
return writeUsagePeople(command, io, body);
|
|
40
|
+
}
|
|
41
|
+
function addUsageGroupingFilters(command, query) {
|
|
42
|
+
if (command.byTask)
|
|
43
|
+
query.set("groupBy", "ticket");
|
|
81
44
|
if (command.bySubject)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
45
|
+
query.set("groupBy", "topic");
|
|
46
|
+
if (command.minConfidence)
|
|
47
|
+
query.set("minConfidence", command.minConfidence);
|
|
48
|
+
if (command.byTopic)
|
|
49
|
+
query.set("groupBy", "task_type");
|
|
50
|
+
if (command.byRepo)
|
|
51
|
+
query.set("groupBy", "repo");
|
|
52
|
+
if (command.byClient)
|
|
53
|
+
query.set("groupBy", "client");
|
|
54
|
+
if (command.until)
|
|
55
|
+
query.set("until", command.until);
|
|
56
|
+
if (command.includeAutomated)
|
|
57
|
+
query.set("include_automated", "1");
|
|
58
|
+
}
|
|
59
|
+
async function runUsageSessions(command, io, door, query) {
|
|
60
|
+
for (const [key, value] of [["person", command.person], ["tool", command.tool], ["cursor", command.cursor], ["limit", command.limit], ["task", command.task], ["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
61
|
+
if (value)
|
|
62
|
+
query.set(key, value);
|
|
63
|
+
const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
|
|
64
|
+
if (!response.ok)
|
|
65
|
+
return failAgentDoor(door, "[usage]", response.reason, response.detail);
|
|
66
|
+
if (door.json || command.session)
|
|
67
|
+
return emitAgentDoor(door, response.body);
|
|
68
|
+
const sessions = response.body.sessions ?? [];
|
|
69
|
+
for (const session of sessions)
|
|
70
|
+
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.start_time ?? "start unknown"} ${session.tool ?? session.task_type} ${session.model ?? "model unknown"} ${session.title ?? "no title yet"}`);
|
|
71
|
+
writeLine(io.stdout, `${sessions.length} sessions`);
|
|
72
|
+
const page = response.body;
|
|
73
|
+
for (const line of page.read_coverage ?? [])
|
|
74
|
+
writeLine(io.stdout, line);
|
|
75
|
+
if (page.next_cursor)
|
|
76
|
+
writeLine(io.stdout, `Next page: ${page.window ? `--since ${page.window.since} --until ${page.window.until} ` : ""}--cursor ${page.next_cursor}`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
function filterUsageToPerson(body, email) {
|
|
80
|
+
body.people = (body.people ?? []).filter((person) => person.email?.toLowerCase() === email.toLowerCase());
|
|
81
|
+
body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0), sessions_counted_at_upload: body.people.reduce((total, person) => total + (person.sessions_counted_at_upload ?? 0), 0), sessions_counted_by_server: body.people.reduce((total, person) => total + (person.sessions_counted_by_server ?? 0), 0), sessions_counted_by_ladder: body.people.reduce((total, person) => total + (person.sessions_counted_by_ladder ?? 0), 0), sessions_awaiting_count: body.people.reduce((total, person) => total + (person.sessions_awaiting_count ?? 0), 0), sessions_automated_excluded: body.people.reduce((total, person) => total + (person.sessions_automated_excluded ?? 0), 0) };
|
|
82
|
+
}
|
|
83
|
+
function writeUsagePeople(command, io, body) {
|
|
84
|
+
const nameWidth = usageNameWidth(command, body);
|
|
88
85
|
// BLI-12561: the three GitHub columns sit right after Coverage, ahead of the
|
|
89
86
|
// optional token splits, and only when the dashboard sent a `github` block.
|
|
90
87
|
// A sub-row has no GitHub counts of its own and prints blanks in their place,
|
|
@@ -105,24 +102,14 @@ export async function runUsage(command, io) {
|
|
|
105
102
|
...(command.detail ? [row.output_tokens, row.input_tokens, row.cache_read_input_tokens, row.cache_creation_input_tokens].map(formatCount) : []),
|
|
106
103
|
]);
|
|
107
104
|
if (command.bySubject)
|
|
108
|
-
|
|
109
|
-
printRow([` ${subject.repo_label} / ${subject.topic}`, formatCount(subject.tokens), formatUsageDollars(subject.api_list_price_equivalent_usd), `${subject.extracted_sessions}/${subject.sessions}`, ...githubBlanks, ...(command.detail ? [subject.output, subject.input, subject.cache_read, subject.cache_creation].map(formatCount) : [])]);
|
|
110
|
-
for (const sub of subject.children ?? [])
|
|
111
|
-
printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...githubBlanks, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
|
|
112
|
-
}
|
|
105
|
+
writeUsageSubjects(command, row, githubBlanks, printRow);
|
|
113
106
|
if (command.byTask || command.byRepo || command.byTopic) {
|
|
114
|
-
|
|
115
|
-
for (const repo of command.all ? repos : repos.slice(0, 10)) {
|
|
116
|
-
if (command.byTask)
|
|
117
|
-
writeLine(io.stdout, ` ${repo.ticket_title ?? ""} Sources: ${Object.entries(repo.binding_sources ?? {}).map(([source, count]) => `${source} ${count}`).join(", ")}`);
|
|
118
|
-
if (command.byTopic && repo.confidence)
|
|
119
|
-
writeLine(io.stdout, ` Confidence: ${repo.confidence.high} high, ${repo.confidence.medium} medium, ${repo.confidence.low} low`);
|
|
120
|
-
printRow([` ${repo.repo_label}`, formatCount(repo.tokens), formatUsageDollars(repo.api_list_price_equivalent_usd), `${repo.extracted_sessions}/${repo.sessions}`, ...githubBlanks, ...(command.detail ? [repo.output, repo.input, repo.cache_read, repo.cache_creation].map(formatCount) : [])]);
|
|
121
|
-
}
|
|
122
|
-
if (!command.all && repos.length > 10)
|
|
123
|
-
writeLine(io.stdout, ` and ${repos.length - 10} more ${command.byTask ? "tasks" : command.byTopic ? "topics" : "repos"}`);
|
|
107
|
+
writeUsageGroups(command, row, io, githubBlanks, printRow);
|
|
124
108
|
}
|
|
125
109
|
}
|
|
110
|
+
return writeUsageCoverage(command, io, body);
|
|
111
|
+
}
|
|
112
|
+
function writeUsageCoverage(command, io, body) {
|
|
126
113
|
// BLI-4554: the door sends the label, and the label carries the price
|
|
127
114
|
// sheet's version and date, so a figure on a terminal can be traced to the
|
|
128
115
|
// rates that produced it. The fallback is only reached against a dashboard
|
|
@@ -163,4 +150,41 @@ export async function runUsage(command, io) {
|
|
|
163
150
|
if (words.label)
|
|
164
151
|
writeLine(io.stdout, words.label);
|
|
165
152
|
return 0;
|
|
153
|
+
}
|
|
154
|
+
function usageNameWidth(command, body) {
|
|
155
|
+
let nameWidth = 20;
|
|
156
|
+
if (command.byTask || command.byRepo || command.byTopic) {
|
|
157
|
+
for (const person of body.people ?? []) {
|
|
158
|
+
const repos = (command.byTask ? person.tickets : command.byTopic ? person.task_types : person.repos) ?? [];
|
|
159
|
+
for (const repo of command.all ? repos : repos.slice(0, 10))
|
|
160
|
+
nameWidth = Math.max(nameWidth, repo.repo_label.length + 2);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (command.bySubject)
|
|
164
|
+
for (const person of body.people ?? [])
|
|
165
|
+
for (const subject of person.subjects ?? []) {
|
|
166
|
+
nameWidth = Math.max(nameWidth, subject.repo_label.length + subject.topic.length + 5);
|
|
167
|
+
for (const sub of subject.children ?? [])
|
|
168
|
+
nameWidth = Math.max(nameWidth, sub.subtopic.length + 4);
|
|
169
|
+
}
|
|
170
|
+
return nameWidth;
|
|
171
|
+
}
|
|
172
|
+
function writeUsageSubjects(command, row, githubBlanks, printRow) {
|
|
173
|
+
for (const subject of row.subjects ?? []) {
|
|
174
|
+
printRow([` ${subject.repo_label} / ${subject.topic}`, formatCount(subject.tokens), formatUsageDollars(subject.api_list_price_equivalent_usd), `${subject.extracted_sessions}/${subject.sessions}`, ...githubBlanks, ...(command.detail ? [subject.output, subject.input, subject.cache_read, subject.cache_creation].map(formatCount) : [])]);
|
|
175
|
+
for (const sub of subject.children ?? [])
|
|
176
|
+
printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...githubBlanks, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function writeUsageGroups(command, row, io, githubBlanks, printRow) {
|
|
180
|
+
const repos = (command.byTask ? row.tickets : command.byTopic ? row.task_types : row.repos) ?? [];
|
|
181
|
+
for (const repo of command.all ? repos : repos.slice(0, 10)) {
|
|
182
|
+
if (command.byTask)
|
|
183
|
+
writeLine(io.stdout, ` ${repo.ticket_title ?? ""} Sources: ${Object.entries(repo.binding_sources ?? {}).map(([source, count]) => `${source} ${count}`).join(", ")}`);
|
|
184
|
+
if (command.byTopic && repo.confidence)
|
|
185
|
+
writeLine(io.stdout, ` Confidence: ${repo.confidence.high} high, ${repo.confidence.medium} medium, ${repo.confidence.low} low`);
|
|
186
|
+
printRow([` ${repo.repo_label}`, formatCount(repo.tokens), formatUsageDollars(repo.api_list_price_equivalent_usd), `${repo.extracted_sessions}/${repo.sessions}`, ...githubBlanks, ...(command.detail ? [repo.output, repo.input, repo.cache_read, repo.cache_creation].map(formatCount) : [])]);
|
|
187
|
+
}
|
|
188
|
+
if (!command.all && repos.length > 10)
|
|
189
|
+
writeLine(io.stdout, ` and ${repos.length - 10} more ${command.byTask ? "tasks" : command.byTopic ? "topics" : "repos"}`);
|
|
166
190
|
}
|
|
@@ -91,19 +91,22 @@ export function renderMarkdownText(markdown, width, styled) {
|
|
|
91
91
|
continue;
|
|
92
92
|
}
|
|
93
93
|
if (text.startsWith("- ")) {
|
|
94
|
-
|
|
95
|
-
const body = plain(item.replace(/^-\s+/, ""));
|
|
96
|
-
const wrapped = wrap(body, width - 4);
|
|
97
|
-
wrapped.forEach((line, index) => {
|
|
98
|
-
out.push(index === 0 ? ` · ${line}` : ` ${line}`);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
94
|
+
appendMarkdownList(out, text, width);
|
|
101
95
|
continue;
|
|
102
96
|
}
|
|
103
97
|
out.push(...wrap(plain(text), width));
|
|
104
98
|
}
|
|
105
99
|
return out;
|
|
106
100
|
}
|
|
101
|
+
function appendMarkdownList(out, text, width) {
|
|
102
|
+
for (const item of text.split("\n")) {
|
|
103
|
+
const body = plain(item.replace(/^-\s+/, ""));
|
|
104
|
+
const wrapped = wrap(body, width - 4);
|
|
105
|
+
wrapped.forEach((line, index) => {
|
|
106
|
+
out.push(index === 0 ? ` · ${line}` : ` ${line}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
107
110
|
/**
|
|
108
111
|
* A GFM pipe table as padded columns. When the padded table would not fit the
|
|
109
112
|
* width, each row is printed as `header: cell` lines instead — narrower, and
|