@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
@@ -0,0 +1,457 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { select, checkbox } from '@inquirer/prompts';
7
+ import { resolveAgentName, agentLabel } from '../lib/agents.js';
8
+ import { cloneRepo } from '../lib/git.js';
9
+ import { WORKFLOW_CAPABLE_AGENTS, discoverWorkflowsFromRepo, installWorkflowCentrally, removeWorkflow, listInstalledWorkflows, listWorkflowsForAgent, removeWorkflowFromVersion, iterWorkflowsCapableVersions, } from '../lib/workflows.js';
10
+ import { getVersionHomePath, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, resolveAgentVersionTargets, } from '../lib/versions.js';
11
+ import { recordVersionResources, getUserWorkflowsDir } from '../lib/state.js';
12
+ import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, printWithPager, promptRemovalTargets, } from './utils.js';
13
+ import { showResourceList, buildTargetsSection, } from './resource-view.js';
14
+ /** Register the `agents workflows` command tree (list, view, add, remove). */
15
+ export function registerWorkflowsCommands(program) {
16
+ const workflowsCmd = program
17
+ .command('workflows')
18
+ .description('Manage multi-agent pipeline workflows (WORKFLOW.md bundles)')
19
+ .addHelpText('after', `
20
+ Workflows are directory bundles (WORKFLOW.md + subagents/) that define multi-agent pipelines run via:
21
+ agents run <workflow-name>
22
+
23
+ Examples:
24
+ # See what workflows are available
25
+ agents workflows list
26
+
27
+ # Install from GitHub
28
+ agents workflows add gh:user/workflows
29
+
30
+ # Install a local workflow directory
31
+ agents workflows add ./rdev
32
+
33
+ # Remove a workflow
34
+ agents workflows remove rdev
35
+ `);
36
+ workflowsCmd
37
+ .command('list [agent]')
38
+ .description('Show installed workflows and which agent versions they are synced to')
39
+ .option('-a, --agent <agent>', 'Filter to a specific agent')
40
+ .action(async (agentArg, options) => {
41
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
42
+ const agentInput = agentArg || options.agent;
43
+ let filterAgent;
44
+ let filterVersion;
45
+ if (agentInput) {
46
+ const parts = agentInput.split('@');
47
+ const resolved = resolveAgentName(parts[0]);
48
+ if (!resolved) {
49
+ spinner.stop();
50
+ console.log(chalk.red(`Unknown agent: ${parts[0]}`));
51
+ process.exit(1);
52
+ }
53
+ filterAgent = resolved;
54
+ filterVersion = parts[1] ? resolveVersionAlias(resolved, parts[1]) : undefined;
55
+ }
56
+ const rows = buildWorkflowRows({ filterAgent, filterVersion });
57
+ spinner.stop();
58
+ await showResourceList({
59
+ resourcePlural: 'workflows',
60
+ resourceSingular: 'workflow',
61
+ extraLabel: 'Agents',
62
+ rows,
63
+ emptyMessage: filterAgent
64
+ ? `No workflows in central storage for ${agentLabel(filterAgent)}.`
65
+ : 'No workflows in ~/.agents/workflows/. Add one with: agents workflows add gh:user/repo',
66
+ centralPath: getUserWorkflowsDir(),
67
+ filterAgent,
68
+ filterVersion,
69
+ });
70
+ });
71
+ workflowsCmd
72
+ .command('add [source]')
73
+ .description('Install workflows from a source (GitHub, local) or pick from central storage')
74
+ .option('-a, --agents <list>', 'Targets: claude, claude@2.1.138')
75
+ .option('-y, --yes', 'Skip confirmation prompts')
76
+ .addHelpText('after', `
77
+ Examples:
78
+ # Install from GitHub
79
+ agents workflows add gh:user/workflows
80
+
81
+ # Install a local workflow directory (must contain WORKFLOW.md)
82
+ agents workflows add ./rdev
83
+
84
+ # Install and sync to a specific version
85
+ agents workflows add gh:user/workflows --agents claude@2.1.138
86
+ `)
87
+ .action(async (source, options) => {
88
+ try {
89
+ let workflows;
90
+ if (!source) {
91
+ // Interactive: pick from central storage
92
+ const installed = listInstalledWorkflows();
93
+ if (installed.size === 0) {
94
+ console.log(chalk.yellow('No workflows in ~/.agents/workflows/'));
95
+ console.log(chalk.gray('\nTo add workflows from a repo:'));
96
+ console.log(chalk.cyan(' agents workflows add gh:user/repo'));
97
+ return;
98
+ }
99
+ if (!isInteractiveTerminal()) {
100
+ requireInteractiveSelection('Selecting workflows', [
101
+ 'agents workflows add gh:user/repo',
102
+ ]);
103
+ }
104
+ const choices = Array.from(installed.values()).map(w => ({
105
+ value: w.name,
106
+ name: w.frontmatter.description
107
+ ? `${w.name} ${chalk.gray(w.frontmatter.description.slice(0, 50))}`
108
+ : w.name,
109
+ }));
110
+ const selected = await checkbox({ message: 'Select workflows to sync', choices });
111
+ if (selected.length === 0) {
112
+ console.log(chalk.gray('No workflows selected.'));
113
+ return;
114
+ }
115
+ workflows = selected.map(name => ({ name, path: installed.get(name).path }));
116
+ }
117
+ else {
118
+ // Fetch from repo or local path
119
+ const spinner = ora('Fetching workflows...').start();
120
+ const isGitRepo = source.startsWith('gh:') || source.startsWith('git:') ||
121
+ source.startsWith('https://') || source.startsWith('http://');
122
+ let localPath;
123
+ if (isGitRepo) {
124
+ const result = await cloneRepo(source);
125
+ localPath = result.localPath;
126
+ spinner.succeed('Repository cloned');
127
+ }
128
+ else {
129
+ localPath = source.startsWith('~')
130
+ ? path.join(os.homedir(), source.slice(1))
131
+ : path.resolve(source);
132
+ if (!fs.existsSync(localPath)) {
133
+ spinner.fail(`Path not found: ${localPath}`);
134
+ return;
135
+ }
136
+ spinner.succeed('Using local path');
137
+ }
138
+ const discovered = discoverWorkflowsFromRepo(localPath);
139
+ if (discovered.length === 0) {
140
+ console.log(chalk.yellow('No workflows found (looking for WORKFLOW.md files)'));
141
+ return;
142
+ }
143
+ console.log(chalk.bold(`\nFound ${discovered.length} workflow(s):`));
144
+ for (const w of discovered) {
145
+ console.log(`\n ${chalk.cyan(w.name)}: ${w.frontmatter.description || 'no description'}`);
146
+ if (w.subagentCount > 0) {
147
+ console.log(` ${chalk.gray(`${w.subagentCount} subagent${w.subagentCount === 1 ? '' : 's'}`)}`);
148
+ }
149
+ }
150
+ const installSpinner = ora('Installing to central storage...').start();
151
+ let installed = 0;
152
+ for (const w of discovered) {
153
+ const result = installWorkflowCentrally(w.path, w.name);
154
+ if (result.success) {
155
+ installed++;
156
+ }
157
+ else {
158
+ installSpinner.stop();
159
+ console.log(chalk.red(`\n Failed to install ${w.name}: ${result.error}`));
160
+ installSpinner.start();
161
+ }
162
+ }
163
+ installSpinner.succeed(`Installed ${installed} workflow(s) to ~/.agents/workflows/`);
164
+ workflows = discovered.map(w => ({
165
+ name: w.name,
166
+ path: path.join(getUserWorkflowsDir(), w.name),
167
+ }));
168
+ }
169
+ // Agent/version selection
170
+ let selectedAgents;
171
+ let versionSelections;
172
+ if (options.agents) {
173
+ const result = resolveAgentVersionTargets(options.agents, WORKFLOW_CAPABLE_AGENTS);
174
+ selectedAgents = result.selectedAgents;
175
+ versionSelections = result.versionSelections;
176
+ }
177
+ else {
178
+ const result = await promptAgentVersionSelection(WORKFLOW_CAPABLE_AGENTS, {
179
+ skipPrompts: options.yes || !isInteractiveTerminal(),
180
+ });
181
+ selectedAgents = result.selectedAgents;
182
+ versionSelections = result.versionSelections;
183
+ }
184
+ if (selectedAgents.length === 0) {
185
+ console.log(chalk.yellow('\nNo agents selected.'));
186
+ return;
187
+ }
188
+ const syncSpinner = ora('Syncing to agent versions...').start();
189
+ let synced = 0;
190
+ const workflowNames = workflows.map(w => w.name);
191
+ for (const [agentId, versions] of versionSelections) {
192
+ for (const version of versions) {
193
+ syncResourcesToVersion(agentId, version);
194
+ recordVersionResources(agentId, version, 'workflows', workflowNames);
195
+ synced++;
196
+ }
197
+ }
198
+ if (synced > 0) {
199
+ syncSpinner.succeed(`Synced to ${synced} agent version(s)`);
200
+ }
201
+ else {
202
+ syncSpinner.info('No version-managed agents to sync');
203
+ }
204
+ console.log(chalk.green('\nWorkflows installed.'));
205
+ console.log(chalk.gray('Run with: agents run <workflow-name>'));
206
+ }
207
+ catch (err) {
208
+ if (isPromptCancelled(err)) {
209
+ console.log(chalk.gray('\nCancelled'));
210
+ return;
211
+ }
212
+ console.error(chalk.red('Failed to add workflows'));
213
+ console.error(chalk.red(err.message));
214
+ process.exit(1);
215
+ }
216
+ });
217
+ workflowsCmd
218
+ .command('remove [name]')
219
+ .description('Remove a workflow from version homes (interactive picker if no name given)')
220
+ .addHelpText('after', `
221
+ Examples:
222
+ # Remove a workflow by name
223
+ agents workflows remove rdev
224
+
225
+ # Interactive picker
226
+ agents workflows remove
227
+ `)
228
+ .action(async (name) => {
229
+ const workflowTargetMap = new Map();
230
+ for (const { agent, version } of iterWorkflowsCapableVersions()) {
231
+ const home = getVersionHomePath(agent, version);
232
+ for (const n of listWorkflowsForAgent(agent, home)) {
233
+ const existing = workflowTargetMap.get(n);
234
+ if (existing) {
235
+ existing.targets.push({ agent, version });
236
+ }
237
+ else {
238
+ workflowTargetMap.set(n, { name: n, targets: [{ agent, version }] });
239
+ }
240
+ }
241
+ }
242
+ let toRemove;
243
+ if (name) {
244
+ toRemove = [name];
245
+ }
246
+ else {
247
+ if (workflowTargetMap.size === 0) {
248
+ console.log(chalk.yellow('No workflows synced to any version.'));
249
+ return;
250
+ }
251
+ if (!isInteractiveTerminal()) {
252
+ requireInteractiveSelection('Selecting workflows to remove', [
253
+ 'agents workflows remove rdev',
254
+ ]);
255
+ }
256
+ try {
257
+ const selected = await checkbox({
258
+ message: 'Select workflows to remove',
259
+ choices: Array.from(workflowTargetMap.values()).map(w => ({
260
+ value: w.name,
261
+ name: w.name,
262
+ })),
263
+ });
264
+ if (selected.length === 0) {
265
+ console.log(chalk.gray('No workflows selected.'));
266
+ return;
267
+ }
268
+ toRemove = selected;
269
+ }
270
+ catch (err) {
271
+ if (isPromptCancelled(err)) {
272
+ console.log(chalk.gray('Cancelled'));
273
+ return;
274
+ }
275
+ throw err;
276
+ }
277
+ }
278
+ let removed = 0;
279
+ for (const workflowName of toRemove) {
280
+ const info = workflowTargetMap.get(workflowName);
281
+ if (!info || info.targets.length === 0) {
282
+ // Not synced to any version — try removing from central storage directly
283
+ const result = removeWorkflow(workflowName);
284
+ if (result.success) {
285
+ console.log(` ${chalk.red('-')} ${workflowName}: removed from central storage`);
286
+ removed++;
287
+ }
288
+ else {
289
+ console.log(chalk.yellow(` Workflow '${workflowName}' not found.`));
290
+ }
291
+ continue;
292
+ }
293
+ const removalTargets = info.targets.map(t => ({
294
+ agent: t.agent,
295
+ version: t.version,
296
+ label: `${agentLabel(t.agent)}@${t.version}`,
297
+ }));
298
+ const selectedTargets = await promptRemovalTargets(workflowName, removalTargets);
299
+ if (selectedTargets.length === 0) {
300
+ console.log(chalk.gray(` Skipped '${workflowName}'.`));
301
+ continue;
302
+ }
303
+ for (const target of selectedTargets) {
304
+ const result = removeWorkflowFromVersion(target.agent, target.version, workflowName);
305
+ if (result.success) {
306
+ console.log(` ${chalk.red('-')} ${target.label}: ${workflowName}`);
307
+ removed++;
308
+ }
309
+ else if (result.error) {
310
+ console.log(` ${chalk.yellow('!')} ${target.label}: ${result.error}`);
311
+ }
312
+ }
313
+ }
314
+ if (removed === 0) {
315
+ console.log(chalk.yellow('No workflows removed.'));
316
+ }
317
+ else {
318
+ console.log(chalk.green(`\nRemoved ${removed} workflow(s) from version homes.`));
319
+ console.log(chalk.gray('Central source unchanged. Use "agents workflows remove <name>" again to remove from ~/.agents/workflows/.'));
320
+ }
321
+ });
322
+ workflowsCmd
323
+ .command('view [name]')
324
+ .description('Read workflow details (description, subagents, model, MCP)')
325
+ .addHelpText('after', `
326
+ Examples:
327
+ # View a specific workflow
328
+ agents workflows view rdev
329
+
330
+ # Interactive picker
331
+ agents workflows view
332
+ `)
333
+ .action(async (name) => {
334
+ const installed = listInstalledWorkflows();
335
+ if (!name) {
336
+ if (installed.size === 0) {
337
+ console.log(chalk.yellow('No workflows installed'));
338
+ return;
339
+ }
340
+ if (!isInteractiveTerminal()) {
341
+ requireInteractiveSelection('Selecting a workflow to view', [
342
+ 'agents workflows view rdev',
343
+ ]);
344
+ }
345
+ try {
346
+ name = await select({
347
+ message: 'Select a workflow to view',
348
+ choices: Array.from(installed.values()).map(w => ({
349
+ value: w.name,
350
+ name: w.frontmatter.description
351
+ ? `${w.name} - ${w.frontmatter.description}`
352
+ : w.name,
353
+ })),
354
+ });
355
+ }
356
+ catch (err) {
357
+ if (isPromptCancelled(err)) {
358
+ console.log(chalk.gray('Cancelled'));
359
+ return;
360
+ }
361
+ throw err;
362
+ }
363
+ }
364
+ const workflow = installed.get(name);
365
+ if (!workflow) {
366
+ console.log(chalk.yellow(`Workflow '${name}' not found`));
367
+ return;
368
+ }
369
+ const lines = [];
370
+ const fm = workflow.frontmatter;
371
+ lines.push(chalk.bold(`\n${fm.name || workflow.name}\n`));
372
+ if (fm.description)
373
+ lines.push(` ${fm.description}`);
374
+ lines.push('');
375
+ if (fm.model)
376
+ lines.push(` Model: ${fm.model}`);
377
+ if (fm.tools?.length)
378
+ lines.push(` Tools: ${fm.tools.join(', ')}`);
379
+ if (fm.mcpServers?.length)
380
+ lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
381
+ if (fm.skills?.length)
382
+ lines.push(` Skills: ${fm.skills.join(', ')}`);
383
+ lines.push(` Path: ${workflow.path}`);
384
+ if (fm.allowedAgents?.length) {
385
+ lines.push(chalk.bold(`\n Subagents (${workflow.subagentCount}):`));
386
+ for (const a of fm.allowedAgents) {
387
+ lines.push(` ${chalk.cyan(a)}`);
388
+ }
389
+ }
390
+ lines.push('');
391
+ printWithPager(lines.join('\n'), lines.length);
392
+ });
393
+ }
394
+ function buildWorkflowRows(opts) {
395
+ const central = listInstalledWorkflows();
396
+ if (central.size === 0)
397
+ return [];
398
+ const targetPairs = iterWorkflowsCapableVersions({
399
+ agent: opts.filterAgent,
400
+ version: opts.filterVersion,
401
+ });
402
+ const syncedByTarget = new Map();
403
+ const defaultByAgent = new Map();
404
+ for (const { agent, version } of targetPairs) {
405
+ if (!defaultByAgent.has(agent))
406
+ defaultByAgent.set(agent, getGlobalDefault(agent));
407
+ const home = getVersionHomePath(agent, version);
408
+ syncedByTarget.set(`${agent}@${version}`, new Set(listWorkflowsForAgent(agent, home)));
409
+ }
410
+ const rows = [];
411
+ for (const [name, workflow] of central) {
412
+ const targets = targetPairs.map(({ agent, version }) => ({
413
+ agent,
414
+ version,
415
+ isDefault: defaultByAgent.get(agent) === version,
416
+ status: syncedByTarget.get(`${agent}@${version}`)?.has(name) ? 'synced' : 'missing',
417
+ }));
418
+ rows.push({
419
+ name,
420
+ description: workflow.frontmatter.description,
421
+ extra: workflow.subagentCount > 0 ? `${workflow.subagentCount}` : '-',
422
+ targets,
423
+ buildDetail: () => formatWorkflowDetail(workflow, targets),
424
+ });
425
+ }
426
+ rows.sort((a, b) => {
427
+ const aSynced = a.targets.filter(t => t.status === 'synced').length;
428
+ const bSynced = b.targets.filter(t => t.status === 'synced').length;
429
+ if (aSynced !== bSynced)
430
+ return bSynced - aSynced;
431
+ return a.name.localeCompare(b.name);
432
+ });
433
+ return rows;
434
+ }
435
+ function formatWorkflowDetail(workflow, targets) {
436
+ const lines = [];
437
+ const fm = workflow.frontmatter;
438
+ lines.push(chalk.bold.cyan(workflow.name));
439
+ if (fm.description)
440
+ lines.push(chalk.gray(fm.description));
441
+ lines.push('');
442
+ const meta = [];
443
+ if (fm.model)
444
+ meta.push(`model ${chalk.white(fm.model)}`);
445
+ if (fm.mcpServers?.length)
446
+ meta.push(`${chalk.white(fm.mcpServers.length)} MCP`);
447
+ if (fm.skills?.length)
448
+ meta.push(`${chalk.white(fm.skills.length)} skill${fm.skills.length === 1 ? '' : 's'}`);
449
+ meta.push(`${chalk.white(workflow.subagentCount)} subagent${workflow.subagentCount === 1 ? '' : 's'}`);
450
+ if (meta.length)
451
+ lines.push(' ' + meta.join(chalk.gray(' · ')));
452
+ lines.push(' ' + chalk.gray(workflow.path));
453
+ lines.push('');
454
+ lines.push(chalk.bold(' Synced to'));
455
+ lines.push(buildTargetsSection(targets));
456
+ return lines.join('\n');
457
+ }
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ import { registerTrashCommands } from './commands/trash.js';
46
46
  import { registerDoctorCommand } from './commands/doctor.js';
47
47
  import { registerSubagentsCommands } from './commands/subagents.js';
48
48
  import { registerPluginsCommands } from './commands/plugins.js';
49
+ import { registerWorkflowsCommands } from './commands/workflows.js';
49
50
  import { registerSyncCommand } from './commands/sync.js';
50
51
  import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
51
52
  import { registerDriveCommands } from './commands/drive.js';
@@ -60,8 +61,8 @@ import { registerBetaCommands } from './commands/beta.js';
60
61
  import { applyGlobalHelpConventions } from './lib/help.js';
61
62
  import { isPromptCancelled } from './commands/utils.js';
62
63
  import { AGENTS } from './lib/agents.js';
63
- import { getGlobalDefault } from './lib/versions.js';
64
- import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
64
+ import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
65
+ import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
65
66
  const program = new Command();
66
67
  program
67
68
  .name('agents')
@@ -88,7 +89,8 @@ Agent versions:
88
89
  add <agent>[@version] Install an agent CLI (e.g. agents add codex)
89
90
  remove <agent>[@version] Uninstall a version
90
91
  use <agent>@<version> Set the default version
91
- prune [target] Remove orphan resources (commands/skills/hooks) and/or older duplicate version installs
92
+ prune [target] Remove orphan resources and older duplicate version installs (targets: commands, sessions, runs, trash)
93
+ trash Inspect and restore soft-deleted version directories
92
94
  view [agent[@version]] List versions, or inspect one in detail
93
95
 
94
96
  Agent configuration (synced across versions):
@@ -97,7 +99,7 @@ Agent configuration (synced across versions):
97
99
  skills Knowledge packs (SKILL.md + supporting files)
98
100
  mcp MCP servers (stdio or HTTP)
99
101
  permissions Allow/deny rules for tool calls
100
- hooks Shell scripts that run on agent events
102
+ hooks Shell scripts that run on agent events (hooks.yaml in agents.yaml)
101
103
  subagents Named sub-agent definitions
102
104
  plugins Bundles of skills, hooks, and scripts
103
105
 
@@ -105,19 +107,30 @@ Packages:
105
107
  search <query> Find MCP servers and skills in registries
106
108
  install <pkg> Install from registry (mcp:name, skill:user/repo)
107
109
 
108
- Run agents:
110
+ Run and dispatch:
109
111
  run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
110
112
  teams Coordinate multiple agents on shared work
111
113
  routines Run agents on a cron schedule (scheduler auto-starts)
112
- sessions Browse and replay past runs
114
+ sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
115
+ browser Automate a browser — navigate, click, screenshot, console, network
116
+ pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
113
117
 
114
- Credentials:
118
+ Credentials and profiles:
115
119
  profiles Bundles of (host CLI, endpoint, model, auth)
116
- secrets Keychain-backed env bundles injected at spawn
120
+ secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
117
121
 
118
- Helpers:
119
- beta Enable preview features like drive and factory
120
- pty Drive interactive terminal programs (REPLs, TUIs)
122
+ Diagnostics:
123
+ doctor [agent[@version]] Diagnose CLI availability, sync status, and resource divergence
124
+ usage [agent] Show rate-limit and quota usage per agent
125
+
126
+ Config sync:
127
+ drive Sync session history across machines via rsync
128
+ pull Clone or pull the system repo at ~/.agents-system/
129
+ repo init --path <dir> Scaffold your own editable repo from a template
130
+ repo add <path|gh:user/repo> Merge an extra repo after the system repo
131
+
132
+ Beta features:
133
+ beta Enable preview features (factory, drive, and more)
121
134
 
122
135
  Automation tips:
123
136
  Pass explicit names/IDs Avoid pickers: agents sessions <id> --markdown
@@ -126,11 +139,6 @@ Automation tips:
126
139
  Use agent@version targets e.g. --agents claude@2.1.79,codex@default
127
140
  Non-TTY shells apply defaults Omitted required selections fail with a plain hint
128
141
 
129
- Config sync (portable setup via git):
130
- pull Clone or pull the system repo at ~/.agents-system/
131
- repo init --path <dir> Scaffold your own editable repo from a template
132
- repo add <path|gh:user/repo> Merge an extra repo after the system repo
133
-
134
142
  Options:
135
143
  -V, --version Show version number
136
144
  -h, --help Show help
@@ -335,6 +343,12 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
335
343
  if (status !== 'current') {
336
344
  createdOrUpdated.push(`${status === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand} shim`);
337
345
  }
346
+ for (const version of listInstalledVersions(agent)) {
347
+ const aliasStatus = ensureVersionedAliasCurrent(agent, version);
348
+ if (aliasStatus !== 'current') {
349
+ createdOrUpdated.push(`${aliasStatus === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand}@${version} alias`);
350
+ }
351
+ }
338
352
  }
339
353
  for (const notice of createdOrUpdated) {
340
354
  console.log(chalk.green(notice));
@@ -444,6 +458,7 @@ program
444
458
  registerMcpCommands(program);
445
459
  registerSubagentsCommands(program);
446
460
  registerPluginsCommands(program);
461
+ registerWorkflowsCommands(program);
447
462
  registerVersionsCommands(program);
448
463
  registerPackagesCommands(program);
449
464
  registerDaemonCommands(program);
@@ -101,10 +101,13 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
101
101
  return;
102
102
  const matches = {
103
103
  chrome: ['chrome', 'google-chrome', 'headlesschrome'],
104
- chromium: ['chromium', 'headlesschrome'],
105
- comet: ['comet'],
106
- brave: ['brave', 'brave-browser'],
107
- edge: ['edge', 'microsoft-edge', 'msedge'],
104
+ chromium: ['chromium', 'headlesschrome', 'chrome'],
105
+ // Comet reports itself as plain "Chrome/<version>" in /json/version — it
106
+ // doesn't override the Chromium branding. Accept chrome here so attaching
107
+ // to a Comet instance doesn't trip a false "identity mismatch".
108
+ comet: ['comet', 'chrome'],
109
+ brave: ['brave', 'brave-browser', 'chrome'],
110
+ edge: ['edge', 'microsoft-edge', 'msedge', 'chrome'],
108
111
  };
109
112
  const accepted = matches[expected] || [expected];
110
113
  if (accepted.includes(reported))
@@ -14,3 +14,13 @@ export declare function getRunningChromeInfo(profileName: string): {
14
14
  port: number;
15
15
  } | null;
16
16
  export declare function allocatePort(): number;
17
+ export interface PortOccupant {
18
+ pid: number;
19
+ command: string;
20
+ }
21
+ /**
22
+ * Identify the process listening on a TCP port via lsof. Returns null when nothing is bound.
23
+ * Used for clearer error messages when a profile's configured port is taken by a non-debug
24
+ * process (e.g. Comet running without --remote-debugging-port).
25
+ */
26
+ export declare function getPortOccupant(port: number): PortOccupant | null;
@@ -69,6 +69,7 @@ export async function launchBrowser(profileName, browserType, port, options = {}
69
69
  const runtimeDir = getProfileRuntimeDir(profileName);
70
70
  const userDataDir = path.join(runtimeDir, 'chrome-data');
71
71
  fs.mkdirSync(userDataDir, { recursive: true });
72
+ const viewport = options.viewport ?? { width: 1512, height: 982 };
72
73
  const args = [
73
74
  `--remote-debugging-port=${port}`,
74
75
  `--user-data-dir=${userDataDir}`,
@@ -77,7 +78,10 @@ export async function launchBrowser(profileName, browserType, port, options = {}
77
78
  '--disable-backgrounding-occluded-windows',
78
79
  '--disable-renderer-backgrounding',
79
80
  ...(options.headless ? ['--headless=new'] : []),
80
- ...(options.viewport ? [`--window-size=${options.viewport.width},${options.viewport.height}`] : []),
81
+ `--window-size=${viewport.width},${viewport.height}`,
82
+ ...(viewport.x !== undefined && viewport.y !== undefined
83
+ ? [`--window-position=${viewport.x},${viewport.y}`]
84
+ : []),
81
85
  ...(options.args || []),
82
86
  ];
83
87
  let env = { ...process.env };
@@ -150,7 +154,11 @@ function isProcessRunning(pid) {
150
154
  process.kill(pid, 0);
151
155
  return true;
152
156
  }
153
- catch {
157
+ catch (err) {
158
+ // EPERM means the process exists but we lack permission to signal it —
159
+ // treat as alive. ESRCH means the process does not exist.
160
+ if (err && err.code === 'EPERM')
161
+ return true;
154
162
  return false;
155
163
  }
156
164
  }
@@ -170,3 +178,30 @@ export function allocatePort() {
170
178
  }
171
179
  throw new Error('No available ports in range 9200-9300');
172
180
  }
181
+ /**
182
+ * Identify the process listening on a TCP port via lsof. Returns null when nothing is bound.
183
+ * Used for clearer error messages when a profile's configured port is taken by a non-debug
184
+ * process (e.g. Comet running without --remote-debugging-port).
185
+ */
186
+ export function getPortOccupant(port) {
187
+ try {
188
+ const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -Fpcn`, {
189
+ encoding: 'utf8',
190
+ stdio: ['ignore', 'pipe', 'ignore'],
191
+ });
192
+ let pid = 0;
193
+ let command = '';
194
+ for (const line of out.split('\n')) {
195
+ if (line.startsWith('p'))
196
+ pid = parseInt(line.slice(1), 10) || 0;
197
+ else if (line.startsWith('c') && !command)
198
+ command = line.slice(1);
199
+ }
200
+ if (!pid)
201
+ return null;
202
+ return { pid, command: command || 'unknown' };
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }