@phnx-labs/agents-cli 1.22.26 → 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.
Files changed (131) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/README.md +18 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/bench.d.ts +2 -0
  5. package/dist/commands/bench.js +101 -0
  6. package/dist/commands/doctor.js +24 -10
  7. package/dist/commands/exec.js +17 -16
  8. package/dist/commands/insights.js +39 -5
  9. package/dist/commands/output.js +100 -25
  10. package/dist/commands/resume.d.ts +11 -0
  11. package/dist/commands/resume.js +51 -0
  12. package/dist/commands/sessions-picker.js +12 -7
  13. package/dist/commands/sessions.d.ts +47 -1
  14. package/dist/commands/sessions.js +345 -37
  15. package/dist/commands/ssh.js +184 -11
  16. package/dist/commands/teams.js +3 -3
  17. package/dist/index.js +16 -20
  18. package/dist/lib/auth-health.d.ts +47 -3
  19. package/dist/lib/auth-health.js +74 -15
  20. package/dist/lib/bench/index.d.ts +4 -0
  21. package/dist/lib/bench/index.js +4 -0
  22. package/dist/lib/bench/runner.d.ts +16 -0
  23. package/dist/lib/bench/runner.js +111 -0
  24. package/dist/lib/bench/schema.d.ts +5 -0
  25. package/dist/lib/bench/schema.js +91 -0
  26. package/dist/lib/bench/storage.d.ts +5 -0
  27. package/dist/lib/bench/storage.js +32 -0
  28. package/dist/lib/bench/types.d.ts +40 -0
  29. package/dist/lib/bench/types.js +1 -0
  30. package/dist/lib/claude-account-token.d.ts +2 -0
  31. package/dist/lib/claude-account-token.js +27 -1
  32. package/dist/lib/crabbox/cli.d.ts +2 -0
  33. package/dist/lib/crabbox/cli.js +2 -0
  34. package/dist/lib/crabbox/lease.js +7 -1
  35. package/dist/lib/daemon.js +20 -0
  36. package/dist/lib/devices/connect.d.ts +2 -0
  37. package/dist/lib/devices/connect.js +7 -0
  38. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  39. package/dist/lib/devices/doctor-findings.js +15 -1
  40. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  41. package/dist/lib/devices/harness-inventory.js +0 -0
  42. package/dist/lib/devices/registry.d.ts +2 -0
  43. package/dist/lib/devices/ssh-config.js +3 -0
  44. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  45. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  46. package/dist/lib/exec.bench.d.ts +1 -0
  47. package/dist/lib/exec.bench.js +186 -0
  48. package/dist/lib/exec.js +18 -8
  49. package/dist/lib/fleet/apply.js +9 -7
  50. package/dist/lib/fleet/remote-login.d.ts +4 -3
  51. package/dist/lib/fleet/remote-login.js +11 -9
  52. package/dist/lib/gemini-settings.d.ts +0 -1
  53. package/dist/lib/gemini-settings.js +12 -7
  54. package/dist/lib/hooks.d.ts +8 -5
  55. package/dist/lib/hooks.js +10 -6
  56. package/dist/lib/hosts/dispatch.js +7 -3
  57. package/dist/lib/hosts/passthrough.d.ts +22 -0
  58. package/dist/lib/hosts/passthrough.js +14 -8
  59. package/dist/lib/hosts/progress.d.ts +2 -1
  60. package/dist/lib/hosts/progress.js +4 -3
  61. package/dist/lib/hosts/providers/devices.js +1 -0
  62. package/dist/lib/hosts/ready.d.ts +4 -2
  63. package/dist/lib/hosts/ready.js +24 -8
  64. package/dist/lib/hosts/reconcile.d.ts +1 -1
  65. package/dist/lib/hosts/reconcile.js +16 -7
  66. package/dist/lib/hosts/reconnect.d.ts +45 -12
  67. package/dist/lib/hosts/reconnect.js +90 -34
  68. package/dist/lib/hosts/registry.d.ts +2 -2
  69. package/dist/lib/hosts/registry.js +3 -5
  70. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  71. package/dist/lib/hosts/remote-cmd.js +29 -0
  72. package/dist/lib/hosts/tasks.d.ts +2 -0
  73. package/dist/lib/hosts/types.d.ts +1 -0
  74. package/dist/lib/hosts/types.js +3 -0
  75. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  76. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  77. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  78. package/dist/lib/menubar/install-menubar.js +20 -9
  79. package/dist/lib/models.d.ts +30 -1
  80. package/dist/lib/models.js +42 -144
  81. package/dist/lib/pricing/cost.d.ts +9 -0
  82. package/dist/lib/pricing/cost.js +24 -0
  83. package/dist/lib/pricing/index.d.ts +1 -1
  84. package/dist/lib/pricing/index.js +1 -1
  85. package/dist/lib/redact.js +8 -3
  86. package/dist/lib/remote-agents-json.d.ts +32 -0
  87. package/dist/lib/remote-agents-json.js +47 -16
  88. package/dist/lib/resource-profiles.js +1 -2
  89. package/dist/lib/routine-notify-owner.d.ts +102 -0
  90. package/dist/lib/routine-notify-owner.js +232 -0
  91. package/dist/lib/routines.d.ts +10 -0
  92. package/dist/lib/runner.d.ts +9 -1
  93. package/dist/lib/runner.js +182 -17
  94. package/dist/lib/sandbox.d.ts +0 -2
  95. package/dist/lib/sandbox.js +2 -19
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.js +3 -2
  99. package/dist/lib/secrets/lease.d.ts +25 -0
  100. package/dist/lib/secrets/lease.js +44 -0
  101. package/dist/lib/self-update.d.ts +20 -5
  102. package/dist/lib/self-update.js +93 -16
  103. package/dist/lib/session/db.d.ts +19 -2
  104. package/dist/lib/session/db.js +54 -5
  105. package/dist/lib/session/discover.d.ts +21 -1
  106. package/dist/lib/session/discover.js +124 -32
  107. package/dist/lib/session/insights.d.ts +19 -0
  108. package/dist/lib/session/insights.js +78 -10
  109. package/dist/lib/session/recovery.d.ts +17 -1
  110. package/dist/lib/session/recovery.js +111 -4
  111. package/dist/lib/session/remote-list.d.ts +10 -9
  112. package/dist/lib/session/remote-list.js +25 -23
  113. package/dist/lib/session/resume-owner.d.ts +55 -0
  114. package/dist/lib/session/resume-owner.js +69 -0
  115. package/dist/lib/session/team-filter.d.ts +65 -0
  116. package/dist/lib/session/team-filter.js +98 -3
  117. package/dist/lib/session/types.d.ts +33 -0
  118. package/dist/lib/smart-launch.d.ts +31 -5
  119. package/dist/lib/smart-launch.js +43 -7
  120. package/dist/lib/ssh-exec.d.ts +27 -0
  121. package/dist/lib/ssh-exec.js +34 -1
  122. package/dist/lib/ssh-tunnel.d.ts +3 -2
  123. package/dist/lib/ssh-tunnel.js +25 -16
  124. package/dist/lib/startup/command-registry.d.ts +1 -0
  125. package/dist/lib/startup/command-registry.js +2 -0
  126. package/dist/lib/teams/placement-probe.d.ts +1 -1
  127. package/dist/lib/teams/placement-probe.js +24 -24
  128. package/dist/lib/tmux/session.d.ts +8 -0
  129. package/dist/lib/tmux/session.js +22 -0
  130. package/dist/lib/types.d.ts +2 -0
  131. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { GLOBAL_HARNESS } from './scope.js';
3
+ export const MIN_LEASE_MS = 60 * 1000;
4
+ export const MAX_LEASE_MS = 30 * 24 * 60 * 60 * 1000;
5
+ export function clampLeaseTtlMs(ttlMs) {
6
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
7
+ throw new Error('Lease duration must be a positive finite number.');
8
+ }
9
+ return Math.min(Math.max(Math.floor(ttlMs), MIN_LEASE_MS), MAX_LEASE_MS);
10
+ }
11
+ export function normalizeLeaseKeys(keys, availableKeys) {
12
+ const requested = [...new Set(keys.map((key) => key.trim()).filter(Boolean))].sort();
13
+ if (requested.length === 0)
14
+ throw new Error('A secret lease requires at least one key.');
15
+ const available = new Set(availableKeys);
16
+ const missing = requested.filter((key) => !available.has(key));
17
+ if (missing.length > 0)
18
+ throw new Error(`Unknown secret lease key(s): ${missing.join(', ')}`);
19
+ return requested;
20
+ }
21
+ export function createSecretLease(input) {
22
+ const createdAt = input.now ?? Date.now();
23
+ const ttlMs = clampLeaseTtlMs(input.ttlMs);
24
+ return {
25
+ id: input.id ?? randomBytes(12).toString('hex'),
26
+ bundle: input.bundle,
27
+ keys: normalizeLeaseKeys(input.keys, input.availableKeys),
28
+ createdAt,
29
+ expiresAt: createdAt + ttlMs,
30
+ harness: input.harness ?? GLOBAL_HARNESS,
31
+ sleepPersist: input.sleepPersist ?? false,
32
+ };
33
+ }
34
+ export function leaseIsActive(lease, now = Date.now()) {
35
+ return now < lease.expiresAt;
36
+ }
37
+ export function selectLeasedEnv(lease, env, now = Date.now()) {
38
+ if (!leaseIsActive(lease, now))
39
+ throw new Error(`Secret lease '${lease.id}' has expired.`);
40
+ const missing = lease.keys.filter((key) => typeof env[key] !== 'string');
41
+ if (missing.length > 0)
42
+ throw new Error(`Secret lease '${lease.id}' is missing value(s): ${missing.join(', ')}`);
43
+ return Object.fromEntries(lease.keys.map((key) => [key, env[key]]));
44
+ }
@@ -136,15 +136,30 @@ export declare function verifyInstalledVersion(packageRoot: string, expectedVers
136
136
  */
137
137
  export declare function refreshAliasShims(packageRoot: string): void;
138
138
  export interface AgentsCliInstall {
139
- /** The PATH entry (`<dir>/agents`) that resolves to this install. */
140
- binPath: string;
139
+ /** The PATH entry (`<dir>/agents`) that resolves to this install, when found through PATH. */
140
+ binPath?: string;
141
141
  /** Package root containing package.json and dist/. */
142
142
  packageRoot: string;
143
143
  version: string;
144
+ /** Whether this copy uses the serialized, atomic helper-bundle installer. */
145
+ atomicHelperInstall: boolean;
144
146
  }
147
+ export interface FindAgentsCliInstallsOptions {
148
+ homeDir?: string;
149
+ fnmDir?: string;
150
+ npmCacheDir?: string;
151
+ globalNodeModulesDirs?: string[];
152
+ }
153
+ export interface MultiInstallInventoryEntry {
154
+ packageRoot: string;
155
+ version: string;
156
+ note: string;
157
+ }
158
+ export declare function buildMultiInstallInventory(runningRoot: string, runningVersion: string, installs: AgentsCliInstall[]): MultiInstallInventoryEntry[];
145
159
  /**
146
- * Scan PATH for `agents` entrypoints and resolve each to the agents-cli
147
- * package root it executes. More than one distinct root means upgrades,
160
+ * Resolve every `agents` entrypoint on PATH, then inspect the bounded global
161
+ * install layouts used by NVM, fnm, Volta, Bun, npm, and npx. More than one
162
+ * distinct package root means upgrades,
148
163
  * shims, and the command the user types can act on different copies — the
149
164
  * divergence behind silently-failing self-updates.
150
165
  *
@@ -158,4 +173,4 @@ export interface AgentsCliInstall {
158
173
  * @phnx-labs/agents-cli is some other tool and is skipped.
159
174
  * POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
160
175
  */
161
- export declare function findAgentsCliInstalls(pathEnv: string): AgentsCliInstall[];
176
+ export declare function findAgentsCliInstalls(pathEnv: string, opts?: FindAgentsCliInstallsOptions): AgentsCliInstall[];
@@ -327,9 +327,89 @@ function packageRootForEntry(real) {
327
327
  }
328
328
  return null;
329
329
  }
330
+ export function buildMultiInstallInventory(runningRoot, runningVersion, installs) {
331
+ const byRoot = new Map();
332
+ byRoot.set(runningRoot, { packageRoot: runningRoot, version: runningVersion, note: 'running' });
333
+ for (const install of installs) {
334
+ const notes = [install.packageRoot === runningRoot
335
+ ? 'running'
336
+ : install.binPath
337
+ ? `agents on PATH: ${install.binPath}`
338
+ : 'discovered install'];
339
+ if (!install.atomicHelperInstall)
340
+ notes.push('unsafe legacy helper installer — remove this copy');
341
+ byRoot.set(install.packageRoot, {
342
+ packageRoot: install.packageRoot,
343
+ version: install.version,
344
+ note: notes.join('; '),
345
+ });
346
+ }
347
+ return [...byRoot.values()];
348
+ }
349
+ function childDirectories(parent) {
350
+ try {
351
+ return fs.readdirSync(parent, { withFileTypes: true })
352
+ .filter((entry) => entry.isDirectory())
353
+ .map((entry) => path.join(parent, entry.name));
354
+ }
355
+ catch {
356
+ return [];
357
+ }
358
+ }
359
+ function knownPackageRoots(opts) {
360
+ const homeDir = opts.homeDir ?? os.homedir();
361
+ const fnmRoots = opts.fnmDir !== undefined
362
+ ? [opts.fnmDir]
363
+ : [process.env.FNM_DIR, path.join(homeDir, '.local', 'share', 'fnm')]
364
+ .filter((value) => Boolean(value));
365
+ const roots = [];
366
+ const packageTail = path.join('lib', 'node_modules', ...NPM_PACKAGE_NAME.split('/'));
367
+ for (const nodeDir of childDirectories(path.join(homeDir, '.nvm', 'versions', 'node'))) {
368
+ roots.push(path.join(nodeDir, packageTail));
369
+ }
370
+ for (const fnmRoot of fnmRoots) {
371
+ for (const versionDir of childDirectories(path.join(fnmRoot, 'node-versions'))) {
372
+ roots.push(path.join(versionDir, 'installation', packageTail));
373
+ }
374
+ }
375
+ roots.push(path.join(homeDir, '.volta', 'tools', 'image', 'packages', ...NPM_PACKAGE_NAME.split('/'), packageTail), path.join(homeDir, '.local', packageTail), path.join(homeDir, '.bun', 'install', 'global', 'node_modules', ...NPM_PACKAGE_NAME.split('/')));
376
+ const npmCacheDir = opts.npmCacheDir ?? process.env.npm_config_cache ?? path.join(homeDir, '.npm');
377
+ for (const npxRunDir of childDirectories(path.join(npmCacheDir, '_npx'))) {
378
+ roots.push(path.join(npxRunDir, 'node_modules', ...NPM_PACKAGE_NAME.split('/')));
379
+ }
380
+ const globalNodeModulesDirs = opts.globalNodeModulesDirs ?? [
381
+ '/opt/homebrew/lib/node_modules',
382
+ '/usr/local/lib/node_modules',
383
+ '/usr/lib/node_modules',
384
+ ];
385
+ for (const nodeModulesDir of globalNodeModulesDirs) {
386
+ roots.push(path.join(nodeModulesDir, ...NPM_PACKAGE_NAME.split('/')));
387
+ }
388
+ return roots;
389
+ }
390
+ function readAgentsCliInstall(packageRoot, binPath) {
391
+ let canonicalRoot;
392
+ let pkg;
393
+ try {
394
+ canonicalRoot = fs.realpathSync(packageRoot);
395
+ pkg = JSON.parse(fs.readFileSync(path.join(canonicalRoot, 'package.json'), 'utf-8'));
396
+ }
397
+ catch {
398
+ return null;
399
+ }
400
+ if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
401
+ return null;
402
+ return {
403
+ ...(binPath ? { binPath } : {}),
404
+ packageRoot: canonicalRoot,
405
+ version: pkg.version,
406
+ atomicHelperInstall: fs.existsSync(path.join(canonicalRoot, 'dist', 'lib', 'app-bundle-install.js')),
407
+ };
408
+ }
330
409
  /**
331
- * Scan PATH for `agents` entrypoints and resolve each to the agents-cli
332
- * package root it executes. More than one distinct root means upgrades,
410
+ * Resolve every `agents` entrypoint on PATH, then inspect the bounded global
411
+ * install layouts used by NVM, fnm, Volta, Bun, npm, and npx. More than one
412
+ * distinct package root means upgrades,
333
413
  * shims, and the command the user types can act on different copies — the
334
414
  * divergence behind silently-failing self-updates.
335
415
  *
@@ -343,11 +423,17 @@ function packageRootForEntry(real) {
343
423
  * @phnx-labs/agents-cli is some other tool and is skipped.
344
424
  * POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
345
425
  */
346
- export function findAgentsCliInstalls(pathEnv) {
426
+ export function findAgentsCliInstalls(pathEnv, opts = {}) {
347
427
  if (process.platform === 'win32')
348
428
  return [];
349
429
  const installs = [];
350
430
  const seenRoots = new Set();
431
+ const addInstall = (install) => {
432
+ if (!install || seenRoots.has(install.packageRoot))
433
+ return;
434
+ seenRoots.add(install.packageRoot);
435
+ installs.push(install);
436
+ };
351
437
  for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) {
352
438
  const candidate = path.join(dir, 'agents');
353
439
  let real;
@@ -360,19 +446,10 @@ export function findAgentsCliInstalls(pathEnv) {
360
446
  const packageRoot = packageRootForEntry(real);
361
447
  if (!packageRoot)
362
448
  continue;
363
- if (seenRoots.has(packageRoot))
364
- continue;
365
- let pkg;
366
- try {
367
- pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
368
- }
369
- catch {
370
- continue;
371
- }
372
- if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
373
- continue;
374
- seenRoots.add(packageRoot);
375
- installs.push({ binPath: candidate, packageRoot, version: pkg.version });
449
+ addInstall(readAgentsCliInstall(packageRoot, candidate));
450
+ }
451
+ for (const packageRoot of knownPackageRoots(opts)) {
452
+ addInstall(readAgentsCliInstall(packageRoot));
376
453
  }
377
454
  return installs;
378
455
  }
@@ -12,7 +12,7 @@ import { type IndexedToolCall } from './tool-calls.js';
12
12
  /** Current schema version; bumped when migrations are added. Exported so tests
13
13
  * assert against the constant instead of hardcoding a number that every bump
14
14
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
15
- export declare const SCHEMA_VERSION = 36;
15
+ export declare const SCHEMA_VERSION = 37;
16
16
  /**
17
17
  * Bump to force `agents sessions backfill resources` to re-derive every
18
18
  * session's skill/slash-command tallies on its next run (resource_scan_ledger
@@ -26,7 +26,8 @@ export declare const RESOURCE_INDEX_VERSION = 1;
26
26
  * re-derives on the next `agents insights` instead of silently reporting stale
27
27
  * numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
28
28
  */
29
- export declare const INSIGHTS_EXTRACTOR_VERSION = 4;
29
+ /** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
30
+ export declare const INSIGHTS_EXTRACTOR_VERSION = 6;
30
31
  /** Raw row shape returned from the sessions table. */
31
32
  export interface SessionRow {
32
33
  id: string;
@@ -50,7 +51,11 @@ export interface SessionRow {
50
51
  message_count: number | null;
51
52
  token_count: number | null;
52
53
  output_tokens: number | null;
54
+ input_tokens: number | null;
55
+ cache_read_tokens: number | null;
56
+ cache_write_tokens: number | null;
53
57
  cost_usd: number | null;
58
+ cost_usd_nocache: number | null;
54
59
  duration_ms: number | null;
55
60
  model: string | null;
56
61
  tool_call_count: number | null;
@@ -335,11 +340,23 @@ export interface UsageRollupRow {
335
340
  */
336
341
  label?: string;
337
342
  costUsd: number;
343
+ /**
344
+ * USD cost priced as if caching were off (cache read/write at the input rate),
345
+ * summed from `cost_usd_nocache`. Backs `agents output --pricing no-cache`.
346
+ * Equals `costUsd` for rows whose sessions record no cache split (RUSH-2287).
347
+ */
348
+ costUsdNoCache: number;
338
349
  durationMs: number;
339
350
  sessionCount: number;
340
351
  tokenCount: number;
341
352
  /** Real generated (output) tokens — excludes cache-read/-write context. */
342
353
  outputTokens: number;
354
+ /** Uncached input tokens summed across the group (0 where no harness recorded a split). */
355
+ inputTokens: number;
356
+ /** Cache-read tokens summed across the group. */
357
+ cacheReadTokens: number;
358
+ /** Cache-write (cache-creation) tokens summed across the group. */
359
+ cacheWriteTokens: number;
343
360
  }
344
361
  /** What to group a usage rollup by. */
345
362
  /**
@@ -26,7 +26,7 @@ const DB_PATH = getSessionsDbPath();
26
26
  /** Current schema version; bumped when migrations are added. Exported so tests
27
27
  * assert against the constant instead of hardcoding a number that every bump
28
28
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
29
- export const SCHEMA_VERSION = 36;
29
+ export const SCHEMA_VERSION = 37;
30
30
  /**
31
31
  * Bump to force `agents sessions backfill resources` to re-derive every
32
32
  * session's skill/slash-command tallies on its next run (resource_scan_ledger
@@ -81,7 +81,11 @@ CREATE TABLE IF NOT EXISTS sessions (
81
81
  message_count INTEGER,
82
82
  token_count INTEGER,
83
83
  output_tokens INTEGER,
84
+ input_tokens INTEGER,
85
+ cache_read_tokens INTEGER,
86
+ cache_write_tokens INTEGER,
84
87
  cost_usd REAL,
88
+ cost_usd_nocache REAL,
85
89
  duration_ms INTEGER,
86
90
  model TEXT,
87
91
  tool_call_count INTEGER,
@@ -308,7 +312,8 @@ CREATE TABLE IF NOT EXISTS session_insights (
308
312
  * re-derives on the next `agents insights` instead of silently reporting stale
309
313
  * numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
310
314
  */
311
- export const INSIGHTS_EXTRACTOR_VERSION = 4;
315
+ /** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
316
+ export const INSIGHTS_EXTRACTOR_VERSION = 6;
312
317
  let dbInstance = null;
313
318
  /**
314
319
  * Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
@@ -875,6 +880,25 @@ function migrateSchema(db, fromVersion) {
875
880
  FROM tool_calls;
876
881
  `);
877
882
  }
883
+ if (fromVersion < 37) {
884
+ // v36 -> v37: persist the burn SPLIT (uncached input / cache-read /
885
+ // cache-write) and a second "no-cache" cost per session, so `agents output`
886
+ // can report the token split and a --pricing no-cache scenario (RUSH-2287).
887
+ // These are new nullable columns — do NOT flush scan_ledger (adding a column
888
+ // must keep warm session ledgers warm, the contract the v33->v34 note above
889
+ // states). Pre-upgrade rows stay NULL for the split until their transcript is
890
+ // re-scanned; `agents output` reports the split only where it is present, so
891
+ // an absent split reads as "not available for this session", never as zero.
892
+ const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((c) => c.name));
893
+ if (!cols.has('input_tokens'))
894
+ db.exec(`ALTER TABLE sessions ADD COLUMN input_tokens INTEGER`);
895
+ if (!cols.has('cache_read_tokens'))
896
+ db.exec(`ALTER TABLE sessions ADD COLUMN cache_read_tokens INTEGER`);
897
+ if (!cols.has('cache_write_tokens'))
898
+ db.exec(`ALTER TABLE sessions ADD COLUMN cache_write_tokens INTEGER`);
899
+ if (!cols.has('cost_usd_nocache'))
900
+ db.exec(`ALTER TABLE sessions ADD COLUMN cost_usd_nocache REAL`);
901
+ }
878
902
  }
879
903
  /**
880
904
  * Stamp `account_key` / `account_org` / `account` on every Claude row from its
@@ -1369,7 +1393,8 @@ const upsertSessionStmt = (db) => db.prepare(`
1369
1393
  id, short_id, agent, origin, routine_name, routine_run_id,
1370
1394
  version, account, account_key, account_org, mode, timestamp, last_activity,
1371
1395
  project, cwd, git_branch, topic, label, message_count, token_count,
1372
- output_tokens, cost_usd, duration_ms, model, tool_call_count,
1396
+ output_tokens, input_tokens, cache_read_tokens, cache_write_tokens,
1397
+ cost_usd, cost_usd_nocache, duration_ms, model, tool_call_count,
1373
1398
  file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
1374
1399
  pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
1375
1400
  recent_directories_touched, linear_project, linear_project_url, machine,
@@ -1378,7 +1403,8 @@ const upsertSessionStmt = (db) => db.prepare(`
1378
1403
  @id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
1379
1404
  @version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
1380
1405
  @project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
1381
- @output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
1406
+ @output_tokens, @input_tokens, @cache_read_tokens, @cache_write_tokens,
1407
+ @cost_usd, @cost_usd_nocache, @duration_ms, @model, @tool_call_count,
1382
1408
  @file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
1383
1409
  @pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
1384
1410
  @recent_directories_touched, @linear_project, @linear_project_url, @machine,
@@ -1413,7 +1439,11 @@ const upsertSessionStmt = (db) => db.prepare(`
1413
1439
  message_count = excluded.message_count,
1414
1440
  token_count = excluded.token_count,
1415
1441
  output_tokens = excluded.output_tokens,
1442
+ input_tokens = excluded.input_tokens,
1443
+ cache_read_tokens = excluded.cache_read_tokens,
1444
+ cache_write_tokens = excluded.cache_write_tokens,
1416
1445
  cost_usd = excluded.cost_usd,
1446
+ cost_usd_nocache = excluded.cost_usd_nocache,
1417
1447
  duration_ms = excluded.duration_ms,
1418
1448
  model = excluded.model,
1419
1449
  tool_call_count = excluded.tool_call_count,
@@ -1652,7 +1682,11 @@ export function upsertSession(meta, content, scan) {
1652
1682
  message_count: meta.messageCount ?? null,
1653
1683
  token_count: meta.tokenCount ?? null,
1654
1684
  output_tokens: meta.outputTokens ?? null,
1685
+ input_tokens: meta.inputTokens ?? null,
1686
+ cache_read_tokens: meta.cacheReadTokens ?? null,
1687
+ cache_write_tokens: meta.cacheWriteTokens ?? null,
1655
1688
  cost_usd: meta.costUsd ?? null,
1689
+ cost_usd_nocache: meta.costUsdNoCache ?? null,
1656
1690
  duration_ms: meta.durationMs ?? null,
1657
1691
  model: meta.model ?? null,
1658
1692
  tool_call_count: meta.toolCallCount ?? null,
@@ -1832,7 +1866,11 @@ export function upsertSessionsBatch(entries) {
1832
1866
  message_count: meta.messageCount ?? null,
1833
1867
  token_count: meta.tokenCount ?? null,
1834
1868
  output_tokens: meta.outputTokens ?? null,
1869
+ input_tokens: meta.inputTokens ?? null,
1870
+ cache_read_tokens: meta.cacheReadTokens ?? null,
1871
+ cache_write_tokens: meta.cacheWriteTokens ?? null,
1835
1872
  cost_usd: meta.costUsd ?? null,
1873
+ cost_usd_nocache: meta.costUsdNoCache ?? null,
1836
1874
  duration_ms: meta.durationMs ?? null,
1837
1875
  model: meta.model ?? null,
1838
1876
  tool_call_count: meta.toolCallCount ?? null,
@@ -2046,7 +2084,11 @@ function rowToMeta(row) {
2046
2084
  messageCount: row.message_count ?? undefined,
2047
2085
  tokenCount: row.token_count ?? undefined,
2048
2086
  outputTokens: row.output_tokens ?? undefined,
2087
+ inputTokens: row.input_tokens ?? undefined,
2088
+ cacheReadTokens: row.cache_read_tokens ?? undefined,
2089
+ cacheWriteTokens: row.cache_write_tokens ?? undefined,
2049
2090
  costUsd: row.cost_usd ?? undefined,
2091
+ costUsdNoCache: row.cost_usd_nocache ?? undefined,
2050
2092
  durationMs: row.duration_ms ?? undefined,
2051
2093
  model: row.model ?? undefined,
2052
2094
  toolCallCount: row.tool_call_count ?? undefined,
@@ -2494,10 +2536,17 @@ export function queryUsageRollup(options) {
2494
2536
  THEN account_org || ' <' || account || '>' END) AS label,`
2495
2537
  : ''}
2496
2538
  IFNULL(SUM(cost_usd), 0) AS costUsd,
2539
+ -- A session with a cost but no persisted no-cache figure records no cache
2540
+ -- split, so its no-cache cost equals its actual cost — fall back to cost_usd
2541
+ -- so it still contributes to the scenario total rather than dropping to 0.
2542
+ IFNULL(SUM(COALESCE(cost_usd_nocache, cost_usd)), 0) AS costUsdNoCache,
2497
2543
  IFNULL(SUM(duration_ms), 0) AS durationMs,
2498
2544
  COUNT(*) AS sessionCount,
2499
2545
  IFNULL(SUM(token_count), 0) AS tokenCount,
2500
- IFNULL(SUM(output_tokens), 0) AS outputTokens
2546
+ IFNULL(SUM(output_tokens), 0) AS outputTokens,
2547
+ IFNULL(SUM(input_tokens), 0) AS inputTokens,
2548
+ IFNULL(SUM(cache_read_tokens), 0) AS cacheReadTokens,
2549
+ IFNULL(SUM(cache_write_tokens), 0) AS cacheWriteTokens
2501
2550
  FROM sessions
2502
2551
  ${clause}
2503
2552
  GROUP BY key
@@ -71,8 +71,14 @@ interface ClaudeSessionScan {
71
71
  tokenCount?: number;
72
72
  /** Real generated (output) tokens, excluding cache-read/-write context. */
73
73
  outputTokens?: number;
74
+ /** Burn split — uncached input / cache-read / cache-write tokens (RUSH-2287). */
75
+ inputTokens?: number;
76
+ cacheReadTokens?: number;
77
+ cacheWriteTokens?: number;
74
78
  /** Total USD cost accumulated from per-(model, direction) token usage. */
75
79
  costUsd?: number;
80
+ /** USD cost with cache read/write repriced at the input rate (RUSH-2287). */
81
+ costUsdNoCache?: number;
76
82
  /** Wall-clock duration in ms between the first and last timestamped event. */
77
83
  durationMs?: number;
78
84
  /** ISO time of the last timestamped event — the session's last activity. */
@@ -122,7 +128,13 @@ interface CodexSessionScan {
122
128
  tokenCount?: number;
123
129
  /** Real generated (output) tokens, excluding cache-read/-write context. */
124
130
  outputTokens?: number;
131
+ /** Burn split — uncached input / cache-read tokens (Codex has no cache-write) (RUSH-2287). */
132
+ inputTokens?: number;
133
+ cacheReadTokens?: number;
134
+ cacheWriteTokens?: number;
125
135
  costUsd?: number;
136
+ /** USD cost with cache read repriced at the input rate (RUSH-2287). */
137
+ costUsdNoCache?: number;
126
138
  durationMs?: number;
127
139
  lastActivity?: string;
128
140
  contentText?: string;
@@ -417,8 +429,12 @@ export interface ClaudeParseState {
417
429
  toolCallCount: number;
418
430
  tokenCount: number;
419
431
  outputTokens: number;
432
+ inputTokens: number;
433
+ cacheReadTokens: number;
434
+ cacheWriteTokens: number;
420
435
  sawTokenCount: boolean;
421
436
  costUsd: number;
437
+ costUsdNoCache: number;
422
438
  sawCost: boolean;
423
439
  firstTsMs?: number;
424
440
  lastTsMs?: number;
@@ -468,7 +484,7 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
468
484
  * exact even when the recent window is smaller than the true count.
469
485
  */
470
486
  export interface ClaudeParserState {
471
- v: 2;
487
+ v: 3;
472
488
  offset: number;
473
489
  jsonlDroppingOversizedLine?: boolean;
474
490
  timestamp?: string;
@@ -487,9 +503,13 @@ export interface ClaudeParserState {
487
503
  toolCallCount: number;
488
504
  tokenCount: number;
489
505
  outputTokens: number;
506
+ inputTokens: number;
507
+ cacheReadTokens: number;
508
+ cacheWriteTokens: number;
490
509
  sawTokenCount: boolean;
491
510
  sawCost: boolean;
492
511
  costUsd: number;
512
+ costUsdNoCache: number;
493
513
  seenIdsSize: number;
494
514
  seenIdsRecent: string[];
495
515
  sawPrCreate: boolean;