@phnx-labs/agents-cli 1.19.1 → 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.
- package/README.md +1 -1
- package/dist/browser.js +0 -0
- package/dist/commands/exec.js +1 -1
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/secrets.js +4 -4
- package/dist/commands/sessions.d.ts +8 -7
- package/dist/commands/sessions.js +32 -20
- package/dist/commands/versions.js +21 -2
- package/dist/computer.js +0 -0
- package/dist/index.js +0 -0
- package/dist/lib/agents.js +66 -0
- package/dist/lib/browser/chrome.js +1 -1
- package/dist/lib/exec.js +21 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +11 -1
- package/dist/lib/secrets/bundles.js +37 -12
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +101 -26
- package/dist/lib/session/db.d.ts +10 -0
- package/dist/lib/session/db.js +26 -0
- package/dist/lib/shims.d.ts +5 -1
- package/dist/lib/shims.js +22 -6
- package/dist/lib/types.d.ts +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -3
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /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 (`
|
|
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
|
package/dist/commands/exec.js
CHANGED
|
@@ -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));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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();
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
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
|
|
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 (
|
|
660
|
-
return [
|
|
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 (
|
|
664
|
-
return [
|
|
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
|
package/dist/lib/agents.js
CHANGED
|
@@ -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) {
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -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. */
|
package/dist/lib/registry.js
CHANGED
|
@@ -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. */
|
|
Binary file
|
|
@@ -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>
|
|
8
|
+
<string>Agents CLI</string>
|
|
9
|
+
<key>CFBundleDisplayName</key>
|
|
10
|
+
<string>Agents CLI</string>
|
|
9
11
|
<key>CFBundleExecutable</key>
|
|
10
|
-
<string>
|
|
12
|
+
<string>Agents CLI</string>
|
|
11
13
|
<key>CFBundlePackageType</key>
|
|
12
14
|
<string>APPL</string>
|
|
13
15
|
<key>CFBundleVersion</key>
|
|
Binary file
|
|
@@ -71,7 +71,17 @@ export interface BundleEntryInfo {
|
|
|
71
71
|
detail: string;
|
|
72
72
|
}
|
|
73
73
|
export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[];
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-platform secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
* macOS: Uses Keychain via signed Swift helper (
|
|
4
|
+
* macOS: Uses Keychain via signed Swift helper (Agents CLI.app) or `security` CLI.
|
|
5
5
|
* Linux: Uses libsecret (GNOME Keyring) via `secret-tool` CLI.
|
|
6
6
|
* Windows: Not yet supported.
|
|
7
7
|
*
|
|
@@ -56,6 +56,20 @@ export declare function setKeychainBackendForTest(b: KeychainBackend | null): Ke
|
|
|
56
56
|
export declare function hasKeychainToken(item: string, sync?: boolean): boolean;
|
|
57
57
|
/** Retrieve a secret value from the keychain/keyring. Throws if not found. */
|
|
58
58
|
export declare function getKeychainToken(item: string, sync?: boolean): string;
|
|
59
|
+
/**
|
|
60
|
+
* Batch-read multiple keychain items behind a single LocalAuthentication
|
|
61
|
+
* prompt. macOS shows ONE Touch ID prompt and every requested item is
|
|
62
|
+
* unlocked in the same process. Returns a Map keyed by item name. Missing
|
|
63
|
+
* items are absent from the map (caller decides whether that's an error).
|
|
64
|
+
*
|
|
65
|
+
* `reason` is shown to the user under the Touch ID prompt — e.g.
|
|
66
|
+
* "read 'hetzner.com' secrets for agent 'claude'". Apple's HIG recommends
|
|
67
|
+
* a lowercase verb phrase that completes the sentence "X is required to ___".
|
|
68
|
+
*
|
|
69
|
+
* On Linux or when a test backend is installed, falls back to individual
|
|
70
|
+
* `get` calls — no biometric prompt path on those platforms.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getKeychainTokensBatch(items: string[], _sync?: boolean, reason?: string): Map<string, string>;
|
|
59
73
|
/** Store or update a secret value in the keychain/keyring. iCloud-synced when sync=true (macOS only). */
|
|
60
74
|
export declare function setKeychainToken(item: string, value: string, sync?: boolean): void;
|
|
61
75
|
/** Delete a keychain/keyring item. Returns true if it existed. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-platform secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
* macOS: Uses Keychain via signed Swift helper (
|
|
4
|
+
* macOS: Uses Keychain via signed Swift helper (Agents CLI.app) or `security` CLI.
|
|
5
5
|
* Linux: Uses libsecret (GNOME Keyring) via `secret-tool` CLI.
|
|
6
6
|
* Windows: Not yet supported.
|
|
7
7
|
*
|
|
@@ -55,7 +55,7 @@ export function profileKeychainItem(provider) {
|
|
|
55
55
|
export function secretsKeychainItem(bundle, key) {
|
|
56
56
|
return `${SERVICE_PREFIX}.secrets.${bundle}.${key}`;
|
|
57
57
|
}
|
|
58
|
-
// Resolve the bundled, signed-and-notarized
|
|
58
|
+
// Resolve the bundled, signed-and-notarized Agents CLI.app shipped
|
|
59
59
|
// alongside the compiled JS. The .app embeds a provisioning profile that
|
|
60
60
|
// grants the application-identifier + keychain-access-groups entitlements
|
|
61
61
|
// macOS requires for kSecAttrSynchronizable writes. Bare CLI binaries
|
|
@@ -64,7 +64,7 @@ export function secretsKeychainItem(bundle, key) {
|
|
|
64
64
|
// be prebuilt by `scripts/build-keychain-helper.sh` and shipped.
|
|
65
65
|
function ensureKeychainHelper() {
|
|
66
66
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
67
|
-
const binPath = path.join(here, '
|
|
67
|
+
const binPath = path.join(here, 'Agents CLI.app', 'Contents', 'MacOS', 'Agents CLI');
|
|
68
68
|
if (!fs.existsSync(binPath)) {
|
|
69
69
|
throw new Error(`Keychain helper missing at ${binPath}. ` +
|
|
70
70
|
'This npm package was built without the signed helper bundle. Reinstall agents-cli.');
|
|
@@ -118,7 +118,9 @@ export function getKeychainToken(item, sync = false) {
|
|
|
118
118
|
if (token)
|
|
119
119
|
return token;
|
|
120
120
|
}
|
|
121
|
-
// Fallback: binary searches both synced and non-synced via kSecAttrSynchronizableAny
|
|
121
|
+
// Fallback: binary searches both synced and non-synced via kSecAttrSynchronizableAny.
|
|
122
|
+
// `get` is the unauthenticated path — no LocalAuthentication prompt. Used by
|
|
123
|
+
// profiles.ts (OAuth refresh) where biometric on every API call is too noisy.
|
|
122
124
|
const bin = ensureKeychainHelper();
|
|
123
125
|
const result = spawnSync(bin, ['get', item, os.userInfo().username], {
|
|
124
126
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -134,6 +136,87 @@ export function getKeychainToken(item, sync = false) {
|
|
|
134
136
|
throw new Error(`Keychain item '${item}' exists but is empty.`);
|
|
135
137
|
return token;
|
|
136
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Batch-read multiple keychain items behind a single LocalAuthentication
|
|
141
|
+
* prompt. macOS shows ONE Touch ID prompt and every requested item is
|
|
142
|
+
* unlocked in the same process. Returns a Map keyed by item name. Missing
|
|
143
|
+
* items are absent from the map (caller decides whether that's an error).
|
|
144
|
+
*
|
|
145
|
+
* `reason` is shown to the user under the Touch ID prompt — e.g.
|
|
146
|
+
* "read 'hetzner.com' secrets for agent 'claude'". Apple's HIG recommends
|
|
147
|
+
* a lowercase verb phrase that completes the sentence "X is required to ___".
|
|
148
|
+
*
|
|
149
|
+
* On Linux or when a test backend is installed, falls back to individual
|
|
150
|
+
* `get` calls — no biometric prompt path on those platforms.
|
|
151
|
+
*/
|
|
152
|
+
export function getKeychainTokensBatch(items, _sync = false, reason) {
|
|
153
|
+
const result = new Map();
|
|
154
|
+
if (items.length === 0)
|
|
155
|
+
return result;
|
|
156
|
+
if (backend) {
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
try {
|
|
159
|
+
result.set(item, backend.get(item, _sync));
|
|
160
|
+
}
|
|
161
|
+
catch { /* missing — skip */ }
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
assertSupportedPlatform();
|
|
166
|
+
if (isLinux()) {
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
try {
|
|
169
|
+
result.set(item, linuxBackend.get(item, _sync));
|
|
170
|
+
}
|
|
171
|
+
catch { /* missing — skip */ }
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
// macOS: single helper invocation with Touch ID gate.
|
|
176
|
+
const bin = ensureKeychainHelper();
|
|
177
|
+
const helperArgs = ['get-batch', os.userInfo().username];
|
|
178
|
+
if (reason)
|
|
179
|
+
helperArgs.push('--reason', reason);
|
|
180
|
+
helperArgs.push(...items);
|
|
181
|
+
const child = spawnSync(bin, helperArgs, {
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
});
|
|
184
|
+
if (child.status !== 0) {
|
|
185
|
+
const msg = child.stderr?.toString().trim();
|
|
186
|
+
throw new Error(msg || `Failed to batch-read ${items.length} keychain items.`);
|
|
187
|
+
}
|
|
188
|
+
const out = child.stdout?.toString() ?? '';
|
|
189
|
+
// Parser. Output is a sequence of records:
|
|
190
|
+
// "V <service>\n<value>\n" (present)
|
|
191
|
+
// "M <service>\n" (missing)
|
|
192
|
+
// Service names cannot contain '\n' (validated at write time); values are
|
|
193
|
+
// also newline-free (rejected by setKeychainToken). So splitting on '\n'
|
|
194
|
+
// and walking line-by-line is unambiguous.
|
|
195
|
+
const lines = out.split('\n');
|
|
196
|
+
// Last entry from split is the empty string after a trailing newline.
|
|
197
|
+
let i = 0;
|
|
198
|
+
while (i < lines.length) {
|
|
199
|
+
const line = lines[i];
|
|
200
|
+
if (line === '' && i === lines.length - 1)
|
|
201
|
+
break;
|
|
202
|
+
if (line.startsWith('V ')) {
|
|
203
|
+
const service = line.slice(2);
|
|
204
|
+
const value = lines[i + 1] ?? '';
|
|
205
|
+
result.set(service, value);
|
|
206
|
+
i += 2;
|
|
207
|
+
}
|
|
208
|
+
else if (line.startsWith('M ')) {
|
|
209
|
+
i += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (line === '') {
|
|
212
|
+
i += 1;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
throw new Error(`Malformed get-batch output line: ${JSON.stringify(line)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
137
220
|
/** Store or update a secret value in the keychain/keyring. iCloud-synced when sync=true (macOS only). */
|
|
138
221
|
export function setKeychainToken(item, value, sync = false) {
|
|
139
222
|
if (backend) {
|
|
@@ -151,28 +234,23 @@ export function setKeychainToken(item, value, sync = false) {
|
|
|
151
234
|
linuxBackend.set(item, value, sync);
|
|
152
235
|
return;
|
|
153
236
|
}
|
|
154
|
-
// macOS path
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
// `security -i` keeps the value out of argv (and `ps`).
|
|
168
|
-
const user = os.userInfo().username;
|
|
169
|
-
const cmd = `add-generic-password -a ${quoteForSecurityCli(user)} -s ${quoteForSecurityCli(item)} -w ${quoteForSecurityCli(value)} -T ${quoteForSecurityCli('')} -U\n`;
|
|
170
|
-
const result = spawnSync('security', ['-i'], {
|
|
171
|
-
input: cmd,
|
|
237
|
+
// macOS path. Both sync and non-sync writes go through the .app helper so
|
|
238
|
+
// the item picks up our SecAccess ACL (helper as trusted reader). That ACL
|
|
239
|
+
// is what stops macOS from showing the legacy "enter password" sheet on
|
|
240
|
+
// subsequent reads. The helper takes an optional `nosync` arg for
|
|
241
|
+
// device-local writes; sync writes get kSecAttrSynchronizable=true by
|
|
242
|
+
// default.
|
|
243
|
+
const bin = ensureKeychainHelper();
|
|
244
|
+
const args = ['set', item, os.userInfo().username];
|
|
245
|
+
if (!sync)
|
|
246
|
+
args.push('nosync');
|
|
247
|
+
const result = spawnSync(bin, args, {
|
|
248
|
+
input: value,
|
|
172
249
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
173
250
|
});
|
|
174
251
|
if (result.status !== 0) {
|
|
175
|
-
|
|
252
|
+
const msg = result.stderr?.toString().trim();
|
|
253
|
+
throw new Error(msg || `Failed to write keychain item '${item}'.`);
|
|
176
254
|
}
|
|
177
255
|
}
|
|
178
256
|
/** Delete a keychain/keyring item. Returns true if it existed. */
|
|
@@ -193,9 +271,6 @@ export function deleteKeychainToken(item, sync = false) {
|
|
|
193
271
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
272
|
}).status === 0;
|
|
195
273
|
}
|
|
196
|
-
function quoteForSecurityCli(s) {
|
|
197
|
-
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
198
|
-
}
|
|
199
274
|
/** Enumerate keychain/keyring item names starting with the given prefix. */
|
|
200
275
|
export function listKeychainItems(prefix) {
|
|
201
276
|
if (backend)
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -157,5 +157,15 @@ export declare function getRowCount(): {
|
|
|
157
157
|
};
|
|
158
158
|
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
159
159
|
export declare function countSessionsOlderThan(cutoffMs: number): number;
|
|
160
|
+
/**
|
|
161
|
+
* Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
|
|
162
|
+
* it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
|
|
163
|
+
* entries so they are re-indexed from the new location on the next scan.
|
|
164
|
+
*
|
|
165
|
+
* Used by removeVersion after soft-deleting a version directory to trash, so
|
|
166
|
+
* that session reads (transcript view, /continue) still work from the trash path.
|
|
167
|
+
* Returns the number of session rows updated.
|
|
168
|
+
*/
|
|
169
|
+
export declare function updateSessionFilePaths(oldPrefix: string, newPrefix: string): number;
|
|
160
170
|
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
161
171
|
export declare function deleteSessionsOlderThan(cutoffMs: number): number;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -749,6 +749,32 @@ export function countSessionsOlderThan(cutoffMs) {
|
|
|
749
749
|
const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
|
|
750
750
|
return row.n;
|
|
751
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
|
|
754
|
+
* it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
|
|
755
|
+
* entries so they are re-indexed from the new location on the next scan.
|
|
756
|
+
*
|
|
757
|
+
* Used by removeVersion after soft-deleting a version directory to trash, so
|
|
758
|
+
* that session reads (transcript view, /continue) still work from the trash path.
|
|
759
|
+
* Returns the number of session rows updated.
|
|
760
|
+
*/
|
|
761
|
+
export function updateSessionFilePaths(oldPrefix, newPrefix) {
|
|
762
|
+
const db = getDB();
|
|
763
|
+
const rows = db
|
|
764
|
+
.prepare(`SELECT id, file_path FROM sessions WHERE file_path LIKE ?`)
|
|
765
|
+
.all(oldPrefix + '%');
|
|
766
|
+
if (rows.length === 0)
|
|
767
|
+
return 0;
|
|
768
|
+
const txn = db.transaction(() => {
|
|
769
|
+
for (const { id, file_path } of rows) {
|
|
770
|
+
const newPath = newPrefix + file_path.slice(oldPrefix.length);
|
|
771
|
+
db.prepare(`UPDATE sessions SET file_path = ? WHERE id = ?`).run(newPath, id);
|
|
772
|
+
db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
txn();
|
|
776
|
+
return rows.length;
|
|
777
|
+
}
|
|
752
778
|
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
753
779
|
export function deleteSessionsOlderThan(cutoffMs) {
|
|
754
780
|
const db = getDB();
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -55,8 +55,12 @@ export interface ConflictInfo {
|
|
|
55
55
|
* v12 — helper calls inside generated shims use the absolute agents-cli
|
|
56
56
|
* entrypoint instead of PATH-resolved `agents`.
|
|
57
57
|
* v13 — validate agents.yaml version strings before constructing binary paths.
|
|
58
|
+
* v14 — derive `configDirName` from `agentConfig.configDir` relative to $HOME
|
|
59
|
+
* instead of hardcoding `.${agent}`. Backwards-compatible for every
|
|
60
|
+
* existing agent (their configDir is `~/.{agent}`); enables nested
|
|
61
|
+
* layouts like Antigravity's `~/.gemini/antigravity-cli/`.
|
|
58
62
|
*/
|
|
59
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
63
|
+
export declare const SHIM_SCHEMA_VERSION = 14;
|
|
60
64
|
/**
|
|
61
65
|
* Generate the full bash shim script for the given agent. The returned string
|
|
62
66
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -179,8 +179,12 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
179
179
|
* v12 — helper calls inside generated shims use the absolute agents-cli
|
|
180
180
|
* entrypoint instead of PATH-resolved `agents`.
|
|
181
181
|
* v13 — validate agents.yaml version strings before constructing binary paths.
|
|
182
|
+
* v14 — derive `configDirName` from `agentConfig.configDir` relative to $HOME
|
|
183
|
+
* instead of hardcoding `.${agent}`. Backwards-compatible for every
|
|
184
|
+
* existing agent (their configDir is `~/.{agent}`); enables nested
|
|
185
|
+
* layouts like Antigravity's `~/.gemini/antigravity-cli/`.
|
|
182
186
|
*/
|
|
183
|
-
export const SHIM_SCHEMA_VERSION =
|
|
187
|
+
export const SHIM_SCHEMA_VERSION = 14;
|
|
184
188
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
185
189
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
186
190
|
function shellQuote(value) {
|
|
@@ -196,7 +200,11 @@ function getAgentsBinForGeneratedShim() {
|
|
|
196
200
|
export function generateShimScript(agent) {
|
|
197
201
|
const agentConfig = AGENTS[agent];
|
|
198
202
|
const cliCommand = agentConfig.cliCommand;
|
|
199
|
-
|
|
203
|
+
// Derive the relative config-dir path from the registry. For most agents
|
|
204
|
+
// this is just `.${agent}` (e.g., `.claude`, `.codex`); for nested layouts
|
|
205
|
+
// like Antigravity (`~/.gemini/antigravity-cli`) it carries the full
|
|
206
|
+
// subpath so per-version HOME symlinks reach the right place.
|
|
207
|
+
const configDirName = path.relative(os.homedir(), agentConfig.configDir);
|
|
200
208
|
const agentsBin = shellQuote(getAgentsBinForGeneratedShim());
|
|
201
209
|
const managedEnv = agent === 'claude'
|
|
202
210
|
? `
|
|
@@ -504,7 +512,9 @@ function assertSafeVersion(version) {
|
|
|
504
512
|
export function generateVersionedAliasScript(agent, version) {
|
|
505
513
|
assertSafeVersion(version);
|
|
506
514
|
const agentConfig = AGENTS[agent];
|
|
507
|
-
|
|
515
|
+
// Same derivation as `generateShimScript` so nested layouts (e.g.,
|
|
516
|
+
// Antigravity's `~/.gemini/antigravity-cli`) land in the right place.
|
|
517
|
+
const configDirName = path.relative(os.homedir(), agentConfig.configDir);
|
|
508
518
|
const managedEnv = agent === 'claude'
|
|
509
519
|
? `
|
|
510
520
|
# Claude stores OAuth credentials in the macOS keychain. Scope them to this
|
|
@@ -637,7 +647,9 @@ function getAgentConfigPath(agent) {
|
|
|
637
647
|
function getVersionConfigPath(agent, version) {
|
|
638
648
|
const agentConfig = AGENTS[agent];
|
|
639
649
|
const versionsDir = getVersionsDir();
|
|
640
|
-
|
|
650
|
+
// Carry the agent's full configDir subpath so nested layouts work.
|
|
651
|
+
// e.g., antigravity → `.gemini/antigravity-cli`, claude → `.claude`.
|
|
652
|
+
const configDirName = path.relative(os.homedir(), agentConfig.configDir);
|
|
641
653
|
return path.join(versionsDir, agent, version, 'home', configDirName);
|
|
642
654
|
}
|
|
643
655
|
/**
|
|
@@ -709,6 +721,7 @@ export async function switchConfigSymlink(agent, version) {
|
|
|
709
721
|
}
|
|
710
722
|
// Different target - update it
|
|
711
723
|
fs.unlinkSync(configPath);
|
|
724
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
712
725
|
fs.symlinkSync(versionConfigPath, configPath);
|
|
713
726
|
return { success: true };
|
|
714
727
|
}
|
|
@@ -721,7 +734,7 @@ export async function switchConfigSymlink(agent, version) {
|
|
|
721
734
|
const finalBackupPath = path.join(agentBackupDir, String(timestamp));
|
|
722
735
|
fs.mkdirSync(agentBackupDir, { recursive: true });
|
|
723
736
|
fs.renameSync(configPath, finalBackupPath);
|
|
724
|
-
// Create symlink
|
|
737
|
+
// Create symlink (parent already exists since the dir we just moved was here)
|
|
725
738
|
fs.symlinkSync(versionConfigPath, configPath);
|
|
726
739
|
return { success: true, backupPath: finalBackupPath };
|
|
727
740
|
}
|
|
@@ -731,7 +744,10 @@ export async function switchConfigSymlink(agent, version) {
|
|
|
731
744
|
}
|
|
732
745
|
catch (err) {
|
|
733
746
|
if (err.code === 'ENOENT') {
|
|
734
|
-
// Config path doesn't exist - create symlink
|
|
747
|
+
// Config path doesn't exist - create symlink.
|
|
748
|
+
// For nested layouts (e.g., ~/.gemini/antigravity-cli) the parent dir
|
|
749
|
+
// may also be missing if the parent agent (Gemini) is not installed.
|
|
750
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
735
751
|
fs.symlinkSync(versionConfigPath, configPath);
|
|
736
752
|
return { success: true };
|
|
737
753
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* formats for each supported agent.
|
|
7
7
|
*/
|
|
8
8
|
/** Unique identifier for a supported AI coding agent. */
|
|
9
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo';
|
|
9
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok';
|
|
10
10
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
11
11
|
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
12
12
|
/** Preview features that users can opt into via `agents beta`. */
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@phnx-labs/agents-cli",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.2",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist/**/*.js",
|
|
25
25
|
"dist/**/*.d.ts",
|
|
26
|
-
"dist/lib/secrets/
|
|
26
|
+
"dist/lib/secrets/Agents CLI.app/**",
|
|
27
27
|
"npm-shrinkwrap.json",
|
|
28
28
|
"scripts/postinstall.js",
|
|
29
29
|
"CHANGELOG.md",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"url": "https://github.com/phnx-labs/agents-cli/issues"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"build": "tsc && rm -rf dist/lib/secrets/AgentsKeychain.app && ([ \"$(uname)\" = \"Darwin\" ] && cp -R bin/
|
|
45
|
+
"build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' && ([ \"$(uname)\" = \"Darwin\" ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true)",
|
|
46
46
|
"prepack": "scripts/verify-keychain-helper.sh",
|
|
47
47
|
"postinstall": "node scripts/postinstall.js",
|
|
48
48
|
"dev": "tsx src/index.ts",
|
|
Binary file
|
|
File without changes
|
/package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile
RENAMED
|
File without changes
|