@phnx-labs/agents-cli 1.20.58 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +15 -7
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +39 -2
  5. package/dist/commands/output.d.ts +19 -0
  6. package/dist/commands/output.js +333 -0
  7. package/dist/commands/secrets.js +6 -6
  8. package/dist/index.js +2 -1
  9. package/dist/lib/agents.js +19 -13
  10. package/dist/lib/hosts/dispatch.d.ts +36 -0
  11. package/dist/lib/hosts/dispatch.js +40 -2
  12. package/dist/lib/hosts/passthrough.js +1 -0
  13. package/dist/lib/mcp.js +1 -1
  14. package/dist/lib/output/git-output.d.ts +74 -0
  15. package/dist/lib/output/git-output.js +213 -0
  16. package/dist/lib/permissions.d.ts +26 -5
  17. package/dist/lib/permissions.js +212 -37
  18. package/dist/lib/plugins.d.ts +8 -0
  19. package/dist/lib/plugins.js +108 -0
  20. package/dist/lib/project-root.js +2 -1
  21. package/dist/lib/resources/mcp.js +1 -1
  22. package/dist/lib/resources/permissions.d.ts +1 -1
  23. package/dist/lib/resources/permissions.js +8 -2
  24. package/dist/lib/resources/skills.js +6 -1
  25. package/dist/lib/resources/types.d.ts +1 -1
  26. package/dist/lib/routines.d.ts +16 -0
  27. package/dist/lib/routines.js +46 -1
  28. package/dist/lib/secrets/remote.d.ts +7 -2
  29. package/dist/lib/secrets/remote.js +11 -10
  30. package/dist/lib/session/db.d.ts +3 -0
  31. package/dist/lib/session/db.js +20 -4
  32. package/dist/lib/session/discover.d.ts +2 -0
  33. package/dist/lib/session/discover.js +40 -4
  34. package/dist/lib/session/types.d.ts +2 -0
  35. package/dist/lib/shims.js +13 -3
  36. package/dist/lib/skills.js +14 -1
  37. package/dist/lib/staleness/detectors/permissions.js +50 -3
  38. package/dist/lib/staleness/detectors/subagents.js +31 -12
  39. package/dist/lib/staleness/detectors/workflows.js +33 -0
  40. package/dist/lib/staleness/writers/commands.js +3 -3
  41. package/dist/lib/staleness/writers/subagents.js +13 -5
  42. package/dist/lib/startup/command-registry.d.ts +1 -0
  43. package/dist/lib/startup/command-registry.js +2 -0
  44. package/dist/lib/subagents.d.ts +11 -1
  45. package/dist/lib/subagents.js +117 -26
  46. package/dist/lib/versions.js +12 -2
  47. package/dist/lib/workflows.d.ts +5 -3
  48. package/dist/lib/workflows.js +246 -9
  49. package/package.json +1 -1
@@ -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
- /** List workflow names synced into a specific agent version home (at {versionHome}/workflows/). */
477
- export function listWorkflowsForAgent(_agent, versionHome) {
478
- const workflowsDir = path.join(versionHome, 'workflows');
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, _agent, versionHome) {
492
- const targetDir = path.join(versionHome, 'workflows', name);
662
+ export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
663
+ if (agent === 'goose') {
664
+ return syncWorkflowToGooseRecipe(workflowPath, name, versionHome);
665
+ }
493
666
  try {
494
- fs.mkdirSync(path.join(versionHome, 'workflows'), { recursive: true });
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
- const targetDir = path.join(versionHome, 'workflows', name);
509
- if (!fs.existsSync(targetDir)) {
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
- fs.rmSync(targetDir, { recursive: true, force: true });
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.58",
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",