@phnx-labs/agents-cli 1.20.59 → 1.20.60
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 +13 -1
- package/README.md +8 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +38 -1
- package/dist/lib/agents.js +6 -4
- package/dist/lib/hosts/dispatch.d.ts +36 -0
- package/dist/lib/hosts/dispatch.js +40 -2
- package/dist/lib/permissions.d.ts +14 -5
- package/dist/lib/permissions.js +139 -28
- package/dist/lib/plugins.d.ts +8 -0
- package/dist/lib/plugins.js +108 -0
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +46 -1
- package/dist/lib/shims.js +13 -3
- package/dist/lib/skills.js +14 -1
- package/dist/lib/staleness/detectors/permissions.js +28 -2
- package/dist/lib/staleness/detectors/subagents.js +20 -1
- package/dist/lib/staleness/detectors/workflows.js +33 -0
- package/dist/lib/staleness/writers/subagents.js +13 -5
- package/dist/lib/subagents.d.ts +10 -1
- package/dist/lib/subagents.js +102 -14
- package/dist/lib/versions.js +12 -2
- package/dist/lib/workflows.d.ts +5 -3
- package/dist/lib/workflows.js +246 -9
- package/package.json +1 -1
package/dist/lib/subagents.js
CHANGED
|
@@ -274,6 +274,39 @@ export function transformSubagentForDroid(subagentDir) {
|
|
|
274
274
|
* See GitHub docs for custom agents.
|
|
275
275
|
*/
|
|
276
276
|
export const transformSubagentForCopilot = transformSubagentForDroid;
|
|
277
|
+
/**
|
|
278
|
+
* Transform a subagent into Antigravity's custom-agent markdown shape.
|
|
279
|
+
*
|
|
280
|
+
* Antigravity exposes custom agents as Markdown files with YAML frontmatter,
|
|
281
|
+
* close to Gemini CLI subagents. Keep portable frontmatter fields and flatten
|
|
282
|
+
* sibling markdown files into the prompt body like the other markdown-backed
|
|
283
|
+
* agents.
|
|
284
|
+
*/
|
|
285
|
+
export function transformSubagentForAntigravity(subagentDir) {
|
|
286
|
+
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
287
|
+
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
288
|
+
const body = getSubagentBody(agentMd);
|
|
289
|
+
if (!frontmatter) {
|
|
290
|
+
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
291
|
+
}
|
|
292
|
+
const frontmatterYaml = yaml.stringify({
|
|
293
|
+
name: frontmatter.name,
|
|
294
|
+
description: frontmatter.description,
|
|
295
|
+
kind: 'local',
|
|
296
|
+
...(frontmatter.model && { model: frontmatter.model }),
|
|
297
|
+
}).trim();
|
|
298
|
+
let result = `---\n${frontmatterYaml}\n---\n\n${body}`;
|
|
299
|
+
const files = fs.readdirSync(subagentDir)
|
|
300
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
301
|
+
.sort();
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
304
|
+
const sectionName = file.replace('.md', '');
|
|
305
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
306
|
+
result += `\n\n## ${title}\n\n${content}`;
|
|
307
|
+
}
|
|
308
|
+
return `${result.trim()}\n`;
|
|
309
|
+
}
|
|
277
310
|
/**
|
|
278
311
|
* Transform a subagent into an OpenCode agent markdown file.
|
|
279
312
|
*
|
|
@@ -489,10 +522,10 @@ export function syncSubagentToOpenclaw(subagentDir, targetDir) {
|
|
|
489
522
|
* Install a subagent to a specific agent's home
|
|
490
523
|
*/
|
|
491
524
|
export function installSubagentToAgent(subagentDir, subagentName, agent, agentHome) {
|
|
492
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
493
|
-
// Claude / Grok: flatten to single .md under
|
|
494
|
-
|
|
495
|
-
const agentsDir = path.join(agentHome,
|
|
525
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
526
|
+
// Claude / Gemini / Grok: flatten to single .md under the native agents dir.
|
|
527
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
528
|
+
const agentsDir = path.join(agentHome, agentsRoot, 'agents');
|
|
496
529
|
if (!fs.existsSync(agentsDir)) {
|
|
497
530
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
498
531
|
}
|
|
@@ -543,6 +576,19 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
543
576
|
return { success: false, error: String(err) };
|
|
544
577
|
}
|
|
545
578
|
}
|
|
579
|
+
else if (agent === 'antigravity') {
|
|
580
|
+
// Antigravity: custom-agent markdown under ~/.gemini/config/agents/<name>/agent.md.
|
|
581
|
+
const agentDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
|
|
582
|
+
if (!fs.existsSync(agentDir))
|
|
583
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
584
|
+
try {
|
|
585
|
+
fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(subagentDir));
|
|
586
|
+
return { success: true };
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
return { success: false, error: String(err) };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
546
592
|
else if (agent === 'openclaw') {
|
|
547
593
|
// OpenClaw: copy full directory
|
|
548
594
|
const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
|
|
@@ -573,8 +619,8 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
573
619
|
*/
|
|
574
620
|
export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
575
621
|
try {
|
|
576
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
577
|
-
const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
|
|
622
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
623
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
578
624
|
const targetPath = safeJoin(path.join(agentHome, agentsRoot, 'agents'), `${subagentName}.md`);
|
|
579
625
|
if (fs.existsSync(targetPath)) {
|
|
580
626
|
fs.unlinkSync(targetPath);
|
|
@@ -604,6 +650,12 @@ export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
|
604
650
|
fs.unlinkSync(targetPath);
|
|
605
651
|
return { success: true };
|
|
606
652
|
}
|
|
653
|
+
else if (agent === 'antigravity') {
|
|
654
|
+
const targetDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
|
|
655
|
+
if (fs.existsSync(targetDir))
|
|
656
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
657
|
+
return { success: true };
|
|
658
|
+
}
|
|
607
659
|
else if (agent === 'openclaw') {
|
|
608
660
|
const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
|
|
609
661
|
if (fs.existsSync(targetDir)) {
|
|
@@ -655,16 +707,17 @@ export function subagentContentMatches(installedDir, sourceDir) {
|
|
|
655
707
|
// source of truth.
|
|
656
708
|
/**
|
|
657
709
|
* List subagents installed to a specific agent's home
|
|
658
|
-
* Claude: scans ~/.
|
|
710
|
+
* Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
|
|
659
711
|
* Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
|
|
660
712
|
* Kiro: scans ~/.kiro/agents/{name}.json
|
|
661
713
|
* OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
|
|
662
714
|
*/
|
|
663
715
|
export function listSubagentsForAgent(agentId, home) {
|
|
664
716
|
const subagents = [];
|
|
665
|
-
if (agentId === 'claude' || agentId === 'grok') {
|
|
666
|
-
// Claude / Grok: flat .md files in agents/
|
|
667
|
-
const
|
|
717
|
+
if (agentId === 'claude' || agentId === 'gemini' || agentId === 'grok') {
|
|
718
|
+
// Claude / Gemini / Grok: flat .md files in agents/
|
|
719
|
+
const agentsRoot = agentId === 'grok' ? '.grok' : agentId === 'gemini' ? '.gemini' : '.claude';
|
|
720
|
+
const agentsDir = path.join(home, agentsRoot, 'agents');
|
|
668
721
|
if (!fs.existsSync(agentsDir))
|
|
669
722
|
return subagents;
|
|
670
723
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
@@ -730,6 +783,20 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
730
783
|
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
731
784
|
}
|
|
732
785
|
}
|
|
786
|
+
else if (agentId === 'antigravity') {
|
|
787
|
+
const agentsDir = path.join(home, '.gemini', 'config', 'agents');
|
|
788
|
+
if (!fs.existsSync(agentsDir))
|
|
789
|
+
return subagents;
|
|
790
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
791
|
+
if (!entry.isDirectory())
|
|
792
|
+
continue;
|
|
793
|
+
const filePath = path.join(agentsDir, entry.name, 'agent.md');
|
|
794
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
795
|
+
continue;
|
|
796
|
+
const frontmatter = parseSubagentFrontmatter(filePath) ?? { name: entry.name, description: '' };
|
|
797
|
+
subagents.push({ name: entry.name, path: filePath, files: ['agent.md'], frontmatter });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
733
800
|
else if (agentId === 'copilot') {
|
|
734
801
|
// Copilot: flat `<name>.agent.md` files under ~/.copilot/agents/
|
|
735
802
|
const agentsDir = path.join(home, '.copilot', 'agents');
|
|
@@ -840,8 +907,9 @@ export function diffVersionSubagents(agent, version) {
|
|
|
840
907
|
}
|
|
841
908
|
}
|
|
842
909
|
// Check what's installed
|
|
843
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
844
|
-
const
|
|
910
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
911
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
912
|
+
const agentsDir = path.join(versionHome, agentsRoot, 'agents');
|
|
845
913
|
if (fs.existsSync(agentsDir)) {
|
|
846
914
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
847
915
|
if (!file.endsWith('.md'))
|
|
@@ -878,6 +946,19 @@ export function diffVersionSubagents(agent, version) {
|
|
|
878
946
|
}
|
|
879
947
|
}
|
|
880
948
|
}
|
|
949
|
+
else if (agent === 'antigravity') {
|
|
950
|
+
const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
|
|
951
|
+
if (fs.existsSync(agentsDir)) {
|
|
952
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
953
|
+
if (!entry.isDirectory())
|
|
954
|
+
continue;
|
|
955
|
+
if (!fs.existsSync(path.join(agentsDir, entry.name, 'agent.md')))
|
|
956
|
+
continue;
|
|
957
|
+
if (!discovered.has(entry.name))
|
|
958
|
+
orphans.push(entry.name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
881
962
|
else if (agent === 'openclaw') {
|
|
882
963
|
const openclawDir = path.join(versionHome, '.openclaw');
|
|
883
964
|
if (fs.existsSync(openclawDir)) {
|
|
@@ -932,8 +1013,8 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
932
1013
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
933
1014
|
const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
|
|
934
1015
|
try {
|
|
935
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
936
|
-
const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
|
|
1016
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
1017
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
937
1018
|
const targetPath = path.join(versionHome, agentsRoot, 'agents', `${subagentName}.md`);
|
|
938
1019
|
if (fs.existsSync(targetPath)) {
|
|
939
1020
|
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
@@ -961,6 +1042,13 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
961
1042
|
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
962
1043
|
}
|
|
963
1044
|
}
|
|
1045
|
+
else if (agent === 'antigravity') {
|
|
1046
|
+
const targetDir = path.join(versionHome, '.gemini', 'config', 'agents', subagentName);
|
|
1047
|
+
if (fs.existsSync(targetDir)) {
|
|
1048
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1049
|
+
fs.renameSync(targetDir, path.join(trashDir, stamp));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
964
1052
|
else if (agent === 'copilot') {
|
|
965
1053
|
const targetPath = path.join(versionHome, '.copilot', 'agents', `${subagentName}.agent.md`);
|
|
966
1054
|
if (fs.existsSync(targetPath)) {
|
package/dist/lib/versions.js
CHANGED
|
@@ -2462,9 +2462,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2462
2462
|
// reads only user + system layers (project excluded for the same defense
|
|
2463
2463
|
// as commands/skills/hooks).
|
|
2464
2464
|
const subagentsWriter = getWriter('subagents', agent);
|
|
2465
|
-
const
|
|
2465
|
+
const subagentsGate = supports(agent, 'subagents', version);
|
|
2466
|
+
const subagentsRequested = selection
|
|
2466
2467
|
? resolveSelection(selection.subagents, available.subagents)
|
|
2467
2468
|
: (subagentsWriter ? available.subagents : []);
|
|
2469
|
+
const subagentsToSync = subagentsGate.ok ? subagentsRequested : [];
|
|
2470
|
+
if (subagentsRequested.length > 0 && !subagentsGate.ok) {
|
|
2471
|
+
console.warn(explainSkip(agent, 'subagents', subagentsGate, version) + ' -- skipped');
|
|
2472
|
+
}
|
|
2468
2473
|
if (subagentsToSync.length > 0 && subagentsWriter) {
|
|
2469
2474
|
const r = subagentsWriter.write({ version, versionHome, selection: subagentsToSync, cwd });
|
|
2470
2475
|
result.subagents.push(...r.synced);
|
|
@@ -2499,9 +2504,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2499
2504
|
}
|
|
2500
2505
|
// Sync workflows — dispatch through WRITERS.workflows.
|
|
2501
2506
|
const workflowsWriter = getWriter('workflows', agent);
|
|
2502
|
-
const
|
|
2507
|
+
const workflowsGate = supports(agent, 'workflows', version);
|
|
2508
|
+
const workflowsRequested = selection
|
|
2503
2509
|
? resolveSelection(selection.workflows, available.workflows)
|
|
2504
2510
|
: (workflowsWriter ? available.workflows : []);
|
|
2511
|
+
const workflowsToSync = workflowsGate.ok ? workflowsRequested : [];
|
|
2512
|
+
if (workflowsRequested.length > 0 && !workflowsGate.ok) {
|
|
2513
|
+
console.warn(explainSkip(agent, 'workflows', workflowsGate, version) + ' -- skipped');
|
|
2514
|
+
}
|
|
2505
2515
|
if (workflowsToSync.length > 0 && workflowsWriter) {
|
|
2506
2516
|
const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
|
|
2507
2517
|
result.workflows.push(...r.synced);
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -263,6 +263,8 @@ export declare function resolveAllowedSubagents(available: string[], allowedAgen
|
|
|
263
263
|
export declare function pruneStaleWorkflowSubagents(sharedAgentsDir: string, workflowSubagentFiles: string[], allowedStems: string[]): string[];
|
|
264
264
|
/** Count subagent .md files in a workflow's subagents/ directory. */
|
|
265
265
|
export declare function countWorkflowSubagents(workflowDir: string): number;
|
|
266
|
+
/** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
|
|
267
|
+
export declare function transformWorkflowForKimi(workflowPath: string, name: string): string;
|
|
266
268
|
/**
|
|
267
269
|
* Resolve an `agents run <workflow>` reference.
|
|
268
270
|
*
|
|
@@ -291,10 +293,10 @@ export declare function removeWorkflow(name: string): {
|
|
|
291
293
|
success: boolean;
|
|
292
294
|
error?: string;
|
|
293
295
|
};
|
|
294
|
-
/** List workflow names synced into a specific agent version home
|
|
295
|
-
export declare function listWorkflowsForAgent(
|
|
296
|
+
/** List workflow names synced into a specific agent version home. */
|
|
297
|
+
export declare function listWorkflowsForAgent(agent: AgentId, versionHome: string): string[];
|
|
296
298
|
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
297
|
-
export declare function syncWorkflowToVersion(workflowPath: string, name: string,
|
|
299
|
+
export declare function syncWorkflowToVersion(workflowPath: string, name: string, agent: AgentId, versionHome: string): {
|
|
298
300
|
success: boolean;
|
|
299
301
|
error?: string;
|
|
300
302
|
};
|
package/dist/lib/workflows.js
CHANGED
|
@@ -56,6 +56,19 @@ export function parseWorkflowFrontmatter(workflowDir) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
function readWorkflowBody(workflowDir) {
|
|
60
|
+
const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
|
|
61
|
+
if (!fs.existsSync(workflowMdPath))
|
|
62
|
+
return '';
|
|
63
|
+
const content = fs.readFileSync(workflowMdPath, 'utf-8');
|
|
64
|
+
const lines = content.split('\n');
|
|
65
|
+
if (lines[0] !== '---')
|
|
66
|
+
return content.trim();
|
|
67
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
68
|
+
if (endIndex < 0)
|
|
69
|
+
return content.trim();
|
|
70
|
+
return lines.slice(endIndex + 2).join('\n').trim();
|
|
71
|
+
}
|
|
59
72
|
/**
|
|
60
73
|
* Defensively coerce a frontmatter `loop:` value into a LoopConfigRaw.
|
|
61
74
|
*
|
|
@@ -315,6 +328,47 @@ export function countWorkflowSubagents(workflowDir) {
|
|
|
315
328
|
return 0;
|
|
316
329
|
}
|
|
317
330
|
}
|
|
331
|
+
function getWorkflowBody(workflowDir) {
|
|
332
|
+
const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
|
|
333
|
+
if (!fs.existsSync(workflowMdPath))
|
|
334
|
+
return '';
|
|
335
|
+
const content = fs.readFileSync(workflowMdPath, 'utf-8');
|
|
336
|
+
const lines = content.split('\n');
|
|
337
|
+
if (lines[0] === '---') {
|
|
338
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
339
|
+
if (endIndex >= 0)
|
|
340
|
+
return lines.slice(endIndex + 2).join('\n').trim();
|
|
341
|
+
}
|
|
342
|
+
return content.trim();
|
|
343
|
+
}
|
|
344
|
+
function indentD2BlockString(content) {
|
|
345
|
+
return content
|
|
346
|
+
.split('\n')
|
|
347
|
+
.map(line => ` ${line}`)
|
|
348
|
+
.join('\n');
|
|
349
|
+
}
|
|
350
|
+
function containsFlowDiagram(content) {
|
|
351
|
+
return /```(?:mermaid|d2)\b/i.test(content);
|
|
352
|
+
}
|
|
353
|
+
const KIMI_WORKFLOW_MARKER = 'agents_workflow';
|
|
354
|
+
/** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
|
|
355
|
+
export function transformWorkflowForKimi(workflowPath, name) {
|
|
356
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
357
|
+
if (!fm)
|
|
358
|
+
throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
|
|
359
|
+
const body = getWorkflowBody(workflowPath);
|
|
360
|
+
const frontmatter = yaml.stringify({
|
|
361
|
+
name,
|
|
362
|
+
description: fm.description,
|
|
363
|
+
type: 'flow',
|
|
364
|
+
[KIMI_WORKFLOW_MARKER]: name,
|
|
365
|
+
}).trim();
|
|
366
|
+
if (containsFlowDiagram(body)) {
|
|
367
|
+
return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
|
|
368
|
+
}
|
|
369
|
+
const instructions = (body || fm.description).trim();
|
|
370
|
+
return `---\n${frontmatter}\n---\n\n\`\`\`d2\nBEGIN -> step -> END\nstep: |md\n${indentD2BlockString(instructions)}\n|\n\`\`\`\n`;
|
|
371
|
+
}
|
|
318
372
|
function expandWorkflowPath(ref) {
|
|
319
373
|
if (ref === '~')
|
|
320
374
|
return process.env.HOME ?? ref;
|
|
@@ -473,9 +527,36 @@ export function removeWorkflow(name) {
|
|
|
473
527
|
return { success: false, error: err.message };
|
|
474
528
|
}
|
|
475
529
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
530
|
+
function workflowTargetRoot(agent, versionHome) {
|
|
531
|
+
if (agent === 'kimi')
|
|
532
|
+
return path.join(versionHome, '.kimi-code', 'skills');
|
|
533
|
+
return path.join(versionHome, 'workflows');
|
|
534
|
+
}
|
|
535
|
+
/** List workflow names synced into a specific agent version home. */
|
|
536
|
+
export function listWorkflowsForAgent(agent, versionHome) {
|
|
537
|
+
if (agent === 'kimi') {
|
|
538
|
+
const skillsDir = workflowTargetRoot(agent, versionHome);
|
|
539
|
+
if (!fs.existsSync(skillsDir))
|
|
540
|
+
return [];
|
|
541
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
542
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
|
|
543
|
+
.filter(d => kimiWorkflowMarker(path.join(skillsDir, d.name, 'SKILL.md')) === d.name)
|
|
544
|
+
.map(d => d.name);
|
|
545
|
+
}
|
|
546
|
+
if (agent === 'goose') {
|
|
547
|
+
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
548
|
+
if (!fs.existsSync(recipesDir))
|
|
549
|
+
return [];
|
|
550
|
+
try {
|
|
551
|
+
return fs.readdirSync(recipesDir, { withFileTypes: true })
|
|
552
|
+
.filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
|
|
553
|
+
.map(d => d.name.slice(0, -'.yaml'.length));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const workflowsDir = workflowTargetRoot(agent, versionHome);
|
|
479
560
|
if (!fs.existsSync(workflowsDir))
|
|
480
561
|
return [];
|
|
481
562
|
try {
|
|
@@ -487,11 +568,120 @@ export function listWorkflowsForAgent(_agent, versionHome) {
|
|
|
487
568
|
return [];
|
|
488
569
|
}
|
|
489
570
|
}
|
|
571
|
+
function parseSubrecipeFrontmatter(filePath) {
|
|
572
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
573
|
+
const lines = content.split('\n');
|
|
574
|
+
if (lines[0] !== '---')
|
|
575
|
+
return { body: content.trim() };
|
|
576
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
577
|
+
if (endIndex < 0)
|
|
578
|
+
return { body: content.trim() };
|
|
579
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
580
|
+
let parsed = {};
|
|
581
|
+
try {
|
|
582
|
+
const value = yaml.parse(frontmatter);
|
|
583
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
584
|
+
parsed = value;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch { /* ignore malformed subagent frontmatter */ }
|
|
588
|
+
return {
|
|
589
|
+
name: typeof parsed.name === 'string' ? parsed.name : undefined,
|
|
590
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
591
|
+
body: lines.slice(endIndex + 2).join('\n').trim(),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function selectedWorkflowSubagents(workflowPath, allowedAgents) {
|
|
595
|
+
const subagentsDir = path.join(workflowPath, 'subagents');
|
|
596
|
+
if (!fs.existsSync(subagentsDir))
|
|
597
|
+
return [];
|
|
598
|
+
const allowed = allowedAgents ? new Set(allowedAgents) : null;
|
|
599
|
+
return fs.readdirSync(subagentsDir, { withFileTypes: true })
|
|
600
|
+
.filter(e => e.isFile() && e.name.endsWith('.md') && !e.name.startsWith('.'))
|
|
601
|
+
.map(e => e.name.slice(0, -'.md'.length))
|
|
602
|
+
.filter(name => !allowed || allowed.has(name))
|
|
603
|
+
.sort();
|
|
604
|
+
}
|
|
605
|
+
function writeGooseSubrecipe(workflowPath, subrecipeName, destDir) {
|
|
606
|
+
const sourcePath = path.join(workflowPath, 'subagents', `${subrecipeName}.md`);
|
|
607
|
+
const parsed = parseSubrecipeFrontmatter(sourcePath);
|
|
608
|
+
const body = parsed.body || parsed.description || subrecipeName;
|
|
609
|
+
const recipe = {
|
|
610
|
+
version: '1.0.0',
|
|
611
|
+
title: parsed.name || subrecipeName,
|
|
612
|
+
description: parsed.description || `Subrecipe for ${subrecipeName}`,
|
|
613
|
+
instructions: body,
|
|
614
|
+
prompt: body,
|
|
615
|
+
};
|
|
616
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
617
|
+
fs.writeFileSync(path.join(destDir, `${subrecipeName}.yaml`), yaml.stringify(recipe), 'utf-8');
|
|
618
|
+
}
|
|
619
|
+
function syncWorkflowToGooseRecipe(workflowPath, name, versionHome) {
|
|
620
|
+
const frontmatter = parseWorkflowFrontmatter(workflowPath);
|
|
621
|
+
if (!frontmatter) {
|
|
622
|
+
return { success: false, error: `Workflow '${name}' has invalid WORKFLOW.md frontmatter` };
|
|
623
|
+
}
|
|
624
|
+
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
625
|
+
const recipePath = path.join(recipesDir, `${name}.yaml`);
|
|
626
|
+
const subrecipesDir = path.join(recipesDir, `${name}.subrecipes`);
|
|
627
|
+
const body = readWorkflowBody(workflowPath) || frontmatter.description || name;
|
|
628
|
+
const subagents = selectedWorkflowSubagents(workflowPath, frontmatter.allowedAgents);
|
|
629
|
+
const recipe = {
|
|
630
|
+
version: '1.0.0',
|
|
631
|
+
title: frontmatter.name || name,
|
|
632
|
+
description: frontmatter.description || name,
|
|
633
|
+
instructions: body,
|
|
634
|
+
prompt: body,
|
|
635
|
+
};
|
|
636
|
+
if (frontmatter.model) {
|
|
637
|
+
recipe.settings = { goose_model: frontmatter.model };
|
|
638
|
+
}
|
|
639
|
+
if (subagents.length > 0) {
|
|
640
|
+
recipe.sub_recipes = subagents.map(subagentName => ({
|
|
641
|
+
name: subagentName,
|
|
642
|
+
path: `./${name}.subrecipes/${subagentName}.yaml`,
|
|
643
|
+
description: `Workflow subrecipe ${subagentName}`,
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
fs.mkdirSync(recipesDir, { recursive: true });
|
|
648
|
+
if (fs.existsSync(subrecipesDir)) {
|
|
649
|
+
fs.rmSync(subrecipesDir, { recursive: true, force: true });
|
|
650
|
+
}
|
|
651
|
+
for (const subagentName of subagents) {
|
|
652
|
+
writeGooseSubrecipe(workflowPath, subagentName, subrecipesDir);
|
|
653
|
+
}
|
|
654
|
+
fs.writeFileSync(recipePath, yaml.stringify(recipe), 'utf-8');
|
|
655
|
+
return { success: true };
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
return { success: false, error: err.message };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
490
661
|
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
491
|
-
export function syncWorkflowToVersion(workflowPath, name,
|
|
492
|
-
|
|
662
|
+
export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
|
|
663
|
+
if (agent === 'goose') {
|
|
664
|
+
return syncWorkflowToGooseRecipe(workflowPath, name, versionHome);
|
|
665
|
+
}
|
|
493
666
|
try {
|
|
494
|
-
|
|
667
|
+
if (agent === 'kimi') {
|
|
668
|
+
const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
669
|
+
const targetFile = path.join(targetDir, 'SKILL.md');
|
|
670
|
+
if (fs.existsSync(targetFile)) {
|
|
671
|
+
const marker = kimiWorkflowMarker(targetFile);
|
|
672
|
+
if (marker !== name) {
|
|
673
|
+
return { success: false, error: `Kimi skill '${name}' already exists and is not managed by agents-cli` };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
677
|
+
fs.writeFileSync(targetFile, transformWorkflowForKimi(workflowPath, name), 'utf-8');
|
|
678
|
+
return { success: true };
|
|
679
|
+
}
|
|
680
|
+
if (agent === 'antigravity') {
|
|
681
|
+
return { success: false, error: 'Antigravity workflow sync is not supported for version homes' };
|
|
682
|
+
}
|
|
683
|
+
const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
684
|
+
fs.mkdirSync(workflowTargetRoot(agent, versionHome), { recursive: true });
|
|
495
685
|
if (fs.existsSync(targetDir)) {
|
|
496
686
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
497
687
|
}
|
|
@@ -505,18 +695,65 @@ export function syncWorkflowToVersion(workflowPath, name, _agent, versionHome) {
|
|
|
505
695
|
/** Remove a workflow from a specific agent version home. */
|
|
506
696
|
export function removeWorkflowFromVersion(agent, version, name) {
|
|
507
697
|
const versionHome = getVersionHomePath(agent, version);
|
|
508
|
-
|
|
509
|
-
|
|
698
|
+
if (agent === 'antigravity') {
|
|
699
|
+
return { success: false, error: 'Antigravity workflow sync is not supported for version homes' };
|
|
700
|
+
}
|
|
701
|
+
if (agent === 'goose') {
|
|
702
|
+
const recipePath = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.yaml`);
|
|
703
|
+
const subrecipesDir = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.subrecipes`);
|
|
704
|
+
if (!fs.existsSync(recipePath) && !fs.existsSync(subrecipesDir)) {
|
|
705
|
+
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
if (fs.existsSync(recipePath))
|
|
709
|
+
fs.rmSync(recipePath, { force: true });
|
|
710
|
+
if (fs.existsSync(subrecipesDir))
|
|
711
|
+
fs.rmSync(subrecipesDir, { recursive: true, force: true });
|
|
712
|
+
return { success: true };
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
return { success: false, error: err.message };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const targetPath = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
719
|
+
if (!fs.existsSync(targetPath)) {
|
|
510
720
|
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
511
721
|
}
|
|
512
722
|
try {
|
|
513
|
-
|
|
723
|
+
if (agent === 'kimi') {
|
|
724
|
+
const targetFile = path.join(targetPath, 'SKILL.md');
|
|
725
|
+
if (kimiWorkflowMarker(targetFile) !== name) {
|
|
726
|
+
return { success: false, error: `Kimi skill '${name}' is not managed by agents-cli` };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
514
730
|
return { success: true };
|
|
515
731
|
}
|
|
516
732
|
catch (err) {
|
|
517
733
|
return { success: false, error: err.message };
|
|
518
734
|
}
|
|
519
735
|
}
|
|
736
|
+
function parseSkillFrontmatter(filePath) {
|
|
737
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
738
|
+
const lines = content.split('\n');
|
|
739
|
+
if (lines[0] !== '---')
|
|
740
|
+
return null;
|
|
741
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
742
|
+
if (endIndex < 0)
|
|
743
|
+
return null;
|
|
744
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
745
|
+
const parsed = yaml.parse(frontmatter);
|
|
746
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
747
|
+
}
|
|
748
|
+
function kimiWorkflowMarker(filePath) {
|
|
749
|
+
try {
|
|
750
|
+
const fm = parseSkillFrontmatter(filePath);
|
|
751
|
+
return fm?.type === 'flow' && typeof fm.agents_workflow === 'string' ? fm.agents_workflow : null;
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
520
757
|
/** Iterate all installed (agent, version) pairs that support workflows. */
|
|
521
758
|
export function iterWorkflowsCapableVersions(filter) {
|
|
522
759
|
const result = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.60",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|