@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.
- package/CHANGELOG.md +65 -0
- package/dist/commands/browser.js +248 -9
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/exec.js +70 -1
- package/dist/commands/plugins.js +179 -5
- package/dist/commands/prune.js +6 -0
- package/dist/commands/secrets.js +117 -19
- package/dist/commands/view.js +21 -8
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.js +31 -16
- package/dist/lib/browser/cdp.js +7 -4
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +37 -2
- package/dist/lib/browser/drivers/local.js +13 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +14 -0
- package/dist/lib/browser/profiles.d.ts +5 -0
- package/dist/lib/browser/profiles.js +45 -0
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +29 -1
- package/dist/lib/browser/types.d.ts +11 -1
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +68 -13
- package/dist/lib/commands.d.ts +0 -15
- package/dist/lib/commands.js +5 -5
- package/dist/lib/hooks.js +24 -11
- package/dist/lib/migrate.js +59 -1
- package/dist/lib/permissions.d.ts +0 -58
- package/dist/lib/permissions.js +10 -10
- package/dist/lib/plugins.d.ts +75 -34
- package/dist/lib/plugins.js +640 -133
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +106 -7
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +28 -17
- package/dist/lib/shims.d.ts +3 -51
- package/dist/lib/shims.js +18 -10
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +15 -2
- package/dist/lib/state.js +29 -8
- package/dist/lib/types.d.ts +43 -14
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +139 -27
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +59 -58
- package/dist/commands/fork.d.ts +0 -10
- 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
|
|
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
|
|
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
|
|
120
|
+
secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
+
}
|