@phnx-labs/agents-cli 1.18.6 → 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 (104) 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.js +1 -1
  97. package/dist/lib/types.d.ts +4 -3
  98. package/dist/lib/types.js +0 -2
  99. package/dist/lib/versions.js +65 -40
  100. package/dist/lib/workflows.d.ts +7 -0
  101. package/dist/lib/workflows.js +42 -1
  102. package/npm-shrinkwrap.json +3256 -0
  103. package/package.json +32 -26
  104. package/scripts/postinstall.js +8 -2
@@ -27,6 +27,7 @@ export interface AgentConfig {
27
27
  commandsSubdir: string;
28
28
  skillsDir: string;
29
29
  hooksDir: string;
30
+ pluginManifestDir?: string;
30
31
  instructionsFile: string;
31
32
  format: 'markdown' | 'toml';
32
33
  variableSyntax: string;
@@ -109,6 +110,8 @@ export interface ManifestHook {
109
110
  agents?: AgentId[];
110
111
  /** Set to false in user hooks.yaml to disable a system-shipped hook. */
111
112
  enabled?: boolean;
113
+ /** Set true on user hooks that intentionally shadow system-shipped hooks. */
114
+ override?: boolean;
112
115
  /** Optional pre-filter predicates evaluated before invoking the script. */
113
116
  matches?: HookMatches;
114
117
  }
@@ -187,8 +190,6 @@ export interface RepoInfo {
187
190
  }
188
191
  /** Canonical system repo cloned into ~/.agents-system/. */
189
192
  export declare const DEFAULT_SYSTEM_REPO = "gh:phnx-labs/.agents-system";
190
- /** Legacy system repo — kept so existing installs still recognize their origin. */
191
- export declare const MIRROR_SYSTEM_REPO = "gh:muqsitnawaz/.agents-system";
192
193
  /** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
193
194
  export declare function systemRepoSlug(repo?: string): string;
194
195
  /** Kind of package that can be searched and installed from a registry. */
@@ -465,7 +466,7 @@ export interface BrowserProfileConfig {
465
466
  };
466
467
  /** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
467
468
  logDir?: string;
468
- /** Optional SSH host where logDir lives, e.g. "muqsit@mac-mini". */
469
+ /** Optional SSH host where logDir lives, e.g. "user@mac-mini". */
469
470
  logHost?: string;
470
471
  }
471
472
  /** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
package/dist/lib/types.js CHANGED
@@ -7,8 +7,6 @@
7
7
  */
8
8
  /** Canonical system repo cloned into ~/.agents-system/. */
9
9
  export const DEFAULT_SYSTEM_REPO = 'gh:phnx-labs/.agents-system';
10
- /** Legacy system repo — kept so existing installs still recognize their origin. */
11
- export const MIRROR_SYSTEM_REPO = 'gh:muqsitnawaz/.agents-system';
12
10
  /** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
13
11
  export function systemRepoSlug(repo = DEFAULT_SYSTEM_REPO) {
14
12
  return repo.replace(/^gh:/, '').replace(/\.git$/, '');
@@ -50,7 +50,39 @@ const RULES_DOC_FILENAME = 'README.md';
50
50
  // at parse time so it can't reach an exec/shell boundary or get interpolated
51
51
  // into a generated bash alias. Must allow "latest" plus npm-dist-tag /
52
52
  // semver-shaped values (digits, dots, dashes, +, _).
53
- const VERSION_RE = /^(?:latest|[A-Za-z0-9._+-]{1,64})$/;
53
+ const VERSION_RE = /^(?:latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
54
+ function getResourceBases(cwd) {
55
+ const projectAgentsDir = getProjectAgentsDir(cwd);
56
+ const userBase = getUserAgentsDir();
57
+ const systemBase = getAgentsDir();
58
+ const resourceBases = [];
59
+ if (projectAgentsDir) {
60
+ resourceBases.push({ scope: 'project', base: projectAgentsDir });
61
+ }
62
+ resourceBases.push({ scope: 'user', base: userBase });
63
+ resourceBases.push({ scope: 'user', base: systemBase });
64
+ for (const extra of getEnabledExtraRepos()) {
65
+ resourceBases.push({ scope: 'user', base: extra.dir });
66
+ }
67
+ return resourceBases;
68
+ }
69
+ function getScopedMcpResources(cwd) {
70
+ const resources = new Map();
71
+ for (const { base, scope } of getResourceBases(cwd)) {
72
+ const mcpDir = path.join(base, 'mcp');
73
+ if (!fs.existsSync(mcpDir))
74
+ continue;
75
+ const files = fs.readdirSync(mcpDir)
76
+ .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
77
+ for (const file of files) {
78
+ const config = parseMcpServerConfig(path.join(mcpDir, file));
79
+ if (config?.name && !resources.has(config.name)) {
80
+ resources.set(config.name, { name: config.name, scope });
81
+ }
82
+ }
83
+ }
84
+ return Array.from(resources.values());
85
+ }
54
86
  /**
55
87
  * Get all available resources from ~/.agents/.
56
88
  */
@@ -68,19 +100,7 @@ export function getAvailableResources(cwd = process.cwd()) {
68
100
  promptcuts: false,
69
101
  };
70
102
  const projectAgentsDir = getProjectAgentsDir(cwd);
71
- const userBase = getUserAgentsDir();
72
- const systemBase = getAgentsDir();
73
- const resourceBases = [];
74
- if (projectAgentsDir) {
75
- resourceBases.push({ scope: 'project', base: projectAgentsDir });
76
- }
77
- resourceBases.push({ scope: 'user', base: userBase });
78
- resourceBases.push({ scope: 'user', base: systemBase });
79
- // Extra DotAgent repos registered via `agents repo add`. Ordered last so
80
- // project/user/system names win on collision.
81
- for (const extra of getEnabledExtraRepos()) {
82
- resourceBases.push({ scope: 'user', base: extra.dir });
83
- }
103
+ const resourceBases = getResourceBases(cwd);
84
104
  // Commands (*.md files)
85
105
  const commandNames = new Set();
86
106
  for (const { base } of resourceBases) {
@@ -169,21 +189,7 @@ export function getAvailableResources(cwd = process.cwd()) {
169
189
  }
170
190
  }
171
191
  result.memory = Array.from(presetNames);
172
- // MCP servers (*.yaml files) use the `name:` field inside, not filename
173
- const mcpNames = new Set();
174
- for (const { base } of resourceBases) {
175
- const mcpDir = path.join(base, 'mcp');
176
- if (!fs.existsSync(mcpDir))
177
- continue;
178
- const files = fs.readdirSync(mcpDir)
179
- .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
180
- for (const file of files) {
181
- const config = parseMcpServerConfig(path.join(mcpDir, file));
182
- if (config?.name)
183
- mcpNames.add(config.name);
184
- }
185
- }
186
- result.mcp = Array.from(mcpNames);
192
+ result.mcp = getScopedMcpResources(cwd).map(resource => resource.name);
187
193
  // Permission groups (from permissions/groups/*.yaml)
188
194
  const permissionNames = new Set();
189
195
  for (const { base } of resourceBases) {
@@ -839,6 +845,9 @@ export async function promptResourceSelection(agent) {
839
845
  */
840
846
  export function parseAgentSpec(spec) {
841
847
  const parts = spec.split('@');
848
+ if (parts.length > 2) {
849
+ return null;
850
+ }
842
851
  const agentName = parts[0].toLowerCase();
843
852
  const version = parts[1] || 'latest';
844
853
  if (!AGENTS[agentName]) {
@@ -984,6 +993,12 @@ export async function installVersion(agent, version, onProgress) {
984
993
  if (!agentConfig.npmPackage) {
985
994
  return { success: false, installedVersion: version, error: 'Agent has no npm package' };
986
995
  }
996
+ // Validate before deriving filesystem paths or npm package specs. The CLI
997
+ // parser already enforces this for user input; this guard protects direct
998
+ // callers and tests the critical install path at the source.
999
+ if (!VERSION_RE.test(version)) {
1000
+ throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1001
+ }
987
1002
  ensureAgentsDir();
988
1003
  const versionDir = getVersionDir(agent, version);
989
1004
  // Create version directory and isolated home
@@ -1000,12 +1015,6 @@ export async function installVersion(agent, version, onProgress) {
1000
1015
  const packageSpec = version === 'latest'
1001
1016
  ? agentConfig.npmPackage
1002
1017
  : `${agentConfig.npmPackage}@${version}`;
1003
- // Defense-in-depth: even if a future caller bypasses parseAgentSpec, the
1004
- // version string never reaches /bin/sh because we use execFile (argv form)
1005
- // and re-validate here.
1006
- if (version !== 'latest' && !VERSION_RE.test(version)) {
1007
- throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1008
- }
1009
1018
  try {
1010
1019
  // Check npm is available
1011
1020
  try {
@@ -1261,10 +1270,17 @@ export function getProjectVersion(agent, startPath) {
1261
1270
  const parsed = yaml.parse(content);
1262
1271
  const version = parsed?.agents?.[agent];
1263
1272
  if (typeof version === 'string' && version.trim()) {
1264
- return version.trim();
1273
+ const normalized = version.trim();
1274
+ if (!VERSION_RE.test(normalized)) {
1275
+ throw new Error(`Invalid version in agents.yaml for ${agent}: ${normalized}. Allowed: latest or [A-Za-z0-9._+-]{1,64}`);
1276
+ }
1277
+ return normalized;
1265
1278
  }
1266
1279
  }
1267
- catch {
1280
+ catch (err) {
1281
+ if (err instanceof Error && err.message.startsWith('Invalid version in agents.yaml')) {
1282
+ throw err;
1283
+ }
1268
1284
  // Ignore parsing errors
1269
1285
  }
1270
1286
  }
@@ -1520,9 +1536,9 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1520
1536
  const permMap = new Map(available.permissions.map(n => [n, 'system']));
1521
1537
  patternSelection.permissions = expandPatterns(vr.permissions, permMap);
1522
1538
  }
1523
- // mcp: all declared servers are 'user' source.
1539
+ // mcp: pattern matching must preserve project vs user scope.
1524
1540
  if (Array.isArray(vr.mcp) && vr.mcp.length > 0) {
1525
- const mcpMap = new Map(available.mcp.map(n => [n, 'user']));
1541
+ const mcpMap = new Map(getScopedMcpResources(cwd).map(resource => [resource.name, resource.scope]));
1526
1542
  patternSelection.mcp = expandPatterns(vr.mcp, mcpMap);
1527
1543
  }
1528
1544
  // plugins: treat all as 'user' source for now.
@@ -1649,6 +1665,15 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1649
1665
  : available.skills;
1650
1666
  if (skillsToSync.length > 0) {
1651
1667
  const skillsTarget = path.join(agentDir, 'skills');
1668
+ // Old version homes may have skills -> ~/.agents/skills. Replace the
1669
+ // parent symlink before touching children so removePath(destDir) cannot
1670
+ // delete the central source through it.
1671
+ try {
1672
+ if (fs.lstatSync(skillsTarget).isSymbolicLink()) {
1673
+ removePath(skillsTarget);
1674
+ }
1675
+ }
1676
+ catch { /* target does not exist yet */ }
1652
1677
  fs.mkdirSync(skillsTarget, { recursive: true });
1653
1678
  const syncedSkills = [];
1654
1679
  for (const skill of skillsToSync) {
@@ -1855,7 +1880,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1855
1880
  const plugin = pluginMap.get(name);
1856
1881
  if (!plugin || !pluginSupportsAgent(plugin, agent))
1857
1882
  continue;
1858
- const pluginResult = syncPluginToVersion(plugin, agent, versionHome);
1883
+ const pluginResult = syncPluginToVersion(plugin, agent, versionHome, { version });
1859
1884
  if (pluginResult.success) {
1860
1885
  result.plugins.push(name);
1861
1886
  }
@@ -43,6 +43,13 @@ export interface InstalledWorkflow {
43
43
  export declare function parseWorkflowFrontmatter(workflowDir: string): WorkflowFrontmatter | null;
44
44
  /** Count subagent .md files in a workflow's subagents/ directory. */
45
45
  export declare function countWorkflowSubagents(workflowDir: string): number;
46
+ /**
47
+ * Resolve an `agents run <workflow>` reference.
48
+ *
49
+ * Directories are accepted anywhere on disk when they contain WORKFLOW.md.
50
+ * Name lookup keeps the normal resource precedence: project > user > system > extras.
51
+ */
52
+ export declare function resolveWorkflowRef(ref: string, cwd?: string): string | null;
46
53
  /**
47
54
  * Discover all workflow directories (those containing WORKFLOW.md) in a local path.
48
55
  * Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,
@@ -8,7 +8,7 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import * as yaml from 'yaml';
11
- import { getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, } from './state.js';
11
+ import { getProjectAgentsDir, getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, } from './state.js';
12
12
  import { listInstalledVersions, getVersionHomePath } from './versions.js';
13
13
  /** Agents that support running workflows via `agents run`. */
14
14
  export const WORKFLOW_CAPABLE_AGENTS = ['claude'];
@@ -56,6 +56,47 @@ export function countWorkflowSubagents(workflowDir) {
56
56
  return 0;
57
57
  }
58
58
  }
59
+ function expandWorkflowPath(ref) {
60
+ if (ref === '~')
61
+ return process.env.HOME ?? ref;
62
+ if (ref.startsWith('~/')) {
63
+ const home = process.env.HOME;
64
+ return home ? path.join(home, ref.slice(2)) : ref;
65
+ }
66
+ return ref;
67
+ }
68
+ function isWorkflowDir(dir) {
69
+ return fs.existsSync(path.join(dir, 'WORKFLOW.md'));
70
+ }
71
+ function resolveWorkflowPath(ref, cwd) {
72
+ const expanded = expandWorkflowPath(ref);
73
+ const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
74
+ return isWorkflowDir(candidate) ? candidate : null;
75
+ }
76
+ /**
77
+ * Resolve an `agents run <workflow>` reference.
78
+ *
79
+ * Directories are accepted anywhere on disk when they contain WORKFLOW.md.
80
+ * Name lookup keeps the normal resource precedence: project > user > system > extras.
81
+ */
82
+ export function resolveWorkflowRef(ref, cwd = process.cwd()) {
83
+ const direct = resolveWorkflowPath(ref, cwd);
84
+ if (direct)
85
+ return direct;
86
+ const projectAgentsDir = getProjectAgentsDir(cwd);
87
+ const searchDirs = [
88
+ ...(projectAgentsDir ? [path.join(projectAgentsDir, 'workflows')] : []),
89
+ getUserWorkflowsDir(),
90
+ getSystemWorkflowsDir(),
91
+ ...getEnabledExtraRepos().map(r => path.join(r.dir, 'workflows')),
92
+ ];
93
+ for (const dir of searchDirs) {
94
+ const workflowPath = path.join(dir, ref);
95
+ if (isWorkflowDir(workflowPath))
96
+ return workflowPath;
97
+ }
98
+ return null;
99
+ }
59
100
  /**
60
101
  * Discover all workflow directories (those containing WORKFLOW.md) in a local path.
61
102
  * Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,