@phnx-labs/agents-cli 1.20.67 → 1.20.69

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 (83) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +14 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/check.js +96 -2
  5. package/dist/commands/computer.d.ts +26 -0
  6. package/dist/commands/computer.js +173 -149
  7. package/dist/commands/doctor.js +37 -9
  8. package/dist/commands/exec.d.ts +18 -0
  9. package/dist/commands/exec.js +126 -6
  10. package/dist/commands/fork.d.ts +9 -0
  11. package/dist/commands/fork.js +67 -0
  12. package/dist/commands/plugins.js +11 -2
  13. package/dist/commands/repo.js +47 -2
  14. package/dist/commands/run-account-picker.d.ts +14 -0
  15. package/dist/commands/run-account-picker.js +131 -0
  16. package/dist/commands/setup-browser.d.ts +18 -0
  17. package/dist/commands/setup-browser.js +142 -0
  18. package/dist/commands/setup-computer.d.ts +19 -0
  19. package/dist/commands/setup-computer.js +133 -0
  20. package/dist/commands/setup-share.d.ts +17 -0
  21. package/dist/commands/setup-share.js +85 -0
  22. package/dist/commands/setup.d.ts +1 -1
  23. package/dist/commands/setup.js +58 -1
  24. package/dist/commands/share.d.ts +15 -0
  25. package/dist/commands/share.js +10 -4
  26. package/dist/commands/ssh.js +267 -2
  27. package/dist/commands/teams.d.ts +11 -0
  28. package/dist/commands/teams.js +39 -1
  29. package/dist/commands/view.d.ts +4 -14
  30. package/dist/commands/view.js +39 -36
  31. package/dist/index.js +2 -1
  32. package/dist/lib/agents.d.ts +21 -5
  33. package/dist/lib/agents.js +59 -24
  34. package/dist/lib/auth-health.d.ts +103 -0
  35. package/dist/lib/auth-health.js +232 -0
  36. package/dist/lib/browser/chrome.d.ts +9 -0
  37. package/dist/lib/browser/chrome.js +22 -0
  38. package/dist/lib/browser/profiles.js +4 -25
  39. package/dist/lib/cli-entry.d.ts +23 -0
  40. package/dist/lib/cli-entry.js +116 -0
  41. package/dist/lib/computer/download.d.ts +51 -0
  42. package/dist/lib/computer/download.js +145 -0
  43. package/dist/lib/computer-rpc.js +9 -3
  44. package/dist/lib/daemon.d.ts +2 -2
  45. package/dist/lib/daemon.js +8 -89
  46. package/dist/lib/devices/connect.d.ts +15 -0
  47. package/dist/lib/devices/connect.js +17 -5
  48. package/dist/lib/devices/fleet.d.ts +23 -0
  49. package/dist/lib/devices/fleet.js +38 -0
  50. package/dist/lib/devices/health-report.d.ts +46 -0
  51. package/dist/lib/devices/health-report.js +159 -0
  52. package/dist/lib/devices/health.d.ts +14 -4
  53. package/dist/lib/devices/health.js +49 -8
  54. package/dist/lib/devices/terminfo.d.ts +44 -0
  55. package/dist/lib/devices/terminfo.js +167 -0
  56. package/dist/lib/git.d.ts +10 -0
  57. package/dist/lib/git.js +23 -0
  58. package/dist/lib/hosts/option.js +1 -1
  59. package/dist/lib/hosts/ready.js +5 -3
  60. package/dist/lib/hosts/remote-cmd.d.ts +12 -0
  61. package/dist/lib/hosts/remote-cmd.js +17 -1
  62. package/dist/lib/mcp.d.ts +21 -0
  63. package/dist/lib/mcp.js +278 -13
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  65. package/dist/lib/resources/mcp.js +20 -342
  66. package/dist/lib/rotate.d.ts +12 -8
  67. package/dist/lib/rotate.js +22 -23
  68. package/dist/lib/routines.d.ts +13 -0
  69. package/dist/lib/routines.js +21 -0
  70. package/dist/lib/runner.js +18 -40
  71. package/dist/lib/secrets/agent.js +11 -15
  72. package/dist/lib/session/fork.d.ts +32 -0
  73. package/dist/lib/session/fork.js +101 -0
  74. package/dist/lib/share/capture.js +21 -3
  75. package/dist/lib/signin-badge.d.ts +41 -0
  76. package/dist/lib/signin-badge.js +64 -0
  77. package/dist/lib/ssh-exec.d.ts +5 -0
  78. package/dist/lib/ssh-exec.js +55 -1
  79. package/dist/lib/startup/command-registry.d.ts +1 -0
  80. package/dist/lib/startup/command-registry.js +2 -0
  81. package/dist/lib/usage.d.ts +40 -3
  82. package/dist/lib/usage.js +147 -16
  83. package/package.json +1 -1
@@ -66,8 +66,18 @@ export declare function buildCanonicalUsageContext(inputs: UsageIdentityInput[])
66
66
  canonicalByUsageKey: Map<string, AccountInfo>;
67
67
  usageFetchInputs: Map<string, UsageFetchInput>;
68
68
  };
69
+ /**
70
+ * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid via
71
+ * a live API, Codex via local session logs. Everything else has no usage concept,
72
+ * so callers use this to decide whether a missing snapshot is worth flagging as
73
+ * "usage unavailable" (a signed-in Claude account with no data) versus simply not
74
+ * applicable (Antigravity, Grok, OpenCode).
75
+ */
76
+ export declare function agentReportsUsage(agentId: AgentId): boolean;
69
77
  /** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
70
- export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Promise<{
78
+ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: {
79
+ forceRefresh?: boolean;
80
+ }): Promise<{
71
81
  canonicalByUsageKey: Map<string, AccountInfo>;
72
82
  usageByKey: Map<string, UsageInfo>;
73
83
  }>;
@@ -83,9 +93,13 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
83
93
  * cache; every run after that returns instantly while the cache silently
84
94
  * refreshes in the background.
85
95
  */
86
- export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Promise<UsageInfo>;
96
+ export declare function getUsageInfoForIdentity(input: UsageIdentityInput, opts?: {
97
+ forceRefresh?: boolean;
98
+ }): Promise<UsageInfo>;
87
99
  /** Format a one-line usage summary with compact bars for inline display. */
88
- export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
100
+ export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: {
101
+ unavailable?: boolean;
102
+ }): string;
89
103
  /**
90
104
  * Derive an account's real throttle state from its live usage windows — the
91
105
  * single signal both the `agents view` badge and run-rotation eligibility share
@@ -170,6 +184,29 @@ export interface DroidBillingLimitsResponse {
170
184
  } | null;
171
185
  } | null;
172
186
  }
187
+ /**
188
+ * Live auth probes — the same authenticated GET the usage fetchers above do,
189
+ * but surfacing the raw HTTP status instead of swallowing 401/expired to null.
190
+ * These back `agents fleet ping` and the fleet auth-health cache: completing a
191
+ * real request is the only proof a token is accepted. The local "signed in"
192
+ * flag cannot tell a revoked-but-unexpired token from a good one. Classification
193
+ * of the returned status into a verdict lives in lib/auth-health.ts (kept there
194
+ * so it stays pure/testable and to avoid an import cycle).
195
+ */
196
+ export interface ProviderProbe {
197
+ /** HTTP status of the probe request, or null when no request was made (missing/expired token) or the request threw. */
198
+ status: number | null;
199
+ /** Local credential state observed before the request. */
200
+ token: 'present' | 'missing' | 'expired';
201
+ /** Network/parse error message when status is null but a token was present. */
202
+ error?: string;
203
+ }
204
+ /** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
205
+ export declare function probeClaudeStatus(home?: string, cliVersion?: string | null): Promise<ProviderProbe>;
206
+ /** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
207
+ export declare function probeKimiStatus(home?: string): Promise<ProviderProbe>;
208
+ /** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
209
+ export declare function probeDroidStatus(home?: string): Promise<ProviderProbe>;
173
210
  /**
174
211
  * Normalize the Factory billing-limits payload into the common UsageWindow
175
212
  * shape. Orgs on the legacy (non token-rate-limit) billing model have no
package/dist/lib/usage.js CHANGED
@@ -87,8 +87,18 @@ export function buildCanonicalUsageContext(inputs) {
87
87
  }
88
88
  return { canonicalByUsageKey, usageFetchInputs };
89
89
  }
90
+ /**
91
+ * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid via
92
+ * a live API, Codex via local session logs. Everything else has no usage concept,
93
+ * so callers use this to decide whether a missing snapshot is worth flagging as
94
+ * "usage unavailable" (a signed-in Claude account with no data) versus simply not
95
+ * applicable (Antigravity, Grok, OpenCode).
96
+ */
97
+ export function agentReportsUsage(agentId) {
98
+ return agentId === 'claude' || agentId === 'codex' || agentId === 'kimi' || agentId === 'droid';
99
+ }
90
100
  /** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
91
- export async function getUsageInfoByIdentity(inputs) {
101
+ export async function getUsageInfoByIdentity(inputs, opts) {
92
102
  const { canonicalByUsageKey, usageFetchInputs } = buildCanonicalUsageContext(inputs);
93
103
  const usageResults = await Promise.all([...usageFetchInputs.entries()].map(async ([key, input]) => ({
94
104
  key,
@@ -97,7 +107,7 @@ export async function getUsageInfoByIdentity(inputs) {
97
107
  home: input.home,
98
108
  cliVersion: input.cliVersion,
99
109
  info: canonicalByUsageKey.get(key),
100
- }),
110
+ }, opts),
101
111
  })));
102
112
  return {
103
113
  canonicalByUsageKey,
@@ -120,8 +130,9 @@ const inFlightRefreshes = new Map();
120
130
  * cache; every run after that returns instantly while the cache silently
121
131
  * refreshes in the background.
122
132
  */
123
- export async function getUsageInfoForIdentity(input) {
133
+ export async function getUsageInfoForIdentity(input, opts) {
124
134
  const usageKey = getUsageLookupKey(input.info);
135
+ const forceRefresh = opts?.forceRefresh === true;
125
136
  // Agents whose usage comes from a live network call (Claude, Kimi, Droid) go
126
137
  // through the stale-while-revalidate cache below so `agents run`/`agents view`
127
138
  // stay off the network on the hot path. Everything else (Codex reads local
@@ -139,15 +150,20 @@ export async function getUsageInfoForIdentity(input) {
139
150
  }
140
151
  const cached = readClaudeUsageCache(usageKey);
141
152
  const ageMs = cached?.capturedAt ? Date.now() - cached.capturedAt.getTime() : Infinity;
142
- // Fresh: cache is recent enough, skip network entirely.
143
- if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
144
- return { snapshot: cached, error: null };
145
- }
146
- // Stale-while-revalidate: cache exists and isn't ancient, return it now and
147
- // refresh in the background so the next invocation has fresh data.
148
- if (cached && ageMs < USAGE_CACHE_SWR_MS) {
149
- triggerBackgroundUsageRefresh(input, usageKey);
150
- return { snapshot: cached, error: null };
153
+ // `--refresh` (forceRefresh) skips both cache short-circuits and blocks on a
154
+ // live fetch below, so `agents view --refresh` repopulates every account we can
155
+ // actually reach a token for.
156
+ if (!forceRefresh) {
157
+ // Fresh: cache is recent enough, skip network entirely.
158
+ if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
159
+ return { snapshot: cached, error: null };
160
+ }
161
+ // Stale-while-revalidate: cache exists and isn't ancient, return it now and
162
+ // refresh in the background so the next invocation has fresh data.
163
+ if (cached && ageMs < USAGE_CACHE_SWR_MS) {
164
+ triggerBackgroundUsageRefresh(input, usageKey);
165
+ return { snapshot: cached, error: null };
166
+ }
151
167
  }
152
168
  // Cold cache or > 24h old: block on live fetch.
153
169
  const usage = await getUsageInfo(input.agentId, {
@@ -201,7 +217,7 @@ function triggerBackgroundUsageRefresh(input, usageKey) {
201
217
  inFlightRefreshes.set(usageKey, promise);
202
218
  }
203
219
  /** Format a one-line usage summary with compact bars for inline display. */
204
- export function formatUsageSummary(plan, snapshot, planWidth = 3) {
220
+ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
205
221
  const parts = [];
206
222
  if (plan) {
207
223
  parts.push(chalk.gray(plan.padEnd(planWidth)));
@@ -212,14 +228,26 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
212
228
  // account throttled by its month window (Droid meters on 5h/week/month)
213
229
  // shows the bar that explains why. Claude's Sonnet week is a per-model
214
230
  // sub-limit, not a blocking window; it renders only in the full
215
- // per-version usage section.
231
+ // per-version usage section. Each window reads "S: ███░░ 58% (3d)" — the
232
+ // gauge, the exact percentage, and a compact hint of when it resets.
216
233
  const windows = snapshot.windows
217
234
  .filter((window) => window.key !== 'sonnet_week')
218
- .map((window) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
235
+ .map((window) => {
236
+ const bar = renderCompactUsageBar(window.usedPercent);
237
+ const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
238
+ const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
239
+ return `${chalk.gray(`${window.shortLabel}:`)} ${bar} ${pct}${reset}`;
240
+ });
219
241
  if (windows.length > 0) {
220
- parts.push(windows.join(' '));
242
+ parts.push(windows.join(' '));
221
243
  }
222
244
  }
245
+ else if (opts?.unavailable) {
246
+ // Signed-in account we could NOT fetch usage for (no live token in a reachable
247
+ // home / org mismatch / fetch error). Say so explicitly instead of drawing a
248
+ // blank gauge that reads like "0% used".
249
+ parts.push(chalk.dim('usage unavailable'));
250
+ }
223
251
  return parts.join(' ');
224
252
  }
225
253
  /**
@@ -581,6 +609,90 @@ async function getDroidUsageInfo(options) {
581
609
  return { snapshot: null, error: null };
582
610
  }
583
611
  }
612
+ /** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
613
+ export async function probeClaudeStatus(home, cliVersion) {
614
+ const oauth = await loadClaudeOauth(home);
615
+ if (!oauth?.accessToken)
616
+ return { status: null, token: 'missing' };
617
+ let access = null;
618
+ try {
619
+ access = await getClaudeAccessToken(oauth, home);
620
+ }
621
+ catch {
622
+ access = null;
623
+ }
624
+ // Fall back to the stored (possibly stale) token so the server returns the
625
+ // real verdict — a 401 here IS the revoked/expired signal we want to surface.
626
+ const bearer = access || oauth.accessToken.trim();
627
+ try {
628
+ const response = await fetch(CLAUDE_USAGE_URL, {
629
+ method: 'GET',
630
+ headers: {
631
+ Authorization: `Bearer ${bearer}`,
632
+ 'Content-Type': 'application/json',
633
+ 'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
634
+ 'User-Agent': getClaudeUserAgent(cliVersion),
635
+ },
636
+ signal: AbortSignal.timeout(8000),
637
+ });
638
+ return { status: response.status, token: 'present' };
639
+ }
640
+ catch (err) {
641
+ return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
642
+ }
643
+ }
644
+ /** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
645
+ export async function probeKimiStatus(home) {
646
+ const credPath = resolveKimiCredentialPath(home);
647
+ if (!credPath)
648
+ return { status: null, token: 'missing' };
649
+ let accessToken;
650
+ let expiresAt = null;
651
+ try {
652
+ const cred = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
653
+ accessToken = typeof cred?.access_token === 'string' ? cred.access_token : undefined;
654
+ expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
655
+ }
656
+ catch {
657
+ return { status: null, token: 'missing' };
658
+ }
659
+ if (!accessToken)
660
+ return { status: null, token: 'missing' };
661
+ if (expiresAt !== null && Date.now() / 1000 >= expiresAt)
662
+ return { status: null, token: 'expired' };
663
+ try {
664
+ const response = await fetch(KIMI_USAGES_URL, {
665
+ method: 'GET',
666
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
667
+ signal: AbortSignal.timeout(8000),
668
+ });
669
+ return { status: response.status, token: 'present' };
670
+ }
671
+ catch (err) {
672
+ return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
673
+ }
674
+ }
675
+ /** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
676
+ export async function probeDroidStatus(home) {
677
+ const cred = decryptDroidAuthPayload(home || os.homedir());
678
+ const accessToken = cred?.access_token;
679
+ if (typeof accessToken !== 'string' || !accessToken)
680
+ return { status: null, token: 'missing' };
681
+ const exp = decodeJwtPayload(accessToken)?.exp;
682
+ if (typeof exp === 'number' && Date.now() / 1000 >= exp)
683
+ return { status: null, token: 'expired' };
684
+ try {
685
+ const response = await fetch(DROID_USAGE_URL, {
686
+ method: 'GET',
687
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
688
+ signal: AbortSignal.timeout(8000),
689
+ });
690
+ return { status: response.status, token: 'present' };
691
+ }
692
+ catch (err) {
693
+ return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
694
+ }
695
+ }
584
696
  /**
585
697
  * Normalize the Factory billing-limits payload into the common UsageWindow
586
698
  * shape. Orgs on the legacy (non token-rate-limit) billing model have no
@@ -1096,6 +1208,25 @@ function formatPercent(value) {
1096
1208
  const rounded = Math.round(value * 10) / 10;
1097
1209
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
1098
1210
  }
1211
+ /**
1212
+ * Compact "time until reset" hint for the inline usage bars: "5m", "2h", "3d",
1213
+ * or "now" once elapsed. Deliberately coarse (single unit, whole numbers) so it
1214
+ * fits after a bar without wrapping the row — the detailed section
1215
+ * (`formatResetAt`) carries the precise clock time.
1216
+ */
1217
+ function formatResetHint(date) {
1218
+ const diffMs = date.getTime() - Date.now();
1219
+ if (diffMs <= 0)
1220
+ return 'now';
1221
+ const mins = Math.round(diffMs / 60000);
1222
+ if (mins < 60)
1223
+ return `${Math.max(1, mins)}m`;
1224
+ const hours = Math.round(mins / 60);
1225
+ if (hours < 24)
1226
+ return `${hours}h`;
1227
+ const days = Math.round(hours / 24);
1228
+ return `${days}d`;
1229
+ }
1099
1230
  /** Format a reset timestamp as a human-readable relative or absolute time. */
1100
1231
  function formatResetAt(date) {
1101
1232
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.67",
3
+ "version": "1.20.69",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",