@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -0,0 +1,184 @@
1
+ import chalk from 'chalk';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { syncTraces } from '../lib/traces/sync.js';
4
+ import { readSyncLedger } from '../lib/traces/sync.js';
5
+ import { managedTracesBaseUrl, resolveTracesBackend } from '../lib/traces/backend.js';
6
+ import { showUrl } from '../lib/open-url.js';
7
+ import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
8
+ import { PHOENIX_ID_BASE } from '../lib/identity/client.js';
9
+ import { DEFAULT_BUCKET_NAME, DEFAULT_WORKER_NAME } from '../lib/traces/config.js';
10
+ import { DEFAULT_TRACES_DOMAIN } from '../lib/traces/backend.js';
11
+ import { provisionTraces } from '../lib/traces/provision.js';
12
+ // ---------------------------------------------------------------------------
13
+ // Help text
14
+ // ---------------------------------------------------------------------------
15
+ const SYNC_EXAMPLES = `
16
+ $ agents traces sync
17
+ Push this device's derived, redacted trajectories to your Phoenix account.
18
+
19
+ $ agents traces sync --limit 20
20
+ Sync at most 20 sessions (useful for a first run on a large fleet device).
21
+
22
+ $ agents traces sync --dry-run --out ./trace-preview
23
+ Compute the shards from your real sessions.db and WRITE them locally — no
24
+ Phoenix sign-in, no worker, no upload. Point a console at ./trace-preview to
25
+ see your real trajectories before wiring the hosted path.
26
+ `.trimStart();
27
+ const SYNC_NOTES = `
28
+ Reads from the local sessions.db, computes a derived SessionTrajectory (steps +
29
+ gaps + stats, no raw transcript text), redacts secrets, and PUTs each to
30
+ the agents-traces store.
31
+
32
+ An incremental gate (traces-sync.json ledger) means re-runs only upload
33
+ sessions whose file_mtime_ms changed since the last sync.
34
+
35
+ Sign in first with: agents auth login
36
+ `.trimStart();
37
+ const STATUS_EXAMPLES = `
38
+ $ agents traces status
39
+ Show what's been synced — session count and last sync time for this device.
40
+ `.trimStart();
41
+ const OPEN_EXAMPLES = `
42
+ $ agents traces open
43
+ Open the Phoenix Evals console for your account.
44
+ `.trimStart();
45
+ const SETUP_EXAMPLES = `
46
+ $ agents traces setup
47
+ Provision the private agents-traces Worker, R2 bucket, and managed domain.
48
+
49
+ $ agents traces setup --account <id> --token <token>
50
+ Provision non-interactively with explicit Cloudflare credentials.
51
+ `.trimStart();
52
+ // ---------------------------------------------------------------------------
53
+ // Command handlers
54
+ // ---------------------------------------------------------------------------
55
+ async function handleSync(opts) {
56
+ const limit = opts.limit !== undefined ? parseInt(opts.limit, 10) : undefined;
57
+ const dryRun = opts.dryRun === true;
58
+ if (dryRun && !opts.out) {
59
+ console.error(chalk.red('traces sync --dry-run requires --out <dir>'));
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ // A dry-run computes locally and needs no Phoenix backend; a real sync does.
64
+ if (!dryRun) {
65
+ try {
66
+ resolveTracesBackend();
67
+ }
68
+ catch (err) {
69
+ console.error(chalk.red(err.message));
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ }
74
+ console.log(chalk.dim(dryRun ? `Computing traces locally → ${opts.out}` : 'Syncing traces…'));
75
+ let result;
76
+ try {
77
+ result = await syncTraces({ limit, dryRun, outDir: opts.out });
78
+ }
79
+ catch (err) {
80
+ console.error(chalk.red(`Sync failed: ${err.message}`));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ const parts = [];
85
+ if (result.uploaded > 0) {
86
+ parts.push(chalk.green(`${result.uploaded} ${dryRun ? 'written' : 'uploaded'}`));
87
+ }
88
+ if (result.skipped > 0) {
89
+ parts.push(chalk.dim(`${result.skipped} skipped`));
90
+ }
91
+ if (result.errors > 0) {
92
+ parts.push(chalk.yellow(`${result.errors} errors`));
93
+ }
94
+ if (parts.length === 0) {
95
+ parts.push(chalk.dim('nothing new'));
96
+ }
97
+ console.log(parts.join(chalk.dim(' · ')));
98
+ }
99
+ async function handleStatus() {
100
+ const ledger = readSyncLedger();
101
+ if (!ledger.lastSyncMtime) {
102
+ console.log(chalk.dim('No sync recorded on this device. Run: agents traces sync'));
103
+ return;
104
+ }
105
+ const when = new Date(ledger.lastSyncMtime).toLocaleString();
106
+ console.log(`Last sync: ${chalk.bold(when)}`);
107
+ console.log(chalk.dim('Run `agents traces sync` to push new sessions.'));
108
+ }
109
+ async function handleOpen() {
110
+ // M2 console — for now open the base URL which will route to the console once deployed.
111
+ const url = managedTracesBaseUrl();
112
+ console.log(chalk.dim(`Opening ${url}`));
113
+ const outcome = await showUrl(url);
114
+ if (outcome.via === 'none') {
115
+ console.log(url);
116
+ }
117
+ }
118
+ async function handleSetup(opts) {
119
+ const { input } = await import('@inquirer/prompts');
120
+ const { apiToken, accountId: bundledAccountId } = readCloudflareCreds(opts.bundle, {
121
+ apiToken: opts.token,
122
+ accountId: opts.account,
123
+ });
124
+ const accountId = opts.account ?? bundledAccountId ?? await input({ message: 'Cloudflare account id' });
125
+ if (!accountId)
126
+ throw new Error('A Cloudflare account id is required.');
127
+ const result = await provisionTraces({
128
+ apiToken,
129
+ accountId,
130
+ workerName: opts.worker,
131
+ bucketName: opts.bucket,
132
+ domain: opts.domain,
133
+ phoenixIdBase: PHOENIX_ID_BASE,
134
+ });
135
+ console.log(chalk.green(`Traces endpoint ready → ${chalk.bold(result.baseUrl)}`));
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Registration
139
+ // ---------------------------------------------------------------------------
140
+ export function registerTracesCommands(program) {
141
+ const tracesCmd = program
142
+ .command('traces')
143
+ .description("Sync this device's derived, redacted trajectories to your Phoenix account");
144
+ const setupCmd = tracesCmd
145
+ .command('setup')
146
+ .description('Provision the private Cloudflare Worker and R2 bucket for traces')
147
+ .option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
148
+ .option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
149
+ .option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
150
+ .option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
151
+ .option('--token <token>', 'Cloudflare API token (else read from the --bundle)')
152
+ .option('--domain <host>', 'custom domain to map', DEFAULT_TRACES_DOMAIN)
153
+ .action(async (opts) => {
154
+ try {
155
+ await handleSetup(opts);
156
+ }
157
+ catch (err) {
158
+ console.error(chalk.red(err.message));
159
+ process.exitCode = 1;
160
+ }
161
+ });
162
+ setHelpSections(setupCmd, { examples: SETUP_EXAMPLES });
163
+ // sync
164
+ const syncCmd = tracesCmd
165
+ .command('sync')
166
+ .description('Push derived, redacted trajectories (incremental)')
167
+ .option('--limit <n>', 'Maximum number of sessions to sync in this run')
168
+ .option('--dry-run', 'Compute the shards from real sessions.db and WRITE them locally (no auth, no upload)')
169
+ .option('--out <dir>', 'Directory to write index.json + sessions/<id>.json (required with --dry-run)')
170
+ .action(handleSync);
171
+ setHelpSections(syncCmd, { examples: SYNC_EXAMPLES, notes: SYNC_NOTES });
172
+ // status
173
+ const statusCmd = tracesCmd
174
+ .command('status')
175
+ .description('Show last sync time for this device')
176
+ .action(handleStatus);
177
+ setHelpSections(statusCmd, { examples: STATUS_EXAMPLES });
178
+ // open
179
+ const openCmd = tracesCmd
180
+ .command('open')
181
+ .description('Open the Phoenix Evals console')
182
+ .action(handleOpen);
183
+ setHelpSections(openCmd, { examples: OPEN_EXAMPLES });
184
+ }
@@ -15,7 +15,7 @@ import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
15
15
  export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
16
16
  import { type ProfileSummary } from '../lib/profiles.js';
17
17
  import { type ByokUsageResult } from '../lib/byok-usage.js';
18
- export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined): FormatUsageSummaryOpts;
18
+ export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined, version?: string): FormatUsageSummaryOpts;
19
19
  /** Shared account identity formatter, re-exported for the view-specific tests. */
20
20
  export declare const accountColumnLabel: typeof accountDisplayLabel;
21
21
  /**
@@ -7,7 +7,7 @@ import * as path from 'path';
7
7
  import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanagedCliState, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
8
8
  import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
9
9
  import { machineId } from '../lib/machine-id.js';
10
- import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
10
+ import { authCacheKey, readAuthHealthCache } from '../lib/auth-health.js';
11
11
  import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
12
12
  import { readManifest } from '../lib/manifest.js';
13
13
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
@@ -32,7 +32,7 @@ import { renderHarnessDetail } from './harness.js';
32
32
  import { confirm } from '@inquirer/prompts';
33
33
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
34
34
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
35
- export function viewUsageSummaryOptions(agentId, signedIn, usageInfo, maxWindows) {
35
+ export function viewUsageSummaryOptions(agentId, signedIn, usageInfo, maxWindows, version) {
36
36
  const headless = isUsageHeadlessScopeError(usageInfo?.error);
37
37
  const benignState = usageInfo ? getUsageBenignState(usageInfo) : null;
38
38
  return {
@@ -40,9 +40,15 @@ export function viewUsageSummaryOptions(agentId, signedIn, usageInfo, maxWindows
40
40
  unverified: !headless && !!usageInfo?.snapshot && !!usageInfo.error,
41
41
  headless,
42
42
  maxWindows,
43
+ expectedWindows: agentId === 'claude'
44
+ ? [{ key: 'session', shortLabel: 'S' }, { key: 'week', shortLabel: 'W' }]
45
+ : undefined,
43
46
  errorKind: classifyUsageErrorKind(usageInfo?.error),
44
47
  errorDetail: usageInfo?.error ?? null,
45
48
  benignState,
49
+ noRecentUsageLabel: agentId === 'grok'
50
+ ? `run grok${version ? `@${version}` : ''} once to refresh usage`
51
+ : null,
46
52
  };
47
53
  }
48
54
  /** Shared account identity formatter, re-exported for the view-specific tests. */
@@ -92,8 +98,6 @@ export function compareAccountOrderedVersions(a, b, globalDefault) {
92
98
  * views leave the cap unset and show every blocking window.
93
99
  */
94
100
  const OVERVIEW_MAX_USAGE_WINDOWS = 2;
95
- /** Fixed width for the last-active column ("just now", "8h ago", "2d ago"). */
96
- const LAST_ACTIVE_COL_WIDTH = 10;
97
101
  /**
98
102
  * Join fixed view columns with a consistent two-space gutter. Empty trailing
99
103
  * columns are dropped so a row without an auth chip does not grow a dangling
@@ -312,22 +316,6 @@ function renderHostClisSection(cwd) {
312
316
  console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
313
317
  }
314
318
  }
315
- /**
316
- * A compact live-auth chip for a version row, read from the fleet auth-health
317
- * cache (written by `agents fleet ping` / the daemon). Empty when the install
318
- * hasn't been probed — this is the local "signed in" flag's ground-truth
319
- * companion: ● live, ○ revoked (re-login), ◐ unverified/limited, plus its age.
320
- */
321
- function liveAuthChip(cache, host, agentId, version) {
322
- const h = cache[authCacheKey(host, agentId, version)];
323
- if (!h)
324
- return '';
325
- const glyph = h.verdict === 'live' ? chalk.green('●')
326
- : h.verdict === 'revoked' ? chalk.red('○')
327
- : h.verdict === 'expired' ? chalk.yellow('○')
328
- : chalk.yellow('◐');
329
- return `${glyph} ${chalk.gray(formatCheckedAge(h.checkedAt))}`;
330
- }
331
319
  async function showInstalledVersions(filterAgentId, viewOpts) {
332
320
  const spinnerText = filterAgentId
333
321
  ? `Checking ${agentLabel(filterAgentId)} agents...`
@@ -378,9 +366,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
378
366
  }
379
367
  }
380
368
  // Shim healing is silent — users don't need to know about internal repairs
381
- const selfHost = machineId();
382
- // Read the auth-health cache once (not per version row — see the batching note above).
383
- const authCache = readAuthHealthCache();
384
369
  // Pre-fetch account info for all versions in parallel. Spinner stays up through
385
370
  // account + usage so a multi-account cold path doesn't leave a blank terminal
386
371
  // after "Checking…" vanishes (the hang the screenshots caught).
@@ -548,7 +533,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
548
533
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
549
534
  const usageKey = getUsageLookupKey(info);
550
535
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
551
- const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, !!info?.signedIn, usageInfo, usageWindowCap));
536
+ const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, !!info?.signedIn, usageInfo, usageWindowCap, v));
552
537
  maxUsageWidth = Math.max(maxUsageWidth, stringWidth(usageStr));
553
538
  const statusStr = formatUsageStatusBadge(info?.usageStatus);
554
539
  maxStatusWidth = Math.max(maxStatusWidth, stringWidth(statusStr));
@@ -608,11 +593,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
608
593
  }
609
594
  const hasEmail = !!vInfo?.email;
610
595
  const signedIn = !!vInfo?.signedIn;
611
- const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, signedIn, usageInfo, usageWindowCap));
596
+ const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, signedIn, usageInfo, usageWindowCap, version));
612
597
  const hasUsage = usageStr.length > 0;
613
598
  // Only show lastActive for versions with an actual logged-in account.
614
599
  // Otherwise it reflects install time (misleading "just now" for fresh installs).
615
- const activeStr = vInfo && hasEmail ? formatLastActive(vInfo.lastActive) : '';
600
+ const lastActive = vInfo && hasEmail ? formatLastActive(vInfo.lastActive) : '';
601
+ const activeStr = lastActive;
616
602
  const hasActive = activeStr.length > 0;
617
603
  // The model now has its own column above; keep only the run mode here.
618
604
  const runDefaults = resolveRunDefaults(agentId, version);
@@ -649,16 +635,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
649
635
  const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
650
636
  parts.push(padToWidth(statusStr, maxStatusWidth));
651
637
  }
652
- // Fixed-width lastActive so the auth chip (●/○/◐) lines up even when
653
- // some rows have no email-derived lastActive.
654
- parts.push(hasActive ? padToWidth(activeStr, LAST_ACTIVE_COL_WIDTH) : ' '.repeat(LAST_ACTIVE_COL_WIDTH));
638
+ if (hasActive)
639
+ parts.push(activeStr);
655
640
  }
656
641
  if (runDefaultBits.length > 0) {
657
642
  parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
658
643
  }
659
- const authChip = liveAuthChip(authCache, selfHost, agentId, version);
660
- if (authChip)
661
- parts.push(authChip);
662
644
  console.log(joinViewColumns(parts));
663
645
  if (showPaths) {
664
646
  const versionDir = getVersionDir(agentId, version);
@@ -747,7 +729,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
747
729
  const gUsageKey = getUsageLookupKey(gInfo);
748
730
  const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
749
731
  const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, viewUsageSummaryOptions(agentId, !!gInfo?.signedIn, gUsage, usageWindowCap));
750
- const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
732
+ const gLastActive = gInfo ? formatLastActive(gInfo.lastActive) : '';
733
+ const gActiveStr = gLastActive;
751
734
  if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
752
735
  const gDisplay = accountColumnLabel(gInfo);
753
736
  parts.push(gDisplay ? chalk.cyan(padToWidth(gDisplay, gMaxEmail)) : ' '.repeat(gMaxEmail));
@@ -758,7 +741,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
758
741
  parts.push(padToWidth(formatUsageStatusBadge(gInfo?.usageStatus), gMaxStatusWidth));
759
742
  }
760
743
  if (gActiveStr)
761
- parts.push(padToWidth(gActiveStr, LAST_ACTIVE_COL_WIDTH));
744
+ parts.push(gActiveStr);
762
745
  console.log(joinViewColumns(parts));
763
746
  if (showPaths && cliState?.path) {
764
747
  console.log(chalk.gray(` ${cliState.path}`));
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * - `__vault-age-helper`
15
15
  * - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
16
16
  * - `__shim`
17
+ * - `__claude-statusline`
17
18
  * - `__daemon-run`
18
19
  *
19
20
  * The tokens are imported from the leaf module sync-commands.ts — the SAME
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * - `__vault-age-helper`
15
15
  * - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
16
16
  * - `__shim`
17
+ * - `__claude-statusline`
17
18
  * - `__daemon-run`
18
19
  *
19
20
  * The tokens are imported from the leaf module sync-commands.ts — the SAME
@@ -70,6 +71,10 @@ if (process.argv[2] === '__shim') {
70
71
  const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
71
72
  process.exit(code);
72
73
  }
74
+ if (process.argv[2] === '__claude-statusline') {
75
+ const { runClaudeStatusLine } = await import('./lib/claude-statusline.js');
76
+ process.exit(await runClaudeStatusLine());
77
+ }
73
78
  if (process.argv[2] === '__daemon-run') {
74
79
  const { runDaemon, log: daemonLog } = await import('./lib/daemon/daemon.js');
75
80
  // RUSH-2418: the daemon is the one always-on process here, and it ran with no
@@ -125,6 +125,8 @@ export interface UsageSnapshot {
125
125
  capturedAt: Date | null;
126
126
  windows: UsageWindow[];
127
127
  plan?: string | null;
128
+ /** Action that makes an event-fed source emit a current reading. */
129
+ refreshHint?: string | null;
128
130
  /**
129
131
  * A refusal observed from a real harness run, independent of API windows.
130
132
  * `session_limit` recovers on a clock (`resetsAt`). `out_of_credits` is a
@@ -306,6 +308,11 @@ export interface FormatUsageSummaryOpts {
306
308
  * column width; single-agent and detail views leave this unset.
307
309
  */
308
310
  maxWindows?: number;
311
+ /** Windows that must keep a visible slot even when the provider omits one. */
312
+ expectedWindows?: Array<{
313
+ key: string;
314
+ shortLabel: string;
315
+ }>;
309
316
  /**
310
317
  * The classified cause of `usageInfo.error` (RUSH-3040), from
311
318
  * {@link classifyUsageErrorKind}. Lets the no-bars branch below name the
@@ -326,6 +333,8 @@ export interface FormatUsageSummaryOpts {
326
333
  errorDetail?: string | null;
327
334
  /** Benign state from {@link getUsageBenignState}; never sourced from `UsageInfo.error`. */
328
335
  benignState?: UsageBenignState | null;
336
+ /** Provider-specific replacement for the generic no-local-event marker. */
337
+ noRecentUsageLabel?: string | null;
329
338
  }
330
339
  /** Format a one-line usage summary with compact bars for inline display. */
331
340
  export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: FormatUsageSummaryOpts): string;
@@ -552,7 +561,7 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
552
561
  * a file-based setup-token and, when none is provisioned, gets `null` — it never
553
562
  * reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
554
563
  * to Anthropic's API is what gets it revoked; see the branch body and
555
- * docs/credential-management.md). It is OFF by default so full-credential
564
+ * docs/secrets.md). It is OFF by default so full-credential
556
565
  * callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
557
566
  * read the interactive login. Rush Cloud dispatch does not call this helper
558
567
  * at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
@@ -588,6 +597,8 @@ export declare function readClaudeUsageCache(usageKey: string, cachePath?: strin
588
597
  export declare function pruneExpiredClaudeUsageCacheEntry(usageKey: string, cachePath?: string, now?: Date): void;
589
598
  /** Write a usage snapshot to the on-disk cache. */
590
599
  export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageSnapshot, cachePath?: string): void;
600
+ /** Atomically merge partial native Claude windows into the current fresh row. */
601
+ export declare function mergeClaudeUsageCacheWindows(usageKey: string, snapshot: UsageSnapshot, cachePath?: string): void;
591
602
  /**
592
603
  * Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
593
604
  * spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
@@ -614,8 +625,8 @@ export declare function parseClaudeSessionLimitReset(text: string, nowMs?: numbe
614
625
  * Returns true only when a valid access token can be obtained.
615
626
  */
616
627
  export declare function isClaudeAuthValid(home?: string): Promise<boolean>;
617
- /** Render a colored block-character progress bar. */
618
- export declare function renderBar(usedPercent: number, length: number, minimumVisible?: number): string;
628
+ /** Render a proportional colored progress bar at one-eighth-cell resolution. */
629
+ export declare function renderBar(usedPercent: number, length: number): string;
619
630
  /** Return a chalk color function based on the usage percentage threshold. */
620
631
  export declare function getUsageColor(usedPercent: number): (text: string) => string;
621
632
  /** Parse the latest billing info from Grok's unified log. */
@@ -26,6 +26,7 @@ import { getCacheDir } from '../state.js';
26
26
  import { mapBounded } from '../concurrency.js';
27
27
  import { atomicWriteFileSync, ensureLockTarget, withFileLock } from '../fs-atomic.js';
28
28
  import { withRefreshLease } from '../refresh-coordinator.js';
29
+ import { padToWidth } from '../session/width.js';
29
30
  const execFileAsync = promisify(execFile);
30
31
  const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
31
32
  const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
@@ -216,6 +217,7 @@ const COMPACT_BAR_LEN = 5;
216
217
  const USAGE_BAR_LEN = 10;
217
218
  const FULL = '\u2588';
218
219
  const EMPTY = '\u2591';
220
+ const PARTIAL_BLOCKS = ['', '\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589'];
219
221
  /** Construct the benign no-local-log result without overloading `error`. */
220
222
  export function usageNoRecentUsageInfo() {
221
223
  return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
@@ -498,11 +500,20 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
498
500
  // multi-meter agent cannot force the whole table to wrap.
499
501
  const selected = pickCompactUsageWindows(snapshot.windows, opts?.maxWindows);
500
502
  const hidden = Math.max(0, snapshot.windows.filter((w) => w.key !== 'sonnet_week').length - selected.length);
501
- const windowParts = selected.map((window) => {
503
+ const expected = opts?.expectedWindows;
504
+ const windowsToRender = expected
505
+ ? expected.map(({ key, shortLabel }) => ({ window: selected.find((item) => item.key === key), shortLabel }))
506
+ : selected.map((window) => ({ window, shortLabel: window.shortLabel }));
507
+ const windowParts = windowsToRender.map(({ window, shortLabel }, index) => {
508
+ if (!window) {
509
+ const missing = chalk.red(`${shortLabel}: ${FULL.repeat(COMPACT_BAR_LEN)} unavailable`);
510
+ return index < windowsToRender.length - 1 ? padToWidth(missing, 20) : missing;
511
+ }
502
512
  const bar = renderCompactUsageBar(window.usedPercent);
503
513
  const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
504
514
  const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
505
- return `${chalk.gray(`${window.shortLabel}:`)} ${bar} ${pct}${reset}`;
515
+ const rendered = `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}${reset}`;
516
+ return index < windowsToRender.length - 1 ? padToWidth(rendered, 20) : rendered;
506
517
  });
507
518
  if (hidden > 0) {
508
519
  windowParts.push(chalk.dim(`+${hidden}`));
@@ -510,6 +521,9 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
510
521
  if (windowParts.length > 0) {
511
522
  parts.push(windowParts.join(' '));
512
523
  }
524
+ else if (snapshot.refreshHint) {
525
+ parts.push(chalk.dim(snapshot.refreshHint));
526
+ }
513
527
  // The bars came from the cache and the live read that should have confirmed
514
528
  // them failed, so they are the last thing we saw — not the current state.
515
529
  // Drawing them unmarked is what let a 26h-old "48% used" read as fact.
@@ -528,7 +542,7 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
528
542
  parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
529
543
  }
530
544
  else if (opts?.benignState === 'no-recent-usage') {
531
- parts.push(chalk.dim(USAGE_NO_RECENT_USAGE_MARKER));
545
+ parts.push(chalk.dim(opts.noRecentUsageLabel || USAGE_NO_RECENT_USAGE_MARKER));
532
546
  }
533
547
  else if (opts?.unavailable) {
534
548
  // Signed-in account we could NOT fetch usage for (no live token in a reachable
@@ -1410,7 +1424,7 @@ function deleteCachedClaudeOauth(service) {
1410
1424
  * a file-based setup-token and, when none is provisioned, gets `null` — it never
1411
1425
  * reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
1412
1426
  * to Anthropic's API is what gets it revoked; see the branch body and
1413
- * docs/credential-management.md). It is OFF by default so full-credential
1427
+ * docs/secrets.md). It is OFF by default so full-credential
1414
1428
  * callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
1415
1429
  * read the interactive login. Rush Cloud dispatch does not call this helper
1416
1430
  * at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
@@ -1443,7 +1457,7 @@ export async function loadClaudeOauth(home, opts) {
1443
1457
  // interactive credential used programmatically, which Anthropic flags and
1444
1458
  // revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
1445
1459
  // invariant that the interactive/rotating login is untouchable
1446
- // (docs/credential-management.md). Report unprovisioned (-> probe
1460
+ // (docs/secrets.md). Report unprovisioned (-> probe
1447
1461
  // token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
1448
1462
  // setup-token via the mint-auth path to restore usage/probe for the account.
1449
1463
  return null;
@@ -1604,6 +1618,32 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
1604
1618
  /* best-effort cache write — lock busy or disk full */
1605
1619
  }
1606
1620
  }
1621
+ /** Atomically merge partial native Claude windows into the current fresh row. */
1622
+ export function mergeClaudeUsageCacheWindows(usageKey, snapshot, cachePath = getClaudeUsageCachePath()) {
1623
+ try {
1624
+ ensureLockTarget(cachePath, '{}');
1625
+ withFileLock(cachePath, () => {
1626
+ const cache = readClaudeUsageCacheFile(cachePath);
1627
+ const prior = cache[usageKey];
1628
+ const priorSnapshot = prior
1629
+ ? deserializeClaudeUsageSnapshot(prior, snapshot.capturedAt ?? new Date())
1630
+ : null;
1631
+ const windows = new Map(priorSnapshot?.windows.map((window) => [window.key, window]) ?? []);
1632
+ for (const window of snapshot.windows)
1633
+ windows.set(window.key, window);
1634
+ cache[usageKey] = serializeClaudeUsageSnapshot({
1635
+ ...snapshot,
1636
+ windows: [...windows.values()],
1637
+ plan: snapshot.plan ?? priorSnapshot?.plan ?? null,
1638
+ unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
1639
+ });
1640
+ atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
1641
+ });
1642
+ }
1643
+ catch {
1644
+ /* best-effort cache write — lock busy or disk full */
1645
+ }
1646
+ }
1607
1647
  /** Read the entire usage cache file from disk. */
1608
1648
  function readClaudeUsageCacheFile(cachePath) {
1609
1649
  if (!fs.existsSync(cachePath)) {
@@ -1632,6 +1672,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
1632
1672
  return {
1633
1673
  capturedAt: snapshot.capturedAt?.toISOString() || null,
1634
1674
  plan: snapshot.plan ?? null,
1675
+ refreshHint: snapshot.refreshHint ?? null,
1635
1676
  unavailable: snapshot.unavailable
1636
1677
  ? {
1637
1678
  reason: snapshot.unavailable.reason,
@@ -1686,7 +1727,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
1686
1727
  // and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
1687
1728
  // it can never read as a 0% bar or an "available" badge (the RUSH-2858
1688
1729
  // property that made expired windows drop in the first place).
1689
- if (windows.length === 0 && !unavailable && !snapshot.plan) {
1730
+ if (windows.length === 0 && !unavailable && !snapshot.plan && !snapshot.refreshHint) {
1690
1731
  return null;
1691
1732
  }
1692
1733
  return {
@@ -1695,6 +1736,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
1695
1736
  capturedAt,
1696
1737
  windows,
1697
1738
  plan: snapshot.plan ?? null,
1739
+ refreshHint: snapshot.refreshHint ?? null,
1698
1740
  unavailable,
1699
1741
  };
1700
1742
  }
@@ -1973,14 +2015,17 @@ function renderUsageBar(usedPercent) {
1973
2015
  }
1974
2016
  /** Render a compact usage bar for inline summaries. */
1975
2017
  function renderCompactUsageBar(usedPercent) {
1976
- return renderBar(usedPercent, COMPACT_BAR_LEN, usedPercent > 0 ? 1 : 0);
1977
- }
1978
- /** Render a colored block-character progress bar. */
1979
- export function renderBar(usedPercent, length, minimumVisible = 0) {
1980
- const rounded = Math.round((usedPercent / 100) * length);
1981
- const filled = Math.max(minimumVisible, Math.max(0, Math.min(length, rounded)));
2018
+ return renderBar(usedPercent, COMPACT_BAR_LEN);
2019
+ }
2020
+ /** Render a proportional colored progress bar at one-eighth-cell resolution. */
2021
+ export function renderBar(usedPercent, length) {
2022
+ const clamped = Math.max(0, Math.min(100, usedPercent));
2023
+ const eighths = Math.round((clamped / 100) * length * 8);
2024
+ const filled = Math.floor(eighths / 8);
2025
+ const partial = eighths % 8;
1982
2026
  const color = getUsageColor(usedPercent);
1983
- return color(FULL.repeat(filled)) + chalk.dim(EMPTY.repeat(length - filled));
2027
+ const gauge = FULL.repeat(filled) + PARTIAL_BLOCKS[partial];
2028
+ return color(gauge) + chalk.dim(EMPTY.repeat(length - filled - (partial > 0 ? 1 : 0)));
1984
2029
  }
1985
2030
  /** Apply the appropriate color to a text string based on usage percentage. */
1986
2031
  function colorUsage(text, usedPercent) {
@@ -2080,10 +2125,10 @@ async function getGrokUsageInfo(options) {
2080
2125
  // (see readLatestGrokBilling).
2081
2126
  const now = new Date();
2082
2127
  const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
2083
- // NOTE: an empty `windows` here (all windows expired) still returns a real
2084
- // snapshot, not the no-recent-usage marker — the plan/tier is still valid
2085
- // and callers (e.g. `formatUsageSummary`) render "no bars" correctly from
2086
- // an empty array. The marker above is only for "nothing was ever recorded".
2128
+ const version = options?.cliVersion;
2129
+ const refreshHint = windows.length === 0
2130
+ ? `run grok${version ? `@${version}` : ''} once to refresh usage`
2131
+ : null;
2087
2132
  return {
2088
2133
  snapshot: {
2089
2134
  source: 'last_seen',
@@ -2091,6 +2136,7 @@ async function getGrokUsageInfo(options) {
2091
2136
  capturedAt: match.capturedAt,
2092
2137
  windows,
2093
2138
  plan: match.subscriptionTier,
2139
+ refreshHint,
2094
2140
  },
2095
2141
  error: null,
2096
2142
  };
@@ -2483,7 +2529,7 @@ export function normalizeCursorUsageSummary(data) {
2483
2529
  function readCursorCredentials(base) {
2484
2530
  try {
2485
2531
  const cfgPath = path.join(base, '.cursor', 'cli-config.json');
2486
- const authPath = path.join(base, '.config', 'cursor', 'auth.json');
2532
+ const authPath = path.join(base, '.cursor', 'auth.json');
2487
2533
  if (!fs.existsSync(cfgPath) || !fs.existsSync(authPath))
2488
2534
  return null;
2489
2535
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
@@ -1504,12 +1504,9 @@ const CREDENTIAL_FILE_SEGMENTS = {
1504
1504
  // Muse Code stores OAuth / API credentials at ~/.config/muse/auth.json
1505
1505
  // (or META_API_KEY in the environment, which is not a file).
1506
1506
  muse: [['.config', 'muse', 'auth.json']],
1507
- // Cursor's OAuth token the login gate — is at $XDG_CONFIG_HOME/cursor/auth.json
1508
- // (~/.config/cursor/auth.json by default). cli-config.json holds only account
1509
- // metadata; the token file is what a real launch authenticates from, so a
1510
- // version home with no token of its own is genuinely logged out once runs pin
1511
- // XDG_CONFIG_HOME per home (see buildExecEnv).
1512
- cursor: [['.config', 'cursor', 'auth.json']],
1507
+ // With AGENT_CLI_CREDENTIAL_STORE=file, Cursor stores both OAuth tokens at
1508
+ // ~/.cursor/auth.json. cli-config.json holds account metadata only.
1509
+ cursor: [['.cursor', 'auth.json']],
1513
1510
  };
1514
1511
  /** Whether an agent's credential file exists under a given home. */
1515
1512
  function credentialFileExistsUnder(agentId, home) {
@@ -2112,7 +2109,7 @@ export async function getAccountInfo(agentId, home) {
2112
2109
  case 'cursor': {
2113
2110
  // Cursor CLI keeps account metadata in ~/.cursor/cli-config.json
2114
2111
  // (authInfo: { email, userId, authId }) and its OAuth tokens SEPARATELY
2115
- // in ~/.config/cursor/auth.json ({ accessToken, refreshToken }). Presence
2112
+ // in ~/.cursor/auth.json ({ accessToken, refreshToken }). Presence
2116
2113
  // of an access token is the signed-in signal; email/ids come from
2117
2114
  // cli-config. authId is the OAuth subject (e.g. "google-oauth2|<n>") — the
2118
2115
  // same value the usage endpoint keys on (see getCursorUsageInfo).
@@ -2126,7 +2123,7 @@ export async function getAccountInfo(agentId, home) {
2126
2123
  const accountId = normalizeIdentityPart(authInfo?.authId ?? authInfo?.userId);
2127
2124
  if (!email && !accountId)
2128
2125
  return { ...empty, lastActive };
2129
- const authPath = resolveAccountCredentialPath(base, '.config', 'cursor', 'auth.json');
2126
+ const authPath = resolveAccountCredentialPath(base, '.cursor', 'auth.json');
2130
2127
  let hasToken = false;
2131
2128
  if (authPath) {
2132
2129
  try {
@@ -2135,8 +2132,13 @@ export async function getAccountInfo(agentId, home) {
2135
2132
  }
2136
2133
  catch { /* unreadable token file */ }
2137
2134
  }
2135
+ // cli-config survives logout and may describe a different keychain
2136
+ // account. Without this version's file token, its identity is stale
2137
+ // metadata rather than a usable managed login.
2138
+ if (!hasToken)
2139
+ return { ...empty, lastActive };
2138
2140
  const accountKey = buildIdentityKey(agentId, [['user', accountId]]);
2139
- return { ...empty, email, accountId, accountKey, signedIn: hasToken || !!email, lastActive };
2141
+ return { ...empty, email, accountId, accountKey, signedIn: true, lastActive };
2140
2142
  }
2141
2143
  catch { }
2142
2144
  return { ...empty, lastActive };