@humain/terminal 0.1.1 → 0.2.1
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 +16 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/chunk-EY4DWHQU.js +2 -0
- package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-I47ZWR55.js} +483 -506
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-QCV26HNS.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +18 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +43 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +31 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +203 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +214 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +8 -0
- package/docs/json.md +16 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { getAgentPath } from "./agent-dir.ts";
|
|
4
|
+
import { markBuiltInAgentPlugin } from "./agent-plugin-provenance.ts";
|
|
4
5
|
import type { McpConfig, ServerEntry } from "./types.ts";
|
|
6
|
+
import { resolveRealContainedPath } from "./utils.ts";
|
|
5
7
|
|
|
6
8
|
const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
7
9
|
const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
@@ -21,6 +23,8 @@ const PLUGIN_MANIFEST_FIELDS = new Set([
|
|
|
21
23
|
const MCP_CONFIG_FIELDS = new Set(["$schema", "mcpServers"]);
|
|
22
24
|
const STDIO_FIELDS = new Set(["type", "command", "args", "env", "cwd"]);
|
|
23
25
|
const HTTP_FIELDS = new Set(["type", "url", "headers"]);
|
|
26
|
+
const STRING_MANIFEST_FIELDS = ["version", "description", "homepage", "repository", "license"] as const;
|
|
27
|
+
const AUTHOR_FIELDS = new Set(["name", "email", "url"]);
|
|
24
28
|
|
|
25
29
|
interface AgentPluginManifest {
|
|
26
30
|
name: string;
|
|
@@ -50,7 +54,7 @@ export function loadAgentPluginConfigs(paths: unknown, cwd = process.cwd()): Mcp
|
|
|
50
54
|
|
|
51
55
|
export function getAgentPluginSummaries(paths: unknown, cwd = process.cwd()): AgentPluginSummary[] {
|
|
52
56
|
return getPluginPaths(paths).map(path => {
|
|
53
|
-
const pluginRoot =
|
|
57
|
+
const pluginRoot = resolvePluginRoot(path, cwd);
|
|
54
58
|
const loaded = loadAgentPluginMcpConfig(path, cwd);
|
|
55
59
|
const manifest = loaded ? readPluginManifest(pluginRoot, false) : null;
|
|
56
60
|
return {
|
|
@@ -66,7 +70,7 @@ function getPluginPaths(paths: unknown): string[] {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function loadAgentPluginMcpConfig(path: string, cwd: string): McpConfig | null {
|
|
69
|
-
const pluginRoot =
|
|
73
|
+
const pluginRoot = resolvePluginRoot(path, cwd);
|
|
70
74
|
const manifest = readPluginManifest(pluginRoot, true);
|
|
71
75
|
if (!manifest) return null;
|
|
72
76
|
|
|
@@ -76,10 +80,15 @@ function loadAgentPluginMcpConfig(path: string, cwd: string): McpConfig | null {
|
|
|
76
80
|
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: mcp.json is not a regular file`);
|
|
77
81
|
return { mcpServers: {} };
|
|
78
82
|
}
|
|
83
|
+
const resolvedMcpPath = resolveRealContainedPath(pluginRoot, mcpPath);
|
|
84
|
+
if (!resolvedMcpPath) {
|
|
85
|
+
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: mcp.json must stay inside the plugin directory`);
|
|
86
|
+
return { mcpServers: {} };
|
|
87
|
+
}
|
|
79
88
|
|
|
80
89
|
let raw: unknown;
|
|
81
90
|
try {
|
|
82
|
-
raw = JSON.parse(readFileSync(
|
|
91
|
+
raw = JSON.parse(readFileSync(resolvedMcpPath, "utf8"));
|
|
83
92
|
} catch (error) {
|
|
84
93
|
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: failed to parse mcp.json`, error);
|
|
85
94
|
return { mcpServers: {} };
|
|
@@ -98,10 +107,15 @@ function readPluginManifest(pluginRoot: string, report: boolean): AgentPluginMan
|
|
|
98
107
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json is not a regular file`);
|
|
99
108
|
return null;
|
|
100
109
|
}
|
|
110
|
+
const resolvedManifestPath = resolveRealContainedPath(pluginRoot, manifestPath);
|
|
111
|
+
if (!resolvedManifestPath) {
|
|
112
|
+
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json must stay inside the plugin directory`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
101
115
|
|
|
102
116
|
let raw: unknown;
|
|
103
117
|
try {
|
|
104
|
-
raw = JSON.parse(readFileSync(
|
|
118
|
+
raw = JSON.parse(readFileSync(resolvedManifestPath, "utf8"));
|
|
105
119
|
} catch (error) {
|
|
106
120
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: failed to parse plugin.json`, error);
|
|
107
121
|
return null;
|
|
@@ -125,6 +139,20 @@ function readPluginManifest(pluginRoot: string, report: boolean): AgentPluginMan
|
|
|
125
139
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json name is invalid`);
|
|
126
140
|
return null;
|
|
127
141
|
}
|
|
142
|
+
for (const field of STRING_MANIFEST_FIELDS) {
|
|
143
|
+
if (manifest[field] !== undefined && typeof manifest[field] !== "string") {
|
|
144
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json ${field} must be a string`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || manifest.keywords.some(value => typeof value !== "string"))) {
|
|
149
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json keywords must be an array of strings`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.author !== undefined && !isValidManifestAuthor(manifest.author)) {
|
|
153
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json author is invalid`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
128
156
|
if (manifest.extensions !== undefined && (!manifest.extensions || typeof manifest.extensions !== "object" || Array.isArray(manifest.extensions))) {
|
|
129
157
|
if (report) console.warn(`Agent Plugin ${manifest.name} ignores non-object plugin.json extensions`);
|
|
130
158
|
}
|
|
@@ -205,14 +233,14 @@ function translateStdioServer(
|
|
|
205
233
|
const env = translateEnv(raw.env, manifest, serverName);
|
|
206
234
|
if (env === null) return null;
|
|
207
235
|
|
|
208
|
-
const command = raw.command.startsWith("./") ?
|
|
209
|
-
if (command === null) return skipServer(manifest, serverName, "command must
|
|
236
|
+
const command = raw.command.startsWith("./") ? resolveRealContainedPath(pluginRoot, resolve(pluginRoot, raw.command)) : raw.command;
|
|
237
|
+
if (command === null) return skipServer(manifest, serverName, "command must resolve to an accessible path inside the plugin directory");
|
|
210
238
|
|
|
211
239
|
const pluginDataDir = getAgentPath("agent-plugin-data", manifest.name);
|
|
212
240
|
const cwd = resolvePluginCwd(raw.cwd, pluginRoot, pluginDataDir);
|
|
213
|
-
if (cwd === null) return skipServer(manifest, serverName, "cwd must
|
|
241
|
+
if (cwd === null) return skipServer(manifest, serverName, "cwd must resolve from an allowed root and stay contained");
|
|
214
242
|
|
|
215
|
-
return {
|
|
243
|
+
return markBuiltInAgentPlugin({
|
|
216
244
|
command,
|
|
217
245
|
args: args.map(value => expandPluginPlaceholders(value, pluginRoot, pluginDataDir)),
|
|
218
246
|
env: {
|
|
@@ -223,7 +251,7 @@ function translateStdioServer(
|
|
|
223
251
|
cwd,
|
|
224
252
|
pluginDataDir,
|
|
225
253
|
literalEnv: true,
|
|
226
|
-
};
|
|
254
|
+
}, ["args", "env", "cwd"]);
|
|
227
255
|
}
|
|
228
256
|
|
|
229
257
|
function translateHttpServer(
|
|
@@ -240,11 +268,11 @@ function translateHttpServer(
|
|
|
240
268
|
const headers = translateHeaders(raw.headers, manifest, serverName);
|
|
241
269
|
if (headers === null) return null;
|
|
242
270
|
|
|
243
|
-
return {
|
|
271
|
+
return markBuiltInAgentPlugin({
|
|
244
272
|
url: raw.url,
|
|
245
273
|
httpTransport: type,
|
|
246
274
|
...(headers ? { headers } : {}),
|
|
247
|
-
};
|
|
275
|
+
}, headers ? ["headers"] : []);
|
|
248
276
|
}
|
|
249
277
|
|
|
250
278
|
function formatAgentPluginServerName(pluginName: string, serverName: string): string {
|
|
@@ -273,7 +301,7 @@ function translateEnv(value: unknown, manifest: AgentPluginManifest, serverName:
|
|
|
273
301
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env must be an object of strings`);
|
|
274
302
|
return null;
|
|
275
303
|
}
|
|
276
|
-
const
|
|
304
|
+
const entries: Array<[string, string]> = [];
|
|
277
305
|
for (const [key, entry] of Object.entries(value)) {
|
|
278
306
|
if (key === "PLUGIN_ROOT" || key === "PLUGIN_DATA") {
|
|
279
307
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env must not define ${key}`);
|
|
@@ -283,9 +311,9 @@ function translateEnv(value: unknown, manifest: AgentPluginManifest, serverName:
|
|
|
283
311
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env values must be strings`);
|
|
284
312
|
return null;
|
|
285
313
|
}
|
|
286
|
-
|
|
314
|
+
entries.push([key, entry]);
|
|
287
315
|
}
|
|
288
|
-
return
|
|
316
|
+
return Object.fromEntries(entries);
|
|
289
317
|
}
|
|
290
318
|
|
|
291
319
|
function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverName: string): Record<string, string> | undefined | null {
|
|
@@ -294,7 +322,7 @@ function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverN
|
|
|
294
322
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: headers must be an object of strings`);
|
|
295
323
|
return null;
|
|
296
324
|
}
|
|
297
|
-
const
|
|
325
|
+
const entries: Array<[string, string]> = [];
|
|
298
326
|
const seen = new Set<string>();
|
|
299
327
|
for (const [key, entry] of Object.entries(value)) {
|
|
300
328
|
if (typeof entry !== "string") {
|
|
@@ -307,8 +335,9 @@ function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverN
|
|
|
307
335
|
return null;
|
|
308
336
|
}
|
|
309
337
|
seen.add(normalized);
|
|
310
|
-
|
|
338
|
+
entries.push([key, entry]);
|
|
311
339
|
}
|
|
340
|
+
const headers = Object.fromEntries(entries);
|
|
312
341
|
try {
|
|
313
342
|
new Headers(headers);
|
|
314
343
|
} catch {
|
|
@@ -324,6 +353,20 @@ function resolvePluginPath(path: string, cwd: string): string {
|
|
|
324
353
|
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
325
354
|
}
|
|
326
355
|
|
|
356
|
+
function resolvePluginRoot(path: string, cwd: string): string {
|
|
357
|
+
const resolved = resolvePluginPath(path, cwd);
|
|
358
|
+
try {
|
|
359
|
+
return realpathSync(resolved);
|
|
360
|
+
} catch {
|
|
361
|
+
return resolved;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isValidManifestAuthor(value: unknown): boolean {
|
|
366
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
367
|
+
return Object.entries(value).every(([key, entry]) => AUTHOR_FIELDS.has(key) && typeof entry === "string");
|
|
368
|
+
}
|
|
369
|
+
|
|
327
370
|
function isBareCommand(command: string): boolean {
|
|
328
371
|
return !command.includes("/") && !command.includes("\\") && !command.includes("${PLUGIN_ROOT}") && !command.includes("${PLUGIN_DATA}");
|
|
329
372
|
}
|
|
@@ -331,27 +374,23 @@ function isBareCommand(command: string): boolean {
|
|
|
331
374
|
function resolvePluginCwd(value: unknown, pluginRoot: string, pluginDataDir: string): string | null {
|
|
332
375
|
if (value === undefined) return pluginRoot;
|
|
333
376
|
if (typeof value !== "string") return null;
|
|
334
|
-
|
|
335
|
-
if (value === "${PLUGIN_ROOT}" || value.startsWith("${PLUGIN_ROOT}/")) {
|
|
336
|
-
return
|
|
377
|
+
const expanded = expandPluginPlaceholders(value, pluginRoot, pluginDataDir);
|
|
378
|
+
if (value.startsWith("./") || value === "${PLUGIN_ROOT}" || value.startsWith("${PLUGIN_ROOT}/")) {
|
|
379
|
+
return resolveRealContainedPath(pluginRoot, resolve(pluginRoot, expanded));
|
|
337
380
|
}
|
|
338
381
|
if (value === "${PLUGIN_DATA}" || value.startsWith("${PLUGIN_DATA}/")) {
|
|
339
|
-
return
|
|
382
|
+
return resolvePluginDataCwd(pluginDataDir, expanded);
|
|
340
383
|
}
|
|
341
384
|
return null;
|
|
342
385
|
}
|
|
343
386
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep) && !isAbsolute(rel))) return resolved;
|
|
348
|
-
return null;
|
|
387
|
+
function resolvePluginDataCwd(pluginDataDir: string, expanded: string): string | null {
|
|
388
|
+
if (existsSync(pluginDataDir) && !resolveRealContainedPath(dirname(pluginDataDir), pluginDataDir)) return null;
|
|
389
|
+
return resolveRealContainedPath(pluginDataDir, resolve(pluginDataDir, expanded), true);
|
|
349
390
|
}
|
|
350
391
|
|
|
351
392
|
function expandPluginPlaceholders(value: string, pluginRoot: string, pluginDataDir: string): string {
|
|
352
|
-
return value
|
|
353
|
-
.replaceAll("${PLUGIN_ROOT}", pluginRoot)
|
|
354
|
-
.replaceAll("${PLUGIN_DATA}", pluginDataDir);
|
|
393
|
+
return value.replace(/\$\{PLUGIN_(ROOT|DATA)\}/g, (_, name: string) => name === "ROOT" ? pluginRoot : pluginDataDir);
|
|
355
394
|
}
|
|
356
395
|
|
|
357
396
|
function isValidAgentPluginUrl(value: string): boolean {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ServerEntry } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const LITERAL_PLUGIN_FIELDS = ["args", "env", "cwd", "headers"] as const;
|
|
4
|
+
type LiteralPluginField = typeof LITERAL_PLUGIN_FIELDS[number];
|
|
5
|
+
const BUILT_IN_AGENT_PLUGIN = Symbol("built-in-agent-plugin");
|
|
6
|
+
type BuiltInAgentPluginEntry = ServerEntry & { [BUILT_IN_AGENT_PLUGIN]?: ReadonlySet<LiteralPluginField> };
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export function markBuiltInAgentPlugin(definition: ServerEntry, fields: LiteralPluginField[]): ServerEntry {
|
|
10
|
+
(definition as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN] = new Set(fields);
|
|
11
|
+
return definition;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function isBuiltInAgentPlugin(definition: ServerEntry, field: LiteralPluginField): boolean {
|
|
16
|
+
return (definition as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN]?.has(field) === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export function cloneBuiltInAgentPluginEntry(source: ServerEntry): ServerEntry | undefined {
|
|
21
|
+
const fields = LITERAL_PLUGIN_FIELDS.filter(field => Object.hasOwn(source, field) && isBuiltInAgentPlugin(source, field));
|
|
22
|
+
if (fields.length === 0) return undefined;
|
|
23
|
+
return markBuiltInAgentPlugin(structuredClone(source), fields);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @internal */
|
|
27
|
+
export function mergeBuiltInAgentPluginEntries(base: ServerEntry, next: ServerEntry): ServerEntry {
|
|
28
|
+
const merged = { ...base, ...next };
|
|
29
|
+
delete (merged as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN];
|
|
30
|
+
const fields = LITERAL_PLUGIN_FIELDS.filter(field => {
|
|
31
|
+
const owner = Object.hasOwn(next, field) ? next : base;
|
|
32
|
+
return Object.hasOwn(owner, field) && isBuiltInAgentPlugin(owner, field);
|
|
33
|
+
});
|
|
34
|
+
if (fields.length > 0) markBuiltInAgentPlugin(merged, fields);
|
|
35
|
+
return merged;
|
|
36
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, fstatSync, openSync, readFileSync, readdirSync, realpathSync, statSync, type Dirent, type Stats } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { formatServerNamespace, type ClaudePluginConfig, type McpConfig, type ServerEntry } from "./types.ts";
|
|
4
|
+
import { parseJsonWithComments } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
interface ClaudePluginManifest {
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClaudePluginBundleResult {
|
|
11
|
+
mcpServers: Record<string, ServerEntry>;
|
|
12
|
+
skillPaths: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadClaudePluginBundles(
|
|
16
|
+
plugins: readonly ClaudePluginConfig[] | undefined,
|
|
17
|
+
cwd: string,
|
|
18
|
+
validateConfig: (raw: unknown) => McpConfig,
|
|
19
|
+
components: { mcp: boolean; skills: boolean },
|
|
20
|
+
): ClaudePluginBundleResult {
|
|
21
|
+
const mcpServers: Record<string, ServerEntry> = {};
|
|
22
|
+
const skillPaths: string[] = [];
|
|
23
|
+
const seenRoots = new Set<string>();
|
|
24
|
+
const seenNames = new Map<string, string>();
|
|
25
|
+
const serverSources = new Map<string, string>();
|
|
26
|
+
const normalizedServerNames = new Map<string, string>();
|
|
27
|
+
|
|
28
|
+
for (const plugin of plugins ?? []) {
|
|
29
|
+
const configuredPath = resolvePluginPath(plugin.path, cwd);
|
|
30
|
+
const pluginRoot = resolvePluginRoot(configuredPath);
|
|
31
|
+
if (!pluginRoot) continue;
|
|
32
|
+
if (seenRoots.has(pluginRoot)) {
|
|
33
|
+
console.warn(`Claude plugin path is configured more than once: ${pluginRoot}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
seenRoots.add(pluginRoot);
|
|
37
|
+
|
|
38
|
+
const manifest = readPluginManifest(pluginRoot);
|
|
39
|
+
if (manifest === false) continue;
|
|
40
|
+
const pluginName = manifest?.name ?? basename(pluginRoot);
|
|
41
|
+
const previousPluginRoot = seenNames.get(pluginName);
|
|
42
|
+
if (previousPluginRoot && previousPluginRoot !== pluginRoot) {
|
|
43
|
+
console.warn(`Claude plugin name conflict for "${pluginName}": ${previousPluginRoot} and ${pluginRoot}`);
|
|
44
|
+
} else {
|
|
45
|
+
seenNames.set(pluginName, pluginRoot);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (components.mcp && plugin.mcp === true) {
|
|
49
|
+
const configPath = resolveContainedComponent(pluginRoot, ".mcp.json", "file", pluginName);
|
|
50
|
+
if (configPath) {
|
|
51
|
+
const config = readPluginMcpConfig(configPath, pluginName, pluginRoot, validateConfig);
|
|
52
|
+
for (const [serverName, server] of Object.entries(config.mcpServers)) {
|
|
53
|
+
const normalizedName = formatServerNamespace(serverName);
|
|
54
|
+
const previousName = normalizedServerNames.get(normalizedName);
|
|
55
|
+
const previousSource = previousName ? serverSources.get(previousName) : undefined;
|
|
56
|
+
if (previousName && previousSource) {
|
|
57
|
+
console.warn(`Claude plugin MCP server name conflict for "${serverName}": normalized name collides with "${previousName}" from ${previousSource}; the first plugin wins over ${configPath}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
normalizedServerNames.set(normalizedName, serverName);
|
|
61
|
+
serverSources.set(serverName, configPath);
|
|
62
|
+
mcpServers[serverName] = server;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (components.skills && plugin.skills === true) {
|
|
68
|
+
const skillsPath = resolveContainedComponent(pluginRoot, "skills", "directory", pluginName);
|
|
69
|
+
if (skillsPath) {
|
|
70
|
+
try {
|
|
71
|
+
for (const skillPath of discoverContainedSkillFiles(skillsPath, pluginName)) {
|
|
72
|
+
if (!skillPaths.includes(skillPath)) skillPaths.push(skillPath);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn(`Claude plugin "${pluginName}" skills could not be read at ${skillsPath}: ${formatError(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { mcpServers, skillPaths };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvePluginPath(path: string, cwd: string): string {
|
|
85
|
+
if (path === "~") return resolve(process.env.HOME ?? "", ".");
|
|
86
|
+
if (path.startsWith("~/")) return resolve(process.env.HOME ?? "", path.slice(2));
|
|
87
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolvePluginRoot(path: string): string | null {
|
|
91
|
+
if (!existsSync(path)) {
|
|
92
|
+
console.warn(`Claude plugin path does not exist: ${path}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const stats = safeStat(path, "Claude plugin path");
|
|
96
|
+
if (!stats) return null;
|
|
97
|
+
if (!stats.isDirectory()) {
|
|
98
|
+
console.warn(`Claude plugin path is not a directory: ${path}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return safeRealpath(path, "Claude plugin path");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readPluginManifest(pluginRoot: string): ClaudePluginManifest | null | false {
|
|
105
|
+
const configuredManifestPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
|
|
106
|
+
if (!existsSync(configuredManifestPath)) return null;
|
|
107
|
+
const stats = safeStat(configuredManifestPath, "Claude plugin manifest");
|
|
108
|
+
if (!stats) return null;
|
|
109
|
+
if (!stats.isFile()) {
|
|
110
|
+
console.warn(`Claude plugin manifest is not a regular file: ${configuredManifestPath}`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const manifestPath = safeRealpath(configuredManifestPath, "Claude plugin manifest");
|
|
114
|
+
if (!manifestPath) return null;
|
|
115
|
+
if (!isContainedPath(pluginRoot, manifestPath)) {
|
|
116
|
+
console.warn(`Claude plugin manifest resolves outside the configured plugin directory: ${manifestPath}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const manifestText = safeReadFile(manifestPath, "Claude plugin manifest", pluginRoot);
|
|
121
|
+
if (manifestText === null) return null;
|
|
122
|
+
|
|
123
|
+
let raw: unknown;
|
|
124
|
+
try {
|
|
125
|
+
raw = parseJsonWithComments(manifestText);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn(`Claude plugin manifest contains invalid JSON at ${manifestPath}: ${formatError(error)}`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
131
|
+
console.warn(`Claude plugin manifest must be a JSON object: ${manifestPath}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const manifest = raw as Record<string, unknown>;
|
|
136
|
+
if (typeof manifest.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.name)) {
|
|
137
|
+
console.warn(`Claude plugin manifest has an invalid name at ${manifestPath}: expected kebab-case`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (!hasValidManifestFieldTypes(manifest)) {
|
|
141
|
+
console.warn(`Claude plugin manifest has invalid field types at ${manifestPath}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return { name: manifest.name };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function hasValidManifestFieldTypes(manifest: Record<string, unknown>): boolean {
|
|
148
|
+
for (const field of ["$schema", "displayName", "version", "description", "homepage", "repository", "license"] as const) {
|
|
149
|
+
if (manifest[field] !== undefined && typeof manifest[field] !== "string") return false;
|
|
150
|
+
}
|
|
151
|
+
for (const field of ["commands", "agents", "skills", "hooks", "mcpServers", "lspServers", "outputStyles"] as const) {
|
|
152
|
+
const value = manifest[field];
|
|
153
|
+
if (value !== undefined && typeof value !== "string" && (!Array.isArray(value) || value.some(item => typeof item !== "string"))) return false;
|
|
154
|
+
}
|
|
155
|
+
if (manifest.defaultEnabled !== undefined && typeof manifest.defaultEnabled !== "boolean") return false;
|
|
156
|
+
if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || manifest.keywords.some(value => typeof value !== "string"))) return false;
|
|
157
|
+
if (manifest.author !== undefined) {
|
|
158
|
+
if (!manifest.author || typeof manifest.author !== "object" || Array.isArray(manifest.author)) return false;
|
|
159
|
+
const author = manifest.author as Record<string, unknown>;
|
|
160
|
+
if (typeof author.name !== "string") return false;
|
|
161
|
+
if (author.email !== undefined && typeof author.email !== "string") return false;
|
|
162
|
+
if (author.url !== undefined && typeof author.url !== "string") return false;
|
|
163
|
+
}
|
|
164
|
+
if (manifest.metadata !== undefined && (!manifest.metadata || typeof manifest.metadata !== "object" || Array.isArray(manifest.metadata))) return false;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveContainedComponent(
|
|
169
|
+
pluginRoot: string,
|
|
170
|
+
component: string,
|
|
171
|
+
expected: "file" | "directory",
|
|
172
|
+
pluginName: string,
|
|
173
|
+
): string | null {
|
|
174
|
+
const path = resolve(pluginRoot, component);
|
|
175
|
+
if (!existsSync(path)) {
|
|
176
|
+
console.warn(`Claude plugin "${pluginName}" is missing ${component}: ${path}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const stats = safeStat(path, `Claude plugin "${pluginName}" ${component}`);
|
|
180
|
+
if (!stats) return null;
|
|
181
|
+
if (expected === "file" ? !stats.isFile() : !stats.isDirectory()) {
|
|
182
|
+
console.warn(`Claude plugin "${pluginName}" ${component} is not a ${expected}: ${path}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const realPath = safeRealpath(path, `Claude plugin "${pluginName}" ${component}`);
|
|
186
|
+
if (!realPath) return null;
|
|
187
|
+
if (!isContainedPath(pluginRoot, realPath)) {
|
|
188
|
+
console.warn(`Claude plugin "${pluginName}" ${component} resolves outside the configured plugin directory: ${realPath}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return realPath;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function discoverContainedSkillFiles(skillsRoot: string, pluginName: string): string[] {
|
|
195
|
+
const skillFiles: string[] = [];
|
|
196
|
+
const visitedDirectories = new Set<string>();
|
|
197
|
+
const visit = (directory: string): void => {
|
|
198
|
+
const realDirectory = safeRealpath(directory, `Claude plugin "${pluginName}" skill directory`);
|
|
199
|
+
if (!realDirectory) return;
|
|
200
|
+
if (!isContainedPath(skillsRoot, realDirectory)) {
|
|
201
|
+
console.warn(`Claude plugin "${pluginName}" skill path resolves outside its skills directory: ${directory} -> ${realDirectory}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (visitedDirectories.has(realDirectory)) return;
|
|
205
|
+
visitedDirectories.add(realDirectory);
|
|
206
|
+
const entries = safeReadDir(realDirectory, `Claude plugin "${pluginName}" skill directory`);
|
|
207
|
+
if (!entries) return;
|
|
208
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
209
|
+
const path = resolve(realDirectory, entry.name);
|
|
210
|
+
if (entry.isSymbolicLink()) {
|
|
211
|
+
const target = safeRealpath(path, `Claude plugin "${pluginName}" skill path`);
|
|
212
|
+
if (!target) continue;
|
|
213
|
+
if (!isContainedPath(skillsRoot, target)) {
|
|
214
|
+
console.warn(`Claude plugin "${pluginName}" skill path resolves outside its skills directory: ${path} -> ${target}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const targetStats = safeStat(target, `Claude plugin "${pluginName}" skill path`);
|
|
218
|
+
if (!targetStats) continue;
|
|
219
|
+
if (targetStats.isDirectory()) visit(target);
|
|
220
|
+
else if (targetStats.isFile() && entry.name === "SKILL.md") skillFiles.push(target);
|
|
221
|
+
} else if (entry.isDirectory()) {
|
|
222
|
+
visit(path);
|
|
223
|
+
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
224
|
+
const realPath = safeRealpath(path, `Claude plugin "${pluginName}" skill path`);
|
|
225
|
+
if (!realPath) continue;
|
|
226
|
+
if (!isContainedPath(skillsRoot, realPath)) {
|
|
227
|
+
console.warn(`Claude plugin "${pluginName}" skill path resolves outside its skills directory: ${path} -> ${realPath}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
skillFiles.push(realPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
visit(skillsRoot);
|
|
235
|
+
return skillFiles;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readPluginMcpConfig(
|
|
239
|
+
configPath: string,
|
|
240
|
+
pluginName: string,
|
|
241
|
+
pluginRoot: string,
|
|
242
|
+
validateConfig: (raw: unknown) => McpConfig,
|
|
243
|
+
): McpConfig {
|
|
244
|
+
let raw: unknown;
|
|
245
|
+
try {
|
|
246
|
+
const configText = safeReadFile(configPath, `Claude plugin "${pluginName}" MCP config`, pluginRoot);
|
|
247
|
+
if (configText === null) return { mcpServers: {} };
|
|
248
|
+
raw = parseJsonWithComments(configText);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.warn(`Claude plugin "${pluginName}" has invalid MCP config at ${configPath}: ${formatError(error)}`);
|
|
251
|
+
return { mcpServers: {} };
|
|
252
|
+
}
|
|
253
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
254
|
+
console.warn(`Claude plugin "${pluginName}" MCP config must be a JSON object: ${configPath}`);
|
|
255
|
+
return { mcpServers: {} };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const servers = (raw as Record<string, unknown>).mcpServers;
|
|
259
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
260
|
+
console.warn(`Claude plugin "${pluginName}" MCP config must contain an mcpServers object: ${configPath}`);
|
|
261
|
+
return { mcpServers: {} };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const config = validateConfig(raw);
|
|
265
|
+
const validated: Record<string, ServerEntry> = {};
|
|
266
|
+
for (const serverName of Object.keys(servers)) {
|
|
267
|
+
if (serverName.trim().length === 0) {
|
|
268
|
+
console.warn(`Claude plugin "${pluginName}" has an empty MCP server name at ${configPath}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const parsed = config.mcpServers[serverName];
|
|
272
|
+
if (!parsed || !hasValidServerTransport(parsed)) {
|
|
273
|
+
console.warn(`Claude plugin "${pluginName}" has invalid MCP server "${serverName}" at ${configPath}: expected exactly one non-empty command, url, or socket transport`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
validated[serverName] = replacePluginRoot(parsed, pluginRoot);
|
|
277
|
+
}
|
|
278
|
+
return { mcpServers: validated };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function hasValidServerTransport(entry: ServerEntry): boolean {
|
|
282
|
+
return [entry.command, entry.url, entry.socket].filter(value => typeof value === "string" && value.trim().length > 0).length === 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function replacePluginRoot(entry: ServerEntry, pluginRoot: string): ServerEntry {
|
|
286
|
+
const replace = (value: string): string => value.replaceAll("${CLAUDE_PLUGIN_ROOT}", pluginRoot);
|
|
287
|
+
const replaced = replaceStrings(entry, replace) as ServerEntry;
|
|
288
|
+
if (typeof replaced.command === "string") {
|
|
289
|
+
replaced.env = { ...replaced.env, CLAUDE_PLUGIN_ROOT: pluginRoot };
|
|
290
|
+
}
|
|
291
|
+
return replaced;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function replaceStrings(value: unknown, replace: (value: string) => string): unknown {
|
|
295
|
+
if (typeof value === "string") return replace(value);
|
|
296
|
+
if (Array.isArray(value)) return value.map(item => replaceStrings(item, replace));
|
|
297
|
+
if (!value || typeof value !== "object") return value;
|
|
298
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, replaceStrings(entry, replace)]));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isContainedPath(root: string, path: string): boolean {
|
|
302
|
+
const rel = relative(root, path);
|
|
303
|
+
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep) && !isAbsolute(rel));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function safeStat(path: string, description: string): Stats | null {
|
|
307
|
+
try {
|
|
308
|
+
return statSync(path);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.warn(`${description} could not be read: ${path}: ${formatError(error)}`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function safeRealpath(path: string, description: string): string | null {
|
|
316
|
+
try {
|
|
317
|
+
return realpathSync(path);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn(`${description} could not be read: ${path}: ${formatError(error)}`);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function safeReadFile(path: string, description: string, containmentRoot: string): string | null {
|
|
325
|
+
let fd: number | undefined;
|
|
326
|
+
try {
|
|
327
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
328
|
+
const openedStats = fstatSync(fd);
|
|
329
|
+
if (!openedStats.isFile()) {
|
|
330
|
+
console.warn(`${description} is not a regular file: ${path}`);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const resolvedPath = safeRealpath(path, description);
|
|
334
|
+
if (!resolvedPath) return null;
|
|
335
|
+
if (!isContainedPath(containmentRoot, resolvedPath)) {
|
|
336
|
+
console.warn(`${description} resolves outside the configured plugin directory: ${resolvedPath}`);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const currentStats = safeStat(resolvedPath, description);
|
|
340
|
+
if (!currentStats) return null;
|
|
341
|
+
if (openedStats.dev !== currentStats.dev || openedStats.ino !== currentStats.ino) {
|
|
342
|
+
console.warn(`${description} changed while being read: ${path}`);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return readFileSync(fd, "utf8");
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.warn(`${description} could not be read: ${path}: ${formatError(error)}`);
|
|
348
|
+
return null;
|
|
349
|
+
} finally {
|
|
350
|
+
if (fd !== undefined) closeSync(fd);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function safeReadDir(path: string, description: string): Dirent[] | null {
|
|
355
|
+
try {
|
|
356
|
+
return readdirSync(path, { withFileTypes: true });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.warn(`${description} could not be read: ${path}: ${formatError(error)}`);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function formatError(error: unknown): string {
|
|
364
|
+
return error instanceof Error ? error.message : String(error);
|
|
365
|
+
}
|