@phnx-labs/agents-cli 1.18.5 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/README.md +22 -20
  3. package/dist/commands/browser.js +25 -2
  4. package/dist/commands/cloud.js +3 -3
  5. package/dist/commands/computer.d.ts +6 -0
  6. package/dist/commands/computer.js +477 -0
  7. package/dist/commands/doctor.js +19 -17
  8. package/dist/commands/exec.js +37 -59
  9. package/dist/commands/factory.js +12 -5
  10. package/dist/commands/import.js +6 -1
  11. package/dist/commands/mcp.js +9 -4
  12. package/dist/commands/packages.d.ts +3 -0
  13. package/dist/commands/packages.js +20 -12
  14. package/dist/commands/permissions.d.ts +2 -0
  15. package/dist/commands/permissions.js +20 -1
  16. package/dist/commands/plugins.d.ts +2 -0
  17. package/dist/commands/plugins.js +23 -4
  18. package/dist/commands/profiles.js +1 -1
  19. package/dist/commands/pty.js +126 -112
  20. package/dist/commands/pull.js +29 -25
  21. package/dist/commands/repo.js +24 -26
  22. package/dist/commands/routines.js +29 -26
  23. package/dist/commands/secrets.js +66 -73
  24. package/dist/commands/sessions-tail.js +21 -22
  25. package/dist/commands/sessions.js +36 -68
  26. package/dist/commands/setup.js +20 -24
  27. package/dist/commands/teams.js +30 -39
  28. package/dist/commands/versions.js +60 -68
  29. package/dist/commands/worktree.d.ts +20 -0
  30. package/dist/commands/worktree.js +242 -0
  31. package/dist/computer.d.ts +2 -0
  32. package/dist/computer.js +7 -0
  33. package/dist/index.js +70 -26
  34. package/dist/lib/agents.d.ts +4 -1
  35. package/dist/lib/agents.js +23 -5
  36. package/dist/lib/browser/cdp.d.ts +15 -1
  37. package/dist/lib/browser/cdp.js +77 -8
  38. package/dist/lib/browser/chrome.js +17 -24
  39. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  40. package/dist/lib/browser/drivers/ssh.js +20 -8
  41. package/dist/lib/browser/ipc.js +38 -5
  42. package/dist/lib/browser/profiles.js +34 -2
  43. package/dist/lib/browser/runtime-state.d.ts +1 -2
  44. package/dist/lib/browser/runtime-state.js +11 -3
  45. package/dist/lib/browser/service.d.ts +5 -0
  46. package/dist/lib/browser/service.js +32 -4
  47. package/dist/lib/browser/types.d.ts +1 -1
  48. package/dist/lib/browser/upload.d.ts +2 -0
  49. package/dist/lib/browser/upload.js +34 -0
  50. package/dist/lib/cloud/rush.d.ts +2 -1
  51. package/dist/lib/cloud/rush.js +28 -9
  52. package/dist/lib/computer-rpc.d.ts +24 -0
  53. package/dist/lib/computer-rpc.js +263 -0
  54. package/dist/lib/daemon.js +7 -7
  55. package/dist/lib/exec.d.ts +2 -1
  56. package/dist/lib/exec.js +3 -2
  57. package/dist/lib/fs-atomic.d.ts +18 -0
  58. package/dist/lib/fs-atomic.js +76 -0
  59. package/dist/lib/git.js +2 -4
  60. package/dist/lib/help.d.ts +15 -0
  61. package/dist/lib/help.js +41 -0
  62. package/dist/lib/hooks/match.d.ts +1 -0
  63. package/dist/lib/hooks/match.js +57 -12
  64. package/dist/lib/hooks.d.ts +1 -0
  65. package/dist/lib/hooks.js +27 -10
  66. package/dist/lib/import.d.ts +1 -0
  67. package/dist/lib/import.js +7 -0
  68. package/dist/lib/manifest.js +27 -1
  69. package/dist/lib/mcp.d.ts +14 -0
  70. package/dist/lib/mcp.js +79 -14
  71. package/dist/lib/migrate.js +3 -3
  72. package/dist/lib/models.js +3 -1
  73. package/dist/lib/permissions.d.ts +5 -0
  74. package/dist/lib/permissions.js +35 -0
  75. package/dist/lib/plugin-marketplace.d.ts +3 -1
  76. package/dist/lib/plugin-marketplace.js +36 -1
  77. package/dist/lib/plugins.d.ts +19 -1
  78. package/dist/lib/plugins.js +99 -8
  79. package/dist/lib/redact.d.ts +4 -0
  80. package/dist/lib/redact.js +18 -0
  81. package/dist/lib/registry.d.ts +2 -0
  82. package/dist/lib/registry.js +15 -0
  83. package/dist/lib/sandbox.js +15 -5
  84. package/dist/lib/secrets/bundles.d.ts +7 -12
  85. package/dist/lib/secrets/bundles.js +45 -29
  86. package/dist/lib/secrets/index.js +4 -4
  87. package/dist/lib/session/cloud.d.ts +2 -0
  88. package/dist/lib/session/cloud.js +34 -6
  89. package/dist/lib/session/parse.js +7 -2
  90. package/dist/lib/session/render.d.ts +4 -1
  91. package/dist/lib/session/render.js +81 -35
  92. package/dist/lib/shims.d.ts +5 -2
  93. package/dist/lib/shims.js +29 -7
  94. package/dist/lib/state.d.ts +5 -5
  95. package/dist/lib/state.js +43 -13
  96. package/dist/lib/teams/agents.d.ts +1 -1
  97. package/dist/lib/teams/agents.js +2 -2
  98. package/dist/lib/types.d.ts +4 -3
  99. package/dist/lib/types.js +0 -2
  100. package/dist/lib/versions.js +65 -40
  101. package/dist/lib/workflows.d.ts +7 -0
  102. package/dist/lib/workflows.js +42 -1
  103. package/npm-shrinkwrap.json +3256 -0
  104. package/package.json +32 -26
  105. package/scripts/postinstall.js +8 -2
@@ -8,21 +8,12 @@
8
8
  import chalk from 'chalk';
9
9
  import { buildExecCommand, parseExecEnv, execAgent, runWithFallback, AGENT_COMMANDS, } from '../lib/exec.js';
10
10
  import { profileExists, resolveProfileForRun } from '../lib/profiles.js';
11
- import { getSystemAgentsDir, getUserAgentsDir } from '../lib/state.js';
12
- /** Resolve a workflow by name. User repo wins over system repo. Returns the workflow dir or null. */
13
- function resolveWorkflow(name) {
14
- for (const base of [getUserAgentsDir(), getSystemAgentsDir()]) {
15
- const dir = path.join(base, 'workflows', name);
16
- if (fs.existsSync(path.join(dir, 'WORKFLOW.md')))
17
- return dir;
18
- }
19
- return null;
20
- }
11
+ import { setHelpSections } from '../lib/help.js';
21
12
  import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
22
13
  import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
23
14
  import { getGlobalDefault, getVersionHomePath, resolveVersionAlias } from '../lib/versions.js';
24
15
  import { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion } from '../lib/plugins.js';
25
- import { parseWorkflowFrontmatter } from '../lib/workflows.js';
16
+ import { parseWorkflowFrontmatter, resolveWorkflowRef } from '../lib/workflows.js';
26
17
  import * as fs from 'fs';
27
18
  import * as path from 'path';
28
19
  const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
@@ -39,7 +30,7 @@ function formatRotationBanner(result, verb = 'balanced') {
39
30
  }
40
31
  /** Register the `agents run <agent> [prompt]` command. */
41
32
  export function registerRunCommand(program) {
42
- program
33
+ const runCmd = program
43
34
  .command('run <agent> [prompt]')
44
35
  .description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively.')
45
36
  .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), full (writes + all permissions)', 'plan')
@@ -60,62 +51,50 @@ export function registerRunCommand(program) {
60
51
  .option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,gemini). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
61
52
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
62
53
  .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then pinned. (Legacy `rotate` accepted as alias for `balanced`.)')
63
- .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for gemini, claude (via @zed-industries/claude-code-acp adapter). Unified event stream; emits ndjson when --json.')
64
- .addHelpText('after', `
65
- Modes:
66
- With a prompt -> headless (pipes output, no TTY, exits when the agent finishes).
67
- Without a prompt -> interactive (launches the agent's TUI; stdio is fully inherited).
68
-
69
- Run strategy:
70
- pinned Use the workspace/global pinned version from agents.yaml.
71
- available Use the pinned version if it has usage available; otherwise switch
72
- to another signed-in version with usage available.
73
- balanced Distribute traffic across healthy accounts using weighted random
74
- by remaining capacity — fresher accounts get more, near-exhausted
75
- ones get less. Avoids bursting any single account.
76
- Configure with run.<agent>.strategy in agents.yaml, or override with
77
- --strategy. --balanced is kept as a shortcut for --strategy balanced.
78
- Legacy "rotate" is accepted as an alias for "balanced".
79
- Ignored when @version is pinned, when a profile is used, or with --fallback.
80
-
81
- Examples:
82
- # Interactive with the pinned default version
83
- agents run claude
84
-
85
- # Interactive, distribute load across healthy accounts
86
- agents run claude --strategy balanced
54
+ .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for gemini, claude (via @zed-industries/claude-code-acp adapter). Unified event stream; emits ndjson when --json.');
55
+ setHelpSections(runCmd, {
56
+ examples: `
57
+ # Headless, read-only: investigate or summarize without writing files
58
+ agents run claude "summarize recent git commits" --mode plan
87
59
 
88
- # Headless, switch away from the pinned version when usage is unavailable
89
- agents run claude "summarize recent git commits" --mode plan --strategy available
60
+ # Headless, can edit: have the agent make changes
61
+ agents run claude "fix lint errors in src/" --mode edit
90
62
 
91
- # Pin a specific version (rotation ignored)
92
- agents run codex@0.116.0 "fix linting errors in src/" --mode edit
63
+ # Interactive (TUI) with the pinned default version
64
+ agents run claude
93
65
 
94
- # Full autonomy with maximum reasoning for a complex task
95
- agents run claude "refactor auth to use JWT" --mode full --effort max
66
+ # Pipe JSON events to a parser (--quiet drops the preamble)
67
+ agents run claude "..." --json --quiet | jq
96
68
 
97
- # Resume a previous conversation to continue work
98
- agents run claude "now add rate limiting" --session-id a1b2c3d4 --mode edit
69
+ # Bounded run kill the agent after 30 minutes
70
+ agents run claude "generate sales report for yesterday" --mode plan --timeout 30m
99
71
 
100
- # Automated cron job: generate daily report with 10-minute timeout
101
- agents run claude "generate sales report for yesterday" --mode plan --timeout 10m --json > report.jsonl
72
+ # Inject a keychain-backed secrets bundle
73
+ agents run claude "deploy the worker" --secrets prod --mode edit
74
+ `,
75
+ notes: `
76
+ Modes:
77
+ plan read-only edit can write files full writes + all permissions
102
78
 
103
- # Auto-fallback to codex then gemini if claude hits a rate limit
104
- agents run claude "refactor auth module" --mode edit --fallback codex,gemini
79
+ Run strategy (set via --strategy or run.<agent>.strategy in agents.yaml):
80
+ pinned use the workspace/global pinned version (default)
81
+ available use pinned if usage available; otherwise switch to another signed-in version
82
+ balanced distribute load across healthy accounts by remaining capacity
83
+ --balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
105
84
 
106
- # Inject a named secrets bundle (keychain-backed)
107
- agents run claude "charge a test card" --secrets prod-stripe
85
+ Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
108
86
 
109
- # Pin fallback versions: primary claude@2.0.65, fallback codex@0.116.0 then gemini
110
- agents run claude@2.0.65 "deep refactor" --fallback codex@0.116.0,gemini
111
- `)
112
- .action(async (agentSpec, prompt, options) => {
87
+ Resume: --session-id <id> continues a prior Claude conversation.
88
+ `,
89
+ });
90
+ runCmd.action(async (agentSpec, prompt, options) => {
113
91
  // Parse agent@version
114
92
  const [rawAgent, rawVersion] = agentSpec.split('@');
115
93
  let agent;
116
94
  let version = rawVersion || undefined;
117
95
  let profileEnv;
118
96
  let fromProfile = false;
97
+ const cwd = options.cwd ?? process.cwd();
119
98
  if (isValidAgent(rawAgent)) {
120
99
  agent = rawAgent;
121
100
  }
@@ -138,13 +117,13 @@ Examples:
138
117
  process.exit(1);
139
118
  }
140
119
  }
141
- else if (resolveWorkflow(rawAgent)) {
142
- // Workflow: ~/.agents-system/workflows/<name>/ or ~/.agents/workflows/<name>/
143
- // Resolution: user repo wins over system repo (same precedence as all resources).
120
+ else if (resolveWorkflowRef(rawAgent, cwd)) {
121
+ // Workflow: explicit directory, project .agents/workflows/<name>, user, system, or extra repo.
122
+ // Resolution follows resource precedence: direct path, then project > user > system > extras.
144
123
  // Structure:
145
124
  // WORKFLOW.md ← orchestrator instructions fed to claude as system prompt
146
125
  // subagents/*.md ← flat .md files copied to ~/.claude/agents/ for Agent tool discovery
147
- const workflowDir = resolveWorkflow(rawAgent);
126
+ const workflowDir = resolveWorkflowRef(rawAgent, cwd);
148
127
  agent = 'claude';
149
128
  const resolvedVersion = resolveVersionAlias('claude', version);
150
129
  const versionHome = getVersionHomePath('claude', resolvedVersion ?? getGlobalDefault('claude') ?? '');
@@ -221,7 +200,6 @@ Examples:
221
200
  process.exit(1);
222
201
  }
223
202
  version = resolveVersionAlias(agent, version);
224
- const cwd = options.cwd ?? process.cwd();
225
203
  const configuredStrategy = getConfiguredRunStrategy(agent, cwd);
226
204
  const explicitStrategy = options.strategy ? normalizeRunStrategy(options.strategy) : null;
227
205
  if (options.strategy && !explicitStrategy) {
@@ -4,11 +4,17 @@ import * as path from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
6
6
  import { insertTask } from '../lib/cloud/store.js';
7
- const FACTORY_URL = process.env.FACTORY_FLOOR_URL ?? 'https://agents.427yosemite.com';
8
7
  function die(msg, code = 1) {
9
8
  console.error(chalk.red(msg));
10
9
  process.exit(code);
11
10
  }
11
+ function requireFactoryUrl() {
12
+ const url = process.env.FACTORY_FLOOR_URL;
13
+ if (!url) {
14
+ die('FACTORY_FLOOR_URL is not set. Point it at your Software Factory endpoint.');
15
+ }
16
+ return url;
17
+ }
12
18
  function readRushToken() {
13
19
  const userYaml = path.join(homedir(), '.rush', 'user.yaml');
14
20
  if (!fs.existsSync(userYaml)) {
@@ -22,8 +28,9 @@ function readRushToken() {
22
28
  return match[1].replace(/^['"]|['"]$/g, '');
23
29
  }
24
30
  async function postFactorySubmit(ref) {
31
+ const factoryUrl = requireFactoryUrl();
25
32
  const token = readRushToken();
26
- const res = await fetch(`${FACTORY_URL}/factory/submit`, {
33
+ const res = await fetch(`${factoryUrl}/factory/submit`, {
27
34
  method: 'POST',
28
35
  headers: {
29
36
  'Authorization': `Bearer ${token}`,
@@ -44,8 +51,8 @@ export function registerFactoryCommands(program) {
44
51
  .description('Software Factory -- submit Linear tickets to the cloud orchestrator.')
45
52
  .addHelpText('after', `
46
53
  Examples:
47
- agents factory submit RUSH-2451
48
- agents factory submit https://linear.app/getrush/issue/RUSH-2451
54
+ agents factory submit EXAMPLE-2451
55
+ agents factory submit https://linear.app/example/issue/EXAMPLE-2451
49
56
  `);
50
57
  factory.hook('preAction', () => {
51
58
  if (enabled)
@@ -56,7 +63,7 @@ Examples:
56
63
  });
57
64
  factory
58
65
  .command('submit <linear-ref>')
59
- .description('Submit a Linear issue (RUSH-123 or URL) to the Software Factory.')
66
+ .description('Submit a Linear issue (EXAMPLE-123 or URL) to the Software Factory.')
60
67
  .option('--json', 'Output machine-readable JSON')
61
68
  .action(async (ref, opts) => {
62
69
  const result = await postFactorySubmit(ref);
@@ -28,7 +28,7 @@ import { confirm } from '@inquirer/prompts';
28
28
  import { ALL_AGENT_IDS } from '../lib/agents.js';
29
29
  import { AGENTS, getCliPath, getCliVersion, agentLabel } from '../lib/agents.js';
30
30
  import { getVersionDir } from '../lib/versions.js';
31
- import { finalizeImport, importAgentBinary, importAgentConfig, resolvePackageDirFromBinary, } from '../lib/import.js';
31
+ import { finalizeImport, importAgentBinary, importAgentConfig, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
32
32
  import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
33
33
  function isValidAgentId(value) {
34
34
  return ALL_AGENT_IDS.includes(value);
@@ -98,6 +98,11 @@ async function runImport(agentArg, opts) {
98
98
  console.error(chalk.gray('Pass --version <version> explicitly.'));
99
99
  process.exit(1);
100
100
  }
101
+ if (!isValidImportVersion(version)) {
102
+ console.error(chalk.red(`Invalid version: ${version}`));
103
+ console.error(chalk.gray('Version must be "latest" or 1-64 letters, numbers, dots, underscores, plus signs, or hyphens.'));
104
+ process.exit(1);
105
+ }
101
106
  const versionDir = getVersionDir(agentId, version);
102
107
  console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
103
108
  console.log(` from: ${chalk.gray(globalPath)}`);
@@ -428,7 +428,7 @@ Examples:
428
428
  });
429
429
  mcpCmd
430
430
  .command('register [name]')
431
- .description('Apply MCP servers from manifest to agent config files (stdio only for now)')
431
+ .description('Apply MCP servers from manifest to agent config files')
432
432
  .option('-a, --agents <list>', 'Override manifest targets: claude, codex@0.116.0')
433
433
  .addHelpText('after', `
434
434
  Examples:
@@ -459,19 +459,24 @@ Examples:
459
459
  return;
460
460
  }
461
461
  for (const [mcpName, config] of entries) {
462
- if (config.transport === 'http' || !config.command) {
463
- console.log(`\n ${chalk.cyan(mcpName)}: ${chalk.yellow('HTTP transport not yet supported')}`);
462
+ const transport = config.transport || 'stdio';
463
+ const commandOrUrl = transport === 'http' ? config.url : config.command;
464
+ if (!commandOrUrl) {
465
+ console.log(`\n ${chalk.cyan(mcpName)}: ${chalk.yellow(`missing ${transport === 'http' ? 'url' : 'command'}`)}`);
464
466
  continue;
465
467
  }
466
468
  console.log(`\n ${chalk.cyan(mcpName)}:`);
467
469
  const targets = options.agents
468
470
  ? resolveInstalledAgentTargets(options.agents, MCP_CAPABLE_AGENTS)
469
471
  : resolveConfiguredAgentTargets(config.agents, config.agentVersions, MCP_CAPABLE_AGENTS);
470
- const results = await registerMcpToTargets(targets, mcpName, config.command, config.scope || 'user', config.transport || 'stdio');
472
+ const results = await registerMcpToTargets(targets, mcpName, commandOrUrl, config.scope || 'user', transport, { headers: config.headers });
471
473
  for (const result of results) {
472
474
  if (result.success) {
473
475
  console.log(` ${chalk.green('+')} ${formatTargetLabel(result.agentId, result.version)}`);
474
476
  }
477
+ else if (result.error?.startsWith('skipped:')) {
478
+ console.log(` ${chalk.yellow('-')} ${formatTargetLabel(result.agentId, result.version)}: ${result.error}`);
479
+ }
475
480
  else {
476
481
  console.log(` ${chalk.red('x')} ${formatTargetLabel(result.agentId, result.version)}: ${result.error}`);
477
482
  }
@@ -6,5 +6,8 @@
6
6
  * hooks from configured registries or GitHub sources.
7
7
  */
8
8
  import type { Command } from 'commander';
9
+ import type { McpPackage } from '../lib/types.js';
10
+ import { type McpCommandSpec } from '../lib/mcp.js';
11
+ export declare function buildMcpPackageCommand(pkg: McpPackage): McpCommandSpec;
9
12
  /** Register the `agents registry`, `agents search`, and `agents install` commands. */
10
13
  export declare function registerPackagesCommands(program: Command): void;
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
10
- import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, getAllCliStates, registerMcpToTargets, agentLabel, } from '../lib/agents.js';
10
+ import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, getAllCliStates, agentLabel, } from '../lib/agents.js';
11
11
  import { DEFAULT_REGISTRIES } from '../lib/types.js';
12
- import { getRegistries, setRegistry, removeRegistry, search as searchRegistries, resolvePackage, } from '../lib/registry.js';
12
+ import { getRegistries, setRegistry, removeRegistry, search as searchRegistries, resolvePackage, validatedNpmSpec, validatedPyPISpec, } from '../lib/registry.js';
13
13
  import { cloneRepo } from '../lib/git.js';
14
14
  import { discoverCommands, resolveCommandSource, installCommand, installCommandCentrally, } from '../lib/commands.js';
15
15
  import { discoverSkillsFromRepo, installSkill, installSkillCentrally, } from '../lib/skills.js';
@@ -17,6 +17,17 @@ import { discoverHooksFromRepo, installHooks, installHooksCentrally, } from '../
17
17
  import { listInstalledVersions, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
18
18
  import { isInteractiveTerminal, isPromptCancelled, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
19
19
  import { itemPicker } from '../lib/picker.js';
20
+ import { registerMcpCommandToTargets } from '../lib/mcp.js';
21
+ export function buildMcpPackageCommand(pkg) {
22
+ const packageName = pkg.name || pkg.registry_name;
23
+ if (pkg.runtime === 'node') {
24
+ return { command: 'npx', args: ['-y', validatedNpmSpec(packageName)] };
25
+ }
26
+ if (pkg.runtime === 'python') {
27
+ return { command: 'uvx', args: [validatedPyPISpec(packageName)] };
28
+ }
29
+ throw new Error(`Unsupported MCP runtime: ${pkg.runtime}. Supported: node, python.`);
30
+ }
20
31
  /**
21
32
  * Picker fallback for `registry enable/config [name]`.
22
33
  * Returns the picked name, or null if the user cancels. In non-TTY shells,
@@ -378,16 +389,13 @@ When to use:
378
389
  console.log(` ${arg.name}${req}: ${arg.description || ''}`);
379
390
  }
380
391
  }
381
- // Determine command based on runtime
382
- let command;
383
- if (pkg.runtime === 'node') {
384
- command = `npx -y ${pkg.name || pkg.registry_name}`;
392
+ let commandSpec;
393
+ try {
394
+ commandSpec = buildMcpPackageCommand(pkg);
385
395
  }
386
- else if (pkg.runtime === 'python') {
387
- command = `uvx ${pkg.name || pkg.registry_name}`;
388
- }
389
- else {
390
- command = pkg.name || pkg.registry_name;
396
+ catch (err) {
397
+ console.log(chalk.red(err.message));
398
+ process.exit(1);
391
399
  }
392
400
  const cliStates = await getAllCliStates();
393
401
  const installedAgents = MCP_CAPABLE_AGENTS.filter((id) => cliStates[id]?.installed || listInstalledVersions(id).length > 0);
@@ -399,7 +407,7 @@ When to use:
399
407
  process.exit(1);
400
408
  }
401
409
  console.log(chalk.bold('\nInstalling to agents...'));
402
- const results = await registerMcpToTargets(targets, entry.name, command, 'user');
410
+ const results = await registerMcpCommandToTargets(targets, entry.name, commandSpec, 'user');
403
411
  for (const result of results) {
404
412
  const label = result.version ? `${agentLabel(result.agentId)}@${result.version}` : agentLabel(result.agentId);
405
413
  if (result.success) {
@@ -7,5 +7,7 @@
7
7
  * semantics and multi-version targeting.
8
8
  */
9
9
  import type { Command } from 'commander';
10
+ import { discoverPermissionsFromRepo } from '../lib/permissions.js';
11
+ export declare function shouldRefuseBroadPermissions(permissions: ReturnType<typeof discoverPermissionsFromRepo>, allowBroadPermissions: boolean): boolean;
10
12
  /** Register the `agents permissions` command tree (list, add, remove, view). */
11
13
  export declare function registerPermissionsCommands(program: Command): void;
@@ -6,10 +6,13 @@ import * as path from 'path';
6
6
  import { confirm, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
- import { PERMISSIONS_CAPABLE_AGENTS, listInstalledPermissions, discoverPermissionsFromRepo, installPermissionSet, removePermissionSet, applyPermissionsToVersion, readAgentPermissions, exportPermissionsFromPath, getDefaultPermissionSet, computePermissionsDiff, mergePermissionSets, saveDefaultPermissionSet, } from '../lib/permissions.js';
9
+ import { PERMISSIONS_CAPABLE_AGENTS, listInstalledPermissions, discoverPermissionsFromRepo, installPermissionSet, removePermissionSet, applyPermissionsToVersion, readAgentPermissions, exportPermissionsFromPath, getDefaultPermissionSet, computePermissionsDiff, mergePermissionSets, saveDefaultPermissionSet, containsBroadGrants, } from '../lib/permissions.js';
10
10
  import { listInstalledVersions, getGlobalDefault, getVersionHomePath, promptAgentVersionSelection, resolveAgentVersionTargets, resolveVersionAlias, } from '../lib/versions.js';
11
11
  import { recordVersionResources } from '../lib/state.js';
12
12
  import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, } from './utils.js';
13
+ export function shouldRefuseBroadPermissions(permissions, allowBroadPermissions) {
14
+ return !allowBroadPermissions && permissions.some((perm) => containsBroadGrants(perm.set) !== null);
15
+ }
13
16
  /** Register the `agents permissions` command tree (list, add, remove, view). */
14
17
  export function registerPermissionsCommands(program) {
15
18
  const permissionsCmd = program
@@ -201,6 +204,7 @@ When to use:
201
204
  .option('--names <list>', 'Permission set names from ~/.agents/permissions/ (comma-separated)')
202
205
  .option('--all', 'Apply to all installed versions instead of just defaults')
203
206
  .option('--replace', 'Replace managed permission block instead of merging (drops rules no longer in the set)')
207
+ .option('--allow-broad-permissions', 'Allow permission packs that significantly weaken sandboxing')
204
208
  .option('-y, --yes', 'Skip all prompts and confirmations')
205
209
  .addHelpText('after', `
206
210
  Examples:
@@ -489,6 +493,21 @@ Examples:
489
493
  console.log(` ${chalk.cyan(perm.name)}${desc}`);
490
494
  console.log(` ${chalk.gray(`${perm.set.allow.length} allow, ${perm.set.deny?.length || 0} deny rules`)}`);
491
495
  }
496
+ const broadGrants = permissions
497
+ .map((perm) => ({ name: perm.name, grants: containsBroadGrants(perm.set) }))
498
+ .filter((entry) => entry.grants !== null);
499
+ if (broadGrants.length > 0) {
500
+ console.error(chalk.yellow('\nWARNING: this permission pack contains broad grants that significantly weaken agent sandboxing:'));
501
+ for (const entry of broadGrants) {
502
+ for (const rule of entry.grants.broad) {
503
+ console.error(` ${entry.name}: ${rule}: ${entry.grants.reason}`);
504
+ }
505
+ }
506
+ if (shouldRefuseBroadPermissions(permissions, options.allowBroadPermissions === true)) {
507
+ console.error(chalk.red('Refusing to install. Re-run with --allow-broad-permissions if you trust this permission pack.'));
508
+ process.exit(1);
509
+ }
510
+ }
492
511
  // Confirm installation
493
512
  if (!skipPrompts) {
494
513
  const proceed = await confirm({
@@ -6,5 +6,7 @@
6
6
  * stored in ~/.agents/plugins/.
7
7
  */
8
8
  import type { Command } from 'commander';
9
+ import { type PluginCapabilities } from '../lib/plugins.js';
10
+ export declare function shouldRefusePluginInstall(capabilities: PluginCapabilities, allowExecSurfaces: boolean): boolean;
9
11
  /** Register the `agents plugins` command tree. */
10
12
  export declare function registerPluginsCommands(program: Command): void;
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import chalk from 'chalk';
11
11
  import { input } from '@inquirer/prompts';
12
12
  import { PLUGINS_CAPABLE_AGENTS, agentLabel } from '../lib/agents.js';
13
- import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, } from '../lib/plugins.js';
13
+ import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, hasPluginExecSurfaces, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, syncPluginToVersion, } from '../lib/plugins.js';
14
14
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
15
15
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
16
16
  import { itemPicker } from '../lib/picker.js';
@@ -25,6 +25,9 @@ function formatPath(p) {
25
25
  }
26
26
  return p;
27
27
  }
28
+ export function shouldRefusePluginInstall(capabilities, allowExecSurfaces) {
29
+ return hasPluginExecSurfaces(capabilities) && !allowExecSurfaces;
30
+ }
28
31
  /** Register the `agents plugins` command tree. */
29
32
  export function registerPluginsCommands(program) {
30
33
  const pluginsCmd = program
@@ -405,6 +408,7 @@ Examples:
405
408
  pluginsCmd
406
409
  .command('install <spec>')
407
410
  .description('Install a plugin from a git URL or local path (format: name@source or source)')
411
+ .option('--allow-exec-surfaces', 'Allow installing plugins that ship executable surfaces')
408
412
  .addHelpText('after', `
409
413
  Examples:
410
414
  # Install from a git URL
@@ -416,14 +420,16 @@ Examples:
416
420
  # Named install from a local path
417
421
  agents plugins install rush-toolkit@~/Projects/rush-toolkit
418
422
  `)
419
- .action(async (spec) => {
423
+ .action(async (spec, options) => {
420
424
  console.log(chalk.gray(`Installing plugin from: ${spec}`));
421
425
  let name;
422
426
  let root;
427
+ let capabilities;
423
428
  try {
424
429
  const result = await installPlugin(spec);
425
430
  name = result.name;
426
431
  root = result.root;
432
+ capabilities = result.capabilities;
427
433
  }
428
434
  catch (err) {
429
435
  console.log(chalk.red(`Install failed: ${err.message}`));
@@ -434,6 +440,17 @@ Examples:
434
440
  console.log(chalk.red(`Installed but could not load plugin '${name}'`));
435
441
  process.exit(1);
436
442
  }
443
+ capabilities = inspectPluginCapabilities(root);
444
+ if (shouldRefusePluginInstall(capabilities, options.allowExecSurfaces === true)) {
445
+ const source = parseInstallSpec(spec).source;
446
+ console.error(chalk.red('Install refused: plugin ships executable surfaces:'));
447
+ for (const label of pluginCapabilityLabels(capabilities)) {
448
+ console.error(` ${label}`);
449
+ }
450
+ console.error(`This plugin ships executable surfaces. Re-run with --allow-exec-surfaces if you trust the source: ${source}@HEAD`);
451
+ fs.rmSync(root, { recursive: true, force: true });
452
+ process.exit(1);
453
+ }
437
454
  // Check dependencies
438
455
  const missingDeps = checkPluginDependencies(plugin.manifest);
439
456
  if (missingDeps.length > 0) {
@@ -461,8 +478,10 @@ Examples:
461
478
  const defaultVer = getGlobalDefault(agentId);
462
479
  const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
463
480
  for (const version of targetVersions) {
464
- const syncResult = syncResourcesToVersion(agentId, version, { plugins: [name] });
465
- if (syncResult.plugins.length > 0) {
481
+ const didSync = options.allowExecSurfaces === true
482
+ ? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), { allowExecSurfaces: true }).success
483
+ : syncResourcesToVersion(agentId, version, { plugins: [name] }).plugins.length > 0;
484
+ if (didSync) {
466
485
  console.log(chalk.green(` Synced to ${agentLabel(agentId)}@${version}`));
467
486
  synced++;
468
487
  }
@@ -116,7 +116,7 @@ Examples:
116
116
 
117
117
  # Add MiniMax for SWE-bench style fixes; reuses the same OpenRouter key
118
118
  agents profiles add minimax
119
- agents run minimax "investigate RUSH-2317 and patch the off-by-one in pagination"
119
+ agents run minimax "investigate EXAMPLE-2317 and patch the off-by-one in pagination"
120
120
 
121
121
  # Add DeepSeek for cheap, fast non-reasoning work
122
122
  agents profiles add deepseek