@phnx-labs/agents-cli 1.19.2 → 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 +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- 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 +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- 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 +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- 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 +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- 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 +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- 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/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- 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/npm-shrinkwrap.json +0 -3162
package/dist/lib/versions.js
CHANGED
|
@@ -23,9 +23,9 @@ import { promisify } from 'util';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import * as TOML from 'smol-toml';
|
|
25
25
|
import { checkbox, select } from '@inquirer/prompts';
|
|
26
|
-
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir,
|
|
26
|
+
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
27
27
|
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
28
|
-
import {
|
|
28
|
+
import { listResources } from './resources.js';
|
|
29
29
|
import { AGENTS, getAccountEmail, MCP_CAPABLE_AGENTS, COMMANDS_CAPABLE_AGENTS, getMcpConfigPathForHome, parseMcpConfig, resolveAgentName, formatAgentError } from './agents.js';
|
|
30
30
|
import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
31
31
|
import { installMcpServers, parseMcpServerConfig } from './mcp.js';
|
|
@@ -296,13 +296,21 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
296
296
|
workflows: [],
|
|
297
297
|
promptcuts: false,
|
|
298
298
|
};
|
|
299
|
-
// Commands - check what files exist in version home
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
299
|
+
// Commands - check what files exist in version home.
|
|
300
|
+
// For agent/version pairs that store commands as converted skills (e.g. Codex >= 0.117.0),
|
|
301
|
+
// detect them via the agents_command marker in skills/<name>/SKILL.md — otherwise the
|
|
302
|
+
// diff falsely reports every command as "new" every run and re-prompts on `agents view`.
|
|
303
|
+
if (shouldInstallCommandAsSkill(agent, version)) {
|
|
304
|
+
result.commands = listCommandSkillsInVersion(path.join(configDir));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const commandsDir = path.join(configDir, agentConfig.commandsSubdir);
|
|
308
|
+
if (fs.existsSync(commandsDir)) {
|
|
309
|
+
const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
|
|
310
|
+
result.commands = fs.readdirSync(commandsDir)
|
|
311
|
+
.filter(f => f.endsWith(ext))
|
|
312
|
+
.map(f => f.replace(new RegExp(`\\${ext}$`), ''));
|
|
313
|
+
}
|
|
306
314
|
}
|
|
307
315
|
// Skills - check what directories exist AND content matches central source
|
|
308
316
|
const skillsDir = path.join(configDir, 'skills');
|
|
@@ -503,16 +511,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
503
511
|
}
|
|
504
512
|
return result;
|
|
505
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
|
+
}
|
|
506
619
|
/**
|
|
507
620
|
* Compare available resources with what's ACTUALLY synced to version home.
|
|
508
621
|
* Returns only NEW resources that haven't been synced yet.
|
|
509
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.
|
|
510
628
|
*/
|
|
511
|
-
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
|
+
};
|
|
512
634
|
return {
|
|
513
|
-
commands: available.commands.filter(c => !actuallySynced.commands.includes(c)),
|
|
514
|
-
skills: available.skills.filter(s => !actuallySynced.skills.includes(s)),
|
|
515
|
-
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)),
|
|
516
638
|
// Memory/rules presets are mutually exclusive — only one can be active.
|
|
517
639
|
// If any preset is synced, don't report others as "new".
|
|
518
640
|
memory: actuallySynced.memory.length > 0
|
|
@@ -520,9 +642,9 @@ export function getNewResources(available, actuallySynced) {
|
|
|
520
642
|
: available.memory.filter(m => !actuallySynced.memory.includes(m)),
|
|
521
643
|
mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
|
|
522
644
|
permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
|
|
523
|
-
subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
|
|
524
|
-
plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
|
|
525
|
-
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)),
|
|
526
648
|
// Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
|
|
527
649
|
// directly, so there is never a "new" per-version state to reconcile.
|
|
528
650
|
promptcuts: false,
|
|
@@ -554,10 +676,15 @@ export function hasNewResources(diff, agent, version) {
|
|
|
554
676
|
* Build a summary string of new resources.
|
|
555
677
|
* E.g., "2 commands, 5 permission groups"
|
|
556
678
|
*/
|
|
557
|
-
function buildNewResourcesSummary(newResources, agent) {
|
|
679
|
+
function buildNewResourcesSummary(newResources, agent, version) {
|
|
558
680
|
const agentConfig = AGENTS[agent];
|
|
559
681
|
const parts = [];
|
|
560
|
-
|
|
682
|
+
// Use version-aware gates so Codex >= 0.117.0 (which converts commands to skills) doesn't
|
|
683
|
+
// double-count and so "16 commands" never appears in the summary when commands have
|
|
684
|
+
// already been emitted as skills in the version home.
|
|
685
|
+
const commandsApply = version ? supports(agent, 'commands', version).ok : COMMANDS_CAPABLE_AGENTS.includes(agent);
|
|
686
|
+
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
687
|
+
if (newResources.commands.length > 0 && (commandsApply || commandsAsSkills)) {
|
|
561
688
|
parts.push(`${newResources.commands.length} command${newResources.commands.length === 1 ? '' : 's'}`);
|
|
562
689
|
}
|
|
563
690
|
if (newResources.skills.length > 0) {
|
|
@@ -566,7 +693,7 @@ function buildNewResourcesSummary(newResources, agent) {
|
|
|
566
693
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks) {
|
|
567
694
|
parts.push(`${newResources.hooks.length} hook${newResources.hooks.length === 1 ? '' : 's'}`);
|
|
568
695
|
}
|
|
569
|
-
if (newResources.memory.length > 0 &&
|
|
696
|
+
if (newResources.memory.length > 0 && (commandsApply || commandsAsSkills)) {
|
|
570
697
|
parts.push(`${newResources.memory.length} rule file${newResources.memory.length === 1 ? '' : 's'}`);
|
|
571
698
|
}
|
|
572
699
|
if (newResources.mcp.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
@@ -590,15 +717,21 @@ function buildNewResourcesSummary(newResources, agent) {
|
|
|
590
717
|
* Prompt user to select which NEW resources to sync.
|
|
591
718
|
* Only shows resources that haven't been synced yet.
|
|
592
719
|
*/
|
|
593
|
-
export async function promptNewResourceSelection(agent, newResources) {
|
|
720
|
+
export async function promptNewResourceSelection(agent, newResources, version) {
|
|
594
721
|
const agentConfig = AGENTS[agent];
|
|
595
722
|
const selection = {};
|
|
723
|
+
// Version-aware gates. When version is known, prefer per-version capability checks; the
|
|
724
|
+
// commands branch is allowed when either native commands are supported OR when the
|
|
725
|
+
// version emits commands as converted skills (Codex >= 0.117.0).
|
|
726
|
+
const commandsApply = version ? supports(agent, 'commands', version).ok : COMMANDS_CAPABLE_AGENTS.includes(agent);
|
|
727
|
+
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
728
|
+
const commandsBranch = commandsApply || commandsAsSkills;
|
|
596
729
|
// Get permission group info for display
|
|
597
730
|
const permissionGroups = discoverPermissionGroups();
|
|
598
731
|
const newPermissionGroups = permissionGroups.filter(g => newResources.permissions.includes(g.name));
|
|
599
732
|
const totalNewPermissionRules = newPermissionGroups.reduce((sum, g) => sum + g.ruleCount, 0);
|
|
600
733
|
// Build the summary
|
|
601
|
-
const summary = buildNewResourcesSummary(newResources, agent);
|
|
734
|
+
const summary = buildNewResourcesSummary(newResources, agent, version);
|
|
602
735
|
console.log(chalk.cyan(`\nNew resources available:`));
|
|
603
736
|
console.log(chalk.gray(` ${summary}`));
|
|
604
737
|
// Ask how to handle new resources
|
|
@@ -616,13 +749,13 @@ export async function promptNewResourceSelection(agent, newResources) {
|
|
|
616
749
|
}
|
|
617
750
|
if (action === 'all') {
|
|
618
751
|
// Sync all new resources
|
|
619
|
-
if (newResources.commands.length > 0 &&
|
|
752
|
+
if (newResources.commands.length > 0 && commandsBranch)
|
|
620
753
|
selection.commands = newResources.commands;
|
|
621
754
|
if (newResources.skills.length > 0)
|
|
622
755
|
selection.skills = newResources.skills;
|
|
623
756
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks)
|
|
624
757
|
selection.hooks = newResources.hooks;
|
|
625
|
-
if (newResources.memory.length > 0 &&
|
|
758
|
+
if (newResources.memory.length > 0 && commandsBranch)
|
|
626
759
|
selection.memory = newResources.memory;
|
|
627
760
|
if (newResources.mcp.length > 0 && MCP_CAPABLE_AGENTS.includes(agent))
|
|
628
761
|
selection.mcp = newResources.mcp;
|
|
@@ -637,7 +770,7 @@ export async function promptNewResourceSelection(agent, newResources) {
|
|
|
637
770
|
return selection;
|
|
638
771
|
}
|
|
639
772
|
// Select specific items for each category
|
|
640
|
-
if (newResources.commands.length > 0 &&
|
|
773
|
+
if (newResources.commands.length > 0 && commandsBranch) {
|
|
641
774
|
const selected = await checkbox({
|
|
642
775
|
message: 'Select new commands to sync:',
|
|
643
776
|
choices: newResources.commands.map(c => ({ name: c, value: c, checked: true })),
|
|
@@ -661,7 +794,7 @@ export async function promptNewResourceSelection(agent, newResources) {
|
|
|
661
794
|
if (selected.length > 0)
|
|
662
795
|
selection.hooks = selected;
|
|
663
796
|
}
|
|
664
|
-
if (newResources.memory.length > 0 &&
|
|
797
|
+
if (newResources.memory.length > 0 && commandsBranch) {
|
|
665
798
|
const selected = await checkbox({
|
|
666
799
|
message: 'Select new rule files to sync:',
|
|
667
800
|
choices: newResources.memory.map(m => ({ name: m, value: m, checked: true })),
|
|
@@ -873,8 +1006,26 @@ export function getVersionDir(agent, version) {
|
|
|
873
1006
|
* Get the binary path for a specific agent version.
|
|
874
1007
|
*/
|
|
875
1008
|
export function getBinaryPath(agent, version) {
|
|
876
|
-
const versionDir = getVersionDir(agent, version);
|
|
877
1009
|
const agentConfig = AGENTS[agent];
|
|
1010
|
+
if (agent === 'grok') {
|
|
1011
|
+
// Grok binaries live in the global ~/.grok/downloads, not per-version node_modules.
|
|
1012
|
+
// We return a best-effort path (used for display / checks). Real resolution
|
|
1013
|
+
// happens in agents.ts resolveGrokBinary + the generated shims.
|
|
1014
|
+
const grokDownloads = path.join(os.homedir(), '.grok', 'downloads');
|
|
1015
|
+
// Best effort: first matching file for this version
|
|
1016
|
+
try {
|
|
1017
|
+
const entries = fs.readdirSync(grokDownloads);
|
|
1018
|
+
const match = entries.find((e) => e.includes(version) && e.startsWith('grok-'));
|
|
1019
|
+
if (match)
|
|
1020
|
+
return path.join(grokDownloads, match);
|
|
1021
|
+
const first = entries.find((e) => e.startsWith('grok-'));
|
|
1022
|
+
if (first)
|
|
1023
|
+
return path.join(grokDownloads, first);
|
|
1024
|
+
}
|
|
1025
|
+
catch { }
|
|
1026
|
+
return path.join(grokDownloads, `grok-${version}`);
|
|
1027
|
+
}
|
|
1028
|
+
const versionDir = getVersionDir(agent, version);
|
|
878
1029
|
return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
|
|
879
1030
|
}
|
|
880
1031
|
/**
|
|
@@ -940,7 +1091,7 @@ export function listInstalledVersions(agent) {
|
|
|
940
1091
|
* List every version directory for an agent, including ones missing the
|
|
941
1092
|
* binary (typically home-only leftovers from a prior `removeVersion`).
|
|
942
1093
|
*
|
|
943
|
-
* Used by `agents prune` to surface stale installs that the regular
|
|
1094
|
+
* Used by `agents prune cleanup` to surface stale installs that the regular
|
|
944
1095
|
* `listInstalledVersions` filters out. Do NOT use elsewhere — every other
|
|
945
1096
|
* call site assumes a working binary.
|
|
946
1097
|
*/
|
|
@@ -990,21 +1141,51 @@ export function setGlobalDefault(agent, version) {
|
|
|
990
1141
|
*/
|
|
991
1142
|
export async function installVersion(agent, version, onProgress) {
|
|
992
1143
|
const agentConfig = AGENTS[agent];
|
|
993
|
-
if (!agentConfig.npmPackage) {
|
|
994
|
-
return { success: false, installedVersion: version, error: 'Agent has no npm package' };
|
|
995
|
-
}
|
|
996
1144
|
// Validate before deriving filesystem paths or npm package specs. The CLI
|
|
997
1145
|
// parser already enforces this for user input; this guard protects direct
|
|
998
1146
|
// callers and tests the critical install path at the source.
|
|
999
1147
|
if (!VERSION_RE.test(version)) {
|
|
1000
1148
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
1001
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
|
+
}
|
|
1002
1183
|
ensureAgentsDir();
|
|
1003
1184
|
const versionDir = getVersionDir(agent, version);
|
|
1004
1185
|
// Create version directory and isolated home
|
|
1005
1186
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1006
1187
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1007
|
-
// Initialize package.json
|
|
1188
|
+
// Initialize package.json (only for real npm agents)
|
|
1008
1189
|
const packageJson = {
|
|
1009
1190
|
name: `agents-${agent}-${version}`,
|
|
1010
1191
|
version: '1.0.0',
|
|
@@ -1147,8 +1328,6 @@ export function removeVersion(agent, version) {
|
|
|
1147
1328
|
}
|
|
1148
1329
|
// Remove versioned alias (e.g., claude@2.0.65)
|
|
1149
1330
|
removeVersionedAlias(agent, version);
|
|
1150
|
-
// Clear resource tracking for this version
|
|
1151
|
-
clearVersionResources(agent, version);
|
|
1152
1331
|
// Clear default if it was the removed version - user must explicitly pick a new one
|
|
1153
1332
|
if (getGlobalDefault(agent) === version) {
|
|
1154
1333
|
const meta = readMeta();
|
|
@@ -1175,6 +1354,23 @@ export function removeVersion(agent, version) {
|
|
|
1175
1354
|
emit('version.remove', { agent, version });
|
|
1176
1355
|
return true;
|
|
1177
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Print the standard footer after one or more versions were soft-deleted to
|
|
1359
|
+
* trash. Reminds the user that sessions stay readable and how to restore.
|
|
1360
|
+
*/
|
|
1361
|
+
export function printTrashFooter(moved) {
|
|
1362
|
+
if (moved.length === 0)
|
|
1363
|
+
return;
|
|
1364
|
+
console.log();
|
|
1365
|
+
console.log(chalk.gray('Sessions remain accessible via `agents sessions`.'));
|
|
1366
|
+
if (moved.length === 1) {
|
|
1367
|
+
const { agent, version } = moved[0];
|
|
1368
|
+
console.log(chalk.gray(`Restore with: agents trash restore ${agent}@${version}`));
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
console.log(chalk.gray('Restore with: agents trash restore <agent>@<version> (run `agents trash list` to see)'));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1178
1374
|
/**
|
|
1179
1375
|
* Remove all versions of an agent. Preserves each version's `home/` directory
|
|
1180
1376
|
* so conversation history is never deleted; the per-version folders (now
|
|
@@ -1320,6 +1516,18 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1320
1516
|
return version;
|
|
1321
1517
|
}
|
|
1322
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
|
+
}
|
|
1323
1531
|
/**
|
|
1324
1532
|
* Get the diff between central resources (~/.agents/) and what's synced to a version.
|
|
1325
1533
|
* Uses filesystem state - no tracking needed.
|
|
@@ -1558,12 +1766,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1558
1766
|
}
|
|
1559
1767
|
}
|
|
1560
1768
|
}
|
|
1561
|
-
// Fast guard: skip the entire sync when
|
|
1562
|
-
// has changed since the last full sync.
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
|
|
1566
|
-
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) {
|
|
1567
1774
|
const manifest = loadManifest(agent, version);
|
|
1568
1775
|
if (manifest && !isStale(manifest, agent, version, cwd)) {
|
|
1569
1776
|
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
|
|
@@ -1626,13 +1833,25 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1626
1833
|
}
|
|
1627
1834
|
const syncedCommands = [];
|
|
1628
1835
|
for (const cmd of commandsToSync) {
|
|
1629
|
-
|
|
1630
|
-
|
|
1836
|
+
// Commands are content that gets injected into the agent's prompt
|
|
1837
|
+
// surface (slash commands, skill bodies). We intentionally do NOT pull
|
|
1838
|
+
// from the project's own .agents/commands/ directory: a cloned public
|
|
1839
|
+
// repo could ship a command whose body instructs the agent to do
|
|
1840
|
+
// something harmful the next time the user invokes it. Commands must
|
|
1841
|
+
// come from the user's central ~/.agents/commands/, the system layer,
|
|
1842
|
+
// or an explicitly enabled extra repo. Same defense as hooks below.
|
|
1843
|
+
const candidates = [
|
|
1844
|
+
safeJoin(path.join(userAgentsDir, 'commands'), `${cmd}.md`),
|
|
1845
|
+
safeJoin(getCommandsDir(), `${cmd}.md`),
|
|
1846
|
+
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'commands'), `${cmd}.md`)),
|
|
1847
|
+
];
|
|
1848
|
+
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
1849
|
+
if (!srcFile)
|
|
1631
1850
|
continue;
|
|
1632
|
-
const srcFile = resolved.path;
|
|
1633
1851
|
if (commandsAsSkills) {
|
|
1852
|
+
// Project skills dir is intentionally excluded for the same reason
|
|
1853
|
+
// commands are: the body of a project skill becomes agent context.
|
|
1634
1854
|
const skillSourceDirs = [
|
|
1635
|
-
projectAgentsDir ? path.join(projectAgentsDir, 'skills') : null,
|
|
1636
1855
|
path.join(userAgentsDir, 'skills'),
|
|
1637
1856
|
getSkillsDir(),
|
|
1638
1857
|
...extraRepos.map((e) => path.join(e.dir, 'skills')),
|
|
@@ -1653,6 +1872,30 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1653
1872
|
}
|
|
1654
1873
|
result.commands = syncedCommands.length > 0;
|
|
1655
1874
|
}
|
|
1875
|
+
// Orphan-sweep stale top-level command files from previous syncs under a
|
|
1876
|
+
// different cwd. Only runs in "full sync" mode — i.e. when the caller did
|
|
1877
|
+
// not pass an explicit `selection`. Callers that pass explicit selections
|
|
1878
|
+
// are using the incremental/additive API (sync exactly these; leave others
|
|
1879
|
+
// alone), so the sweep would be a contract violation there. The
|
|
1880
|
+
// cross-project leak always comes from the no-selection shim auto-sync at
|
|
1881
|
+
// launch.
|
|
1882
|
+
if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
|
|
1883
|
+
const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1884
|
+
if (fs.existsSync(commandsTargetSweep)) {
|
|
1885
|
+
const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
|
|
1886
|
+
const trustedCommands = new Set(commandsToSync);
|
|
1887
|
+
for (const entry of fs.readdirSync(commandsTargetSweep, { withFileTypes: true })) {
|
|
1888
|
+
if (!entry.isFile() || entry.name.startsWith('.'))
|
|
1889
|
+
continue;
|
|
1890
|
+
if (!entry.name.endsWith(ext))
|
|
1891
|
+
continue;
|
|
1892
|
+
const name = entry.name.slice(0, -ext.length);
|
|
1893
|
+
if (!trustedCommands.has(name)) {
|
|
1894
|
+
removePath(safeJoin(commandsTargetSweep, entry.name));
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1656
1899
|
// Sync skills (skip if agent natively reads ~/.agents/skills/)
|
|
1657
1900
|
if (agentConfig.nativeAgentsSkillsDir) {
|
|
1658
1901
|
// Clean up stale skills symlink/dir — agent reads from ~/.agents/skills/ directly
|
|
@@ -1677,10 +1920,19 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1677
1920
|
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
1678
1921
|
const syncedSkills = [];
|
|
1679
1922
|
for (const skill of skillsToSync) {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1923
|
+
// Same defense as commands and hooks: don't pull skills from the
|
|
1924
|
+
// project's .agents/skills/ directory. A skill's contents (SKILL.md
|
|
1925
|
+
// and any auxiliary scripts) get loaded into the agent's tool/context
|
|
1926
|
+
// surface, and a malicious public repo could ship a SKILL.md whose
|
|
1927
|
+
// body coerces the agent. Trusted layers only.
|
|
1928
|
+
const skillCandidates = [
|
|
1929
|
+
safeJoin(path.join(userAgentsDir, 'skills'), skill),
|
|
1930
|
+
safeJoin(getSkillsDir(), skill),
|
|
1931
|
+
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'skills'), skill)),
|
|
1932
|
+
];
|
|
1933
|
+
const srcDir = skillCandidates.find((p) => fs.existsSync(p) &&
|
|
1934
|
+
!fs.lstatSync(p).isSymbolicLink() &&
|
|
1935
|
+
fs.lstatSync(p).isDirectory()) || null;
|
|
1684
1936
|
if (!srcDir)
|
|
1685
1937
|
continue;
|
|
1686
1938
|
const destDir = safeJoin(skillsTarget, skill);
|
|
@@ -1690,6 +1942,21 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1690
1942
|
}
|
|
1691
1943
|
result.skills = syncedSkills.length > 0;
|
|
1692
1944
|
}
|
|
1945
|
+
// Orphan-sweep stale skill directories from previous syncs under a
|
|
1946
|
+
// different cwd. Only runs in "full sync" mode (no explicit selection) —
|
|
1947
|
+
// see the matching guard on the commands sweep above for why. Skip
|
|
1948
|
+
// dot-dirs to keep plugin-managed subtrees (.plugins/, .promptcuts) intact.
|
|
1949
|
+
const skillsTargetSweep = path.join(agentDir, 'skills');
|
|
1950
|
+
if (!userPassedSelection && fs.existsSync(skillsTargetSweep) && !fs.lstatSync(skillsTargetSweep).isSymbolicLink()) {
|
|
1951
|
+
const trustedSkills = new Set(skillsToSync);
|
|
1952
|
+
for (const entry of fs.readdirSync(skillsTargetSweep, { withFileTypes: true })) {
|
|
1953
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
1954
|
+
continue;
|
|
1955
|
+
if (!trustedSkills.has(entry.name)) {
|
|
1956
|
+
removePath(safeJoin(skillsTargetSweep, entry.name));
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1693
1960
|
}
|
|
1694
1961
|
// Sync hooks (if agent supports them at this version)
|
|
1695
1962
|
const hooksGate = supports(agent, 'hooks', version);
|
|
@@ -1742,7 +2009,9 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1742
2009
|
result.hooks = syncedHooks.length > 0;
|
|
1743
2010
|
// Register hooks into agent-native settings.json/hooks.json. Gemini
|
|
1744
2011
|
// shipped hooks in 0.26.0; gate already passed above so this is safe.
|
|
1745
|
-
|
|
2012
|
+
// Grok auto-discovers from ~/.grok/hooks/ so the script copy above
|
|
2013
|
+
// is sufficient — no settings.json registration needed.
|
|
2014
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity') {
|
|
1746
2015
|
registerHooksToSettings(agent, versionHome);
|
|
1747
2016
|
}
|
|
1748
2017
|
}
|
|
@@ -1781,6 +2050,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1781
2050
|
// ~/.agents/permissions/presets/<name>.yaml pick a subset via `includes:`.
|
|
1782
2051
|
// If AGENTS_PERMISSION_PRESET is set, we resolve that recipe and use its
|
|
1783
2052
|
// includes list as the group filter (intersected with groups on disk).
|
|
2053
|
+
// Note: discoverPermissionGroups intentionally reads from user + system
|
|
2054
|
+
// only — never from a project's .agents/permissions/. Permissions gate
|
|
2055
|
+
// every other action, so a cloned public repo must not be able to widen
|
|
2056
|
+
// its own sandbox by shipping a permissions group. Same defense as hooks.
|
|
1784
2057
|
const permissionGroups = discoverPermissionGroups();
|
|
1785
2058
|
const allGroupNames = permissionGroups.map(g => g.name);
|
|
1786
2059
|
const activePresetName = getActivePermissionPresetName();
|
|
@@ -1825,15 +2098,30 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1825
2098
|
// Install MCP servers (if agent supports them)
|
|
1826
2099
|
// For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
|
|
1827
2100
|
// For others: edits config files directly
|
|
1828
|
-
|
|
2101
|
+
//
|
|
2102
|
+
// Mirror the hooks defense: exclude project-scoped MCPs from the sync. An
|
|
2103
|
+
// MCP server is an executable invoked under the agent's authority, so a
|
|
2104
|
+
// cloned public repo's .agents/mcp/foo.yaml could install an arbitrary
|
|
2105
|
+
// command. We pre-compute the set of project-scoped names and drop them
|
|
2106
|
+
// before handing the list to installMcpServers. (The deeper helper-side
|
|
2107
|
+
// dedup in lib/mcp.ts still lets a project entry shadow a same-named
|
|
2108
|
+
// user entry, so name-collision shadowing is not fully closed here —
|
|
2109
|
+
// tracked separately for a follow-up in lib/mcp.ts.)
|
|
2110
|
+
const projectScopedMcpNames = new Set(getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
|
|
2111
|
+
const mcpToSyncAll = selection
|
|
1829
2112
|
? resolveSelection(selection.mcp, available.mcp)
|
|
1830
2113
|
: (MCP_CAPABLE_AGENTS.includes(agent) ? available.mcp : []);
|
|
2114
|
+
const mcpToSync = mcpToSyncAll.filter(n => !projectScopedMcpNames.has(n));
|
|
1831
2115
|
if (mcpToSync.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
1832
2116
|
const mcpResult = installMcpServers(agent, version, versionHome, mcpToSync, { cwd });
|
|
1833
2117
|
result.mcp = mcpResult.applied;
|
|
1834
2118
|
// mcp patterns already written via ensureVersionResourcePatterns above.
|
|
1835
2119
|
}
|
|
1836
|
-
// Sync subagents (claude and openclaw only)
|
|
2120
|
+
// Sync subagents (claude and openclaw only).
|
|
2121
|
+
// Note: listInstalledSubagents (used to populate the map below) reads only
|
|
2122
|
+
// user + system layers — never project. Subagents bundle prompts that fire
|
|
2123
|
+
// when the agent delegates work, so a cloned public repo must not be able
|
|
2124
|
+
// to plant a subagent the user later invokes. Same defense as hooks.
|
|
1837
2125
|
const subagentsToSync = selection
|
|
1838
2126
|
? resolveSelection(selection.subagents, available.subagents)
|
|
1839
2127
|
: (SUBAGENT_CAPABLE_AGENTS.includes(agent) ? available.subagents : []);
|
|
@@ -1864,6 +2152,29 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1864
2152
|
}
|
|
1865
2153
|
catch { /* resource sync failed for this item */ }
|
|
1866
2154
|
}
|
|
2155
|
+
// Orphan-sweep stale subagents. Same selection-mode guard as the
|
|
2156
|
+
// commands/skills sweeps above. Claude stores them as flat .md files
|
|
2157
|
+
// under `<agentDir>/agents/`; OpenClaw stores them as subdirs at the
|
|
2158
|
+
// same level as commands/skills/hooks/plugins (no isolated parent dir),
|
|
2159
|
+
// which means a directory-readdir sweep would unsafely hit unrelated
|
|
2160
|
+
// resources. For OpenClaw we lean on the existing per-name copy path —
|
|
2161
|
+
// if the user wants strict isolation on OpenClaw, track via manifest.
|
|
2162
|
+
if (!userPassedSelection && agent === 'claude') {
|
|
2163
|
+
const claudeAgentsDir = path.join(agentDir, 'agents');
|
|
2164
|
+
if (fs.existsSync(claudeAgentsDir)) {
|
|
2165
|
+
const trustedSubagents = new Set(subagentsToSync);
|
|
2166
|
+
for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
|
|
2167
|
+
if (!entry.isFile() || entry.name.startsWith('.'))
|
|
2168
|
+
continue;
|
|
2169
|
+
if (!entry.name.endsWith('.md'))
|
|
2170
|
+
continue;
|
|
2171
|
+
const name = entry.name.slice(0, -'.md'.length);
|
|
2172
|
+
if (!trustedSubagents.has(name)) {
|
|
2173
|
+
removePath(safeJoin(claudeAgentsDir, entry.name));
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
1867
2178
|
// subagent patterns already written via ensureVersionResourcePatterns above.
|
|
1868
2179
|
}
|
|
1869
2180
|
// Sync plugins (claude and openclaw)
|
|
@@ -1929,6 +2240,24 @@ export function getEffectiveHome(agentId) {
|
|
|
1929
2240
|
}
|
|
1930
2241
|
return os.homedir();
|
|
1931
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
|
+
}
|
|
1932
2261
|
/**
|
|
1933
2262
|
* Resolve a comma-separated --agents list into concrete version selections.
|
|
1934
2263
|
* Bare agents target the default version, or the newest installed version when no default exists.
|
|
@@ -1938,10 +2267,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
1938
2267
|
const selectedAgents = [];
|
|
1939
2268
|
const versionSelections = new Map();
|
|
1940
2269
|
const explicitSelections = new Set();
|
|
1941
|
-
const
|
|
2270
|
+
const rawTargets = value
|
|
1942
2271
|
.split(',')
|
|
1943
2272
|
.map((item) => item.trim())
|
|
1944
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
|
+
}
|
|
1945
2290
|
for (const target of targets) {
|
|
1946
2291
|
const atIndex = target.indexOf('@');
|
|
1947
2292
|
const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
|
|
@@ -1950,7 +2295,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
1950
2295
|
continue;
|
|
1951
2296
|
}
|
|
1952
2297
|
if (atIndex !== -1 && !versionToken) {
|
|
1953
|
-
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.`);
|
|
1954
2299
|
}
|
|
1955
2300
|
const agentId = resolveAgentName(agentToken);
|
|
1956
2301
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -1990,8 +2335,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
|
|
|
1990
2335
|
explicitSelections.add(agentId);
|
|
1991
2336
|
continue;
|
|
1992
2337
|
}
|
|
2338
|
+
if (versionToken === 'all') {
|
|
2339
|
+
versionSelections.set(agentId, [...installedVersions]);
|
|
2340
|
+
explicitSelections.add(agentId);
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
1993
2343
|
if (!installedVersions.includes(versionToken)) {
|
|
1994
|
-
throw new
|
|
2344
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
1995
2345
|
}
|
|
1996
2346
|
const explicitVersions = explicitSelections.has(agentId)
|
|
1997
2347
|
? (versionSelections.get(agentId) || [])
|
|
@@ -2014,10 +2364,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2014
2364
|
const selectedAgents = [];
|
|
2015
2365
|
const directAgents = [];
|
|
2016
2366
|
const versionSelections = new Map();
|
|
2017
|
-
const
|
|
2367
|
+
const rawTargets = value
|
|
2018
2368
|
.split(',')
|
|
2019
2369
|
.map((item) => item.trim())
|
|
2020
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
|
+
}
|
|
2021
2389
|
const addVersionTarget = (agentId, version) => {
|
|
2022
2390
|
const versions = versionSelections.get(agentId) || [];
|
|
2023
2391
|
if (!versions.includes(version)) {
|
|
@@ -2037,7 +2405,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2037
2405
|
continue;
|
|
2038
2406
|
}
|
|
2039
2407
|
if (atIndex !== -1 && !versionToken) {
|
|
2040
|
-
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.`);
|
|
2041
2409
|
}
|
|
2042
2410
|
const agentId = resolveAgentName(agentToken);
|
|
2043
2411
|
if (!agentId || !availableAgents.includes(agentId)) {
|
|
@@ -2070,11 +2438,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
|
|
|
2070
2438
|
addVersionTarget(agentId, defaultVersion);
|
|
2071
2439
|
continue;
|
|
2072
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
|
+
}
|
|
2073
2450
|
if (installedVersions.length === 0) {
|
|
2074
2451
|
throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
|
|
2075
2452
|
}
|
|
2076
2453
|
if (!installedVersions.includes(versionToken)) {
|
|
2077
|
-
throw new
|
|
2454
|
+
throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
|
|
2078
2455
|
}
|
|
2079
2456
|
addVersionTarget(agentId, versionToken);
|
|
2080
2457
|
}
|
|
@@ -2128,9 +2505,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2128
2505
|
const defaultVer = getGlobalDefault(agentId);
|
|
2129
2506
|
if (versions.length === 0)
|
|
2130
2507
|
return `${AGENTS[agentId].name} ${chalk.gray('(not installed)')}`;
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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})`)}`;
|
|
2134
2517
|
};
|
|
2135
2518
|
let selectedAgents;
|
|
2136
2519
|
if (options.skipPrompts) {
|
|
@@ -2145,6 +2528,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2145
2528
|
}
|
|
2146
2529
|
}
|
|
2147
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
|
+
}
|
|
2148
2543
|
// Prompt for agent selection
|
|
2149
2544
|
const checkboxResult = await checkbox({
|
|
2150
2545
|
message: 'Which agents should receive these resources?',
|
|
@@ -2179,7 +2574,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
|
|
|
2179
2574
|
const versionResult = await checkbox({
|
|
2180
2575
|
message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
|
|
2181
2576
|
choices: [
|
|
2182
|
-
{ name: chalk.bold(
|
|
2577
|
+
{ name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
|
|
2183
2578
|
...versions.map((v) => {
|
|
2184
2579
|
const base = v === defaultVer ? `${v} (default)` : v;
|
|
2185
2580
|
let label = base.padEnd(maxLabelLen);
|