@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.
- package/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +55 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/routines.js +20 -14
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.js +98 -3
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +27 -17
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +6 -1
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +21 -2
- package/dist/lib/routines.js +35 -12
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- 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
|
|
19
|
-
import {
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
+
}
|