@kolisachint/hoocode-agent 0.5.3 → 0.5.4
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 +2 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -7
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/capabilities/deferral.d.ts +78 -0
- package/dist/core/capabilities/deferral.d.ts.map +1 -0
- package/dist/core/capabilities/deferral.js +99 -0
- package/dist/core/capabilities/deferral.js.map +1 -0
- package/dist/core/capabilities/dense.d.ts +51 -0
- package/dist/core/capabilities/dense.d.ts.map +1 -0
- package/dist/core/capabilities/dense.js +121 -0
- package/dist/core/capabilities/dense.js.map +1 -0
- package/dist/core/capabilities/lexical.d.ts +43 -0
- package/dist/core/capabilities/lexical.d.ts.map +1 -0
- package/dist/core/capabilities/lexical.js +104 -0
- package/dist/core/capabilities/lexical.js.map +1 -0
- package/dist/core/capabilities/registry.d.ts +56 -0
- package/dist/core/capabilities/registry.d.ts.map +1 -0
- package/dist/core/capabilities/registry.js +0 -0
- package/dist/core/capabilities/registry.js.map +1 -0
- package/dist/core/capabilities/search.d.ts +47 -0
- package/dist/core/capabilities/search.d.ts.map +1 -0
- package/dist/core/capabilities/search.js +82 -0
- package/dist/core/capabilities/search.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +27 -3
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +61 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +56 -9
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
- package/dist/core/extensions/plugins/authoring.js +0 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -1
- package/dist/core/extensions/plugins/drift.d.ts +75 -0
- package/dist/core/extensions/plugins/drift.d.ts.map +1 -0
- package/dist/core/extensions/plugins/drift.js +194 -0
- package/dist/core/extensions/plugins/drift.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/claude.js +5 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.d.ts +6 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.js +57 -16
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
- package/dist/core/extensions/plugins/formats/index.d.ts +39 -1
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/index.js +90 -13
- package/dist/core/extensions/plugins/formats/index.js.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +14 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.js +37 -4
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts +48 -24
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.js +62 -30
- package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.d.ts +22 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.js +81 -4
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
- package/dist/core/extensions/plugins/formats/types.d.ts +10 -1
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/types.js.map +1 -1
- package/dist/core/extensions/plugins/gates.d.ts +64 -0
- package/dist/core/extensions/plugins/gates.d.ts.map +1 -0
- package/dist/core/extensions/plugins/gates.js +297 -0
- package/dist/core/extensions/plugins/gates.js.map +1 -0
- package/dist/core/extensions/plugins/hooks-bridge.d.ts +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.js +12 -8
- package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts +36 -5
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +66 -15
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +43 -24
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +167 -54
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/locations.d.ts +84 -0
- package/dist/core/extensions/plugins/locations.d.ts.map +1 -0
- package/dist/core/extensions/plugins/locations.js +137 -0
- package/dist/core/extensions/plugins/locations.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +21 -1
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +2 -2
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/packaging.d.ts +115 -0
- package/dist/core/extensions/plugins/packaging.d.ts.map +1 -0
- package/dist/core/extensions/plugins/packaging.js +362 -0
- package/dist/core/extensions/plugins/packaging.js.map +1 -0
- package/dist/core/extensions/plugins/smoke.d.ts +33 -0
- package/dist/core/extensions/plugins/smoke.d.ts.map +1 -0
- package/dist/core/extensions/plugins/smoke.js +231 -0
- package/dist/core/extensions/plugins/smoke.js.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts +136 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.js +0 -0
- package/dist/core/extensions/plugins/trigger-eval.js.map +1 -0
- package/dist/core/package-manager.d.ts +6 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +15 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/skills.d.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +23 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/tools/package-plugin.d.ts +19 -0
- package/dist/core/tools/package-plugin.d.ts.map +1 -0
- package/dist/core/tools/package-plugin.js +81 -0
- package/dist/core/tools/package-plugin.js.map +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
- package/dist/core/tools/plugin-tool-names.js +6 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -1
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +48 -7
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +49 -21
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +14 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +77 -18
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +39 -2
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -1
- package/dist/extensions/core/mcp-deferred.js +74 -10
- package/dist/extensions/core/mcp-deferred.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +52 -9
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts +3 -3
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +9 -9
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +25 -7
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +8 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +8 -2
- package/dist/utils/tools-manager.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 +5 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/gates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin eval gates G1 and G2 — the green signal an authored or installed plugin\n * must produce before anything activates it.\n *\n * Nothing used to run between \"the model writes a plugin\" and \"the plugin is\n * live\". These gates fill that gap, and they run against the **draft dir**, not\n * a production home: a failure discards the draft, so a plugin that does not\n * pass never reaches `~/.claude/skills/` where Claude Code would load it.\n *\n * G1 structural + conformant — does it parse, is the id legal, is the content\n * portable, and does the *target platform* accept it\n * G2 static safety — can the executable content plausibly run, and is it\n * obviously dangerous\n *\n * G1 measures conformance against the vendor where it can. Round-tripping\n * through our own parser only proves the emitter and the reader agree with each\n * other; it says nothing about whether Claude Code will accept the artifact. So\n * when the `claude` CLI is present, its own `plugin validate` is the authority —\n * it is the exact check the vendor's review pipeline runs, and inventing a\n * second opinion where the vendor ships one would just be a different set of\n * bugs. Copilot has no equivalent, so `github` targets get round-trip + schema.\n *\n * G3 (sandboxed behavioral smoke) and G4 (trigger eval) are separate; see\n * docs/plugin-system-architecture.md §4.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { classifyAllowlist } from \"./authoring.js\";\nimport type { PluginPlatform } from \"./formats/platform-targets.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\nexport interface GateFinding {\n\tgate: \"G1\" | \"G2\" | \"G3\" | \"G4\";\n\t/**\n\t * `error` fails the gate. `warning` is reported and passes, unless `strict`.\n\t * `info` always passes — it exists so a *successful* check can be shown to the\n\t * human in the confirmation prompt without `strict` treating \"the hook ran\n\t * fine\" as a reason to refuse.\n\t */\n\tseverity: \"error\" | \"warning\" | \"info\";\n\tmessage: string;\n}\n\nexport interface GateResult {\n\tok: boolean;\n\tfindings: GateFinding[];\n\t/** How conformance was established, for the record and for the confirm prompt. */\n\tconformance: \"claude-plugin-validate\" | \"round-trip\";\n\tplugin: NormalizedPlugin | null;\n}\n\nexport interface GateOptions {\n\t/** Target platform, which decides whether a vendor validator applies. */\n\tplatform?: PluginPlatform;\n\t/** Treat warnings as errors. Used on the publish path. */\n\tstrict?: boolean;\n\t/** Skip the vendor validator (tests, and callers that must stay hermetic). */\n\tskipVendorValidator?: boolean;\n}\n\n/**\n * Opt out of the vendor validator process-wide.\n *\n * It costs a subprocess of a few hundred milliseconds per plugin — fine once per\n * authored plugin, ruinous across a test suite that authors dozens. Also the\n * escape hatch for a CI image that has the `claude` binary but should not spend\n * the time, or a sandbox where spawning it is undesirable. The gate falls back to\n * round-trip, and says so in `conformance` rather than pretending it validated.\n */\nconst ENV_SKIP_VENDOR_VALIDATE = \"HOOCODE_PLUGIN_SKIP_VENDOR_VALIDATE\";\n\nfunction vendorValidatorDisabled(): boolean {\n\tconst v = process.env[ENV_SKIP_VENDOR_VALIDATE];\n\treturn v === \"1\" || v === \"true\";\n}\n\nconst MAX_ID_LENGTH = 64;\nconst VALID_ID = /^[a-z0-9][a-z0-9-]*$/;\n\n/** Files worth scanning for portability and secret problems. Binary content is skipped. */\nconst TEXT_EXTENSIONS = new Set([\".md\", \".json\", \".txt\", \".yaml\", \".yml\", \".sh\", \".js\", \".ts\", \".toml\"]);\n\n/**\n * Absolute paths that only exist on the author's machine. A plugin carrying one\n * works exactly once — for whoever wrote it — which is the failure mode\n * portability guidance was meant to prevent and could not, being only advice.\n */\nconst MACHINE_PATHS = [/\\/home\\/[a-z0-9._-]+\\//i, /\\/Users\\/[a-z0-9._-]+\\//i, /\\b[A-Z]:\\\\Users\\\\/];\n\n/** Credential shapes worth refusing outright rather than shipping to a marketplace. */\nconst SECRET_PATTERNS: ReadonlyArray<{ re: RegExp; what: string }> = [\n\t{ re: /\\bsk-[A-Za-z0-9]{16,}/, what: \"an API key (sk-…)\" },\n\t{ re: /\\bghp_[A-Za-z0-9]{20,}/, what: \"a GitHub token (ghp_…)\" },\n\t{ re: /\\bAKIA[0-9A-Z]{12,}/, what: \"an AWS access key id\" },\n\t{ re: /\\bAWS_SECRET_ACCESS_KEY\\s*[=:]\\s*\\S+/, what: \"an AWS secret\" },\n\t{ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/, what: \"a private key\" },\n];\n\n/**\n * Shell constructs refused in a hook or MCP command. Not a sandbox and not a\n * general shell analysis — a determined command can evade any of these. It\n * catches the accident and the obvious, which is what a static gate can honestly\n * claim; G3 is where behavior gets checked.\n */\nconst DANGEROUS_COMMANDS: ReadonlyArray<{ re: RegExp; what: string }> = [\n\t{ re: /\\brm\\s+(-[a-zA-Z]*\\s+)*-[a-zA-Z]*[rR][a-zA-Z]*f|\\brm\\s+-fr\\b/, what: \"a recursive force delete\" },\n\t{ re: /\\b(curl|wget)\\b[^|]*\\|\\s*(ba)?sh\\b/, what: \"piping a download into a shell\" },\n\t{ re: /\\bmkfs(\\.[a-z0-9]+)?\\b|\\bdd\\s+[^|]*of=\\/dev\\//, what: \"a raw device write\" },\n\t{ re: />\\s*\\/dev\\/(sd|nvme|disk)/, what: \"a raw device write\" },\n\t{ re: /\\bchmod\\s+(-[a-zA-Z]+\\s+)*777\\b/, what: \"a world-writable chmod\" },\n\t{ re: /\\bsudo\\b/, what: \"a privilege escalation\" },\n];\n\nfunction walkFiles(root: string, out: string[] = []): string[] {\n\tlet entries: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tentries = readdirSync(root, { withFileTypes: true });\n\t} catch {\n\t\treturn out;\n\t}\n\tfor (const entry of entries) {\n\t\tconst full = path.join(root, entry.name);\n\t\tif (entry.isDirectory()) {\n\t\t\twalkFiles(full, out);\n\t\t} else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name))) {\n\t\t\tout.push(full);\n\t\t}\n\t}\n\treturn out;\n}\n\n/** First executable token of a shell command, ignoring `VAR=x` prefixes. */\nexport function commandBinary(command: string): string | undefined {\n\tfor (const token of command.trim().split(/\\s+/)) {\n\t\tif (!token || /^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) continue;\n\t\treturn token;\n\t}\n\treturn undefined;\n}\n\n/** Whether `binary` resolves — on PATH, or as a path relative to the plugin. */\nfunction binaryResolves(binary: string, root: string): boolean {\n\t// A template variable is resolved at run time, so it cannot be checked here.\n\tif (binary.includes(\"${\")) return true;\n\tif (binary.includes(\"/\") || binary.includes(\"\\\\\")) {\n\t\treturn existsSync(path.isAbsolute(binary) ? binary : path.resolve(root, binary));\n\t}\n\t// Shell builtins never appear on PATH.\n\tif ([\"echo\", \"cd\", \"true\", \"false\", \"test\", \"set\", \"export\", \":\"].includes(binary)) return true;\n\ttry {\n\t\texecFileSync(process.platform === \"win32\" ? \"where\" : \"which\", [binary], {\n\t\t\tstdio: \"ignore\",\n\t\t\ttimeout: 5_000,\n\t\t});\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** G1: parses, legal id, portable content, and accepted by the target platform. */\nfunction structural(\n\tdir: string,\n\topts: GateOptions,\n): { findings: GateFinding[]; conformance: GateResult[\"conformance\"]; plugin: NormalizedPlugin | null } {\n\tconst findings: GateFinding[] = [];\n\tconst err = (message: string) => findings.push({ gate: \"G1\", severity: \"error\", message });\n\tconst warn = (message: string) => findings.push({ gate: \"G1\", severity: \"warning\", message });\n\n\tconst plugin = parsePluginDir(dir);\n\tif (!plugin) {\n\t\terr(`No recognizable plugin manifest at ${dir}.`);\n\t\treturn { findings, conformance: \"round-trip\", plugin: null };\n\t}\n\n\tif (plugin.id.length > MAX_ID_LENGTH) err(`Plugin id \"${plugin.id}\" exceeds ${MAX_ID_LENGTH} characters.`);\n\tif (!VALID_ID.test(plugin.id)) {\n\t\terr(`Plugin id \"${plugin.id}\" must be lowercase letters, digits and hyphens, and start with one.`);\n\t}\n\n\tfor (const file of walkFiles(dir)) {\n\t\tlet content: string;\n\t\ttry {\n\t\t\tif (statSync(file).size > 512 * 1024) continue;\n\t\t\tcontent = readFileSync(file, \"utf8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst rel = path.relative(dir, file);\n\t\tfor (const re of MACHINE_PATHS) {\n\t\t\tconst hit = re.exec(content);\n\t\t\tif (hit) {\n\t\t\t\terr(`${rel} contains a machine-specific path (\"${hit[0]}\"); the plugin would only work for its author.`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tfor (const { re, what } of SECRET_PATTERNS) {\n\t\t\tif (re.test(content)) {\n\t\t\t\terr(`${rel} appears to contain ${what}.`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Vendor conformance where one exists. Round-trip alone proves only that our\n\t// emitter and our reader agree.\n\tlet conformance: GateResult[\"conformance\"] = \"round-trip\";\n\tif (opts.platform === \"claude\" && !opts.skipVendorValidator && !vendorValidatorDisabled()) {\n\t\tconst vendor = runClaudeValidate(dir, opts.strict === true);\n\t\tif (vendor) {\n\t\t\tconformance = \"claude-plugin-validate\";\n\t\t\tif (!vendor.ok) err(`claude plugin validate rejected the plugin:\\n${vendor.output}`);\n\t\t\telse if (vendor.output.trim()) warn(`claude plugin validate: ${vendor.output.trim()}`);\n\t\t}\n\t}\n\n\treturn { findings, conformance, plugin };\n}\n\n/** Run the vendor validator, or undefined when the CLI is not installed. */\nfunction runClaudeValidate(dir: string, strict: boolean): { ok: boolean; output: string } | undefined {\n\tconst args = [\"plugin\", \"validate\", dir, ...(strict ? [\"--strict\"] : [])];\n\ttry {\n\t\tconst output = execFileSync(\"claude\", args, { encoding: \"utf8\", timeout: 60_000, stdio: \"pipe\" });\n\t\treturn { ok: true, output };\n\t} catch (error) {\n\t\tconst e = error as { code?: string; status?: number; stdout?: string; stderr?: string };\n\t\t// No CLI on this machine, or it predates `plugin validate` — not a failure\n\t\t// of the plugin, so the gate falls back to round-trip rather than blocking.\n\t\tif (e.code === \"ENOENT\") return undefined;\n\t\tconst output = `${e.stdout ?? \"\"}${e.stderr ?? \"\"}`;\n\t\tif (/unknown command|unrecognized|not a valid/i.test(output)) return undefined;\n\t\treturn { ok: false, output };\n\t}\n}\n\n/** G2: the executable content can plausibly run and is not obviously dangerous. */\nfunction safety(plugin: NormalizedPlugin | null): GateFinding[] {\n\tconst findings: GateFinding[] = [];\n\tif (!plugin) return findings;\n\tconst err = (message: string) => findings.push({ gate: \"G2\", severity: \"error\", message });\n\tconst warn = (message: string) => findings.push({ gate: \"G2\", severity: \"warning\", message });\n\n\tconst checkCommand = (label: string, command: string) => {\n\t\tfor (const { re, what } of DANGEROUS_COMMANDS) {\n\t\t\tif (re.test(command)) err(`${label} performs ${what}: ${command}`);\n\t\t}\n\t\tconst binary = commandBinary(command);\n\t\tif (!binary) {\n\t\t\terr(`${label} has no runnable command.`);\n\t\t\treturn;\n\t\t}\n\t\tif (!binaryResolves(binary, plugin.root)) {\n\t\t\t// A warning, not an error: the binary may be installed later, or by the\n\t\t\t// plugin's own prerequisites. Refusing outright would block a plugin that\n\t\t\t// documents its dependency honestly.\n\t\t\twarn(`${label} runs \"${binary}\", which is not on PATH or in the plugin.`);\n\t\t}\n\t};\n\n\tfor (const [event, groups] of Object.entries(plugin.hooks ?? {})) {\n\t\tfor (const group of groups) {\n\t\t\tfor (const cmd of group.hooks) checkCommand(`hook ${event}`, cmd.command);\n\t\t}\n\t}\n\tfor (const [name, raw] of Object.entries(plugin.mcpServers ?? {})) {\n\t\tconst cfg = raw as { command?: unknown };\n\t\tif (typeof cfg.command === \"string\") checkCommand(`mcp server \"${name}\"`, cfg.command);\n\t}\n\n\t// Subagent grants are classified rather than judged: the confirm gate already\n\t// decides on them, and recording the classification is what lets the human see\n\t// *why* they were asked.\n\tfor (const agentFile of listAgentFiles(plugin)) {\n\t\tconst tools = readToolsFrontmatter(agentFile);\n\t\tif (tools === undefined) continue;\n\t\tconst cls = classifyAllowlist(tools);\n\t\tif (cls.pluginTools.length > 0) {\n\t\t\terr(\n\t\t\t\t`subagent \"${path.basename(agentFile)}\" grants capability-acquisition tools: ${cls.pluginTools.join(\", \")}`,\n\t\t\t);\n\t\t} else if (cls.risk === \"mutating\") {\n\t\t\twarn(`subagent \"${path.basename(agentFile)}\" holds a mutating grant (${cls.reason}).`);\n\t\t}\n\t}\n\n\treturn findings;\n}\n\nfunction listAgentFiles(plugin: NormalizedPlugin): string[] {\n\tif (!plugin.agentsDir || !existsSync(plugin.agentsDir)) return [];\n\ttry {\n\t\treturn readdirSync(plugin.agentsDir)\n\t\t\t.filter((f) => f.endsWith(\".md\"))\n\t\t\t.map((f) => path.join(plugin.agentsDir as string, f));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction readToolsFrontmatter(file: string): string | undefined {\n\ttry {\n\t\tconst match = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---/.exec(readFileSync(file, \"utf8\"));\n\t\treturn /^tools:\\s*(.+)$/m.exec(match?.[1] ?? \"\")?.[1]?.trim();\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Whether a set of findings fails the gate. `info` never does. */\nexport function findingsFail(findings: readonly GateFinding[], strict = false): boolean {\n\treturn findings.some((f) => f.severity === \"error\" || (strict && f.severity === \"warning\"));\n}\n\n/** Run G1 and G2 over a plugin directory. */\nexport function runStaticGates(dir: string, opts: GateOptions = {}): GateResult {\n\tconst { findings: g1, conformance, plugin } = structural(dir, opts);\n\tconst findings = [...g1, ...safety(plugin)];\n\treturn { ok: !findingsFail(findings, opts.strict === true), findings, conformance, plugin };\n}\n\n/** Fold additional findings (G3) into an existing result. */\nexport function withFindings(result: GateResult, extra: readonly GateFinding[], strict = false): GateResult {\n\tconst findings = [...result.findings, ...extra];\n\treturn { ...result, findings, ok: !findingsFail(findings, strict) };\n}\n\n/** Render findings for a tool result or a confirmation prompt. */\nexport function formatGateFindings(result: GateResult): string {\n\tif (result.findings.length === 0) {\n\t\treturn `Checks passed (${result.conformance}).`;\n\t}\n\tconst lines = result.findings.map((f) => ` [${f.gate}/${f.severity}] ${f.message}`);\n\treturn `${result.ok ? \"Checks passed with warnings\" : \"Checks failed\"} (${result.conformance}):\\n${lines.join(\"\\n\")}`;\n}\n"]}
|
|
@@ -20,5 +20,5 @@ import type { PluginHooksConfig } from "./manifest.js";
|
|
|
20
20
|
* Register all hook events for one plugin against the ExtensionAPI.
|
|
21
21
|
* `onError` reports non-blocking failures (kept off the model's path).
|
|
22
22
|
*/
|
|
23
|
-
export declare function installPluginHooks(pi: ExtensionAPI, hooks: PluginHooksConfig, root: string, onError: (message: string) => void): void;
|
|
23
|
+
export declare function installPluginHooks(pi: ExtensionAPI, hooks: PluginHooksConfig, root: string, vars: Record<string, string>, onError: (message: string) => void): void;
|
|
24
24
|
//# sourceMappingURL=hooks-bridge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAkC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAA6C,iBAAiB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"hooks-bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAkC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAA6C,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAwFlG;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAChC,IAAI,CAoGN","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(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: 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\tvars: Record<string, 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\tvars,\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\tvars,\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(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\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(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\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, vars);\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"]}
|
|
@@ -17,11 +17,15 @@
|
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
18
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
19
19
|
/** Run one shell hook command, piping `input` as JSON on stdin. */
|
|
20
|
-
function runHookCommand(cmd, input, root) {
|
|
20
|
+
function runHookCommand(cmd, input, root, vars) {
|
|
21
21
|
return new Promise((resolve) => {
|
|
22
22
|
const child = spawn(cmd.command, {
|
|
23
23
|
shell: true,
|
|
24
|
-
|
|
24
|
+
// Every vendor spelling, so a hook written for either agent resolves:
|
|
25
|
+
// the shell expands these, which is the hook-side equivalent of the
|
|
26
|
+
// string substitution MCP configs get.
|
|
27
|
+
env: { ...process.env, ...vars },
|
|
28
|
+
cwd: root,
|
|
25
29
|
});
|
|
26
30
|
let stdout = "";
|
|
27
31
|
let stderr = "";
|
|
@@ -86,14 +90,14 @@ function allCommands(groups) {
|
|
|
86
90
|
* Register all hook events for one plugin against the ExtensionAPI.
|
|
87
91
|
* `onError` reports non-blocking failures (kept off the model's path).
|
|
88
92
|
*/
|
|
89
|
-
export function installPluginHooks(pi, hooks, root, onError) {
|
|
93
|
+
export function installPluginHooks(pi, hooks, root, vars, onError) {
|
|
90
94
|
// ── PreToolUse → tool_call (blocking) ────────────────────────────────────
|
|
91
95
|
const preGroups = hooks.PreToolUse;
|
|
92
96
|
if (preGroups?.length) {
|
|
93
97
|
pi.on("tool_call", async (event) => {
|
|
94
98
|
const cmds = groupsForTool(preGroups, event.toolName);
|
|
95
99
|
for (const cmd of cmds) {
|
|
96
|
-
const res = await runHookCommand(cmd, { hook_event_name: "PreToolUse", tool_name: event.toolName, tool_input: event.input }, root);
|
|
100
|
+
const res = await runHookCommand(cmd, { hook_event_name: "PreToolUse", tool_name: event.toolName, tool_input: event.input }, root, vars);
|
|
97
101
|
const decision = res.json?.decision ?? res.json?.permissionDecision;
|
|
98
102
|
if (res.exitCode === 2 || decision === "block" || decision === "deny") {
|
|
99
103
|
return { block: true, reason: res.json?.reason || res.stderr.trim() || "Blocked by plugin hook" };
|
|
@@ -114,7 +118,7 @@ export function installPluginHooks(pi, hooks, root, onError) {
|
|
|
114
118
|
tool_name: event.toolName,
|
|
115
119
|
tool_input: event.input,
|
|
116
120
|
tool_response: event.content,
|
|
117
|
-
}, root);
|
|
121
|
+
}, root, vars);
|
|
118
122
|
if (res.exitCode === 2 || res.json?.decision === "block") {
|
|
119
123
|
const reason = res.json?.reason || res.stderr.trim() || "Flagged by plugin hook";
|
|
120
124
|
return {
|
|
@@ -133,7 +137,7 @@ export function installPluginHooks(pi, hooks, root, onError) {
|
|
|
133
137
|
pi.on("before_agent_start", async (event) => {
|
|
134
138
|
let systemPrompt = event.systemPrompt;
|
|
135
139
|
for (const cmd of allCommands(promptGroups)) {
|
|
136
|
-
const res = await runHookCommand(cmd, { hook_event_name: "UserPromptSubmit", prompt: event.prompt }, root);
|
|
140
|
+
const res = await runHookCommand(cmd, { hook_event_name: "UserPromptSubmit", prompt: event.prompt }, root, vars);
|
|
137
141
|
if (res.exitCode !== 0 && res.exitCode !== 2) {
|
|
138
142
|
onError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
139
143
|
continue;
|
|
@@ -150,7 +154,7 @@ export function installPluginHooks(pi, hooks, root, onError) {
|
|
|
150
154
|
if (sessionGroups?.length) {
|
|
151
155
|
pi.on("session_start", async (event) => {
|
|
152
156
|
for (const cmd of allCommands(sessionGroups)) {
|
|
153
|
-
const res = await runHookCommand(cmd, { hook_event_name: "SessionStart", source: event.reason }, root);
|
|
157
|
+
const res = await runHookCommand(cmd, { hook_event_name: "SessionStart", source: event.reason }, root, vars);
|
|
154
158
|
if (res.exitCode !== 0)
|
|
155
159
|
onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
156
160
|
}
|
|
@@ -161,7 +165,7 @@ export function installPluginHooks(pi, hooks, root, onError) {
|
|
|
161
165
|
if (stopGroups?.length) {
|
|
162
166
|
pi.on("agent_end", async () => {
|
|
163
167
|
for (const cmd of allCommands(stopGroups)) {
|
|
164
|
-
const res = await runHookCommand(cmd, { hook_event_name: "Stop" }, root);
|
|
168
|
+
const res = await runHookCommand(cmd, { hook_event_name: "Stop" }, root, vars);
|
|
165
169
|
if (res.exitCode !== 0)
|
|
166
170
|
onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);
|
|
167
171
|
}
|
|
@@ -1 +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"]}
|
|
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(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: 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\tvars: Record<string, 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\tvars,\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\tvars,\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(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\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(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\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, vars);\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"]}
|
|
@@ -17,13 +17,44 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ExtensionFactory } from "../types.js";
|
|
19
19
|
import { type NormalizedPlugin } from "./manifest.js";
|
|
20
|
-
|
|
21
|
-
export
|
|
20
|
+
/** Extension path a plugin is loaded under: `<plugin:my-tool>`. */
|
|
21
|
+
export declare function pluginExtensionPath(id: string): string;
|
|
22
|
+
/** Recover the plugin id from {@link pluginExtensionPath}, or undefined for a non-plugin extension. */
|
|
23
|
+
export declare function pluginIdFromExtensionPath(extensionPath: string): string | undefined;
|
|
22
24
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* The template variables a plugin may use, mapped to their values.
|
|
26
|
+
*
|
|
27
|
+
* All vendor spellings are honored rather than only the ones hoocode invented.
|
|
28
|
+
* A plugin does not know which agent is loading it: a Copilot plugin writes
|
|
29
|
+
* `${PLUGIN_ROOT}`, a Claude one `${CLAUDE_PLUGIN_ROOT}`, and either may use
|
|
30
|
+
* `${*_PLUGIN_DATA}` for runtime state. Supporting one spelling means the others
|
|
31
|
+
* reach the shell or the MCP client as literal text.
|
|
25
32
|
*/
|
|
33
|
+
export declare function pluginVariables(root: string, dataDir: string): Record<string, string>;
|
|
34
|
+
export type { NormalizedPlugin } from "./manifest.js";
|
|
35
|
+
export { parsePluginDir } from "./manifest.js";
|
|
26
36
|
export declare function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[];
|
|
37
|
+
export interface PluginFactoryOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Load only capabilities that cannot execute: skills, commands, subagents,
|
|
40
|
+
* themes. Hooks and MCP servers are skipped and reported.
|
|
41
|
+
*
|
|
42
|
+
* Set for **project-scoped** plugins — those discovered under the workspace,
|
|
43
|
+
* chiefly `<cwd>/.claude/skills/`. That content arrives with a cloned
|
|
44
|
+
* repository rather than from the user, and registering shell hooks or
|
|
45
|
+
* spawning MCP servers from it with no confirmation is a real escalation over
|
|
46
|
+
* reading skill text, which is all a project skills directory gets today.
|
|
47
|
+
*
|
|
48
|
+
* Claude Code gates the same content behind a workspace trust dialog and
|
|
49
|
+
* per-server MCP approval. hoocode has no trust mechanism at all, so it
|
|
50
|
+
* withholds the executable half instead of pretending to gate it. If a trust
|
|
51
|
+
* gate is ever added, this is the flag it replaces.
|
|
52
|
+
* See docs/plugin-system-architecture.md §5.9.
|
|
53
|
+
*/
|
|
54
|
+
passiveOnly?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Capabilities withheld from a passive-only plugin, for reporting. */
|
|
57
|
+
export declare function withheldCapabilities(plugin: NormalizedPlugin): string[];
|
|
27
58
|
/** Build a synthetic extension factory that wires one normalized plugin. */
|
|
28
|
-
export declare function buildPluginFactory(plugin: NormalizedPlugin): ExtensionFactory;
|
|
59
|
+
export declare function buildPluginFactory(plugin: NormalizedPlugin, options?: PluginFactoryOptions): ExtensionFactory;
|
|
29
60
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +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;AA0CtE,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\t// Remote servers ({ type: \"http\" | \"sse\", url }): pass through for the\n\t\t// MCP loader's HTTP transports.\n\t\tif (typeof cfg.url === \"string\" && typeof cfg.command !== \"string\") {\n\t\t\tout[name] = {\n\t\t\t\ttype: cfg.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t\turl: cfg.url,\n\t\t\t\theaders: cfg.headers\n\t\t\t\t\t? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteRoot(String(v), root)]))\n\t\t\t\t\t: undefined,\n\t\t\t\tbackground: cfg.background,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\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 the hoo-core mcp-loader 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"]}
|
|
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;AAGlE,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,eAAe,CAAC;AAEtE,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUrF;AA0CD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAe/C,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAwBxE;AAED,MAAM,WAAW,oBAAoB;IACpC;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAEvE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,gBAAgB,CA6C7G","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 { ensurePluginDataDir, pluginDataDir } from \"./locations.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Extension path a plugin is loaded under: `<plugin:my-tool>`. */\nexport function pluginExtensionPath(id: string): string {\n\treturn `<plugin:${id}>`;\n}\n\n/** Recover the plugin id from {@link pluginExtensionPath}, or undefined for a non-plugin extension. */\nexport function pluginIdFromExtensionPath(extensionPath: string): string | undefined {\n\tconst match = /^<plugin:(.+)>$/.exec(extensionPath);\n\treturn match?.[1];\n}\n\n/**\n * The template variables a plugin may use, mapped to their values.\n *\n * All vendor spellings are honored rather than only the ones hoocode invented.\n * A plugin does not know which agent is loading it: a Copilot plugin writes\n * `${PLUGIN_ROOT}`, a Claude one `${CLAUDE_PLUGIN_ROOT}`, and either may use\n * `${*_PLUGIN_DATA}` for runtime state. Supporting one spelling means the others\n * reach the shell or the MCP client as literal text.\n */\nexport function pluginVariables(root: string, dataDir: string): Record<string, string> {\n\treturn {\n\t\tPLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_ROOT: root,\n\t\tAGENTS_PLUGIN_ROOT: root,\n\t\tCOPILOT_PLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_DATA: dataDir,\n\t\tCOPILOT_PLUGIN_DATA: dataDir,\n\t\tAGENTS_PLUGIN_DATA: dataDir,\n\t};\n}\n\n/** Expand every `${VAR}` in {@link pluginVariables}. Unknown `${...}` is left alone. */\nfunction substituteVars(value: string, vars: Record<string, string>): string {\n\treturn value.replace(/\\$\\{([A-Z_]+)\\}/g, (whole, name: string) => vars[name] ?? whole);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\tvars: Record<string, 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\t// Remote servers ({ type: \"http\" | \"sse\", url }): pass through for the\n\t\t// MCP loader's HTTP transports.\n\t\tif (typeof cfg.url === \"string\" && typeof cfg.command !== \"string\") {\n\t\t\tout[name] = {\n\t\t\t\ttype: cfg.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t\turl: cfg.url,\n\t\t\t\theaders: cfg.headers\n\t\t\t\t\t? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteVars(String(v), vars)]))\n\t\t\t\t\t: undefined,\n\t\t\t\tbackground: cfg.background,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteVars(cfg.command, vars),\n\t\t\targs: cfg.args?.map((a) => substituteVars(String(a), vars)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteVars(String(v), vars)]))\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 */\n/**\n * A skills directory holds plain skills and plugins side by side, and the\n * manifest is the only thing separating them. Everywhere else the manifest is\n * optional and components in their default locations are enough.\n */\nfunction isSkillsDirectory(dir: string): boolean {\n\treturn path.basename(dir) === \"skills\";\n}\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\tconst requireManifest = isSkillsDirectory(dir);\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), { requireManifest });\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\nexport interface PluginFactoryOptions {\n\t/**\n\t * Load only capabilities that cannot execute: skills, commands, subagents,\n\t * themes. Hooks and MCP servers are skipped and reported.\n\t *\n\t * Set for **project-scoped** plugins — those discovered under the workspace,\n\t * chiefly `<cwd>/.claude/skills/`. That content arrives with a cloned\n\t * repository rather than from the user, and registering shell hooks or\n\t * spawning MCP servers from it with no confirmation is a real escalation over\n\t * reading skill text, which is all a project skills directory gets today.\n\t *\n\t * Claude Code gates the same content behind a workspace trust dialog and\n\t * per-server MCP approval. hoocode has no trust mechanism at all, so it\n\t * withholds the executable half instead of pretending to gate it. If a trust\n\t * gate is ever added, this is the flag it replaces.\n\t * See docs/plugin-system-architecture.md §5.9.\n\t */\n\tpassiveOnly?: boolean;\n}\n\n/** Capabilities withheld from a passive-only plugin, for reporting. */\nexport function withheldCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [plugin.hooks && \"hooks\", plugin.mcpServers && \"mcp servers\"].filter((x): x is string => !!x);\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin, options?: PluginFactoryOptions): ExtensionFactory {\n\tconst passiveOnly = options?.passiveOnly === true;\n\t// Only the executable capabilities can reference the data dir, so it is\n\t// created only when one of them is actually wired — no empty directory per\n\t// installed plugin.\n\tconst wiresExecutables = !passiveOnly && !!(plugin.hooks || plugin.mcpServers);\n\tconst vars = pluginVariables(\n\t\tplugin.root,\n\t\twiresExecutables ? ensurePluginDataDir(plugin.id) : pluginDataDir(plugin.id),\n\t);\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. Executable, so withheld from a\n\t\t// project-scoped plugin (see PluginFactoryOptions.passiveOnly).\n\t\tif (plugin.hooks && !passiveOnly) {\n\t\t\tinstallPluginHooks(pi, plugin.hooks, plugin.root, vars, () => {\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 the hoo-core mcp-loader to connect on session_start.\n\t\t// Executable, so withheld from a project-scoped plugin.\n\t\tif (plugin.mcpServers && !passiveOnly) {\n\t\t\tregisterExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, vars));\n\t\t}\n\t};\n\n\tfactory.displayName = `plugin:${plugin.id}`;\n\treturn factory;\n}\n"]}
|
|
@@ -19,13 +19,43 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { registerExtensionMcpServers } from "../../extension-mcp-servers.js";
|
|
21
21
|
import { installPluginHooks } from "./hooks-bridge.js";
|
|
22
|
+
import { ensurePluginDataDir, pluginDataDir } from "./locations.js";
|
|
22
23
|
import { parsePluginDir } from "./manifest.js";
|
|
23
|
-
/**
|
|
24
|
-
function
|
|
25
|
-
return
|
|
24
|
+
/** Extension path a plugin is loaded under: `<plugin:my-tool>`. */
|
|
25
|
+
export function pluginExtensionPath(id) {
|
|
26
|
+
return `<plugin:${id}>`;
|
|
27
|
+
}
|
|
28
|
+
/** Recover the plugin id from {@link pluginExtensionPath}, or undefined for a non-plugin extension. */
|
|
29
|
+
export function pluginIdFromExtensionPath(extensionPath) {
|
|
30
|
+
const match = /^<plugin:(.+)>$/.exec(extensionPath);
|
|
31
|
+
return match?.[1];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The template variables a plugin may use, mapped to their values.
|
|
35
|
+
*
|
|
36
|
+
* All vendor spellings are honored rather than only the ones hoocode invented.
|
|
37
|
+
* A plugin does not know which agent is loading it: a Copilot plugin writes
|
|
38
|
+
* `${PLUGIN_ROOT}`, a Claude one `${CLAUDE_PLUGIN_ROOT}`, and either may use
|
|
39
|
+
* `${*_PLUGIN_DATA}` for runtime state. Supporting one spelling means the others
|
|
40
|
+
* reach the shell or the MCP client as literal text.
|
|
41
|
+
*/
|
|
42
|
+
export function pluginVariables(root, dataDir) {
|
|
43
|
+
return {
|
|
44
|
+
PLUGIN_ROOT: root,
|
|
45
|
+
CLAUDE_PLUGIN_ROOT: root,
|
|
46
|
+
AGENTS_PLUGIN_ROOT: root,
|
|
47
|
+
COPILOT_PLUGIN_ROOT: root,
|
|
48
|
+
CLAUDE_PLUGIN_DATA: dataDir,
|
|
49
|
+
COPILOT_PLUGIN_DATA: dataDir,
|
|
50
|
+
AGENTS_PLUGIN_DATA: dataDir,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Expand every `${VAR}` in {@link pluginVariables}. Unknown `${...}` is left alone. */
|
|
54
|
+
function substituteVars(value, vars) {
|
|
55
|
+
return value.replace(/\$\{([A-Z_]+)\}/g, (whole, name) => vars[name] ?? whole);
|
|
26
56
|
}
|
|
27
57
|
/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */
|
|
28
|
-
function resolveMcpServers(mcpServers,
|
|
58
|
+
function resolveMcpServers(mcpServers, vars) {
|
|
29
59
|
const out = {};
|
|
30
60
|
for (const [name, raw] of Object.entries(mcpServers)) {
|
|
31
61
|
if (!raw || typeof raw !== "object")
|
|
@@ -38,7 +68,7 @@ function resolveMcpServers(mcpServers, root) {
|
|
|
38
68
|
type: cfg.type === "sse" ? "sse" : "http",
|
|
39
69
|
url: cfg.url,
|
|
40
70
|
headers: cfg.headers
|
|
41
|
-
? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k,
|
|
71
|
+
? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteVars(String(v), vars)]))
|
|
42
72
|
: undefined,
|
|
43
73
|
background: cfg.background,
|
|
44
74
|
};
|
|
@@ -47,10 +77,10 @@ function resolveMcpServers(mcpServers, root) {
|
|
|
47
77
|
if (typeof cfg.command !== "string")
|
|
48
78
|
continue;
|
|
49
79
|
out[name] = {
|
|
50
|
-
command:
|
|
51
|
-
args: cfg.args?.map((a) =>
|
|
80
|
+
command: substituteVars(cfg.command, vars),
|
|
81
|
+
args: cfg.args?.map((a) => substituteVars(String(a), vars)),
|
|
52
82
|
env: cfg.env
|
|
53
|
-
? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k,
|
|
83
|
+
? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteVars(String(v), vars)]))
|
|
54
84
|
: undefined,
|
|
55
85
|
background: cfg.background,
|
|
56
86
|
};
|
|
@@ -62,12 +92,21 @@ export { parsePluginDir } from "./manifest.js";
|
|
|
62
92
|
* Discover plugins across the given `plugins/` directories.
|
|
63
93
|
* First-wins on duplicate ids (project dirs should be listed before global).
|
|
64
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* A skills directory holds plain skills and plugins side by side, and the
|
|
97
|
+
* manifest is the only thing separating them. Everywhere else the manifest is
|
|
98
|
+
* optional and components in their default locations are enough.
|
|
99
|
+
*/
|
|
100
|
+
function isSkillsDirectory(dir) {
|
|
101
|
+
return path.basename(dir) === "skills";
|
|
102
|
+
}
|
|
65
103
|
export function discoverPlugins(pluginDirs) {
|
|
66
104
|
const plugins = [];
|
|
67
105
|
const seen = new Set();
|
|
68
106
|
for (const dir of pluginDirs) {
|
|
69
107
|
if (!fs.existsSync(dir))
|
|
70
108
|
continue;
|
|
109
|
+
const requireManifest = isSkillsDirectory(dir);
|
|
71
110
|
let entries;
|
|
72
111
|
try {
|
|
73
112
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -78,7 +117,7 @@ export function discoverPlugins(pluginDirs) {
|
|
|
78
117
|
for (const entry of entries) {
|
|
79
118
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
80
119
|
continue;
|
|
81
|
-
const plugin = parsePluginDir(path.join(dir, entry.name));
|
|
120
|
+
const plugin = parsePluginDir(path.join(dir, entry.name), { requireManifest });
|
|
82
121
|
if (plugin && !seen.has(plugin.id)) {
|
|
83
122
|
seen.add(plugin.id);
|
|
84
123
|
plugins.push(plugin);
|
|
@@ -87,8 +126,18 @@ export function discoverPlugins(pluginDirs) {
|
|
|
87
126
|
}
|
|
88
127
|
return plugins;
|
|
89
128
|
}
|
|
129
|
+
/** Capabilities withheld from a passive-only plugin, for reporting. */
|
|
130
|
+
export function withheldCapabilities(plugin) {
|
|
131
|
+
return [plugin.hooks && "hooks", plugin.mcpServers && "mcp servers"].filter((x) => !!x);
|
|
132
|
+
}
|
|
90
133
|
/** Build a synthetic extension factory that wires one normalized plugin. */
|
|
91
|
-
export function buildPluginFactory(plugin) {
|
|
134
|
+
export function buildPluginFactory(plugin, options) {
|
|
135
|
+
const passiveOnly = options?.passiveOnly === true;
|
|
136
|
+
// Only the executable capabilities can reference the data dir, so it is
|
|
137
|
+
// created only when one of them is actually wired — no empty directory per
|
|
138
|
+
// installed plugin.
|
|
139
|
+
const wiresExecutables = !passiveOnly && !!(plugin.hooks || plugin.mcpServers);
|
|
140
|
+
const vars = pluginVariables(plugin.root, wiresExecutables ? ensurePluginDataDir(plugin.id) : pluginDataDir(plugin.id));
|
|
92
141
|
const factory = (pi) => {
|
|
93
142
|
// Resources: contribute the plugin's capability directories. Commands map to
|
|
94
143
|
// the slash-command surface (`.agents/commands`) and agents to subagent
|
|
@@ -105,15 +154,17 @@ export function buildPluginFactory(plugin) {
|
|
|
105
154
|
for (const provider of plugin.providers ?? []) {
|
|
106
155
|
pi.registerProvider(provider.name, provider.config);
|
|
107
156
|
}
|
|
108
|
-
// Hooks: true-parity shell bridge.
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
// Hooks: true-parity shell bridge. Executable, so withheld from a
|
|
158
|
+
// project-scoped plugin (see PluginFactoryOptions.passiveOnly).
|
|
159
|
+
if (plugin.hooks && !passiveOnly) {
|
|
160
|
+
installPluginHooks(pi, plugin.hooks, plugin.root, vars, () => {
|
|
111
161
|
// Non-blocking hook failures are intentionally quiet (Claude Code parity).
|
|
112
162
|
});
|
|
113
163
|
}
|
|
114
164
|
// MCP servers: register for the hoo-core mcp-loader to connect on session_start.
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
// Executable, so withheld from a project-scoped plugin.
|
|
166
|
+
if (plugin.mcpServers && !passiveOnly) {
|
|
167
|
+
registerExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, vars));
|
|
117
168
|
}
|
|
118
169
|
};
|
|
119
170
|
factory.displayName = `plugin:${plugin.id}`;
|
|
@@ -1 +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\t// Remote servers ({ type: \"http\" | \"sse\", url }): pass through for the\n\t\t// MCP loader's HTTP transports.\n\t\tif (typeof cfg.url === \"string\" && typeof cfg.command !== \"string\") {\n\t\t\tout[name] = {\n\t\t\t\ttype: cfg.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t\turl: cfg.url,\n\t\t\t\theaders: cfg.headers\n\t\t\t\t\t? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteRoot(String(v), root)]))\n\t\t\t\t\t: undefined,\n\t\t\t\tbackground: cfg.background,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\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 the hoo-core mcp-loader 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"]}
|
|
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 { ensurePluginDataDir, pluginDataDir } from \"./locations.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Extension path a plugin is loaded under: `<plugin:my-tool>`. */\nexport function pluginExtensionPath(id: string): string {\n\treturn `<plugin:${id}>`;\n}\n\n/** Recover the plugin id from {@link pluginExtensionPath}, or undefined for a non-plugin extension. */\nexport function pluginIdFromExtensionPath(extensionPath: string): string | undefined {\n\tconst match = /^<plugin:(.+)>$/.exec(extensionPath);\n\treturn match?.[1];\n}\n\n/**\n * The template variables a plugin may use, mapped to their values.\n *\n * All vendor spellings are honored rather than only the ones hoocode invented.\n * A plugin does not know which agent is loading it: a Copilot plugin writes\n * `${PLUGIN_ROOT}`, a Claude one `${CLAUDE_PLUGIN_ROOT}`, and either may use\n * `${*_PLUGIN_DATA}` for runtime state. Supporting one spelling means the others\n * reach the shell or the MCP client as literal text.\n */\nexport function pluginVariables(root: string, dataDir: string): Record<string, string> {\n\treturn {\n\t\tPLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_ROOT: root,\n\t\tAGENTS_PLUGIN_ROOT: root,\n\t\tCOPILOT_PLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_DATA: dataDir,\n\t\tCOPILOT_PLUGIN_DATA: dataDir,\n\t\tAGENTS_PLUGIN_DATA: dataDir,\n\t};\n}\n\n/** Expand every `${VAR}` in {@link pluginVariables}. Unknown `${...}` is left alone. */\nfunction substituteVars(value: string, vars: Record<string, string>): string {\n\treturn value.replace(/\\$\\{([A-Z_]+)\\}/g, (whole, name: string) => vars[name] ?? whole);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\tvars: Record<string, 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\t// Remote servers ({ type: \"http\" | \"sse\", url }): pass through for the\n\t\t// MCP loader's HTTP transports.\n\t\tif (typeof cfg.url === \"string\" && typeof cfg.command !== \"string\") {\n\t\t\tout[name] = {\n\t\t\t\ttype: cfg.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t\turl: cfg.url,\n\t\t\t\theaders: cfg.headers\n\t\t\t\t\t? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteVars(String(v), vars)]))\n\t\t\t\t\t: undefined,\n\t\t\t\tbackground: cfg.background,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteVars(cfg.command, vars),\n\t\t\targs: cfg.args?.map((a) => substituteVars(String(a), vars)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteVars(String(v), vars)]))\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 */\n/**\n * A skills directory holds plain skills and plugins side by side, and the\n * manifest is the only thing separating them. Everywhere else the manifest is\n * optional and components in their default locations are enough.\n */\nfunction isSkillsDirectory(dir: string): boolean {\n\treturn path.basename(dir) === \"skills\";\n}\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\tconst requireManifest = isSkillsDirectory(dir);\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), { requireManifest });\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\nexport interface PluginFactoryOptions {\n\t/**\n\t * Load only capabilities that cannot execute: skills, commands, subagents,\n\t * themes. Hooks and MCP servers are skipped and reported.\n\t *\n\t * Set for **project-scoped** plugins — those discovered under the workspace,\n\t * chiefly `<cwd>/.claude/skills/`. That content arrives with a cloned\n\t * repository rather than from the user, and registering shell hooks or\n\t * spawning MCP servers from it with no confirmation is a real escalation over\n\t * reading skill text, which is all a project skills directory gets today.\n\t *\n\t * Claude Code gates the same content behind a workspace trust dialog and\n\t * per-server MCP approval. hoocode has no trust mechanism at all, so it\n\t * withholds the executable half instead of pretending to gate it. If a trust\n\t * gate is ever added, this is the flag it replaces.\n\t * See docs/plugin-system-architecture.md §5.9.\n\t */\n\tpassiveOnly?: boolean;\n}\n\n/** Capabilities withheld from a passive-only plugin, for reporting. */\nexport function withheldCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [plugin.hooks && \"hooks\", plugin.mcpServers && \"mcp servers\"].filter((x): x is string => !!x);\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin, options?: PluginFactoryOptions): ExtensionFactory {\n\tconst passiveOnly = options?.passiveOnly === true;\n\t// Only the executable capabilities can reference the data dir, so it is\n\t// created only when one of them is actually wired — no empty directory per\n\t// installed plugin.\n\tconst wiresExecutables = !passiveOnly && !!(plugin.hooks || plugin.mcpServers);\n\tconst vars = pluginVariables(\n\t\tplugin.root,\n\t\twiresExecutables ? ensurePluginDataDir(plugin.id) : pluginDataDir(plugin.id),\n\t);\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. Executable, so withheld from a\n\t\t// project-scoped plugin (see PluginFactoryOptions.passiveOnly).\n\t\tif (plugin.hooks && !passiveOnly) {\n\t\t\tinstallPluginHooks(pi, plugin.hooks, plugin.root, vars, () => {\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 the hoo-core mcp-loader to connect on session_start.\n\t\t// Executable, so withheld from a project-scoped plugin.\n\t\tif (plugin.mcpServers && !passiveOnly) {\n\t\t\tregisterExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, vars));\n\t\t}\n\t};\n\n\tfactory.displayName = `plugin:${plugin.id}`;\n\treturn factory;\n}\n"]}
|