@phnx-labs/agents-cli 1.22.25 → 1.22.27
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/CHANGELOG.md +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Insights command —
|
|
2
|
+
* Insights command — one observe verb for "how work looks".
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* any of them:
|
|
4
|
+
* Two data paths under one name (do not re-split into peer top-level commands):
|
|
6
5
|
*
|
|
7
|
-
* agents
|
|
8
|
-
*
|
|
9
|
-
* agents
|
|
10
|
-
*
|
|
11
|
-
* agents
|
|
12
|
-
* agents insights
|
|
6
|
+
* agents insights HOW you work (transcript content: tools, friction,
|
|
7
|
+
* rhythm, edits) — split by Claude account by default
|
|
8
|
+
* agents insights mix COUNTERS (sessions index + usage.db recipes:
|
|
9
|
+
* harness/model mix, token ratios, secrets, browser)
|
|
10
|
+
* agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, …)
|
|
11
|
+
* agents insights query Raw usage.db rows
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* Sibling observe verbs (stay separate — different questions):
|
|
14
|
+
*
|
|
15
|
+
* agents cost what you spent ($ and duration)
|
|
16
|
+
* agents output what shipped (burn vs PRs and commits)
|
|
17
|
+
* agents usage live quota headroom
|
|
18
|
+
* agents perf latency (hooks, CLI commands, agent.run) — not popularity
|
|
19
|
+
* agents sessions stats which skills/slash-commands were explicitly invoked
|
|
20
|
+
*
|
|
21
|
+
* Why mix lives here (not a second top-level `trends`): two abstract "analytics"
|
|
22
|
+
* nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
|
|
23
|
+
* vs transcript facets. Latency stays on `perf` so it is never confused with mix.
|
|
21
24
|
*
|
|
22
25
|
* Modelled on Claude Code's `/insights`, with the difference that motivated it: that
|
|
23
26
|
* command reads one account's directory, while `balanced` rotation sprays sessions
|
|
@@ -27,6 +30,8 @@
|
|
|
27
30
|
* The deterministic report makes zero network calls. `--narrative` is opt-in and adds
|
|
28
31
|
* the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
|
|
29
32
|
* `claude -p`.
|
|
33
|
+
*
|
|
34
|
+
* `agents trends` is a thin deprecated alias of the mix tree only (see commands/trends.ts).
|
|
30
35
|
*/
|
|
31
36
|
import * as fs from 'fs';
|
|
32
37
|
import chalk from 'chalk';
|
|
@@ -37,11 +42,25 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
37
42
|
import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
|
|
38
43
|
import { querySessions, readSessionInsights, writeSessionInsights, clearSessionInsights, } from '../lib/session/db.js';
|
|
39
44
|
import { parseSession } from '../lib/session/parse.js';
|
|
40
|
-
import { computeInsightFacets, mergeFacets, newFacetAccumulator, detectOverlap, percentile, bucketGaps, topEntries, } from '../lib/session/insights.js';
|
|
45
|
+
import { computeInsightFacets, mergeFacets, newFacetAccumulator, detectOverlap, percentile, bucketGaps, topEntries, buildInsightActions, } from '../lib/session/insights.js';
|
|
41
46
|
import { formatUsd } from '../lib/pricing/index.js';
|
|
42
47
|
import { formatDuration } from '../lib/session/render.js';
|
|
43
48
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
49
|
+
import { registerMixCommands } from '../lib/analytics/mix-commands.js';
|
|
44
50
|
const execFileAsync = promisify(execFile);
|
|
51
|
+
function collectAgent(value, previous) {
|
|
52
|
+
return [...previous, value];
|
|
53
|
+
}
|
|
54
|
+
function agentsFromArgv(argv) {
|
|
55
|
+
const values = [];
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
if (argv[i] === '--agent' && argv[i + 1])
|
|
58
|
+
values.push(argv[++i]);
|
|
59
|
+
else if (argv[i].startsWith('--agent='))
|
|
60
|
+
values.push(argv[i].slice('--agent='.length));
|
|
61
|
+
}
|
|
62
|
+
return values;
|
|
63
|
+
}
|
|
45
64
|
function resolveGroup(by) {
|
|
46
65
|
if (by === undefined)
|
|
47
66
|
return 'account';
|
|
@@ -187,7 +206,7 @@ function renderHours(hours, out) {
|
|
|
187
206
|
out.push(` ${chalk.cyan(spark)}`);
|
|
188
207
|
out.push(` ${chalk.gray('0h'.padEnd(6))}${chalk.gray('6h'.padEnd(6))}${chalk.gray('12h'.padEnd(6))}${chalk.gray('18h'.padEnd(5))}${chalk.gray('23h')}`);
|
|
189
208
|
}
|
|
190
|
-
function renderReport(groups, dim, meta) {
|
|
209
|
+
function renderReport(groups, dim, meta, actions, harnesses) {
|
|
191
210
|
const out = [];
|
|
192
211
|
const scope = meta.since ? `last ${meta.since}` : 'all time';
|
|
193
212
|
out.push(chalk.bold('Insights') + chalk.gray(` ${scope} · ${meta.analyzed} of ${meta.scanned} sessions`));
|
|
@@ -198,16 +217,27 @@ function renderReport(groups, dim, meta) {
|
|
|
198
217
|
return;
|
|
199
218
|
}
|
|
200
219
|
// Per-group table — the headline, and the thing no sibling command produces.
|
|
220
|
+
// Includes silent-stall counts so harness/account laziness is visible without --json.
|
|
201
221
|
out.push('');
|
|
202
222
|
out.push(chalk.bold(`By ${dim}`));
|
|
203
|
-
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(
|
|
223
|
+
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(16, terminalWidth() - 58));
|
|
204
224
|
const sessW = Math.max(...groups.map((g) => String(g.sessions).length), 3);
|
|
225
|
+
const stallOf = (g) => Object.entries(g.facets.frictionSignals)
|
|
226
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
227
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
228
|
+
const resumeOf = (g) => g.facets.correctionSignals['resume after silent stall'] ?? 0;
|
|
229
|
+
const stallW = Math.max(...groups.map((g) => String(stallOf(g)).length), 5);
|
|
230
|
+
out.push(chalk.gray(` ${padToWidth('', labelW)} ${''.padStart(sessW)} ` +
|
|
231
|
+
`${''.padStart(9)} ${''.padStart(8)} ${'stalls'.padStart(stallW)} resume`));
|
|
205
232
|
for (const g of groups) {
|
|
206
233
|
const cost = g.costUsd > 0 ? formatUsd(g.costUsd) : '—';
|
|
207
234
|
const dur = g.durationMs > 0 ? formatDuration(g.durationMs) : '—';
|
|
235
|
+
const stalls = stallOf(g);
|
|
236
|
+
const resumes = resumeOf(g);
|
|
208
237
|
out.push(` ${padToWidth(truncateToWidth(g.label, labelW), labelW)} ` +
|
|
209
238
|
`${chalk.gray(String(g.sessions).padStart(sessW))} ${chalk.gray('sess')} ` +
|
|
210
|
-
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(dur)}`
|
|
239
|
+
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(padToWidth(dur, 8))} ` +
|
|
240
|
+
`${chalk.cyan(String(stalls).padStart(stallW))} ${chalk.cyan(String(resumes))}`);
|
|
211
241
|
}
|
|
212
242
|
// Everything below is the whole scope folded together; per-group detail is in --json.
|
|
213
243
|
const all = newFacetAccumulator();
|
|
@@ -216,22 +246,54 @@ function renderReport(groups, dim, meta) {
|
|
|
216
246
|
renderCounts('Top tools', topEntries(all.toolCounts, 8), out);
|
|
217
247
|
renderCounts('Languages', topEntries(all.languages, 6), out);
|
|
218
248
|
renderCounts('Models', topEntries(all.models, 6), out);
|
|
249
|
+
renderCounts('Silent stalls by model', topEntries(all.silentStallsByModel ?? {}, 8), out);
|
|
219
250
|
// Friction — the section that earns the command.
|
|
220
251
|
const gaps = all.responseGaps;
|
|
252
|
+
const silentStalls = Object.entries(all.frictionSignals)
|
|
253
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
254
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
255
|
+
const resumeNudges = all.correctionSignals['resume after silent stall'] ?? 0;
|
|
221
256
|
out.push('');
|
|
222
257
|
out.push(chalk.bold('Friction'));
|
|
223
258
|
out.push(` ${padToWidth('interruptions', 18)} ${chalk.cyan(String(all.interruptions))}` +
|
|
224
259
|
chalk.gray(' turns you cut short'));
|
|
225
260
|
out.push(` ${padToWidth('tool errors', 18)} ${chalk.cyan(String(all.errorCount))}`);
|
|
226
261
|
if (gaps.length > 0) {
|
|
227
|
-
|
|
228
|
-
|
|
262
|
+
// Same timestamps as silent stalls; this line is the distribution. Silent
|
|
263
|
+
// stalls (below) are the agent-attributed long gaps after the model stopped.
|
|
264
|
+
out.push(` ${padToWidth('gap until next msg', 18)} ` +
|
|
265
|
+
chalk.cyan(`p50 ${Math.round(percentile(gaps, 50))}s`) + chalk.gray(` · p90 ${Math.round(percentile(gaps, 90))}s`) +
|
|
266
|
+
chalk.gray(' after assistant last spoke'));
|
|
267
|
+
}
|
|
268
|
+
if (silentStalls > 0) {
|
|
269
|
+
out.push(` ${padToWidth('silent stalls', 18)} ${chalk.cyan(String(silentStalls))}` +
|
|
270
|
+
chalk.gray(' agent idle ≥5m until you resumed (also in By ' + dim + ' table)'));
|
|
271
|
+
}
|
|
272
|
+
if (resumeNudges > 0) {
|
|
273
|
+
out.push(` ${padToWidth('resume nudges', 18)} ${chalk.cyan(String(resumeNudges))}` +
|
|
274
|
+
chalk.gray(' "continue"/"keep going" after a silent stall'));
|
|
229
275
|
}
|
|
230
276
|
const errs = topEntries(all.errorCategories, 6);
|
|
231
277
|
if (errs.length > 0) {
|
|
232
278
|
for (const e of errs)
|
|
233
279
|
out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
|
|
234
280
|
}
|
|
281
|
+
renderCounts('Friction / thrash', topEntries(all.frictionSignals, 10), out);
|
|
282
|
+
renderCounts('Dissatisfaction / corrections', topEntries(all.correctionSignals, 10), out);
|
|
283
|
+
renderCounts('Automatable repeats', topEntries(all.automationSignals, 10), out);
|
|
284
|
+
renderCounts('Harness split', harnesses, out);
|
|
285
|
+
out.push('');
|
|
286
|
+
out.push(chalk.bold('Actions'));
|
|
287
|
+
if (actions.length === 0) {
|
|
288
|
+
out.push(chalk.gray(' No repeated action pattern met the evidence threshold in this window.'));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
out.push(chalk.gray(' pri category evidence sample sessions action'));
|
|
292
|
+
for (const action of actions.slice(0, 12)) {
|
|
293
|
+
out.push(` ${padToWidth(action.priority, 7)} ${padToWidth(action.category, 11)} ` +
|
|
294
|
+
`${String(action.evidenceCount).padStart(8)} ${padToWidth(action.sampleSessionIds.join(', '), 25)} ${action.action}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
235
297
|
// Output
|
|
236
298
|
out.push('');
|
|
237
299
|
out.push(chalk.bold('What you changed'));
|
|
@@ -286,7 +348,7 @@ function renderReport(groups, dim, meta) {
|
|
|
286
348
|
out.push(chalk.yellow(` ${meta.unreadable} transcripts could not be read; their behaviour is missing from these totals.`));
|
|
287
349
|
}
|
|
288
350
|
if (all.gapsOverCeiling > 0) {
|
|
289
|
-
out.push(chalk.gray(` ${all.gapsOverCeiling}
|
|
351
|
+
out.push(chalk.gray(` ${all.gapsOverCeiling} gaps over an hour excluded from p50/p90 (still counted as silent stall: 1h+ when the assistant last spoke).`));
|
|
290
352
|
}
|
|
291
353
|
out.push('');
|
|
292
354
|
out.push(chalk.gray(' `agents insights --by project` to see it per repo'));
|
|
@@ -306,6 +368,13 @@ async function renderNarrative(payload) {
|
|
|
306
368
|
'2. What is costing you — split into the assistant\'s fault vs your own workflow.',
|
|
307
369
|
'3. Quick wins — concrete, tied to a number in the data.',
|
|
308
370
|
'4. Worth trying — one more ambitious workflow change.',
|
|
371
|
+
'',
|
|
372
|
+
'Silent stalls (required): if frictionSignals contain "silent stall: …" or',
|
|
373
|
+
'correctionSignals contain "resume after silent stall" / "continue / keep going",',
|
|
374
|
+
'call that out explicitly in section 2 or 3 with the counts. Those mean the model',
|
|
375
|
+
'stopped mid-session and sat idle until the human pinged it (timestamps: last',
|
|
376
|
+
'assistant event → next user message ≥ 5 minutes). Do not reframe them as the',
|
|
377
|
+
'user being slow unless the data only shows short reply gaps.',
|
|
309
378
|
'Be specific and cite the numbers. No preamble, no flattery, no bullet padding.',
|
|
310
379
|
'',
|
|
311
380
|
JSON.stringify(payload),
|
|
@@ -351,11 +420,11 @@ async function insightsAction(options) {
|
|
|
351
420
|
if (options.refresh)
|
|
352
421
|
clearSessionInsights();
|
|
353
422
|
const filter = { sinceMs };
|
|
354
|
-
if (options.agent)
|
|
355
|
-
filter.agent = options.agent;
|
|
356
423
|
const scanned = querySessions(filter);
|
|
357
424
|
const wanted = options.account?.toLowerCase();
|
|
358
425
|
const inScope = scanned.filter((m) => {
|
|
426
|
+
if (options.agent?.length && !options.agent.includes(m.agent))
|
|
427
|
+
return false;
|
|
359
428
|
if (!wanted)
|
|
360
429
|
return true;
|
|
361
430
|
return [m.accountKey, m.account, m.accountOrg]
|
|
@@ -381,6 +450,15 @@ async function insightsAction(options) {
|
|
|
381
450
|
});
|
|
382
451
|
const overlap = detectOverlap(spans);
|
|
383
452
|
const groups = buildGroups(substantive, facetsById, dim);
|
|
453
|
+
const evidence = substantive.flatMap((m) => {
|
|
454
|
+
const facets = facetsById.get(m.id);
|
|
455
|
+
return facets ? [{ id: m.id, facets }] : [];
|
|
456
|
+
});
|
|
457
|
+
const actions = buildInsightActions(evidence);
|
|
458
|
+
const harnesses = topEntries(substantive.reduce((counts, row) => {
|
|
459
|
+
counts[row.agent] = (counts[row.agent] ?? 0) + 1;
|
|
460
|
+
return counts;
|
|
461
|
+
}, {}), 20);
|
|
384
462
|
if (options.json) {
|
|
385
463
|
const payload = {
|
|
386
464
|
generatedAt: new Date().toISOString(),
|
|
@@ -392,6 +470,8 @@ async function insightsAction(options) {
|
|
|
392
470
|
minMessages,
|
|
393
471
|
by: dim,
|
|
394
472
|
overlap,
|
|
473
|
+
actions,
|
|
474
|
+
harnesses,
|
|
395
475
|
groups: groups.map((g) => ({
|
|
396
476
|
key: g.key,
|
|
397
477
|
label: g.label,
|
|
@@ -420,7 +500,7 @@ async function insightsAction(options) {
|
|
|
420
500
|
unreadable,
|
|
421
501
|
minMessages,
|
|
422
502
|
overlap,
|
|
423
|
-
});
|
|
503
|
+
}, actions, harnesses);
|
|
424
504
|
if (options.narrative) {
|
|
425
505
|
await renderNarrative(groups.map((g) => ({
|
|
426
506
|
account: g.label, sessions: g.sessions, costUsd: g.costUsd,
|
|
@@ -434,49 +514,77 @@ async function insightsAction(options) {
|
|
|
434
514
|
})));
|
|
435
515
|
}
|
|
436
516
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.description('How
|
|
517
|
+
function configureInsightsCommand(cmd) {
|
|
518
|
+
addHostOption(cmd)
|
|
519
|
+
.description('How work looks — behavioural report (default) or counter mix (`mix`, recipes)')
|
|
440
520
|
.option('--json', 'Output the full report as JSON')
|
|
441
521
|
.option('--since <time>', 'Window: 7d, 4w, 3mo, an ISO date, or "all" (default 30d)')
|
|
442
522
|
.option('--all', 'Every session ever indexed. Alias for --since all')
|
|
443
523
|
.option('--by <dimension>', 'Group by: account (default), agent, project, or day')
|
|
444
524
|
.option('--account <match>', 'Only sessions whose account key, email, or org contains this')
|
|
445
|
-
.option('--agent <id>', 'Only
|
|
525
|
+
.option('--agent <id>', 'Only these harnesses; repeat for more than one', collectAgent, [])
|
|
446
526
|
.option('--min-messages <n>', 'Skip sessions under this many messages, both roles counted (default 2)')
|
|
447
527
|
.option('--refresh', 'Discard cached facets and re-read every transcript')
|
|
448
528
|
.option('--narrative', 'Add a written read on the numbers via a headless `claude -p`')
|
|
449
529
|
.action(async (options) => {
|
|
450
|
-
|
|
530
|
+
const inherited = cmd.parent?.name() === 'sessions'
|
|
531
|
+
? cmd.parent.opts()
|
|
532
|
+
: {};
|
|
533
|
+
const inheritedAgent = typeof inherited.agent === 'string' ? [inherited.agent] : [];
|
|
534
|
+
const rawAgents = agentsFromArgv(process.argv.slice(2));
|
|
535
|
+
await insightsAction({
|
|
536
|
+
...inherited,
|
|
537
|
+
...options,
|
|
538
|
+
agent: rawAgents.length > 0 ? rawAgents : [...inheritedAgent, ...(options.agent ?? [])],
|
|
539
|
+
json: options.json ?? inherited.json,
|
|
540
|
+
since: options.since ?? inherited.since,
|
|
541
|
+
});
|
|
451
542
|
});
|
|
543
|
+
// Cheap counter recipes (former top-level `agents trends`) — same parent, no peer verb.
|
|
544
|
+
registerMixCommands(cmd);
|
|
452
545
|
setHelpSections(cmd, {
|
|
453
546
|
examples: `
|
|
454
|
-
#
|
|
547
|
+
# Behavioural report — last 30 days, split by Claude account (default)
|
|
455
548
|
agents insights
|
|
456
549
|
|
|
457
550
|
# Which repo is eating the time
|
|
458
551
|
agents insights --by project --since 90d
|
|
459
552
|
|
|
553
|
+
# Counter mix board (harness/model/token/secrets recipes) — former agents trends
|
|
554
|
+
agents insights mix
|
|
555
|
+
agents insights mix --days 30
|
|
556
|
+
agents insights harness-mix --json
|
|
557
|
+
agents insights query --kind secret --days 7
|
|
558
|
+
|
|
460
559
|
# One account only, all of its history
|
|
461
560
|
agents insights --account "Turing Labs" --all
|
|
462
561
|
|
|
463
562
|
# Machine-readable, for a dashboard or a slash command
|
|
464
|
-
agents insights --json
|
|
563
|
+
agents sessions insights --agent claude --agent codex --json
|
|
465
564
|
|
|
466
565
|
# Add a written read on what to change
|
|
467
566
|
agents insights --narrative
|
|
468
567
|
`,
|
|
469
568
|
notes: `
|
|
470
|
-
|
|
471
|
-
|
|
569
|
+
Two paths under one verb:
|
|
570
|
+
bare \`agents insights\` — transcript behaviour (tools, friction, rhythm, by account)
|
|
571
|
+
\`agents insights mix\` — cheap counters from sessions.db + usage.db
|
|
572
|
+
Latency is \`agents perf\` (not mix). Quota is \`agents usage\`. Skill/slash popularity
|
|
573
|
+
is \`agents sessions stats\`. \`agents trends\` is a deprecated alias of the mix tree.
|
|
472
574
|
|
|
473
|
-
The
|
|
575
|
+
The behavioural report parses in-scope transcripts once and caches facets; later runs
|
|
474
576
|
re-read only files that changed. \`--refresh\` forces a full re-read.
|
|
475
577
|
|
|
476
|
-
|
|
477
|
-
|
|
578
|
+
\`agents insights\` is the top-level alias of \`agents sessions insights\`.
|
|
579
|
+
Repeat \`--agent\` to compare several harnesses in one report.
|
|
478
580
|
|
|
479
581
|
Everything except \`--narrative\` is local and makes no network calls.
|
|
480
582
|
`,
|
|
481
583
|
});
|
|
482
584
|
}
|
|
585
|
+
export function registerInsightsCommand(program) {
|
|
586
|
+
configureInsightsCommand(program.command('insights'));
|
|
587
|
+
}
|
|
588
|
+
export function registerSessionsInsightsCommand(sessions) {
|
|
589
|
+
configureInsightsCommand(sessions.command('insights'));
|
|
590
|
+
}
|
package/dist/commands/output.js
CHANGED
|
@@ -12,6 +12,7 @@ import { terminalWidth, truncateToWidth, padToWidth } from '../lib/session/width
|
|
|
12
12
|
import { collectGitOutput } from '../lib/output/git-output.js';
|
|
13
13
|
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
14
14
|
import { machineId } from '../lib/session/sync/config.js';
|
|
15
|
+
import { stripClixml } from '../lib/hosts/remote-cmd.js';
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
export function registerOutputCommand(program) {
|
|
17
18
|
addHostOption(program.command('output'))
|
|
@@ -24,16 +25,21 @@ export function registerOutputCommand(program) {
|
|
|
24
25
|
.option('--login <login...>', 'Count PRs for these GitHub logins (default: current gh user)')
|
|
25
26
|
.option('--no-prs', 'Skip the GitHub PR lookup (commits only)')
|
|
26
27
|
.option('--all-hosts', 'Aggregate across every online device (ag devices) over SSH')
|
|
28
|
+
.option('--pricing <scenario>', 'Cost scenario: actual (default, cache-discounted) or no-cache (cache read/write billed at the full input rate)')
|
|
27
29
|
.addHelpText('after', `
|
|
28
30
|
Examples:
|
|
29
31
|
agents output Last 7 days: burn, output tokens, PRs, commits, ratios
|
|
30
32
|
agents output --since 24h Last 24 hours
|
|
31
33
|
agents output --since 1mo Last month (units: 1h 24h 7d 4w 1mo 1y, or ISO date)
|
|
34
|
+
agents output --pricing no-cache Model the burn as if prompt caching were off
|
|
32
35
|
agents output --all-hosts Fleet-wide, folding in every online machine
|
|
33
36
|
agents output --by day --json Machine-readable daily burn/output rollup
|
|
34
37
|
|
|
35
38
|
Burn (cost) is computed offline from a versioned per-model price table (${PRICING_VERSION}).
|
|
36
39
|
Output tokens are the real generated tokens — NOT the cache-inflated total token count.
|
|
40
|
+
The burn is split into input / cache-read / cache-write where the harness records it
|
|
41
|
+
(Claude/Codex/Gemini/Droid). --pricing no-cache reprices cached tokens at the input rate,
|
|
42
|
+
so you can see what caching is saving. --json always carries both actual and no-cache costs.
|
|
37
43
|
`)
|
|
38
44
|
.action(async (options) => {
|
|
39
45
|
await outputAction(options);
|
|
@@ -47,6 +53,14 @@ function resolveGroup(by) {
|
|
|
47
53
|
console.error(chalk.red('error: --by must be one of: agent, project, day, account'));
|
|
48
54
|
process.exit(1);
|
|
49
55
|
}
|
|
56
|
+
function resolvePricing(pricing) {
|
|
57
|
+
if (pricing === undefined || pricing === 'actual')
|
|
58
|
+
return 'actual';
|
|
59
|
+
if (pricing === 'no-cache')
|
|
60
|
+
return 'no-cache';
|
|
61
|
+
console.error(chalk.red('error: --pricing must be one of: actual, no-cache'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
50
64
|
/** Compact token formatter: 38.6M, 4.1K, 10.6B. */
|
|
51
65
|
function formatCompact(n) {
|
|
52
66
|
const abs = Math.abs(n);
|
|
@@ -59,13 +73,27 @@ function formatCompact(n) {
|
|
|
59
73
|
return String(n);
|
|
60
74
|
}
|
|
61
75
|
function emptyBurn() {
|
|
62
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
costUsd: 0,
|
|
78
|
+
costUsdNoCache: 0,
|
|
79
|
+
outputTokens: 0,
|
|
80
|
+
tokenCount: 0,
|
|
81
|
+
inputTokens: 0,
|
|
82
|
+
cacheReadTokens: 0,
|
|
83
|
+
cacheWriteTokens: 0,
|
|
84
|
+
sessionCount: 0,
|
|
85
|
+
durationMs: 0,
|
|
86
|
+
};
|
|
63
87
|
}
|
|
64
88
|
function sumBurn(rows) {
|
|
65
89
|
return rows.reduce((acc, r) => {
|
|
66
90
|
acc.costUsd += r.costUsd;
|
|
91
|
+
acc.costUsdNoCache += r.costUsdNoCache;
|
|
67
92
|
acc.outputTokens += r.outputTokens;
|
|
68
93
|
acc.tokenCount += r.tokenCount;
|
|
94
|
+
acc.inputTokens += r.inputTokens;
|
|
95
|
+
acc.cacheReadTokens += r.cacheReadTokens;
|
|
96
|
+
acc.cacheWriteTokens += r.cacheWriteTokens;
|
|
69
97
|
acc.sessionCount += r.sessionCount;
|
|
70
98
|
acc.durationMs += r.durationMs;
|
|
71
99
|
return acc;
|
|
@@ -122,7 +150,9 @@ async function fetchRemotePayload(device, options) {
|
|
|
122
150
|
timeout: 120_000,
|
|
123
151
|
maxBuffer: 64 * 1024 * 1024,
|
|
124
152
|
});
|
|
125
|
-
|
|
153
|
+
// A Windows device relays its payload through PowerShell, which can prefix a
|
|
154
|
+
// CLIXML banner ahead of the JSON — strip it before parsing (RUSH-2286).
|
|
155
|
+
const parsed = JSON.parse(stripClixml(stdout));
|
|
126
156
|
parsed.machine = parsed.machine || device;
|
|
127
157
|
return parsed;
|
|
128
158
|
}
|
|
@@ -141,13 +171,17 @@ async function fetchRemotePayload(device, options) {
|
|
|
141
171
|
}
|
|
142
172
|
async function outputAction(options) {
|
|
143
173
|
const includePrs = options.prs !== false;
|
|
174
|
+
// Validate --pricing up front so a bad value errors even under --json. The JSON
|
|
175
|
+
// payload always carries BOTH costs regardless of the scenario — the flag only
|
|
176
|
+
// chooses which one the text renderer leads with.
|
|
177
|
+
const scenario = resolvePricing(options.pricing);
|
|
144
178
|
if (!options.allHosts) {
|
|
145
179
|
const payload = await computeLocalPayload(options, includePrs);
|
|
146
180
|
if (options.json) {
|
|
147
181
|
process.stdout.write(JSON.stringify(withRatios(payload, [payload]), null, 2) + '\n');
|
|
148
182
|
return;
|
|
149
183
|
}
|
|
150
|
-
renderSingle(payload);
|
|
184
|
+
renderSingle(payload, scenario);
|
|
151
185
|
return;
|
|
152
186
|
}
|
|
153
187
|
// Fleet: local payload + every online device, folded in over SSH.
|
|
@@ -166,7 +200,7 @@ async function outputAction(options) {
|
|
|
166
200
|
process.stdout.write(JSON.stringify(withRatios(mergeMachines(machines, options), machines), null, 2) + '\n');
|
|
167
201
|
return;
|
|
168
202
|
}
|
|
169
|
-
renderFleet(machines, options);
|
|
203
|
+
renderFleet(machines, options, scenario);
|
|
170
204
|
}
|
|
171
205
|
/** Merge per-machine payloads into a combined one (burn + commits summed; PRs local-only). */
|
|
172
206
|
function mergeMachines(machines, options) {
|
|
@@ -179,8 +213,12 @@ function mergeMachines(machines, options) {
|
|
|
179
213
|
const uncosted = new Set();
|
|
180
214
|
for (const m of machines) {
|
|
181
215
|
burn.costUsd += m.burn.costUsd;
|
|
216
|
+
burn.costUsdNoCache += m.burn.costUsdNoCache;
|
|
182
217
|
burn.outputTokens += m.burn.outputTokens;
|
|
183
218
|
burn.tokenCount += m.burn.tokenCount;
|
|
219
|
+
burn.inputTokens += m.burn.inputTokens;
|
|
220
|
+
burn.cacheReadTokens += m.burn.cacheReadTokens;
|
|
221
|
+
burn.cacheWriteTokens += m.burn.cacheWriteTokens;
|
|
184
222
|
burn.sessionCount += m.burn.sessionCount;
|
|
185
223
|
burn.durationMs += m.burn.durationMs;
|
|
186
224
|
for (const s of m.output.commitShas)
|
|
@@ -188,15 +226,23 @@ function mergeMachines(machines, options) {
|
|
|
188
226
|
for (const a of m.uncostedAgents)
|
|
189
227
|
uncosted.add(a);
|
|
190
228
|
for (const r of m.breakdown.rows) {
|
|
191
|
-
const cur = byKey.get(r.key) ?? {
|
|
229
|
+
const cur = byKey.get(r.key) ?? {
|
|
230
|
+
key: r.key, label: r.label, costUsd: 0, costUsdNoCache: 0, durationMs: 0,
|
|
231
|
+
sessionCount: 0, tokenCount: 0, outputTokens: 0,
|
|
232
|
+
inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
233
|
+
};
|
|
192
234
|
// Peers resolve their own labels; keep the first non-empty one so a machine that
|
|
193
235
|
// has not indexed an account yet does not blank a label another machine supplied.
|
|
194
236
|
cur.label ??= r.label;
|
|
195
237
|
cur.costUsd += r.costUsd;
|
|
238
|
+
cur.costUsdNoCache += r.costUsdNoCache;
|
|
196
239
|
cur.durationMs += r.durationMs;
|
|
197
240
|
cur.sessionCount += r.sessionCount;
|
|
198
241
|
cur.tokenCount += r.tokenCount;
|
|
199
242
|
cur.outputTokens += r.outputTokens;
|
|
243
|
+
cur.inputTokens += r.inputTokens;
|
|
244
|
+
cur.cacheReadTokens += r.cacheReadTokens;
|
|
245
|
+
cur.cacheWriteTokens += r.cacheWriteTokens;
|
|
200
246
|
byKey.set(r.key, cur);
|
|
201
247
|
}
|
|
202
248
|
}
|
|
@@ -226,47 +272,76 @@ function withRatios(payload, machines) {
|
|
|
226
272
|
machines: machines.length > 1 ? machines.map(m => ({ machine: m.machine, burn: m.burn, commits: m.output.commits, error: m.error })) : undefined,
|
|
227
273
|
};
|
|
228
274
|
}
|
|
229
|
-
/**
|
|
230
|
-
function
|
|
275
|
+
/** The cost the text renderer leads with for the chosen scenario. */
|
|
276
|
+
function scenarioCost(x, scenario) {
|
|
277
|
+
return scenario === 'no-cache' ? x.costUsdNoCache : x.costUsd;
|
|
278
|
+
}
|
|
279
|
+
/** Shared header line: burned · output tokens · PRs · commits, plus ratios + burn split. */
|
|
280
|
+
function headerLines(payload, scenario) {
|
|
231
281
|
const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
|
|
282
|
+
const burn = payload.burn;
|
|
283
|
+
const cost = scenarioCost(burn, scenario);
|
|
232
284
|
const out = [];
|
|
233
285
|
out.push(' ' +
|
|
234
|
-
`${chalk.green(formatUsd(
|
|
286
|
+
`${chalk.green(formatUsd(cost))} burned${scenario === 'no-cache' ? chalk.gray(' (no-cache)') : ''}` +
|
|
235
287
|
chalk.gray(' · ') +
|
|
236
|
-
`${chalk.cyan(formatCompact(
|
|
288
|
+
`${chalk.cyan(formatCompact(burn.outputTokens))} output tokens` +
|
|
237
289
|
chalk.gray(' · ') +
|
|
238
290
|
`${chalk.yellow(String(prsTotal))} PRs ${chalk.gray(`(${payload.output.prsMerged} merged)`)}` +
|
|
239
291
|
chalk.gray(' · ') +
|
|
240
292
|
`${chalk.yellow(String(payload.output.commits))} commits`);
|
|
241
293
|
const ratios = [];
|
|
242
294
|
if (prsTotal > 0)
|
|
243
|
-
ratios.push(`${formatUsd(
|
|
295
|
+
ratios.push(`${formatUsd(cost / prsTotal)}/PR`);
|
|
244
296
|
if (payload.output.commits > 0)
|
|
245
|
-
ratios.push(`${formatUsd(
|
|
246
|
-
if (
|
|
247
|
-
ratios.push(`${formatCompact(Math.round(
|
|
297
|
+
ratios.push(`${formatUsd(cost / payload.output.commits)}/commit`);
|
|
298
|
+
if (cost > 0)
|
|
299
|
+
ratios.push(`${formatCompact(Math.round(burn.outputTokens / cost))} out-tok/$`);
|
|
248
300
|
if (ratios.length > 0)
|
|
249
301
|
out.push(chalk.gray(' ' + ratios.join(' · ')));
|
|
302
|
+
// Burn split — only for harnesses that recorded one (input/cache totals > 0).
|
|
303
|
+
const splitTotal = burn.inputTokens + burn.cacheReadTokens + burn.cacheWriteTokens;
|
|
304
|
+
if (splitTotal > 0) {
|
|
305
|
+
out.push(chalk.gray(' burn split: ') +
|
|
306
|
+
`${chalk.cyan(formatCompact(burn.inputTokens))} input` +
|
|
307
|
+
chalk.gray(' · ') +
|
|
308
|
+
`${chalk.cyan(formatCompact(burn.cacheReadTokens))} cache-read` +
|
|
309
|
+
chalk.gray(' · ') +
|
|
310
|
+
`${chalk.cyan(formatCompact(burn.cacheWriteTokens))} cache-write`);
|
|
311
|
+
}
|
|
312
|
+
// No-cache comparison — shown whenever caching actually moved the number, so an
|
|
313
|
+
// operator sees the saving in `actual` mode too (RUSH-2287: "both if useful").
|
|
314
|
+
if (burn.costUsdNoCache > burn.costUsd && burn.costUsd > 0) {
|
|
315
|
+
const saved = burn.costUsdNoCache - burn.costUsd;
|
|
316
|
+
const pct = Math.round((saved / burn.costUsdNoCache) * 100);
|
|
317
|
+
out.push(chalk.gray(' caching: ') +
|
|
318
|
+
`actual ${chalk.green(formatUsd(burn.costUsd))}` +
|
|
319
|
+
chalk.gray(' vs ') +
|
|
320
|
+
`no-cache ${chalk.yellow(formatUsd(burn.costUsdNoCache))}` +
|
|
321
|
+
chalk.gray(` (saved ${formatUsd(saved)}, ${pct}%)`));
|
|
322
|
+
}
|
|
250
323
|
return out;
|
|
251
324
|
}
|
|
252
325
|
/** Render the per-group burn/output table. */
|
|
253
|
-
function renderBreakdown(rows, groupBy) {
|
|
326
|
+
function renderBreakdown(rows, groupBy, scenario) {
|
|
254
327
|
const out = [chalk.bold(`By ${groupBy}`)];
|
|
255
328
|
if (rows.length === 0)
|
|
256
329
|
return out;
|
|
330
|
+
const rowCost = (r) => scenarioCost(r, scenario);
|
|
257
331
|
const cols = terminalWidth();
|
|
258
|
-
const
|
|
332
|
+
const burnHeader = scenario === 'no-cache' ? 'burn(nc)' : 'burn';
|
|
333
|
+
const burnW = Math.max(...rows.map(r => formatUsd(rowCost(r)).length), burnHeader.length);
|
|
259
334
|
const outW = Math.max(...rows.map(r => formatCompact(r.outputTokens).length), 6);
|
|
260
335
|
const sessW = Math.max(...rows.map(r => String(r.sessionCount).length), 3);
|
|
261
336
|
const fixedW = 2 + 2 + burnW + 2 + outW + 2 + sessW + 8;
|
|
262
337
|
const display = (r) => r.label ?? r.key;
|
|
263
338
|
const keyW = Math.max(8, Math.min(Math.max(...rows.map(r => display(r).length), groupBy.length), cols - fixedW));
|
|
264
|
-
out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth(
|
|
339
|
+
out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth(burnHeader, burnW)) + ' ' + chalk.gray(padToWidth('output', outW)) + ' ' + chalk.gray('sessions'));
|
|
265
340
|
for (const r of rows) {
|
|
266
341
|
out.push(' ' +
|
|
267
342
|
padToWidth(truncateToWidth(display(r), keyW), keyW) +
|
|
268
343
|
' ' +
|
|
269
|
-
chalk.green(padToWidth(formatUsd(r
|
|
344
|
+
chalk.green(padToWidth(formatUsd(rowCost(r)), burnW)) +
|
|
270
345
|
' ' +
|
|
271
346
|
chalk.cyan(padToWidth(formatCompact(r.outputTokens), outW)) +
|
|
272
347
|
' ' +
|
|
@@ -274,17 +349,17 @@ function renderBreakdown(rows, groupBy) {
|
|
|
274
349
|
}
|
|
275
350
|
return out;
|
|
276
351
|
}
|
|
277
|
-
function renderSingle(payload) {
|
|
352
|
+
function renderSingle(payload, scenario) {
|
|
278
353
|
const out = [];
|
|
279
354
|
out.push(chalk.bold('Output') + chalk.gray(` · pricing ${payload.pricingVersion} · since ${payload.since}`));
|
|
280
|
-
out.push(...headerLines(payload));
|
|
355
|
+
out.push(...headerLines(payload, scenario));
|
|
281
356
|
out.push('');
|
|
282
357
|
if (payload.burn.sessionCount === 0) {
|
|
283
358
|
out.push(chalk.gray('No sessions with cost data found. Run `agents sessions --all` to index, then retry.'));
|
|
284
359
|
console.log(out.join('\n'));
|
|
285
360
|
return;
|
|
286
361
|
}
|
|
287
|
-
out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by));
|
|
362
|
+
out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by, scenario));
|
|
288
363
|
out.push('');
|
|
289
364
|
out.push(chalk.bold('Shipped'));
|
|
290
365
|
out.push(` ${chalk.yellow(String(payload.output.commits))} commits across ${payload.output.reposScanned} repos` + chalk.gray(` (authors: ${payload.output.authors.length > 0 ? payload.output.authors.join(', ') : 'none detected'})`));
|
|
@@ -300,22 +375,22 @@ function renderSingle(payload) {
|
|
|
300
375
|
out.push(chalk.gray(`agent wall-clock: ${formatDuration(payload.burn.durationMs)}`));
|
|
301
376
|
console.log(out.join('\n'));
|
|
302
377
|
}
|
|
303
|
-
function renderFleet(machines, options) {
|
|
378
|
+
function renderFleet(machines, options, scenario) {
|
|
304
379
|
const merged = mergeMachines(machines, options);
|
|
305
380
|
const out = [];
|
|
306
381
|
out.push(chalk.bold('Output') + chalk.gray(` · fleet (${machines.length} machines) · pricing ${merged.pricingVersion} · since ${merged.since}`));
|
|
307
|
-
out.push(...headerLines(merged));
|
|
382
|
+
out.push(...headerLines(merged, scenario));
|
|
308
383
|
out.push('');
|
|
309
384
|
// By machine.
|
|
310
385
|
out.push(chalk.bold('By machine'));
|
|
311
386
|
const nameW = Math.max(...machines.map(m => m.machine.length), 7);
|
|
312
|
-
const burnW = Math.max(...machines.map(m => formatUsd(m.burn
|
|
387
|
+
const burnW = Math.max(...machines.map(m => formatUsd(scenarioCost(m.burn, scenario)).length), 4);
|
|
313
388
|
for (const m of machines) {
|
|
314
389
|
const note = m.error ? chalk.red(` (${m.error})`) : '';
|
|
315
390
|
out.push(' ' +
|
|
316
391
|
padToWidth(m.machine, nameW) +
|
|
317
392
|
' ' +
|
|
318
|
-
chalk.green(padToWidth(formatUsd(m.burn
|
|
393
|
+
chalk.green(padToWidth(formatUsd(scenarioCost(m.burn, scenario)), burnW)) +
|
|
319
394
|
' ' +
|
|
320
395
|
chalk.cyan(padToWidth(formatCompact(m.burn.outputTokens), 7)) +
|
|
321
396
|
' ' +
|
|
@@ -323,7 +398,7 @@ function renderFleet(machines, options) {
|
|
|
323
398
|
note);
|
|
324
399
|
}
|
|
325
400
|
out.push('');
|
|
326
|
-
out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by));
|
|
401
|
+
out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by, scenario));
|
|
327
402
|
out.push('');
|
|
328
403
|
out.push(chalk.gray(notCountedLine(merged.uncostedAgents)));
|
|
329
404
|
console.log(out.join('\n'));
|