@phnx-labs/agents-cli 1.20.0 → 1.20.3
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 +73 -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/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 +164 -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 +18 -14
- 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/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/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/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 +267 -64
- package/package.json +9 -8
- 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
|
@@ -511,16 +511,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
511
511
|
}
|
|
512
512
|
return result;
|
|
513
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Names that exist ONLY in the project's `.agents/` layer (no matching entry in
|
|
516
|
+
* user/system/extra layers). Sync intentionally skips project-layer commands,
|
|
517
|
+
* skills, hooks, subagents, plugins, and workflows for security — see the
|
|
518
|
+
* defense comments above each sync branch in syncResourcesToVersion. Without
|
|
519
|
+
* this filter, those names would forever appear in the "New resources" diff
|
|
520
|
+
* because they live in `available` but never reach `actuallySynced`.
|
|
521
|
+
*/
|
|
522
|
+
export function getProjectOnlyResources(cwd = process.cwd()) {
|
|
523
|
+
const empty = {
|
|
524
|
+
commands: new Set(), skills: new Set(), hooks: new Set(),
|
|
525
|
+
subagents: new Set(), plugins: new Set(), workflows: new Set(),
|
|
526
|
+
};
|
|
527
|
+
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
528
|
+
if (!projectAgentsDir)
|
|
529
|
+
return empty;
|
|
530
|
+
const trustedBases = [getUserAgentsDir(), getAgentsDir(), ...getEnabledExtraRepos().map(e => e.dir)];
|
|
531
|
+
const trustedNames = (relSubdir, predicate) => {
|
|
532
|
+
const acc = new Set();
|
|
533
|
+
for (const base of trustedBases) {
|
|
534
|
+
const dir = path.join(base, relSubdir);
|
|
535
|
+
if (!fs.existsSync(dir))
|
|
536
|
+
continue;
|
|
537
|
+
try {
|
|
538
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
539
|
+
if (entry.startsWith('.'))
|
|
540
|
+
continue;
|
|
541
|
+
if (predicate(path.join(dir, entry), entry))
|
|
542
|
+
acc.add(entry);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch { /* ignore unreadable */ }
|
|
546
|
+
}
|
|
547
|
+
return acc;
|
|
548
|
+
};
|
|
549
|
+
const readProjectNames = (relSubdir, predicate) => {
|
|
550
|
+
const dir = path.join(projectAgentsDir, relSubdir);
|
|
551
|
+
if (!fs.existsSync(dir))
|
|
552
|
+
return [];
|
|
553
|
+
try {
|
|
554
|
+
return fs.readdirSync(dir)
|
|
555
|
+
.filter(e => !e.startsWith('.'))
|
|
556
|
+
.filter(e => predicate(path.join(dir, e), e));
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
const isMdFile = (full, name) => name.endsWith('.md') && (() => { try {
|
|
563
|
+
return fs.statSync(full).isFile();
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return false;
|
|
567
|
+
} })();
|
|
568
|
+
const isDir = (full) => { try {
|
|
569
|
+
return fs.statSync(full).isDirectory();
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return false;
|
|
573
|
+
} };
|
|
574
|
+
const hasFile = (sub) => (full) => isDir(full) && fs.existsSync(path.join(full, sub));
|
|
575
|
+
const stripMd = (n) => n.replace(/\.md$/, '');
|
|
576
|
+
const trustedCommands = new Set([...trustedNames('commands', isMdFile)].map(stripMd));
|
|
577
|
+
const projectCommands = readProjectNames('commands', isMdFile).map(stripMd);
|
|
578
|
+
for (const n of projectCommands)
|
|
579
|
+
if (!trustedCommands.has(n))
|
|
580
|
+
empty.commands.add(n);
|
|
581
|
+
const trustedSkills = trustedNames('skills', (full) => isDir(full));
|
|
582
|
+
for (const n of readProjectNames('skills', (full) => isDir(full)))
|
|
583
|
+
if (!trustedSkills.has(n))
|
|
584
|
+
empty.skills.add(n);
|
|
585
|
+
// Hooks: project entries are files; trusted entries are also files. Name match
|
|
586
|
+
// is filename-with-extension (sync compares by full filename, line 2031).
|
|
587
|
+
const trustedHooks = trustedNames('hooks', (full) => { try {
|
|
588
|
+
return fs.statSync(full).isFile();
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return false;
|
|
592
|
+
} });
|
|
593
|
+
for (const n of readProjectNames('hooks', (full) => { try {
|
|
594
|
+
return fs.statSync(full).isFile();
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return false;
|
|
598
|
+
} })) {
|
|
599
|
+
if (!trustedHooks.has(n))
|
|
600
|
+
empty.hooks.add(n);
|
|
601
|
+
}
|
|
602
|
+
const trustedSubagents = trustedNames('subagents', hasFile('AGENT.md'));
|
|
603
|
+
for (const n of readProjectNames('subagents', hasFile('AGENT.md'))) {
|
|
604
|
+
if (!trustedSubagents.has(n))
|
|
605
|
+
empty.subagents.add(n);
|
|
606
|
+
}
|
|
607
|
+
const trustedWorkflows = trustedNames('workflows', hasFile('WORKFLOW.md'));
|
|
608
|
+
for (const n of readProjectNames('workflows', hasFile('WORKFLOW.md'))) {
|
|
609
|
+
if (!trustedWorkflows.has(n))
|
|
610
|
+
empty.workflows.add(n);
|
|
611
|
+
}
|
|
612
|
+
const trustedPlugins = trustedNames('plugins', hasFile('.claude-plugin/plugin.json'));
|
|
613
|
+
for (const n of readProjectNames('plugins', hasFile('.claude-plugin/plugin.json'))) {
|
|
614
|
+
if (!trustedPlugins.has(n))
|
|
615
|
+
empty.plugins.add(n);
|
|
616
|
+
}
|
|
617
|
+
return empty;
|
|
618
|
+
}
|
|
514
619
|
/**
|
|
515
620
|
* Compare available resources with what's ACTUALLY synced to version home.
|
|
516
621
|
* Returns only NEW resources that haven't been synced yet.
|
|
517
622
|
* Source of truth: the actual files/config, NOT agents.yaml tracking.
|
|
623
|
+
*
|
|
624
|
+
* `projectOnly` (recommended): the result of `getProjectOnlyResources(cwd)`.
|
|
625
|
+
* Names listed there are filtered out for kinds that sync intentionally
|
|
626
|
+
* excludes the project layer — otherwise they would re-appear as "new"
|
|
627
|
+
* on every run and "Yes, sync all new" would silently do nothing for them.
|
|
518
628
|
*/
|
|
519
|
-
export function getNewResources(available, actuallySynced) {
|
|
629
|
+
export function getNewResources(available, actuallySynced, projectOnly) {
|
|
630
|
+
const exclude = projectOnly || {
|
|
631
|
+
commands: new Set(), skills: new Set(), hooks: new Set(),
|
|
632
|
+
subagents: new Set(), plugins: new Set(), workflows: new Set(),
|
|
633
|
+
};
|
|
520
634
|
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)),
|
|
635
|
+
commands: available.commands.filter(c => !actuallySynced.commands.includes(c) && !exclude.commands.has(c)),
|
|
636
|
+
skills: available.skills.filter(s => !actuallySynced.skills.includes(s) && !exclude.skills.has(s)),
|
|
637
|
+
hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h) && !exclude.hooks.has(h)),
|
|
524
638
|
// Memory/rules presets are mutually exclusive — only one can be active.
|
|
525
639
|
// If any preset is synced, don't report others as "new".
|
|
526
640
|
memory: actuallySynced.memory.length > 0
|
|
@@ -528,9 +642,9 @@ export function getNewResources(available, actuallySynced) {
|
|
|
528
642
|
: available.memory.filter(m => !actuallySynced.memory.includes(m)),
|
|
529
643
|
mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
|
|
530
644
|
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)),
|
|
645
|
+
subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s) && !exclude.subagents.has(s)),
|
|
646
|
+
plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p) && !exclude.plugins.has(p)),
|
|
647
|
+
workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w) && !exclude.workflows.has(w)),
|
|
534
648
|
// Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
|
|
535
649
|
// directly, so there is never a "new" per-version state to reconcile.
|
|
536
650
|
promptcuts: false,
|
|
@@ -1027,56 +1141,50 @@ export function setGlobalDefault(agent, version) {
|
|
|
1027
1141
|
*/
|
|
1028
1142
|
export async function installVersion(agent, version, onProgress) {
|
|
1029
1143
|
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
1144
|
// Validate before deriving filesystem paths or npm package specs. The CLI
|
|
1062
1145
|
// parser already enforces this for user input; this guard protects direct
|
|
1063
1146
|
// callers and tests the critical install path at the source.
|
|
1064
1147
|
if (!VERSION_RE.test(version)) {
|
|
1065
1148
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
1066
1149
|
}
|
|
1150
|
+
if (!agentConfig.npmPackage) {
|
|
1151
|
+
if (!agentConfig.installScript) {
|
|
1152
|
+
return { success: false, installedVersion: version, error: 'Agent has no npm package' };
|
|
1153
|
+
}
|
|
1154
|
+
if (version !== 'latest' && !agentConfig.installScript.includes('VERSION')) {
|
|
1155
|
+
return {
|
|
1156
|
+
success: false,
|
|
1157
|
+
installedVersion: version,
|
|
1158
|
+
error: `${agentConfig.name} installer does not support version-pinned installs. Use ${agent}@latest.`,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
let installedVersion = version;
|
|
1162
|
+
try {
|
|
1163
|
+
const script = agentConfig.installScript.replaceAll('VERSION', version);
|
|
1164
|
+
onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
|
|
1165
|
+
await execAsync(script, { timeout: 120000 });
|
|
1166
|
+
if (version === 'latest') {
|
|
1167
|
+
installedVersion = await getCliVersionFromPath(agent) || version;
|
|
1168
|
+
}
|
|
1169
|
+
onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
emit('version.install', { agent, version, error: err.message });
|
|
1173
|
+
return { success: false, installedVersion: version, error: `${agentConfig.name} installer failed: ${err.message}` };
|
|
1174
|
+
}
|
|
1175
|
+
ensureAgentsDir();
|
|
1176
|
+
const versionDir = getVersionDir(agent, installedVersion);
|
|
1177
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
1178
|
+
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1179
|
+
createVersionedAlias(agent, installedVersion);
|
|
1180
|
+
emit('version.install', { agent, version: installedVersion });
|
|
1181
|
+
return { success: true, installedVersion };
|
|
1182
|
+
}
|
|
1067
1183
|
ensureAgentsDir();
|
|
1068
1184
|
const versionDir = getVersionDir(agent, version);
|
|
1069
1185
|
// Create version directory and isolated home
|
|
1070
1186
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1071
1187
|
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
1188
|
// Initialize package.json (only for real npm agents)
|
|
1081
1189
|
const packageJson = {
|
|
1082
1190
|
name: `agents-${agent}-${version}`,
|
|
@@ -1408,6 +1516,18 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1408
1516
|
return version;
|
|
1409
1517
|
}
|
|
1410
1518
|
}
|
|
1519
|
+
async function getCliVersionFromPath(agent) {
|
|
1520
|
+
const agentConfig = AGENTS[agent];
|
|
1521
|
+
try {
|
|
1522
|
+
await execFileAsync('which', [agentConfig.cliCommand]);
|
|
1523
|
+
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
|
|
1524
|
+
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
1525
|
+
return match ? match[1] : null;
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1411
1531
|
/**
|
|
1412
1532
|
* Get the diff between central resources (~/.agents/) and what's synced to a version.
|
|
1413
1533
|
* Uses filesystem state - no tracking needed.
|
|
@@ -1646,12 +1766,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1646
1766
|
}
|
|
1647
1767
|
}
|
|
1648
1768
|
}
|
|
1649
|
-
// Fast guard: skip the entire sync when
|
|
1650
|
-
// has changed since the last full sync.
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1654
|
-
if (!selection && !options.force) {
|
|
1769
|
+
// Fast guard: skip the entire sync when the caller requested a full sync and
|
|
1770
|
+
// nothing has changed since the last full sync. Pattern-derived selections
|
|
1771
|
+
// still count as full syncs because they are the persisted intended scope,
|
|
1772
|
+
// not a one-off caller override.
|
|
1773
|
+
if (!userPassedSelection && !options.force) {
|
|
1655
1774
|
const manifest = loadManifest(agent, version);
|
|
1656
1775
|
if (manifest && !isStale(manifest, agent, version, cwd)) {
|
|
1657
1776
|
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
|
|
@@ -1758,8 +1877,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1758
1877
|
// not pass an explicit `selection`. Callers that pass explicit selections
|
|
1759
1878
|
// are using the incremental/additive API (sync exactly these; leave others
|
|
1760
1879
|
// alone), so the sweep would be a contract violation there. The
|
|
1761
|
-
// cross-project leak
|
|
1762
|
-
//
|
|
1880
|
+
// cross-project leak always comes from the no-selection shim auto-sync at
|
|
1881
|
+
// launch.
|
|
1763
1882
|
if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
|
|
1764
1883
|
const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1765
1884
|
if (fs.existsSync(commandsTargetSweep)) {
|
|
@@ -2121,6 +2240,24 @@ export function getEffectiveHome(agentId) {
|
|
|
2121
2240
|
}
|
|
2122
2241
|
return os.homedir();
|
|
2123
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Thrown when the user references an agent@version that is not installed.
|
|
2245
|
+
* Carries the parsed (agentId, version) so callers can react — e.g. prompt
|
|
2246
|
+
* to install it on demand — without having to parse the error message.
|
|
2247
|
+
*/
|
|
2248
|
+
export class VersionNotInstalledError extends Error {
|
|
2249
|
+
agentId;
|
|
2250
|
+
version;
|
|
2251
|
+
installedVersions;
|
|
2252
|
+
constructor(agentId, version, installedVersions) {
|
|
2253
|
+
const installed = installedVersions.length > 0 ? installedVersions.join(', ') : '(none)';
|
|
2254
|
+
super(`Version ${version} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installed}`);
|
|
2255
|
+
this.agentId = agentId;
|
|
2256
|
+
this.version = version;
|
|
2257
|
+
this.installedVersions = installedVersions;
|
|
2258
|
+
this.name = 'VersionNotInstalledError';
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2124
2261
|
/**
|
|
2125
2262
|
* Resolve a comma-separated --agents list into concrete version selections.
|
|
2126
2263
|
* Bare agents target the default version, or the newest installed version when no default exists.
|
|
@@ -2130,10 +2267,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2130
2267
|
const selectedAgents = [];
|
|
2131
2268
|
const versionSelections = new Map();
|
|
2132
2269
|
const explicitSelections = new Set();
|
|
2133
|
-
const
|
|
2270
|
+
const rawTargets = value
|
|
2134
2271
|
.split(',')
|
|
2135
2272
|
.map((item) => item.trim())
|
|
2136
2273
|
.filter(Boolean);
|
|
2274
|
+
// Expand literal `all` (with optional @all) into every available agent's all
|
|
2275
|
+
// installed versions. Skip agents with no installed versions so `all` is
|
|
2276
|
+
// lenient — only explicit `claude@all` errors when claude isn't installed.
|
|
2277
|
+
const targets = [];
|
|
2278
|
+
for (const t of rawTargets) {
|
|
2279
|
+
if (t === 'all' || t === 'all@all') {
|
|
2280
|
+
for (const a of availableAgents) {
|
|
2281
|
+
if (listInstalledVersions(a).length > 0) {
|
|
2282
|
+
targets.push(`${a}@all`);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
targets.push(t);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2137
2290
|
for (const target of targets) {
|
|
2138
2291
|
const atIndex = target.indexOf('@');
|
|
2139
2292
|
const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
|
|
@@ -2142,7 +2295,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2142
2295
|
continue;
|
|
2143
2296
|
}
|
|
2144
2297
|
if (atIndex !== -1 && !versionToken) {
|
|
2145
|
-
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@
|
|
2298
|
+
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
|
|
2146
2299
|
}
|
|
2147
2300
|
const agentId = resolveAgentName(agentToken);
|
|
2148
2301
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -2182,8 +2335,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
2182
2335
|
explicitSelections.add(agentId);
|
|
2183
2336
|
continue;
|
|
2184
2337
|
}
|
|
2338
|
+
if (versionToken === 'all') {
|
|
2339
|
+
versionSelections.set(agentId, [...installedVersions]);
|
|
2340
|
+
explicitSelections.add(agentId);
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2185
2343
|
if (!installedVersions.includes(versionToken)) {
|
|
2186
|
-
throw new
|
|
2344
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
2187
2345
|
}
|
|
2188
2346
|
const explicitVersions = explicitSelections.has(agentId)
|
|
2189
2347
|
? (versionSelections.get(agentId) || [])
|
|
@@ -2206,10 +2364,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2206
2364
|
const selectedAgents = [];
|
|
2207
2365
|
const directAgents = [];
|
|
2208
2366
|
const versionSelections = new Map();
|
|
2209
|
-
const
|
|
2367
|
+
const rawTargets = value
|
|
2210
2368
|
.split(',')
|
|
2211
2369
|
.map((item) => item.trim())
|
|
2212
2370
|
.filter(Boolean);
|
|
2371
|
+
// Expand literal `all` (with optional @all) into every available agent's all
|
|
2372
|
+
// installed versions. Skip agents with no installed versions so `all` is
|
|
2373
|
+
// lenient — only explicit `claude@all` errors when claude isn't installed.
|
|
2374
|
+
// Mirrors resolveAgentVersionTargets so every --agents flag site supports
|
|
2375
|
+
// the same selector syntax.
|
|
2376
|
+
const targets = [];
|
|
2377
|
+
for (const t of rawTargets) {
|
|
2378
|
+
if (t === 'all' || t === 'all@all') {
|
|
2379
|
+
for (const a of availableAgents) {
|
|
2380
|
+
if (listInstalledVersions(a).length > 0) {
|
|
2381
|
+
targets.push(`${a}@all`);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
else {
|
|
2386
|
+
targets.push(t);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2213
2389
|
const addVersionTarget = (agentId, version) => {
|
|
2214
2390
|
const versions = versionSelections.get(agentId) || [];
|
|
2215
2391
|
if (!versions.includes(version)) {
|
|
@@ -2229,7 +2405,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2229
2405
|
continue;
|
|
2230
2406
|
}
|
|
2231
2407
|
if (atIndex !== -1 && !versionToken) {
|
|
2232
|
-
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@
|
|
2408
|
+
throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
|
|
2233
2409
|
}
|
|
2234
2410
|
const agentId = resolveAgentName(agentToken);
|
|
2235
2411
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -2262,11 +2438,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2262
2438
|
addVersionTarget(agentId, defaultVersion);
|
|
2263
2439
|
continue;
|
|
2264
2440
|
}
|
|
2441
|
+
if (versionToken === 'all') {
|
|
2442
|
+
if (installedVersions.length === 0) {
|
|
2443
|
+
throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
|
|
2444
|
+
}
|
|
2445
|
+
for (const version of installedVersions) {
|
|
2446
|
+
addVersionTarget(agentId, version);
|
|
2447
|
+
}
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2265
2450
|
if (installedVersions.length === 0) {
|
|
2266
2451
|
throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
|
|
2267
2452
|
}
|
|
2268
2453
|
if (!installedVersions.includes(versionToken)) {
|
|
2269
|
-
throw new
|
|
2454
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
2270
2455
|
}
|
|
2271
2456
|
addVersionTarget(agentId, versionToken);
|
|
2272
2457
|
}
|
|
@@ -2320,9 +2505,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2320
2505
|
const defaultVer = getGlobalDefault(agentId);
|
|
2321
2506
|
if (versions.length === 0)
|
|
2322
2507
|
return `${AGENTS[agentId].name} ${chalk.gray('(not installed)')}`;
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2508
|
+
// Surface the version count when there's more than one — mirrors the new
|
|
2509
|
+
// `--agents <agent>@all` syntax so users can see at a glance how many
|
|
2510
|
+
// versions `@all` would target before the per-version prompt fires.
|
|
2511
|
+
const detail = versions.length > 1
|
|
2512
|
+
? (defaultVer
|
|
2513
|
+
? `active: ${defaultVer}, ${versions.length} versions installed`
|
|
2514
|
+
: `${versions.length} versions installed`)
|
|
2515
|
+
: (defaultVer ?? versions[0]);
|
|
2516
|
+
return `${AGENTS[agentId].name} ${chalk.gray(`(${detail})`)}`;
|
|
2326
2517
|
};
|
|
2327
2518
|
let selectedAgents;
|
|
2328
2519
|
if (options.skipPrompts) {
|
|
@@ -2337,6 +2528,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2337
2528
|
}
|
|
2338
2529
|
}
|
|
2339
2530
|
else {
|
|
2531
|
+
// Non-TTY without an explicit --agents value used to silently fall through
|
|
2532
|
+
// to default-picking inside the caller. That's surprising in scripts — fail
|
|
2533
|
+
// loud and point at the new `--agents` syntax instead.
|
|
2534
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
2535
|
+
throw new Error('Non-interactive shell: cannot prompt for agent/version selection.\n' +
|
|
2536
|
+
'Pass --agents explicitly. Examples:\n' +
|
|
2537
|
+
' --agents claude (default version)\n' +
|
|
2538
|
+
' --agents claude@all (every installed Claude version)\n' +
|
|
2539
|
+
' --agents claude@2.1.141 (a specific version)\n' +
|
|
2540
|
+
' --agents all (every installed version of every capable agent)\n' +
|
|
2541
|
+
'Or pass --yes to auto-pick defaults.');
|
|
2542
|
+
}
|
|
2340
2543
|
// Prompt for agent selection
|
|
2341
2544
|
const checkboxResult = await checkbox({
|
|
2342
2545
|
message: 'Which agents should receive these resources?',
|
|
@@ -2371,7 +2574,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2371
2574
|
const versionResult = await checkbox({
|
|
2372
2575
|
message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
|
|
2373
2576
|
choices: [
|
|
2374
|
-
{ name: chalk.bold(
|
|
2577
|
+
{ name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
|
|
2375
2578
|
...versions.map((v) => {
|
|
2376
2579
|
const base = v === defaultVer ? `${v} (default)` : v;
|
|
2377
2580
|
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.3",
|
|
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",
|
|
@@ -78,8 +79,8 @@
|
|
|
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",
|
|
@@ -88,14 +89,14 @@
|
|
|
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
|
+
"@types/node": "25.9.1",
|
|
97
98
|
"tsx": "4.22.3",
|
|
98
|
-
"typescript": "
|
|
99
|
+
"typescript": "6.0.3",
|
|
99
100
|
"vitest": "4.1.6"
|
|
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();
|
package/scripts/postinstall.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import * as readline from 'readline';
|
|
10
|
+
import { spawnSync } from 'child_process';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
12
|
|
|
12
13
|
const HOME = os.homedir();
|
|
@@ -14,6 +15,16 @@ const SHIMS_DIR = path.join(HOME, '.agents', '.cache', 'shims');
|
|
|
14
15
|
const SYSTEM_DIR = path.join(HOME, '.agents-system');
|
|
15
16
|
const USER_DIR = path.join(HOME, '.agents');
|
|
16
17
|
const AGENTS_BIN = fileURLToPath(new URL('../dist/index.js', import.meta.url));
|
|
18
|
+
const INSTALL_HELPER_SCRIPT = fileURLToPath(new URL('./install-helper.js', import.meta.url));
|
|
19
|
+
|
|
20
|
+
function installKeychainHelper() {
|
|
21
|
+
if (process.platform !== 'darwin') return;
|
|
22
|
+
if (!fs.existsSync(INSTALL_HELPER_SCRIPT)) return;
|
|
23
|
+
// Sub-process so a hard failure (codesign / spctl missing on a weird host)
|
|
24
|
+
// can't take down the rest of postinstall. install-helper.js stays silent
|
|
25
|
+
// on no-op and emits one stdout line on success.
|
|
26
|
+
spawnSync(process.execPath, [INSTALL_HELPER_SCRIPT], { stdio: 'inherit' });
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
function shellQuote(value) {
|
|
19
30
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -24,6 +35,7 @@ const isGlobalInstall = process.env.npm_config_global || process.argv.includes('
|
|
|
24
35
|
if (!isGlobalInstall) {
|
|
25
36
|
// Still create user directories for local installs
|
|
26
37
|
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
38
|
+
installKeychainHelper();
|
|
27
39
|
console.log(`
|
|
28
40
|
agents-cli installed locally.
|
|
29
41
|
To complete setup, run: npx agents setup
|
|
@@ -36,6 +48,10 @@ fs.mkdirSync(SHIMS_DIR, { recursive: true });
|
|
|
36
48
|
fs.mkdirSync(SYSTEM_DIR, { recursive: true });
|
|
37
49
|
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
38
50
|
|
|
51
|
+
// Copy the signed macOS Keychain helper to a stable user path so its trusted-app
|
|
52
|
+
// ACLs survive future npm publishes (which re-sign the bundle).
|
|
53
|
+
installKeychainHelper();
|
|
54
|
+
|
|
39
55
|
// One-shot idempotent migrations
|
|
40
56
|
function runMigrations() {
|
|
41
57
|
// 1. Move agents.yaml from system to user repo
|
|
Binary file
|