@phnx-labs/agents-cli 1.20.0 → 1.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +4 -4
- package/dist/commands/cli.js +3 -3
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +24 -7
- package/dist/commands/exec.js +36 -16
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +86 -7
- package/dist/commands/import.js +90 -37
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +117 -4
- package/dist/commands/pull.js +4 -4
- package/dist/commands/routines.js +6 -6
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +74 -39
- package/dist/commands/skills.js +22 -5
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +48 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.js +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +169 -8
- package/dist/commands/workflows.js +29 -6
- package/dist/index.js +4 -0
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +41 -17
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/chrome.js +4 -0
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +3 -3
- package/dist/lib/browser/service.js +19 -0
- package/dist/lib/browser/types.d.ts +4 -4
- package/dist/lib/cli-resources.d.ts +36 -8
- package/dist/lib/cli-resources.js +268 -46
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +39 -11
- package/dist/lib/exec.js +90 -31
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +68 -15
- package/dist/lib/import.d.ts +21 -0
- package/dist/lib/import.js +55 -2
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +40 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +51 -1
- package/dist/lib/plugin-marketplace.d.ts +10 -0
- package/dist/lib/plugin-marketplace.js +47 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +187 -8
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/pty-server.js +27 -3
- package/dist/lib/routines-format.d.ts +17 -5
- package/dist/lib/routines-format.js +37 -16
- package/dist/lib/routines.d.ts +1 -1
- package/dist/lib/routines.js +2 -2
- package/dist/lib/runner.js +64 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +18 -22
- package/dist/lib/secrets/bundles.js +75 -99
- package/dist/lib/secrets/index.d.ts +51 -27
- package/dist/lib/secrets/index.js +147 -156
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +5 -35
- package/dist/lib/state.d.ts +14 -1
- package/dist/lib/state.js +49 -5
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +47 -21
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/types.d.ts +57 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +35 -1
- package/dist/lib/versions.js +288 -64
- package/package.json +13 -12
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
package/dist/lib/versions.js
CHANGED
|
@@ -31,6 +31,7 @@ import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_A
|
|
|
31
31
|
import { installMcpServers, parseMcpServerConfig } from './mcp.js';
|
|
32
32
|
import { markdownToToml } from './convert.js';
|
|
33
33
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
34
|
+
import { importInstallScriptBinary } from './import.js';
|
|
34
35
|
import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
|
|
35
36
|
import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
|
|
36
37
|
import { registerHooksToSettings } from './hooks.js';
|
|
@@ -511,16 +512,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
511
512
|
}
|
|
512
513
|
return result;
|
|
513
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Names that exist ONLY in the project's `.agents/` layer (no matching entry in
|
|
517
|
+
* user/system/extra layers). Sync intentionally skips project-layer commands,
|
|
518
|
+
* skills, hooks, subagents, plugins, and workflows for security — see the
|
|
519
|
+
* defense comments above each sync branch in syncResourcesToVersion. Without
|
|
520
|
+
* this filter, those names would forever appear in the "New resources" diff
|
|
521
|
+
* because they live in `available` but never reach `actuallySynced`.
|
|
522
|
+
*/
|
|
523
|
+
export function getProjectOnlyResources(cwd = process.cwd()) {
|
|
524
|
+
const empty = {
|
|
525
|
+
commands: new Set(), skills: new Set(), hooks: new Set(),
|
|
526
|
+
subagents: new Set(), plugins: new Set(), workflows: new Set(),
|
|
527
|
+
};
|
|
528
|
+
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
529
|
+
if (!projectAgentsDir)
|
|
530
|
+
return empty;
|
|
531
|
+
const trustedBases = [getUserAgentsDir(), getAgentsDir(), ...getEnabledExtraRepos().map(e => e.dir)];
|
|
532
|
+
const trustedNames = (relSubdir, predicate) => {
|
|
533
|
+
const acc = new Set();
|
|
534
|
+
for (const base of trustedBases) {
|
|
535
|
+
const dir = path.join(base, relSubdir);
|
|
536
|
+
if (!fs.existsSync(dir))
|
|
537
|
+
continue;
|
|
538
|
+
try {
|
|
539
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
540
|
+
if (entry.startsWith('.'))
|
|
541
|
+
continue;
|
|
542
|
+
if (predicate(path.join(dir, entry), entry))
|
|
543
|
+
acc.add(entry);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch { /* ignore unreadable */ }
|
|
547
|
+
}
|
|
548
|
+
return acc;
|
|
549
|
+
};
|
|
550
|
+
const readProjectNames = (relSubdir, predicate) => {
|
|
551
|
+
const dir = path.join(projectAgentsDir, relSubdir);
|
|
552
|
+
if (!fs.existsSync(dir))
|
|
553
|
+
return [];
|
|
554
|
+
try {
|
|
555
|
+
return fs.readdirSync(dir)
|
|
556
|
+
.filter(e => !e.startsWith('.'))
|
|
557
|
+
.filter(e => predicate(path.join(dir, e), e));
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const isMdFile = (full, name) => name.endsWith('.md') && (() => { try {
|
|
564
|
+
return fs.statSync(full).isFile();
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return false;
|
|
568
|
+
} })();
|
|
569
|
+
const isDir = (full) => { try {
|
|
570
|
+
return fs.statSync(full).isDirectory();
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return false;
|
|
574
|
+
} };
|
|
575
|
+
const hasFile = (sub) => (full) => isDir(full) && fs.existsSync(path.join(full, sub));
|
|
576
|
+
const stripMd = (n) => n.replace(/\.md$/, '');
|
|
577
|
+
const trustedCommands = new Set([...trustedNames('commands', isMdFile)].map(stripMd));
|
|
578
|
+
const projectCommands = readProjectNames('commands', isMdFile).map(stripMd);
|
|
579
|
+
for (const n of projectCommands)
|
|
580
|
+
if (!trustedCommands.has(n))
|
|
581
|
+
empty.commands.add(n);
|
|
582
|
+
const trustedSkills = trustedNames('skills', (full) => isDir(full));
|
|
583
|
+
for (const n of readProjectNames('skills', (full) => isDir(full)))
|
|
584
|
+
if (!trustedSkills.has(n))
|
|
585
|
+
empty.skills.add(n);
|
|
586
|
+
// Hooks: project entries are files; trusted entries are also files. Name match
|
|
587
|
+
// is filename-with-extension (sync compares by full filename, line 2031).
|
|
588
|
+
const trustedHooks = trustedNames('hooks', (full) => { try {
|
|
589
|
+
return fs.statSync(full).isFile();
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return false;
|
|
593
|
+
} });
|
|
594
|
+
for (const n of readProjectNames('hooks', (full) => { try {
|
|
595
|
+
return fs.statSync(full).isFile();
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return false;
|
|
599
|
+
} })) {
|
|
600
|
+
if (!trustedHooks.has(n))
|
|
601
|
+
empty.hooks.add(n);
|
|
602
|
+
}
|
|
603
|
+
const trustedSubagents = trustedNames('subagents', hasFile('AGENT.md'));
|
|
604
|
+
for (const n of readProjectNames('subagents', hasFile('AGENT.md'))) {
|
|
605
|
+
if (!trustedSubagents.has(n))
|
|
606
|
+
empty.subagents.add(n);
|
|
607
|
+
}
|
|
608
|
+
const trustedWorkflows = trustedNames('workflows', hasFile('WORKFLOW.md'));
|
|
609
|
+
for (const n of readProjectNames('workflows', hasFile('WORKFLOW.md'))) {
|
|
610
|
+
if (!trustedWorkflows.has(n))
|
|
611
|
+
empty.workflows.add(n);
|
|
612
|
+
}
|
|
613
|
+
const trustedPlugins = trustedNames('plugins', hasFile('.claude-plugin/plugin.json'));
|
|
614
|
+
for (const n of readProjectNames('plugins', hasFile('.claude-plugin/plugin.json'))) {
|
|
615
|
+
if (!trustedPlugins.has(n))
|
|
616
|
+
empty.plugins.add(n);
|
|
617
|
+
}
|
|
618
|
+
return empty;
|
|
619
|
+
}
|
|
514
620
|
/**
|
|
515
621
|
* Compare available resources with what's ACTUALLY synced to version home.
|
|
516
622
|
* Returns only NEW resources that haven't been synced yet.
|
|
517
623
|
* Source of truth: the actual files/config, NOT agents.yaml tracking.
|
|
624
|
+
*
|
|
625
|
+
* `projectOnly` (recommended): the result of `getProjectOnlyResources(cwd)`.
|
|
626
|
+
* Names listed there are filtered out for kinds that sync intentionally
|
|
627
|
+
* excludes the project layer — otherwise they would re-appear as "new"
|
|
628
|
+
* on every run and "Yes, sync all new" would silently do nothing for them.
|
|
518
629
|
*/
|
|
519
|
-
export function getNewResources(available, actuallySynced) {
|
|
630
|
+
export function getNewResources(available, actuallySynced, projectOnly) {
|
|
631
|
+
const exclude = projectOnly || {
|
|
632
|
+
commands: new Set(), skills: new Set(), hooks: new Set(),
|
|
633
|
+
subagents: new Set(), plugins: new Set(), workflows: new Set(),
|
|
634
|
+
};
|
|
520
635
|
return {
|
|
521
|
-
commands: available.commands.filter(c => !actuallySynced.commands.includes(c)),
|
|
522
|
-
skills: available.skills.filter(s => !actuallySynced.skills.includes(s)),
|
|
523
|
-
hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h)),
|
|
636
|
+
commands: available.commands.filter(c => !actuallySynced.commands.includes(c) && !exclude.commands.has(c)),
|
|
637
|
+
skills: available.skills.filter(s => !actuallySynced.skills.includes(s) && !exclude.skills.has(s)),
|
|
638
|
+
hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h) && !exclude.hooks.has(h)),
|
|
524
639
|
// Memory/rules presets are mutually exclusive — only one can be active.
|
|
525
640
|
// If any preset is synced, don't report others as "new".
|
|
526
641
|
memory: actuallySynced.memory.length > 0
|
|
@@ -528,9 +643,9 @@ export function getNewResources(available, actuallySynced) {
|
|
|
528
643
|
: available.memory.filter(m => !actuallySynced.memory.includes(m)),
|
|
529
644
|
mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
|
|
530
645
|
permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
|
|
531
|
-
subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
|
|
532
|
-
plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
|
|
533
|
-
workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w)),
|
|
646
|
+
subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s) && !exclude.subagents.has(s)),
|
|
647
|
+
plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p) && !exclude.plugins.has(p)),
|
|
648
|
+
workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w) && !exclude.workflows.has(w)),
|
|
534
649
|
// Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
|
|
535
650
|
// directly, so there is never a "new" per-version state to reconcile.
|
|
536
651
|
promptcuts: false,
|
|
@@ -1027,56 +1142,70 @@ export function setGlobalDefault(agent, version) {
|
|
|
1027
1142
|
*/
|
|
1028
1143
|
export async function installVersion(agent, version, onProgress) {
|
|
1029
1144
|
const agentConfig = AGENTS[agent];
|
|
1030
|
-
if (!agentConfig.npmPackage) {
|
|
1031
|
-
// Support agents that provide an installScript (cursor, roo, goose, kiro, grok, etc.)
|
|
1032
|
-
if (agentConfig.installScript) {
|
|
1033
|
-
// For grok we give special love because the user asked for first-class support
|
|
1034
|
-
if (agent === 'grok') {
|
|
1035
|
-
onProgress?.(`Installing Grok ${version} via official installer...`);
|
|
1036
|
-
try {
|
|
1037
|
-
const script = agentConfig.installScript.replace('VERSION', version);
|
|
1038
|
-
// The official installer supports -s <version>
|
|
1039
|
-
const { exec } = await import('child_process');
|
|
1040
|
-
const { promisify } = await import('util');
|
|
1041
|
-
const execAsync = promisify(exec);
|
|
1042
|
-
await execAsync(script, { timeout: 120000 });
|
|
1043
|
-
onProgress?.('Grok binary installed. Setting up agents-cli version home for isolation...');
|
|
1044
|
-
}
|
|
1045
|
-
catch (err) {
|
|
1046
|
-
return { success: false, installedVersion: version, error: `Grok installer failed: ${err.message}` };
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
else {
|
|
1050
|
-
return {
|
|
1051
|
-
success: false,
|
|
1052
|
-
installedVersion: version,
|
|
1053
|
-
error: `${agent} uses an external installer. Run: ${agentConfig.installScript}`,
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
else {
|
|
1058
|
-
return { success: false, installedVersion: version, error: 'Agent has no npm package' };
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
1145
|
// Validate before deriving filesystem paths or npm package specs. The CLI
|
|
1062
1146
|
// parser already enforces this for user input; this guard protects direct
|
|
1063
1147
|
// callers and tests the critical install path at the source.
|
|
1064
1148
|
if (!VERSION_RE.test(version)) {
|
|
1065
1149
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
1066
1150
|
}
|
|
1151
|
+
if (!agentConfig.npmPackage) {
|
|
1152
|
+
if (!agentConfig.installScript) {
|
|
1153
|
+
return { success: false, installedVersion: version, error: 'Agent has no npm package' };
|
|
1154
|
+
}
|
|
1155
|
+
if (version !== 'latest' && !agentConfig.installScript.includes('VERSION')) {
|
|
1156
|
+
return {
|
|
1157
|
+
success: false,
|
|
1158
|
+
installedVersion: version,
|
|
1159
|
+
error: `${agentConfig.name} installer does not support version-pinned installs. Use ${agent}@latest.`,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
let installedVersion = version;
|
|
1163
|
+
try {
|
|
1164
|
+
const script = agentConfig.installScript.replaceAll('VERSION', version);
|
|
1165
|
+
onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
|
|
1166
|
+
await execAsync(script, { timeout: 120000 });
|
|
1167
|
+
if (version === 'latest') {
|
|
1168
|
+
installedVersion = await getCliVersionFromPath(agent) || version;
|
|
1169
|
+
}
|
|
1170
|
+
onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
emit('version.install', { agent, version, error: err.message });
|
|
1174
|
+
return { success: false, installedVersion: version, error: `${agentConfig.name} installer failed: ${err.message}` };
|
|
1175
|
+
}
|
|
1176
|
+
ensureAgentsDir();
|
|
1177
|
+
const versionDir = getVersionDir(agent, installedVersion);
|
|
1178
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
1179
|
+
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1180
|
+
// Symlink the installed binary into the version's node_modules/.bin so
|
|
1181
|
+
// listInstalledVersions (which checks getBinaryPath) sees this version as
|
|
1182
|
+
// installed. Without this, `agents add antigravity@latest` succeeds
|
|
1183
|
+
// but `agents view` shows the agent under "Not Managed" because
|
|
1184
|
+
// listInstalledVersions returns [] — the installer drops the binary in
|
|
1185
|
+
// ~/.local/bin (or similar) rather than the version's node_modules/.bin.
|
|
1186
|
+
// Grok is special-cased in getBinaryPath itself (binary lives in
|
|
1187
|
+
// ~/.grok/downloads), so we skip the symlink there.
|
|
1188
|
+
if (agent !== 'grok') {
|
|
1189
|
+
try {
|
|
1190
|
+
const { stdout: whichOut } = await execFileAsync('which', [agentConfig.cliCommand]);
|
|
1191
|
+
const installedBinary = whichOut.trim();
|
|
1192
|
+
if (installedBinary && fs.existsSync(installedBinary)) {
|
|
1193
|
+
importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
/* binary missing from PATH — install script failed silently; surface via the existing version.install error path below isn't possible here since the script returned 0. Leave the version dir empty so getBinaryPath check correctly reports it uninstalled. */
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
createVersionedAlias(agent, installedVersion);
|
|
1201
|
+
emit('version.install', { agent, version: installedVersion });
|
|
1202
|
+
return { success: true, installedVersion };
|
|
1203
|
+
}
|
|
1067
1204
|
ensureAgentsDir();
|
|
1068
1205
|
const versionDir = getVersionDir(agent, version);
|
|
1069
1206
|
// Create version directory and isolated home
|
|
1070
1207
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1071
1208
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1072
|
-
// For agents using external installers (especially grok), we don't do npm install.
|
|
1073
|
-
// The binary is managed by the agent's own installer; we only manage the isolated home + resources.
|
|
1074
|
-
if (agent === 'grok' || !agentConfig.npmPackage) {
|
|
1075
|
-
// Grok (and similar) — binary already installed by the step above (or user ran external installer).
|
|
1076
|
-
// We still want to record the version for config isolation.
|
|
1077
|
-
createVersionedAlias(agent, version);
|
|
1078
|
-
return { success: true, installedVersion: version };
|
|
1079
|
-
}
|
|
1080
1209
|
// Initialize package.json (only for real npm agents)
|
|
1081
1210
|
const packageJson = {
|
|
1082
1211
|
name: `agents-${agent}-${version}`,
|
|
@@ -1408,6 +1537,18 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1408
1537
|
return version;
|
|
1409
1538
|
}
|
|
1410
1539
|
}
|
|
1540
|
+
async function getCliVersionFromPath(agent) {
|
|
1541
|
+
const agentConfig = AGENTS[agent];
|
|
1542
|
+
try {
|
|
1543
|
+
await execFileAsync('which', [agentConfig.cliCommand]);
|
|
1544
|
+
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
|
|
1545
|
+
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
1546
|
+
return match ? match[1] : null;
|
|
1547
|
+
}
|
|
1548
|
+
catch {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1411
1552
|
/**
|
|
1412
1553
|
* Get the diff between central resources (~/.agents/) and what's synced to a version.
|
|
1413
1554
|
* Uses filesystem state - no tracking needed.
|
|
@@ -1646,12 +1787,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1646
1787
|
}
|
|
1647
1788
|
}
|
|
1648
1789
|
}
|
|
1649
|
-
// Fast guard: skip the entire sync when
|
|
1650
|
-
// has changed since the last full sync.
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1654
|
-
if (!selection && !options.force) {
|
|
1790
|
+
// Fast guard: skip the entire sync when the caller requested a full sync and
|
|
1791
|
+
// nothing has changed since the last full sync. Pattern-derived selections
|
|
1792
|
+
// still count as full syncs because they are the persisted intended scope,
|
|
1793
|
+
// not a one-off caller override.
|
|
1794
|
+
if (!userPassedSelection && !options.force) {
|
|
1655
1795
|
const manifest = loadManifest(agent, version);
|
|
1656
1796
|
if (manifest && !isStale(manifest, agent, version, cwd)) {
|
|
1657
1797
|
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
|
|
@@ -1758,8 +1898,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1758
1898
|
// not pass an explicit `selection`. Callers that pass explicit selections
|
|
1759
1899
|
// are using the incremental/additive API (sync exactly these; leave others
|
|
1760
1900
|
// alone), so the sweep would be a contract violation there. The
|
|
1761
|
-
// cross-project leak
|
|
1762
|
-
//
|
|
1901
|
+
// cross-project leak always comes from the no-selection shim auto-sync at
|
|
1902
|
+
// launch.
|
|
1763
1903
|
if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
|
|
1764
1904
|
const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1765
1905
|
if (fs.existsSync(commandsTargetSweep)) {
|
|
@@ -2121,6 +2261,24 @@ export function getEffectiveHome(agentId) {
|
|
|
2121
2261
|
}
|
|
2122
2262
|
return os.homedir();
|
|
2123
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Thrown when the user references an agent@version that is not installed.
|
|
2266
|
+
* Carries the parsed (agentId, version) so callers can react — e.g. prompt
|
|
2267
|
+
* to install it on demand — without having to parse the error message.
|
|
2268
|
+
*/
|
|
2269
|
+
export class VersionNotInstalledError extends Error {
|
|
2270
|
+
agentId;
|
|
2271
|
+
version;
|
|
2272
|
+
installedVersions;
|
|
2273
|
+
constructor(agentId, version, installedVersions) {
|
|
2274
|
+
const installed = installedVersions.length > 0 ? installedVersions.join(', ') : '(none)';
|
|
2275
|
+
super(`Version ${version} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installed}`);
|
|
2276
|
+
this.agentId = agentId;
|
|
2277
|
+
this.version = version;
|
|
2278
|
+
this.installedVersions = installedVersions;
|
|
2279
|
+
this.name = 'VersionNotInstalledError';
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2124
2282
|
/**
|
|
2125
2283
|
* Resolve a comma-separated --agents list into concrete version selections.
|
|
2126
2284
|
* Bare agents target the default version, or the newest installed version when no default exists.
|
|
@@ -2130,10 +2288,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2130
2288
|
const selectedAgents = [];
|
|
2131
2289
|
const versionSelections = new Map();
|
|
2132
2290
|
const explicitSelections = new Set();
|
|
2133
|
-
const
|
|
2291
|
+
const rawTargets = value
|
|
2134
2292
|
.split(',')
|
|
2135
2293
|
.map((item) => item.trim())
|
|
2136
2294
|
.filter(Boolean);
|
|
2295
|
+
// Expand literal `all` (with optional @all) into every available agent's all
|
|
2296
|
+
// installed versions. Skip agents with no installed versions so `all` is
|
|
2297
|
+
// lenient — only explicit `claude@all` errors when claude isn't installed.
|
|
2298
|
+
const targets = [];
|
|
2299
|
+
for (const t of rawTargets) {
|
|
2300
|
+
if (t === 'all' || t === 'all@all') {
|
|
2301
|
+
for (const a of availableAgents) {
|
|
2302
|
+
if (listInstalledVersions(a).length > 0) {
|
|
2303
|
+
targets.push(`${a}@all`);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
else {
|
|
2308
|
+
targets.push(t);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2137
2311
|
for (const target of targets) {
|
|
2138
2312
|
const atIndex = target.indexOf('@');
|
|
2139
2313
|
const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
|
|
@@ -2142,7 +2316,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2142
2316
|
continue;
|
|
2143
2317
|
}
|
|
2144
2318
|
if (atIndex !== -1 && !versionToken) {
|
|
2145
|
-
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@
|
|
2319
|
+
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
|
|
2146
2320
|
}
|
|
2147
2321
|
const agentId = resolveAgentName(agentToken);
|
|
2148
2322
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -2182,8 +2356,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2182
2356
|
explicitSelections.add(agentId);
|
|
2183
2357
|
continue;
|
|
2184
2358
|
}
|
|
2359
|
+
if (versionToken === 'all') {
|
|
2360
|
+
versionSelections.set(agentId, [...installedVersions]);
|
|
2361
|
+
explicitSelections.add(agentId);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2185
2364
|
if (!installedVersions.includes(versionToken)) {
|
|
2186
|
-
throw new
|
|
2365
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
2187
2366
|
}
|
|
2188
2367
|
const explicitVersions = explicitSelections.has(agentId)
|
|
2189
2368
|
? (versionSelections.get(agentId) || [])
|
|
@@ -2206,10 +2385,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2206
2385
|
const selectedAgents = [];
|
|
2207
2386
|
const directAgents = [];
|
|
2208
2387
|
const versionSelections = new Map();
|
|
2209
|
-
const
|
|
2388
|
+
const rawTargets = value
|
|
2210
2389
|
.split(',')
|
|
2211
2390
|
.map((item) => item.trim())
|
|
2212
2391
|
.filter(Boolean);
|
|
2392
|
+
// Expand literal `all` (with optional @all) into every available agent's all
|
|
2393
|
+
// installed versions. Skip agents with no installed versions so `all` is
|
|
2394
|
+
// lenient — only explicit `claude@all` errors when claude isn't installed.
|
|
2395
|
+
// Mirrors resolveAgentVersionTargets so every --agents flag site supports
|
|
2396
|
+
// the same selector syntax.
|
|
2397
|
+
const targets = [];
|
|
2398
|
+
for (const t of rawTargets) {
|
|
2399
|
+
if (t === 'all' || t === 'all@all') {
|
|
2400
|
+
for (const a of availableAgents) {
|
|
2401
|
+
if (listInstalledVersions(a).length > 0) {
|
|
2402
|
+
targets.push(`${a}@all`);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
else {
|
|
2407
|
+
targets.push(t);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2213
2410
|
const addVersionTarget = (agentId, version) => {
|
|
2214
2411
|
const versions = versionSelections.get(agentId) || [];
|
|
2215
2412
|
if (!versions.includes(version)) {
|
|
@@ -2229,7 +2426,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2229
2426
|
continue;
|
|
2230
2427
|
}
|
|
2231
2428
|
if (atIndex !== -1 && !versionToken) {
|
|
2232
|
-
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@
|
|
2429
|
+
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
|
|
2233
2430
|
}
|
|
2234
2431
|
const agentId = resolveAgentName(agentToken);
|
|
2235
2432
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -2262,11 +2459,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2262
2459
|
addVersionTarget(agentId, defaultVersion);
|
|
2263
2460
|
continue;
|
|
2264
2461
|
}
|
|
2462
|
+
if (versionToken === 'all') {
|
|
2463
|
+
if (installedVersions.length === 0) {
|
|
2464
|
+
throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
|
|
2465
|
+
}
|
|
2466
|
+
for (const version of installedVersions) {
|
|
2467
|
+
addVersionTarget(agentId, version);
|
|
2468
|
+
}
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2265
2471
|
if (installedVersions.length === 0) {
|
|
2266
2472
|
throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
|
|
2267
2473
|
}
|
|
2268
2474
|
if (!installedVersions.includes(versionToken)) {
|
|
2269
|
-
throw new
|
|
2475
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
2270
2476
|
}
|
|
2271
2477
|
addVersionTarget(agentId, versionToken);
|
|
2272
2478
|
}
|
|
@@ -2320,9 +2526,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2320
2526
|
const defaultVer = getGlobalDefault(agentId);
|
|
2321
2527
|
if (versions.length === 0)
|
|
2322
2528
|
return `${AGENTS[agentId].name} ${chalk.gray('(not installed)')}`;
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2529
|
+
// Surface the version count when there's more than one — mirrors the new
|
|
2530
|
+
// `--agents <agent>@all` syntax so users can see at a glance how many
|
|
2531
|
+
// versions `@all` would target before the per-version prompt fires.
|
|
2532
|
+
const detail = versions.length > 1
|
|
2533
|
+
? (defaultVer
|
|
2534
|
+
? `active: ${defaultVer}, ${versions.length} versions installed`
|
|
2535
|
+
: `${versions.length} versions installed`)
|
|
2536
|
+
: (defaultVer ?? versions[0]);
|
|
2537
|
+
return `${AGENTS[agentId].name} ${chalk.gray(`(${detail})`)}`;
|
|
2326
2538
|
};
|
|
2327
2539
|
let selectedAgents;
|
|
2328
2540
|
if (options.skipPrompts) {
|
|
@@ -2337,6 +2549,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2337
2549
|
}
|
|
2338
2550
|
}
|
|
2339
2551
|
else {
|
|
2552
|
+
// Non-TTY without an explicit --agents value used to silently fall through
|
|
2553
|
+
// to default-picking inside the caller. That's surprising in scripts — fail
|
|
2554
|
+
// loud and point at the new `--agents` syntax instead.
|
|
2555
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
2556
|
+
throw new Error('Non-interactive shell: cannot prompt for agent/version selection.\n' +
|
|
2557
|
+
'Pass --agents explicitly. Examples:\n' +
|
|
2558
|
+
' --agents claude (default version)\n' +
|
|
2559
|
+
' --agents claude@all (every installed Claude version)\n' +
|
|
2560
|
+
' --agents claude@2.1.141 (a specific version)\n' +
|
|
2561
|
+
' --agents all (every installed version of every capable agent)\n' +
|
|
2562
|
+
'Or pass --yes to auto-pick defaults.');
|
|
2563
|
+
}
|
|
2340
2564
|
// Prompt for agent selection
|
|
2341
2565
|
const checkboxResult = await checkbox({
|
|
2342
2566
|
message: 'Which agents should receive these resources?',
|
|
@@ -2371,7 +2595,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2371
2595
|
const versionResult = await checkbox({
|
|
2372
2596
|
message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
|
|
2373
2597
|
choices: [
|
|
2374
|
-
{ name: chalk.bold(
|
|
2598
|
+
{ name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
|
|
2375
2599
|
...versions.map((v) => {
|
|
2376
2600
|
const base = v === defaultVer ? `${v} (default)` : v;
|
|
2377
2601
|
let label = base.padEnd(maxLabelLen);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.4",
|
|
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",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"dist/**/*.d.ts",
|
|
26
26
|
"dist/lib/secrets/Agents CLI.app/**",
|
|
27
27
|
"scripts/postinstall.js",
|
|
28
|
+
"scripts/install-helper.js",
|
|
28
29
|
"CHANGELOG.md",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
32
33
|
"publishConfig": {
|
|
33
|
-
"provenance":
|
|
34
|
+
"provenance": false
|
|
34
35
|
},
|
|
35
36
|
"repository": {
|
|
36
37
|
"type": "git",
|
|
@@ -73,29 +74,29 @@
|
|
|
73
74
|
"npm": ">=9"
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
|
-
"@inquirer/prompts": "8.5.
|
|
77
|
+
"@inquirer/prompts": "8.5.2",
|
|
77
78
|
"@types/proper-lockfile": "4.1.4",
|
|
78
79
|
"@xterm/headless": "6.0.0",
|
|
79
80
|
"@zed-industries/agent-client-protocol": "0.4.5",
|
|
80
81
|
"chalk": "5.6.2",
|
|
81
|
-
"commander": "
|
|
82
|
-
"croner": "
|
|
82
|
+
"commander": "15.0.0",
|
|
83
|
+
"croner": "10.0.1",
|
|
83
84
|
"diff": "9.0.0",
|
|
84
85
|
"marked": "15.0.12",
|
|
85
86
|
"marked-terminal": "7.3.0",
|
|
86
87
|
"node-pty": "1.1.0",
|
|
87
|
-
"ora": "
|
|
88
|
+
"ora": "9.4.0",
|
|
88
89
|
"proper-lockfile": "4.1.2",
|
|
89
90
|
"simple-git": "3.36.0",
|
|
90
91
|
"smol-toml": "1.6.1",
|
|
91
|
-
"yaml": "2.
|
|
92
|
+
"yaml": "2.9.0"
|
|
92
93
|
},
|
|
93
94
|
"devDependencies": {
|
|
94
|
-
"@types/diff": "
|
|
95
|
+
"@types/diff": "8.0.0",
|
|
95
96
|
"@types/marked-terminal": "6.1.1",
|
|
96
|
-
"@types/node": "
|
|
97
|
-
"tsx": "4.22.
|
|
98
|
-
"typescript": "
|
|
99
|
-
"vitest": "4.1.
|
|
97
|
+
"@types/node": "25.9.2",
|
|
98
|
+
"tsx": "4.22.4",
|
|
99
|
+
"typescript": "6.0.3",
|
|
100
|
+
"vitest": "4.1.8"
|
|
100
101
|
}
|
|
101
102
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copy the bundled `Agents CLI.app` to a stable user-scoped path so its
|
|
4
|
+
* keychain ACLs survive future npm installs/updates.
|
|
5
|
+
*
|
|
6
|
+
* Source: dist/lib/secrets/Agents CLI.app (in the installed npm package)
|
|
7
|
+
* bin/Agents CLI.app (in a raw working tree)
|
|
8
|
+
* Destination: ~/Library/Application Support/agents-cli/Agents CLI.app
|
|
9
|
+
*
|
|
10
|
+
* Invoked by scripts/postinstall.js on global install, and by
|
|
11
|
+
* scripts/install.sh for dev installs. Pure Node, no agents-cli imports,
|
|
12
|
+
* so it runs before the package's TS is wired up.
|
|
13
|
+
*
|
|
14
|
+
* macOS only — silently no-ops on other platforms so postinstall stays clean.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'child_process';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const APP_BUNDLE_NAME = 'Agents CLI.app';
|
|
24
|
+
const INSTALL_DIR_NAME = 'agents-cli';
|
|
25
|
+
|
|
26
|
+
function destAppPath() {
|
|
27
|
+
return path.join(os.homedir(), 'Library', 'Application Support', INSTALL_DIR_NAME, APP_BUNDLE_NAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findSourceApp() {
|
|
31
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
// From scripts/install-helper.js, look in the installed npm layout first
|
|
33
|
+
// (../dist/lib/secrets/...) then a raw repo layout (../bin/...).
|
|
34
|
+
const candidates = [
|
|
35
|
+
path.resolve(here, '..', 'dist', 'lib', 'secrets', APP_BUNDLE_NAME),
|
|
36
|
+
path.resolve(here, '..', 'bin', APP_BUNDLE_NAME),
|
|
37
|
+
];
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
if (fs.existsSync(c)) return c;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function codesignVerify(appPath) {
|
|
45
|
+
const r = spawnSync('codesign', ['--verify', '--deep', '--strict', appPath], {
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
});
|
|
49
|
+
return { ok: r.status === 0, output: (r.stderr || r.stdout || '').toString().trim() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function copyAppBundle(src, dest) {
|
|
53
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
54
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
55
|
+
const r = spawnSync('cp', ['-R', src, dest], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
|
|
56
|
+
if (r.status !== 0) {
|
|
57
|
+
const msg = (r.stderr || r.stdout || '').toString().trim();
|
|
58
|
+
throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
if (process.platform !== 'darwin') return;
|
|
64
|
+
const force = process.argv.includes('--force');
|
|
65
|
+
const dest = destAppPath();
|
|
66
|
+
|
|
67
|
+
if (!force && fs.existsSync(dest) && codesignVerify(dest).ok) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const src = findSourceApp();
|
|
72
|
+
if (!src) {
|
|
73
|
+
// No source bundle to install. Stay silent during postinstall so we don't
|
|
74
|
+
// create noise on Linux/Windows or on builds that intentionally omit the
|
|
75
|
+
// helper. `agents helper install` surfaces a clearer error if a user
|
|
76
|
+
// actually needs the helper.
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
copyAppBundle(src, dest);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
process.stderr.write(`agents-cli: failed to install Keychain helper: ${err.message}\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const verify = codesignVerify(dest);
|
|
88
|
+
if (!verify.ok) {
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
`agents-cli: installed helper failed codesign verification at ${dest}\n${verify.output}\n`
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(` Installed Keychain helper: ${dest}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main();
|