@phnx-labs/agents-cli 1.22.8 → 1.22.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +41 -9
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -1
- package/dist/commands/harness.js +316 -76
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +12 -1
- package/dist/index.js +9 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.js +18 -1
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/redact.js +4 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/startup/command-registry.js +15 -1
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/package.json +1 -1
|
@@ -1,49 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon-owned
|
|
2
|
+
* Daemon-owned usage refresher (per host).
|
|
3
3
|
*
|
|
4
4
|
* The routing hot path (`agents run` → collectRunCandidates) reads usage
|
|
5
5
|
* CACHE-ONLY (`getUsageInfoForIdentity` `readOnly`, RUSH-2061) and never blocks
|
|
6
6
|
* on a provider fetch. Something still has to keep that cache fresh — this
|
|
7
|
-
* module is that something, running inside the daemon
|
|
7
|
+
* module is that something, running inside the daemon on **each machine**.
|
|
8
8
|
*
|
|
9
|
-
* Design
|
|
10
|
-
* touches accounts whose credentials live locally, so no cross-host
|
|
11
|
-
* coordination and no shared-token contention):
|
|
9
|
+
* Design:
|
|
12
10
|
*
|
|
13
|
-
* - **
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
11
|
+
* - **Per-host sole writer.** Only accounts whose credentials live on THIS
|
|
12
|
+
* box are listed; no fleet-wide usage sync.
|
|
13
|
+
* - **Fixed 5-minute cadence** (`REFRESH_INTERVAL_MS`). Enough to keep
|
|
14
|
+
* balanced/`agents view` off multi-hour stale data without thrashing
|
|
15
|
+
* provider APIs when the user runs agents frequently. The delay helpers
|
|
16
|
+
* still accept a burn projection for tests/future tuning, but the default
|
|
17
|
+
* floor and ceiling are both 5 minutes.
|
|
18
|
+
* - **Hard hourly cap** (`HOURLY_CALL_CAP`) so a stuck "due" loop cannot
|
|
19
|
+
* hammer an endpoint past ~12 calls/account/hour.
|
|
20
|
+
* - **429 backoff.** A provider under `usageRateLimitedUntil` is skipped
|
|
21
|
+
* entirely — no live fetch, no Touch ID, no re-armed penalty.
|
|
22
|
+
* - **File-only credentials on the daemon path.** Refresh never opens the
|
|
23
|
+
* ACL-bound macOS keychain item (Touch ID storm). It uses the no-ACL
|
|
24
|
+
* access-token cache / setup-token / `.credentials.json` only
|
|
25
|
+
* (`fileOnly: true` on `getUsageInfo`).
|
|
26
|
+
* - **Concurrency-safe cache writes.** Usage + headroom files are updated
|
|
27
|
+
* under `withFileLock` + atomic rename so a concurrent `agents view`
|
|
28
|
+
* background refresh cannot tear or drop another account's row.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* Scenarios (what this path must survive):
|
|
31
|
+
*
|
|
32
|
+
* 1. **Daemon tick overlaps a slow tick** — overlap guard in daemon.ts;
|
|
33
|
+
* second tick is a no-op.
|
|
34
|
+
* 2. **`agents view` writes cache while daemon refreshes** — file lock
|
|
35
|
+
* serializes read-modify-write; no lost updates.
|
|
36
|
+
* 3. **macOS keychain ACL / Touch ID** — fileOnly refresh never calls
|
|
37
|
+
* `security find-generic-password` on Claude's ACL item.
|
|
38
|
+
* 4. **Provider 429** — whole provider skipped until Retry-After; cache
|
|
39
|
+
* freezes (visible as aged `capturedAt`) rather than hammering.
|
|
40
|
+
* 5. **Expired access token (no refresh)** — usage path never rotates
|
|
41
|
+
* single-use refresh tokens; counts as `failed`, reschedules 5m later.
|
|
42
|
+
* 6. **No file credential on this host** — account skipped / failed; no
|
|
43
|
+
* keychain fallback from the daemon refresher.
|
|
44
|
+
* 7. **Grok/Codex (network:false)** — not listed by `buildLocalUsageAccounts`;
|
|
45
|
+
* their "cache" is local logs, not this HTTP refresher.
|
|
29
46
|
*/
|
|
30
47
|
import * as fs from 'fs';
|
|
31
48
|
import * as path from 'path';
|
|
32
49
|
import { getCacheDir } from './state.js';
|
|
50
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
33
51
|
import { deriveUsageHeadroom, getUsageInfo, buildCanonicalUsageContext, agentUsesNetworkUsage, USAGE_SOURCE_AGENT_IDS, } from './usage.js';
|
|
34
52
|
import { getAccountInfo } from './agents.js';
|
|
35
53
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Default schedule between successful (or attempted) live usage fetches for one
|
|
56
|
+
* account. Floor and ceiling of the delay helper are pinned to this so the
|
|
57
|
+
* daemon does not poll faster than 5 minutes even under high burn, and does not
|
|
58
|
+
* let an idle account rot longer than 5 minutes between attempts.
|
|
59
|
+
*/
|
|
60
|
+
export const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
61
|
+
/** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
|
|
62
|
+
export const REFRESH_MIN_MS = REFRESH_INTERVAL_MS;
|
|
63
|
+
/** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
|
|
64
|
+
export const REFRESH_MAX_MS = REFRESH_INTERVAL_MS;
|
|
65
|
+
/** Burn-rate divisor retained for the pure delay helper / tests; with min=max
|
|
66
|
+
* the divisor does not change the scheduled interval. */
|
|
44
67
|
export const REFRESH_BURN_DIVISOR = 4;
|
|
45
|
-
/** At most this many live fetches per account per rolling hour. */
|
|
46
|
-
export const HOURLY_CALL_CAP =
|
|
68
|
+
/** At most this many live fetches per account per rolling hour (5m cadence ⇒ 12). */
|
|
69
|
+
export const HOURLY_CALL_CAP = 12;
|
|
70
|
+
/** How often the daemon wakes to *consider* a refresh pass (due accounts only). */
|
|
71
|
+
export const USAGE_REFRESH_TICK_MS = 60 * 1000;
|
|
47
72
|
const HOUR_MS = 60 * 60 * 1000;
|
|
48
73
|
/** Test seam for the headroom cache path (see usage.ts `setClaudeUsageCachePathForTest`). */
|
|
49
74
|
let headroomCachePathOverride = null;
|
|
@@ -74,27 +99,30 @@ export function readHeadroomEntry(usageKey) {
|
|
|
74
99
|
/** Merge entries into the cache (best-effort; preserves other accounts' rows). */
|
|
75
100
|
export function writeHeadroomEntries(entries) {
|
|
76
101
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
const cachePath = headroomCachePath();
|
|
103
|
+
ensureLockTarget(cachePath, JSON.stringify({ version: 1, entries: {} }, null, 2));
|
|
104
|
+
withFileLock(cachePath, () => {
|
|
105
|
+
// Re-read under the lock so a concurrent tick/view cannot drop rows.
|
|
106
|
+
const merged = {
|
|
107
|
+
version: 1,
|
|
108
|
+
entries: { ...readHeadroomCache(), ...entries },
|
|
109
|
+
};
|
|
110
|
+
atomicWriteFileSync(cachePath, JSON.stringify(merged, null, 2));
|
|
111
|
+
});
|
|
85
112
|
}
|
|
86
113
|
catch {
|
|
87
114
|
// best-effort; a failed write just means the router sees no projection
|
|
88
115
|
}
|
|
89
116
|
}
|
|
90
117
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
118
|
+
* Interval until the next refresh attempt, clamped to [minMs, maxMs].
|
|
119
|
+
* Defaults pin both ends to {@link REFRESH_INTERVAL_MS} (5 minutes) so the
|
|
120
|
+
* live daemon path is a fixed schedule. Tests may pass a wider range to
|
|
121
|
+
* exercise burn-aware scheduling without changing production cadence.
|
|
94
122
|
*/
|
|
95
123
|
export function computeNextRefreshDelayMs(minutesToLimit, opts = {}) {
|
|
96
|
-
const minMs = opts.minMs ??
|
|
97
|
-
const maxMs = opts.maxMs ??
|
|
124
|
+
const minMs = opts.minMs ?? REFRESH_INTERVAL_MS;
|
|
125
|
+
const maxMs = opts.maxMs ?? REFRESH_INTERVAL_MS;
|
|
98
126
|
const divisor = opts.divisor ?? REFRESH_BURN_DIVISOR;
|
|
99
127
|
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
100
128
|
return maxMs;
|
|
@@ -169,10 +197,14 @@ export async function buildLocalUsageAccounts() {
|
|
|
169
197
|
accounts.push({
|
|
170
198
|
usageKey,
|
|
171
199
|
agentId,
|
|
200
|
+
// fileOnly: never open the ACL-bound keychain item from the daemon —
|
|
201
|
+
// that path is the Touch ID storm. Usage reads setup-token /
|
|
202
|
+
// no-ACL cache / .credentials.json only (see loadClaudeOauth).
|
|
172
203
|
fetch: () => getUsageInfo(agentId, {
|
|
173
204
|
home: fetchInput.home,
|
|
174
205
|
cliVersion: fetchInput.cliVersion,
|
|
175
206
|
organizationId: fetchInput.organizationId,
|
|
207
|
+
fileOnly: true,
|
|
176
208
|
}),
|
|
177
209
|
});
|
|
178
210
|
}
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -86,6 +86,13 @@ interface UsageOptions {
|
|
|
86
86
|
home?: string;
|
|
87
87
|
cliVersion?: string | null;
|
|
88
88
|
organizationId?: string | null;
|
|
89
|
+
/**
|
|
90
|
+
* When true, never open the ACL-bound OS keychain item (macOS Touch ID).
|
|
91
|
+
* Daemon usage refresh sets this so a background tick cannot pop biometrics.
|
|
92
|
+
* Credentials come from the no-ACL access-token cache, a file-based
|
|
93
|
+
* setup-token, or `<home>/.claude/.credentials.json` only.
|
|
94
|
+
*/
|
|
95
|
+
fileOnly?: boolean;
|
|
89
96
|
}
|
|
90
97
|
/** Canonical input for a single usage fetch operation. */
|
|
91
98
|
export interface UsageFetchInput {
|
|
@@ -462,9 +469,14 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
|
|
|
462
469
|
* `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
|
|
463
470
|
* for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
|
|
464
471
|
* access-token-only consumers (the usage fetch and the auth-health probe) opt in.
|
|
472
|
+
*
|
|
473
|
+
* `opts.fileOnly` (implies access-token-only consumers) skips the ACL keychain
|
|
474
|
+
* read entirely — setup-token, no-ACL cache, and `.credentials.json` only. Used
|
|
475
|
+
* by the daemon usage refresher so a background tick can never pop Touch ID.
|
|
465
476
|
*/
|
|
466
477
|
export declare function loadClaudeOauth(home?: string, opts?: {
|
|
467
478
|
accessTokenCache?: boolean;
|
|
479
|
+
fileOnly?: boolean;
|
|
468
480
|
}): Promise<ClaudeOauthCredentials | null>;
|
|
469
481
|
/**
|
|
470
482
|
* Save Claude OAuth credentials to the system keychain/keyring.
|
package/dist/lib/usage.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
|
24
24
|
import { formatBackoffRemaining, noteUsageRateLimited, usageRateLimitedUntil, } from './usage-backoff.js';
|
|
25
25
|
import { getCacheDir } from './state.js';
|
|
26
26
|
import { mapBounded } from './concurrency.js';
|
|
27
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
29
|
const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
29
30
|
const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
@@ -654,7 +655,12 @@ async function getClaudeUsageInfo(options) {
|
|
|
654
655
|
try {
|
|
655
656
|
// Opt into the no-ACL access-token cache: this is the every-60s watchdog hot
|
|
656
657
|
// path and usage needs only the access token, so it kills the Touch ID storm.
|
|
657
|
-
|
|
658
|
+
// Daemon refresh also sets fileOnly so we never fall through to the ACL
|
|
659
|
+
// keychain item (that path is the Touch ID prompt).
|
|
660
|
+
const oauth = await loadClaudeOauth(options?.home, {
|
|
661
|
+
accessTokenCache: true,
|
|
662
|
+
fileOnly: options?.fileOnly === true,
|
|
663
|
+
});
|
|
658
664
|
if (!oauth?.accessToken) {
|
|
659
665
|
return { snapshot: null, error: usageNoCredentialError('Claude') };
|
|
660
666
|
}
|
|
@@ -1333,6 +1339,10 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1333
1339
|
* `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
|
|
1334
1340
|
* for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
|
|
1335
1341
|
* access-token-only consumers (the usage fetch and the auth-health probe) opt in.
|
|
1342
|
+
*
|
|
1343
|
+
* `opts.fileOnly` (implies access-token-only consumers) skips the ACL keychain
|
|
1344
|
+
* read entirely — setup-token, no-ACL cache, and `.credentials.json` only. Used
|
|
1345
|
+
* by the daemon usage refresher so a background tick can never pop Touch ID.
|
|
1336
1346
|
*/
|
|
1337
1347
|
export async function loadClaudeOauth(home, opts) {
|
|
1338
1348
|
// Read-only usage/probe callers (accessTokenCache) authenticate with a
|
|
@@ -1359,7 +1369,11 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1359
1369
|
// anywhere, so the platform check must yield to it exactly as the inner
|
|
1360
1370
|
// claudeOauthCacheActive() gate does — otherwise this whole block is dead on
|
|
1361
1371
|
// a Windows runner and the tests below it read an empty home.
|
|
1362
|
-
|
|
1372
|
+
//
|
|
1373
|
+
// fileOnly: daemon refresh must never open the ACL-bound item (Touch ID).
|
|
1374
|
+
// It may still read the no-ACL cache item (set-no-acl — prompt-free).
|
|
1375
|
+
if (!opts?.fileOnly
|
|
1376
|
+
&& (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden())) {
|
|
1363
1377
|
const service = getClaudeKeychainService(home);
|
|
1364
1378
|
// Serve the no-ACL cache first (opt-in, macOS/test only) so the ACL-gated read
|
|
1365
1379
|
// below — the one that pops Touch ID — happens at most once per token lifetime
|
|
@@ -1386,6 +1400,13 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1386
1400
|
deleteCachedClaudeOauth(service);
|
|
1387
1401
|
}
|
|
1388
1402
|
}
|
|
1403
|
+
else if (opts?.fileOnly === true && opts?.accessTokenCache === true && claudeOauthCacheActive()) {
|
|
1404
|
+
// fileOnly + accessTokenCache: still allow the no-ACL cache (never Touch ID).
|
|
1405
|
+
const service = getClaudeKeychainService(home);
|
|
1406
|
+
const cached = readCachedClaudeOauth(service);
|
|
1407
|
+
if (cached)
|
|
1408
|
+
return cached;
|
|
1409
|
+
}
|
|
1389
1410
|
const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
|
|
1390
1411
|
try {
|
|
1391
1412
|
if (fs.existsSync(credsPath)) {
|
|
@@ -1487,9 +1508,19 @@ export function readClaudeUsageCache(usageKey, cachePath = getClaudeUsageCachePa
|
|
|
1487
1508
|
}
|
|
1488
1509
|
/** Write a usage snapshot to the on-disk cache. */
|
|
1489
1510
|
export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeUsageCachePath()) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1511
|
+
try {
|
|
1512
|
+
ensureLockTarget(cachePath, '{}');
|
|
1513
|
+
withFileLock(cachePath, () => {
|
|
1514
|
+
// Re-read under the lock so a concurrent daemon tick / agents view
|
|
1515
|
+
// refresh cannot drop another account's row (lost update).
|
|
1516
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1517
|
+
cache[usageKey] = serializeClaudeUsageSnapshot(snapshot);
|
|
1518
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
catch {
|
|
1522
|
+
/* best-effort cache write — lock busy or disk full */
|
|
1523
|
+
}
|
|
1493
1524
|
}
|
|
1494
1525
|
/** Read the entire usage cache file from disk. */
|
|
1495
1526
|
function readClaudeUsageCacheFile(cachePath) {
|
|
@@ -1508,7 +1539,7 @@ function readClaudeUsageCacheFile(cachePath) {
|
|
|
1508
1539
|
function writeClaudeUsageCacheFile(cache, cachePath) {
|
|
1509
1540
|
try {
|
|
1510
1541
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1511
|
-
|
|
1542
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1512
1543
|
}
|
|
1513
1544
|
catch {
|
|
1514
1545
|
/* best-effort cache write */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.9",
|
|
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",
|