@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
@@ -1,123 +1,32 @@
1
- /**
2
- * Subagents writer. Claude/Gemini/Grok flatten each subagent into a single
3
- * .md file under their native agents directory. Codex writes TOML under
4
- * `.codex/agents/`.
5
- * Droid (Factory AI) flattens each into a custom droid .md under
6
- * `<versionHome>/.factory/droids/`. Cursor flattens each into a custom
7
- * subagent .md under `<versionHome>/.cursor/agents/`. OpenClaw copies the
8
- * full subagent directory (with AGENT.md renamed to AGENTS.md) into
9
- * `<versionHome>/.openclaw/<name>/`.
10
- *
11
- * Source-side discovery is `listInstalledSubagents` from lib/subagents.ts —
12
- * it reads user + system layers only (project layer excluded for the same
13
- * defense as commands/skills/hooks).
14
- */
15
- import * as fs from 'fs';
16
- import * as path from 'path';
17
1
  import { capableAgents } from '../../capabilities.js';
18
- import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForAntigravity, transformSubagentForDroid, transformSubagentForForge, transformSubagentForKiro, transformSubagentForCursor, transformSubagentForGoose, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
19
- import { safeJoin } from '../../paths.js';
2
+ import { listInstalledSubagents } from '../../subagents.js';
3
+ import { subagentTarget } from '../../subagents-registry.js';
20
4
  import { lazyAgentMap } from './lazy-map.js';
21
5
  function buildSubagentsWriter(agent) {
22
6
  return {
23
7
  kind: 'subagents',
24
8
  agent,
25
9
  write({ versionHome, selection }) {
10
+ const target = subagentTarget(agent);
11
+ if (!target)
12
+ return { synced: [] };
26
13
  const all = listInstalledSubagents();
27
14
  const map = new Map(all.map(s => [s.name, s]));
15
+ const dir = target.dir(versionHome);
28
16
  const synced = [];
29
17
  for (const name of selection) {
30
18
  const sub = map.get(name);
31
19
  if (!sub)
32
20
  continue;
33
21
  try {
34
- if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
35
- const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
36
- const agentsDir = path.join(versionHome, agentsRoot, 'agents');
37
- fs.mkdirSync(agentsDir, { recursive: true });
38
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForClaude(sub.path));
39
- synced.push(sub.name);
40
- }
41
- else if (agent === 'codex') {
42
- const agentsDir = path.join(versionHome, '.codex', 'agents');
43
- fs.mkdirSync(agentsDir, { recursive: true });
44
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.toml`), transformSubagentForCodex(sub.path));
45
- synced.push(sub.name);
46
- }
47
- else if (agent === 'kimi') {
48
- writeKimiSubagentFiles(path.join(versionHome, '.kimi-code', 'agents'), sub.path, sub.name);
49
- synced.push(sub.name);
50
- }
51
- else if (agent === 'opencode') {
52
- const agentsDir = path.join(versionHome, '.config', 'opencode', 'agents');
53
- fs.mkdirSync(agentsDir, { recursive: true });
54
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForOpenCode(sub.path));
55
- synced.push(sub.name);
56
- }
57
- else if (agent === 'antigravity') {
58
- const agentDir = safeJoin(path.join(versionHome, '.gemini', 'config', 'agents'), sub.name);
59
- fs.mkdirSync(agentDir, { recursive: true });
60
- fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(sub.path));
61
- synced.push(sub.name);
62
- }
63
- else if (agent === 'droid') {
64
- const droidsDir = path.join(versionHome, '.factory', 'droids');
65
- fs.mkdirSync(droidsDir, { recursive: true });
66
- fs.writeFileSync(safeJoin(droidsDir, `${sub.name}.md`), transformSubagentForDroid(sub.path));
67
- synced.push(sub.name);
68
- }
69
- else if (agent === 'copilot') {
70
- const agentsDir = path.join(versionHome, '.copilot', 'agents');
71
- fs.mkdirSync(agentsDir, { recursive: true });
72
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.agent.md`), transformSubagentForCopilot(sub.path));
73
- synced.push(sub.name);
74
- }
75
- else if (agent === 'openclaw') {
76
- const target = safeJoin(path.join(versionHome, '.openclaw'), sub.name);
77
- const r = syncSubagentToOpenclaw(sub.path, target);
78
- if (r.success)
79
- synced.push(sub.name);
80
- }
81
- else if (agent === 'kiro') {
82
- const agentsDir = path.join(versionHome, '.kiro', 'agents');
83
- fs.mkdirSync(agentsDir, { recursive: true });
84
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.json`), transformSubagentForKiro(sub.path));
85
- synced.push(sub.name);
86
- }
87
- else if (agent === 'cursor') {
88
- const agentsDir = path.join(versionHome, '.cursor', 'agents');
89
- fs.mkdirSync(agentsDir, { recursive: true });
90
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForCursor(sub.path));
91
- synced.push(sub.name);
92
- }
93
- else if (agent === 'forge') {
94
- const agentsDir = path.join(versionHome, '.forge', 'agents');
95
- fs.mkdirSync(agentsDir, { recursive: true });
96
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForForge(sub.path));
97
- synced.push(sub.name);
98
- }
99
- else if (agent === 'goose') {
100
- const agentsDir = path.join(versionHome, '.config', 'goose', 'agents');
101
- fs.mkdirSync(agentsDir, { recursive: true });
102
- fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.yaml`), transformSubagentForGoose(sub.path));
103
- synced.push(sub.name);
104
- }
22
+ target.write(dir, sub);
23
+ synced.push(sub.name);
105
24
  }
106
25
  catch { /* per-item sync failure: skip */ }
107
26
  }
108
- // Kimi parent agent file listing all synced subagents for --agent-file.
109
- if (agent === 'kimi' && synced.length > 0) {
110
- const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
111
- const entries = synced.map((name) => {
112
- const sub = map.get(name);
113
- const fm = parseSubagentFrontmatter(path.join(sub.path, 'AGENT.md'));
114
- return {
115
- name,
116
- description: fm?.description ?? name,
117
- relativePath: `./${name}.yaml`,
118
- };
119
- });
120
- fs.writeFileSync(safeJoin(agentsDir, KIMI_SUBAGENTS_PARENT_FILE), buildKimiSubagentsParentYaml(entries));
27
+ // Optional cross-item pass (e.g. Kimi's `_agents-cli.yaml` parent index).
28
+ if (target.finalize && synced.length > 0) {
29
+ target.finalize(dir, synced.map((name) => map.get(name)));
121
30
  }
122
31
  return { synced };
123
32
  },
@@ -74,6 +74,7 @@ export declare const loadWatchdog: ModuleLoader;
74
74
  export declare const loadBrowser: ModuleLoader;
75
75
  export declare const loadComputer: ModuleLoader;
76
76
  export declare const loadHosts: ModuleLoader;
77
+ export declare const loadLease: ModuleLoader;
77
78
  export declare const loadLogs: ModuleLoader;
78
79
  export declare const loadEvents: ModuleLoader;
79
80
  export declare const loadSsh: ModuleLoader;
@@ -86,6 +87,7 @@ export declare const loadTeams: ModuleLoader;
86
87
  export declare const loadCloud: ModuleLoader;
87
88
  export declare const loadMessage: ModuleLoader;
88
89
  export declare const loadFeed: ModuleLoader;
90
+ export declare const loadMailboxes: ModuleLoader;
89
91
  export declare const loadServe: ModuleLoader;
90
92
  export declare const loadAudit: ModuleLoader;
91
93
  export declare const loadWebhook: ModuleLoader;
@@ -52,6 +52,7 @@ export const loadWatchdog = async () => (await import('../../commands/watchdog.j
52
52
  export const loadBrowser = async () => (await import('../../commands/browser.js')).registerBrowserCommand;
53
53
  export const loadComputer = async () => (await import('../../commands/computer.js')).registerComputerCommand;
54
54
  export const loadHosts = async () => (await import('../../commands/hosts.js')).registerHostsCommand;
55
+ export const loadLease = async () => (await import('../../commands/lease.js')).registerLeaseCommand;
55
56
  export const loadLogs = async () => (await import('../../commands/logs.js')).registerLogsCommand;
56
57
  export const loadEvents = async () => (await import('../../commands/events.js')).registerEventsCommand;
57
58
  export const loadSsh = async () => (await import('../../commands/ssh.js')).registerSshCommands;
@@ -64,6 +65,7 @@ export const loadTeams = async () => (await import('../../commands/teams.js')).r
64
65
  export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
65
66
  export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
66
67
  export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
68
+ export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
67
69
  export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
68
70
  export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
69
71
  export const loadWebhook = async () => (await import('../../commands/webhook.js')).registerWebhookCommand;
@@ -149,6 +151,7 @@ export const COMMAND_LOADERS = {
149
151
  browser: [loadBrowser],
150
152
  computer: [loadComputer],
151
153
  hosts: [loadHosts],
154
+ lease: [loadLease],
152
155
  logs: [loadLogs],
153
156
  events: [loadEvents],
154
157
  ssh: [loadSsh],
@@ -169,6 +172,8 @@ export const COMMAND_LOADERS = {
169
172
  cloud: [loadCloud],
170
173
  message: [loadMessage],
171
174
  feed: [loadFeed],
175
+ mailboxes: [loadMailboxes],
176
+ mailbox: [loadMailboxes],
172
177
  serve: [loadServe],
173
178
  audit: [loadAudit],
174
179
  webhook: [loadWebhook],
@@ -0,0 +1,85 @@
1
+ import type { AgentId, InstalledSubagent, SubagentFrontmatter } from './types.js';
2
+ /** A path an installed subagent occupies, tagged for removal/trash handling. */
3
+ export interface OccupiedEntry {
4
+ path: string;
5
+ kind: 'file' | 'dir';
6
+ }
7
+ /** Parsed metadata for one installed subagent (drives the rich listing). */
8
+ export interface SubagentMeta {
9
+ frontmatter: SubagentFrontmatter;
10
+ files: string[];
11
+ /** Primary on-disk path for the listing's `path` field (a file or a dir, per layout). */
12
+ path: string;
13
+ }
14
+ /**
15
+ * The complete on-disk contract for one agent's subagents. Every operation is
16
+ * expressed here so the engine below never branches on the agent id.
17
+ */
18
+ export interface SubagentTarget {
19
+ /** Absolute container dir under a home root (a version home or an agent home). */
20
+ dir(home: string): string;
21
+ /** Materialize central subagent `sub` into container `dir`. Throws on fs error. */
22
+ write(dir: string, sub: {
23
+ name: string;
24
+ path: string;
25
+ }): void;
26
+ /** Installed subagent names in `dir` (detector + orphan diff). */
27
+ names(dir: string): string[];
28
+ /** On-disk paths subagent `name` occupies (for removal / soft-delete). */
29
+ occupied(dir: string, name: string): OccupiedEntry[];
30
+ /** Rich metadata for `name`; `null` skips it from the listing. */
31
+ read(dir: string, name: string): SubagentMeta | null;
32
+ /** Optional post-sync pass over the just-synced subagents (Kimi's parent index). */
33
+ finalize?(dir: string, synced: Array<{
34
+ name: string;
35
+ path: string;
36
+ }>): void;
37
+ }
38
+ /**
39
+ * Copy every file in `src` into `dest` (created if missing), applying
40
+ * `rename` (source filename -> target filename) on the way. Directories in
41
+ * `src` are skipped -- subagents are flat file sets. Throws on fs error.
42
+ */
43
+ export declare function copyDirWithRename(src: string, dest: string, rename?: Record<string, string>): void;
44
+ /**
45
+ * Single source of truth for how each subagents-capable agent stores subagents.
46
+ * The keys MUST match `capableAgents('subagents')` (the `subagents` flag in
47
+ * `agents.ts`): the capability flag is the version gate, this table is the shape.
48
+ */
49
+ export declare const SUBAGENT_TARGETS: Partial<Record<AgentId, SubagentTarget>>;
50
+ /** The registry entry for `agent`, or undefined if it stores no subagents. */
51
+ export declare function subagentTarget(agent: AgentId): SubagentTarget | undefined;
52
+ /**
53
+ * Materialize central subagent `sub` into `home` for `agent`. Returns whether a
54
+ * write happened (false when the agent has no registry entry). Throws only on
55
+ * unexpected fs errors -- bulk callers wrap per-item.
56
+ */
57
+ export declare function writeSubagentToHome(agent: AgentId, home: string, sub: {
58
+ name: string;
59
+ path: string;
60
+ }): boolean;
61
+ /** Installed subagent names for `agent` under `home` (detector + orphan diff). */
62
+ export declare function listInstalledSubagentNames(agent: AgentId, home: string): string[];
63
+ /**
64
+ * Rich listing of subagents installed for `agent` under `home`, with parsed
65
+ * metadata. Enumerates names, then reads each -- entries whose metadata is
66
+ * unreadable (per the target's reader) are dropped.
67
+ */
68
+ export declare function listInstalledSubagentsRich(agent: AgentId, home: string): InstalledSubagent[];
69
+ /**
70
+ * Remove subagent `name` for `agent` from `home` (hard delete). No-op success
71
+ * when the agent has no registry entry or nothing is installed.
72
+ */
73
+ export declare function removeSubagentFromHome(agent: AgentId, home: string, name: string): {
74
+ success: boolean;
75
+ error?: string;
76
+ };
77
+ /**
78
+ * Soft-delete subagent `name` for `agent` from `home` into `trashDir`, stamping
79
+ * each moved entry. Files land as `<basename>.<stamp>`, directories as
80
+ * `<stamp>/`. No-op success when nothing is installed.
81
+ */
82
+ export declare function trashSubagentFromHome(agent: AgentId, home: string, name: string, trashDir: string, stamp: string): {
83
+ success: boolean;
84
+ error?: string;
85
+ };
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Declarative subagent-target registry.
3
+ *
4
+ * Each subagents-capable agent gets ONE table entry (`SUBAGENT_TARGETS`)
5
+ * describing how a central subagent (`~/.agents/subagents/<name>/`) is
6
+ * materialized into that agent's home, how it is enumerated, and where it lives
7
+ * on disk. Generic install / list / detect / orphan / remove logic iterates the
8
+ * table instead of the near-identical `else if (agent === '...')` chains that
9
+ * used to be copy-pasted across `subagents.ts`, the staleness writer, and the
10
+ * staleness detector -- roughly O(agents x operations) arms.
11
+ *
12
+ * Adding a *standard* integration is now one line here (plus the `subagents`
13
+ * capability flag in `agents.ts`, the version gate). Three layout builders cover
14
+ * every current agent, so most entries are a single call:
15
+ *
16
+ * - `flatFile` one `<name><ext>` file, body from a `transform` fn.
17
+ * (claude, gemini, grok, droid, codex, opencode, copilot,
18
+ * cursor, forge, kiro, goose)
19
+ * - `dirFile` a `<name>/` directory holding one generated `<file>`.
20
+ * (antigravity: `<name>/agent.md`)
21
+ * - `dirCopy` copy the whole source directory to `<name>/`, applying
22
+ * renames, detected by a `marker` file. (openclaw)
23
+ *
24
+ * Genuinely-bespoke agents keep an explicit handler in the same table -- Kimi
25
+ * writes two files per subagent plus a managed parent index, so it is a hand
26
+ * -written `SubagentTarget` rather than a builder call.
27
+ *
28
+ * The per-agent `transform`/metadata parsers are the escape hatch: they live in
29
+ * `subagents.ts` and are referenced by the table, so the generic engine has zero
30
+ * per-agent branches. See the integration tiers in `docs/subagents.md`.
31
+ */
32
+ import * as fs from 'fs';
33
+ import * as path from 'path';
34
+ import * as yaml from 'yaml';
35
+ import { safeJoin } from './paths.js';
36
+ import { parseSubagentFrontmatter, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, transformSubagentForCursor, transformSubagentForDroid, transformSubagentForForge, transformSubagentForGoose, transformSubagentForKiro, transformSubagentForOpenCode, transformSubagentForAntigravity, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, } from './subagents.js';
37
+ // ── metadata readers (the per-format escape hatch) ───────────────────────────
38
+ /** Frontmatter, skipping files that lack a valid block (claude/gemini/grok/droid/codex). */
39
+ function metaFrontmatterSkip(filePath) {
40
+ return parseSubagentFrontmatter(filePath);
41
+ }
42
+ /** Frontmatter, falling back to an empty description (opencode/copilot/cursor/forge). */
43
+ function metaFrontmatterFallback(filePath, name) {
44
+ return parseSubagentFrontmatter(filePath) ?? { name, description: '' };
45
+ }
46
+ /** Kiro custom-agent JSON: read name/description/model; skip on parse error. */
47
+ function metaJson(filePath, name) {
48
+ try {
49
+ const cfg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
50
+ return { name: cfg.name || name, description: cfg.description || '', model: cfg.model };
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /** Goose recipe YAML: title -> name, description; skip on parse error. */
57
+ function metaGooseYaml(filePath, name) {
58
+ try {
59
+ const recipe = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
60
+ return { name: recipe?.title || name, description: recipe?.description || '' };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ // ── shared fs primitive ──────────────────────────────────────────────────────
67
+ /**
68
+ * Copy every file in `src` into `dest` (created if missing), applying
69
+ * `rename` (source filename -> target filename) on the way. Directories in
70
+ * `src` are skipped -- subagents are flat file sets. Throws on fs error.
71
+ */
72
+ export function copyDirWithRename(src, dest, rename) {
73
+ if (!fs.existsSync(dest)) {
74
+ fs.mkdirSync(dest, { recursive: true });
75
+ }
76
+ for (const file of fs.readdirSync(src)) {
77
+ const sourcePath = path.join(src, file);
78
+ if (!fs.statSync(sourcePath).isFile())
79
+ continue;
80
+ const targetName = rename?.[file] ?? file;
81
+ fs.copyFileSync(sourcePath, path.join(dest, targetName));
82
+ }
83
+ }
84
+ // ── layout builders ──────────────────────────────────────────────────────────
85
+ /** One flattened `<name><ext>` file per subagent under `subdir`. */
86
+ function flatFile(opts) {
87
+ const readMeta = opts.readMeta ?? metaFrontmatterSkip;
88
+ return {
89
+ dir: (home) => path.join(home, ...opts.subdir),
90
+ write(dir, sub) {
91
+ fs.mkdirSync(dir, { recursive: true });
92
+ fs.writeFileSync(safeJoin(dir, `${sub.name}${opts.ext}`), opts.transform(sub.path));
93
+ },
94
+ names(dir) {
95
+ if (!fs.existsSync(dir))
96
+ return [];
97
+ return fs
98
+ .readdirSync(dir)
99
+ .filter((f) => f.endsWith(opts.ext))
100
+ .map((f) => f.slice(0, -opts.ext.length));
101
+ },
102
+ occupied(dir, name) {
103
+ return [{ path: safeJoin(dir, `${name}${opts.ext}`), kind: 'file' }];
104
+ },
105
+ read(dir, name) {
106
+ const filePath = path.join(dir, `${name}${opts.ext}`);
107
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
108
+ return null;
109
+ const frontmatter = readMeta(filePath, name);
110
+ if (!frontmatter)
111
+ return null;
112
+ return { frontmatter, files: [`${name}${opts.ext}`], path: filePath };
113
+ },
114
+ };
115
+ }
116
+ /** A `<name>/` directory holding one generated `<file>` per subagent. */
117
+ function dirFile(opts) {
118
+ const readMeta = opts.readMeta ?? ((p, name) => metaFrontmatterFallback(p, name));
119
+ return {
120
+ dir: (home) => path.join(home, ...opts.subdir),
121
+ write(dir, sub) {
122
+ const target = safeJoin(dir, sub.name);
123
+ fs.mkdirSync(target, { recursive: true });
124
+ fs.writeFileSync(safeJoin(target, opts.file), opts.transform(sub.path));
125
+ },
126
+ names(dir) {
127
+ if (!fs.existsSync(dir))
128
+ return [];
129
+ return fs
130
+ .readdirSync(dir, { withFileTypes: true })
131
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, opts.file)))
132
+ .map((e) => e.name);
133
+ },
134
+ occupied(dir, name) {
135
+ return [{ path: safeJoin(dir, name), kind: 'dir' }];
136
+ },
137
+ read(dir, name) {
138
+ const filePath = path.join(dir, name, opts.file);
139
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
140
+ return null;
141
+ const frontmatter = readMeta(filePath, name);
142
+ if (!frontmatter)
143
+ return null;
144
+ return { frontmatter, files: [opts.file], path: filePath };
145
+ },
146
+ };
147
+ }
148
+ /** Copy the whole source directory to `<name>/`, detected by `marker`. */
149
+ function dirCopy(opts) {
150
+ return {
151
+ dir: (home) => path.join(home, ...opts.subdir),
152
+ write(dir, sub) {
153
+ copyDirWithRename(sub.path, safeJoin(dir, sub.name), opts.rename);
154
+ },
155
+ names(dir) {
156
+ if (!fs.existsSync(dir))
157
+ return [];
158
+ return fs
159
+ .readdirSync(dir, { withFileTypes: true })
160
+ .filter((d) => d.isDirectory() && fs.existsSync(path.join(dir, d.name, opts.marker)))
161
+ .map((d) => d.name);
162
+ },
163
+ occupied(dir, name) {
164
+ return [{ path: safeJoin(dir, name), kind: 'dir' }];
165
+ },
166
+ read(dir, name) {
167
+ const markerPath = path.join(dir, name, opts.marker);
168
+ if (!fs.existsSync(markerPath))
169
+ return null;
170
+ // The marker may lack frontmatter; fall back to the first content line.
171
+ let frontmatter = { name, description: '' };
172
+ const parsed = parseSubagentFrontmatter(markerPath);
173
+ if (parsed) {
174
+ frontmatter = parsed;
175
+ }
176
+ else {
177
+ const content = fs.readFileSync(markerPath, 'utf-8');
178
+ const firstLine = content.split('\n').find((l) => l.trim() && !l.startsWith('#'));
179
+ frontmatter.description = firstLine?.slice(0, 80) || `${name}`;
180
+ }
181
+ const subagentDir = path.join(dir, name);
182
+ const files = fs
183
+ .readdirSync(subagentDir)
184
+ .filter((f) => f.endsWith('.md'))
185
+ .sort();
186
+ return { frontmatter, files, path: subagentDir };
187
+ },
188
+ };
189
+ }
190
+ /**
191
+ * Kimi (genuinely bespoke): each subagent is a `<name>.yaml` + sibling
192
+ * `<name>.system.md`, and a managed `_agents-cli.yaml` parent lists them all for
193
+ * `kimi --agent-file`. The parent index is a cross-item concern, so it is built
194
+ * in `finalize`, not per-item.
195
+ */
196
+ const kimiTarget = {
197
+ dir: (home) => path.join(home, '.kimi-code', 'agents'),
198
+ write(dir, sub) {
199
+ writeKimiSubagentFiles(dir, sub.path, sub.name);
200
+ },
201
+ names(dir) {
202
+ if (!fs.existsSync(dir))
203
+ return [];
204
+ // The parent is `_agents-cli.yaml` (underscore-prefixed, reserved).
205
+ return fs
206
+ .readdirSync(dir)
207
+ .filter((f) => f.endsWith('.yaml') && !f.startsWith('_'))
208
+ .map((f) => f.slice(0, -'.yaml'.length));
209
+ },
210
+ occupied(dir, name) {
211
+ return [
212
+ { path: safeJoin(dir, `${name}.yaml`), kind: 'file' },
213
+ { path: safeJoin(dir, `${name}.system.md`), kind: 'file' },
214
+ ];
215
+ },
216
+ read(dir, name) {
217
+ const yamlPath = path.join(dir, `${name}.yaml`);
218
+ if (!fs.existsSync(yamlPath) || !fs.statSync(yamlPath).isFile())
219
+ return null;
220
+ let description = '';
221
+ try {
222
+ const parsed = yaml.parse(fs.readFileSync(yamlPath, 'utf-8'));
223
+ description = parsed?.agent?.description ?? '';
224
+ }
225
+ catch {
226
+ /* leave description empty */
227
+ }
228
+ const files = [`${name}.yaml`];
229
+ const promptFile = `${name}.system.md`;
230
+ if (fs.existsSync(path.join(dir, promptFile)))
231
+ files.push(promptFile);
232
+ return { frontmatter: { name, description }, files, path: yamlPath };
233
+ },
234
+ finalize(dir, synced) {
235
+ const entries = synced.map((sub) => {
236
+ const fm = parseSubagentFrontmatter(path.join(sub.path, 'AGENT.md'));
237
+ return { name: sub.name, description: fm?.description ?? sub.name, relativePath: `./${sub.name}.yaml` };
238
+ });
239
+ fs.writeFileSync(safeJoin(dir, KIMI_SUBAGENTS_PARENT_FILE), buildKimiSubagentsParentYaml(entries));
240
+ },
241
+ };
242
+ // ── the registry ─────────────────────────────────────────────────────────────
243
+ /**
244
+ * Single source of truth for how each subagents-capable agent stores subagents.
245
+ * The keys MUST match `capableAgents('subagents')` (the `subagents` flag in
246
+ * `agents.ts`): the capability flag is the version gate, this table is the shape.
247
+ */
248
+ export const SUBAGENT_TARGETS = {
249
+ // Tier 1 -- flat markdown, Claude-compatible flatten.
250
+ claude: flatFile({ subdir: ['.claude', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
251
+ gemini: flatFile({ subdir: ['.gemini', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
252
+ grok: flatFile({ subdir: ['.grok', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
253
+ droid: flatFile({ subdir: ['.factory', 'droids'], ext: '.md', transform: transformSubagentForDroid }),
254
+ // Bespoke frontmatter/format, still one flat file.
255
+ codex: flatFile({ subdir: ['.codex', 'agents'], ext: '.toml', transform: transformSubagentForCodex }),
256
+ opencode: flatFile({
257
+ subdir: ['.config', 'opencode', 'agents'],
258
+ ext: '.md',
259
+ transform: transformSubagentForOpenCode,
260
+ readMeta: metaFrontmatterFallback,
261
+ }),
262
+ copilot: flatFile({
263
+ subdir: ['.copilot', 'agents'],
264
+ ext: '.agent.md',
265
+ transform: transformSubagentForCopilot,
266
+ readMeta: metaFrontmatterFallback,
267
+ }),
268
+ cursor: flatFile({
269
+ subdir: ['.cursor', 'agents'],
270
+ ext: '.md',
271
+ transform: transformSubagentForCursor,
272
+ readMeta: metaFrontmatterFallback,
273
+ }),
274
+ forge: flatFile({
275
+ subdir: ['.forge', 'agents'],
276
+ ext: '.md',
277
+ transform: transformSubagentForForge,
278
+ readMeta: metaFrontmatterFallback,
279
+ }),
280
+ kiro: flatFile({
281
+ subdir: ['.kiro', 'agents'],
282
+ ext: '.json',
283
+ transform: transformSubagentForKiro,
284
+ readMeta: metaJson,
285
+ }),
286
+ goose: flatFile({
287
+ subdir: ['.config', 'goose', 'agents'],
288
+ ext: '.yaml',
289
+ transform: transformSubagentForGoose,
290
+ readMeta: metaGooseYaml,
291
+ }),
292
+ // Directory layouts.
293
+ antigravity: dirFile({
294
+ subdir: ['.gemini', 'config', 'agents'],
295
+ file: 'agent.md',
296
+ transform: transformSubagentForAntigravity,
297
+ }),
298
+ openclaw: dirCopy({ subdir: ['.openclaw'], marker: 'AGENTS.md', rename: { 'AGENT.md': 'AGENTS.md' } }),
299
+ // Bespoke multi-file + parent index.
300
+ kimi: kimiTarget,
301
+ };
302
+ /** The registry entry for `agent`, or undefined if it stores no subagents. */
303
+ export function subagentTarget(agent) {
304
+ return SUBAGENT_TARGETS[agent];
305
+ }
306
+ // ── generic engine (zero per-agent branches) ─────────────────────────────────
307
+ /**
308
+ * Materialize central subagent `sub` into `home` for `agent`. Returns whether a
309
+ * write happened (false when the agent has no registry entry). Throws only on
310
+ * unexpected fs errors -- bulk callers wrap per-item.
311
+ */
312
+ export function writeSubagentToHome(agent, home, sub) {
313
+ const target = SUBAGENT_TARGETS[agent];
314
+ if (!target)
315
+ return false;
316
+ target.write(target.dir(home), sub);
317
+ return true;
318
+ }
319
+ /** Installed subagent names for `agent` under `home` (detector + orphan diff). */
320
+ export function listInstalledSubagentNames(agent, home) {
321
+ const target = SUBAGENT_TARGETS[agent];
322
+ if (!target)
323
+ return [];
324
+ return target.names(target.dir(home));
325
+ }
326
+ /**
327
+ * Rich listing of subagents installed for `agent` under `home`, with parsed
328
+ * metadata. Enumerates names, then reads each -- entries whose metadata is
329
+ * unreadable (per the target's reader) are dropped.
330
+ */
331
+ export function listInstalledSubagentsRich(agent, home) {
332
+ const target = SUBAGENT_TARGETS[agent];
333
+ if (!target)
334
+ return [];
335
+ const dir = target.dir(home);
336
+ const out = [];
337
+ for (const name of target.names(dir)) {
338
+ const meta = target.read(dir, name);
339
+ if (!meta)
340
+ continue;
341
+ out.push({ name, path: meta.path, files: meta.files, frontmatter: meta.frontmatter });
342
+ }
343
+ return out;
344
+ }
345
+ /**
346
+ * Remove subagent `name` for `agent` from `home` (hard delete). No-op success
347
+ * when the agent has no registry entry or nothing is installed.
348
+ */
349
+ export function removeSubagentFromHome(agent, home, name) {
350
+ const target = SUBAGENT_TARGETS[agent];
351
+ if (!target)
352
+ return { success: true };
353
+ try {
354
+ for (const entry of target.occupied(target.dir(home), name)) {
355
+ if (!fs.existsSync(entry.path))
356
+ continue;
357
+ if (entry.kind === 'dir')
358
+ fs.rmSync(entry.path, { recursive: true, force: true });
359
+ else
360
+ fs.unlinkSync(entry.path);
361
+ }
362
+ return { success: true };
363
+ }
364
+ catch (err) {
365
+ return { success: false, error: String(err) };
366
+ }
367
+ }
368
+ /**
369
+ * Soft-delete subagent `name` for `agent` from `home` into `trashDir`, stamping
370
+ * each moved entry. Files land as `<basename>.<stamp>`, directories as
371
+ * `<stamp>/`. No-op success when nothing is installed.
372
+ */
373
+ export function trashSubagentFromHome(agent, home, name, trashDir, stamp) {
374
+ const target = SUBAGENT_TARGETS[agent];
375
+ if (!target)
376
+ return { success: true };
377
+ try {
378
+ const present = target.occupied(target.dir(home), name).filter((e) => fs.existsSync(e.path));
379
+ if (present.length > 0) {
380
+ fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
381
+ for (const entry of present) {
382
+ const dest = entry.kind === 'dir'
383
+ ? path.join(trashDir, stamp)
384
+ : path.join(trashDir, `${path.basename(entry.path)}.${stamp}`);
385
+ fs.renameSync(entry.path, dest);
386
+ }
387
+ }
388
+ return { success: true };
389
+ }
390
+ catch (err) {
391
+ return { success: false, error: err.message };
392
+ }
393
+ }