@phnx-labs/agents-cli 1.20.89 → 1.20.90

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 (58) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +6 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +7 -1
  5. package/dist/commands/harness.d.ts +27 -0
  6. package/dist/commands/harness.js +120 -13
  7. package/dist/commands/profiles.d.ts +3 -0
  8. package/dist/commands/profiles.js +1 -1
  9. package/dist/commands/routines.d.ts +19 -0
  10. package/dist/commands/routines.js +28 -6
  11. package/dist/commands/secrets.d.ts +10 -1
  12. package/dist/commands/secrets.js +18 -6
  13. package/dist/commands/sessions-browser.d.ts +4 -0
  14. package/dist/commands/sessions-browser.js +51 -9
  15. package/dist/commands/sessions-favorite.d.ts +20 -0
  16. package/dist/commands/sessions-favorite.js +120 -0
  17. package/dist/commands/sessions.d.ts +103 -20
  18. package/dist/commands/sessions.js +356 -62
  19. package/dist/commands/setup-secrets.d.ts +7 -0
  20. package/dist/commands/setup-secrets.js +12 -9
  21. package/dist/commands/versions.js +12 -4
  22. package/dist/commands/view.d.ts +14 -1
  23. package/dist/commands/view.js +103 -128
  24. package/dist/lib/agents.d.ts +4 -2
  25. package/dist/lib/agents.js +21 -6
  26. package/dist/lib/hosts/dispatch.js +19 -1
  27. package/dist/lib/hq/floor.js +12 -0
  28. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  29. package/dist/lib/picker.d.ts +27 -2
  30. package/dist/lib/picker.js +71 -7
  31. package/dist/lib/profiles.d.ts +48 -0
  32. package/dist/lib/profiles.js +67 -0
  33. package/dist/lib/rotate.d.ts +24 -2
  34. package/dist/lib/rotate.js +63 -6
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/session/active.d.ts +109 -3
  38. package/dist/lib/session/active.js +269 -13
  39. package/dist/lib/session/db.d.ts +14 -0
  40. package/dist/lib/session/db.js +35 -0
  41. package/dist/lib/session/favorites.d.ts +39 -0
  42. package/dist/lib/session/favorites.js +101 -0
  43. package/dist/lib/session/host-link.d.ts +68 -0
  44. package/dist/lib/session/host-link.js +64 -0
  45. package/dist/lib/session/presence.d.ts +85 -0
  46. package/dist/lib/session/presence.js +150 -0
  47. package/dist/lib/session/remote-list.d.ts +10 -0
  48. package/dist/lib/session/remote-list.js +47 -9
  49. package/dist/lib/tmux/binary.d.ts +7 -0
  50. package/dist/lib/tmux/binary.js +11 -1
  51. package/dist/lib/types.d.ts +4 -3
  52. package/dist/lib/usage-backoff.d.ts +29 -0
  53. package/dist/lib/usage-backoff.js +165 -0
  54. package/dist/lib/usage.d.ts +112 -5
  55. package/dist/lib/usage.js +464 -46
  56. package/dist/lib/watchdog/runner.d.ts +13 -0
  57. package/dist/lib/watchdog/runner.js +16 -1
  58. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ import type { AgentId } from './types.js';
2
+ export declare function setUsageBackoffDirForTest(dir: string | null): string | null;
3
+ /**
4
+ * Parse a `Retry-After` header. HTTP allows either delta-seconds or an HTTP
5
+ * date; both appear in the wild, so handle both and ignore anything else.
6
+ * Returns milliseconds from `now`, or null when there is nothing usable.
7
+ */
8
+ export declare function parseRetryAfterMs(header: string | null | undefined, now?: number): number | null;
9
+ /**
10
+ * Record that `agent`'s usage endpoint threw a 429. `retryAfter` is the raw
11
+ * header; when it is absent or unparseable we still back off for `fallbackMs`,
12
+ * because continuing to poll an endpoint that just said no is what created the
13
+ * loop in the first place.
14
+ */
15
+ export declare function noteUsageRateLimited(agent: AgentId, retryAfter: string | null | undefined, opts?: {
16
+ now?: number;
17
+ fallbackMs?: number;
18
+ }): void;
19
+ /**
20
+ * Epoch ms until which `agent`'s usage endpoint should not be called, or null
21
+ * when it is free — the furthest recorded deadline still in the future, so a
22
+ * concurrently-written shorter one can never pull it in.
23
+ *
24
+ * Sweeps elapsed files while it is here: they can only accumulate at the rate
25
+ * penalties are issued, and this is the one place that already lists them.
26
+ */
27
+ export declare function usageRateLimitedUntil(agent: AgentId, now?: number): number | null;
28
+ /** Human-readable remaining backoff, for the error a skipped read returns. */
29
+ export declare function formatBackoffRemaining(untilMs: number, now?: number): string;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Respect a usage endpoint's `Retry-After` instead of hammering through it.
3
+ *
4
+ * The failure this exists to stop, measured on `yosemite-s1` (2026-08-03): every
5
+ * Claude account there returned `429 rate_limit_error` with `retry-after: 2678`
6
+ * — about 45 minutes — while the credentials themselves read healthy
7
+ * (`probeClaudeStatus` → `status=429 token=present`). The daemon warms
8
+ * auth-health every 3 minutes (`daemon.ts`, `setInterval(..., 3 * 60_000)`) and
9
+ * `probeLocalFleetAuth` fans out over every installed version home in one
10
+ * `Promise.all`, so five Claude homes meant five concurrent requests to one
11
+ * endpoint every three minutes — ~100/hour from a single machine. Nothing read
12
+ * `Retry-After`, so each tick fired deep inside the penalty window and re-armed
13
+ * the throttle. Measured 75 minutes apart, the box never got out: `retry-after`
14
+ * 2678s at 08:42Z, still 429 at 09:57Z on a freshly-issued 1208s penalty. Its
15
+ * usage cache froze — exactly the permanently-stale state the routing freshness
16
+ * rule (`rotate.ts`, `USAGE_DECISION_MAX_AGE_MS`) was written to defend against.
17
+ *
18
+ * So a 429 is recorded here with its deadline, and every usage read and health
19
+ * probe for that provider short-circuits until the deadline passes — no request,
20
+ * no renewed penalty. State is on disk rather than in memory because the
21
+ * offenders are separate processes: the long-lived daemon, and every one-shot
22
+ * `agents view` / `agents run` invocation.
23
+ *
24
+ * ## The deadline lives in the FILENAME, and that is the whole design
25
+ *
26
+ * The obvious shape — one JSON document holding `{agent: deadline}` — is
27
+ * read-modify-write, and without a lock two processes recording the same
28
+ * provider can both read the old value and let the SHORTER deadline write last,
29
+ * silently undoing the longer penalty. That is not a theoretical race here: the
30
+ * triggering condition is a batch of concurrent same-provider 429s, which is
31
+ * precisely what the daemon issues, and it can recur on every batch. Reading
32
+ * back and retrying does not fix it either — that only detects a clobber which
33
+ * already landed, and a stale writer can still write after the check.
34
+ *
35
+ * So no shared document. Each penalty is its own file named `<agent>.<deadline>`
36
+ * with empty contents, and a read takes the MAXIMUM deadline across that
37
+ * provider's files. Two concurrent writers create two different files and
38
+ * neither can erase the other, so a shorter deadline cannot displace a longer
39
+ * one — monotonicity is structural rather than argued, and there is no lock to
40
+ * go stale on a path every usage read touches. Elapsed files are swept on read.
41
+ *
42
+ * Deliberately per-provider, not per-account: the endpoint throttles the caller,
43
+ * and the observed 429 hit all five accounts on the box at once. Backing off one
44
+ * account while the others keep firing would not clear the penalty.
45
+ */
46
+ import * as fs from 'fs';
47
+ import * as path from 'path';
48
+ import { getCacheDir } from './state.js';
49
+ /** Cap a server-supplied delay so a bad header cannot park a provider forever. */
50
+ const MAX_BACKOFF_MS = 60 * 60 * 1000;
51
+ /**
52
+ * Test seam, mirroring `setKeychainBackendForTest`. The cache dir is resolved
53
+ * from a module-level constant at import time, so overriding `HOME` in a test
54
+ * does NOT redirect this state — it silently writes into the developer's real
55
+ * `~/.agents/.cache/` and parks their own usage reads behind a 45-minute
56
+ * penalty. (It did exactly that once while this was being written.) Returns the
57
+ * previous value so a test can restore it.
58
+ */
59
+ let backoffDirOverride = null;
60
+ export function setUsageBackoffDirForTest(dir) {
61
+ const prev = backoffDirOverride;
62
+ backoffDirOverride = dir;
63
+ return prev;
64
+ }
65
+ function backoffDir() {
66
+ return backoffDirOverride ?? path.join(getCacheDir(), 'usage-backoff');
67
+ }
68
+ /**
69
+ * Parse a `Retry-After` header. HTTP allows either delta-seconds or an HTTP
70
+ * date; both appear in the wild, so handle both and ignore anything else.
71
+ * Returns milliseconds from `now`, or null when there is nothing usable.
72
+ */
73
+ export function parseRetryAfterMs(header, now = Date.now()) {
74
+ const raw = (header ?? '').trim();
75
+ if (!raw)
76
+ return null;
77
+ if (/^\d+$/.test(raw)) {
78
+ const ms = Number(raw) * 1000;
79
+ return ms > 0 ? Math.min(ms, MAX_BACKOFF_MS) : null;
80
+ }
81
+ const at = Date.parse(raw);
82
+ if (Number.isNaN(at))
83
+ return null;
84
+ const ms = at - now;
85
+ return ms > 0 ? Math.min(ms, MAX_BACKOFF_MS) : null;
86
+ }
87
+ /** Every recorded deadline for `agent`, newest-first. Never throws. */
88
+ function deadlinesFor(agent) {
89
+ let names;
90
+ try {
91
+ names = fs.readdirSync(backoffDir());
92
+ }
93
+ catch {
94
+ // No directory yet: nothing is throttled.
95
+ return [];
96
+ }
97
+ const prefix = `${agent}.`;
98
+ const out = [];
99
+ for (const name of names) {
100
+ if (!name.startsWith(prefix))
101
+ continue;
102
+ const at = Number(name.slice(prefix.length));
103
+ if (Number.isFinite(at))
104
+ out.push(at);
105
+ }
106
+ return out;
107
+ }
108
+ /**
109
+ * Record that `agent`'s usage endpoint threw a 429. `retryAfter` is the raw
110
+ * header; when it is absent or unparseable we still back off for `fallbackMs`,
111
+ * because continuing to poll an endpoint that just said no is what created the
112
+ * loop in the first place.
113
+ */
114
+ export function noteUsageRateLimited(agent, retryAfter, opts) {
115
+ const now = opts?.now ?? Date.now();
116
+ const fallbackMs = opts?.fallbackMs ?? 15 * 60 * 1000;
117
+ const ms = parseRetryAfterMs(retryAfter, now) ?? fallbackMs;
118
+ const deadline = now + Math.min(ms, MAX_BACKOFF_MS);
119
+ try {
120
+ fs.mkdirSync(backoffDir(), { recursive: true });
121
+ // Empty file: the name carries the whole value, so there is no content a
122
+ // concurrent reader could catch half-written, and no document to merge.
123
+ fs.writeFileSync(path.join(backoffDir(), `${agent}.${deadline}`), '');
124
+ }
125
+ catch {
126
+ // Best-effort. An unwritable cache dir costs the cross-process backoff, not
127
+ // the correctness of this read.
128
+ }
129
+ }
130
+ /**
131
+ * Epoch ms until which `agent`'s usage endpoint should not be called, or null
132
+ * when it is free — the furthest recorded deadline still in the future, so a
133
+ * concurrently-written shorter one can never pull it in.
134
+ *
135
+ * Sweeps elapsed files while it is here: they can only accumulate at the rate
136
+ * penalties are issued, and this is the one place that already lists them.
137
+ */
138
+ export function usageRateLimitedUntil(agent, now = Date.now()) {
139
+ let latest = null;
140
+ for (const at of deadlinesFor(agent)) {
141
+ if (at > now) {
142
+ if (latest === null || at > latest)
143
+ latest = at;
144
+ }
145
+ else {
146
+ try {
147
+ fs.rmSync(path.join(backoffDir(), `${agent}.${at}`), { force: true });
148
+ }
149
+ catch {
150
+ /* another process may have swept it already */
151
+ }
152
+ }
153
+ }
154
+ return latest;
155
+ }
156
+ /** Human-readable remaining backoff, for the error a skipped read returns. */
157
+ export function formatBackoffRemaining(untilMs, now = Date.now()) {
158
+ const mins = Math.ceil((untilMs - now) / 60_000);
159
+ if (mins <= 1)
160
+ return 'under a minute';
161
+ if (mins < 60)
162
+ return `${mins} minutes`;
163
+ const hours = Math.round(mins / 60);
164
+ return hours === 1 ? 'about an hour' : `about ${hours} hours`;
165
+ }
@@ -1,5 +1,42 @@
1
1
  import { type AccountInfo } from './agents.js';
2
2
  import type { AgentId } from './types.js';
3
+ /**
4
+ * Why a usage read produced no snapshot, when the cause is the credential or the
5
+ * server rather than the payload. Every provider used to return `error: null`
6
+ * for all three, which made an account nobody can read indistinguishable from a
7
+ * healthy one: the caller fell back to whatever was in the SWR cache and
8
+ * rendered its bars as fact. On `yosemite-s1` that hid five Claude accounts
9
+ * whose stored access token had expired — one of them eleven days earlier —
10
+ * behind a cache frozen for 26h, and balanced routing launched into an account
11
+ * that was actually at its weekly cap.
12
+ *
13
+ * No usage read ever refreshes a token (RUSH-1822 for Claude; the same rule for
14
+ * Kimi/Droid/Cursor, whose own CLIs rotate on their next launch), so an expired
15
+ * credential cannot heal on its own — the account stays unreadable until that
16
+ * agent actually runs, or a long-lived token is provisioned for it.
17
+ *
18
+ * Shared across all four networked providers on purpose: the failure shape is
19
+ * identical, and wiring only Claude would leave `agents view --refresh`
20
+ * reporting Claude accounts while silently presenting stale Kimi, Droid, and
21
+ * Cursor readings as confirmed.
22
+ */
23
+ export declare function usageNoCredentialError(agent: string): string;
24
+ export declare function usageExpiredCredentialError(agent: string): string;
25
+ export declare function usageRejectedError(agent: string, status: number): string;
26
+ /**
27
+ * The read threw rather than answering — a timeout, DNS/TLS failure, a payload
28
+ * that would not parse, a credential that would not decrypt. Every provider
29
+ * swallowed these into `error: null`, which is the same silence as an expired
30
+ * token: the caller renders a stale snapshot as confirmed. The cause is carried
31
+ * verbatim because these are the failures a user cannot otherwise see.
32
+ */
33
+ /**
34
+ * The provider told us to back off and we are still inside that window, so this
35
+ * read made no request at all. Distinct from `usageRejectedError(agent, 429)`,
36
+ * which is the 429 itself: this one says we are *honouring* it.
37
+ */
38
+ export declare function usageThrottledError(agent: string, untilMs: number): string;
39
+ export declare function usageUnreachableError(agent: string, cause?: unknown): string;
3
40
  /**
4
41
  * True when a Claude OAuth access token is within the refresh leeway of expiry
5
42
  * (or already expired) — i.e. it "would need a refresh" before the next use.
@@ -92,6 +129,10 @@ declare const USAGE_SOURCES: {
92
129
  readonly fetch: typeof getCursorUsageInfo;
93
130
  readonly network: true;
94
131
  };
132
+ readonly antigravity: {
133
+ readonly fetch: typeof getAntigravityUsageInfo;
134
+ readonly network: true;
135
+ };
95
136
  };
96
137
  export declare const USAGE_SOURCE_AGENT_IDS: (keyof typeof USAGE_SOURCES)[];
97
138
  /** Fetch usage info for a given agent through the canonical source registry. */
@@ -107,20 +148,29 @@ export declare function buildCanonicalUsageContext(inputs: UsageIdentityInput[])
107
148
  usageFetchInputs: Map<string, UsageFetchInput>;
108
149
  };
109
150
  /**
110
- * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid/Cursor
111
- * via a live API, Codex/Grok via local session logs. Everything else has no usage
112
- * concept, so callers use this to decide whether a missing snapshot is worth
113
- * flagging as "usage unavailable" (a signed-in Claude account with no data)
114
- * versus simply not applicable (Antigravity, OpenCode).
151
+ * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid/
152
+ * Cursor/Antigravity via a live API, Codex/Grok via local session logs.
153
+ * Everything else has no usage concept, so callers use this to decide whether
154
+ * a missing snapshot is worth flagging as "usage unavailable" (a signed-in
155
+ * Claude account with no data) versus simply not applicable (OpenCode).
115
156
  */
116
157
  export declare function agentReportsUsage(agentId: AgentId): boolean;
117
158
  /** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
118
159
  export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: {
119
160
  forceRefresh?: boolean;
161
+ maxAgeMs?: number;
120
162
  }): Promise<{
121
163
  canonicalByUsageKey: Map<string, AccountInfo>;
122
164
  usageByKey: Map<string, UsageInfo>;
123
165
  }>;
166
+ /**
167
+ * How stale a cached snapshot may be before the read stops serving it and blocks
168
+ * on the network. Defaults to the full 24h stale-while-revalidate window; a
169
+ * caller that is about to ROUTE on the number passes a shorter `maxAgeMs` and
170
+ * gets a live read instead of a day-old one. Never widens past 24h — a caller
171
+ * cannot opt into more staleness than the cache policy allows.
172
+ */
173
+ export declare function swrWindowMsFor(maxAgeMs?: number): number;
124
174
  /**
125
175
  * Fetch usage for a single identity using stale-while-revalidate.
126
176
  *
@@ -135,10 +185,12 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opt
135
185
  */
136
186
  export declare function getUsageInfoForIdentity(input: UsageIdentityInput, opts?: {
137
187
  forceRefresh?: boolean;
188
+ maxAgeMs?: number;
138
189
  }): Promise<UsageInfo>;
139
190
  /** Format a one-line usage summary with compact bars for inline display. */
140
191
  export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: {
141
192
  unavailable?: boolean;
193
+ unverified?: boolean;
142
194
  }): string;
143
195
  /**
144
196
  * Derive an account's real throttle state from its live usage windows — the
@@ -390,4 +442,59 @@ export declare function normalizeCursorUsage(data: CursorUsageResponse): UsageWi
390
442
  * row still renders, without a misleading empty gauge).
391
443
  */
392
444
  declare function getCursorUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
445
+ /** The OAuth token `agy` stores (inside `{ token: … }`) in the OS keyring or file. */
446
+ interface AntigravityOauthToken {
447
+ access_token?: string | null;
448
+ refresh_token?: string | null;
449
+ /** RFC3339 expiry timestamp for the access token. */
450
+ expiry?: string | null;
451
+ }
452
+ /** One per-model quota bucket from the :retrieveUserQuota response. */
453
+ export interface AntigravityQuotaBucket {
454
+ modelId?: string | null;
455
+ tokenType?: string | null;
456
+ remainingFraction?: number | null;
457
+ resetTime?: string | null;
458
+ }
459
+ /** Response shape from the Code Assist :retrieveUserQuota endpoint. */
460
+ export interface AntigravityQuotaResponse {
461
+ buckets?: AntigravityQuotaBucket[] | null;
462
+ }
463
+ /**
464
+ * Parse a stored `agy` OAuth payload into its token. Handles both on-disk
465
+ * shapes: the raw `{ token: {…} }` JSON (Linux file fallback) and the
466
+ * `go-keyring-base64:<base64>` wrapper zalando/go-keyring writes into the
467
+ * macOS Keychain item (service `gemini`, account `antigravity`). Never throws
468
+ * (malformed input => null).
469
+ */
470
+ export declare function parseAntigravityOauthPayload(raw: string): AntigravityOauthToken | null;
471
+ /**
472
+ * True when the stored access token is expired (or inside the refresh leeway).
473
+ * A missing/unparseable expiry is treated as still-fresh — the quota call
474
+ * below is the source of truth if the token is actually dead (401 => render
475
+ * nothing), and we never want to force a refresh without evidence.
476
+ */
477
+ export declare function antigravityTokenNeedsRefresh(expiry: string | null | undefined, nowMs?: number): boolean;
478
+ /** Compact model tag for the inline bar — 'gemini-2.5-flash-lite' => '2.5FL'. */
479
+ export declare function antigravityModelShortLabel(modelId: string): string;
480
+ /**
481
+ * Normalize the per-model quota buckets into the common UsageWindow shape —
482
+ * one window per model (`gemini-3.1-pro`, `gemini-2.5-flash`, …), keyed
483
+ * `session` since each bucket is a short-cycle quota with its own reset time.
484
+ * Duplicate buckets for one model keep the LOWEST remaining fraction (the
485
+ * most conservative read). Sorted most-used first so the bar closest to
486
+ * throttling leads the row. `windowMinutes` stays null: the API reports only
487
+ * the reset timestamp, not the window length, and an inferred 5h session
488
+ * length would wrongly zero the SWR cache between resets.
489
+ */
490
+ export declare function normalizeAntigravityWindows(buckets: AntigravityQuotaBucket[]): UsageWindow[];
491
+ /**
492
+ * Fetch Antigravity usage via Google Code Assist's :retrieveUserQuota — the
493
+ * quota API `agy` itself talks to (its log shows the sibling :loadCodeAssist
494
+ * and :fetchAvailableModels calls on the same host). Auth is the stored `agy`
495
+ * OAuth token (OS keyring on macOS, file fallback on Linux), refreshed
496
+ * in-memory when expired — safe because Google's refresh tokens are
497
+ * non-rotating (see refreshAntigravityAccessToken).
498
+ */
499
+ declare function getAntigravityUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
393
500
  export {};