@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
package/dist/lib/versions.js
CHANGED
|
@@ -23,14 +23,16 @@ import { promisify } from 'util';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import * as TOML from 'smol-toml';
|
|
25
25
|
import { checkbox, select } from '@inquirer/prompts';
|
|
26
|
-
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources,
|
|
27
|
-
import {
|
|
26
|
+
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
27
|
+
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
28
|
+
import { resolveResource, listResources } from './resources.js';
|
|
28
29
|
import { AGENTS, getAccountEmail, MCP_CAPABLE_AGENTS, COMMANDS_CAPABLE_AGENTS, getMcpConfigPathForHome, parseMcpConfig, resolveAgentName, formatAgentError } from './agents.js';
|
|
29
30
|
import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
30
31
|
import { installMcpServers, parseMcpServerConfig } from './mcp.js';
|
|
31
32
|
import { markdownToToml } from './convert.js';
|
|
32
33
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
33
34
|
import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
|
|
35
|
+
import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
|
|
34
36
|
import { registerHooksToSettings } from './hooks.js';
|
|
35
37
|
import { supports, explainSkip } from './capabilities.js';
|
|
36
38
|
import { discoverPlugins, syncPluginToVersion, isPluginSynced, pluginSupportsAgent, cleanOrphanedPluginSkills } from './plugins.js';
|
|
@@ -62,6 +64,7 @@ export function getAvailableResources(cwd = process.cwd()) {
|
|
|
62
64
|
permissions: [],
|
|
63
65
|
subagents: [],
|
|
64
66
|
plugins: [],
|
|
67
|
+
workflows: [],
|
|
65
68
|
promptcuts: false,
|
|
66
69
|
};
|
|
67
70
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
@@ -189,6 +192,20 @@ export function getAvailableResources(cwd = process.cwd()) {
|
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
result.subagents = Array.from(subagentNames);
|
|
195
|
+
// Workflows (directories with WORKFLOW.md)
|
|
196
|
+
const workflowNames = new Set();
|
|
197
|
+
for (const { base } of resourceBases) {
|
|
198
|
+
const workflowsDir = path.join(base, 'workflows');
|
|
199
|
+
if (!fs.existsSync(workflowsDir))
|
|
200
|
+
continue;
|
|
201
|
+
const names = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
202
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
|
|
203
|
+
.map(d => d.name);
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
workflowNames.add(name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
result.workflows = Array.from(workflowNames);
|
|
192
209
|
// Plugins (directories with .claude-plugin/plugin.json)
|
|
193
210
|
const allPlugins = discoverPlugins();
|
|
194
211
|
result.plugins = allPlugins.map(p => p.name);
|
|
@@ -250,6 +267,7 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
250
267
|
permissions: [],
|
|
251
268
|
subagents: [],
|
|
252
269
|
plugins: [],
|
|
270
|
+
workflows: [],
|
|
253
271
|
promptcuts: false,
|
|
254
272
|
};
|
|
255
273
|
// Commands - check what files exist in version home
|
|
@@ -448,6 +466,15 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
448
466
|
}
|
|
449
467
|
}
|
|
450
468
|
}
|
|
469
|
+
// Workflows - check {versionHome}/workflows/ for synced workflow directories
|
|
470
|
+
if (WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
471
|
+
const workflowsDir = path.join(versionHome, 'workflows');
|
|
472
|
+
if (fs.existsSync(workflowsDir)) {
|
|
473
|
+
result.workflows = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
474
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
|
|
475
|
+
.map(d => d.name);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
451
478
|
return result;
|
|
452
479
|
}
|
|
453
480
|
/**
|
|
@@ -469,6 +496,7 @@ export function getNewResources(available, actuallySynced) {
|
|
|
469
496
|
permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
|
|
470
497
|
subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
|
|
471
498
|
plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
|
|
499
|
+
workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w)),
|
|
472
500
|
// Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
|
|
473
501
|
// directly, so there is never a "new" per-version state to reconcile.
|
|
474
502
|
promptcuts: false,
|
|
@@ -485,6 +513,7 @@ export function hasNewResources(diff, agent, version) {
|
|
|
485
513
|
const permsApply = agent ? supports(agent, 'allowlist', version).ok : true;
|
|
486
514
|
const subagentsApply = agent ? SUBAGENT_CAPABLE_AGENTS.includes(agent) : true;
|
|
487
515
|
const pluginsApply = agent ? supports(agent, 'plugins', version).ok : true;
|
|
516
|
+
const workflowsApply = agent ? WORKFLOW_CAPABLE_AGENTS.includes(agent) : true;
|
|
488
517
|
return ((diff.commands.length > 0 && commandsApply) ||
|
|
489
518
|
diff.skills.length > 0 ||
|
|
490
519
|
(diff.hooks.length > 0 && hooksApply) ||
|
|
@@ -492,7 +521,8 @@ export function hasNewResources(diff, agent, version) {
|
|
|
492
521
|
(diff.mcp.length > 0 && mcpApply) ||
|
|
493
522
|
(diff.permissions.length > 0 && permsApply) ||
|
|
494
523
|
(diff.subagents.length > 0 && subagentsApply) ||
|
|
495
|
-
(diff.plugins.length > 0 && pluginsApply)
|
|
524
|
+
(diff.plugins.length > 0 && pluginsApply) ||
|
|
525
|
+
(diff.workflows.length > 0 && workflowsApply));
|
|
496
526
|
}
|
|
497
527
|
/**
|
|
498
528
|
* Build a summary string of new resources.
|
|
@@ -525,6 +555,9 @@ function buildNewResourcesSummary(newResources, agent) {
|
|
|
525
555
|
if (newResources.plugins.length > 0 && PLUGINS_CAPABLE_AGENTS.includes(agent)) {
|
|
526
556
|
parts.push(`${newResources.plugins.length} plugin${newResources.plugins.length === 1 ? '' : 's'}`);
|
|
527
557
|
}
|
|
558
|
+
if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
559
|
+
parts.push(`${newResources.workflows.length} workflow${newResources.workflows.length === 1 ? '' : 's'}`);
|
|
560
|
+
}
|
|
528
561
|
return parts.join(', ');
|
|
529
562
|
}
|
|
530
563
|
/**
|
|
@@ -573,6 +606,8 @@ export async function promptNewResourceSelection(agent, newResources) {
|
|
|
573
606
|
selection.subagents = newResources.subagents;
|
|
574
607
|
if (newResources.plugins.length > 0 && PLUGINS_CAPABLE_AGENTS.includes(agent))
|
|
575
608
|
selection.plugins = newResources.plugins;
|
|
609
|
+
if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent))
|
|
610
|
+
selection.workflows = newResources.workflows;
|
|
576
611
|
return selection;
|
|
577
612
|
}
|
|
578
613
|
// Select specific items for each category
|
|
@@ -650,6 +685,14 @@ export async function promptNewResourceSelection(agent, newResources) {
|
|
|
650
685
|
if (selected.length > 0)
|
|
651
686
|
selection.plugins = selected;
|
|
652
687
|
}
|
|
688
|
+
if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
689
|
+
const selected = await checkbox({
|
|
690
|
+
message: 'Select new workflows to sync:',
|
|
691
|
+
choices: newResources.workflows.map(w => ({ name: w, value: w, checked: true })),
|
|
692
|
+
});
|
|
693
|
+
if (selected.length > 0)
|
|
694
|
+
selection.workflows = selected;
|
|
695
|
+
}
|
|
653
696
|
return selection;
|
|
654
697
|
}
|
|
655
698
|
/**
|
|
@@ -1399,7 +1442,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1399
1442
|
const versionHome = getVersionHomePath(agent, version);
|
|
1400
1443
|
const agentDir = path.join(versionHome, `.${agent}`);
|
|
1401
1444
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
1402
|
-
const result = { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [] };
|
|
1445
|
+
const result = { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
|
|
1403
1446
|
const cwd = options.cwd || process.cwd();
|
|
1404
1447
|
const projectAgentsDir = options.projectDir || getProjectAgentsDir(cwd);
|
|
1405
1448
|
const userAgentsDir = getUserAgentsDir();
|
|
@@ -1407,13 +1450,80 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1407
1450
|
// project/user/system repos win on name collisions.
|
|
1408
1451
|
const extraRepos = getEnabledExtraRepos();
|
|
1409
1452
|
const available = getAvailableResources(cwd);
|
|
1453
|
+
// Write default resource selection patterns for this version (idempotent —
|
|
1454
|
+
// only sets fields that aren't already present, preserving user edits).
|
|
1455
|
+
{
|
|
1456
|
+
const extraAliases = extraRepos.map(e => e.alias);
|
|
1457
|
+
const allLayers = defaultPatterns(extraAliases);
|
|
1458
|
+
const noProject = defaultPatterns(extraAliases, false);
|
|
1459
|
+
ensureVersionResourcePatterns(agent, version, {
|
|
1460
|
+
commands: allLayers,
|
|
1461
|
+
skills: allLayers,
|
|
1462
|
+
hooks: noProject, // hooks: no project layer (security)
|
|
1463
|
+
subagents: noProject,
|
|
1464
|
+
plugins: noProject,
|
|
1465
|
+
workflows: noProject,
|
|
1466
|
+
permissions: ['system:*'],
|
|
1467
|
+
mcp: ['user:*'],
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
// If no explicit selection was passed, build one from the persisted resource
|
|
1471
|
+
// patterns. This lets users customize agents.yaml to control which resources
|
|
1472
|
+
// are synced (e.g. "skills: [system:brain-scan user:creative]").
|
|
1473
|
+
// When patterns are the default (every layer wildcard), the expanded result
|
|
1474
|
+
// equals the full available set — identical to the old behavior.
|
|
1475
|
+
if (!selection) {
|
|
1476
|
+
const vr = getVersionResources(agent, version);
|
|
1477
|
+
if (vr) {
|
|
1478
|
+
const patternSelection = {};
|
|
1479
|
+
// Listable resource types: use listResources to get name→source maps.
|
|
1480
|
+
const listableTypes = [
|
|
1481
|
+
['commands', 'commands'],
|
|
1482
|
+
['skills', 'skills'],
|
|
1483
|
+
['hooks', 'hooks'],
|
|
1484
|
+
['subagents', 'subagents'],
|
|
1485
|
+
];
|
|
1486
|
+
for (const [type, kind] of listableTypes) {
|
|
1487
|
+
const patterns = vr[type];
|
|
1488
|
+
if (!Array.isArray(patterns) || patterns.length === 0)
|
|
1489
|
+
continue;
|
|
1490
|
+
const sourceMap = new Map(listResources(kind, cwd).map(r => [r.name, r.source]));
|
|
1491
|
+
patternSelection[type] = expandPatterns(patterns, sourceMap);
|
|
1492
|
+
}
|
|
1493
|
+
// permissions: all groups are 'system' source.
|
|
1494
|
+
if (Array.isArray(vr.permissions) && vr.permissions.length > 0) {
|
|
1495
|
+
const permMap = new Map(available.permissions.map(n => [n, 'system']));
|
|
1496
|
+
patternSelection.permissions = expandPatterns(vr.permissions, permMap);
|
|
1497
|
+
}
|
|
1498
|
+
// mcp: all declared servers are 'user' source.
|
|
1499
|
+
if (Array.isArray(vr.mcp) && vr.mcp.length > 0) {
|
|
1500
|
+
const mcpMap = new Map(available.mcp.map(n => [n, 'user']));
|
|
1501
|
+
patternSelection.mcp = expandPatterns(vr.mcp, mcpMap);
|
|
1502
|
+
}
|
|
1503
|
+
// plugins: treat all as 'user' source for now.
|
|
1504
|
+
if (Array.isArray(vr.plugins) && vr.plugins.length > 0) {
|
|
1505
|
+
const pluginMap = new Map(available.plugins.map(n => [n, 'user']));
|
|
1506
|
+
patternSelection.plugins = expandPatterns(vr.plugins, pluginMap);
|
|
1507
|
+
}
|
|
1508
|
+
// workflows: treat all as 'user' source.
|
|
1509
|
+
if (Array.isArray(vr.workflows) && vr.workflows.length > 0) {
|
|
1510
|
+
const workflowMap = new Map(available.workflows.map(n => [n, 'user']));
|
|
1511
|
+
patternSelection.workflows = expandPatterns(vr.workflows, workflowMap);
|
|
1512
|
+
}
|
|
1513
|
+
// memory is not pattern-controlled (rulesPreset handles it) — always sync.
|
|
1514
|
+
patternSelection.memory = 'all';
|
|
1515
|
+
if (Object.keys(patternSelection).length > 0) {
|
|
1516
|
+
selection = patternSelection;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1410
1520
|
// Fast guard: skip the entire sync when no selection is active and nothing
|
|
1411
1521
|
// has changed since the last full sync. Drops steady-state cost from ~16s
|
|
1412
1522
|
// (unconditional file copies) to ~2ms (stat calls + manifest read).
|
|
1413
1523
|
if (!selection && !options.force) {
|
|
1414
1524
|
const manifest = loadSyncManifest(agent, version);
|
|
1415
1525
|
if (manifest && !isSyncStale(manifest, available, agent, version, cwd)) {
|
|
1416
|
-
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [] };
|
|
1526
|
+
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
|
|
1417
1527
|
}
|
|
1418
1528
|
}
|
|
1419
1529
|
// Helper: remove a path (symlink or real) if it exists
|
|
@@ -1499,9 +1609,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1499
1609
|
syncedCommands.push(cmd);
|
|
1500
1610
|
}
|
|
1501
1611
|
result.commands = syncedCommands.length > 0;
|
|
1502
|
-
if (syncedCommands.length > 0) {
|
|
1503
|
-
recordVersionResources(agent, version, 'commands', syncedCommands);
|
|
1504
|
-
}
|
|
1505
1612
|
}
|
|
1506
1613
|
// Sync skills (skip if agent natively reads ~/.agents/skills/)
|
|
1507
1614
|
if (agentConfig.nativeAgentsSkillsDir) {
|
|
@@ -1530,9 +1637,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1530
1637
|
syncedSkills.push(skill);
|
|
1531
1638
|
}
|
|
1532
1639
|
result.skills = syncedSkills.length > 0;
|
|
1533
|
-
if (syncedSkills.length > 0) {
|
|
1534
|
-
recordVersionResources(agent, version, 'skills', syncedSkills);
|
|
1535
|
-
}
|
|
1536
1640
|
}
|
|
1537
1641
|
}
|
|
1538
1642
|
// Sync hooks (if agent supports them at this version)
|
|
@@ -1598,9 +1702,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1598
1702
|
}
|
|
1599
1703
|
}
|
|
1600
1704
|
result.hooks = syncedHooks.length > 0;
|
|
1601
|
-
if (syncedHooks.length > 0) {
|
|
1602
|
-
recordVersionResources(agent, version, 'hooks', syncedHooks);
|
|
1603
|
-
}
|
|
1604
1705
|
// Register hooks into agent-native settings.json/hooks.json. Gemini
|
|
1605
1706
|
// shipped hooks in 0.26.0; gate already passed above so this is safe.
|
|
1606
1707
|
if (agent === 'claude' || agent === 'codex' || agent === 'gemini') {
|
|
@@ -1629,8 +1730,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1629
1730
|
removePath(destFile);
|
|
1630
1731
|
fs.writeFileSync(destFile, composed.content);
|
|
1631
1732
|
result.memory.push(targetName);
|
|
1632
|
-
//
|
|
1633
|
-
recordVersionResources(agent, version, 'memory', [composed.preset]);
|
|
1733
|
+
// rulesPreset is tracked separately via setActiveRulesPreset.
|
|
1634
1734
|
}
|
|
1635
1735
|
catch (err) {
|
|
1636
1736
|
// No rules.yaml yet, or a typo'd preset name. Don't fail the whole sync —
|
|
@@ -1681,9 +1781,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1681
1781
|
if (builtPerms.allow.length > 0 || (builtPerms.deny && builtPerms.deny.length > 0)) {
|
|
1682
1782
|
const permResult = applyPermsToVersion(agent, builtPerms, versionHome, true);
|
|
1683
1783
|
result.permissions = permResult.success;
|
|
1684
|
-
|
|
1685
|
-
recordVersionResources(agent, version, 'permissions', permsToSync);
|
|
1686
|
-
}
|
|
1784
|
+
// permissions patterns already written via ensureVersionResourcePatterns above.
|
|
1687
1785
|
}
|
|
1688
1786
|
}
|
|
1689
1787
|
// Install MCP servers (if agent supports them)
|
|
@@ -1695,9 +1793,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1695
1793
|
if (mcpToSync.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
1696
1794
|
const mcpResult = installMcpServers(agent, version, versionHome, mcpToSync, { cwd });
|
|
1697
1795
|
result.mcp = mcpResult.applied;
|
|
1698
|
-
|
|
1699
|
-
recordVersionResources(agent, version, 'mcp', mcpResult.applied);
|
|
1700
|
-
}
|
|
1796
|
+
// mcp patterns already written via ensureVersionResourcePatterns above.
|
|
1701
1797
|
}
|
|
1702
1798
|
// Sync subagents (claude and openclaw only)
|
|
1703
1799
|
const subagentsToSync = selection
|
|
@@ -1730,9 +1826,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1730
1826
|
}
|
|
1731
1827
|
catch { /* resource sync failed for this item */ }
|
|
1732
1828
|
}
|
|
1733
|
-
|
|
1734
|
-
recordVersionResources(agent, version, 'subagents', result.subagents);
|
|
1735
|
-
}
|
|
1829
|
+
// subagent patterns already written via ensureVersionResourcePatterns above.
|
|
1736
1830
|
}
|
|
1737
1831
|
// Sync plugins (claude and openclaw)
|
|
1738
1832
|
const pluginsToSync = selection
|
|
@@ -1753,9 +1847,27 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1753
1847
|
result.plugins.push(name);
|
|
1754
1848
|
}
|
|
1755
1849
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1850
|
+
// plugin patterns already written via ensureVersionResourcePatterns above.
|
|
1851
|
+
}
|
|
1852
|
+
// Sync workflows (claude only)
|
|
1853
|
+
const workflowsToSync = selection
|
|
1854
|
+
? resolveSelection(selection.workflows, available.workflows)
|
|
1855
|
+
: (WORKFLOW_CAPABLE_AGENTS.includes(agent) ? available.workflows : []);
|
|
1856
|
+
if (workflowsToSync.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
1857
|
+
const allWorkflows = listInstalledWorkflows();
|
|
1858
|
+
for (const name of workflowsToSync) {
|
|
1859
|
+
const workflow = allWorkflows.get(name);
|
|
1860
|
+
if (!workflow)
|
|
1861
|
+
continue;
|
|
1862
|
+
try {
|
|
1863
|
+
const syncResult = syncWorkflowToVersion(workflow.path, name, agent, versionHome);
|
|
1864
|
+
if (syncResult.success) {
|
|
1865
|
+
result.workflows.push(name);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
catch { /* resource sync failed for this item */ }
|
|
1758
1869
|
}
|
|
1870
|
+
// workflow patterns already written via ensureVersionResourcePatterns above.
|
|
1759
1871
|
}
|
|
1760
1872
|
// Write manifest after a successful full sync so the next launch can skip this work.
|
|
1761
1873
|
if (!selection) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow management library.
|
|
3
|
+
*
|
|
4
|
+
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
|
+
* They optionally contain subagents/, skills/, and plugins/ subdirectories that
|
|
6
|
+
* are composed at runtime by `agents run <workflow>`.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentId } from './types.js';
|
|
9
|
+
/** Agents that support running workflows via `agents run`. */
|
|
10
|
+
export declare const WORKFLOW_CAPABLE_AGENTS: AgentId[];
|
|
11
|
+
/** Parsed WORKFLOW.md frontmatter. */
|
|
12
|
+
export interface WorkflowFrontmatter {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
tools?: string[];
|
|
17
|
+
skills?: string[];
|
|
18
|
+
mcpServers?: string[];
|
|
19
|
+
allowedAgents?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** A workflow found during repo discovery. */
|
|
22
|
+
export interface DiscoveredWorkflow {
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
frontmatter: WorkflowFrontmatter;
|
|
26
|
+
subagentCount: number;
|
|
27
|
+
}
|
|
28
|
+
/** A workflow in central storage (~/.agents/workflows/ or ~/.agents-system/workflows/). */
|
|
29
|
+
export interface InstalledWorkflow {
|
|
30
|
+
name: string;
|
|
31
|
+
path: string;
|
|
32
|
+
frontmatter: WorkflowFrontmatter;
|
|
33
|
+
subagentCount: number;
|
|
34
|
+
}
|
|
35
|
+
/** Parse WORKFLOW.md frontmatter from a workflow directory. Returns null if invalid. */
|
|
36
|
+
export declare function parseWorkflowFrontmatter(workflowDir: string): WorkflowFrontmatter | null;
|
|
37
|
+
/** Count subagent .md files in a workflow's subagents/ directory. */
|
|
38
|
+
export declare function countWorkflowSubagents(workflowDir: string): number;
|
|
39
|
+
/**
|
|
40
|
+
* Discover all workflow directories (those containing WORKFLOW.md) in a local path.
|
|
41
|
+
* Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,
|
|
42
|
+
* then falls back to scanning all immediate subdirectories.
|
|
43
|
+
*/
|
|
44
|
+
export declare function discoverWorkflowsFromRepo(repoPath: string): DiscoveredWorkflow[];
|
|
45
|
+
/**
|
|
46
|
+
* List all workflows in central storage.
|
|
47
|
+
* User layer (~/.agents/workflows/) wins over system (~/.agents-system/workflows/).
|
|
48
|
+
*/
|
|
49
|
+
export declare function listInstalledWorkflows(): Map<string, InstalledWorkflow>;
|
|
50
|
+
/** Copy a workflow directory into user central storage (~/.agents/workflows/<name>/). */
|
|
51
|
+
export declare function installWorkflowCentrally(sourcePath: string, name: string): {
|
|
52
|
+
success: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
};
|
|
55
|
+
/** Move a workflow from user central storage to trash. */
|
|
56
|
+
export declare function removeWorkflow(name: string): {
|
|
57
|
+
success: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
};
|
|
60
|
+
/** List workflow names synced into a specific agent version home (at {versionHome}/workflows/). */
|
|
61
|
+
export declare function listWorkflowsForAgent(_agent: AgentId, versionHome: string): string[];
|
|
62
|
+
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
63
|
+
export declare function syncWorkflowToVersion(workflowPath: string, name: string, _agent: AgentId, versionHome: string): {
|
|
64
|
+
success: boolean;
|
|
65
|
+
error?: string;
|
|
66
|
+
};
|
|
67
|
+
/** Remove a workflow from a specific agent version home. */
|
|
68
|
+
export declare function removeWorkflowFromVersion(agent: AgentId, version: string, name: string): {
|
|
69
|
+
success: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Iterate all installed (agent, version) pairs that support workflows. */
|
|
73
|
+
export declare function iterWorkflowsCapableVersions(filter?: {
|
|
74
|
+
agent?: AgentId;
|
|
75
|
+
version?: string;
|
|
76
|
+
}): Array<{
|
|
77
|
+
agent: AgentId;
|
|
78
|
+
version: string;
|
|
79
|
+
}>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow management library.
|
|
3
|
+
*
|
|
4
|
+
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
|
+
* They optionally contain subagents/, skills/, and plugins/ subdirectories that
|
|
6
|
+
* are composed at runtime by `agents run <workflow>`.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as yaml from 'yaml';
|
|
11
|
+
import { getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, } from './state.js';
|
|
12
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
13
|
+
/** Agents that support running workflows via `agents run`. */
|
|
14
|
+
export const WORKFLOW_CAPABLE_AGENTS = ['claude'];
|
|
15
|
+
/** Parse WORKFLOW.md frontmatter from a workflow directory. Returns null if invalid. */
|
|
16
|
+
export function parseWorkflowFrontmatter(workflowDir) {
|
|
17
|
+
const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
|
|
18
|
+
if (!fs.existsSync(workflowMdPath))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(workflowMdPath, 'utf-8');
|
|
22
|
+
const lines = content.split('\n');
|
|
23
|
+
if (lines[0] !== '---')
|
|
24
|
+
return null;
|
|
25
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
26
|
+
if (endIndex < 0)
|
|
27
|
+
return null;
|
|
28
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
29
|
+
const parsed = yaml.parse(frontmatter);
|
|
30
|
+
if (!parsed || typeof parsed !== 'object')
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
name: parsed.name || '',
|
|
34
|
+
description: parsed.description || '',
|
|
35
|
+
model: parsed.model,
|
|
36
|
+
tools: parsed.tools,
|
|
37
|
+
skills: parsed.skills,
|
|
38
|
+
mcpServers: parsed.mcpServers,
|
|
39
|
+
allowedAgents: parsed.allowedAgents,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Count subagent .md files in a workflow's subagents/ directory. */
|
|
47
|
+
export function countWorkflowSubagents(workflowDir) {
|
|
48
|
+
const subagentsDir = path.join(workflowDir, 'subagents');
|
|
49
|
+
if (!fs.existsSync(subagentsDir))
|
|
50
|
+
return 0;
|
|
51
|
+
try {
|
|
52
|
+
return fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md')).length;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Discover all workflow directories (those containing WORKFLOW.md) in a local path.
|
|
60
|
+
* Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,
|
|
61
|
+
* then falls back to scanning all immediate subdirectories.
|
|
62
|
+
*/
|
|
63
|
+
export function discoverWorkflowsFromRepo(repoPath) {
|
|
64
|
+
const results = [];
|
|
65
|
+
// The path itself may be a single workflow directory.
|
|
66
|
+
if (fs.existsSync(path.join(repoPath, 'WORKFLOW.md'))) {
|
|
67
|
+
const frontmatter = parseWorkflowFrontmatter(repoPath);
|
|
68
|
+
if (frontmatter) {
|
|
69
|
+
return [{
|
|
70
|
+
name: path.basename(repoPath),
|
|
71
|
+
path: repoPath,
|
|
72
|
+
frontmatter,
|
|
73
|
+
subagentCount: countWorkflowSubagents(repoPath),
|
|
74
|
+
}];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Try a workflows/ subdirectory first, then fall back to scanning root subdirectories.
|
|
78
|
+
const workflowsSubdir = path.join(repoPath, 'workflows');
|
|
79
|
+
const scanDir = fs.existsSync(workflowsSubdir) ? workflowsSubdir : repoPath;
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
89
|
+
continue;
|
|
90
|
+
const workflowPath = path.join(scanDir, entry.name);
|
|
91
|
+
const frontmatter = parseWorkflowFrontmatter(workflowPath);
|
|
92
|
+
if (frontmatter) {
|
|
93
|
+
results.push({
|
|
94
|
+
name: entry.name,
|
|
95
|
+
path: workflowPath,
|
|
96
|
+
frontmatter,
|
|
97
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* List all workflows in central storage.
|
|
105
|
+
* User layer (~/.agents/workflows/) wins over system (~/.agents-system/workflows/).
|
|
106
|
+
*/
|
|
107
|
+
export function listInstalledWorkflows() {
|
|
108
|
+
const result = new Map();
|
|
109
|
+
const extraRepos = getEnabledExtraRepos();
|
|
110
|
+
const searchDirs = [
|
|
111
|
+
getUserWorkflowsDir(),
|
|
112
|
+
getSystemWorkflowsDir(),
|
|
113
|
+
...extraRepos.map(r => path.join(r.dir, 'workflows')),
|
|
114
|
+
];
|
|
115
|
+
for (const dir of searchDirs) {
|
|
116
|
+
if (!fs.existsSync(dir))
|
|
117
|
+
continue;
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
127
|
+
continue;
|
|
128
|
+
if (result.has(entry.name))
|
|
129
|
+
continue; // Higher-priority layer already present
|
|
130
|
+
const workflowPath = path.join(dir, entry.name);
|
|
131
|
+
const frontmatter = parseWorkflowFrontmatter(workflowPath);
|
|
132
|
+
if (!frontmatter)
|
|
133
|
+
continue;
|
|
134
|
+
result.set(entry.name, {
|
|
135
|
+
name: entry.name,
|
|
136
|
+
path: workflowPath,
|
|
137
|
+
frontmatter,
|
|
138
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
/** Copy a workflow directory into user central storage (~/.agents/workflows/<name>/). */
|
|
145
|
+
export function installWorkflowCentrally(sourcePath, name) {
|
|
146
|
+
const targetPath = path.join(getUserWorkflowsDir(), name);
|
|
147
|
+
try {
|
|
148
|
+
fs.mkdirSync(getUserWorkflowsDir(), { recursive: true });
|
|
149
|
+
if (fs.existsSync(targetPath)) {
|
|
150
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
153
|
+
return { success: true };
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
return { success: false, error: err.message };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Move a workflow from user central storage to trash. */
|
|
160
|
+
export function removeWorkflow(name) {
|
|
161
|
+
const sourcePath = path.join(getUserWorkflowsDir(), name);
|
|
162
|
+
if (!fs.existsSync(sourcePath)) {
|
|
163
|
+
return { success: false, error: `Workflow '${name}' not found in ~/.agents/workflows/` };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const trashDir = getTrashWorkflowsDir();
|
|
167
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
168
|
+
fs.renameSync(sourcePath, path.join(trashDir, `${name}-${Date.now()}`));
|
|
169
|
+
return { success: true };
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return { success: false, error: err.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** List workflow names synced into a specific agent version home (at {versionHome}/workflows/). */
|
|
176
|
+
export function listWorkflowsForAgent(_agent, versionHome) {
|
|
177
|
+
const workflowsDir = path.join(versionHome, 'workflows');
|
|
178
|
+
if (!fs.existsSync(workflowsDir))
|
|
179
|
+
return [];
|
|
180
|
+
try {
|
|
181
|
+
return fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
182
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
|
|
183
|
+
.map(d => d.name);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
190
|
+
export function syncWorkflowToVersion(workflowPath, name, _agent, versionHome) {
|
|
191
|
+
const targetDir = path.join(versionHome, 'workflows', name);
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(path.join(versionHome, 'workflows'), { recursive: true });
|
|
194
|
+
if (fs.existsSync(targetDir)) {
|
|
195
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
fs.cpSync(workflowPath, targetDir, { recursive: true });
|
|
198
|
+
return { success: true };
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return { success: false, error: err.message };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Remove a workflow from a specific agent version home. */
|
|
205
|
+
export function removeWorkflowFromVersion(agent, version, name) {
|
|
206
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
207
|
+
const targetDir = path.join(versionHome, 'workflows', name);
|
|
208
|
+
if (!fs.existsSync(targetDir)) {
|
|
209
|
+
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
213
|
+
return { success: true };
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { success: false, error: err.message };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Iterate all installed (agent, version) pairs that support workflows. */
|
|
220
|
+
export function iterWorkflowsCapableVersions(filter) {
|
|
221
|
+
const result = [];
|
|
222
|
+
for (const agentId of WORKFLOW_CAPABLE_AGENTS) {
|
|
223
|
+
if (filter?.agent && filter.agent !== agentId)
|
|
224
|
+
continue;
|
|
225
|
+
const versions = listInstalledVersions(agentId);
|
|
226
|
+
for (const version of versions) {
|
|
227
|
+
if (filter?.version && filter.version !== version)
|
|
228
|
+
continue;
|
|
229
|
+
result.push({ agent: agentId, version });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -66,9 +66,7 @@
|
|
|
66
66
|
"node": ">=22.5.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@aws-sdk/client-s3": "3.1033.0",
|
|
70
69
|
"@inquirer/prompts": "^7.0.0",
|
|
71
|
-
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
72
70
|
"@types/proper-lockfile": "^4.1.4",
|
|
73
71
|
"@xterm/headless": "^6.0.0",
|
|
74
72
|
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
@@ -81,7 +79,6 @@
|
|
|
81
79
|
"node-pty": "1.1.0",
|
|
82
80
|
"ora": "^8.1.0",
|
|
83
81
|
"proper-lockfile": "^4.1.2",
|
|
84
|
-
"semver": "^7.6.0",
|
|
85
82
|
"simple-git": "3.36.0",
|
|
86
83
|
"smol-toml": "^1.6.1",
|
|
87
84
|
"yaml": "^2.8.3"
|
|
@@ -90,7 +87,6 @@
|
|
|
90
87
|
"@types/diff": "^6.0.0",
|
|
91
88
|
"@types/marked-terminal": "^6.1.1",
|
|
92
89
|
"@types/node": "^22.0.0",
|
|
93
|
-
"@types/semver": "^7.5.0",
|
|
94
90
|
"tsx": "^4.19.0",
|
|
95
91
|
"typescript": "^5.5.0",
|
|
96
92
|
"vitest": "^2.0.0"
|