@phnx-labs/agents-cli 1.22.29 → 1.22.30
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 +82 -0
- package/README.md +39 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -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/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -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/secrets/agent.d.ts +19 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
package/dist/lib/usage.js
CHANGED
|
@@ -681,10 +681,11 @@ export function claudeUsageAccessTokenNoRefresh(oauth) {
|
|
|
681
681
|
/** Fetch Claude usage via the Anthropic OAuth usage API. */
|
|
682
682
|
async function getClaudeUsageInfo(options) {
|
|
683
683
|
try {
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
684
|
+
// accessTokenCache: this is the every-60s watchdog hot path and usage needs
|
|
685
|
+
// only the access token, so it reads ONLY the file-based setup-token and never
|
|
686
|
+
// the interactive login (reading that ACL-bound token and firing it at the
|
|
687
|
+
// usage API is what got it revoked — RUSH-1822). No setup-token => null =>
|
|
688
|
+
// "usage pending". fileOnly additionally forbids the ACL keychain path.
|
|
688
689
|
const oauth = await loadClaudeOauth(options?.home, {
|
|
689
690
|
accessTokenCache: true,
|
|
690
691
|
fileOnly: options?.fileOnly === true,
|
|
@@ -977,9 +978,10 @@ async function getDroidUsageInfo(options) {
|
|
|
977
978
|
}
|
|
978
979
|
/** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
|
|
979
980
|
export async function probeClaudeStatus(home, cliVersion) {
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
981
|
+
// accessTokenCache: the daemon warms this probe every ~3 min per account, so it
|
|
982
|
+
// reads ONLY the file-based setup-token and never the interactive login —
|
|
983
|
+
// transmitting that ACL-bound token to the usage API from a background loop is
|
|
984
|
+
// what got it revoked (RUSH-1822). No setup-token => token 'missing' below.
|
|
983
985
|
const oauth = await loadClaudeOauth(home, { accessTokenCache: true });
|
|
984
986
|
const accessToken = oauth?.accessToken?.trim();
|
|
985
987
|
if (!accessToken)
|
|
@@ -1261,97 +1263,29 @@ function parseClaudeOauthPayload(raw) {
|
|
|
1261
1263
|
return null;
|
|
1262
1264
|
}
|
|
1263
1265
|
}
|
|
1264
|
-
// ──
|
|
1266
|
+
// ── Stale no-ACL Claude OAuth cache eviction (retired subsystem) ──
|
|
1265
1267
|
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
// NO-ACL keychain item — the same `set-no-acl` mechanism secrets/session-store.ts
|
|
1275
|
-
// uses for unlocked bundles, whose reads never prompt — and serve every later read
|
|
1276
|
-
// from it until the token's own `expiresAt`. The ACL read then happens at most once
|
|
1277
|
-
// per token lifetime (~a few times a day), shared across every agent process.
|
|
1278
|
-
//
|
|
1279
|
-
// Security posture: only the short-lived access token is cached, never the refresh
|
|
1280
|
-
// token, and the entry dies at the token's expiry (capped below). This mirrors the
|
|
1281
|
-
// no-ACL posture session-store.ts already accepts for held bundles.
|
|
1268
|
+
// Earlier versions cached Claude's OAuth ACCESS token in a device-local no-ACL
|
|
1269
|
+
// keychain item so a read-only usage/probe read wouldn't pop the macOS Touch ID
|
|
1270
|
+
// prompt that the ACL-bound source item (`Claude Code-credentials-<hash>`) forces.
|
|
1271
|
+
// That cache is retired: read-only probes now authenticate ONLY with a file-based
|
|
1272
|
+
// setup-token and never read the interactive login (see loadClaudeOauth), so
|
|
1273
|
+
// nothing populates the cache anymore. deleteCachedClaudeOauth remains — a
|
|
1274
|
+
// credential rotation still evicts a stale item an earlier version may have
|
|
1275
|
+
// written, so an old no-ACL copy of the interactive token can't linger.
|
|
1282
1276
|
const CLAUDE_OAUTH_CACHE_PREFIX = 'agents-cli.claude-oauth-cache.';
|
|
1283
|
-
/** Hard cap so a token with a distant or absent `expiresAt` can't pin a stale entry. */
|
|
1284
|
-
const CLAUDE_OAUTH_CACHE_MAX_TTL_MS = 8 * 60 * 60 * 1000;
|
|
1285
1277
|
/** The no-ACL cache item name for a Claude keychain service (hashed to stay tidy). */
|
|
1286
1278
|
function claudeOauthCacheItem(service) {
|
|
1287
1279
|
const hash = createHash('sha256').update(service).digest('hex').slice(0, 16);
|
|
1288
1280
|
return `${CLAUDE_OAUTH_CACHE_PREFIX}${hash}`;
|
|
1289
1281
|
}
|
|
1290
|
-
/**
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
* or the token itself has expired — in which case the stale entry is dropped. */
|
|
1298
|
-
function readCachedClaudeOauth(service) {
|
|
1299
|
-
try {
|
|
1300
|
-
// The cache item is written no-ACL (writeCachedClaudeOauth) — its whole purpose is
|
|
1301
|
-
// serving the token prompt-free, so attest that to the raw-read storm guard.
|
|
1302
|
-
const entry = JSON.parse(getKeychainToken(claudeOauthCacheItem(service), { silentNoAcl: true }));
|
|
1303
|
-
if (!entry || typeof entry.accessToken !== 'string' || !entry.accessToken)
|
|
1304
|
-
return null;
|
|
1305
|
-
const now = Date.now();
|
|
1306
|
-
const tokenExpired = typeof entry.expiresAt === 'number' && entry.expiresAt > 0 && now >= entry.expiresAt;
|
|
1307
|
-
if (now >= entry.cacheExpiresAt || tokenExpired) {
|
|
1308
|
-
try {
|
|
1309
|
-
deleteKeychainToken(claudeOauthCacheItem(service));
|
|
1310
|
-
}
|
|
1311
|
-
catch {
|
|
1312
|
-
/* best-effort eviction */
|
|
1313
|
-
}
|
|
1314
|
-
return null;
|
|
1315
|
-
}
|
|
1316
|
-
return {
|
|
1317
|
-
accessToken: entry.accessToken,
|
|
1318
|
-
refreshToken: null,
|
|
1319
|
-
expiresAt: entry.expiresAt ?? null,
|
|
1320
|
-
scopes: entry.scopes ?? null,
|
|
1321
|
-
subscriptionType: entry.subscriptionType ?? null,
|
|
1322
|
-
rateLimitTier: entry.rateLimitTier ?? null,
|
|
1323
|
-
organizationUuid: entry.organizationUuid ?? null,
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
catch {
|
|
1327
|
-
return null;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
/** Populate the no-ACL cache after a real (prompting) source read. Best-effort — the
|
|
1331
|
-
* cache is an optimization, so any failure just means the next read prompts again. */
|
|
1332
|
-
function writeCachedClaudeOauth(service, creds) {
|
|
1333
|
-
if (!creds.accessToken)
|
|
1334
|
-
return;
|
|
1335
|
-
try {
|
|
1336
|
-
const now = Date.now();
|
|
1337
|
-
const cap = now + CLAUDE_OAUTH_CACHE_MAX_TTL_MS;
|
|
1338
|
-
const tokenExp = typeof creds.expiresAt === 'number' && creds.expiresAt > now ? creds.expiresAt : cap;
|
|
1339
|
-
const entry = {
|
|
1340
|
-
accessToken: creds.accessToken,
|
|
1341
|
-
expiresAt: creds.expiresAt ?? null,
|
|
1342
|
-
scopes: creds.scopes ?? null,
|
|
1343
|
-
subscriptionType: creds.subscriptionType ?? null,
|
|
1344
|
-
rateLimitTier: creds.rateLimitTier ?? null,
|
|
1345
|
-
organizationUuid: creds.organizationUuid ?? null,
|
|
1346
|
-
cacheExpiresAt: Math.min(tokenExp, cap),
|
|
1347
|
-
};
|
|
1348
|
-
setKeychainToken(claudeOauthCacheItem(service), JSON.stringify(entry), { noAcl: true });
|
|
1349
|
-
}
|
|
1350
|
-
catch {
|
|
1351
|
-
/* best-effort — cache is an optimization */
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
/** Evict the no-ACL cache so a source rotation or sign-out is reflected immediately. */
|
|
1282
|
+
/**
|
|
1283
|
+
* Evict any no-ACL access-token cache item so a source rotation or sign-out is
|
|
1284
|
+
* reflected immediately. The cache itself is retired — read-only probes no longer
|
|
1285
|
+
* read or write it (loadClaudeOauth returns a file-based setup-token or nothing) —
|
|
1286
|
+
* but this eviction remains so a credential rotation still clears a stale cache
|
|
1287
|
+
* item that an earlier agents-cli version may have written no-ACL.
|
|
1288
|
+
*/
|
|
1355
1289
|
function deleteCachedClaudeOauth(service) {
|
|
1356
1290
|
try {
|
|
1357
1291
|
deleteKeychainToken(claudeOauthCacheItem(service));
|
|
@@ -1377,26 +1311,27 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1377
1311
|
* (run remotely over SSH by `--host`) rendered no usage bars even though the
|
|
1378
1312
|
* account + plan — read from the plaintext `.claude.json` — showed fine.
|
|
1379
1313
|
*
|
|
1380
|
-
* `opts.accessTokenCache`
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1314
|
+
* `opts.accessTokenCache` marks a read-only, access-token-only consumer (the
|
|
1315
|
+
* usage fetch and the auth-health probe). Such a caller authenticates ONLY with
|
|
1316
|
+
* a file-based setup-token and, when none is provisioned, gets `null` — it never
|
|
1317
|
+
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
1318
|
+
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
1319
|
+
* docs/design/credential-management.md). It is OFF by default so full-credential
|
|
1320
|
+
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) or export
|
|
1321
|
+
* the full blob (`readClaudeCredentialsBlob` for Rush Cloud dispatch) still read
|
|
1322
|
+
* the interactive login.
|
|
1387
1323
|
*
|
|
1388
|
-
* `opts.fileOnly`
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1324
|
+
* `opts.fileOnly` skips the ACL keychain read entirely — setup-token and
|
|
1325
|
+
* `.credentials.json` only. Used by the daemon usage refresher so a background
|
|
1326
|
+
* tick can never pop Touch ID.
|
|
1391
1327
|
*/
|
|
1392
1328
|
export async function loadClaudeOauth(home, opts) {
|
|
1393
|
-
// Read-only usage/probe callers (accessTokenCache) authenticate with a
|
|
1394
|
-
// file-based setup-token
|
|
1395
|
-
// any sk-ant-oat01 bearer, and
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
// the fleet is fully seeded, per docs/design/credential-management.md).
|
|
1329
|
+
// Read-only usage/probe callers (accessTokenCache) authenticate ONLY with a
|
|
1330
|
+
// file-based setup-token from the `auth` bundle — never Claude Code's
|
|
1331
|
+
// interactive login. The usage endpoint accepts any sk-ant-oat01 bearer, and
|
|
1332
|
+
// the file-based token never pops Touch ID. When no setup-token is provisioned
|
|
1333
|
+
// the probe reports unprovisioned rather than reading the interactive
|
|
1334
|
+
// credential (see below) — that is the whole point of this branch.
|
|
1400
1335
|
if (opts?.accessTokenCache === true) {
|
|
1401
1336
|
const setupToken = resolveClaudeSetupToken(home);
|
|
1402
1337
|
if (setupToken) {
|
|
@@ -1406,52 +1341,36 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1406
1341
|
// the source of truth if it has actually been revoked.
|
|
1407
1342
|
return { accessToken: setupToken };
|
|
1408
1343
|
}
|
|
1344
|
+
// No provisioned setup-token: a read-only usage/health probe MUST NOT fall
|
|
1345
|
+
// through to Claude Code's interactive login credential. The daemon's usage
|
|
1346
|
+
// (~60s) and auth-health (~3min) warms would otherwise read the ACL-bound
|
|
1347
|
+
// OAuth token and transmit it to api.anthropic.com/api/oauth/usage — an
|
|
1348
|
+
// interactive credential used programmatically, which Anthropic flags and
|
|
1349
|
+
// revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
|
|
1350
|
+
// invariant that the interactive/rotating login is untouchable
|
|
1351
|
+
// (docs/design/credential-management.md). Report unprovisioned (-> probe
|
|
1352
|
+
// token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
|
|
1353
|
+
// setup-token via the mint-auth path to restore usage/probe for the account.
|
|
1354
|
+
return null;
|
|
1409
1355
|
}
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1416
|
-
// a Windows runner and the tests below it read an empty home.
|
|
1417
|
-
//
|
|
1418
|
-
// fileOnly: daemon refresh must never open the ACL-bound item (Touch ID).
|
|
1419
|
-
// It may still read the no-ACL cache item (set-no-acl — prompt-free).
|
|
1356
|
+
// Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken; Rush
|
|
1357
|
+
// Cloud dispatch) legitimately read the interactive login to run/refresh
|
|
1358
|
+
// Claude. The OS keychain/keyring step is macOS/Linux-only; Windows and any
|
|
1359
|
+
// fileOnly caller skip to the .credentials.json read below (the Claude CLI
|
|
1360
|
+
// stores its OAuth token in that file too). An injected test backend makes the
|
|
1361
|
+
// keychain path exercisable anywhere, so the platform check yields to it.
|
|
1420
1362
|
if (!opts?.fileOnly
|
|
1421
1363
|
&& (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden())) {
|
|
1422
1364
|
const service = getClaudeKeychainService(home);
|
|
1423
|
-
// Serve the no-ACL cache first (opt-in, macOS/test only) so the ACL-gated read
|
|
1424
|
-
// below — the one that pops Touch ID — happens at most once per token lifetime
|
|
1425
|
-
// instead of on every usage refresh. Off unless the caller opts in, because the
|
|
1426
|
-
// cached copy drops the refresh token (see the doc comment above).
|
|
1427
|
-
const cacheActive = opts?.accessTokenCache === true && claudeOauthCacheActive();
|
|
1428
|
-
if (cacheActive) {
|
|
1429
|
-
const cached = readCachedClaudeOauth(service);
|
|
1430
|
-
if (cached)
|
|
1431
|
-
return cached;
|
|
1432
|
-
}
|
|
1433
1365
|
try {
|
|
1434
1366
|
const fromKeychain = parseClaudeOauthPayload(getKeychainToken(service));
|
|
1435
|
-
if (fromKeychain)
|
|
1436
|
-
if (cacheActive)
|
|
1437
|
-
writeCachedClaudeOauth(service, fromKeychain);
|
|
1367
|
+
if (fromKeychain)
|
|
1438
1368
|
return fromKeychain;
|
|
1439
|
-
}
|
|
1440
1369
|
}
|
|
1441
1370
|
catch {
|
|
1442
1371
|
// No keychain item, or no reachable keyring (headless Linux) — fall through.
|
|
1443
|
-
// Evict any stale no-ACL cache so a sign-out/deletion isn't masked.
|
|
1444
|
-
if (cacheActive)
|
|
1445
|
-
deleteCachedClaudeOauth(service);
|
|
1446
1372
|
}
|
|
1447
1373
|
}
|
|
1448
|
-
else if (opts?.fileOnly === true && opts?.accessTokenCache === true && claudeOauthCacheActive()) {
|
|
1449
|
-
// fileOnly + accessTokenCache: still allow the no-ACL cache (never Touch ID).
|
|
1450
|
-
const service = getClaudeKeychainService(home);
|
|
1451
|
-
const cached = readCachedClaudeOauth(service);
|
|
1452
|
-
if (cached)
|
|
1453
|
-
return cached;
|
|
1454
|
-
}
|
|
1455
1374
|
const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
|
|
1456
1375
|
try {
|
|
1457
1376
|
if (fs.existsSync(credsPath)) {
|
|
@@ -1472,8 +1391,8 @@ export async function saveClaudeOauth(home, credentials) {
|
|
|
1472
1391
|
// Windows not yet supported. An injected test backend is the exception, for
|
|
1473
1392
|
// the same reason as the loadClaudeOauth guard above: it makes the keychain
|
|
1474
1393
|
// path exercisable anywhere, and without it this returns before the rotated
|
|
1475
|
-
// credential is written OR
|
|
1476
|
-
//
|
|
1394
|
+
// credential is written OR a stale no-ACL cache item an earlier version wrote
|
|
1395
|
+
// is evicted (deleteCachedClaudeOauth) — leaving that stale item behind.
|
|
1477
1396
|
if (process.platform !== 'darwin' && process.platform !== 'linux' && !isKeychainBackendOverridden()) {
|
|
1478
1397
|
return false;
|
|
1479
1398
|
}
|
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.30",
|
|
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",
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"postinstall": "node scripts/postinstall.js",
|
|
53
53
|
"dev": "tsx src/index.ts",
|
|
54
54
|
"changelog": "bun scripts/gen-changelog.ts",
|
|
55
|
+
"gen:index": "bun scripts/gen-command-index.ts",
|
|
56
|
+
"verify:index": "bash scripts/verify-command-index.sh",
|
|
55
57
|
"start": "node dist/index.js",
|
|
56
58
|
"test": "node ./node_modules/vitest/vitest.mjs run",
|
|
57
59
|
"bench": "node ./node_modules/vitest/vitest.mjs bench --run",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"commander": "15.0.0",
|
|
98
100
|
"croner": "10.0.1",
|
|
99
101
|
"diff": "9.0.0",
|
|
102
|
+
"lru-cache": "11.5.2",
|
|
100
103
|
"marked": "15.0.12",
|
|
101
104
|
"marked-terminal": "7.3.0",
|
|
102
105
|
"ora": "9.4.1",
|