@kolisachint/hoocode-agent 0.4.100 → 0.4.102
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 +20 -0
- package/dist/cli/args.d.ts +2 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +9 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extension-mcp-servers.d.ts +33 -0
- package/dist/core/extension-mcp-servers.d.ts.map +1 -0
- package/dist/core/extension-mcp-servers.js +29 -0
- package/dist/core/extension-mcp-servers.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +14 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +37 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.d.ts +24 -0
- package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -0
- package/dist/core/extensions/plugins/hooks-bridge.js +171 -0
- package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -0
- package/dist/core/extensions/plugins/index.d.ts +29 -0
- package/dist/core/extensions/plugins/index.d.ts.map +1 -0
- package/dist/core/extensions/plugins/index.js +109 -0
- package/dist/core/extensions/plugins/index.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +71 -0
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -0
- package/dist/core/extensions/plugins/manifest.js +109 -0
- package/dist/core/extensions/plugins/manifest.js.map +1 -0
- package/dist/core/extensions/plugins/marketplace.d.ts +57 -0
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -0
- package/dist/core/extensions/plugins/marketplace.js +82 -0
- package/dist/core/extensions/plugins/marketplace.js.map +1 -0
- package/dist/core/extensions/runner.d.ts +8 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/resource-loader.d.ts +10 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +46 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scheduler.d.ts +58 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/browser-continue.d.ts +25 -0
- package/dist/core/tools/browser-continue.d.ts.map +1 -0
- package/dist/core/tools/{browser-resume.js → browser-continue.js} +19 -19
- package/dist/core/tools/browser-continue.js.map +1 -0
- package/dist/core/tools/{browser-flow.d.ts → browser-run.d.ts} +10 -10
- package/dist/core/tools/browser-run.d.ts.map +1 -0
- package/dist/core/tools/{browser-flow.js → browser-run.js} +29 -20
- package/dist/core/tools/browser-run.js.map +1 -0
- package/dist/core/tools/browsertools-shared.d.ts +9 -5
- package/dist/core/tools/browsertools-shared.d.ts.map +1 -1
- package/dist/core/tools/browsertools-shared.js +8 -4
- package/dist/core/tools/browsertools-shared.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +18 -18
- package/dist/core/tools/index.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +24 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +411 -2
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/tools/browser-flow.d.ts.map +0 -1
- package/dist/core/tools/browser-flow.js.map +0 -1
- package/dist/core/tools/browser-resume.d.ts +0 -25
- package/dist/core/tools/browser-resume.d.ts.map +0 -1
- package/dist/core/tools/browser-resume.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code hooks are shell commands wired to named events and matched by tool
|
|
5
|
+
* name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.
|
|
6
|
+
* This bridge registers handlers that shell out per the hook protocol and translate
|
|
7
|
+
* stdin JSON + exit codes + stdout JSON back into hoocode result objects.
|
|
8
|
+
*
|
|
9
|
+
* Protocol (faithful to Claude Code):
|
|
10
|
+
* - Input: a JSON object on stdin describing the event.
|
|
11
|
+
* - Exit 0: success. stdout may carry a JSON decision; for prompt/session events
|
|
12
|
+
* plain stdout is treated as additional context.
|
|
13
|
+
* - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.
|
|
14
|
+
* - Other non-zero: non-blocking error (logged, not surfaced to the model).
|
|
15
|
+
* - Optional stdout JSON: `{ decision: "block"|"approve", reason, permissionDecision }`.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
19
|
+
/** Run one shell hook command, piping `input` as JSON on stdin. */
|
|
20
|
+
function runHookCommand(cmd, input, root) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const child = spawn(cmd.command, {
|
|
23
|
+
shell: true,
|
|
24
|
+
env: { ...process.env, CLAUDE_PLUGIN_ROOT: root, AGENTS_PLUGIN_ROOT: root },
|
|
25
|
+
});
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);
|
|
29
|
+
timer.unref?.();
|
|
30
|
+
child.stdout.on("data", (d) => {
|
|
31
|
+
stdout += d.toString();
|
|
32
|
+
});
|
|
33
|
+
child.stderr.on("data", (d) => {
|
|
34
|
+
stderr += d.toString();
|
|
35
|
+
});
|
|
36
|
+
child.on("error", () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve({ exitCode: 1, stdout, stderr, json: undefined });
|
|
39
|
+
});
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
let json;
|
|
43
|
+
const trimmed = stdout.trim();
|
|
44
|
+
if (trimmed.startsWith("{")) {
|
|
45
|
+
try {
|
|
46
|
+
json = JSON.parse(trimmed);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
json = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolve({ exitCode: code ?? 0, stdout, stderr, json });
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
child.stdin.write(JSON.stringify(input));
|
|
56
|
+
child.stdin.end();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* child may have already exited */
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Empty / "*" matcher matches everything; otherwise treat as an anchored regex on the tool name. */
|
|
64
|
+
function matcherMatches(matcher, toolName) {
|
|
65
|
+
if (!matcher || matcher === "*")
|
|
66
|
+
return true;
|
|
67
|
+
try {
|
|
68
|
+
return new RegExp(`^(?:${matcher})$`).test(toolName);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return matcher === toolName;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function groupsForTool(groups, toolName) {
|
|
75
|
+
const cmds = [];
|
|
76
|
+
for (const g of groups) {
|
|
77
|
+
if (matcherMatches(g.matcher, toolName))
|
|
78
|
+
cmds.push(...g.hooks);
|
|
79
|
+
}
|
|
80
|
+
return cmds;
|
|
81
|
+
}
|
|
82
|
+
function allCommands(groups) {
|
|
83
|
+
return groups.flatMap((g) => g.hooks);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register all hook events for one plugin against the ExtensionAPI.
|
|
87
|
+
* `onError` reports non-blocking failures (kept off the model's path).
|
|
88
|
+
*/
|
|
89
|
+
export function installPluginHooks(pi, hooks, root, onError) {
|
|
90
|
+
// ── PreToolUse → tool_call (blocking) ────────────────────────────────────
|
|
91
|
+
const preGroups = hooks.PreToolUse;
|
|
92
|
+
if (preGroups?.length) {
|
|
93
|
+
pi.on("tool_call", async (event) => {
|
|
94
|
+
const cmds = groupsForTool(preGroups, event.toolName);
|
|
95
|
+
for (const cmd of cmds) {
|
|
96
|
+
const res = await runHookCommand(cmd, { hook_event_name: "PreToolUse", tool_name: event.toolName, tool_input: event.input }, root);
|
|
97
|
+
const decision = res.json?.decision ?? res.json?.permissionDecision;
|
|
98
|
+
if (res.exitCode === 2 || decision === "block" || decision === "deny") {
|
|
99
|
+
return { block: true, reason: res.json?.reason || res.stderr.trim() || "Blocked by plugin hook" };
|
|
100
|
+
}
|
|
101
|
+
if (res.exitCode !== 0)
|
|
102
|
+
onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// ── PostToolUse → tool_result (best-effort) ──────────────────────────────
|
|
107
|
+
const postGroups = hooks.PostToolUse;
|
|
108
|
+
if (postGroups?.length) {
|
|
109
|
+
pi.on("tool_result", async (event) => {
|
|
110
|
+
const cmds = groupsForTool(postGroups, event.toolName);
|
|
111
|
+
for (const cmd of cmds) {
|
|
112
|
+
const res = await runHookCommand(cmd, {
|
|
113
|
+
hook_event_name: "PostToolUse",
|
|
114
|
+
tool_name: event.toolName,
|
|
115
|
+
tool_input: event.input,
|
|
116
|
+
tool_response: event.content,
|
|
117
|
+
}, root);
|
|
118
|
+
if (res.exitCode === 2 || res.json?.decision === "block") {
|
|
119
|
+
const reason = res.json?.reason || res.stderr.trim() || "Flagged by plugin hook";
|
|
120
|
+
return {
|
|
121
|
+
content: [...event.content, { type: "text", text: `\n[plugin hook] ${reason}` }],
|
|
122
|
+
isError: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (res.exitCode !== 0)
|
|
126
|
+
onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────
|
|
131
|
+
const promptGroups = hooks.UserPromptSubmit;
|
|
132
|
+
if (promptGroups?.length) {
|
|
133
|
+
pi.on("before_agent_start", async (event) => {
|
|
134
|
+
let systemPrompt = event.systemPrompt;
|
|
135
|
+
for (const cmd of allCommands(promptGroups)) {
|
|
136
|
+
const res = await runHookCommand(cmd, { hook_event_name: "UserPromptSubmit", prompt: event.prompt }, root);
|
|
137
|
+
if (res.exitCode !== 0 && res.exitCode !== 2) {
|
|
138
|
+
onError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();
|
|
142
|
+
if (extra)
|
|
143
|
+
systemPrompt = `${systemPrompt}\n\n<!-- plugin hook -->\n${extra}`;
|
|
144
|
+
}
|
|
145
|
+
return systemPrompt === event.systemPrompt ? undefined : { systemPrompt };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ── SessionStart → session_start (side effects) ──────────────────────────
|
|
149
|
+
const sessionGroups = hooks.SessionStart;
|
|
150
|
+
if (sessionGroups?.length) {
|
|
151
|
+
pi.on("session_start", async (event) => {
|
|
152
|
+
for (const cmd of allCommands(sessionGroups)) {
|
|
153
|
+
const res = await runHookCommand(cmd, { hook_event_name: "SessionStart", source: event.reason }, root);
|
|
154
|
+
if (res.exitCode !== 0)
|
|
155
|
+
onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ── Stop → agent_end (side effects) ──────────────────────────────────────
|
|
160
|
+
const stopGroups = hooks.Stop;
|
|
161
|
+
if (stopGroups?.length) {
|
|
162
|
+
pi.on("agent_end", async () => {
|
|
163
|
+
for (const cmd of allCommands(stopGroups)) {
|
|
164
|
+
const res = await runHookCommand(cmd, { hook_event_name: "Stop" }, root);
|
|
165
|
+
if (res.exitCode !== 0)
|
|
166
|
+
onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=hooks-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks-bridge.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.\n *\n * Claude Code hooks are shell commands wired to named events and matched by tool\n * name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.\n * This bridge registers handlers that shell out per the hook protocol and translate\n * stdin JSON + exit codes + stdout JSON back into hoocode result objects.\n *\n * Protocol (faithful to Claude Code):\n * - Input: a JSON object on stdin describing the event.\n * - Exit 0: success. stdout may carry a JSON decision; for prompt/session events\n * plain stdout is treated as additional context.\n * - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.\n * - Other non-zero: non-blocking error (logged, not surfaced to the model).\n * - Optional stdout JSON: `{ decision: \"block\"|\"approve\", reason, permissionDecision }`.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from \"../types.js\";\nimport type { PluginHookCommand, PluginHookMatcherGroup, PluginHooksConfig } from \"./manifest.js\";\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface HookRunResult {\n\texitCode: number;\n\tstdout: string;\n\tstderr: string;\n\tjson: { decision?: string; reason?: string; permissionDecision?: string; continue?: boolean } | undefined;\n}\n\n/** Run one shell hook command, piping `input` as JSON on stdin. */\nfunction runHookCommand(cmd: PluginHookCommand, input: unknown, root: string): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\tenv: { ...process.env, CLAUDE_PLUGIN_ROOT: root, AGENTS_PLUGIN_ROOT: root },\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tconst timer = setTimeout(() => child.kill(\"SIGTERM\"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);\n\t\ttimer.unref?.();\n\n\t\tchild.stdout.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: 1, stdout, stderr, json: undefined });\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tlet json: HookRunResult[\"json\"];\n\t\t\tconst trimmed = stdout.trim();\n\t\t\tif (trimmed.startsWith(\"{\")) {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(trimmed);\n\t\t\t\t} catch {\n\t\t\t\t\tjson = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresolve({ exitCode: code ?? 0, stdout, stderr, json });\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(input));\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t/* child may have already exited */\n\t\t}\n\t});\n}\n\n/** Empty / \"*\" matcher matches everything; otherwise treat as an anchored regex on the tool name. */\nfunction matcherMatches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher || matcher === \"*\") return true;\n\ttry {\n\t\treturn new RegExp(`^(?:${matcher})$`).test(toolName);\n\t} catch {\n\t\treturn matcher === toolName;\n\t}\n}\n\nfunction groupsForTool(groups: PluginHookMatcherGroup[], toolName: string): PluginHookCommand[] {\n\tconst cmds: PluginHookCommand[] = [];\n\tfor (const g of groups) {\n\t\tif (matcherMatches(g.matcher, toolName)) cmds.push(...g.hooks);\n\t}\n\treturn cmds;\n}\n\nfunction allCommands(groups: PluginHookMatcherGroup[]): PluginHookCommand[] {\n\treturn groups.flatMap((g) => g.hooks);\n}\n\n/**\n * Register all hook events for one plugin against the ExtensionAPI.\n * `onError` reports non-blocking failures (kept off the model's path).\n */\nexport function installPluginHooks(\n\tpi: ExtensionAPI,\n\thooks: PluginHooksConfig,\n\troot: string,\n\tonError: (message: string) => void,\n): void {\n\t// ── PreToolUse → tool_call (blocking) ────────────────────────────────────\n\tconst preGroups = hooks.PreToolUse;\n\tif (preGroups?.length) {\n\t\tpi.on(\"tool_call\", async (event: ToolCallEvent) => {\n\t\t\tconst cmds = groupsForTool(preGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"PreToolUse\", tool_name: event.toolName, tool_input: event.input },\n\t\t\t\t\troot,\n\t\t\t\t);\n\t\t\t\tconst decision = res.json?.decision ?? res.json?.permissionDecision;\n\t\t\t\tif (res.exitCode === 2 || decision === \"block\" || decision === \"deny\") {\n\t\t\t\t\treturn { block: true, reason: res.json?.reason || res.stderr.trim() || \"Blocked by plugin hook\" };\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── PostToolUse → tool_result (best-effort) ──────────────────────────────\n\tconst postGroups = hooks.PostToolUse;\n\tif (postGroups?.length) {\n\t\tpi.on(\"tool_result\", async (event: ToolResultEvent) => {\n\t\t\tconst cmds = groupsForTool(postGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{\n\t\t\t\t\t\thook_event_name: \"PostToolUse\",\n\t\t\t\t\t\ttool_name: event.toolName,\n\t\t\t\t\t\ttool_input: event.input,\n\t\t\t\t\t\ttool_response: event.content,\n\t\t\t\t\t},\n\t\t\t\t\troot,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode === 2 || res.json?.decision === \"block\") {\n\t\t\t\t\tconst reason = res.json?.reason || res.stderr.trim() || \"Flagged by plugin hook\";\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n[plugin hook] ${reason}` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────\n\tconst promptGroups = hooks.UserPromptSubmit;\n\tif (promptGroups?.length) {\n\t\tpi.on(\"before_agent_start\", async (event) => {\n\t\t\tlet systemPrompt = event.systemPrompt;\n\t\t\tfor (const cmd of allCommands(promptGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"UserPromptSubmit\", prompt: event.prompt }, root);\n\t\t\t\tif (res.exitCode !== 0 && res.exitCode !== 2) {\n\t\t\t\t\tonError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();\n\t\t\t\tif (extra) systemPrompt = `${systemPrompt}\\n\\n<!-- plugin hook -->\\n${extra}`;\n\t\t\t}\n\t\t\treturn systemPrompt === event.systemPrompt ? undefined : { systemPrompt };\n\t\t});\n\t}\n\n\t// ── SessionStart → session_start (side effects) ──────────────────────────\n\tconst sessionGroups = hooks.SessionStart;\n\tif (sessionGroups?.length) {\n\t\tpi.on(\"session_start\", async (event) => {\n\t\t\tfor (const cmd of allCommands(sessionGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"SessionStart\", source: event.reason }, root);\n\t\t\t\tif (res.exitCode !== 0) onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── Stop → agent_end (side effects) ──────────────────────────────────────\n\tconst stopGroups = hooks.Stop;\n\tif (stopGroups?.length) {\n\t\tpi.on(\"agent_end\", async () => {\n\t\t\tfor (const cmd of allCommands(stopGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"Stop\" }, root);\n\t\t\t\tif (res.exitCode !== 0) onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin discovery and wiring.
|
|
3
|
+
*
|
|
4
|
+
* Discovers plugin directories under the `plugins/` folders, parses their
|
|
5
|
+
* manifests (see {@link parsePluginDir}), and turns each into a synthetic
|
|
6
|
+
* {@link ExtensionFactory} that registers the plugin's capabilities through the
|
|
7
|
+
* existing ExtensionAPI. The factory is loaded by the standard extension loader,
|
|
8
|
+
* so plugins are just extensions assembled from a manifest instead of code.
|
|
9
|
+
*
|
|
10
|
+
* Capability wiring (minimum):
|
|
11
|
+
* - skills / themes → `resources_discover` skill/theme paths
|
|
12
|
+
* - commands → `resources_discover` slash-command paths (`.agents/commands`)
|
|
13
|
+
* - agents → `resources_discover` agent paths (`.agents/agents` subagents)
|
|
14
|
+
* - providers (native only) → `registerProvider`
|
|
15
|
+
* - hooks → shell-protocol bridge (see hooks-bridge.ts)
|
|
16
|
+
* - mcpServers → parsed; wiring deferred (see design doc)
|
|
17
|
+
*/
|
|
18
|
+
import type { ExtensionFactory } from "../types.js";
|
|
19
|
+
import { type NormalizedPlugin } from "./manifest.js";
|
|
20
|
+
export type { NormalizedPlugin } from "./manifest.js";
|
|
21
|
+
export { parsePluginDir } from "./manifest.js";
|
|
22
|
+
/**
|
|
23
|
+
* Discover plugins across the given `plugins/` directories.
|
|
24
|
+
* First-wins on duplicate ids (project dirs should be listed before global).
|
|
25
|
+
*/
|
|
26
|
+
export declare function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[];
|
|
27
|
+
/** Build a synthetic extension factory that wires one normalized plugin. */
|
|
28
|
+
export declare function buildPluginFactory(plugin: NormalizedPlugin): ExtensionFactory;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,eAAe,CAAC;AA6BtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAuBxE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAkC7E","sourcesContent":["/**\n * Plugin discovery and wiring.\n *\n * Discovers plugin directories under the `plugins/` folders, parses their\n * manifests (see {@link parsePluginDir}), and turns each into a synthetic\n * {@link ExtensionFactory} that registers the plugin's capabilities through the\n * existing ExtensionAPI. The factory is loaded by the standard extension loader,\n * so plugins are just extensions assembled from a manifest instead of code.\n *\n * Capability wiring (minimum):\n * - skills / themes → `resources_discover` skill/theme paths\n * - commands → `resources_discover` slash-command paths (`.agents/commands`)\n * - agents → `resources_discover` agent paths (`.agents/agents` subagents)\n * - providers (native only) → `registerProvider`\n * - hooks → shell-protocol bridge (see hooks-bridge.ts)\n * - mcpServers → parsed; wiring deferred (see design doc)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { type ExtensionMcpServerConfig, registerExtensionMcpServers } from \"../../extension-mcp-servers.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"../types.js\";\nimport { installPluginHooks } from \"./hooks-bridge.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Substitute ${CLAUDE_PLUGIN_ROOT} / ${AGENTS_PLUGIN_ROOT} with the plugin root in a string. */\nfunction substituteRoot(value: string, root: string): string {\n\treturn value.replace(/\\$\\{(?:CLAUDE|AGENTS)_PLUGIN_ROOT\\}/g, root);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\troot: string,\n): Record<string, ExtensionMcpServerConfig> {\n\tconst out: Record<string, ExtensionMcpServerConfig> = {};\n\tfor (const [name, raw] of Object.entries(mcpServers)) {\n\t\tif (!raw || typeof raw !== \"object\") continue;\n\t\tconst cfg = raw as ExtensionMcpServerConfig;\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteRoot(cfg.command, root),\n\t\t\targs: cfg.args?.map((a) => substituteRoot(String(a), root)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteRoot(String(v), root)]))\n\t\t\t\t: undefined,\n\t\t\tbackground: cfg.background,\n\t\t};\n\t}\n\treturn out;\n}\n\nexport type { NormalizedPlugin } from \"./manifest.js\";\nexport { parsePluginDir } from \"./manifest.js\";\n\n/**\n * Discover plugins across the given `plugins/` directories.\n * First-wins on duplicate ids (project dirs should be listed before global).\n */\nexport function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[] {\n\tconst plugins: NormalizedPlugin[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const dir of pluginDirs) {\n\t\tif (!fs.existsSync(dir)) continue;\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\t\tconst plugin = parsePluginDir(path.join(dir, entry.name));\n\t\t\tif (plugin && !seen.has(plugin.id)) {\n\t\t\t\tseen.add(plugin.id);\n\t\t\t\tplugins.push(plugin);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn plugins;\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin): ExtensionFactory {\n\tconst factory: ExtensionFactory = (pi: ExtensionAPI) => {\n\t\t// Resources: contribute the plugin's capability directories. Commands map to\n\t\t// the slash-command surface (`.agents/commands`) and agents to subagent\n\t\t// definitions (`.agents/agents`), matching hoocode's native conventions.\n\t\tif (plugin.skillsDir || plugin.commandsDir || plugin.themesDir || plugin.agentsDir) {\n\t\t\tpi.on(\"resources_discover\", () => ({\n\t\t\t\tskillPaths: plugin.skillsDir ? [plugin.skillsDir] : undefined,\n\t\t\t\tthemePaths: plugin.themesDir ? [plugin.themesDir] : undefined,\n\t\t\t\tslashCommandPaths: plugin.commandsDir ? [plugin.commandsDir] : undefined,\n\t\t\t\tagentPaths: plugin.agentsDir ? [plugin.agentsDir] : undefined,\n\t\t\t}));\n\t\t}\n\n\t\t// Providers (native plugins only).\n\t\tfor (const provider of plugin.providers ?? []) {\n\t\t\tpi.registerProvider(provider.name, provider.config);\n\t\t}\n\n\t\t// Hooks: true-parity shell bridge.\n\t\tif (plugin.hooks) {\n\t\t\tinstallPluginHooks(pi, plugin.hooks, plugin.root, () => {\n\t\t\t\t// Non-blocking hook failures are intentionally quiet (Claude Code parity).\n\t\t\t});\n\t\t}\n\n\t\t// MCP servers: register for hoo-core's setupMcpLoader to connect on session_start.\n\t\tif (plugin.mcpServers) {\n\t\t\tregisterExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, plugin.root));\n\t\t}\n\t};\n\n\tfactory.displayName = `plugin:${plugin.id}`;\n\treturn factory;\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin discovery and wiring.
|
|
3
|
+
*
|
|
4
|
+
* Discovers plugin directories under the `plugins/` folders, parses their
|
|
5
|
+
* manifests (see {@link parsePluginDir}), and turns each into a synthetic
|
|
6
|
+
* {@link ExtensionFactory} that registers the plugin's capabilities through the
|
|
7
|
+
* existing ExtensionAPI. The factory is loaded by the standard extension loader,
|
|
8
|
+
* so plugins are just extensions assembled from a manifest instead of code.
|
|
9
|
+
*
|
|
10
|
+
* Capability wiring (minimum):
|
|
11
|
+
* - skills / themes → `resources_discover` skill/theme paths
|
|
12
|
+
* - commands → `resources_discover` slash-command paths (`.agents/commands`)
|
|
13
|
+
* - agents → `resources_discover` agent paths (`.agents/agents` subagents)
|
|
14
|
+
* - providers (native only) → `registerProvider`
|
|
15
|
+
* - hooks → shell-protocol bridge (see hooks-bridge.ts)
|
|
16
|
+
* - mcpServers → parsed; wiring deferred (see design doc)
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { registerExtensionMcpServers } from "../../extension-mcp-servers.js";
|
|
21
|
+
import { installPluginHooks } from "./hooks-bridge.js";
|
|
22
|
+
import { parsePluginDir } from "./manifest.js";
|
|
23
|
+
/** Substitute ${CLAUDE_PLUGIN_ROOT} / ${AGENTS_PLUGIN_ROOT} with the plugin root in a string. */
|
|
24
|
+
function substituteRoot(value, root) {
|
|
25
|
+
return value.replace(/\$\{(?:CLAUDE|AGENTS)_PLUGIN_ROOT\}/g, root);
|
|
26
|
+
}
|
|
27
|
+
/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */
|
|
28
|
+
function resolveMcpServers(mcpServers, root) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [name, raw] of Object.entries(mcpServers)) {
|
|
31
|
+
if (!raw || typeof raw !== "object")
|
|
32
|
+
continue;
|
|
33
|
+
const cfg = raw;
|
|
34
|
+
if (typeof cfg.command !== "string")
|
|
35
|
+
continue;
|
|
36
|
+
out[name] = {
|
|
37
|
+
command: substituteRoot(cfg.command, root),
|
|
38
|
+
args: cfg.args?.map((a) => substituteRoot(String(a), root)),
|
|
39
|
+
env: cfg.env
|
|
40
|
+
? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteRoot(String(v), root)]))
|
|
41
|
+
: undefined,
|
|
42
|
+
background: cfg.background,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export { parsePluginDir } from "./manifest.js";
|
|
48
|
+
/**
|
|
49
|
+
* Discover plugins across the given `plugins/` directories.
|
|
50
|
+
* First-wins on duplicate ids (project dirs should be listed before global).
|
|
51
|
+
*/
|
|
52
|
+
export function discoverPlugins(pluginDirs) {
|
|
53
|
+
const plugins = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const dir of pluginDirs) {
|
|
56
|
+
if (!fs.existsSync(dir))
|
|
57
|
+
continue;
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
67
|
+
continue;
|
|
68
|
+
const plugin = parsePluginDir(path.join(dir, entry.name));
|
|
69
|
+
if (plugin && !seen.has(plugin.id)) {
|
|
70
|
+
seen.add(plugin.id);
|
|
71
|
+
plugins.push(plugin);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return plugins;
|
|
76
|
+
}
|
|
77
|
+
/** Build a synthetic extension factory that wires one normalized plugin. */
|
|
78
|
+
export function buildPluginFactory(plugin) {
|
|
79
|
+
const factory = (pi) => {
|
|
80
|
+
// Resources: contribute the plugin's capability directories. Commands map to
|
|
81
|
+
// the slash-command surface (`.agents/commands`) and agents to subagent
|
|
82
|
+
// definitions (`.agents/agents`), matching hoocode's native conventions.
|
|
83
|
+
if (plugin.skillsDir || plugin.commandsDir || plugin.themesDir || plugin.agentsDir) {
|
|
84
|
+
pi.on("resources_discover", () => ({
|
|
85
|
+
skillPaths: plugin.skillsDir ? [plugin.skillsDir] : undefined,
|
|
86
|
+
themePaths: plugin.themesDir ? [plugin.themesDir] : undefined,
|
|
87
|
+
slashCommandPaths: plugin.commandsDir ? [plugin.commandsDir] : undefined,
|
|
88
|
+
agentPaths: plugin.agentsDir ? [plugin.agentsDir] : undefined,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
// Providers (native plugins only).
|
|
92
|
+
for (const provider of plugin.providers ?? []) {
|
|
93
|
+
pi.registerProvider(provider.name, provider.config);
|
|
94
|
+
}
|
|
95
|
+
// Hooks: true-parity shell bridge.
|
|
96
|
+
if (plugin.hooks) {
|
|
97
|
+
installPluginHooks(pi, plugin.hooks, plugin.root, () => {
|
|
98
|
+
// Non-blocking hook failures are intentionally quiet (Claude Code parity).
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// MCP servers: register for hoo-core's setupMcpLoader to connect on session_start.
|
|
102
|
+
if (plugin.mcpServers) {
|
|
103
|
+
registerExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, plugin.root));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
factory.displayName = `plugin:${plugin.id}`;
|
|
107
|
+
return factory;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin discovery and wiring.\n *\n * Discovers plugin directories under the `plugins/` folders, parses their\n * manifests (see {@link parsePluginDir}), and turns each into a synthetic\n * {@link ExtensionFactory} that registers the plugin's capabilities through the\n * existing ExtensionAPI. The factory is loaded by the standard extension loader,\n * so plugins are just extensions assembled from a manifest instead of code.\n *\n * Capability wiring (minimum):\n * - skills / themes → `resources_discover` skill/theme paths\n * - commands → `resources_discover` slash-command paths (`.agents/commands`)\n * - agents → `resources_discover` agent paths (`.agents/agents` subagents)\n * - providers (native only) → `registerProvider`\n * - hooks → shell-protocol bridge (see hooks-bridge.ts)\n * - mcpServers → parsed; wiring deferred (see design doc)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { type ExtensionMcpServerConfig, registerExtensionMcpServers } from \"../../extension-mcp-servers.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"../types.js\";\nimport { installPluginHooks } from \"./hooks-bridge.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Substitute ${CLAUDE_PLUGIN_ROOT} / ${AGENTS_PLUGIN_ROOT} with the plugin root in a string. */\nfunction substituteRoot(value: string, root: string): string {\n\treturn value.replace(/\\$\\{(?:CLAUDE|AGENTS)_PLUGIN_ROOT\\}/g, root);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\troot: string,\n): Record<string, ExtensionMcpServerConfig> {\n\tconst out: Record<string, ExtensionMcpServerConfig> = {};\n\tfor (const [name, raw] of Object.entries(mcpServers)) {\n\t\tif (!raw || typeof raw !== \"object\") continue;\n\t\tconst cfg = raw as ExtensionMcpServerConfig;\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteRoot(cfg.command, root),\n\t\t\targs: cfg.args?.map((a) => substituteRoot(String(a), root)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteRoot(String(v), root)]))\n\t\t\t\t: undefined,\n\t\t\tbackground: cfg.background,\n\t\t};\n\t}\n\treturn out;\n}\n\nexport type { NormalizedPlugin } from \"./manifest.js\";\nexport { parsePluginDir } from \"./manifest.js\";\n\n/**\n * Discover plugins across the given `plugins/` directories.\n * First-wins on duplicate ids (project dirs should be listed before global).\n */\nexport function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[] {\n\tconst plugins: NormalizedPlugin[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const dir of pluginDirs) {\n\t\tif (!fs.existsSync(dir)) continue;\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\t\tconst plugin = parsePluginDir(path.join(dir, entry.name));\n\t\t\tif (plugin && !seen.has(plugin.id)) {\n\t\t\t\tseen.add(plugin.id);\n\t\t\t\tplugins.push(plugin);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn plugins;\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin): ExtensionFactory {\n\tconst factory: ExtensionFactory = (pi: ExtensionAPI) => {\n\t\t// Resources: contribute the plugin's capability directories. Commands map to\n\t\t// the slash-command surface (`.agents/commands`) and agents to subagent\n\t\t// definitions (`.agents/agents`), matching hoocode's native conventions.\n\t\tif (plugin.skillsDir || plugin.commandsDir || plugin.themesDir || plugin.agentsDir) {\n\t\t\tpi.on(\"resources_discover\", () => ({\n\t\t\t\tskillPaths: plugin.skillsDir ? [plugin.skillsDir] : undefined,\n\t\t\t\tthemePaths: plugin.themesDir ? [plugin.themesDir] : undefined,\n\t\t\t\tslashCommandPaths: plugin.commandsDir ? [plugin.commandsDir] : undefined,\n\t\t\t\tagentPaths: plugin.agentsDir ? [plugin.agentsDir] : undefined,\n\t\t\t}));\n\t\t}\n\n\t\t// Providers (native plugins only).\n\t\tfor (const provider of plugin.providers ?? []) {\n\t\t\tpi.registerProvider(provider.name, provider.config);\n\t\t}\n\n\t\t// Hooks: true-parity shell bridge.\n\t\tif (plugin.hooks) {\n\t\t\tinstallPluginHooks(pi, plugin.hooks, plugin.root, () => {\n\t\t\t\t// Non-blocking hook failures are intentionally quiet (Claude Code parity).\n\t\t\t});\n\t\t}\n\n\t\t// MCP servers: register for hoo-core's setupMcpLoader to connect on session_start.\n\t\tif (plugin.mcpServers) {\n\t\t\tregisterExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, plugin.root));\n\t\t}\n\t};\n\n\tfactory.displayName = `plugin:${plugin.id}`;\n\treturn factory;\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manifest parsing and normalization.
|
|
3
|
+
*
|
|
4
|
+
* A "plugin" is a directory that bundles agent capabilities (skills, commands,
|
|
5
|
+
* agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.
|
|
6
|
+
* Two manifest formats are supported and parsed into one {@link NormalizedPlugin}:
|
|
7
|
+
*
|
|
8
|
+
* - `.agents-plugin/plugin.json` — native format (a strict superset)
|
|
9
|
+
* - `.claude-plugin/plugin.json` — Claude Code compatible format
|
|
10
|
+
*
|
|
11
|
+
* When a directory carries both, the native format wins (no merge).
|
|
12
|
+
*/
|
|
13
|
+
import type { ProviderConfig } from "../types.js";
|
|
14
|
+
/** Marker subdirectory + manifest filename for each supported format. */
|
|
15
|
+
export declare const NATIVE_MANIFEST_DIR = ".agents-plugin";
|
|
16
|
+
export declare const CLAUDE_MANIFEST_DIR = ".claude-plugin";
|
|
17
|
+
export declare const MANIFEST_FILE = "plugin.json";
|
|
18
|
+
/** A single shell command invoked by a hook. */
|
|
19
|
+
export interface PluginHookCommand {
|
|
20
|
+
type?: "command";
|
|
21
|
+
command: string;
|
|
22
|
+
/** Timeout in seconds (Claude Code convention). */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
}
|
|
25
|
+
/** A matcher group: run these commands when `matcher` matches. */
|
|
26
|
+
export interface PluginHookMatcherGroup {
|
|
27
|
+
/** Regex string matched against the tool name. Empty / "*" matches everything. */
|
|
28
|
+
matcher?: string;
|
|
29
|
+
hooks: PluginHookCommand[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Hook event map keyed by Claude Code event name
|
|
33
|
+
* (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ...).
|
|
34
|
+
*/
|
|
35
|
+
export type PluginHooksConfig = Record<string, PluginHookMatcherGroup[]>;
|
|
36
|
+
/** A provider contributed by a native plugin (config-only; uses a built-in API handler). */
|
|
37
|
+
export interface PluginProvider {
|
|
38
|
+
name: string;
|
|
39
|
+
config: ProviderConfig;
|
|
40
|
+
}
|
|
41
|
+
/** Normalized, format-agnostic representation of a plugin. */
|
|
42
|
+
export interface NormalizedPlugin {
|
|
43
|
+
/** Stable identifier (manifest `name`). */
|
|
44
|
+
id: string;
|
|
45
|
+
version?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
author?: string;
|
|
48
|
+
/** Absolute path to the plugin root directory. */
|
|
49
|
+
root: string;
|
|
50
|
+
/** Absolute path to the parsed manifest file. */
|
|
51
|
+
manifestPath: string;
|
|
52
|
+
/** Which manifest format produced this plugin. */
|
|
53
|
+
format: "agents" | "claude";
|
|
54
|
+
/** Resolved capability directories (only set when they exist on disk). */
|
|
55
|
+
skillsDir?: string;
|
|
56
|
+
commandsDir?: string;
|
|
57
|
+
agentsDir?: string;
|
|
58
|
+
themesDir?: string;
|
|
59
|
+
/** Parsed hooks (from `hooks/hooks.json` or inline `hooks`). */
|
|
60
|
+
hooks?: PluginHooksConfig;
|
|
61
|
+
/** Parsed MCP servers (from `.mcp.json` or inline `mcpServers`); connected via the MCP registry. */
|
|
62
|
+
mcpServers?: Record<string, unknown>;
|
|
63
|
+
/** Native-only: providers to register. */
|
|
64
|
+
providers?: PluginProvider[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse and normalize a single plugin directory.
|
|
68
|
+
* Returns null if no recognized manifest is present or it is invalid.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parsePluginDir(root: string): NormalizedPlugin | null;
|
|
71
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IACjC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACtC,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,EAAE,CAAC,CAAC;AAEzE,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAChC,2CAA2C;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,oGAAoG;IACpG,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B;AA0DD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA0CpE","sourcesContent":["/**\n * Plugin manifest parsing and normalization.\n *\n * A \"plugin\" is a directory that bundles agent capabilities (skills, commands,\n * agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.\n * Two manifest formats are supported and parsed into one {@link NormalizedPlugin}:\n *\n * - `.agents-plugin/plugin.json` — native format (a strict superset)\n * - `.claude-plugin/plugin.json` — Claude Code compatible format\n *\n * When a directory carries both, the native format wins (no merge).\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { ProviderConfig } from \"../types.js\";\n\n/** Marker subdirectory + manifest filename for each supported format. */\nexport const NATIVE_MANIFEST_DIR = \".agents-plugin\";\nexport const CLAUDE_MANIFEST_DIR = \".claude-plugin\";\nexport const MANIFEST_FILE = \"plugin.json\";\n\n/** A single shell command invoked by a hook. */\nexport interface PluginHookCommand {\n\ttype?: \"command\";\n\tcommand: string;\n\t/** Timeout in seconds (Claude Code convention). */\n\ttimeout?: number;\n}\n\n/** A matcher group: run these commands when `matcher` matches. */\nexport interface PluginHookMatcherGroup {\n\t/** Regex string matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\thooks: PluginHookCommand[];\n}\n\n/**\n * Hook event map keyed by Claude Code event name\n * (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ...).\n */\nexport type PluginHooksConfig = Record<string, PluginHookMatcherGroup[]>;\n\n/** A provider contributed by a native plugin (config-only; uses a built-in API handler). */\nexport interface PluginProvider {\n\tname: string;\n\tconfig: ProviderConfig;\n}\n\n/** Normalized, format-agnostic representation of a plugin. */\nexport interface NormalizedPlugin {\n\t/** Stable identifier (manifest `name`). */\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/** Absolute path to the plugin root directory. */\n\troot: string;\n\t/** Absolute path to the parsed manifest file. */\n\tmanifestPath: string;\n\t/** Which manifest format produced this plugin. */\n\tformat: \"agents\" | \"claude\";\n\t/** Resolved capability directories (only set when they exist on disk). */\n\tskillsDir?: string;\n\tcommandsDir?: string;\n\tagentsDir?: string;\n\tthemesDir?: string;\n\t/** Parsed hooks (from `hooks/hooks.json` or inline `hooks`). */\n\thooks?: PluginHooksConfig;\n\t/** Parsed MCP servers (from `.mcp.json` or inline `mcpServers`); connected via the MCP registry. */\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only: providers to register. */\n\tproviders?: PluginProvider[];\n}\n\ninterface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: PluginProvider[];\n}\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map. */\nfunction normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction normalizeMcp(raw: RawManifest[\"mcpServers\"], root: string): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, \".mcp.json\");\n\tif (fs.existsSync(mcpFile)) {\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown> }>(mcpFile);\n\t\tif (fileRaw?.mcpServers && Object.keys(fileRaw.mcpServers).length > 0) return fileRaw.mcpServers;\n\t}\n\treturn undefined;\n}\n\n/**\n * Parse and normalize a single plugin directory.\n * Returns null if no recognized manifest is present or it is invalid.\n */\nexport function parsePluginDir(root: string): NormalizedPlugin | null {\n\t// Native format wins when both are present (no merge).\n\tconst nativePath = path.join(root, NATIVE_MANIFEST_DIR, MANIFEST_FILE);\n\tconst claudePath = path.join(root, CLAUDE_MANIFEST_DIR, MANIFEST_FILE);\n\n\tlet manifestPath: string;\n\tlet format: NormalizedPlugin[\"format\"];\n\tif (fs.existsSync(nativePath)) {\n\t\tmanifestPath = nativePath;\n\t\tformat = \"agents\";\n\t} else if (fs.existsSync(claudePath)) {\n\t\tmanifestPath = claudePath;\n\t\tformat = \"claude\";\n\t} else {\n\t\treturn null;\n\t}\n\n\tconst raw = readJson<RawManifest>(manifestPath);\n\tif (!raw) return null;\n\n\tconst id = (raw.name ?? path.basename(root)).trim();\n\tif (!id) return null;\n\n\tconst author = typeof raw.author === \"string\" ? raw.author : raw.author?.name;\n\n\treturn {\n\t\tid,\n\t\tversion: raw.version,\n\t\tdescription: raw.description,\n\t\tauthor,\n\t\troot,\n\t\tmanifestPath,\n\t\tformat,\n\t\tskillsDir: dirIfExists(root, \"skills\"),\n\t\tcommandsDir: dirIfExists(root, \"commands\"),\n\t\tagentsDir: dirIfExists(root, \"agents\"),\n\t\tthemesDir: dirIfExists(root, \"themes\"),\n\t\thooks: normalizeHooks(raw.hooks, root),\n\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t// Providers are native-only; ignored on the Claude-compat path.\n\t\tproviders: format === \"agents\" && Array.isArray(raw.providers) ? raw.providers : undefined,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manifest parsing and normalization.
|
|
3
|
+
*
|
|
4
|
+
* A "plugin" is a directory that bundles agent capabilities (skills, commands,
|
|
5
|
+
* agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.
|
|
6
|
+
* Two manifest formats are supported and parsed into one {@link NormalizedPlugin}:
|
|
7
|
+
*
|
|
8
|
+
* - `.agents-plugin/plugin.json` — native format (a strict superset)
|
|
9
|
+
* - `.claude-plugin/plugin.json` — Claude Code compatible format
|
|
10
|
+
*
|
|
11
|
+
* When a directory carries both, the native format wins (no merge).
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
/** Marker subdirectory + manifest filename for each supported format. */
|
|
16
|
+
export const NATIVE_MANIFEST_DIR = ".agents-plugin";
|
|
17
|
+
export const CLAUDE_MANIFEST_DIR = ".claude-plugin";
|
|
18
|
+
export const MANIFEST_FILE = "plugin.json";
|
|
19
|
+
function readJson(file) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function dirIfExists(root, name) {
|
|
28
|
+
const p = path.join(root, name);
|
|
29
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Accept either `{ hooks: {...} }` or a bare event map. */
|
|
32
|
+
function normalizeHooks(raw, root) {
|
|
33
|
+
let config;
|
|
34
|
+
if (raw && typeof raw === "object") {
|
|
35
|
+
config = "hooks" in raw && raw.hooks ? raw.hooks : raw;
|
|
36
|
+
}
|
|
37
|
+
// Fall back to the conventional hooks/hooks.json file.
|
|
38
|
+
if (!config) {
|
|
39
|
+
const hooksFile = path.join(root, "hooks", "hooks.json");
|
|
40
|
+
if (fs.existsSync(hooksFile)) {
|
|
41
|
+
const fileRaw = readJson(hooksFile);
|
|
42
|
+
if (fileRaw && typeof fileRaw === "object") {
|
|
43
|
+
config =
|
|
44
|
+
"hooks" in fileRaw && fileRaw.hooks
|
|
45
|
+
? fileRaw.hooks
|
|
46
|
+
: fileRaw;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return config && Object.keys(config).length > 0 ? config : undefined;
|
|
51
|
+
}
|
|
52
|
+
function normalizeMcp(raw, root) {
|
|
53
|
+
if (raw && Object.keys(raw).length > 0)
|
|
54
|
+
return raw;
|
|
55
|
+
const mcpFile = path.join(root, ".mcp.json");
|
|
56
|
+
if (fs.existsSync(mcpFile)) {
|
|
57
|
+
const fileRaw = readJson(mcpFile);
|
|
58
|
+
if (fileRaw?.mcpServers && Object.keys(fileRaw.mcpServers).length > 0)
|
|
59
|
+
return fileRaw.mcpServers;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse and normalize a single plugin directory.
|
|
65
|
+
* Returns null if no recognized manifest is present or it is invalid.
|
|
66
|
+
*/
|
|
67
|
+
export function parsePluginDir(root) {
|
|
68
|
+
// Native format wins when both are present (no merge).
|
|
69
|
+
const nativePath = path.join(root, NATIVE_MANIFEST_DIR, MANIFEST_FILE);
|
|
70
|
+
const claudePath = path.join(root, CLAUDE_MANIFEST_DIR, MANIFEST_FILE);
|
|
71
|
+
let manifestPath;
|
|
72
|
+
let format;
|
|
73
|
+
if (fs.existsSync(nativePath)) {
|
|
74
|
+
manifestPath = nativePath;
|
|
75
|
+
format = "agents";
|
|
76
|
+
}
|
|
77
|
+
else if (fs.existsSync(claudePath)) {
|
|
78
|
+
manifestPath = claudePath;
|
|
79
|
+
format = "claude";
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const raw = readJson(manifestPath);
|
|
85
|
+
if (!raw)
|
|
86
|
+
return null;
|
|
87
|
+
const id = (raw.name ?? path.basename(root)).trim();
|
|
88
|
+
if (!id)
|
|
89
|
+
return null;
|
|
90
|
+
const author = typeof raw.author === "string" ? raw.author : raw.author?.name;
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
version: raw.version,
|
|
94
|
+
description: raw.description,
|
|
95
|
+
author,
|
|
96
|
+
root,
|
|
97
|
+
manifestPath,
|
|
98
|
+
format,
|
|
99
|
+
skillsDir: dirIfExists(root, "skills"),
|
|
100
|
+
commandsDir: dirIfExists(root, "commands"),
|
|
101
|
+
agentsDir: dirIfExists(root, "agents"),
|
|
102
|
+
themesDir: dirIfExists(root, "themes"),
|
|
103
|
+
hooks: normalizeHooks(raw.hooks, root),
|
|
104
|
+
mcpServers: normalizeMcp(raw.mcpServers, root),
|
|
105
|
+
// Providers are native-only; ignored on the Claude-compat path.
|
|
106
|
+
providers: format === "agents" && Array.isArray(raw.providers) ? raw.providers : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,yEAAyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAkE3C,SAAS,QAAQ,CAAI,IAAY,EAAY;IAC5C,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAM,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,IAAY,EAAsB;IACpE,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACxE;AAED,4DAA4D;AAC5D,SAAS,cAAc,CAAC,GAAyB,EAAE,IAAY,EAAiC;IAC/F,IAAI,MAAqC,CAAC;IAC1C,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,GAAG,OAAO,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,CAAC,KAA2B,CAAC,CAAC,CAAE,GAAyB,CAAC;IACtG,CAAC;IACD,uDAAuD;IACvD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;QACzD,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,QAAQ,CAAuB,SAAS,CAAC,CAAC;YAC1D,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC5C,MAAM;oBACL,OAAO,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK;wBAClC,CAAC,CAAE,OAAO,CAAC,KAA2B;wBACtC,CAAC,CAAE,OAA6B,CAAC;YACpC,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACrE;AAED,SAAS,YAAY,CAAC,GAA8B,EAAE,IAAY,EAAuC;IACxG,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC7C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAA2C,OAAO,CAAC,CAAC;QAC5E,IAAI,OAAO,EAAE,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,UAAU,CAAC;IAClG,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAA2B;IACrE,uDAAuD;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,EAAE,aAAa,CAAC,CAAC;IACvE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,EAAE,aAAa,CAAC,CAAC;IAEvE,IAAI,YAAoB,CAAC;IACzB,IAAI,MAAkC,CAAC;IACvC,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,YAAY,GAAG,UAAU,CAAC;QAC1B,MAAM,GAAG,QAAQ,CAAC;IACnB,CAAC;SAAM,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,YAAY,GAAG,UAAU,CAAC;QAC1B,MAAM,GAAG,QAAQ,CAAC;IACnB,CAAC;SAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,CAAc,YAAY,CAAC,CAAC;IAChD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEtB,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAErB,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC;IAE9E,OAAO;QACN,EAAE;QACF,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,MAAM;QACN,IAAI;QACJ,YAAY;QACZ,MAAM;QACN,SAAS,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC;QACtC,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC;QAC1C,SAAS,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC;QACtC,SAAS,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC;QACtC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC;QACtC,UAAU,EAAE,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC;QAC9C,gEAAgE;QAChE,SAAS,EAAE,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;KAC1F,CAAC;AAAA,CACF","sourcesContent":["/**\n * Plugin manifest parsing and normalization.\n *\n * A \"plugin\" is a directory that bundles agent capabilities (skills, commands,\n * agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.\n * Two manifest formats are supported and parsed into one {@link NormalizedPlugin}:\n *\n * - `.agents-plugin/plugin.json` — native format (a strict superset)\n * - `.claude-plugin/plugin.json` — Claude Code compatible format\n *\n * When a directory carries both, the native format wins (no merge).\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { ProviderConfig } from \"../types.js\";\n\n/** Marker subdirectory + manifest filename for each supported format. */\nexport const NATIVE_MANIFEST_DIR = \".agents-plugin\";\nexport const CLAUDE_MANIFEST_DIR = \".claude-plugin\";\nexport const MANIFEST_FILE = \"plugin.json\";\n\n/** A single shell command invoked by a hook. */\nexport interface PluginHookCommand {\n\ttype?: \"command\";\n\tcommand: string;\n\t/** Timeout in seconds (Claude Code convention). */\n\ttimeout?: number;\n}\n\n/** A matcher group: run these commands when `matcher` matches. */\nexport interface PluginHookMatcherGroup {\n\t/** Regex string matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\thooks: PluginHookCommand[];\n}\n\n/**\n * Hook event map keyed by Claude Code event name\n * (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ...).\n */\nexport type PluginHooksConfig = Record<string, PluginHookMatcherGroup[]>;\n\n/** A provider contributed by a native plugin (config-only; uses a built-in API handler). */\nexport interface PluginProvider {\n\tname: string;\n\tconfig: ProviderConfig;\n}\n\n/** Normalized, format-agnostic representation of a plugin. */\nexport interface NormalizedPlugin {\n\t/** Stable identifier (manifest `name`). */\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/** Absolute path to the plugin root directory. */\n\troot: string;\n\t/** Absolute path to the parsed manifest file. */\n\tmanifestPath: string;\n\t/** Which manifest format produced this plugin. */\n\tformat: \"agents\" | \"claude\";\n\t/** Resolved capability directories (only set when they exist on disk). */\n\tskillsDir?: string;\n\tcommandsDir?: string;\n\tagentsDir?: string;\n\tthemesDir?: string;\n\t/** Parsed hooks (from `hooks/hooks.json` or inline `hooks`). */\n\thooks?: PluginHooksConfig;\n\t/** Parsed MCP servers (from `.mcp.json` or inline `mcpServers`); connected via the MCP registry. */\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only: providers to register. */\n\tproviders?: PluginProvider[];\n}\n\ninterface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: PluginProvider[];\n}\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map. */\nfunction normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction normalizeMcp(raw: RawManifest[\"mcpServers\"], root: string): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, \".mcp.json\");\n\tif (fs.existsSync(mcpFile)) {\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown> }>(mcpFile);\n\t\tif (fileRaw?.mcpServers && Object.keys(fileRaw.mcpServers).length > 0) return fileRaw.mcpServers;\n\t}\n\treturn undefined;\n}\n\n/**\n * Parse and normalize a single plugin directory.\n * Returns null if no recognized manifest is present or it is invalid.\n */\nexport function parsePluginDir(root: string): NormalizedPlugin | null {\n\t// Native format wins when both are present (no merge).\n\tconst nativePath = path.join(root, NATIVE_MANIFEST_DIR, MANIFEST_FILE);\n\tconst claudePath = path.join(root, CLAUDE_MANIFEST_DIR, MANIFEST_FILE);\n\n\tlet manifestPath: string;\n\tlet format: NormalizedPlugin[\"format\"];\n\tif (fs.existsSync(nativePath)) {\n\t\tmanifestPath = nativePath;\n\t\tformat = \"agents\";\n\t} else if (fs.existsSync(claudePath)) {\n\t\tmanifestPath = claudePath;\n\t\tformat = \"claude\";\n\t} else {\n\t\treturn null;\n\t}\n\n\tconst raw = readJson<RawManifest>(manifestPath);\n\tif (!raw) return null;\n\n\tconst id = (raw.name ?? path.basename(root)).trim();\n\tif (!id) return null;\n\n\tconst author = typeof raw.author === \"string\" ? raw.author : raw.author?.name;\n\n\treturn {\n\t\tid,\n\t\tversion: raw.version,\n\t\tdescription: raw.description,\n\t\tauthor,\n\t\troot,\n\t\tmanifestPath,\n\t\tformat,\n\t\tskillsDir: dirIfExists(root, \"skills\"),\n\t\tcommandsDir: dirIfExists(root, \"commands\"),\n\t\tagentsDir: dirIfExists(root, \"agents\"),\n\t\tthemesDir: dirIfExists(root, \"themes\"),\n\t\thooks: normalizeHooks(raw.hooks, root),\n\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t// Providers are native-only; ignored on the Claude-compat path.\n\t\tproviders: format === \"agents\" && Array.isArray(raw.providers) ? raw.providers : undefined,\n\t};\n}\n"]}
|