@phnx-labs/agents-cli 1.20.63 → 1.20.64

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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +55 -28
  5. package/dist/commands/feed.d.ts +4 -0
  6. package/dist/commands/feed.js +27 -8
  7. package/dist/commands/lease.d.ts +23 -0
  8. package/dist/commands/lease.js +201 -0
  9. package/dist/commands/mailboxes.d.ts +20 -0
  10. package/dist/commands/mailboxes.js +390 -0
  11. package/dist/commands/routines.js +20 -14
  12. package/dist/commands/sessions-export.d.ts +2 -0
  13. package/dist/commands/sessions-export.js +279 -0
  14. package/dist/commands/sessions-import.d.ts +2 -0
  15. package/dist/commands/sessions-import.js +230 -0
  16. package/dist/commands/sessions.js +4 -0
  17. package/dist/commands/ssh.js +98 -3
  18. package/dist/commands/usage.d.ts +2 -0
  19. package/dist/commands/usage.js +7 -2
  20. package/dist/commands/view.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/lib/agents.d.ts +18 -0
  23. package/dist/lib/agents.js +27 -17
  24. package/dist/lib/browser/drivers/ssh.js +19 -2
  25. package/dist/lib/comms-render.d.ts +37 -0
  26. package/dist/lib/comms-render.js +89 -0
  27. package/dist/lib/crabbox/cli.d.ts +72 -0
  28. package/dist/lib/crabbox/cli.js +158 -9
  29. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  30. package/dist/lib/crabbox/runtimes.js +24 -0
  31. package/dist/lib/daemon.js +6 -1
  32. package/dist/lib/devices/health.d.ts +77 -0
  33. package/dist/lib/devices/health.js +186 -0
  34. package/dist/lib/mailbox.d.ts +39 -0
  35. package/dist/lib/mailbox.js +112 -0
  36. package/dist/lib/paths.d.ts +13 -0
  37. package/dist/lib/paths.js +26 -4
  38. package/dist/lib/routines.d.ts +21 -2
  39. package/dist/lib/routines.js +35 -12
  40. package/dist/lib/runner.js +255 -13
  41. package/dist/lib/sandbox.d.ts +9 -1
  42. package/dist/lib/sandbox.js +11 -2
  43. package/dist/lib/session/bundle.d.ts +150 -0
  44. package/dist/lib/session/bundle.js +189 -0
  45. package/dist/lib/session/remote-bundle.d.ts +12 -0
  46. package/dist/lib/session/remote-bundle.js +61 -0
  47. package/dist/lib/session/sync/agents.d.ts +54 -6
  48. package/dist/lib/session/sync/agents.js +0 -0
  49. package/dist/lib/session/sync/manifest.d.ts +14 -3
  50. package/dist/lib/session/sync/manifest.js +4 -0
  51. package/dist/lib/session/sync/sync.d.ts +23 -2
  52. package/dist/lib/session/sync/sync.js +177 -74
  53. package/dist/lib/ssh-tunnel.js +13 -1
  54. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  55. package/dist/lib/staleness/detectors/subagents.js +5 -192
  56. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  57. package/dist/lib/staleness/writers/subagents.js +11 -102
  58. package/dist/lib/startup/command-registry.d.ts +2 -0
  59. package/dist/lib/startup/command-registry.js +5 -0
  60. package/dist/lib/subagents-registry.d.ts +85 -0
  61. package/dist/lib/subagents-registry.js +393 -0
  62. package/dist/lib/subagents.d.ts +8 -8
  63. package/dist/lib/subagents.js +32 -663
  64. package/dist/lib/sync-umbrella.d.ts +1 -0
  65. package/dist/lib/sync-umbrella.js +14 -3
  66. package/dist/lib/types.d.ts +9 -0
  67. package/dist/lib/usage.d.ts +42 -3
  68. package/dist/lib/usage.js +162 -22
  69. package/package.json +1 -1
@@ -13,6 +13,7 @@ import { capableAgents } from './capabilities.js';
13
13
  import { getSubagentsDir, getUserSubagentsDir, getTrashSubagentsDir } from './state.js';
14
14
  import { listInstalledVersions, getVersionHomePath } from './versions.js';
15
15
  import { safeJoin } from './paths.js';
16
+ import { subagentTarget, writeSubagentToHome, listInstalledSubagentNames, listInstalledSubagentsRich, removeSubagentFromHome, trashSubagentFromHome, copyDirWithRename, } from './subagents-registry.js';
16
17
  /**
17
18
  * Parse AGENT.md frontmatter to extract subagent metadata
18
19
  */
@@ -273,7 +274,9 @@ export function transformSubagentForDroid(subagentDir) {
273
274
  * sections, `color` dropped), so this is an alias of transformSubagentForDroid.
274
275
  * See GitHub docs for custom agents.
275
276
  */
276
- export const transformSubagentForCopilot = transformSubagentForDroid;
277
+ export function transformSubagentForCopilot(subagentDir) {
278
+ return transformSubagentForDroid(subagentDir);
279
+ }
277
280
  /**
278
281
  * Transform a subagent into a Cursor CLI custom subagent `.md` file.
279
282
  *
@@ -284,7 +287,9 @@ export const transformSubagentForCopilot = transformSubagentForDroid;
284
287
  * an alias of transformSubagentForDroid, same as Copilot.
285
288
  * See https://cursor.com/docs/subagents.
286
289
  */
287
- export const transformSubagentForCursor = transformSubagentForDroid;
290
+ export function transformSubagentForCursor(subagentDir) {
291
+ return transformSubagentForDroid(subagentDir);
292
+ }
288
293
  /**
289
294
  * Transform a subagent into a ForgeCode custom subagent `.md` file.
290
295
  *
@@ -294,7 +299,9 @@ export const transformSubagentForCursor = transformSubagentForDroid;
294
299
  * no `color` field, so this is an alias of transformSubagentForDroid, same as
295
300
  * Copilot/Cursor. See https://forgecode.dev/docs/agent-definition-guide/.
296
301
  */
297
- export const transformSubagentForForge = transformSubagentForDroid;
302
+ export function transformSubagentForForge(subagentDir) {
303
+ return transformSubagentForDroid(subagentDir);
304
+ }
298
305
  /**
299
306
  * Transform a subagent into Antigravity's custom-agent markdown shape.
300
307
  *
@@ -559,19 +566,7 @@ export function transformSubagentForGoose(subagentDir) {
559
566
  */
560
567
  export function syncSubagentToOpenclaw(subagentDir, targetDir) {
561
568
  try {
562
- // Ensure target directory exists
563
- if (!fs.existsSync(targetDir)) {
564
- fs.mkdirSync(targetDir, { recursive: true });
565
- }
566
- // Copy all files
567
- for (const file of fs.readdirSync(subagentDir)) {
568
- const sourcePath = path.join(subagentDir, file);
569
- const targetFile = file === 'AGENT.md' ? 'AGENTS.md' : file;
570
- const targetPath = path.join(targetDir, targetFile);
571
- if (fs.statSync(sourcePath).isFile()) {
572
- fs.copyFileSync(sourcePath, targetPath);
573
- }
574
- }
569
+ copyDirWithRename(subagentDir, targetDir, { 'AGENT.md': 'AGENTS.md' });
575
570
  return { success: true };
576
571
  }
577
572
  catch (err) {
@@ -582,227 +577,23 @@ export function syncSubagentToOpenclaw(subagentDir, targetDir) {
582
577
  * Install a subagent to a specific agent's home
583
578
  */
584
579
  export function installSubagentToAgent(subagentDir, subagentName, agent, agentHome) {
585
- if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
586
- // Claude / Gemini / Grok: flatten to single .md under the native agents dir.
587
- const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
588
- const agentsDir = path.join(agentHome, agentsRoot, 'agents');
589
- if (!fs.existsSync(agentsDir)) {
590
- fs.mkdirSync(agentsDir, { recursive: true });
591
- }
592
- try {
593
- const transformed = transformSubagentForClaude(subagentDir);
594
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformed);
595
- return { success: true };
596
- }
597
- catch (err) {
598
- return { success: false, error: String(err) };
599
- }
600
- }
601
- else if (agent === 'codex') {
602
- // Codex: standalone TOML under ~/.codex/agents/<name>.toml
603
- const agentsDir = path.join(agentHome, '.codex', 'agents');
604
- if (!fs.existsSync(agentsDir)) {
605
- fs.mkdirSync(agentsDir, { recursive: true });
606
- }
607
- try {
608
- const toml = transformSubagentForCodex(subagentDir);
609
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.toml`), toml);
610
- return { success: true };
611
- }
612
- catch (err) {
613
- return { success: false, error: String(err) };
614
- }
615
- }
616
- else if (agent === 'kimi') {
617
- // Kimi: YAML + sibling system-prompt md under ~/.kimi-code/agents/
618
- try {
619
- writeKimiSubagentFiles(path.join(agentHome, '.kimi-code', 'agents'), subagentDir, subagentName);
620
- return { success: true };
621
- }
622
- catch (err) {
623
- return { success: false, error: String(err) };
624
- }
625
- }
626
- else if (agent === 'opencode') {
627
- // OpenCode: markdown agents under ~/.config/opencode/agents/
628
- const agentsDir = path.join(agentHome, '.config', 'opencode', 'agents');
629
- if (!fs.existsSync(agentsDir))
630
- fs.mkdirSync(agentsDir, { recursive: true });
631
- try {
632
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformSubagentForOpenCode(subagentDir));
633
- return { success: true };
634
- }
635
- catch (err) {
636
- return { success: false, error: String(err) };
637
- }
638
- }
639
- else if (agent === 'antigravity') {
640
- // Antigravity: custom-agent markdown under ~/.gemini/config/agents/<name>/agent.md.
641
- const agentDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
642
- if (!fs.existsSync(agentDir))
643
- fs.mkdirSync(agentDir, { recursive: true });
644
- try {
645
- fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(subagentDir));
646
- return { success: true };
647
- }
648
- catch (err) {
649
- return { success: false, error: String(err) };
650
- }
651
- }
652
- else if (agent === 'openclaw') {
653
- // OpenClaw: copy full directory
654
- const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
655
- return syncSubagentToOpenclaw(subagentDir, targetDir);
656
- }
657
- else if (agent === 'kiro') {
658
- // Kiro: JSON custom-agent file under ~/.kiro/agents/
659
- const agentsDir = path.join(agentHome, '.kiro', 'agents');
660
- if (!fs.existsSync(agentsDir)) {
661
- fs.mkdirSync(agentsDir, { recursive: true });
662
- }
663
- try {
664
- const transformed = transformSubagentForKiro(subagentDir);
665
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.json`), transformed);
666
- return { success: true };
667
- }
668
- catch (err) {
669
- return { success: false, error: String(err) };
670
- }
671
- }
672
- else if (agent === 'cursor') {
673
- // Cursor: flattened .md custom subagent under ~/.cursor/agents/
674
- const agentsDir = path.join(agentHome, '.cursor', 'agents');
675
- if (!fs.existsSync(agentsDir)) {
676
- fs.mkdirSync(agentsDir, { recursive: true });
677
- }
678
- try {
679
- const transformed = transformSubagentForCursor(subagentDir);
680
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformed);
681
- return { success: true };
682
- }
683
- catch (err) {
684
- return { success: false, error: String(err) };
685
- }
686
- }
687
- else if (agent === 'forge') {
688
- // ForgeCode: flattened .md custom subagent under ~/.forge/agents/
689
- const agentsDir = path.join(agentHome, '.forge', 'agents');
690
- if (!fs.existsSync(agentsDir)) {
691
- fs.mkdirSync(agentsDir, { recursive: true });
692
- }
693
- try {
694
- const transformed = transformSubagentForForge(subagentDir);
695
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformed);
696
- return { success: true };
697
- }
698
- catch (err) {
699
- return { success: false, error: String(err) };
700
- }
580
+ if (!subagentTarget(agent)) {
581
+ return { success: false, error: `Agent '${agent}' does not support subagents` };
701
582
  }
702
- else if (agent === 'goose') {
703
- // Goose: recipe YAML custom-agent file under ~/.config/goose/agents/
704
- const agentsDir = path.join(agentHome, '.config', 'goose', 'agents');
705
- if (!fs.existsSync(agentsDir)) {
706
- fs.mkdirSync(agentsDir, { recursive: true });
707
- }
708
- try {
709
- const transformed = transformSubagentForGoose(subagentDir);
710
- fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.yaml`), transformed);
711
- return { success: true };
712
- }
713
- catch (err) {
714
- return { success: false, error: String(err) };
715
- }
583
+ try {
584
+ writeSubagentToHome(agent, agentHome, { name: subagentName, path: subagentDir });
585
+ return { success: true };
716
586
  }
717
- else {
718
- // Other agents don't support subagents yet
719
- return { success: false, error: `Agent '${agent}' does not support subagents` };
587
+ catch (err) {
588
+ return { success: false, error: String(err) };
720
589
  }
721
590
  }
722
591
  /**
723
592
  * Remove a subagent from a specific agent's home
724
593
  */
725
594
  export function removeSubagentFromAgent(subagentName, agent, agentHome) {
726
- try {
727
- if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
728
- const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
729
- const targetPath = safeJoin(path.join(agentHome, agentsRoot, 'agents'), `${subagentName}.md`);
730
- if (fs.existsSync(targetPath)) {
731
- fs.unlinkSync(targetPath);
732
- }
733
- return { success: true };
734
- }
735
- else if (agent === 'codex') {
736
- const targetPath = safeJoin(path.join(agentHome, '.codex', 'agents'), `${subagentName}.toml`);
737
- if (fs.existsSync(targetPath)) {
738
- fs.unlinkSync(targetPath);
739
- }
740
- return { success: true };
741
- }
742
- else if (agent === 'kimi') {
743
- const agentsDir = path.join(agentHome, '.kimi-code', 'agents');
744
- const targetPath = safeJoin(agentsDir, `${subagentName}.yaml`);
745
- const promptPath = safeJoin(agentsDir, `${subagentName}.system.md`);
746
- if (fs.existsSync(targetPath))
747
- fs.unlinkSync(targetPath);
748
- if (fs.existsSync(promptPath))
749
- fs.unlinkSync(promptPath);
750
- return { success: true };
751
- }
752
- else if (agent === 'opencode') {
753
- const targetPath = safeJoin(path.join(agentHome, '.config', 'opencode', 'agents'), `${subagentName}.md`);
754
- if (fs.existsSync(targetPath))
755
- fs.unlinkSync(targetPath);
756
- return { success: true };
757
- }
758
- else if (agent === 'antigravity') {
759
- const targetDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
760
- if (fs.existsSync(targetDir))
761
- fs.rmSync(targetDir, { recursive: true, force: true });
762
- return { success: true };
763
- }
764
- else if (agent === 'openclaw') {
765
- const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
766
- if (fs.existsSync(targetDir)) {
767
- fs.rmSync(targetDir, { recursive: true });
768
- }
769
- return { success: true };
770
- }
771
- else if (agent === 'kiro') {
772
- const targetPath = safeJoin(path.join(agentHome, '.kiro', 'agents'), `${subagentName}.json`);
773
- if (fs.existsSync(targetPath)) {
774
- fs.unlinkSync(targetPath);
775
- }
776
- return { success: true };
777
- }
778
- else if (agent === 'cursor') {
779
- const targetPath = safeJoin(path.join(agentHome, '.cursor', 'agents'), `${subagentName}.md`);
780
- if (fs.existsSync(targetPath)) {
781
- fs.unlinkSync(targetPath);
782
- }
783
- return { success: true };
784
- }
785
- else if (agent === 'forge') {
786
- const targetPath = safeJoin(path.join(agentHome, '.forge', 'agents'), `${subagentName}.md`);
787
- if (fs.existsSync(targetPath)) {
788
- fs.unlinkSync(targetPath);
789
- }
790
- return { success: true };
791
- }
792
- else if (agent === 'goose') {
793
- const targetPath = safeJoin(path.join(agentHome, '.config', 'goose', 'agents'), `${subagentName}.yaml`);
794
- if (fs.existsSync(targetPath)) {
795
- fs.unlinkSync(targetPath);
796
- }
797
- return { success: true };
798
- }
799
- else {
800
- return { success: true }; // No-op for unsupported agents
801
- }
802
- }
803
- catch (err) {
804
- return { success: false, error: String(err) };
805
- }
595
+ // No-op success for agents without a registry entry, matching prior behavior.
596
+ return removeSubagentFromHome(agent, agentHome, subagentName);
806
597
  }
807
598
  /**
808
599
  * Check if subagent content matches between source and installed
@@ -832,242 +623,14 @@ export function subagentContentMatches(installedDir, sourceDir) {
832
623
  // lib/capabilities.ts. The capability matrix on AgentConfig is the single
833
624
  // source of truth.
834
625
  /**
835
- * List subagents installed to a specific agent's home
836
- * Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
837
- * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
838
- * Kiro: scans ~/.kiro/agents/{name}.json
839
- * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
626
+ * List subagents installed to a specific agent's home.
627
+ *
628
+ * Layout (target dir, file/dir shape, metadata reader) is declared once per
629
+ * agent in the subagent registry; this is a thin wrapper over the generic
630
+ * enumerator so a new agent needs no arm here -- just a registry entry.
840
631
  */
841
632
  export function listSubagentsForAgent(agentId, home) {
842
- const subagents = [];
843
- if (agentId === 'claude' || agentId === 'gemini' || agentId === 'grok') {
844
- // Claude / Gemini / Grok: flat .md files in agents/
845
- const agentsRoot = agentId === 'grok' ? '.grok' : agentId === 'gemini' ? '.gemini' : '.claude';
846
- const agentsDir = path.join(home, agentsRoot, 'agents');
847
- if (!fs.existsSync(agentsDir))
848
- return subagents;
849
- for (const file of fs.readdirSync(agentsDir)) {
850
- if (!file.endsWith('.md'))
851
- continue;
852
- const filePath = path.join(agentsDir, file);
853
- if (!fs.statSync(filePath).isFile())
854
- continue;
855
- const frontmatter = parseSubagentFrontmatter(filePath);
856
- if (!frontmatter)
857
- continue;
858
- const name = file.replace('.md', '');
859
- subagents.push({
860
- name,
861
- path: filePath,
862
- files: [file],
863
- frontmatter,
864
- });
865
- }
866
- }
867
- else if (agentId === 'kimi') {
868
- // Kimi: flat YAML under .kimi-code/agents/ (exclude managed parent _*.yaml)
869
- const agentsDir = path.join(home, '.kimi-code', 'agents');
870
- if (!fs.existsSync(agentsDir))
871
- return subagents;
872
- for (const file of fs.readdirSync(agentsDir)) {
873
- if (!file.endsWith('.yaml') || file.startsWith('_'))
874
- continue;
875
- const filePath = path.join(agentsDir, file);
876
- if (!fs.statSync(filePath).isFile())
877
- continue;
878
- const name = file.replace(/\.yaml$/, '');
879
- let description = '';
880
- try {
881
- const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
882
- description = parsed?.agent?.description ?? '';
883
- }
884
- catch { /* leave description empty */ }
885
- const files = [file];
886
- const promptFile = `${name}.system.md`;
887
- if (fs.existsSync(path.join(agentsDir, promptFile)))
888
- files.push(promptFile);
889
- subagents.push({
890
- name,
891
- path: filePath,
892
- files,
893
- frontmatter: { name, description },
894
- });
895
- }
896
- }
897
- else if (agentId === 'opencode') {
898
- const agentsDir = path.join(home, '.config', 'opencode', 'agents');
899
- if (!fs.existsSync(agentsDir))
900
- return subagents;
901
- for (const file of fs.readdirSync(agentsDir)) {
902
- if (!file.endsWith('.md'))
903
- continue;
904
- const filePath = path.join(agentsDir, file);
905
- if (!fs.statSync(filePath).isFile())
906
- continue;
907
- const name = file.replace(/\.md$/, '');
908
- const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
909
- subagents.push({ name, path: filePath, files: [file], frontmatter });
910
- }
911
- }
912
- else if (agentId === 'antigravity') {
913
- const agentsDir = path.join(home, '.gemini', 'config', 'agents');
914
- if (!fs.existsSync(agentsDir))
915
- return subagents;
916
- for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
917
- if (!entry.isDirectory())
918
- continue;
919
- const filePath = path.join(agentsDir, entry.name, 'agent.md');
920
- if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
921
- continue;
922
- const frontmatter = parseSubagentFrontmatter(filePath) ?? { name: entry.name, description: '' };
923
- subagents.push({ name: entry.name, path: filePath, files: ['agent.md'], frontmatter });
924
- }
925
- }
926
- else if (agentId === 'copilot') {
927
- // Copilot: flat `<name>.agent.md` files under ~/.copilot/agents/
928
- const agentsDir = path.join(home, '.copilot', 'agents');
929
- if (!fs.existsSync(agentsDir))
930
- return subagents;
931
- for (const file of fs.readdirSync(agentsDir)) {
932
- if (!file.endsWith('.agent.md'))
933
- continue;
934
- const filePath = path.join(agentsDir, file);
935
- if (!fs.statSync(filePath).isFile())
936
- continue;
937
- const name = file.replace(/\.agent\.md$/, '');
938
- const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
939
- subagents.push({ name, path: filePath, files: [file], frontmatter });
940
- }
941
- }
942
- else if (agentId === 'openclaw') {
943
- // OpenClaw: directories with AGENTS.md
944
- const openclawDir = path.join(home, '.openclaw');
945
- if (!fs.existsSync(openclawDir))
946
- return subagents;
947
- for (const dir of fs.readdirSync(openclawDir)) {
948
- const dirPath = path.join(openclawDir, dir);
949
- if (!fs.statSync(dirPath).isDirectory())
950
- continue;
951
- // OpenClaw uses AGENTS.md (not AGENT.md)
952
- const agentsMd = path.join(dirPath, 'AGENTS.md');
953
- if (!fs.existsSync(agentsMd))
954
- continue;
955
- // Parse frontmatter - OpenClaw may not have standard frontmatter
956
- // Try to extract name/description from content
957
- const content = fs.readFileSync(agentsMd, 'utf-8');
958
- let frontmatter = {
959
- name: dir,
960
- description: '',
961
- };
962
- // Try to parse frontmatter if present
963
- const parsed = parseSubagentFrontmatter(agentsMd);
964
- if (parsed) {
965
- frontmatter = parsed;
966
- }
967
- else {
968
- // Extract first non-empty line as description
969
- const firstLine = content.split('\n').find(l => l.trim() && !l.startsWith('#'));
970
- frontmatter.description = firstLine?.slice(0, 80) || `OpenClaw agent: ${dir}`;
971
- }
972
- const files = fs.readdirSync(dirPath)
973
- .filter(f => f.endsWith('.md'))
974
- .sort();
975
- subagents.push({
976
- name: dir,
977
- path: dirPath,
978
- files,
979
- frontmatter,
980
- });
981
- }
982
- }
983
- else if (agentId === 'kiro') {
984
- // Kiro: JSON files under ~/.kiro/agents/
985
- const agentsDir = path.join(home, '.kiro', 'agents');
986
- if (!fs.existsSync(agentsDir))
987
- return subagents;
988
- for (const file of fs.readdirSync(agentsDir)) {
989
- if (!file.endsWith('.json'))
990
- continue;
991
- const filePath = path.join(agentsDir, file);
992
- if (!fs.statSync(filePath).isFile())
993
- continue;
994
- let frontmatter;
995
- try {
996
- const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
997
- frontmatter = {
998
- name: config.name || file.replace('.json', ''),
999
- description: config.description || '',
1000
- model: config.model,
1001
- };
1002
- }
1003
- catch {
1004
- continue;
1005
- }
1006
- const name = file.replace('.json', '');
1007
- subagents.push({
1008
- name,
1009
- path: filePath,
1010
- files: [file],
1011
- frontmatter,
1012
- });
1013
- }
1014
- }
1015
- else if (agentId === 'cursor') {
1016
- // Cursor: flat `<name>.md` files under ~/.cursor/agents/
1017
- const agentsDir = path.join(home, '.cursor', 'agents');
1018
- if (!fs.existsSync(agentsDir))
1019
- return subagents;
1020
- for (const file of fs.readdirSync(agentsDir)) {
1021
- if (!file.endsWith('.md'))
1022
- continue;
1023
- const filePath = path.join(agentsDir, file);
1024
- if (!fs.statSync(filePath).isFile())
1025
- continue;
1026
- const name = file.replace(/\.md$/, '');
1027
- const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
1028
- subagents.push({ name, path: filePath, files: [file], frontmatter });
1029
- }
1030
- }
1031
- else if (agentId === 'forge') {
1032
- // ForgeCode: flat `<name>.md` files under ~/.forge/agents/
1033
- const agentsDir = path.join(home, '.forge', 'agents');
1034
- if (!fs.existsSync(agentsDir))
1035
- return subagents;
1036
- for (const file of fs.readdirSync(agentsDir)) {
1037
- if (!file.endsWith('.md'))
1038
- continue;
1039
- const filePath = path.join(agentsDir, file);
1040
- if (!fs.statSync(filePath).isFile())
1041
- continue;
1042
- const name = file.replace(/\.md$/, '');
1043
- const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
1044
- subagents.push({ name, path: filePath, files: [file], frontmatter });
1045
- }
1046
- }
1047
- else if (agentId === 'goose') {
1048
- // Goose: recipe YAML files under ~/.config/goose/agents/
1049
- const agentsDir = path.join(home, '.config', 'goose', 'agents');
1050
- if (!fs.existsSync(agentsDir))
1051
- return subagents;
1052
- for (const file of fs.readdirSync(agentsDir)) {
1053
- if (!file.endsWith('.yaml'))
1054
- continue;
1055
- const filePath = path.join(agentsDir, file);
1056
- if (!fs.statSync(filePath).isFile())
1057
- continue;
1058
- const name = file.replace(/\.yaml$/, '');
1059
- let frontmatter;
1060
- try {
1061
- const recipe = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
1062
- frontmatter = { name: recipe?.title || name, description: recipe?.description || '' };
1063
- }
1064
- catch {
1065
- continue;
1066
- }
1067
- subagents.push({ name, path: filePath, files: [file], frontmatter });
1068
- }
1069
- }
1070
- return subagents;
633
+ return listInstalledSubagentsRich(agentId, home);
1071
634
  }
1072
635
  /**
1073
636
  * Compare a version home's subagents against discovered subagents.
@@ -1075,7 +638,6 @@ export function listSubagentsForAgent(agentId, home) {
1075
638
  */
1076
639
  export function diffVersionSubagents(agent, version) {
1077
640
  const versionHome = getVersionHomePath(agent, version);
1078
- const orphans = [];
1079
641
  // Get all discovered subagent names
1080
642
  const discovered = new Set();
1081
643
  for (const dir of [getSubagentsDir(), getUserSubagentsDir()]) {
@@ -1087,120 +649,9 @@ export function diffVersionSubagents(agent, version) {
1087
649
  }
1088
650
  }
1089
651
  }
1090
- // Check what's installed
1091
- if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
1092
- const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
1093
- const agentsDir = path.join(versionHome, agentsRoot, 'agents');
1094
- if (fs.existsSync(agentsDir)) {
1095
- for (const file of fs.readdirSync(agentsDir)) {
1096
- if (!file.endsWith('.md'))
1097
- continue;
1098
- const name = path.basename(file, '.md');
1099
- if (!discovered.has(name)) {
1100
- orphans.push(name);
1101
- }
1102
- }
1103
- }
1104
- }
1105
- else if (agent === 'kimi') {
1106
- const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
1107
- if (fs.existsSync(agentsDir)) {
1108
- for (const file of fs.readdirSync(agentsDir)) {
1109
- if (!file.endsWith('.yaml') || file.startsWith('_'))
1110
- continue;
1111
- const name = path.basename(file, '.yaml');
1112
- if (!discovered.has(name)) {
1113
- orphans.push(name);
1114
- }
1115
- }
1116
- }
1117
- }
1118
- else if (agent === 'opencode') {
1119
- const agentsDir = path.join(versionHome, '.config', 'opencode', 'agents');
1120
- if (fs.existsSync(agentsDir)) {
1121
- for (const file of fs.readdirSync(agentsDir)) {
1122
- if (!file.endsWith('.md'))
1123
- continue;
1124
- const name = path.basename(file, '.md');
1125
- if (!discovered.has(name))
1126
- orphans.push(name);
1127
- }
1128
- }
1129
- }
1130
- else if (agent === 'antigravity') {
1131
- const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
1132
- if (fs.existsSync(agentsDir)) {
1133
- for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
1134
- if (!entry.isDirectory())
1135
- continue;
1136
- if (!fs.existsSync(path.join(agentsDir, entry.name, 'agent.md')))
1137
- continue;
1138
- if (!discovered.has(entry.name))
1139
- orphans.push(entry.name);
1140
- }
1141
- }
1142
- }
1143
- else if (agent === 'openclaw') {
1144
- const openclawDir = path.join(versionHome, '.openclaw');
1145
- if (fs.existsSync(openclawDir)) {
1146
- for (const entry of fs.readdirSync(openclawDir, { withFileTypes: true })) {
1147
- if (!entry.isDirectory())
1148
- continue;
1149
- if (!discovered.has(entry.name)) {
1150
- orphans.push(entry.name);
1151
- }
1152
- }
1153
- }
1154
- }
1155
- else if (agent === 'kiro') {
1156
- const agentsDir = path.join(versionHome, '.kiro', 'agents');
1157
- if (fs.existsSync(agentsDir)) {
1158
- for (const file of fs.readdirSync(agentsDir)) {
1159
- if (!file.endsWith('.json'))
1160
- continue;
1161
- const name = path.basename(file, '.json');
1162
- if (!discovered.has(name)) {
1163
- orphans.push(name);
1164
- }
1165
- }
1166
- }
1167
- }
1168
- else if (agent === 'cursor') {
1169
- const agentsDir = path.join(versionHome, '.cursor', 'agents');
1170
- if (fs.existsSync(agentsDir)) {
1171
- for (const file of fs.readdirSync(agentsDir)) {
1172
- if (!file.endsWith('.md'))
1173
- continue;
1174
- const name = path.basename(file, '.md');
1175
- if (!discovered.has(name))
1176
- orphans.push(name);
1177
- }
1178
- }
1179
- }
1180
- else if (agent === 'forge') {
1181
- const agentsDir = path.join(versionHome, '.forge', 'agents');
1182
- if (fs.existsSync(agentsDir)) {
1183
- for (const file of fs.readdirSync(agentsDir)) {
1184
- if (!file.endsWith('.md'))
1185
- continue;
1186
- const name = path.basename(file, '.md');
1187
- if (!discovered.has(name))
1188
- orphans.push(name);
1189
- }
1190
- }
1191
- }
1192
- else if (agent === 'goose') {
1193
- const agentsDir = path.join(versionHome, '.config', 'goose', 'agents');
1194
- if (fs.existsSync(agentsDir)) {
1195
- for (const file of fs.readdirSync(agentsDir)) {
1196
- if (!file.endsWith('.yaml'))
1197
- continue;
1198
- const name = path.basename(file, '.yaml');
1199
- if (!discovered.has(name))
1200
- orphans.push(name);
1201
- }
1202
- }
1203
- }
652
+ // Anything installed in the version home that isn't a discovered subagent is
653
+ // an orphan. Installed enumeration is registry-driven (per-agent layout).
654
+ const orphans = listInstalledSubagentNames(agent, versionHome).filter((name) => !discovered.has(name));
1204
655
  return { agent, version, orphans: orphans.sort() };
1205
656
  }
1206
657
  /**
@@ -1229,89 +680,7 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
1229
680
  const versionHome = getVersionHomePath(agent, version);
1230
681
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1231
682
  const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
1232
- try {
1233
- if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
1234
- const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
1235
- const targetPath = path.join(versionHome, agentsRoot, 'agents', `${subagentName}.md`);
1236
- if (fs.existsSync(targetPath)) {
1237
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1238
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
1239
- }
1240
- }
1241
- else if (agent === 'kimi') {
1242
- const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
1243
- const yamlPath = path.join(agentsDir, `${subagentName}.yaml`);
1244
- const promptPath = path.join(agentsDir, `${subagentName}.system.md`);
1245
- if (fs.existsSync(yamlPath) || fs.existsSync(promptPath)) {
1246
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1247
- if (fs.existsSync(yamlPath)) {
1248
- fs.renameSync(yamlPath, path.join(trashDir, `${subagentName}.yaml.${stamp}`));
1249
- }
1250
- if (fs.existsSync(promptPath)) {
1251
- fs.renameSync(promptPath, path.join(trashDir, `${subagentName}.system.md.${stamp}`));
1252
- }
1253
- }
1254
- }
1255
- else if (agent === 'opencode') {
1256
- const targetPath = path.join(versionHome, '.config', 'opencode', 'agents', `${subagentName}.md`);
1257
- if (fs.existsSync(targetPath)) {
1258
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1259
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
1260
- }
1261
- }
1262
- else if (agent === 'antigravity') {
1263
- const targetDir = path.join(versionHome, '.gemini', 'config', 'agents', subagentName);
1264
- if (fs.existsSync(targetDir)) {
1265
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1266
- fs.renameSync(targetDir, path.join(trashDir, stamp));
1267
- }
1268
- }
1269
- else if (agent === 'copilot') {
1270
- const targetPath = path.join(versionHome, '.copilot', 'agents', `${subagentName}.agent.md`);
1271
- if (fs.existsSync(targetPath)) {
1272
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1273
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.agent.md.${stamp}`));
1274
- }
1275
- }
1276
- else if (agent === 'openclaw') {
1277
- const targetDir = path.join(versionHome, '.openclaw', subagentName);
1278
- if (fs.existsSync(targetDir)) {
1279
- const trashDest = path.join(trashDir, stamp);
1280
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1281
- fs.renameSync(targetDir, trashDest);
1282
- }
1283
- }
1284
- else if (agent === 'kiro') {
1285
- const targetPath = path.join(versionHome, '.kiro', 'agents', `${subagentName}.json`);
1286
- if (fs.existsSync(targetPath)) {
1287
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1288
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.json.${stamp}`));
1289
- }
1290
- }
1291
- else if (agent === 'cursor') {
1292
- const targetPath = path.join(versionHome, '.cursor', 'agents', `${subagentName}.md`);
1293
- if (fs.existsSync(targetPath)) {
1294
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1295
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
1296
- }
1297
- }
1298
- else if (agent === 'forge') {
1299
- const targetPath = path.join(versionHome, '.forge', 'agents', `${subagentName}.md`);
1300
- if (fs.existsSync(targetPath)) {
1301
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1302
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
1303
- }
1304
- }
1305
- else if (agent === 'goose') {
1306
- const targetPath = path.join(versionHome, '.config', 'goose', 'agents', `${subagentName}.yaml`);
1307
- if (fs.existsSync(targetPath)) {
1308
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1309
- fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.yaml.${stamp}`));
1310
- }
1311
- }
1312
- return { success: true };
1313
- }
1314
- catch (err) {
1315
- return { success: false, error: err.message };
1316
- }
683
+ // Registry-driven soft-delete: files land as `<basename>.<stamp>`, dirs as
684
+ // `<stamp>/`, matching the prior per-agent behavior for every layout.
685
+ return trashSubagentFromHome(agent, versionHome, subagentName, trashDir, stamp);
1317
686
  }