@phnx-labs/agents-cli 1.22.4 → 1.22.5

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 (39) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/events.js +8 -0
  4. package/dist/commands/exec.js +7 -5
  5. package/dist/commands/inspect.js +18 -5
  6. package/dist/commands/models.d.ts +1 -1
  7. package/dist/commands/models.js +68 -9
  8. package/dist/commands/view.d.ts +12 -0
  9. package/dist/commands/view.js +2 -0
  10. package/dist/commands/webhook.js +8 -4
  11. package/dist/lib/browser/chrome.d.ts +12 -0
  12. package/dist/lib/browser/chrome.js +27 -11
  13. package/dist/lib/cloud/antigravity.js +8 -2
  14. package/dist/lib/crabbox/cli.js +72 -30
  15. package/dist/lib/event-stream.d.ts +3 -0
  16. package/dist/lib/event-stream.js +5 -0
  17. package/dist/lib/events.d.ts +2 -0
  18. package/dist/lib/events.js +6 -1
  19. package/dist/lib/feed.d.ts +1 -1
  20. package/dist/lib/feed.js +19 -0
  21. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  22. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  23. package/dist/lib/model-tier-overrides.d.ts +43 -0
  24. package/dist/lib/model-tier-overrides.js +97 -0
  25. package/dist/lib/model-tiers.d.ts +13 -7
  26. package/dist/lib/model-tiers.js +104 -35
  27. package/dist/lib/models.d.ts +16 -0
  28. package/dist/lib/models.js +17 -3
  29. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  30. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  31. package/dist/lib/secrets/index.d.ts +29 -0
  32. package/dist/lib/secrets/index.js +32 -0
  33. package/dist/lib/secrets/mcp.js +8 -4
  34. package/dist/lib/session/sync/config.d.ts +12 -11
  35. package/dist/lib/session/sync/config.js +40 -38
  36. package/dist/lib/share/config.js +4 -0
  37. package/dist/lib/types.d.ts +10 -0
  38. package/dist/lib/versions.js +35 -0
  39. package/package.json +1 -1
@@ -257,6 +257,25 @@ function writeHmacKeyRecord(rec) {
257
257
  setKeychainToken(HMAC_KEY_ITEM, JSON.stringify(rec), { noAcl: true });
258
258
  hashStateCache = null;
259
259
  }
260
+ /**
261
+ * Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
262
+ * migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
263
+ * keychain lookup pop the generic "Agents CLI needs to authenticate" sheet,
264
+ * because the HMAC key is read before every hashed name resolves. The migration
265
+ * fix stopped the re-stamping but never un-stamped an already-damaged item, and
266
+ * nothing else re-stores it once hashing is already active — so it prompts forever.
267
+ *
268
+ * This re-stores the record no-ACL exactly once per machine (guarded by
269
+ * `healedNoAcl`), turning every future read silent. The read that produced `rec`
270
+ * has already happened (and already prompted if it was ACL'd); this only writes.
271
+ * Returns true if it healed. Exported for tests. No-op when already healed.
272
+ */
273
+ export function healHmacKeyNoAclOnce(rec) {
274
+ if (rec.healedNoAcl)
275
+ return false;
276
+ writeHmacKeyRecord({ ...rec, healedNoAcl: true });
277
+ return true;
278
+ }
260
279
  function resolveHashState() {
261
280
  if (forcedTestKey)
262
281
  return { active: true, key: forcedTestKey, record: null };
@@ -404,6 +423,19 @@ function maybeAutoRekey() {
404
423
  return;
405
424
  const st = resolveHashState();
406
425
  if (st.active) {
426
+ // Heal an already-active machine whose hmackey was re-stamped ACL'd by an old
427
+ // helper (its read popped the generic Touch ID sheet on every hashed lookup).
428
+ // Runs once per machine; mutate the local so a later finishPendingDeletes write
429
+ // preserves the healed flag.
430
+ if (st.record && !st.record.healedNoAcl) {
431
+ try {
432
+ healHmacKeyNoAclOnce(st.record);
433
+ st.record.healedNoAcl = true;
434
+ }
435
+ catch {
436
+ /* next process retries */
437
+ }
438
+ }
407
439
  if (st.record?.pendingDeletes?.length) {
408
440
  try {
409
441
  finishPendingDeletes(st.record);
@@ -24,7 +24,7 @@
24
24
  * unit-testable in-process.
25
25
  */
26
26
  import * as readline from 'readline';
27
- import { listBundles, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, validateBundleName, } from './bundles.js';
27
+ import { listBundles, readAndResolveBundleEnv, readBundle, validateBundleName, } from './bundles.js';
28
28
  /** MCP protocol revision this server negotiates. */
29
29
  export const MCP_PROTOCOL_VERSION = '2024-11-05';
30
30
  /** The single tool this server exposes. */
@@ -68,9 +68,13 @@ export function resolveSecret(bundle, key) {
68
68
  throw new Error(`Key '${key}' not found in bundle '${bundle}'.` +
69
69
  (available.length ? ` Available keys: ${available.join(', ')}.` : ' Bundle has no keys.'));
70
70
  }
71
- // The MCP get_secret tool is typically served by a background/headless agent
72
- // process; resolve broker-only there so it never raises an unwatched prompt.
73
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
71
+ // An MCP `get_secret` tool call is a program asking for a value, never a human
72
+ // at a Touch ID sheet — so the read is always `agentOnly` (SEC-13: never pop
73
+ // biometry on its own). A `never`/no-ACL or broker-held bundle resolves
74
+ // silently; a locked bundle THROWS the actionable "unlock <name>" message,
75
+ // which propagates as the MCP tool error (the caller surfaces it) rather than
76
+ // popping an unanswerable prompt.
77
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', keys: [key], keyMode: 'storage', agentOnly: true });
74
78
  const value = env[key];
75
79
  if (value === undefined) {
76
80
  throw new Error(`Key '${key}' in bundle '${bundle}' could not be resolved.`);
@@ -29,24 +29,25 @@ export interface R2Config {
29
29
  */
30
30
  syncEncKey?: string;
31
31
  }
32
- /** Window after a prompt-bearing resolution failure during which we skip
33
- * re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
34
- export declare const RESOLVE_RETRY_COOLDOWN_MS: number;
35
32
  /** Drop the cached resolution so the next call reads the bundle fresh. Called on
36
33
  * daemon SIGHUP (to pick up rotated credentials) and between tests. */
37
34
  export declare function clearR2ConfigCache(): void;
38
35
  /**
39
36
  * Resolve R2 credentials, reading the keychain at most once per process. The
40
- * first call reads (and may prompt for Touch ID); every later call returns the
41
- * memoized result. Throws if the bundle/keys are missing — failures are not
42
- * memoized, but see isSyncConfigured for the re-prompt cooldown.
37
+ * read is `agentOnly` (resolveR2Config), so it never prompts: a `never`/no-ACL or
38
+ * broker-held bundle resolves silently and is memoized; a locked `hold`/`always`
39
+ * bundle throws the actionable "unlock r2.backups" error. Throws (not memoized)
40
+ * when the bundle/keys are missing or locked — isSyncConfigured catches the throw
41
+ * and degrades to no-transport.
43
42
  */
44
43
  export declare function loadR2Config(): R2Config;
45
44
  /**
46
- * True when the sync bundle exists and resolves, without throwing. After a
47
- * prompt-bearing failure (e.g. a cancelled Touch ID) it returns false without
48
- * re-reading the keychain for RESOLVE_RETRY_COOLDOWN_MS, so a dismissed prompt
49
- * does not re-storm every cycle. `now` is injectable for tests.
45
+ * True when the sync bundle exists and resolves, without throwing. A missing OR
46
+ * locked bundle resolves to false (session-sync degrades to no-transport) and,
47
+ * because the `agentOnly` read never prompts, it is re-checked each cycle — so a
48
+ * later `agents secrets add` / `agents secrets unlock r2.backups` is picked up
49
+ * promptly with no daemon restart. `now` is accepted for a stable test signature
50
+ * but no longer gates a cooldown (there is no prompt-bearing failure to back off).
50
51
  */
51
- export declare function isSyncConfigured(now?: number): boolean;
52
+ export declare function isSyncConfigured(_now?: number): boolean;
52
53
  export { machineId, normalizeHost } from '../../machine-id.js';
@@ -11,7 +11,7 @@
11
11
  * actually wired. Credentials come from the `r2.backups` secrets bundle (OS
12
12
  * keychain on macOS, libsecret on Linux) — never from env or disk.
13
13
  */
14
- import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../../secrets/bundles.js';
14
+ import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
15
15
  /** Secrets bundle holding the R2 credentials. */
16
16
  export const SYNC_BUNDLE = 'r2.backups';
17
17
  /**
@@ -20,13 +20,17 @@ export const SYNC_BUNDLE = 'r2.backups';
20
20
  * without real credentials (no silent fallback).
21
21
  */
22
22
  function resolveR2Config() {
23
- // A headless caller (no TTY — e.g. a routine or SSH-dispatched command) must
24
- // resolve broker-only: isHeadlessSecretsContext() true means a broker miss
25
- // can never pop an unattended Touch ID sheet on the user's screen (the same
26
- // broker-only-when-headless rationale the secrets readers use). Using the
27
- // shared predicate rather than a literal keeps it consistent with the other
28
- // callers and lets any interactive caller of loadR2Config still prompt.
29
- const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'session-transport', agentOnly: isHeadlessSecretsContext() });
23
+ // Session-sync is a BACKGROUND read: the daemon's ~90s cycle (and the ~2-min
24
+ // watchdog) resolve this on their own, never at a human's request — so it must
25
+ // NEVER pop a Touch ID sheet, on the interactive launcher included (SEC-13: an
26
+ // agent launch never raises biometry on its own). The read is always
27
+ // `agentOnly`: a `never`/no-ACL or broker-held `r2.backups` bundle resolves
28
+ // silently; a locked `hold`/`always` bundle THROWS the actionable "unlock
29
+ // r2.backups" message instead of prompting. isSyncConfigured catches that throw
30
+ // and degrades to no-transport (sync disabled) with no prompt and no crash —
31
+ // unlock once (`agents secrets unlock r2.backups`) or set it no-ACL
32
+ // (`agents secrets policy r2.backups never`) for silent zero-friction sync.
33
+ const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'session-transport', agentOnly: true });
30
34
  const accountId = env.R2_ACCOUNT_ID?.trim();
31
35
  const bucket = env.R2_BUCKET_NAME?.trim();
32
36
  const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
@@ -55,31 +59,32 @@ function resolveR2Config() {
55
59
  };
56
60
  }
57
61
  // ── Resolution cache ────────────────────────────────────────────────────────
58
- // The daemon calls isSyncConfigured() + syncSessions() every ~90s, and each used
59
- // to trigger a fresh read of the biometry-gated `r2.backups` keychain items —
60
- // one Touch ID prompt per gated item, every cycle, forever. We instead resolve
61
- // at most once per process: a success is memoized for the process lifetime
62
- // (cleared on daemon SIGHUP via clearR2ConfigCache), so subsequent cycles never
63
- // touch the keychain again. A *prompt-bearing* failure (cancelled Touch ID, etc.)
64
- // starts a cooldown so a dismissed prompt is not re-issued every cycle. A simply
65
- // absent bundle never prompts, so it is re-checked each cycle (fast pickup when
66
- // the user later adds credentials).
62
+ // The daemon calls isSyncConfigured() + syncSessions() every ~90s. The read is
63
+ // now `agentOnly` (resolveR2Config) so it can NEVER pop Touch ID — a locked bundle
64
+ // throws a cheap, deterministic "unlock r2.backups" error instead. We still resolve
65
+ // at most once per process: a success is memoized for the process lifetime (cleared
66
+ // on daemon SIGHUP via clearR2ConfigCache), so subsequent cycles never touch the
67
+ // keychain again. A failure (absent bundle, or a LOCKED `hold`/`always` bundle) is
68
+ // NOT memoized and never prompts, so it is re-checked each cycle — session-sync
69
+ // degrades to no-transport until the bundle is added / unlocked, then picks it up
70
+ // promptly with no restart.
71
+ //
72
+ // The historical prompt-backoff cooldown (a cancelled Touch ID sheet) is gone: with
73
+ // agentOnly there is no sheet to cancel, so no failure is prompt-bearing and none
74
+ // needs a backoff.
67
75
  let cachedConfig = null;
68
- let lastPromptFailureAt = 0;
69
- /** Window after a prompt-bearing resolution failure during which we skip
70
- * re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
71
- export const RESOLVE_RETRY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
72
76
  /** Drop the cached resolution so the next call reads the bundle fresh. Called on
73
77
  * daemon SIGHUP (to pick up rotated credentials) and between tests. */
74
78
  export function clearR2ConfigCache() {
75
79
  cachedConfig = null;
76
- lastPromptFailureAt = 0;
77
80
  }
78
81
  /**
79
82
  * Resolve R2 credentials, reading the keychain at most once per process. The
80
- * first call reads (and may prompt for Touch ID); every later call returns the
81
- * memoized result. Throws if the bundle/keys are missing — failures are not
82
- * memoized, but see isSyncConfigured for the re-prompt cooldown.
83
+ * read is `agentOnly` (resolveR2Config), so it never prompts: a `never`/no-ACL or
84
+ * broker-held bundle resolves silently and is memoized; a locked `hold`/`always`
85
+ * bundle throws the actionable "unlock r2.backups" error. Throws (not memoized)
86
+ * when the bundle/keys are missing or locked — isSyncConfigured catches the throw
87
+ * and degrades to no-transport.
83
88
  */
84
89
  export function loadR2Config() {
85
90
  if (cachedConfig)
@@ -88,26 +93,23 @@ export function loadR2Config() {
88
93
  return cachedConfig;
89
94
  }
90
95
  /**
91
- * True when the sync bundle exists and resolves, without throwing. After a
92
- * prompt-bearing failure (e.g. a cancelled Touch ID) it returns false without
93
- * re-reading the keychain for RESOLVE_RETRY_COOLDOWN_MS, so a dismissed prompt
94
- * does not re-storm every cycle. `now` is injectable for tests.
96
+ * True when the sync bundle exists and resolves, without throwing. A missing OR
97
+ * locked bundle resolves to false (session-sync degrades to no-transport) and,
98
+ * because the `agentOnly` read never prompts, it is re-checked each cycle — so a
99
+ * later `agents secrets add` / `agents secrets unlock r2.backups` is picked up
100
+ * promptly with no daemon restart. `now` is accepted for a stable test signature
101
+ * but no longer gates a cooldown (there is no prompt-bearing failure to back off).
95
102
  */
96
- export function isSyncConfigured(now = Date.now()) {
103
+ export function isSyncConfigured(_now = Date.now()) {
97
104
  if (cachedConfig)
98
105
  return true;
99
- if (lastPromptFailureAt && now - lastPromptFailureAt < RESOLVE_RETRY_COOLDOWN_MS)
100
- return false;
101
106
  try {
102
107
  loadR2Config();
103
108
  return true;
104
109
  }
105
- catch (err) {
106
- // A missing bundle never prompts, so keep re-checking it each cycle (so a
107
- // later `agents secrets add` is picked up quickly). Any other failure may
108
- // have cost a prompt (cancelled Touch ID, keychain error) — back off.
109
- if (!/not found/i.test(err.message))
110
- lastPromptFailureAt = now;
110
+ catch {
111
+ // Absent or locked bundle — never prompted (agentOnly), so no backoff: keep
112
+ // re-checking each cycle for fast pickup once the bundle is added / unlocked.
111
113
  return false;
112
114
  }
113
115
  }
@@ -76,6 +76,9 @@ export function storeWriteToken(token) {
76
76
  export function readWriteTokenFromBundle() {
77
77
  const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
78
78
  caller: 'share',
79
+ // Explicit `agents share` command (a human published a file): a headless agent
80
+ // subprocess resolves broker-only, an interactive human may unlock. This is NOT
81
+ // an agent LAUNCH read (that is exec.ts's --secrets injection, always agentOnly).
79
82
  agentOnly: isHeadlessSecretsContext(),
80
83
  });
81
84
  const token = env[SHARE_TOKEN_KEY];
@@ -133,6 +136,7 @@ export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
133
136
  }
134
137
  const { env } = readAndResolveBundleEnv(bundle, {
135
138
  caller: 'share',
139
+ // Explicit `agents share setup` provisioning read — not an agent launch.
136
140
  agentOnly: isHeadlessSecretsContext(),
137
141
  });
138
142
  const find = (re) => {
@@ -764,6 +764,16 @@ export interface Meta {
764
764
  */
765
765
  isolatedAgents?: Partial<Record<AgentId, string>>;
766
766
  run?: RunConfig;
767
+ /**
768
+ * Cost-tier overrides for `--model cheap|default|best|ultra`. Keyed by the same
769
+ * `<agent>:<version>` selector run.defaults uses (`kimi:*`, `kimi:0.19.2`); each
770
+ * value maps a tier to a concrete model id. Written by `agents models tier set`,
771
+ * never hand-edited. Resolution: exact version selector wins over `<agent>:*`,
772
+ * which wins over the auto-ranking. See lib/model-tier-overrides.ts.
773
+ */
774
+ model?: {
775
+ tiers?: Record<string, Partial<Record<'cheap' | 'default' | 'best' | 'ultra', string>>>;
776
+ };
767
777
  /**
768
778
  * Daemon watchdog config. `rotate` (default `on`) lets the watchdog rotate a
769
779
  * rate-limited session IN PLACE onto a healthy account/harness via
@@ -1358,6 +1358,38 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
1358
1358
  return;
1359
1359
  }
1360
1360
  }
1361
+ /**
1362
+ * Grok's version directory is keyed by release number alone, not by account —
1363
+ * so two DIFFERENT accounts that both self-update to the identical upstream
1364
+ * release ("latest") target the SAME on-disk `versions/grok/<version>/` home.
1365
+ * When that happens, the second account's `agents add grok@latest` writes
1366
+ * into an already-signed-in directory: grok's own auth flow treats
1367
+ * `~/.grok/auth.json` as authoritative for whichever process invoked it, so
1368
+ * the second account's credential record lands in a file that still names it
1369
+ * as the first account's install everywhere else in agents-cli's bookkeeping.
1370
+ *
1371
+ * Refuse before that happens: if the target version's home already has a
1372
+ * signed-in account whose identity differs from the account currently
1373
+ * driving this update (the previous global default), fail loud instead of
1374
+ * letting the second account silently displace the first's install.
1375
+ */
1376
+ async function checkGrokAccountCollision(installedVersion) {
1377
+ const targetHome = getVersionHomePath('grok', installedVersion);
1378
+ if (!fs.existsSync(targetHome))
1379
+ return; // fresh directory, nothing to collide with
1380
+ const sourceVersion = getGlobalDefault('grok');
1381
+ if (!sourceVersion || sourceVersion === installedVersion)
1382
+ return; // same install, not a collision
1383
+ const [targetEmail, sourceEmail] = await Promise.all([
1384
+ getAccountEmail('grok', targetHome),
1385
+ getAccountEmail('grok', getVersionHomePath('grok', sourceVersion)),
1386
+ ]);
1387
+ if (!targetEmail || !sourceEmail || targetEmail === sourceEmail)
1388
+ return;
1389
+ throw new Error(`grok@${installedVersion} is already installed for ${targetEmail}, but this update is running as ${sourceEmail}. ` +
1390
+ `Grok's self-updater can't distinguish two accounts that land on the same release — sign in to ${targetEmail}'s ` +
1391
+ `install (agents use grok@${installedVersion}) before updating it, or wait until the releases diverge.`);
1392
+ }
1361
1393
  /**
1362
1394
  * Install a specific version of an agent.
1363
1395
  */
@@ -1428,6 +1460,9 @@ export async function installVersion(agent, version, onProgress, opts) {
1428
1460
  // install into the real version so it stops shadowing `agents view`.
1429
1461
  await reconcileStaleLatestDir(agent, installedVersion);
1430
1462
  }
1463
+ if (agent === 'grok') {
1464
+ await checkGrokAccountCollision(installedVersion);
1465
+ }
1431
1466
  onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
1432
1467
  }
1433
1468
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.4",
3
+ "version": "1.22.5",
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",