@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.
Files changed (143) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +106 -0
  29. package/dist/commands/cal.js +40 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/doctor-usage-telemetry.js +2 -1
  35. package/dist/commands/heartbeat-post.js +116 -0
  36. package/dist/commands/heartbeat-token-lock.js +46 -33
  37. package/dist/commands/heartbeat.js +43 -125
  38. package/dist/commands/install-update.js +17 -15
  39. package/dist/commands/issue-show.js +27 -28
  40. package/dist/commands/issue-write.js +70 -23
  41. package/dist/commands/issue.js +68 -37
  42. package/dist/commands/jarvis-live-answer.js +99 -0
  43. package/dist/commands/jarvis-render.js +1 -94
  44. package/dist/commands/jarvis-trace.js +30 -22
  45. package/dist/commands/jarvis-turn-report.js +124 -0
  46. package/dist/commands/jarvis-turn.js +108 -182
  47. package/dist/commands/jarvis.js +26 -17
  48. package/dist/commands/local-args-collector-setup.js +82 -84
  49. package/dist/commands/local-args-collector-work.js +37 -38
  50. package/dist/commands/local-args-tower-admin.js +155 -142
  51. package/dist/commands/local-args-tower-cal.js +54 -48
  52. package/dist/commands/local-args-tower-careers.js +73 -54
  53. package/dist/commands/local-args-tower-docs-msg.js +56 -188
  54. package/dist/commands/local-args-tower-github.js +26 -20
  55. package/dist/commands/local-args-tower-hits.js +32 -20
  56. package/dist/commands/local-args-tower-manager.js +85 -4
  57. package/dist/commands/local-args-tower-memory.js +139 -117
  58. package/dist/commands/local-args-tower-msg.js +149 -0
  59. package/dist/commands/local-args-tower-notes.js +346 -0
  60. package/dist/commands/local-args-tower-pages.js +100 -378
  61. package/dist/commands/local-args-tower-settings.js +28 -29
  62. package/dist/commands/local-args-tower-work.js +52 -33
  63. package/dist/commands/local-args.js +80 -129
  64. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  65. package/dist/commands/local-help-commands-tower.js +11 -4
  66. package/dist/commands/local-help-commands.js +342 -341
  67. package/dist/commands/local-help.js +87 -83
  68. package/dist/commands/local-run-guards.js +100 -0
  69. package/dist/commands/local.js +137 -206
  70. package/dist/commands/login.js +13 -9
  71. package/dist/commands/logs.js +55 -38
  72. package/dist/commands/mail-attachments.js +44 -26
  73. package/dist/commands/mail.js +6 -4
  74. package/dist/commands/manager-render-home.js +208 -0
  75. package/dist/commands/manager-render.js +7 -3
  76. package/dist/commands/manager-requests.js +33 -0
  77. package/dist/commands/manager.js +39 -5
  78. package/dist/commands/memory-consolidation.js +18 -9
  79. package/dist/commands/memory-daemon-probe.js +37 -28
  80. package/dist/commands/memory-install-config.js +64 -60
  81. package/dist/commands/memory-install.js +21 -41
  82. package/dist/commands/memory-requeue.js +26 -19
  83. package/dist/commands/memory-rules.js +26 -18
  84. package/dist/commands/memory-search-print.js +26 -0
  85. package/dist/commands/memory-store.js +14 -30
  86. package/dist/commands/memory-verb-list.js +49 -48
  87. package/dist/commands/msg.js +51 -9
  88. package/dist/commands/notes-accounts.js +49 -32
  89. package/dist/commands/notes-sync-loop.js +115 -89
  90. package/dist/commands/notes-wisprflow-login.js +44 -23
  91. package/dist/commands/notes-writes.js +115 -100
  92. package/dist/commands/notes.js +33 -55
  93. package/dist/commands/onboard-flows.js +69 -51
  94. package/dist/commands/onboard.js +11 -9
  95. package/dist/commands/ops-render-memory.js +10 -7
  96. package/dist/commands/ops-render.js +33 -30
  97. package/dist/commands/ops.js +3 -0
  98. package/dist/commands/public-root.js +1 -1
  99. package/dist/commands/session-sync-outside-roots.js +40 -36
  100. package/dist/commands/session-sync-plan.js +14 -10
  101. package/dist/commands/session-sync-upload.js +24 -22
  102. package/dist/commands/session-sync.js +19 -8
  103. package/dist/commands/slack.js +25 -16
  104. package/dist/commands/sources-check.js +13 -7
  105. package/dist/commands/sources-discovery.js +25 -17
  106. package/dist/commands/sources-verdict.js +19 -18
  107. package/dist/commands/speed.js +23 -19
  108. package/dist/commands/start.js +20 -16
  109. package/dist/commands/sync-doctor.js +52 -48
  110. package/dist/commands/sync.js +22 -16
  111. package/dist/commands/usage-telemetry-toml.js +71 -45
  112. package/dist/commands/usage.js +91 -67
  113. package/dist/commands/workbook-render.js +10 -7
  114. package/dist/evidence-reconcile-client.js +75 -64
  115. package/dist/evidence-redelivery-plan.js +37 -34
  116. package/dist/evidence-redelivery.js +32 -29
  117. package/dist/evidence-transcripts-client.js +20 -20
  118. package/dist/evidence-upload-object.js +86 -89
  119. package/dist/evidence-upload-plan.js +94 -83
  120. package/dist/evidence-upload-terminal.js +45 -50
  121. package/dist/image-description-session.js +156 -159
  122. package/dist/local-state-pairing-code.js +39 -31
  123. package/dist/local-state-status.js +9 -2
  124. package/dist/onboarding-roots.js +5 -8
  125. package/dist/process-runner.js +8 -8
  126. package/dist/raw-evidence-gc.js +33 -38
  127. package/dist/raw-evidence-stuck-summary.js +7 -14
  128. package/dist/repo-identity.js +26 -23
  129. package/dist/scheduled-self-update.js +11 -15
  130. package/dist/second-install.js +3 -0
  131. package/dist/slim/codex-rules.js +23 -36
  132. package/dist/sync-lock.js +5 -15
  133. package/dist/tower-client.js +36 -53
  134. package/dist/tower-stream.js +7 -24
  135. package/dist/upload-agent-artifacts.js +58 -64
  136. package/dist/upload-envelope-build.js +60 -59
  137. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  138. package/dist/upload-evidence-delivery-summary.js +32 -34
  139. package/dist/upload-ingest-receipt.js +20 -23
  140. package/dist/upload-session-reports-queue.js +4 -1
  141. package/dist/upload-session-reports-wire.js +64 -68
  142. package/dist/upload-sync.js +171 -157
  143. 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 session = await readLocalCollectorSessionFile(paths).catch(() => null);
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")}`;
@@ -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
- const args = ["sync"];
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
- else if (c === "\\" && quote === '"')
12
+ continue;
13
+ }
14
+ if (c === "\\" && quote === '"') {
13
15
  escaped = true;
14
- else if (c === quote)
16
+ continue;
17
+ }
18
+ if (c === quote)
15
19
  quote = "";
20
+ continue;
16
21
  }
17
- else if (c === '"' || c === "'")
22
+ if (c === '"' || c === "'") {
18
23
  quote = c;
19
- else if (c === "{" || c === "[")
24
+ continue;
25
+ }
26
+ if (c === "{" || c === "[") {
20
27
  depth++;
21
- else if (c === "}" || c === "]")
28
+ continue;
29
+ }
30
+ if (c === "}" || c === "]") {
22
31
  depth--;
23
- else if (c === "," && depth === 0) {
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).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
- });
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
  }
@@ -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
- if (command.byTask)
9
- query.set("groupBy", "ticket");
10
- if (command.bySubject)
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
- body.people = (body.people ?? []).filter((person) => person.email?.toLowerCase() === email.toLowerCase());
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
- let nameWidth = 20;
74
- if (command.byTask || command.byRepo || command.byTopic) {
75
- for (const person of body.people ?? []) {
76
- const repos = (command.byTask ? person.tickets : command.byTopic ? person.task_types : person.repos) ?? [];
77
- for (const repo of command.all ? repos : repos.slice(0, 10))
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
- for (const person of body.people ?? [])
83
- for (const subject of person.subjects ?? []) {
84
- nameWidth = Math.max(nameWidth, subject.repo_label.length + subject.topic.length + 5);
85
- for (const sub of subject.children ?? [])
86
- nameWidth = Math.max(nameWidth, sub.subtopic.length + 4);
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
- for (const subject of row.subjects ?? []) {
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
- const repos = (command.byTask ? row.tickets : command.byTopic ? row.task_types : row.repos) ?? [];
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
- for (const item of text.split("\n")) {
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