@phnx-labs/agents-cli 1.19.0 → 1.19.2

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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.js +0 -0
  3. package/dist/commands/exec.js +1 -1
  4. package/dist/commands/mcp.js +29 -0
  5. package/dist/commands/secrets.js +4 -4
  6. package/dist/commands/sessions.d.ts +8 -7
  7. package/dist/commands/sessions.js +32 -20
  8. package/dist/commands/versions.js +21 -2
  9. package/dist/computer.js +0 -0
  10. package/dist/index.js +0 -0
  11. package/dist/lib/agents.js +66 -0
  12. package/dist/lib/browser/chrome.js +1 -1
  13. package/dist/lib/exec.js +21 -0
  14. package/dist/lib/registry.d.ts +18 -0
  15. package/dist/lib/registry.js +44 -0
  16. package/dist/lib/resources/mcp.js +6 -1
  17. package/dist/lib/resources/types.d.ts +1 -1
  18. package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
  19. package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
  20. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  21. package/dist/lib/secrets/bundles.d.ts +11 -1
  22. package/dist/lib/secrets/bundles.js +37 -12
  23. package/dist/lib/secrets/index.d.ts +15 -1
  24. package/dist/lib/secrets/index.js +101 -26
  25. package/dist/lib/session/db.d.ts +10 -0
  26. package/dist/lib/session/db.js +26 -0
  27. package/dist/lib/shims.d.ts +5 -1
  28. package/dist/lib/shims.js +22 -6
  29. package/dist/lib/types.d.ts +1 -1
  30. package/npm-shrinkwrap.json +890 -984
  31. package/package.json +5 -5
  32. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  33. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
  34. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
package/README.md CHANGED
@@ -414,7 +414,7 @@ agents secrets list # npm-tokens is already there;
414
414
  agents run claude "..." --secrets npm-tokens # injects NPM_TOKEN automatically
415
415
  ```
416
416
 
417
- Under the hood, synced bundles route writes through a notarized helper app (`AgentsKeychain.app`) that holds the entitlement macOS requires for `kSecAttrSynchronizable`. Bundles created with `--no-icloud-sync` stay device-local.
417
+ Under the hood, synced bundles route writes through a notarized helper app (`Agents CLI.app`) that holds the entitlement macOS requires for `kSecAttrSynchronizable`. Bundles created with `--no-icloud-sync` stay device-local.
418
418
 
419
419
  Bundle definitions sync via iCloud Keychain too — no `agents repo push` needed for secrets, no recreate step on each Mac. Nothing about secrets ever lives in plaintext on disk.
420
420
 
package/dist/browser.js CHANGED
File without changes
@@ -276,7 +276,7 @@ export function registerRunCommand(program) {
276
276
  }
277
277
  const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
278
278
  console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
279
- secretsEnv = { ...secretsEnv, ...resolveBundleEnv(bundle) };
279
+ secretsEnv = { ...secretsEnv, ...resolveBundleEnv(bundle, { caller: `agent ${agent}` }) };
280
280
  }
281
281
  catch (err) {
282
282
  console.error(chalk.red(err.message));
@@ -156,11 +156,40 @@ Examples:
156
156
  agents mcp add db-server -- uvx postgres-mcp
157
157
  `)
158
158
  .action(async (name, commandOrUrl, options) => {
159
+ // Registry resolution: if the user just typed `agents mcp add <name>`,
160
+ // try looking up `<name>` in any configured MCP registry (by default the
161
+ // official MCP Registry at registry.modelcontextprotocol.io) and derive
162
+ // the install spec automatically.
163
+ if (commandOrUrl.length === 0) {
164
+ const { getMcpServerInfo, mcpEntryToInstallSpec } = await import('../lib/registry.js');
165
+ const spinner = ora(`Looking up '${name}' in MCP registries…`).start();
166
+ try {
167
+ const entry = await getMcpServerInfo(name);
168
+ if (entry) {
169
+ const spec = mcpEntryToInstallSpec(entry);
170
+ if (spec?.command) {
171
+ commandOrUrl = spec.command.split(' ');
172
+ options.transport = spec.transport;
173
+ spinner.succeed(`Resolved '${name}' → ${chalk.gray(spec.command)}`);
174
+ }
175
+ else {
176
+ spinner.warn(`Found '${name}' in registry but could not derive an install command (likely a remote-only server).`);
177
+ }
178
+ }
179
+ else {
180
+ spinner.fail(`'${name}' not found in any configured MCP registry.`);
181
+ }
182
+ }
183
+ catch (err) {
184
+ spinner.fail(`Registry lookup failed: ${err.message}`);
185
+ }
186
+ }
159
187
  const transport = options.transport;
160
188
  if (commandOrUrl.length === 0) {
161
189
  console.error(chalk.red('Error: Command or URL required'));
162
190
  console.log(chalk.gray('Stdio: agents mcp add <name> -- <command...>'));
163
191
  console.log(chalk.gray('HTTP: agents mcp add <name> <url> --transport http'));
192
+ console.log(chalk.gray("Or list what's discoverable: agents mcp list --available"));
164
193
  process.exit(1);
165
194
  }
166
195
  const localPath = getUserAgentsDir();
@@ -879,7 +879,7 @@ Examples:
879
879
  if (opts.to1password) {
880
880
  assertOpAvailable();
881
881
  const vault = await resolveVault(opts.vault);
882
- const env = resolveBundleEnv(bundle);
882
+ const env = resolveBundleEnv(bundle, { caller: `1Password vault ${vault}` });
883
883
  let created = 0;
884
884
  let overwritten = 0;
885
885
  let skipped = 0;
@@ -913,7 +913,7 @@ Examples:
913
913
  console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
914
914
  process.exit(1);
915
915
  }
916
- const env = resolveBundleEnv(bundle);
916
+ const env = resolveBundleEnv(bundle, { caller: `export to shell` });
917
917
  const prefix = bundleToEnvPrefix(resolvedBundleName);
918
918
  for (const [k, v] of Object.entries(env)) {
919
919
  const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
@@ -941,9 +941,9 @@ Examples:
941
941
  }
942
942
  const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
943
943
  const bundle = readBundle(bundleName);
944
- const secretEnv = resolveBundleEnv(bundle);
945
- const { spawn } = await import('child_process');
946
944
  const [cmd, ...args] = commandParts;
945
+ const secretEnv = resolveBundleEnv(bundle, { caller: `command ${cmd}` });
946
+ const { spawn } = await import('child_process');
947
947
  const proc = spawn(cmd, args, {
948
948
  stdio: 'inherit',
949
949
  env: { ...process.env, ...secretEnv },
@@ -3,14 +3,15 @@ import type { SessionMeta } from '../lib/session/types.js';
3
3
  /**
4
4
  * Build the shell command that resumes a picked session.
5
5
  *
6
- * Cross-version handoff: when the session was created on a different version
7
- * than the one the shim will launch (activeVersion), the session file lives in
8
- * the other version's isolated home and `--resume <id>` would silently fail to
9
- * find it. Fall back to a fresh session seeded with `/continue <id>`, which is
10
- * wired (~/.claude/commands/continue.md) to read the prior transcript via
11
- * `agents sessions <id>` that reader is version-agnostic.
6
+ * When the session's originating version is known, uses the version-pinned
7
+ * binary (e.g. `claude@2.1.138`) so the resume always runs in the same
8
+ * isolated HOME where the JSONL was written regardless of which version is
9
+ * currently the default. Falls back to the bare shim when version is unknown.
10
+ *
11
+ * If the versioned binary is missing (version was removed), the ENOENT
12
+ * handler in handlePickedSession retries via buildFallbackCommand.
12
13
  */
13
- export declare function buildResumeCommand(session: SessionMeta, activeVersion?: string): string[] | null;
14
+ export declare function buildResumeCommand(session: SessionMeta): string[] | null;
14
15
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
15
16
  export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
16
17
  /** Register the `agents sessions` command with all its options and help text. */
@@ -22,7 +22,7 @@ import { parseSession } from '../lib/session/parse.js';
22
22
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
23
23
  import { renderMarkdown } from '../lib/markdown.js';
24
24
  import { colorAgent, resolveAgentName } from '../lib/agents.js';
25
- import { resolveVersion, resolveVersionAliasLoose } from '../lib/versions.js';
25
+ import { resolveVersionAliasLoose } from '../lib/versions.js';
26
26
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
27
27
  import { sessionPicker } from './sessions-picker.js';
28
28
  import { setHelpSections } from '../lib/help.js';
@@ -613,18 +613,12 @@ async function handlePickedSession(picked) {
613
613
  const cwd = picked.session.cwd && fs.existsSync(picked.session.cwd)
614
614
  ? picked.session.cwd
615
615
  : process.cwd();
616
- const activeVersion = resolveVersion(picked.session.agent, cwd) ?? undefined;
617
- const resume = buildResumeCommand(picked.session, activeVersion);
616
+ const resume = buildResumeCommand(picked.session);
618
617
  if (!resume) {
619
618
  console.log(chalk.yellow(`Resume is not supported for ${picked.session.agent} sessions yet. Showing summary instead.`));
620
619
  await renderSession(picked.session, 'summary', {});
621
620
  return;
622
621
  }
623
- if (picked.session.version && activeVersion && picked.session.version !== activeVersion) {
624
- console.log(chalk.gray(`Cross-version handoff: session is ${picked.session.agent} ${picked.session.version}, ` +
625
- `default is ${activeVersion}. Starting fresh and passing /continue so the new agent ` +
626
- `reads the prior transcript via 'agents sessions'.`));
627
- }
628
622
  console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
629
623
  await new Promise((resolve) => {
630
624
  const child = spawn(resume[0], resume.slice(1), {
@@ -633,6 +627,16 @@ async function handlePickedSession(picked) {
633
627
  shell: false,
634
628
  });
635
629
  child.on('error', (err) => {
630
+ if (err.code === 'ENOENT' && picked.session.version) {
631
+ const fallback = buildFallbackCommand(picked.session);
632
+ if (fallback) {
633
+ console.log(chalk.gray(`Version ${picked.session.version} is not installed. Falling back to current version via /continue...`));
634
+ const fb = spawn(fallback[0], fallback.slice(1), { cwd, stdio: 'inherit', shell: false });
635
+ fb.on('error', (e) => { console.error(chalk.red(`Failed: ${e.message}`)); resolve(); });
636
+ fb.on('close', () => resolve());
637
+ return;
638
+ }
639
+ }
636
640
  console.error(chalk.red(`Failed to launch ${resume[0]}: ${err.message}`));
637
641
  if (err.code === 'ENOENT') {
638
642
  console.error(chalk.gray(`Make sure '${resume[0]}' is on your PATH.`));
@@ -645,23 +649,23 @@ async function handlePickedSession(picked) {
645
649
  /**
646
650
  * Build the shell command that resumes a picked session.
647
651
  *
648
- * Cross-version handoff: when the session was created on a different version
649
- * than the one the shim will launch (activeVersion), the session file lives in
650
- * the other version's isolated home and `--resume <id>` would silently fail to
651
- * find it. Fall back to a fresh session seeded with `/continue <id>`, which is
652
- * wired (~/.claude/commands/continue.md) to read the prior transcript via
653
- * `agents sessions <id>` that reader is version-agnostic.
652
+ * When the session's originating version is known, uses the version-pinned
653
+ * binary (e.g. `claude@2.1.138`) so the resume always runs in the same
654
+ * isolated HOME where the JSONL was written regardless of which version is
655
+ * currently the default. Falls back to the bare shim when version is unknown.
656
+ *
657
+ * If the versioned binary is missing (version was removed), the ENOENT
658
+ * handler in handlePickedSession retries via buildFallbackCommand.
654
659
  */
655
- export function buildResumeCommand(session, activeVersion) {
656
- const versionMismatch = !!(session.version && activeVersion && session.version !== activeVersion);
660
+ export function buildResumeCommand(session) {
657
661
  switch (session.agent) {
658
662
  case 'claude':
659
- if (versionMismatch)
660
- return ['claude', `/continue ${session.id}`];
663
+ if (session.version)
664
+ return [`claude@${session.version}`, '--resume', session.id];
661
665
  return ['claude', '--resume', session.id];
662
666
  case 'codex':
663
- if (versionMismatch)
664
- return ['codex', `/continue ${session.id}`];
667
+ if (session.version)
668
+ return [`codex@${session.version}`, 'resume', session.id];
665
669
  return ['codex', 'resume', session.id];
666
670
  case 'opencode':
667
671
  return ['opencode', '--session', session.id];
@@ -673,6 +677,14 @@ export function buildResumeCommand(session, activeVersion) {
673
677
  return null;
674
678
  }
675
679
  }
680
+ /** Fallback resume command when the versioned binary is unavailable (ENOENT). */
681
+ function buildFallbackCommand(session) {
682
+ switch (session.agent) {
683
+ case 'claude': return ['claude', `/continue ${session.id}`];
684
+ case 'codex': return ['codex', `/continue ${session.id}`];
685
+ default: return null;
686
+ }
687
+ }
676
688
  // ---------------------------------------------------------------------------
677
689
  // Cloud session source (--cloud)
678
690
  // ---------------------------------------------------------------------------
@@ -7,12 +7,27 @@ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } f
7
7
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
8
8
  import { viewAction } from './view.js';
9
9
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
10
- import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, } from '../lib/versions.js';
10
+ import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, } from '../lib/versions.js';
11
11
  import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
12
12
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
13
13
  import { tryAutoPull } from '../lib/git.js';
14
- import { getAgentsDir } from '../lib/state.js';
14
+ import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
15
15
  import { setHelpSections } from '../lib/help.js';
16
+ import { updateSessionFilePaths } from '../lib/session/db.js';
17
+ /**
18
+ * After removeVersion soft-deletes a version dir to trash, rewrite session
19
+ * file_path entries in the DB so reads still work from the new trash location.
20
+ */
21
+ function fixSessionFilePaths(agent, version, oldVersionDir) {
22
+ const trashAgentDir = path.join(getTrashVersionsDir(), agent, version);
23
+ if (!fs.existsSync(trashAgentDir))
24
+ return;
25
+ const stamps = fs.readdirSync(trashAgentDir).sort().reverse();
26
+ if (stamps.length === 0)
27
+ return;
28
+ const trashPath = path.join(trashAgentDir, stamps[0]);
29
+ updateSessionFilePaths(oldVersionDir, trashPath);
30
+ }
16
31
  /**
17
32
  * Helper to get actual installed version for an agent.
18
33
  * Returns the latest installed version, or throws if none installed.
@@ -362,7 +377,9 @@ export function registerVersionsCommands(program) {
362
377
  continue;
363
378
  }
364
379
  for (const v of toRemove) {
380
+ const versionDir = getVersionDir(agent, v);
365
381
  removeVersion(agent, v);
382
+ fixSessionFilePaths(agent, v, versionDir);
366
383
  console.log(chalk.green(`Removed ${agentLabel(agentConfig.id)}@${v}`));
367
384
  }
368
385
  // Check if default was removed
@@ -390,7 +407,9 @@ export function registerVersionsCommands(program) {
390
407
  console.log(chalk.gray(`${agentLabel(agentConfig.id)}@${version} not installed`));
391
408
  }
392
409
  else {
410
+ const versionDir = getVersionDir(agent, version);
393
411
  removeVersion(agent, version);
412
+ fixSessionFilePaths(agent, version, versionDir);
394
413
  console.log(chalk.green(`Removed ${agentLabel(agentConfig.id)}@${version}`));
395
414
  // Remove shim if no versions left
396
415
  const remaining = listInstalledVersions(agent);
package/dist/computer.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -335,6 +335,50 @@ export const AGENTS = {
335
335
  supportsHooks: false,
336
336
  capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false },
337
337
  },
338
+ // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
339
+ // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
340
+ // CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
341
+ // isolation works because the shim's configDirName carries the full nested path.
342
+ antigravity: {
343
+ id: 'antigravity',
344
+ name: 'Antigravity',
345
+ color: 'blueBright',
346
+ cliCommand: 'agy',
347
+ npmPackage: '',
348
+ installScript: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
349
+ configDir: path.join(HOME, '.gemini', 'antigravity-cli'),
350
+ commandsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'commands'),
351
+ commandsSubdir: 'commands',
352
+ skillsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'skills'),
353
+ hooksDir: 'hooks',
354
+ instructionsFile: 'AGENTS.md',
355
+ format: 'markdown',
356
+ variableSyntax: '{{args}}',
357
+ supportsHooks: false,
358
+ nativeAgentsSkillsDir: true,
359
+ capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, rulesImports: false },
360
+ },
361
+ // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
362
+ // first launch, or GROK_CODE_XAI_API_KEY env var for headless. MCP supported
363
+ // out of the box; exact config file path verified at first install.
364
+ grok: {
365
+ id: 'grok',
366
+ name: 'Grok',
367
+ color: 'whiteBright',
368
+ cliCommand: 'grok',
369
+ npmPackage: '',
370
+ installScript: 'curl -fsSL https://x.ai/cli/install.sh | bash',
371
+ configDir: path.join(HOME, '.grok'),
372
+ commandsDir: path.join(HOME, '.grok', 'commands'),
373
+ commandsSubdir: 'commands',
374
+ skillsDir: path.join(HOME, '.grok', 'skills'),
375
+ hooksDir: 'hooks',
376
+ instructionsFile: 'AGENTS.md',
377
+ format: 'markdown',
378
+ variableSyntax: '$ARGUMENTS',
379
+ supportsHooks: false,
380
+ capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false },
381
+ },
338
382
  };
339
383
  /** All registered agent IDs derived from the AGENTS registry. */
340
384
  export const ALL_AGENT_IDS = Object.keys(AGENTS);
@@ -1084,6 +1128,12 @@ function getUserMcpConfigPath(agentId) {
1084
1128
  case 'openclaw':
1085
1129
  // OpenClaw uses openclaw.json
1086
1130
  return path.join(agent.configDir, 'openclaw.json');
1131
+ case 'antigravity':
1132
+ // agy uses mcp_config.json inside its nested config dir (~/.gemini/antigravity-cli/)
1133
+ return path.join(agent.configDir, 'mcp_config.json');
1134
+ case 'grok':
1135
+ // grok mcp.json — exact field schema verified at first install
1136
+ return path.join(agent.configDir, 'mcp.json');
1087
1137
  default:
1088
1138
  // Gemini and others use settings.json
1089
1139
  return path.join(agent.configDir, 'settings.json');
@@ -1114,6 +1164,10 @@ export function getMcpConfigPathForHome(agentId, home) {
1114
1164
  return path.join(home, '.config', 'goose', 'config.yaml');
1115
1165
  case 'roo':
1116
1166
  return path.join(home, '.roo', 'mcp.json');
1167
+ case 'antigravity':
1168
+ return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
1169
+ case 'grok':
1170
+ return path.join(home, '.grok', 'mcp.json');
1117
1171
  default:
1118
1172
  return path.join(home, `.${agentId}`, 'settings.json');
1119
1173
  }
@@ -1146,6 +1200,10 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
1146
1200
  return path.join(cwd, '.goose', 'config.yaml');
1147
1201
  case 'roo':
1148
1202
  return path.join(cwd, '.roo', 'mcp.json');
1203
+ case 'antigravity':
1204
+ return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
1205
+ case 'grok':
1206
+ return path.join(cwd, '.grok', 'mcp.json');
1149
1207
  default:
1150
1208
  return path.join(cwd, `.${agentId}`, 'settings.json');
1151
1209
  }
@@ -1275,6 +1333,14 @@ export const AGENT_NAME_ALIASES = {
1275
1333
  roo: 'roo',
1276
1334
  'roo-code': 'roo',
1277
1335
  roocode: 'roo',
1336
+ antigravity: 'antigravity',
1337
+ 'google-antigravity': 'antigravity',
1338
+ agy: 'antigravity',
1339
+ ag: 'antigravity',
1340
+ grok: 'grok',
1341
+ 'grok-build': 'grok',
1342
+ 'xai-grok': 'grok',
1343
+ gk: 'grok',
1278
1344
  };
1279
1345
  /** Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId. */
1280
1346
  export function resolveAgentName(input) {
@@ -110,7 +110,7 @@ isElectron = false) {
110
110
  if (secrets && bundleExists(secrets)) {
111
111
  try {
112
112
  const bundle = readBundle(secrets);
113
- const bundleEnv = resolveBundleEnv(bundle);
113
+ const bundleEnv = resolveBundleEnv(bundle, { caller: 'browser profile' });
114
114
  env = { ...env, ...bundleEnv };
115
115
  }
116
116
  catch {
package/dist/lib/exec.js CHANGED
@@ -197,6 +197,27 @@ export const AGENT_COMMANDS = {
197
197
  },
198
198
  modelFlag: '--model',
199
199
  },
200
+ antigravity: {
201
+ base: ['agy'],
202
+ promptFlag: 'positional',
203
+ modeFlags: {
204
+ plan: [],
205
+ edit: [],
206
+ full: [],
207
+ },
208
+ modelFlag: '--model',
209
+ },
210
+ grok: {
211
+ base: ['grok'],
212
+ promptFlag: '-p',
213
+ modeFlags: {
214
+ plan: [],
215
+ edit: [],
216
+ full: [],
217
+ },
218
+ jsonFlags: ['--output-format', 'streaming-json'],
219
+ modelFlag: '--model',
220
+ },
200
221
  };
201
222
  /** Assemble the full CLI argument array for an agent invocation. */
202
223
  export function buildExecCommand(options) {
@@ -24,6 +24,24 @@ export declare function searchMcpRegistries(query: string, options?: {
24
24
  registry?: string;
25
25
  limit?: number;
26
26
  }): Promise<RegistrySearchResult[]>;
27
+ /**
28
+ * Convert an MCP server registry entry into an install spec suitable for
29
+ * writing into `manifest.mcp`. Returns `null` if the entry has no package we
30
+ * know how to launch (e.g. only remote endpoints, which the current manifest
31
+ * shape supports via `url`+`transport: 'http'` but isn't yet wired to the
32
+ * registry's `remotes` field).
33
+ *
34
+ * Supported package shapes:
35
+ * - npm / runtime=node → `npx -y <name>`
36
+ * - pypi / runtime=python → `uvx <name>`
37
+ * - runtime=docker → `docker run --rm -i <name>`
38
+ * - runtime=binary → `<name>` (assumed to be on PATH)
39
+ */
40
+ export declare function mcpEntryToInstallSpec(entry: McpServerEntry): {
41
+ command?: string;
42
+ url?: string;
43
+ transport: 'stdio' | 'http';
44
+ } | null;
27
45
  /** Look up detailed info for an MCP server by exact name. */
28
46
  export declare function getMcpServerInfo(serverName: string, registryName?: string): Promise<McpServerEntry | null>;
29
47
  /** Search skill registries for entries matching a query string. */
@@ -113,6 +113,50 @@ export async function searchMcpRegistries(query, options) {
113
113
  }
114
114
  return results;
115
115
  }
116
+ /**
117
+ * Convert an MCP server registry entry into an install spec suitable for
118
+ * writing into `manifest.mcp`. Returns `null` if the entry has no package we
119
+ * know how to launch (e.g. only remote endpoints, which the current manifest
120
+ * shape supports via `url`+`transport: 'http'` but isn't yet wired to the
121
+ * registry's `remotes` field).
122
+ *
123
+ * Supported package shapes:
124
+ * - npm / runtime=node → `npx -y <name>`
125
+ * - pypi / runtime=python → `uvx <name>`
126
+ * - runtime=docker → `docker run --rm -i <name>`
127
+ * - runtime=binary → `<name>` (assumed to be on PATH)
128
+ */
129
+ export function mcpEntryToInstallSpec(entry) {
130
+ const pkg = entry.packages?.[0];
131
+ if (!pkg)
132
+ return null;
133
+ // Remote transports (sse / streamable-http) need a URL the registry doesn't
134
+ // currently expose in this client's type. Skip for now; caller can fall back
135
+ // to manual --transport http with an explicit URL.
136
+ if (pkg.transport === 'sse' || pkg.transport === 'streamable-http') {
137
+ return null;
138
+ }
139
+ const reg = pkg.registry_name?.toLowerCase();
140
+ const runtime = pkg.runtime;
141
+ const name = pkg.name;
142
+ if (!name)
143
+ return null;
144
+ if (reg === 'npm' || runtime === 'node') {
145
+ return { command: `npx -y ${name}`, transport: 'stdio' };
146
+ }
147
+ if (reg === 'pypi' || runtime === 'python') {
148
+ return { command: `uvx ${name}`, transport: 'stdio' };
149
+ }
150
+ if (runtime === 'docker') {
151
+ return { command: `docker run --rm -i ${name}`, transport: 'stdio' };
152
+ }
153
+ if (runtime === 'binary') {
154
+ return { command: name, transport: 'stdio' };
155
+ }
156
+ // Unknown registry/runtime — fall back to bare name so the user gets *something*
157
+ // to inspect via `agents mcp view`, rather than a silent miss.
158
+ return { command: name, transport: 'stdio' };
159
+ }
116
160
  /** Look up detailed info for an MCP server by exact name. */
117
161
  export async function getMcpServerInfo(serverName, registryName) {
118
162
  const registries = getEnabledRegistries('mcp');
@@ -15,7 +15,7 @@ import * as yaml from 'yaml';
15
15
  import * as TOML from 'smol-toml';
16
16
  import { getSystemMcpDir, getUserMcpDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
17
17
  /** Agents from resources/types.ts that support MCP. */
18
- const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw'];
18
+ const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw', 'antigravity', 'grok'];
19
19
  /**
20
20
  * Parse an MCP YAML file into an McpItem.
21
21
  */
@@ -119,6 +119,11 @@ export function getMcpConfigPath(agent, versionHome) {
119
119
  return path.join(versionHome, '.gemini', 'settings.json');
120
120
  case 'openclaw':
121
121
  return path.join(versionHome, '.openclaw', 'openclaw.json');
122
+ case 'antigravity':
123
+ // agy nests under ~/.gemini/antigravity-cli/ (shared parent with Gemini, distinct subdir).
124
+ return path.join(versionHome, '.gemini', 'antigravity-cli', 'mcp_config.json');
125
+ case 'grok':
126
+ return path.join(versionHome, '.grok', 'mcp.json');
122
127
  default:
123
128
  return null;
124
129
  }
@@ -5,7 +5,7 @@
5
5
  * - Union: All resources from all layers are combined
6
6
  * - Override on name conflict: Higher layer wins (project > user > system)
7
7
  */
8
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'antigravity' | 'grok';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
10
  export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
11
11
  /** A resolved resource with its origin layer. */
@@ -5,9 +5,11 @@
5
5
  <key>CFBundleIdentifier</key>
6
6
  <string>com.phnx-labs.agents-keychain</string>
7
7
  <key>CFBundleName</key>
8
- <string>AgentsKeychain</string>
8
+ <string>Agents CLI</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Agents CLI</string>
9
11
  <key>CFBundleExecutable</key>
10
- <string>AgentsKeychain</string>
12
+ <string>Agents CLI</string>
11
13
  <key>CFBundlePackageType</key>
12
14
  <string>APPL</string>
13
15
  <key>CFBundleVersion</key>
@@ -71,7 +71,17 @@ export interface BundleEntryInfo {
71
71
  detail: string;
72
72
  }
73
73
  export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[];
74
- export declare function resolveBundleEnv(bundle: SecretsBundle): Record<string, string>;
74
+ /** Options for resolveBundleEnv. */
75
+ export interface ResolveBundleOptions {
76
+ /**
77
+ * Human-readable label for who is requesting the secrets. Shown to the
78
+ * user under the Touch ID prompt so they know what's about to read.
79
+ * Example: "agent claude", "command curl", "browser profile".
80
+ * When omitted the prompt only names the bundle.
81
+ */
82
+ caller?: string;
83
+ }
84
+ export declare function resolveBundleEnv(bundle: SecretsBundle, opts?: ResolveBundleOptions): Record<string, string>;
75
85
  export declare function keychainRef(key: string): string;
76
86
  /** Options for rotateBundleSecret. */
77
87
  export interface RotateOptions {
@@ -15,7 +15,7 @@ import * as fs from 'fs';
15
15
  import * as os from 'os';
16
16
  import * as path from 'path';
17
17
  import * as yaml from 'yaml';
18
- import { deleteKeychainToken, getKeychainToken, hasKeychainToken, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
18
+ import { deleteKeychainToken, getKeychainToken, getKeychainTokensBatch, hasKeychainToken, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
19
19
  import { emit } from '../events.js';
20
20
  /** Allowed values for a secret's `type` metadata field. */
21
21
  export const SECRET_TYPES = [
@@ -253,19 +253,48 @@ function stampLastUsed(bundle) {
253
253
  // Swallow — telemetry must never block secret resolution.
254
254
  }
255
255
  }
256
- // Walk the bundle and produce a flat env map. Keychain refs are translated via
257
- // the bundle-scoped naming scheme so two bundles with the same short ID never
258
- // collide. Throws on the first missing secret so `agents run` fails loudly
259
- // rather than silently injecting empty strings.
260
- export function resolveBundleEnv(bundle) {
256
+ export function resolveBundleEnv(bundle, opts = {}) {
261
257
  stampLastUsed(bundle);
262
- const env = {};
258
+ const parsedByKey = new Map();
259
+ const keychainItemsToFetch = [];
260
+ const keychainItemToKey = new Map();
263
261
  for (const [key, raw] of Object.entries(bundle.vars)) {
264
262
  const parsed = parseBundleValue(raw);
263
+ parsedByKey.set(key, parsed);
264
+ if ('ref' in parsed && parsed.ref.provider === 'keychain') {
265
+ const item = secretsKeychainItem(bundle.name, parsed.ref.value);
266
+ keychainItemsToFetch.push(item);
267
+ keychainItemToKey.set(item, key);
268
+ }
269
+ }
270
+ // Build the localizedReason shown under the Touch ID prompt. Lowercase verb
271
+ // phrase per Apple HIG — the system prepends "<App> is required to ".
272
+ const reason = opts.caller
273
+ ? `read ${bundle.name} secrets (for ${opts.caller})`
274
+ : `read ${bundle.name} secrets`;
275
+ // Single helper invocation, one biometric prompt.
276
+ const fetched = keychainItemsToFetch.length > 0
277
+ ? getKeychainTokensBatch(keychainItemsToFetch, bundle.icloud_sync, reason)
278
+ : new Map();
279
+ // Second pass: assemble env. Keychain values come from the batch; everything
280
+ // else is resolved inline (literals and env/file/exec refs don't prompt).
281
+ const env = {};
282
+ for (const [key, raw] of Object.entries(bundle.vars)) {
283
+ const parsed = parsedByKey.get(key);
265
284
  if ('literal' in parsed) {
266
285
  env[key] = parsed.literal;
267
286
  continue;
268
287
  }
288
+ if (parsed.ref.provider === 'keychain') {
289
+ const item = secretsKeychainItem(bundle.name, parsed.ref.value);
290
+ const value = fetched.get(item);
291
+ if (value === undefined) {
292
+ throw new Error(`Bundle '${bundle.name}' key '${key}': keychain item '${item}' not found. ` +
293
+ `Run: agents secrets add ${bundle.name} ${key}`);
294
+ }
295
+ env[key] = value;
296
+ continue;
297
+ }
269
298
  try {
270
299
  env[key] = resolveRef(parsed.ref, {
271
300
  allowExec: bundle.allow_exec,
@@ -274,11 +303,7 @@ export function resolveBundleEnv(bundle) {
274
303
  });
275
304
  }
276
305
  catch (err) {
277
- const msg = err.message;
278
- if (parsed.ref.provider === 'keychain' && /not found/.test(msg)) {
279
- throw new Error(`${msg} Run: agents secrets add ${bundle.name} ${key}`);
280
- }
281
- throw new Error(`Bundle '${bundle.name}' key '${key}': ${msg}`);
306
+ throw new Error(`Bundle '${bundle.name}' key '${key}': ${err.message}`);
282
307
  }
283
308
  }
284
309
  return env;