@phnx-labs/agents-cli 1.16.0 → 1.17.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 (60) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/commands/browser.js +248 -9
  3. package/dist/commands/cloud.js +8 -0
  4. package/dist/commands/exec.js +70 -1
  5. package/dist/commands/plugins.js +179 -5
  6. package/dist/commands/prune.js +6 -0
  7. package/dist/commands/secrets.js +117 -19
  8. package/dist/commands/view.js +21 -8
  9. package/dist/commands/workflows.d.ts +10 -0
  10. package/dist/commands/workflows.js +457 -0
  11. package/dist/index.js +31 -16
  12. package/dist/lib/browser/cdp.js +7 -4
  13. package/dist/lib/browser/chrome.d.ts +10 -0
  14. package/dist/lib/browser/chrome.js +37 -2
  15. package/dist/lib/browser/drivers/local.js +13 -2
  16. package/dist/lib/browser/input.d.ts +1 -0
  17. package/dist/lib/browser/input.js +3 -0
  18. package/dist/lib/browser/ipc.js +14 -0
  19. package/dist/lib/browser/profiles.d.ts +5 -0
  20. package/dist/lib/browser/profiles.js +45 -0
  21. package/dist/lib/browser/service.d.ts +10 -0
  22. package/dist/lib/browser/service.js +29 -1
  23. package/dist/lib/browser/types.d.ts +11 -1
  24. package/dist/lib/cloud/rush.d.ts +28 -1
  25. package/dist/lib/cloud/rush.js +68 -13
  26. package/dist/lib/commands.d.ts +0 -15
  27. package/dist/lib/commands.js +5 -5
  28. package/dist/lib/hooks.js +24 -11
  29. package/dist/lib/migrate.js +59 -1
  30. package/dist/lib/permissions.d.ts +0 -58
  31. package/dist/lib/permissions.js +10 -10
  32. package/dist/lib/plugins.d.ts +75 -34
  33. package/dist/lib/plugins.js +640 -133
  34. package/dist/lib/resource-patterns.d.ts +41 -0
  35. package/dist/lib/resource-patterns.js +82 -0
  36. package/dist/lib/resources/index.d.ts +17 -0
  37. package/dist/lib/resources/index.js +7 -0
  38. package/dist/lib/resources/types.d.ts +1 -1
  39. package/dist/lib/resources/workflows.d.ts +24 -0
  40. package/dist/lib/resources/workflows.js +110 -0
  41. package/dist/lib/resources.d.ts +6 -1
  42. package/dist/lib/resources.js +12 -2
  43. package/dist/lib/session/db.d.ts +18 -0
  44. package/dist/lib/session/db.js +106 -7
  45. package/dist/lib/session/discover.d.ts +6 -0
  46. package/dist/lib/session/discover.js +28 -17
  47. package/dist/lib/shims.d.ts +3 -51
  48. package/dist/lib/shims.js +18 -10
  49. package/dist/lib/sqlite.js +10 -4
  50. package/dist/lib/state.d.ts +15 -2
  51. package/dist/lib/state.js +29 -8
  52. package/dist/lib/types.d.ts +43 -14
  53. package/dist/lib/versions.d.ts +3 -0
  54. package/dist/lib/versions.js +139 -27
  55. package/dist/lib/workflows.d.ts +79 -0
  56. package/dist/lib/workflows.js +233 -0
  57. package/package.json +1 -5
  58. package/scripts/postinstall.js +59 -58
  59. package/dist/commands/fork.d.ts +0 -10
  60. package/dist/commands/fork.js +0 -146
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.17.0
4
+
5
+ **Workflows: a new first-class resource**
6
+
7
+ - `agents workflows list / add / remove / view` — WORKFLOW.md bundles (with optional `subagents/`, `skills/`, `plugins/`) install from GitHub or a local path and resolve through the same system → user → project layer model as every other resource.
8
+ - `agents run <name>` resolves a workflow or named subagent as an orchestrator: prepends WORKFLOW.md / AGENT.md body to the prompt, copies `subagents/*` into `~/.claude/agents/` for Agent-tool discovery, and syncs workflow-scoped `skills/` and `plugins/` at run time.
9
+ - `agents view` now has a workflows section.
10
+
11
+ **Browser**
12
+
13
+ - Port-per-profile with auto-allocation and viewport enforcement — concurrent browser profiles no longer collide on CDP ports.
14
+ - `agents browser scroll` plus new `profiles launch`, `profiles doctor`, `profiles prime`, viewport position, and port diagnostics commands.
15
+ - `agents browser profiles list` now shows a description column when any profile has one.
16
+ - `isProcessRunning` treats EPERM as process-alive (fixes false-negative on sandboxed processes).
17
+
18
+ **Cloud dispatch**
19
+
20
+ - `--balanced` strategy and `--upload-account-tokens` flag on cloud dispatch.
21
+ - Remote account API client; `--balanced` skips the client manifest path.
22
+
23
+ **Plugin system extension**
24
+
25
+ - Plugins now ship with `commands/`, `agents/`, `bin/`, MCP configs, settings, and `install` / `update` hooks. Discovery and sync extended end-to-end.
26
+
27
+ **Secrets**
28
+
29
+ - `agents secrets import <bundle> --from-1password` / `export <bundle> --to-1password` with vault picker, skip-empty-fields on import, overwrite-only-with-`--force` on export. Wires the existing 1Password library into the CLI.
30
+
31
+ **Sandbox**
32
+
33
+ - `scripts/sandbox.sh --pr` — author real PRs from a Crabbox-isolated box via a bare-mirror clone off main.
34
+ - `sandbox.sh --linear` and `--post-file` post run output to Linear tickets.
35
+ - Dynamic GitHub App token, `gh` CLI installed, stale git credentials cleaned.
36
+
37
+ **Sessions / SQLite concurrency**
38
+
39
+ - Scan coordinator prevents concurrent session indexing.
40
+ - SQLite concurrency hardened with `BEGIN IMMEDIATE` and ledger recheck on contention.
41
+ - Session discovery uses `getHistoryDir` for version roots and backup paths.
42
+
43
+ **Run / shims / hooks**
44
+
45
+ - Versioned alias shims regenerate on startup if missing.
46
+ - Hooks prefer version-home scripts to prevent path breakage when the source dir moves.
47
+ - Linux: claude shim sources `CLAUDE_CODE_OAUTH_TOKEN` from the per-version `.oauth_token` file when unset.
48
+
49
+ **Resource UI**
50
+
51
+ - `agents view` replaces path columns with OSC 8 hyperlinks for commands, skills, and rules.
52
+ - Flat version resource lists replaced with source-pattern selection.
53
+
54
+ **CI / security**
55
+
56
+ - Gitleaks secret-scanning workflow on every push (switched to the free CLI, no org license needed).
57
+
58
+ **Postinstall**
59
+
60
+ - Correct shims dir, expanded aliases, prints changelog on install.
61
+
62
+ **Dev**
63
+
64
+ - Test isolation via vitest `pool: 'forks'`; mock state paths instead of hitting real `~/.agents/`.
65
+ - Concurrent-writes benchmark for the session indexer.
66
+ - Dead code + phantom deps removed: `src/commands/fork.ts`, `@aws-sdk/client-s3`, `@modelcontextprotocol/sdk`, `semver`.
67
+
3
68
  ## 1.16.0
4
69
 
5
70
  **System-repo sweep: ~/.agents-system reduced to npm-shipped defaults only**
@@ -1,4 +1,8 @@
1
- import { listProfiles, getProfile, createProfile, deleteProfile, } from '../lib/browser/profiles.js';
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { listProfiles, getProfile, createProfile, deleteProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, } from '../lib/browser/profiles.js';
4
+ import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
5
+ import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
2
6
  import { sendIPCRequest } from '../lib/browser/ipc.js';
3
7
  import { browserTaskPicker } from './browser-picker.js';
4
8
  import { isInteractiveTerminal } from './utils.js';
@@ -28,11 +32,23 @@ function registerProfilesCommands(browser) {
28
32
  console.log('Create one with: agents browser profiles create <name> --endpoint <url>');
29
33
  return;
30
34
  }
31
- console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'ENDPOINTS');
32
- console.log('-'.repeat(72));
33
- for (const p of allProfiles) {
34
- const endpoints = p.endpoints.join(', ');
35
- console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints);
35
+ const hasDescriptions = allProfiles.some(p => p.description);
36
+ if (hasDescriptions) {
37
+ console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'DESCRIPTION'.padEnd(38) + 'ENDPOINTS');
38
+ console.log('-'.repeat(92));
39
+ for (const p of allProfiles) {
40
+ const endpoints = p.endpoints.join(', ');
41
+ const desc = (p.description ?? '').slice(0, 36).padEnd(38);
42
+ console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + desc + endpoints);
43
+ }
44
+ }
45
+ else {
46
+ console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'ENDPOINTS');
47
+ console.log('-'.repeat(72));
48
+ for (const p of allProfiles) {
49
+ const endpoints = p.endpoints.join(', ');
50
+ console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints);
51
+ }
36
52
  }
37
53
  });
38
54
  const VALID_BROWSERS = ['chrome', 'comet', 'chromium', 'brave', 'edge'];
@@ -40,10 +56,12 @@ function registerProfilesCommands(browser) {
40
56
  .command('create <name>')
41
57
  .description('Create a new browser profile')
42
58
  .requiredOption('-b, --browser <type>', `Browser type: ${VALID_BROWSERS.join(', ')}`)
43
- .requiredOption('-e, --endpoint <url>', 'CDP endpoint URL (repeatable)', collect, [])
59
+ .option('-e, --endpoint <url>', 'CDP endpoint URL (repeatable; auto-assigned if omitted)', collect, [])
44
60
  .option('-s, --secrets <bundle>', 'Secrets bundle to inject')
45
61
  .option('-d, --description <text>', 'Profile description')
46
62
  .option('--headless', 'Run in headless mode')
63
+ .option('--window <WxH>', 'Window size, e.g. 1512x982')
64
+ .option('--position <X,Y>', 'Window position on screen, e.g. 80,80')
47
65
  .action(async (name, opts) => {
48
66
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
49
67
  console.error('Profile name must be lowercase alphanumeric with hyphens');
@@ -53,13 +71,43 @@ function registerProfilesCommands(browser) {
53
71
  console.error(`Invalid browser type. Must be one of: ${VALID_BROWSERS.join(', ')}`);
54
72
  process.exit(1);
55
73
  }
74
+ // Auto-assign a free port if no endpoint was provided
75
+ let endpoints = opts.endpoint;
76
+ if (endpoints.length === 0) {
77
+ const freePort = await findFreeProfilePort();
78
+ endpoints = [`cdp://127.0.0.1:${freePort}`];
79
+ }
80
+ // Viewport is mandatory — default to 1512x982 if --window is not provided
81
+ let viewport = {
82
+ width: 1512,
83
+ height: 982,
84
+ };
85
+ if (opts.window) {
86
+ const m = String(opts.window).match(/^(\d+)x(\d+)$/);
87
+ if (!m) {
88
+ console.error('--window must be WxH, e.g. 1512x982');
89
+ process.exit(1);
90
+ }
91
+ viewport.width = parseInt(m[1], 10);
92
+ viewport.height = parseInt(m[2], 10);
93
+ }
94
+ if (opts.position) {
95
+ const m = String(opts.position).match(/^(-?\d+),(-?\d+)$/);
96
+ if (!m) {
97
+ console.error('--position must be X,Y, e.g. 80,80');
98
+ process.exit(1);
99
+ }
100
+ viewport.x = parseInt(m[1], 10);
101
+ viewport.y = parseInt(m[2], 10);
102
+ }
56
103
  const profile = {
57
104
  name,
58
105
  description: opts.description,
59
106
  browser: opts.browser,
60
- endpoints: opts.endpoint,
107
+ endpoints,
61
108
  secrets: opts.secrets,
62
109
  chrome: opts.headless ? { headless: true } : undefined,
110
+ viewport,
63
111
  };
64
112
  await createProfile(profile);
65
113
  console.log(`Created profile: ${name}`);
@@ -103,6 +151,172 @@ function registerProfilesCommands(browser) {
103
151
  await deleteProfile(name);
104
152
  console.log(`Deleted profile: ${name}`);
105
153
  });
154
+ profiles
155
+ .command('launch <name>')
156
+ .description('Start (or attach to) the profile\'s browser without creating a task')
157
+ .action(async (name) => {
158
+ const profile = await getProfile(name);
159
+ if (!profile) {
160
+ console.error(`Profile "${name}" not found`);
161
+ process.exit(1);
162
+ }
163
+ const response = await sendIPCRequest({
164
+ action: 'launch-profile',
165
+ profile: name,
166
+ });
167
+ if (!response.ok) {
168
+ console.error(response.error);
169
+ process.exit(1);
170
+ }
171
+ const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
172
+ console.log(`Launched "${name}" on port ${response.port} (${pidLabel})`);
173
+ console.log(`Next: agents browser start --profile ${name} --url <url>`);
174
+ });
175
+ profiles
176
+ .command('doctor <name>')
177
+ .description('Diagnose a browser profile: binary, port, user-data-dir, onboarding state')
178
+ .action(async (name) => {
179
+ const profile = await getProfile(name);
180
+ if (!profile) {
181
+ console.error(`Profile "${name}" not found`);
182
+ process.exit(1);
183
+ }
184
+ const checks = [];
185
+ // 1. Binary exists for declared browser type
186
+ try {
187
+ const binPath = findBrowserPath(profile.browser, profile.binary);
188
+ checks.push({ label: 'binary', ok: true, detail: binPath });
189
+ }
190
+ catch (err) {
191
+ checks.push({
192
+ label: 'binary',
193
+ ok: false,
194
+ detail: err instanceof Error ? err.message : String(err),
195
+ });
196
+ }
197
+ // 2. Configured port: free, or already serving the expected browser?
198
+ const port = extractConfiguredPort(profile);
199
+ let attachingToExistingBrowser = false;
200
+ if (port === undefined) {
201
+ checks.push({ label: 'port', ok: true, detail: 'no port in endpoint' });
202
+ }
203
+ else {
204
+ const occupant = getPortOccupant(port);
205
+ if (!occupant) {
206
+ checks.push({ label: 'port', ok: true, detail: `${port} is free` });
207
+ }
208
+ else {
209
+ try {
210
+ const { browser } = await discoverBrowserWsUrl(port);
211
+ verifyBrowserIdentity(browser, profile.browser, port);
212
+ checks.push({
213
+ label: 'port',
214
+ ok: true,
215
+ detail: `${port} serving ${browser} (pid ${occupant.pid})`,
216
+ });
217
+ attachingToExistingBrowser = true;
218
+ }
219
+ catch (err) {
220
+ const msg = err instanceof Error ? err.message : String(err);
221
+ checks.push({
222
+ label: 'port',
223
+ ok: false,
224
+ detail: `${port} taken by ${occupant.command} (pid ${occupant.pid}) — ${msg}`,
225
+ });
226
+ }
227
+ }
228
+ }
229
+ // 3. User-data-dir exists and is writable
230
+ const userDataDir = path.join(getProfileRuntimeDir(name), 'chrome-data');
231
+ try {
232
+ if (!fs.existsSync(userDataDir)) {
233
+ checks.push({
234
+ label: 'user-data-dir',
235
+ ok: true,
236
+ detail: `will be created at ${userDataDir}`,
237
+ });
238
+ }
239
+ else {
240
+ fs.accessSync(userDataDir, fs.constants.W_OK);
241
+ checks.push({ label: 'user-data-dir', ok: true, detail: userDataDir });
242
+ }
243
+ }
244
+ catch (err) {
245
+ checks.push({
246
+ label: 'user-data-dir',
247
+ ok: false,
248
+ detail: `${userDataDir} not writable: ${err instanceof Error ? err.message : err}`,
249
+ });
250
+ }
251
+ // 4. Onboarding heuristic — only meaningful when WE will launch the
252
+ // browser. When the configured port is already serving a debuggable
253
+ // browser, that browser owns its own user-data-dir and the priming
254
+ // status of our managed dir is irrelevant.
255
+ if (attachingToExistingBrowser) {
256
+ checks.push({
257
+ label: 'onboarding',
258
+ ok: true,
259
+ detail: 'n/a (attaching to existing browser)',
260
+ });
261
+ }
262
+ else {
263
+ const localStatePath = path.join(userDataDir, 'Local State');
264
+ if (fs.existsSync(localStatePath)) {
265
+ const size = fs.statSync(localStatePath).size;
266
+ if (size > 0) {
267
+ checks.push({ label: 'onboarding', ok: true, detail: 'Local State present' });
268
+ }
269
+ else {
270
+ checks.push({
271
+ label: 'onboarding',
272
+ ok: false,
273
+ detail: 'Local State is empty — run `agents browser profiles prime ' + name + '`',
274
+ });
275
+ }
276
+ }
277
+ else {
278
+ checks.push({
279
+ label: 'onboarding',
280
+ ok: false,
281
+ detail: 'Not primed yet — run `agents browser profiles prime ' + name + '`',
282
+ });
283
+ }
284
+ }
285
+ const allOk = checks.every((c) => c.ok);
286
+ for (const c of checks) {
287
+ const marker = c.ok ? 'OK ' : 'FAIL';
288
+ console.log(`${marker} ${c.label.padEnd(15)} ${c.detail}`);
289
+ }
290
+ if (!allOk)
291
+ process.exit(1);
292
+ });
293
+ profiles
294
+ .command('prime <name>')
295
+ .description('Launch the profile so you can complete first-run onboarding interactively')
296
+ .action(async (name) => {
297
+ const profile = await getProfile(name);
298
+ if (!profile) {
299
+ console.error(`Profile "${name}" not found`);
300
+ process.exit(1);
301
+ }
302
+ const response = await sendIPCRequest({
303
+ action: 'launch-profile',
304
+ profile: name,
305
+ });
306
+ if (!response.ok) {
307
+ console.error(response.error);
308
+ process.exit(1);
309
+ }
310
+ const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
311
+ console.log(`Launched "${name}" on port ${response.port} (${pidLabel}).`);
312
+ console.log('');
313
+ console.log('Finish any first-run / onboarding screens in the browser window');
314
+ console.log('(welcome, profile setup, default-browser prompt, sign-in, etc.).');
315
+ console.log('Once you reach a normal browsing surface, this profile is primed');
316
+ console.log('— its user-data-dir persists across runs, so you only do this once.');
317
+ console.log('');
318
+ console.log(`Next: agents browser start --profile ${name} --url <url>`);
319
+ });
106
320
  }
107
321
  function registerTaskCommands(browser) {
108
322
  browser
@@ -370,7 +584,10 @@ function registerTaskCommands(browser) {
370
584
  const portLabel = profile.configuredPort && profile.configuredPort !== profile.port
371
585
  ? `port ${profile.port} (configured ${profile.configuredPort})`
372
586
  : `port ${profile.port}`;
373
- console.log(`\n${profile.name} (${portLabel}, pid ${profile.pid})`);
587
+ // pid 0 means the daemon attached to a browser we didn't launch — no
588
+ // tracked pid. Render it as "attached" rather than the literal 0.
589
+ const pidLabel = profile.pid ? `pid ${profile.pid}` : 'attached';
590
+ console.log(`\n${profile.name} (${portLabel}, ${pidLabel})`);
374
591
  if (profile.tasks.length === 0) {
375
592
  console.log(' No active tasks');
376
593
  }
@@ -588,6 +805,28 @@ function registerTaskCommands(browser) {
588
805
  }
589
806
  console.log('Hovered');
590
807
  });
808
+ browser
809
+ .command('scroll <task> <deltaX> <deltaY>')
810
+ .description('Scroll the page by pixel amount')
811
+ .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
812
+ .option('-x, --at-x <x>', 'X coordinate to dispatch scroll from (default 0)', parseInt)
813
+ .option('-y, --at-y <y>', 'Y coordinate to dispatch scroll from (default 0)', parseInt)
814
+ .action(async (task, deltaX, deltaY, opts) => {
815
+ const response = await sendIPCRequest({
816
+ action: 'scroll',
817
+ task,
818
+ tabId: opts.tab,
819
+ scrollX: parseInt(deltaX, 10),
820
+ scrollY: parseInt(deltaY, 10),
821
+ scrollAtX: opts.atX,
822
+ scrollAtY: opts.atY,
823
+ });
824
+ if (!response.ok) {
825
+ console.error(response.error);
826
+ process.exit(1);
827
+ }
828
+ console.log('Scrolled');
829
+ });
591
830
  // ─── Viewport & Device ───────────────────────────────────────────────────────
592
831
  const setCmd = browser.command('set').description('Set browser emulation options');
593
832
  setCmd
@@ -106,6 +106,9 @@ Examples:
106
106
  .option('--env <id>', 'Codex Cloud environment ID')
107
107
  .option('--computer <name>', 'Factory/Droid computer target')
108
108
  .option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
109
+ .option('-b, --balanced', 'Shortcut for --strategy balanced. Route the factory run across all healthy accounts.')
110
+ .option('--strategy <strategy>', 'Account selection strategy for the factory: balanced. Sends all healthy accounts so the factory pod rotates between them on rate-limit.')
111
+ .option('--upload-account-tokens', 'Upload Claude OAuth credentials to Rush Cloud on first dispatch (consent recorded for future runs).')
109
112
  .option('--json', 'Structured JSON output')
110
113
  .option('--no-follow', 'Dispatch and exit without streaming output')
111
114
  .addHelpText('after', `
@@ -165,6 +168,11 @@ Examples:
165
168
  dispatchOptions.providerOptions.computer = options.computer;
166
169
  if (options.mode)
167
170
  dispatchOptions.providerOptions.mode = options.mode;
171
+ if (options.balanced || options.strategy === 'balanced') {
172
+ dispatchOptions.providerOptions.strategy = 'balanced';
173
+ }
174
+ if (options.uploadAccountTokens)
175
+ dispatchOptions.providerOptions.uploadAccountTokens = true;
168
176
  // Dispatch
169
177
  const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
170
178
  let task;
@@ -8,9 +8,22 @@
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
21
  import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
12
22
  import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
13
- import { resolveVersionAlias } from '../lib/versions.js';
23
+ import { getGlobalDefault, getVersionHomePath, resolveVersionAlias } from '../lib/versions.js';
24
+ import { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion } from '../lib/plugins.js';
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
14
27
  const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
15
28
  /** Type guard that narrows a string to a known AgentId. */
16
29
  function isValidAgent(agent) {
@@ -122,6 +135,62 @@ Examples:
122
135
  process.exit(1);
123
136
  }
124
137
  }
138
+ else if (resolveWorkflow(rawAgent)) {
139
+ // Workflow: ~/.agents-system/workflows/<name>/ or ~/.agents/workflows/<name>/
140
+ // Resolution: user repo wins over system repo (same precedence as all resources).
141
+ // Structure:
142
+ // WORKFLOW.md ← orchestrator instructions fed to claude as system prompt
143
+ // subagents/*.md ← flat .md files copied to ~/.claude/agents/ for Agent tool discovery
144
+ const workflowDir = resolveWorkflow(rawAgent);
145
+ agent = 'claude';
146
+ const resolvedVersion = resolveVersionAlias('claude', version);
147
+ const versionHome = getVersionHomePath('claude', resolvedVersion ?? getGlobalDefault('claude') ?? '');
148
+ const claudeAgentsDir = path.join(versionHome, '.claude', 'agents');
149
+ // Copy subagents/*.md into ~/.claude/agents/ so Claude's Agent tool finds them.
150
+ const subagentsDir = path.join(workflowDir, 'subagents');
151
+ if (fs.existsSync(subagentsDir)) {
152
+ fs.mkdirSync(claudeAgentsDir, { recursive: true });
153
+ for (const file of fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md'))) {
154
+ fs.copyFileSync(path.join(subagentsDir, file), path.join(claudeAgentsDir, file));
155
+ }
156
+ }
157
+ // Feed WORKFLOW.md body (strip frontmatter) as orchestrator system context.
158
+ const workflowMd = path.join(workflowDir, 'WORKFLOW.md');
159
+ const orchestratorBody = fs.existsSync(workflowMd)
160
+ ? fs.readFileSync(workflowMd, 'utf-8').replace(/^---[\s\S]*?---\n/, '').trim()
161
+ : '';
162
+ if (orchestratorBody && prompt !== undefined) {
163
+ prompt = `${orchestratorBody}\n\n---\n\n${prompt}`;
164
+ }
165
+ // Sync workflow-scoped skills into the version home's skills dir.
166
+ const workflowSkillsDir = path.join(workflowDir, 'skills');
167
+ if (fs.existsSync(workflowSkillsDir)) {
168
+ const skillsTarget = path.join(claudeAgentsDir, '..', 'skills');
169
+ fs.mkdirSync(skillsTarget, { recursive: true });
170
+ for (const entry of fs.readdirSync(workflowSkillsDir, { withFileTypes: true })) {
171
+ if (!entry.isDirectory())
172
+ continue;
173
+ fs.cpSync(path.join(workflowSkillsDir, entry.name), path.join(skillsTarget, entry.name), { recursive: true });
174
+ }
175
+ }
176
+ // Sync workflow-scoped plugins into the version home.
177
+ const workflowPluginsDir = path.join(workflowDir, 'plugins');
178
+ if (fs.existsSync(workflowPluginsDir)) {
179
+ for (const entry of fs.readdirSync(workflowPluginsDir, { withFileTypes: true })) {
180
+ if (!entry.isDirectory())
181
+ continue;
182
+ const pluginRoot = path.join(workflowPluginsDir, entry.name);
183
+ const manifest = loadPluginManifest(pluginRoot);
184
+ if (!manifest)
185
+ continue;
186
+ syncPluginToVersion(buildDiscoveredPlugin(pluginRoot, manifest), 'claude', versionHome);
187
+ }
188
+ }
189
+ const subagentCount = fs.existsSync(subagentsDir)
190
+ ? fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md')).length
191
+ : 0;
192
+ process.stderr.write(chalk.gray(`Workflow '${rawAgent}' → claude (${subagentCount} subagents)\n`));
193
+ }
125
194
  else {
126
195
  console.error(chalk.red(`Unknown agent: ${rawAgent}`));
127
196
  console.error(chalk.gray(`Available agents: ${VALID_AGENTS.join(', ')}`));