@phnx-labs/agents-cli 1.20.59 → 1.20.61
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 +17 -1
- package/README.md +9 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +38 -1
- package/dist/commands/routines.js +2 -0
- package/dist/lib/agents.js +92 -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 +23 -0
- package/dist/lib/routines.js +64 -1
- package/dist/lib/runner.d.ts +7 -0
- package/dist/lib/runner.js +34 -6
- 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 +62 -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/tmux/session.d.ts +13 -7
- package/dist/lib/tmux/session.js +23 -8
- package/dist/lib/versions.js +84 -2
- package/dist/lib/workflows.d.ts +14 -3
- package/dist/lib/workflows.js +328 -9
- package/package.json +1 -1
package/dist/lib/versions.js
CHANGED
|
@@ -1148,6 +1148,72 @@ export function setGlobalDefault(agent, version) {
|
|
|
1148
1148
|
}
|
|
1149
1149
|
writeMeta(meta);
|
|
1150
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Grok's official installer writes into ~/.grok/downloads, which (because we
|
|
1153
|
+
* symlink ~/.grok to the active version home) resolves to the PREVIOUS default
|
|
1154
|
+
* home during `agents add grok@<new>`. Move the freshly-downloaded binary and
|
|
1155
|
+
* its generic platform copy into the target version's isolated home so
|
|
1156
|
+
* `listInstalledVersions` and the shim resolve the right binary.
|
|
1157
|
+
*/
|
|
1158
|
+
function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
1159
|
+
const hostGrokLink = path.join(getHomeDir(), agentConfigDirName('grok'));
|
|
1160
|
+
let sourceDownloads;
|
|
1161
|
+
try {
|
|
1162
|
+
sourceDownloads = path.join(fs.readlinkSync(hostGrokLink), 'downloads');
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
sourceDownloads = path.join(hostGrokLink, 'downloads');
|
|
1166
|
+
}
|
|
1167
|
+
const targetDownloads = path.join(getVersionHomePath('grok', installedVersion), agentConfigDirName('grok'), 'downloads');
|
|
1168
|
+
if (!fs.existsSync(sourceDownloads))
|
|
1169
|
+
return;
|
|
1170
|
+
if (path.resolve(sourceDownloads) === path.resolve(targetDownloads))
|
|
1171
|
+
return;
|
|
1172
|
+
const entries = fs.readdirSync(sourceDownloads).filter((e) => e.startsWith('grok-'));
|
|
1173
|
+
if (entries.length === 0)
|
|
1174
|
+
return;
|
|
1175
|
+
fs.mkdirSync(targetDownloads, { recursive: true });
|
|
1176
|
+
const escapedVersion = installedVersion.replace(/\./g, '\\.');
|
|
1177
|
+
const versionedPattern = new RegExp(`^grok-${escapedVersion}-`);
|
|
1178
|
+
const movedPaths = [];
|
|
1179
|
+
// Move the versioned binary first.
|
|
1180
|
+
for (const entry of entries) {
|
|
1181
|
+
if (!versionedPattern.test(entry))
|
|
1182
|
+
continue;
|
|
1183
|
+
const src = path.join(sourceDownloads, entry);
|
|
1184
|
+
const dst = path.join(targetDownloads, entry);
|
|
1185
|
+
if (fs.existsSync(dst))
|
|
1186
|
+
continue;
|
|
1187
|
+
try {
|
|
1188
|
+
fs.renameSync(src, dst);
|
|
1189
|
+
movedPaths.push(dst);
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
/* ignore per-file failures */
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (movedPaths.length === 0)
|
|
1196
|
+
return;
|
|
1197
|
+
// The installer also creates a generic platform binary (e.g. grok-macos-aarch64)
|
|
1198
|
+
// that is a copy of the versioned binary. Move it too if its size matches.
|
|
1199
|
+
const movedSize = fs.statSync(movedPaths[0]).size;
|
|
1200
|
+
for (const entry of entries) {
|
|
1201
|
+
if (versionedPattern.test(entry))
|
|
1202
|
+
continue; // already handled
|
|
1203
|
+
const src = path.join(sourceDownloads, entry);
|
|
1204
|
+
const dst = path.join(targetDownloads, entry);
|
|
1205
|
+
if (fs.existsSync(dst))
|
|
1206
|
+
continue;
|
|
1207
|
+
try {
|
|
1208
|
+
if (fs.statSync(src).size === movedSize) {
|
|
1209
|
+
fs.renameSync(src, dst);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
/* ignore per-file failures */
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1151
1217
|
/**
|
|
1152
1218
|
* Install a specific version of an agent.
|
|
1153
1219
|
*/
|
|
@@ -1204,6 +1270,12 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1204
1270
|
const versionDir = getVersionDir(agent, installedVersion);
|
|
1205
1271
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1206
1272
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1273
|
+
// Grok's installer drops the binary into ~/.grok/downloads, which currently
|
|
1274
|
+
// resolves to the PREVIOUS default home. Move it into the target version home
|
|
1275
|
+
// so version isolation is correct.
|
|
1276
|
+
if (agent === 'grok') {
|
|
1277
|
+
relocateGrokBinaryToVersionHome(installedVersion);
|
|
1278
|
+
}
|
|
1207
1279
|
// Symlink the installed binary into the version's node_modules/.bin so
|
|
1208
1280
|
// listInstalledVersions (which checks getBinaryPath) sees this version as
|
|
1209
1281
|
// installed. Without this, `agents add antigravity@latest` succeeds
|
|
@@ -2462,9 +2534,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2462
2534
|
// reads only user + system layers (project excluded for the same defense
|
|
2463
2535
|
// as commands/skills/hooks).
|
|
2464
2536
|
const subagentsWriter = getWriter('subagents', agent);
|
|
2465
|
-
const
|
|
2537
|
+
const subagentsGate = supports(agent, 'subagents', version);
|
|
2538
|
+
const subagentsRequested = selection
|
|
2466
2539
|
? resolveSelection(selection.subagents, available.subagents)
|
|
2467
2540
|
: (subagentsWriter ? available.subagents : []);
|
|
2541
|
+
const subagentsToSync = subagentsGate.ok ? subagentsRequested : [];
|
|
2542
|
+
if (subagentsRequested.length > 0 && !subagentsGate.ok) {
|
|
2543
|
+
console.warn(explainSkip(agent, 'subagents', subagentsGate, version) + ' -- skipped');
|
|
2544
|
+
}
|
|
2468
2545
|
if (subagentsToSync.length > 0 && subagentsWriter) {
|
|
2469
2546
|
const r = subagentsWriter.write({ version, versionHome, selection: subagentsToSync, cwd });
|
|
2470
2547
|
result.subagents.push(...r.synced);
|
|
@@ -2499,9 +2576,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2499
2576
|
}
|
|
2500
2577
|
// Sync workflows — dispatch through WRITERS.workflows.
|
|
2501
2578
|
const workflowsWriter = getWriter('workflows', agent);
|
|
2502
|
-
const
|
|
2579
|
+
const workflowsGate = supports(agent, 'workflows', version);
|
|
2580
|
+
const workflowsRequested = selection
|
|
2503
2581
|
? resolveSelection(selection.workflows, available.workflows)
|
|
2504
2582
|
: (workflowsWriter ? available.workflows : []);
|
|
2583
|
+
const workflowsToSync = workflowsGate.ok ? workflowsRequested : [];
|
|
2584
|
+
if (workflowsRequested.length > 0 && !workflowsGate.ok) {
|
|
2585
|
+
console.warn(explainSkip(agent, 'workflows', workflowsGate, version) + ' -- skipped');
|
|
2586
|
+
}
|
|
2505
2587
|
if (workflowsToSync.length > 0 && workflowsWriter) {
|
|
2506
2588
|
const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
|
|
2507
2589
|
result.workflows.push(...r.synced);
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -263,6 +263,17 @@ 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;
|
|
268
|
+
/**
|
|
269
|
+
* Convert a canonical agents-cli workflow bundle into an Antigravity workflow
|
|
270
|
+
* markdown file. Antigravity discovers workflows as flat `<name>.md` files under
|
|
271
|
+
* `~/.gemini/config/global_workflows/` (scanned by `agy` at startup) and exposes
|
|
272
|
+
* each as a `/<name>` slash command. Frontmatter carries the required `description`
|
|
273
|
+
* plus the shared `agents_workflow` ownership marker so agents-cli never clobbers a
|
|
274
|
+
* user-authored workflow of the same name.
|
|
275
|
+
*/
|
|
276
|
+
export declare function transformWorkflowForAntigravity(workflowPath: string, name: string): string;
|
|
266
277
|
/**
|
|
267
278
|
* Resolve an `agents run <workflow>` reference.
|
|
268
279
|
*
|
|
@@ -291,10 +302,10 @@ export declare function removeWorkflow(name: string): {
|
|
|
291
302
|
success: boolean;
|
|
292
303
|
error?: string;
|
|
293
304
|
};
|
|
294
|
-
/** List workflow names synced into a specific agent version home
|
|
295
|
-
export declare function listWorkflowsForAgent(
|
|
305
|
+
/** List workflow names synced into a specific agent version home. */
|
|
306
|
+
export declare function listWorkflowsForAgent(agent: AgentId, versionHome: string): string[];
|
|
296
307
|
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
297
|
-
export declare function syncWorkflowToVersion(workflowPath: string, name: string,
|
|
308
|
+
export declare function syncWorkflowToVersion(workflowPath: string, name: string, agent: AgentId, versionHome: string): {
|
|
298
309
|
success: boolean;
|
|
299
310
|
error?: string;
|
|
300
311
|
};
|
package/dist/lib/workflows.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* are composed at runtime by `agents run <workflow>`.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
9
10
|
import * as path from 'path';
|
|
10
11
|
import * as yaml from 'yaml';
|
|
11
12
|
import { capableAgents } from './capabilities.js';
|
|
@@ -56,6 +57,19 @@ export function parseWorkflowFrontmatter(workflowDir) {
|
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
function readWorkflowBody(workflowDir) {
|
|
61
|
+
const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
|
|
62
|
+
if (!fs.existsSync(workflowMdPath))
|
|
63
|
+
return '';
|
|
64
|
+
const content = fs.readFileSync(workflowMdPath, 'utf-8');
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
if (lines[0] !== '---')
|
|
67
|
+
return content.trim();
|
|
68
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
69
|
+
if (endIndex < 0)
|
|
70
|
+
return content.trim();
|
|
71
|
+
return lines.slice(endIndex + 2).join('\n').trim();
|
|
72
|
+
}
|
|
59
73
|
/**
|
|
60
74
|
* Defensively coerce a frontmatter `loop:` value into a LoopConfigRaw.
|
|
61
75
|
*
|
|
@@ -315,6 +329,67 @@ export function countWorkflowSubagents(workflowDir) {
|
|
|
315
329
|
return 0;
|
|
316
330
|
}
|
|
317
331
|
}
|
|
332
|
+
function getWorkflowBody(workflowDir) {
|
|
333
|
+
const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
|
|
334
|
+
if (!fs.existsSync(workflowMdPath))
|
|
335
|
+
return '';
|
|
336
|
+
const content = fs.readFileSync(workflowMdPath, 'utf-8');
|
|
337
|
+
const lines = content.split('\n');
|
|
338
|
+
if (lines[0] === '---') {
|
|
339
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
340
|
+
if (endIndex >= 0)
|
|
341
|
+
return lines.slice(endIndex + 2).join('\n').trim();
|
|
342
|
+
}
|
|
343
|
+
return content.trim();
|
|
344
|
+
}
|
|
345
|
+
function indentD2BlockString(content) {
|
|
346
|
+
return content
|
|
347
|
+
.split('\n')
|
|
348
|
+
.map(line => ` ${line}`)
|
|
349
|
+
.join('\n');
|
|
350
|
+
}
|
|
351
|
+
function containsFlowDiagram(content) {
|
|
352
|
+
return /```(?:mermaid|d2)\b/i.test(content);
|
|
353
|
+
}
|
|
354
|
+
const KIMI_WORKFLOW_MARKER = 'agents_workflow';
|
|
355
|
+
/** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
|
|
356
|
+
export function transformWorkflowForKimi(workflowPath, name) {
|
|
357
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
358
|
+
if (!fm)
|
|
359
|
+
throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
|
|
360
|
+
const body = getWorkflowBody(workflowPath);
|
|
361
|
+
const frontmatter = yaml.stringify({
|
|
362
|
+
name,
|
|
363
|
+
description: fm.description,
|
|
364
|
+
type: 'flow',
|
|
365
|
+
[KIMI_WORKFLOW_MARKER]: name,
|
|
366
|
+
}).trim();
|
|
367
|
+
if (containsFlowDiagram(body)) {
|
|
368
|
+
return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
|
|
369
|
+
}
|
|
370
|
+
const instructions = (body || fm.description).trim();
|
|
371
|
+
return `---\n${frontmatter}\n---\n\n\`\`\`d2\nBEGIN -> step -> END\nstep: |md\n${indentD2BlockString(instructions)}\n|\n\`\`\`\n`;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Convert a canonical agents-cli workflow bundle into an Antigravity workflow
|
|
375
|
+
* markdown file. Antigravity discovers workflows as flat `<name>.md` files under
|
|
376
|
+
* `~/.gemini/config/global_workflows/` (scanned by `agy` at startup) and exposes
|
|
377
|
+
* each as a `/<name>` slash command. Frontmatter carries the required `description`
|
|
378
|
+
* plus the shared `agents_workflow` ownership marker so agents-cli never clobbers a
|
|
379
|
+
* user-authored workflow of the same name.
|
|
380
|
+
*/
|
|
381
|
+
export function transformWorkflowForAntigravity(workflowPath, name) {
|
|
382
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
383
|
+
if (!fm)
|
|
384
|
+
throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
|
|
385
|
+
const body = getWorkflowBody(workflowPath) || fm.description;
|
|
386
|
+
const frontmatter = yaml.stringify({
|
|
387
|
+
description: fm.description,
|
|
388
|
+
name: fm.name || name,
|
|
389
|
+
[KIMI_WORKFLOW_MARKER]: name,
|
|
390
|
+
}).trim();
|
|
391
|
+
return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
|
|
392
|
+
}
|
|
318
393
|
function expandWorkflowPath(ref) {
|
|
319
394
|
if (ref === '~')
|
|
320
395
|
return process.env.HOME ?? ref;
|
|
@@ -473,9 +548,65 @@ export function removeWorkflow(name) {
|
|
|
473
548
|
return { success: false, error: err.message };
|
|
474
549
|
}
|
|
475
550
|
}
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
|
|
551
|
+
/**
|
|
552
|
+
* Antigravity user workflows are NOT version-isolated. `agy` scans a single,
|
|
553
|
+
* shared, HOME-global directory at startup — `~/.gemini/config/global_workflows/`
|
|
554
|
+
* — and that dir is a real directory in the user's home, never symlinked into a
|
|
555
|
+
* per-version home (only `~/.gemini/antigravity-cli` is version-scoped). Writing
|
|
556
|
+
* into a version home therefore lands somewhere agy never reads. So every
|
|
557
|
+
* antigravity version resolves to the same real shared dir; `versionHome` is
|
|
558
|
+
* intentionally ignored. (Verified via strace of `agy`: it opens
|
|
559
|
+
* `$HOME/.gemini/config/global_workflows/<name>.md` and never the version home.)
|
|
560
|
+
*/
|
|
561
|
+
function antigravityWorkflowsDir() {
|
|
562
|
+
return path.join(process.env.HOME ?? os.homedir(), '.gemini', 'config', 'global_workflows');
|
|
563
|
+
}
|
|
564
|
+
function workflowTargetRoot(agent, versionHome) {
|
|
565
|
+
if (agent === 'kimi')
|
|
566
|
+
return path.join(versionHome, '.kimi-code', 'skills');
|
|
567
|
+
if (agent === 'antigravity')
|
|
568
|
+
return antigravityWorkflowsDir();
|
|
569
|
+
return path.join(versionHome, 'workflows');
|
|
570
|
+
}
|
|
571
|
+
/** List workflow names synced into a specific agent version home. */
|
|
572
|
+
export function listWorkflowsForAgent(agent, versionHome) {
|
|
573
|
+
if (agent === 'kimi') {
|
|
574
|
+
const skillsDir = workflowTargetRoot(agent, versionHome);
|
|
575
|
+
if (!fs.existsSync(skillsDir))
|
|
576
|
+
return [];
|
|
577
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
578
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
|
|
579
|
+
.filter(d => kimiWorkflowMarker(path.join(skillsDir, d.name, 'SKILL.md')) === d.name)
|
|
580
|
+
.map(d => d.name);
|
|
581
|
+
}
|
|
582
|
+
if (agent === 'antigravity') {
|
|
583
|
+
const dir = workflowTargetRoot(agent, versionHome);
|
|
584
|
+
if (!fs.existsSync(dir))
|
|
585
|
+
return [];
|
|
586
|
+
try {
|
|
587
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
588
|
+
.filter(d => d.isFile() && d.name.endsWith('.md') && !d.name.startsWith('.'))
|
|
589
|
+
.map(d => d.name.slice(0, -'.md'.length))
|
|
590
|
+
.filter(base => antigravityWorkflowMarker(path.join(dir, `${base}.md`)) === base);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (agent === 'goose') {
|
|
597
|
+
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
598
|
+
if (!fs.existsSync(recipesDir))
|
|
599
|
+
return [];
|
|
600
|
+
try {
|
|
601
|
+
return fs.readdirSync(recipesDir, { withFileTypes: true })
|
|
602
|
+
.filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
|
|
603
|
+
.map(d => d.name.slice(0, -'.yaml'.length));
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const workflowsDir = workflowTargetRoot(agent, versionHome);
|
|
479
610
|
if (!fs.existsSync(workflowsDir))
|
|
480
611
|
return [];
|
|
481
612
|
try {
|
|
@@ -487,11 +618,130 @@ export function listWorkflowsForAgent(_agent, versionHome) {
|
|
|
487
618
|
return [];
|
|
488
619
|
}
|
|
489
620
|
}
|
|
621
|
+
function parseSubrecipeFrontmatter(filePath) {
|
|
622
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
623
|
+
const lines = content.split('\n');
|
|
624
|
+
if (lines[0] !== '---')
|
|
625
|
+
return { body: content.trim() };
|
|
626
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
627
|
+
if (endIndex < 0)
|
|
628
|
+
return { body: content.trim() };
|
|
629
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
630
|
+
let parsed = {};
|
|
631
|
+
try {
|
|
632
|
+
const value = yaml.parse(frontmatter);
|
|
633
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
634
|
+
parsed = value;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch { /* ignore malformed subagent frontmatter */ }
|
|
638
|
+
return {
|
|
639
|
+
name: typeof parsed.name === 'string' ? parsed.name : undefined,
|
|
640
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
641
|
+
body: lines.slice(endIndex + 2).join('\n').trim(),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function selectedWorkflowSubagents(workflowPath, allowedAgents) {
|
|
645
|
+
const subagentsDir = path.join(workflowPath, 'subagents');
|
|
646
|
+
if (!fs.existsSync(subagentsDir))
|
|
647
|
+
return [];
|
|
648
|
+
const allowed = allowedAgents ? new Set(allowedAgents) : null;
|
|
649
|
+
return fs.readdirSync(subagentsDir, { withFileTypes: true })
|
|
650
|
+
.filter(e => e.isFile() && e.name.endsWith('.md') && !e.name.startsWith('.'))
|
|
651
|
+
.map(e => e.name.slice(0, -'.md'.length))
|
|
652
|
+
.filter(name => !allowed || allowed.has(name))
|
|
653
|
+
.sort();
|
|
654
|
+
}
|
|
655
|
+
function writeGooseSubrecipe(workflowPath, subrecipeName, destDir) {
|
|
656
|
+
const sourcePath = path.join(workflowPath, 'subagents', `${subrecipeName}.md`);
|
|
657
|
+
const parsed = parseSubrecipeFrontmatter(sourcePath);
|
|
658
|
+
const body = parsed.body || parsed.description || subrecipeName;
|
|
659
|
+
const recipe = {
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
title: parsed.name || subrecipeName,
|
|
662
|
+
description: parsed.description || `Subrecipe for ${subrecipeName}`,
|
|
663
|
+
instructions: body,
|
|
664
|
+
prompt: body,
|
|
665
|
+
};
|
|
666
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
667
|
+
fs.writeFileSync(path.join(destDir, `${subrecipeName}.yaml`), yaml.stringify(recipe), 'utf-8');
|
|
668
|
+
}
|
|
669
|
+
function syncWorkflowToGooseRecipe(workflowPath, name, versionHome) {
|
|
670
|
+
const frontmatter = parseWorkflowFrontmatter(workflowPath);
|
|
671
|
+
if (!frontmatter) {
|
|
672
|
+
return { success: false, error: `Workflow '${name}' has invalid WORKFLOW.md frontmatter` };
|
|
673
|
+
}
|
|
674
|
+
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
675
|
+
const recipePath = path.join(recipesDir, `${name}.yaml`);
|
|
676
|
+
const subrecipesDir = path.join(recipesDir, `${name}.subrecipes`);
|
|
677
|
+
const body = readWorkflowBody(workflowPath) || frontmatter.description || name;
|
|
678
|
+
const subagents = selectedWorkflowSubagents(workflowPath, frontmatter.allowedAgents);
|
|
679
|
+
const recipe = {
|
|
680
|
+
version: '1.0.0',
|
|
681
|
+
title: frontmatter.name || name,
|
|
682
|
+
description: frontmatter.description || name,
|
|
683
|
+
instructions: body,
|
|
684
|
+
prompt: body,
|
|
685
|
+
};
|
|
686
|
+
if (frontmatter.model) {
|
|
687
|
+
recipe.settings = { goose_model: frontmatter.model };
|
|
688
|
+
}
|
|
689
|
+
if (subagents.length > 0) {
|
|
690
|
+
recipe.sub_recipes = subagents.map(subagentName => ({
|
|
691
|
+
name: subagentName,
|
|
692
|
+
path: `./${name}.subrecipes/${subagentName}.yaml`,
|
|
693
|
+
description: `Workflow subrecipe ${subagentName}`,
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
fs.mkdirSync(recipesDir, { recursive: true });
|
|
698
|
+
if (fs.existsSync(subrecipesDir)) {
|
|
699
|
+
fs.rmSync(subrecipesDir, { recursive: true, force: true });
|
|
700
|
+
}
|
|
701
|
+
for (const subagentName of subagents) {
|
|
702
|
+
writeGooseSubrecipe(workflowPath, subagentName, subrecipesDir);
|
|
703
|
+
}
|
|
704
|
+
fs.writeFileSync(recipePath, yaml.stringify(recipe), 'utf-8');
|
|
705
|
+
return { success: true };
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
return { success: false, error: err.message };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
490
711
|
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
|
|
491
|
-
export function syncWorkflowToVersion(workflowPath, name,
|
|
492
|
-
|
|
712
|
+
export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
|
|
713
|
+
if (agent === 'goose') {
|
|
714
|
+
return syncWorkflowToGooseRecipe(workflowPath, name, versionHome);
|
|
715
|
+
}
|
|
493
716
|
try {
|
|
494
|
-
|
|
717
|
+
if (agent === 'kimi') {
|
|
718
|
+
const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
719
|
+
const targetFile = path.join(targetDir, 'SKILL.md');
|
|
720
|
+
if (fs.existsSync(targetFile)) {
|
|
721
|
+
const marker = kimiWorkflowMarker(targetFile);
|
|
722
|
+
if (marker !== name) {
|
|
723
|
+
return { success: false, error: `Kimi skill '${name}' already exists and is not managed by agents-cli` };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
727
|
+
fs.writeFileSync(targetFile, transformWorkflowForKimi(workflowPath, name), 'utf-8');
|
|
728
|
+
return { success: true };
|
|
729
|
+
}
|
|
730
|
+
if (agent === 'antigravity') {
|
|
731
|
+
const targetDir = workflowTargetRoot(agent, versionHome);
|
|
732
|
+
const targetFile = path.join(targetDir, `${name}.md`);
|
|
733
|
+
if (fs.existsSync(targetFile)) {
|
|
734
|
+
const marker = antigravityWorkflowMarker(targetFile);
|
|
735
|
+
if (marker !== name) {
|
|
736
|
+
return { success: false, error: `Antigravity workflow '${name}' already exists and is not managed by agents-cli` };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
740
|
+
fs.writeFileSync(targetFile, transformWorkflowForAntigravity(workflowPath, name), 'utf-8');
|
|
741
|
+
return { success: true };
|
|
742
|
+
}
|
|
743
|
+
const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
744
|
+
fs.mkdirSync(workflowTargetRoot(agent, versionHome), { recursive: true });
|
|
495
745
|
if (fs.existsSync(targetDir)) {
|
|
496
746
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
497
747
|
}
|
|
@@ -505,18 +755,87 @@ export function syncWorkflowToVersion(workflowPath, name, _agent, versionHome) {
|
|
|
505
755
|
/** Remove a workflow from a specific agent version home. */
|
|
506
756
|
export function removeWorkflowFromVersion(agent, version, name) {
|
|
507
757
|
const versionHome = getVersionHomePath(agent, version);
|
|
508
|
-
|
|
509
|
-
|
|
758
|
+
if (agent === 'antigravity') {
|
|
759
|
+
const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.md`);
|
|
760
|
+
if (!fs.existsSync(targetFile)) {
|
|
761
|
+
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
762
|
+
}
|
|
763
|
+
if (antigravityWorkflowMarker(targetFile) !== name) {
|
|
764
|
+
return { success: false, error: `Antigravity workflow '${name}' is not managed by agents-cli` };
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
fs.rmSync(targetFile, { force: true });
|
|
768
|
+
return { success: true };
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
return { success: false, error: err.message };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (agent === 'goose') {
|
|
775
|
+
const recipePath = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.yaml`);
|
|
776
|
+
const subrecipesDir = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.subrecipes`);
|
|
777
|
+
if (!fs.existsSync(recipePath) && !fs.existsSync(subrecipesDir)) {
|
|
778
|
+
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
if (fs.existsSync(recipePath))
|
|
782
|
+
fs.rmSync(recipePath, { force: true });
|
|
783
|
+
if (fs.existsSync(subrecipesDir))
|
|
784
|
+
fs.rmSync(subrecipesDir, { recursive: true, force: true });
|
|
785
|
+
return { success: true };
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
return { success: false, error: err.message };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const targetPath = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
792
|
+
if (!fs.existsSync(targetPath)) {
|
|
510
793
|
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
511
794
|
}
|
|
512
795
|
try {
|
|
513
|
-
|
|
796
|
+
if (agent === 'kimi') {
|
|
797
|
+
const targetFile = path.join(targetPath, 'SKILL.md');
|
|
798
|
+
if (kimiWorkflowMarker(targetFile) !== name) {
|
|
799
|
+
return { success: false, error: `Kimi skill '${name}' is not managed by agents-cli` };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
514
803
|
return { success: true };
|
|
515
804
|
}
|
|
516
805
|
catch (err) {
|
|
517
806
|
return { success: false, error: err.message };
|
|
518
807
|
}
|
|
519
808
|
}
|
|
809
|
+
function parseSkillFrontmatter(filePath) {
|
|
810
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
811
|
+
const lines = content.split('\n');
|
|
812
|
+
if (lines[0] !== '---')
|
|
813
|
+
return null;
|
|
814
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
815
|
+
if (endIndex < 0)
|
|
816
|
+
return null;
|
|
817
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
818
|
+
const parsed = yaml.parse(frontmatter);
|
|
819
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
820
|
+
}
|
|
821
|
+
function kimiWorkflowMarker(filePath) {
|
|
822
|
+
try {
|
|
823
|
+
const fm = parseSkillFrontmatter(filePath);
|
|
824
|
+
return fm?.type === 'flow' && typeof fm.agents_workflow === 'string' ? fm.agents_workflow : null;
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function antigravityWorkflowMarker(filePath) {
|
|
831
|
+
try {
|
|
832
|
+
const fm = parseSkillFrontmatter(filePath);
|
|
833
|
+
return typeof fm?.agents_workflow === 'string' ? fm.agents_workflow : null;
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
520
839
|
/** Iterate all installed (agent, version) pairs that support workflows. */
|
|
521
840
|
export function iterWorkflowsCapableVersions(filter) {
|
|
522
841
|
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.61",
|
|
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",
|