@kolisachint/hoocode-agent 0.4.118 → 0.4.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/dist/core/agent-session.d.ts +35 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +157 -1
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extension-mcp-servers.d.ts +8 -1
  7. package/dist/core/extension-mcp-servers.d.ts.map +1 -1
  8. package/dist/core/extension-mcp-servers.js.map +1 -1
  9. package/dist/core/extensions/index.d.ts +1 -1
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js.map +1 -1
  12. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  13. package/dist/core/extensions/plugins/authoring.js +5 -2
  14. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  15. package/dist/core/extensions/plugins/formats/copilot.d.ts +16 -14
  16. package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
  17. package/dist/core/extensions/plugins/formats/copilot.js +64 -46
  18. package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
  19. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
  20. package/dist/core/extensions/plugins/formats/jsonManifest.js +6 -6
  21. package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
  22. package/dist/core/extensions/plugins/formats/shared.d.ts +12 -1
  23. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  24. package/dist/core/extensions/plugins/formats/shared.js +16 -0
  25. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  26. package/dist/core/extensions/plugins/formats/types.d.ts +7 -2
  27. package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
  28. package/dist/core/extensions/plugins/formats/types.js.map +1 -1
  29. package/dist/core/extensions/plugins/index.d.ts.map +1 -1
  30. package/dist/core/extensions/plugins/index.js +13 -0
  31. package/dist/core/extensions/plugins/index.js.map +1 -1
  32. package/dist/core/extensions/plugins/install.d.ts +23 -1
  33. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  34. package/dist/core/extensions/plugins/install.js +63 -5
  35. package/dist/core/extensions/plugins/install.js.map +1 -1
  36. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
  37. package/dist/core/extensions/plugins/marketplace.js +22 -16
  38. package/dist/core/extensions/plugins/marketplace.js.map +1 -1
  39. package/dist/core/extensions/runner.d.ts +2 -0
  40. package/dist/core/extensions/runner.d.ts.map +1 -1
  41. package/dist/core/extensions/runner.js +15 -0
  42. package/dist/core/extensions/runner.js.map +1 -1
  43. package/dist/core/extensions/types.d.ts +33 -0
  44. package/dist/core/extensions/types.d.ts.map +1 -1
  45. package/dist/core/extensions/types.js.map +1 -1
  46. package/dist/core/settings-defaults.d.ts +1 -1
  47. package/dist/core/settings-defaults.d.ts.map +1 -1
  48. package/dist/core/settings-defaults.js +1 -1
  49. package/dist/core/settings-defaults.js.map +1 -1
  50. package/dist/core/settings-types.d.ts.map +1 -1
  51. package/dist/core/settings-types.js.map +1 -1
  52. package/dist/core/tools/plugins.d.ts.map +1 -1
  53. package/dist/core/tools/plugins.js +26 -5
  54. package/dist/core/tools/plugins.js.map +1 -1
  55. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  56. package/dist/core/tools/propose-plugin.js +10 -3
  57. package/dist/core/tools/propose-plugin.js.map +1 -1
  58. package/dist/extensions/core/mcp-loader.d.ts +11 -3
  59. package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
  60. package/dist/extensions/core/mcp-loader.js +103 -18
  61. package/dist/extensions/core/mcp-loader.js.map +1 -1
  62. package/dist/main.d.ts.map +1 -1
  63. package/dist/main.js +2 -1
  64. package/dist/main.js.map +1 -1
  65. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  66. package/dist/modes/interactive/interactive-mode.js +2 -0
  67. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  68. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  69. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  70. package/examples/extensions/sandbox/package.json +1 -1
  71. package/examples/extensions/with-deps/package.json +1 -1
  72. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"jsonManifest.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/jsonManifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIhG,UAAU,mBAAmB;IAC5B,EAAE,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACjD,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,mBAAmB,CAiGxF","sourcesContent":["/**\n * Factory for the `plugin.json`-style formats (native `.agents-plugin` and\n * Claude Code `.claude-plugin`). They share one on-disk layout — a JSON manifest\n * under a marker directory plus `skills/`, `commands/`, `agents/`, `themes/`,\n * `hooks/hooks.json` — and differ only in the marker directory, the format id,\n * and whether `providers` are honored (native-only). Both adapters are produced\n * from this factory so the shared reader/writer lives in one place.\n */\n\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin, PluginProvider } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tslug,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter, PluginFormatId } from \"./types.js\";\n\nconst MANIFEST_FILE = \"plugin.json\";\n\ninterface JsonManifestOptions {\n\tid: Extract<PluginFormatId, \"agents\" | \"claude\">;\n\t/** Marker subdirectory, e.g. \".agents-plugin\". */\n\tmanifestDir: string;\n\tprecedence: number;\n\tlabel: string;\n\t/** Native honors `providers`; the Claude-compat path ignores them. */\n\tsupportsProviders: boolean;\n}\n\nexport function createJsonManifestAdapter(opts: JsonManifestOptions): PluginFormatAdapter {\n\tconst manifestRelPath = path.join(opts.manifestDir, MANIFEST_FILE);\n\n\treturn {\n\t\tid: opts.id,\n\t\tplatform: opts.id, // \"agents\" | \"claude\" map 1:1 to their platform token\n\t\tprecedence: opts.precedence,\n\t\tlabel: opts.label,\n\t\tmarketplaceFile: path.join(opts.manifestDir, \"marketplace.json\"),\n\n\t\tdetectPlugin(root: string): boolean {\n\t\t\treturn readJson(path.join(root, manifestRelPath)) != null;\n\t\t},\n\n\t\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\t\tconst manifestPath = path.join(root, manifestRelPath);\n\t\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\t\tif (!raw) return null;\n\n\t\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\t\tif (!id) return null;\n\n\t\t\tconst providers: PluginProvider[] | undefined =\n\t\t\t\topts.supportsProviders && Array.isArray(raw.providers) ? (raw.providers as PluginProvider[]) : undefined;\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tversion: raw.version,\n\t\t\t\tdescription: raw.description,\n\t\t\t\tauthor: parseAuthor(raw.author),\n\t\t\t\troot,\n\t\t\t\tmanifestPath,\n\t\t\t\tformat: opts.id,\n\t\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\t\tsupportPlatform: [opts.id],\n\t\t\t\tskillsDir: dirIfExists(root, \"skills\"),\n\t\t\t\tcommandsDir: dirIfExists(root, \"commands\"),\n\t\t\t\tagentsDir: dirIfExists(root, \"agents\"),\n\t\t\t\tthemesDir: dirIfExists(root, \"themes\"),\n\t\t\t\thooks: normalizeHooks(raw.hooks, root),\n\t\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t\t\tproviders,\n\t\t\t};\n\t\t},\n\n\t\temit(draft: PluginDraft): EmittedFile[] {\n\t\t\tconst files: EmittedFile[] = [];\n\t\t\tconst mcpServers = draft.mcpServers?.length\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t: undefined;\n\n\t\t\tfiles.push({\n\t\t\t\tpath: manifestRelPath,\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tname: draft.id,\n\t\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t\t...(draft.author ? { author: draft.author } : {}),\n\t\t\t\t\t...(mcpServers ? { mcpServers } : {}),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tfor (const s of draft.skills ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const c of draft.commands ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const a of draft.agents ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t\t{ name: a.name, description: a.description, tools: a.tools, model: a.model },\n\t\t\t\t\t\ta.body,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (draft.hooks?.length) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn files;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"jsonManifest.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/jsonManifest.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIhG,UAAU,mBAAmB;IAC5B,EAAE,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACjD,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,mBAAmB,CAiGxF","sourcesContent":["/**\n * Factory for the `plugin.json`-style formats (native `.agents-plugin` and\n * Claude Code `.claude-plugin`). They share one on-disk layout — a JSON manifest\n * under a marker directory plus `skills/`, `commands/`, `agents/`, `themes/`,\n * `hooks/hooks.json` — and differ only in the marker directory, the format id,\n * and whether `providers` are honored (native-only). Both adapters are produced\n * from this factory so the shared reader/writer lives in one place.\n */\n\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin, PluginProvider } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter, PluginFormatId } from \"./types.js\";\n\nconst MANIFEST_FILE = \"plugin.json\";\n\ninterface JsonManifestOptions {\n\tid: Extract<PluginFormatId, \"agents\" | \"claude\">;\n\t/** Marker subdirectory, e.g. \".agents-plugin\". */\n\tmanifestDir: string;\n\tprecedence: number;\n\tlabel: string;\n\t/** Native honors `providers`; the Claude-compat path ignores them. */\n\tsupportsProviders: boolean;\n}\n\nexport function createJsonManifestAdapter(opts: JsonManifestOptions): PluginFormatAdapter {\n\tconst manifestRelPath = path.join(opts.manifestDir, MANIFEST_FILE);\n\n\treturn {\n\t\tid: opts.id,\n\t\tplatform: opts.id, // \"agents\" | \"claude\" map 1:1 to their platform token\n\t\tprecedence: opts.precedence,\n\t\tlabel: opts.label,\n\t\tmarketplaceFiles: [path.join(opts.manifestDir, \"marketplace.json\")],\n\n\t\tdetectPlugin(root: string): boolean {\n\t\t\treturn readJson(path.join(root, manifestRelPath)) != null;\n\t\t},\n\n\t\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\t\tconst manifestPath = path.join(root, manifestRelPath);\n\t\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\t\tif (!raw) return null;\n\n\t\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\t\tif (!id) return null;\n\n\t\t\tconst providers: PluginProvider[] | undefined =\n\t\t\t\topts.supportsProviders && Array.isArray(raw.providers) ? (raw.providers as PluginProvider[]) : undefined;\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tversion: raw.version,\n\t\t\t\tdescription: raw.description,\n\t\t\t\tauthor: parseAuthor(raw.author),\n\t\t\t\troot,\n\t\t\t\tmanifestPath,\n\t\t\t\tformat: opts.id,\n\t\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\t\tsupportPlatform: [opts.id],\n\t\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\"),\n\t\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\"),\n\t\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\t\thooks: normalizeHooks(raw.hooks, root),\n\t\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t\t\tproviders,\n\t\t\t};\n\t\t},\n\n\t\temit(draft: PluginDraft): EmittedFile[] {\n\t\t\tconst files: EmittedFile[] = [];\n\t\t\tconst mcpServers = draft.mcpServers?.length\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t: undefined;\n\n\t\t\tfiles.push({\n\t\t\t\tpath: manifestRelPath,\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tname: draft.id,\n\t\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t\t...(draft.author ? { author: draft.author } : {}),\n\t\t\t\t\t...(mcpServers ? { mcpServers } : {}),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tfor (const s of draft.skills ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const c of draft.commands ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const a of draft.agents ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t\t{ name: a.name, description: a.description, tools: a.tools, model: a.model },\n\t\t\t\t\t\ta.body,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (draft.hooks?.length) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn files;\n\t\t},\n\t};\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  * from this factory so the shared reader/writer lives in one place.
8
8
  */
9
9
  import * as path from "node:path";
10
- import { authoredHooksToConfig, dirIfExists, emitJson, emitMarkdown, normalizeHooks, normalizeMcp, parseAuthor, readJson, slug, } from "./shared.js";
10
+ import { authoredHooksToConfig, emitJson, emitMarkdown, normalizeHooks, normalizeMcp, parseAuthor, readJson, resolveCapabilityDir, slug, } from "./shared.js";
11
11
  const MANIFEST_FILE = "plugin.json";
12
12
  export function createJsonManifestAdapter(opts) {
13
13
  const manifestRelPath = path.join(opts.manifestDir, MANIFEST_FILE);
@@ -16,7 +16,7 @@ export function createJsonManifestAdapter(opts) {
16
16
  platform: opts.id, // "agents" | "claude" map 1:1 to their platform token
17
17
  precedence: opts.precedence,
18
18
  label: opts.label,
19
- marketplaceFile: path.join(opts.manifestDir, "marketplace.json"),
19
+ marketplaceFiles: [path.join(opts.manifestDir, "marketplace.json")],
20
20
  detectPlugin(root) {
21
21
  return readJson(path.join(root, manifestRelPath)) != null;
22
22
  },
@@ -39,10 +39,10 @@ export function createJsonManifestAdapter(opts) {
39
39
  format: opts.id,
40
40
  // Single-format view; the registry widens this to every format present.
41
41
  supportPlatform: [opts.id],
42
- skillsDir: dirIfExists(root, "skills"),
43
- commandsDir: dirIfExists(root, "commands"),
44
- agentsDir: dirIfExists(root, "agents"),
45
- themesDir: dirIfExists(root, "themes"),
42
+ skillsDir: resolveCapabilityDir(root, raw.skills, "skills"),
43
+ commandsDir: resolveCapabilityDir(root, raw.commands, "commands"),
44
+ agentsDir: resolveCapabilityDir(root, raw.agents, "agents"),
45
+ themesDir: resolveCapabilityDir(root, raw.themes, "themes"),
46
46
  hooks: normalizeHooks(raw.hooks, root),
47
47
  mcpServers: normalizeMcp(raw.mcpServers, root),
48
48
  providers,
@@ -1 +1 @@
1
- {"version":3,"file":"jsonManifest.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/jsonManifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Factory for the `plugin.json`-style formats (native `.agents-plugin` and\n * Claude Code `.claude-plugin`). They share one on-disk layout — a JSON manifest\n * under a marker directory plus `skills/`, `commands/`, `agents/`, `themes/`,\n * `hooks/hooks.json` — and differ only in the marker directory, the format id,\n * and whether `providers` are honored (native-only). Both adapters are produced\n * from this factory so the shared reader/writer lives in one place.\n */\n\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin, PluginProvider } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tslug,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter, PluginFormatId } from \"./types.js\";\n\nconst MANIFEST_FILE = \"plugin.json\";\n\ninterface JsonManifestOptions {\n\tid: Extract<PluginFormatId, \"agents\" | \"claude\">;\n\t/** Marker subdirectory, e.g. \".agents-plugin\". */\n\tmanifestDir: string;\n\tprecedence: number;\n\tlabel: string;\n\t/** Native honors `providers`; the Claude-compat path ignores them. */\n\tsupportsProviders: boolean;\n}\n\nexport function createJsonManifestAdapter(opts: JsonManifestOptions): PluginFormatAdapter {\n\tconst manifestRelPath = path.join(opts.manifestDir, MANIFEST_FILE);\n\n\treturn {\n\t\tid: opts.id,\n\t\tplatform: opts.id, // \"agents\" | \"claude\" map 1:1 to their platform token\n\t\tprecedence: opts.precedence,\n\t\tlabel: opts.label,\n\t\tmarketplaceFile: path.join(opts.manifestDir, \"marketplace.json\"),\n\n\t\tdetectPlugin(root: string): boolean {\n\t\t\treturn readJson(path.join(root, manifestRelPath)) != null;\n\t\t},\n\n\t\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\t\tconst manifestPath = path.join(root, manifestRelPath);\n\t\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\t\tif (!raw) return null;\n\n\t\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\t\tif (!id) return null;\n\n\t\t\tconst providers: PluginProvider[] | undefined =\n\t\t\t\topts.supportsProviders && Array.isArray(raw.providers) ? (raw.providers as PluginProvider[]) : undefined;\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tversion: raw.version,\n\t\t\t\tdescription: raw.description,\n\t\t\t\tauthor: parseAuthor(raw.author),\n\t\t\t\troot,\n\t\t\t\tmanifestPath,\n\t\t\t\tformat: opts.id,\n\t\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\t\tsupportPlatform: [opts.id],\n\t\t\t\tskillsDir: dirIfExists(root, \"skills\"),\n\t\t\t\tcommandsDir: dirIfExists(root, \"commands\"),\n\t\t\t\tagentsDir: dirIfExists(root, \"agents\"),\n\t\t\t\tthemesDir: dirIfExists(root, \"themes\"),\n\t\t\t\thooks: normalizeHooks(raw.hooks, root),\n\t\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t\t\tproviders,\n\t\t\t};\n\t\t},\n\n\t\temit(draft: PluginDraft): EmittedFile[] {\n\t\t\tconst files: EmittedFile[] = [];\n\t\t\tconst mcpServers = draft.mcpServers?.length\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t: undefined;\n\n\t\t\tfiles.push({\n\t\t\t\tpath: manifestRelPath,\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tname: draft.id,\n\t\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t\t...(draft.author ? { author: draft.author } : {}),\n\t\t\t\t\t...(mcpServers ? { mcpServers } : {}),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tfor (const s of draft.skills ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const c of draft.commands ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const a of draft.agents ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t\t{ name: a.name, description: a.description, tools: a.tools, model: a.model },\n\t\t\t\t\t\ta.body,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (draft.hooks?.length) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn files;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"jsonManifest.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/jsonManifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Factory for the `plugin.json`-style formats (native `.agents-plugin` and\n * Claude Code `.claude-plugin`). They share one on-disk layout — a JSON manifest\n * under a marker directory plus `skills/`, `commands/`, `agents/`, `themes/`,\n * `hooks/hooks.json` — and differ only in the marker directory, the format id,\n * and whether `providers` are honored (native-only). Both adapters are produced\n * from this factory so the shared reader/writer lives in one place.\n */\n\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin, PluginProvider } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter, PluginFormatId } from \"./types.js\";\n\nconst MANIFEST_FILE = \"plugin.json\";\n\ninterface JsonManifestOptions {\n\tid: Extract<PluginFormatId, \"agents\" | \"claude\">;\n\t/** Marker subdirectory, e.g. \".agents-plugin\". */\n\tmanifestDir: string;\n\tprecedence: number;\n\tlabel: string;\n\t/** Native honors `providers`; the Claude-compat path ignores them. */\n\tsupportsProviders: boolean;\n}\n\nexport function createJsonManifestAdapter(opts: JsonManifestOptions): PluginFormatAdapter {\n\tconst manifestRelPath = path.join(opts.manifestDir, MANIFEST_FILE);\n\n\treturn {\n\t\tid: opts.id,\n\t\tplatform: opts.id, // \"agents\" | \"claude\" map 1:1 to their platform token\n\t\tprecedence: opts.precedence,\n\t\tlabel: opts.label,\n\t\tmarketplaceFiles: [path.join(opts.manifestDir, \"marketplace.json\")],\n\n\t\tdetectPlugin(root: string): boolean {\n\t\t\treturn readJson(path.join(root, manifestRelPath)) != null;\n\t\t},\n\n\t\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\t\tconst manifestPath = path.join(root, manifestRelPath);\n\t\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\t\tif (!raw) return null;\n\n\t\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\t\tif (!id) return null;\n\n\t\t\tconst providers: PluginProvider[] | undefined =\n\t\t\t\topts.supportsProviders && Array.isArray(raw.providers) ? (raw.providers as PluginProvider[]) : undefined;\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tversion: raw.version,\n\t\t\t\tdescription: raw.description,\n\t\t\t\tauthor: parseAuthor(raw.author),\n\t\t\t\troot,\n\t\t\t\tmanifestPath,\n\t\t\t\tformat: opts.id,\n\t\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\t\tsupportPlatform: [opts.id],\n\t\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\"),\n\t\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\"),\n\t\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\t\thooks: normalizeHooks(raw.hooks, root),\n\t\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root),\n\t\t\t\tproviders,\n\t\t\t};\n\t\t},\n\n\t\temit(draft: PluginDraft): EmittedFile[] {\n\t\t\tconst files: EmittedFile[] = [];\n\t\t\tconst mcpServers = draft.mcpServers?.length\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t: undefined;\n\n\t\t\tfiles.push({\n\t\t\t\tpath: manifestRelPath,\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tname: draft.id,\n\t\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t\t...(draft.author ? { author: draft.author } : {}),\n\t\t\t\t\t...(mcpServers ? { mcpServers } : {}),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tfor (const s of draft.skills ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const c of draft.commands ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const a of draft.agents ?? []) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t\t{ name: a.name, description: a.description, tools: a.tools, model: a.model },\n\t\t\t\t\t\ta.body,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (draft.hooks?.length) {\n\t\t\t\tfiles.push({\n\t\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn files;\n\t\t},\n\t};\n}\n"]}
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { PluginHooksConfig } from "../manifest.js";
10
10
  import type { AuthoredHook } from "./types.js";
11
- /** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude). */
11
+ /** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude/copilot). */
12
12
  export interface RawManifest {
13
13
  name?: string;
14
14
  version?: string;
@@ -16,6 +16,11 @@ export interface RawManifest {
16
16
  author?: string | {
17
17
  name?: string;
18
18
  };
19
+ /** Optional capability-dir override(s): a path (or list of paths) relative to the plugin root, e.g. `"skills": "./skills/"`. */
20
+ skills?: unknown;
21
+ commands?: unknown;
22
+ agents?: unknown;
23
+ themes?: unknown;
19
24
  hooks?: PluginHooksConfig | {
20
25
  hooks?: PluginHooksConfig;
21
26
  };
@@ -25,6 +30,12 @@ export interface RawManifest {
25
30
  }
26
31
  export declare function readJson<T>(file: string): T | null;
27
32
  export declare function dirIfExists(root: string, name: string): string | undefined;
33
+ /**
34
+ * Resolve a capability directory: an explicit manifest override — a path (or
35
+ * list of paths, first existing wins) relative to the plugin root, e.g.
36
+ * `"skills": "./skills/"` — beats the conventional directory.
37
+ */
38
+ export declare function resolveCapabilityDir(root: string, override: unknown, conventional: string): string | undefined;
28
39
  /** Coerce an author field (string or `{ name }`) to a plain string. */
29
40
  export declare function parseAuthor(author: RawManifest["author"]): string | undefined;
30
41
  /** Accept either `{ hooks: {...} }` or a bare event map, falling back to `hooks/hooks.json`. */
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,KAAK,CAAC,EAAE,iBAAiB,GAAG;QAAE,KAAK,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,mBAAmB;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAMlD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1E;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAmBrG;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACxC,IAAI,EAAE,MAAM,EACZ,WAAW,SAAc,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrC;AAMD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAUlF;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE/C;AAED,kGAAkG;AAClG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWpF;AAED,+EAA+E;AAC/E,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzC","sourcesContent":["/**\n * Shared, format-agnostic helpers used by the plugin format adapters.\n *\n * Kept deliberately free of any runtime dependency on `../manifest.js` (it only\n * imports *types* from there) so the adapter registry can be wired without a\n * module cycle: `manifest.ts → formats/index.ts → formats/<adapter>.ts →\n * shared.ts`, with the back-reference to manifest types erased at compile time.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { PluginHooksConfig } from \"../manifest.js\";\nimport type { AuthoredHook } from \"./types.js\";\n\n/** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude). */\nexport interface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: unknown;\n}\n\nexport function readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/** Coerce an author field (string or `{ name }`) to a plain string. */\nexport function parseAuthor(author: RawManifest[\"author\"]): string | undefined {\n\treturn typeof author === \"string\" ? author : author?.name;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map, falling back to `hooks/hooks.json`. */\nexport function normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\n/** Read inline `mcpServers`, falling back to a `.mcp.json` file at `root`. */\nexport function normalizeMcp(\n\traw: Record<string, unknown> | undefined,\n\troot: string,\n\tmcpFileName = \".mcp.json\",\n): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, mcpFileName);\n\tif (fs.existsSync(mcpFile)) {\n\t\t// VS Code / Copilot use `{ servers }`; Claude/native use `{ mcpServers }`. Accept both.\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown>; servers?: Record<string, unknown> }>(mcpFile);\n\t\tconst servers = fileRaw?.mcpServers ?? fileRaw?.servers;\n\t\tif (servers && Object.keys(servers).length > 0) return servers;\n\t}\n\treturn undefined;\n}\n\n// ============================================================================\n// Emit helpers (used by the writer / scaffolding half of each adapter)\n// ============================================================================\n\n/** Serialize a small set of frontmatter fields to YAML. Values are strings only. */\nexport function emitFrontmatter(fields: Record<string, string | undefined>): string {\n\tconst lines: string[] = [\"---\"];\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (value === undefined || value === \"\") continue;\n\t\t// Quote values that could be misparsed as YAML (contain a colon-space or start punctuation).\n\t\tconst needsQuote = /[:#]|^[\\s>|@`&*!%]/.test(value) || value.includes(\"\\n\");\n\t\tlines.push(`${key}: ${needsQuote ? JSON.stringify(value) : value}`);\n\t}\n\tlines.push(\"---\");\n\treturn lines.join(\"\\n\");\n}\n\n/** A markdown capability file: frontmatter block + body. */\nexport function emitMarkdown(fields: Record<string, string | undefined>, body: string): string {\n\treturn `${emitFrontmatter(fields)}\\n\\n${body.trimEnd()}\\n`;\n}\n\n/** Pretty-print a JSON manifest with a trailing newline (matches repo convention). */\nexport function emitJson(value: unknown): string {\n\treturn `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Convert authored hooks into the on-disk `hooks.json` event-map shape shared by all formats. */\nexport function authoredHooksToConfig(hooks: AuthoredHook[]): Record<string, unknown> {\n\tconst byEvent: Record<string, Array<{ matcher?: string; hooks: unknown[] }>> = {};\n\tfor (const h of hooks) {\n\t\tconst group = byEvent[h.event] ?? [];\n\t\tbyEvent[h.event] = group;\n\t\tgroup.push({\n\t\t\t...(h.matcher ? { matcher: h.matcher } : {}),\n\t\t\thooks: [{ type: \"command\", command: h.command, ...(h.timeout ? { timeout: h.timeout } : {}) }],\n\t\t});\n\t}\n\treturn byEvent;\n}\n\n/** Slug-safe filename component (no path separators, conservative charset). */\nexport function slug(name: string): string {\n\treturn name\n\t\t.trim()\n\t\t.replace(/[^a-zA-Z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 80);\n}\n"]}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,gIAAgI;IAChI,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,iBAAiB,GAAG;QAAE,KAAK,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,mBAAmB;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAMlD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ9G;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAmBrG;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACxC,IAAI,EAAE,MAAM,EACZ,WAAW,SAAc,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAUrC;AAMD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAUlF;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE/C;AAED,kGAAkG;AAClG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWpF;AAED,+EAA+E;AAC/E,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzC","sourcesContent":["/**\n * Shared, format-agnostic helpers used by the plugin format adapters.\n *\n * Kept deliberately free of any runtime dependency on `../manifest.js` (it only\n * imports *types* from there) so the adapter registry can be wired without a\n * module cycle: `manifest.ts → formats/index.ts → formats/<adapter>.ts →\n * shared.ts`, with the back-reference to manifest types erased at compile time.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { PluginHooksConfig } from \"../manifest.js\";\nimport type { AuthoredHook } from \"./types.js\";\n\n/** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude/copilot). */\nexport interface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\t/** Optional capability-dir override(s): a path (or list of paths) relative to the plugin root, e.g. `\"skills\": \"./skills/\"`. */\n\tskills?: unknown;\n\tcommands?: unknown;\n\tagents?: unknown;\n\tthemes?: unknown;\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: unknown;\n}\n\nexport function readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/**\n * Resolve a capability directory: an explicit manifest override — a path (or\n * list of paths, first existing wins) relative to the plugin root, e.g.\n * `\"skills\": \"./skills/\"` — beats the conventional directory.\n */\nexport function resolveCapabilityDir(root: string, override: unknown, conventional: string): string | undefined {\n\tconst candidates = Array.isArray(override) ? override : [override];\n\tfor (const c of candidates) {\n\t\tif (typeof c !== \"string\" || !c.trim()) continue;\n\t\tconst p = path.resolve(root, c.trim());\n\t\tif (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;\n\t}\n\treturn dirIfExists(root, conventional);\n}\n\n/** Coerce an author field (string or `{ name }`) to a plain string. */\nexport function parseAuthor(author: RawManifest[\"author\"]): string | undefined {\n\treturn typeof author === \"string\" ? author : author?.name;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map, falling back to `hooks/hooks.json`. */\nexport function normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\n/** Read inline `mcpServers`, falling back to a `.mcp.json` file at `root`. */\nexport function normalizeMcp(\n\traw: Record<string, unknown> | undefined,\n\troot: string,\n\tmcpFileName = \".mcp.json\",\n): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, mcpFileName);\n\tif (fs.existsSync(mcpFile)) {\n\t\t// VS Code / Copilot use `{ servers }`; Claude/native use `{ mcpServers }`. Accept both.\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown>; servers?: Record<string, unknown> }>(mcpFile);\n\t\tconst servers = fileRaw?.mcpServers ?? fileRaw?.servers;\n\t\tif (servers && Object.keys(servers).length > 0) return servers;\n\t}\n\treturn undefined;\n}\n\n// ============================================================================\n// Emit helpers (used by the writer / scaffolding half of each adapter)\n// ============================================================================\n\n/** Serialize a small set of frontmatter fields to YAML. Values are strings only. */\nexport function emitFrontmatter(fields: Record<string, string | undefined>): string {\n\tconst lines: string[] = [\"---\"];\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (value === undefined || value === \"\") continue;\n\t\t// Quote values that could be misparsed as YAML (contain a colon-space or start punctuation).\n\t\tconst needsQuote = /[:#]|^[\\s>|@`&*!%]/.test(value) || value.includes(\"\\n\");\n\t\tlines.push(`${key}: ${needsQuote ? JSON.stringify(value) : value}`);\n\t}\n\tlines.push(\"---\");\n\treturn lines.join(\"\\n\");\n}\n\n/** A markdown capability file: frontmatter block + body. */\nexport function emitMarkdown(fields: Record<string, string | undefined>, body: string): string {\n\treturn `${emitFrontmatter(fields)}\\n\\n${body.trimEnd()}\\n`;\n}\n\n/** Pretty-print a JSON manifest with a trailing newline (matches repo convention). */\nexport function emitJson(value: unknown): string {\n\treturn `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Convert authored hooks into the on-disk `hooks.json` event-map shape shared by all formats. */\nexport function authoredHooksToConfig(hooks: AuthoredHook[]): Record<string, unknown> {\n\tconst byEvent: Record<string, Array<{ matcher?: string; hooks: unknown[] }>> = {};\n\tfor (const h of hooks) {\n\t\tconst group = byEvent[h.event] ?? [];\n\t\tbyEvent[h.event] = group;\n\t\tgroup.push({\n\t\t\t...(h.matcher ? { matcher: h.matcher } : {}),\n\t\t\thooks: [{ type: \"command\", command: h.command, ...(h.timeout ? { timeout: h.timeout } : {}) }],\n\t\t});\n\t}\n\treturn byEvent;\n}\n\n/** Slug-safe filename component (no path separators, conservative charset). */\nexport function slug(name: string): string {\n\treturn name\n\t\t.trim()\n\t\t.replace(/[^a-zA-Z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 80);\n}\n"]}
@@ -20,6 +20,22 @@ export function dirIfExists(root, name) {
20
20
  const p = path.join(root, name);
21
21
  return fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;
22
22
  }
23
+ /**
24
+ * Resolve a capability directory: an explicit manifest override — a path (or
25
+ * list of paths, first existing wins) relative to the plugin root, e.g.
26
+ * `"skills": "./skills/"` — beats the conventional directory.
27
+ */
28
+ export function resolveCapabilityDir(root, override, conventional) {
29
+ const candidates = Array.isArray(override) ? override : [override];
30
+ for (const c of candidates) {
31
+ if (typeof c !== "string" || !c.trim())
32
+ continue;
33
+ const p = path.resolve(root, c.trim());
34
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory())
35
+ return p;
36
+ }
37
+ return dirIfExists(root, conventional);
38
+ }
23
39
  /** Coerce an author field (string or `{ name }`) to a plain string. */
24
40
  export function parseAuthor(author) {
25
41
  return typeof author === "string" ? author : author?.name;
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared, format-agnostic helpers used by the plugin format adapters.\n *\n * Kept deliberately free of any runtime dependency on `../manifest.js` (it only\n * imports *types* from there) so the adapter registry can be wired without a\n * module cycle: `manifest.ts → formats/index.ts → formats/<adapter>.ts →\n * shared.ts`, with the back-reference to manifest types erased at compile time.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { PluginHooksConfig } from \"../manifest.js\";\nimport type { AuthoredHook } from \"./types.js\";\n\n/** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude). */\nexport interface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: unknown;\n}\n\nexport function readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/** Coerce an author field (string or `{ name }`) to a plain string. */\nexport function parseAuthor(author: RawManifest[\"author\"]): string | undefined {\n\treturn typeof author === \"string\" ? author : author?.name;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map, falling back to `hooks/hooks.json`. */\nexport function normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\n/** Read inline `mcpServers`, falling back to a `.mcp.json` file at `root`. */\nexport function normalizeMcp(\n\traw: Record<string, unknown> | undefined,\n\troot: string,\n\tmcpFileName = \".mcp.json\",\n): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, mcpFileName);\n\tif (fs.existsSync(mcpFile)) {\n\t\t// VS Code / Copilot use `{ servers }`; Claude/native use `{ mcpServers }`. Accept both.\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown>; servers?: Record<string, unknown> }>(mcpFile);\n\t\tconst servers = fileRaw?.mcpServers ?? fileRaw?.servers;\n\t\tif (servers && Object.keys(servers).length > 0) return servers;\n\t}\n\treturn undefined;\n}\n\n// ============================================================================\n// Emit helpers (used by the writer / scaffolding half of each adapter)\n// ============================================================================\n\n/** Serialize a small set of frontmatter fields to YAML. Values are strings only. */\nexport function emitFrontmatter(fields: Record<string, string | undefined>): string {\n\tconst lines: string[] = [\"---\"];\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (value === undefined || value === \"\") continue;\n\t\t// Quote values that could be misparsed as YAML (contain a colon-space or start punctuation).\n\t\tconst needsQuote = /[:#]|^[\\s>|@`&*!%]/.test(value) || value.includes(\"\\n\");\n\t\tlines.push(`${key}: ${needsQuote ? JSON.stringify(value) : value}`);\n\t}\n\tlines.push(\"---\");\n\treturn lines.join(\"\\n\");\n}\n\n/** A markdown capability file: frontmatter block + body. */\nexport function emitMarkdown(fields: Record<string, string | undefined>, body: string): string {\n\treturn `${emitFrontmatter(fields)}\\n\\n${body.trimEnd()}\\n`;\n}\n\n/** Pretty-print a JSON manifest with a trailing newline (matches repo convention). */\nexport function emitJson(value: unknown): string {\n\treturn `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Convert authored hooks into the on-disk `hooks.json` event-map shape shared by all formats. */\nexport function authoredHooksToConfig(hooks: AuthoredHook[]): Record<string, unknown> {\n\tconst byEvent: Record<string, Array<{ matcher?: string; hooks: unknown[] }>> = {};\n\tfor (const h of hooks) {\n\t\tconst group = byEvent[h.event] ?? [];\n\t\tbyEvent[h.event] = group;\n\t\tgroup.push({\n\t\t\t...(h.matcher ? { matcher: h.matcher } : {}),\n\t\t\thooks: [{ type: \"command\", command: h.command, ...(h.timeout ? { timeout: h.timeout } : {}) }],\n\t\t});\n\t}\n\treturn byEvent;\n}\n\n/** Slug-safe filename component (no path separators, conservative charset). */\nexport function slug(name: string): string {\n\treturn name\n\t\t.trim()\n\t\t.replace(/[^a-zA-Z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 80);\n}\n"]}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAqBlC,MAAM,UAAU,QAAQ,CAAI,IAAY,EAAY;IACnD,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAM,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY,EAAsB;IAC3E,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACxE;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,QAAiB,EAAE,YAAoB,EAAsB;IAC/G,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACnE,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE;YAAE,SAAS;QACjD,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvC,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;YAAE,OAAO,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,CACvC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,MAA6B,EAAsB;IAC9E,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,CAC1D;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,GAAyB,EAAE,IAAY,EAAiC;IACtG,IAAI,MAAqC,CAAC;IAC1C,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,GAAG,OAAO,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,CAAC,KAA2B,CAAC,CAAC,CAAE,GAAyB,CAAC;IACtG,CAAC;IACD,uDAAuD;IACvD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;QACzD,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,QAAQ,CAAuB,SAAS,CAAC,CAAC;YAC1D,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC5C,MAAM;oBACL,OAAO,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK;wBAClC,CAAC,CAAE,OAAO,CAAC,KAA2B;wBACtC,CAAC,CAAE,OAA6B,CAAC;YACpC,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACrE;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAC3B,GAAwC,EACxC,IAAY,EACZ,WAAW,GAAG,WAAW,EACa;IACtC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC7C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,wFAAwF;QACxF,MAAM,OAAO,GAAG,QAAQ,CAA8E,OAAO,CAAC,CAAC;QAC/G,MAAM,OAAO,GAAG,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,OAAO,CAAC;QACxD,IAAI,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,+EAA+E;AAC/E,uEAAuE;AACvE,+EAA+E;AAE/E,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAAC,MAA0C,EAAU;IACnF,MAAM,KAAK,GAAa,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAClD,6FAA6F;QAC7F,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAED,4DAA4D;AAC5D,MAAM,UAAU,YAAY,CAAC,MAA0C,EAAE,IAAY,EAAU;IAC9F,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;AAAA,CAC3D;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,KAAc,EAAU;IAChD,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAAA,CAC7C;AAED,kGAAkG;AAClG,MAAM,UAAU,qBAAqB,CAAC,KAAqB,EAA2B;IACrF,MAAM,OAAO,GAAkE,EAAE,CAAC;IAClF,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACrC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC;YACV,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;SAC9F,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAED,+EAA+E;AAC/E,MAAM,UAAU,IAAI,CAAC,IAAY,EAAU;IAC1C,OAAO,IAAI;SACT,IAAI,EAAE;SACN,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC;SACjC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CACf","sourcesContent":["/**\n * Shared, format-agnostic helpers used by the plugin format adapters.\n *\n * Kept deliberately free of any runtime dependency on `../manifest.js` (it only\n * imports *types* from there) so the adapter registry can be wired without a\n * module cycle: `manifest.ts → formats/index.ts → formats/<adapter>.ts →\n * shared.ts`, with the back-reference to manifest types erased at compile time.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { PluginHooksConfig } from \"../manifest.js\";\nimport type { AuthoredHook } from \"./types.js\";\n\n/** Raw manifest shape accepted by the `plugin.json`-style formats (agents/claude/copilot). */\nexport interface RawManifest {\n\tname?: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string | { name?: string };\n\t/** Optional capability-dir override(s): a path (or list of paths) relative to the plugin root, e.g. `\"skills\": \"./skills/\"`. */\n\tskills?: unknown;\n\tcommands?: unknown;\n\tagents?: unknown;\n\tthemes?: unknown;\n\thooks?: PluginHooksConfig | { hooks?: PluginHooksConfig };\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only. */\n\tproviders?: unknown;\n}\n\nexport function readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function dirIfExists(root: string, name: string): string | undefined {\n\tconst p = path.join(root, name);\n\treturn fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;\n}\n\n/**\n * Resolve a capability directory: an explicit manifest override — a path (or\n * list of paths, first existing wins) relative to the plugin root, e.g.\n * `\"skills\": \"./skills/\"` — beats the conventional directory.\n */\nexport function resolveCapabilityDir(root: string, override: unknown, conventional: string): string | undefined {\n\tconst candidates = Array.isArray(override) ? override : [override];\n\tfor (const c of candidates) {\n\t\tif (typeof c !== \"string\" || !c.trim()) continue;\n\t\tconst p = path.resolve(root, c.trim());\n\t\tif (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;\n\t}\n\treturn dirIfExists(root, conventional);\n}\n\n/** Coerce an author field (string or `{ name }`) to a plain string. */\nexport function parseAuthor(author: RawManifest[\"author\"]): string | undefined {\n\treturn typeof author === \"string\" ? author : author?.name;\n}\n\n/** Accept either `{ hooks: {...} }` or a bare event map, falling back to `hooks/hooks.json`. */\nexport function normalizeHooks(raw: RawManifest[\"hooks\"], root: string): PluginHooksConfig | undefined {\n\tlet config: PluginHooksConfig | undefined;\n\tif (raw && typeof raw === \"object\") {\n\t\tconfig = \"hooks\" in raw && raw.hooks ? (raw.hooks as PluginHooksConfig) : (raw as PluginHooksConfig);\n\t}\n\t// Fall back to the conventional hooks/hooks.json file.\n\tif (!config) {\n\t\tconst hooksFile = path.join(root, \"hooks\", \"hooks.json\");\n\t\tif (fs.existsSync(hooksFile)) {\n\t\t\tconst fileRaw = readJson<RawManifest[\"hooks\"]>(hooksFile);\n\t\t\tif (fileRaw && typeof fileRaw === \"object\") {\n\t\t\t\tconfig =\n\t\t\t\t\t\"hooks\" in fileRaw && fileRaw.hooks\n\t\t\t\t\t\t? (fileRaw.hooks as PluginHooksConfig)\n\t\t\t\t\t\t: (fileRaw as PluginHooksConfig);\n\t\t\t}\n\t\t}\n\t}\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\n/** Read inline `mcpServers`, falling back to a `.mcp.json` file at `root`. */\nexport function normalizeMcp(\n\traw: Record<string, unknown> | undefined,\n\troot: string,\n\tmcpFileName = \".mcp.json\",\n): Record<string, unknown> | undefined {\n\tif (raw && Object.keys(raw).length > 0) return raw;\n\tconst mcpFile = path.join(root, mcpFileName);\n\tif (fs.existsSync(mcpFile)) {\n\t\t// VS Code / Copilot use `{ servers }`; Claude/native use `{ mcpServers }`. Accept both.\n\t\tconst fileRaw = readJson<{ mcpServers?: Record<string, unknown>; servers?: Record<string, unknown> }>(mcpFile);\n\t\tconst servers = fileRaw?.mcpServers ?? fileRaw?.servers;\n\t\tif (servers && Object.keys(servers).length > 0) return servers;\n\t}\n\treturn undefined;\n}\n\n// ============================================================================\n// Emit helpers (used by the writer / scaffolding half of each adapter)\n// ============================================================================\n\n/** Serialize a small set of frontmatter fields to YAML. Values are strings only. */\nexport function emitFrontmatter(fields: Record<string, string | undefined>): string {\n\tconst lines: string[] = [\"---\"];\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (value === undefined || value === \"\") continue;\n\t\t// Quote values that could be misparsed as YAML (contain a colon-space or start punctuation).\n\t\tconst needsQuote = /[:#]|^[\\s>|@`&*!%]/.test(value) || value.includes(\"\\n\");\n\t\tlines.push(`${key}: ${needsQuote ? JSON.stringify(value) : value}`);\n\t}\n\tlines.push(\"---\");\n\treturn lines.join(\"\\n\");\n}\n\n/** A markdown capability file: frontmatter block + body. */\nexport function emitMarkdown(fields: Record<string, string | undefined>, body: string): string {\n\treturn `${emitFrontmatter(fields)}\\n\\n${body.trimEnd()}\\n`;\n}\n\n/** Pretty-print a JSON manifest with a trailing newline (matches repo convention). */\nexport function emitJson(value: unknown): string {\n\treturn `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Convert authored hooks into the on-disk `hooks.json` event-map shape shared by all formats. */\nexport function authoredHooksToConfig(hooks: AuthoredHook[]): Record<string, unknown> {\n\tconst byEvent: Record<string, Array<{ matcher?: string; hooks: unknown[] }>> = {};\n\tfor (const h of hooks) {\n\t\tconst group = byEvent[h.event] ?? [];\n\t\tbyEvent[h.event] = group;\n\t\tgroup.push({\n\t\t\t...(h.matcher ? { matcher: h.matcher } : {}),\n\t\t\thooks: [{ type: \"command\", command: h.command, ...(h.timeout ? { timeout: h.timeout } : {}) }],\n\t\t});\n\t}\n\treturn byEvent;\n}\n\n/** Slug-safe filename component (no path separators, conservative charset). */\nexport function slug(name: string): string {\n\treturn name\n\t\t.trim()\n\t\t.replace(/[^a-zA-Z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 80);\n}\n"]}
@@ -106,8 +106,13 @@ export interface PluginFormatAdapter {
106
106
  readonly precedence: number;
107
107
  /** Short human label for messages and scaffolding summaries. */
108
108
  readonly label: string;
109
- /** Location of this format's marketplace index file, relative to a marketplace root (POSIX). */
110
- readonly marketplaceFile: string;
109
+ /**
110
+ * Candidate locations of this format's marketplace index file, relative to a
111
+ * marketplace root (POSIX), in precedence order. Formats with more than one
112
+ * real-world convention (Copilot uses both `.github/plugin/marketplace.json`
113
+ * and `.github/marketplace.json`) list every location they accept.
114
+ */
115
+ readonly marketplaceFiles: readonly string[];
111
116
  /** True if `root` carries this format's plugin manifest. */
112
117
  detectPlugin(root: string): boolean;
113
118
  /** Parse `root` as a plugin of this format. Returns null when unparseable. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin format adapter contract.\n *\n * A `PluginFormatAdapter` owns *everything* format-specific for one vendor\n * layout — how a plugin directory is detected and parsed (the reader), how a\n * marketplace index file is located, and how a plugin is scaffolded from a\n * normalized draft (the writer). The rest of the plugin subsystem is written\n * against {@link NormalizedPlugin} / {@link PluginDraft} and never branches on a\n * format again.\n *\n * This is the extension point the plugin system is designed around: Claude Code\n * and GitHub Copilot conventions evolve independently, so each lives in its own\n * adapter file ({@link ./claude.ts}, {@link ./copilot.ts}, {@link ./agents.ts}).\n * Supporting a new vendor — or tracking a change in an existing one — means\n * editing (or adding) exactly one adapter and registering it; no other module\n * changes.\n */\n\nimport type { NormalizedPlugin } from \"../manifest.js\";\n\n/** Internal format id — matches `NormalizedPlugin.format` / `NormalizedMarketplace.format`. */\nexport type PluginFormatId = \"agents\" | \"claude\" | \"copilot\";\n\n/**\n * Public platform token surfaced to users. The Copilot format lives under\n * `.github/`, so it is surfaced as `\"github\"` rather than the internal\n * `\"copilot\"`.\n */\nexport type MarketplacePlatform = \"agents\" | \"claude\" | \"github\";\n\n/** A file produced by a format's scaffolder. `path` is relative to the plugin root (POSIX). */\nexport interface EmittedFile {\n\tpath: string;\n\tcontent: string;\n}\n\n/** An authored skill: passive instructions loaded lazily (SKILL.md-style). */\nexport interface AuthoredSkill {\n\tname: string;\n\tdescription?: string;\n\t/** Instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored slash command: a passive prompt template. */\nexport interface AuthoredCommand {\n\tname: string;\n\tdescription?: string;\n\t/** Prompt template body (markdown). */\n\tbody: string;\n}\n\n/** An authored subagent definition. `tools` is a comma-separated allowlist (Claude Code convention). */\nexport interface AuthoredAgent {\n\tname: string;\n\tdescription?: string;\n\t/** Comma-separated `allowed-tools` string, e.g. \"read, grep, glob\". */\n\ttools?: string;\n\tmodel?: string;\n\t/** System-prompt / instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored shell hook wired to a tool event. Active code — gated on authoring. */\nexport interface AuthoredHook {\n\t/** Claude Code event name: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ... */\n\tevent: string;\n\t/** Regex matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\tcommand: string;\n\t/** Timeout in seconds. */\n\ttimeout?: number;\n}\n\n/** An authored MCP server. Active code — gated on authoring. */\nexport interface AuthoredMcpServer {\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\n\n/**\n * Format-agnostic authoring spec consumed by {@link PluginFormatAdapter.emit}.\n * `ProposePlugin` builds one of these and asks each requested adapter to render\n * it, so the same authored capability lands correctly in Claude Code and\n * Copilot layouts without the authoring code knowing either.\n */\nexport interface PluginDraft {\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/**\n\t * Which platform layouts to scaffold this draft into (e.g. `[\"claude\",\n\t * \"github\"]`). Consumed as the default target set by `emitForPlatforms`;\n\t * omit to render every registered format.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n\tskills?: AuthoredSkill[];\n\tcommands?: AuthoredCommand[];\n\tagents?: AuthoredAgent[];\n\thooks?: AuthoredHook[];\n\tmcpServers?: AuthoredMcpServer[];\n}\n\n/** Reader + writer for a single plugin format. */\nexport interface PluginFormatAdapter {\n\t/** Internal format id (stable; used in `NormalizedPlugin.format`). */\n\treadonly id: PluginFormatId;\n\t/** Public platform token (Copilot → `\"github\"`). */\n\treadonly platform: MarketplacePlatform;\n\t/**\n\t * Precedence when several formats coexist in one directory (lower wins).\n\t * Native `.agents-plugin` beats Claude beats Copilot.\n\t */\n\treadonly precedence: number;\n\t/** Short human label for messages and scaffolding summaries. */\n\treadonly label: string;\n\t/** Location of this format's marketplace index file, relative to a marketplace root (POSIX). */\n\treadonly marketplaceFile: string;\n\n\t/** True if `root` carries this format's plugin manifest. */\n\tdetectPlugin(root: string): boolean;\n\t/** Parse `root` as a plugin of this format. Returns null when unparseable. */\n\tparsePlugin(root: string): NormalizedPlugin | null;\n\t/** Render `draft` into this format's on-disk layout (paths relative to the plugin root). */\n\temit(draft: PluginDraft): EmittedFile[];\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin format adapter contract.\n *\n * A `PluginFormatAdapter` owns *everything* format-specific for one vendor\n * layout — how a plugin directory is detected and parsed (the reader), how a\n * marketplace index file is located, and how a plugin is scaffolded from a\n * normalized draft (the writer). The rest of the plugin subsystem is written\n * against {@link NormalizedPlugin} / {@link PluginDraft} and never branches on a\n * format again.\n *\n * This is the extension point the plugin system is designed around: Claude Code\n * and GitHub Copilot conventions evolve independently, so each lives in its own\n * adapter file ({@link ./claude.ts}, {@link ./copilot.ts}, {@link ./agents.ts}).\n * Supporting a new vendor — or tracking a change in an existing one — means\n * editing (or adding) exactly one adapter and registering it; no other module\n * changes.\n */\n\nimport type { NormalizedPlugin } from \"../manifest.js\";\n\n/** Internal format id — matches `NormalizedPlugin.format` / `NormalizedMarketplace.format`. */\nexport type PluginFormatId = \"agents\" | \"claude\" | \"copilot\";\n\n/**\n * Public platform token surfaced to users. The Copilot format lives under\n * `.github/`, so it is surfaced as `\"github\"` rather than the internal\n * `\"copilot\"`.\n */\nexport type MarketplacePlatform = \"agents\" | \"claude\" | \"github\";\n\n/** A file produced by a format's scaffolder. `path` is relative to the plugin root (POSIX). */\nexport interface EmittedFile {\n\tpath: string;\n\tcontent: string;\n}\n\n/** An authored skill: passive instructions loaded lazily (SKILL.md-style). */\nexport interface AuthoredSkill {\n\tname: string;\n\tdescription?: string;\n\t/** Instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored slash command: a passive prompt template. */\nexport interface AuthoredCommand {\n\tname: string;\n\tdescription?: string;\n\t/** Prompt template body (markdown). */\n\tbody: string;\n}\n\n/** An authored subagent definition. `tools` is a comma-separated allowlist (Claude Code convention). */\nexport interface AuthoredAgent {\n\tname: string;\n\tdescription?: string;\n\t/** Comma-separated `allowed-tools` string, e.g. \"read, grep, glob\". */\n\ttools?: string;\n\tmodel?: string;\n\t/** System-prompt / instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored shell hook wired to a tool event. Active code — gated on authoring. */\nexport interface AuthoredHook {\n\t/** Claude Code event name: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ... */\n\tevent: string;\n\t/** Regex matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\tcommand: string;\n\t/** Timeout in seconds. */\n\ttimeout?: number;\n}\n\n/** An authored MCP server. Active code — gated on authoring. */\nexport interface AuthoredMcpServer {\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\n\n/**\n * Format-agnostic authoring spec consumed by {@link PluginFormatAdapter.emit}.\n * `ProposePlugin` builds one of these and asks each requested adapter to render\n * it, so the same authored capability lands correctly in Claude Code and\n * Copilot layouts without the authoring code knowing either.\n */\nexport interface PluginDraft {\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/**\n\t * Which platform layouts to scaffold this draft into (e.g. `[\"claude\",\n\t * \"github\"]`). Consumed as the default target set by `emitForPlatforms`;\n\t * omit to render every registered format.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n\tskills?: AuthoredSkill[];\n\tcommands?: AuthoredCommand[];\n\tagents?: AuthoredAgent[];\n\thooks?: AuthoredHook[];\n\tmcpServers?: AuthoredMcpServer[];\n}\n\n/** Reader + writer for a single plugin format. */\nexport interface PluginFormatAdapter {\n\t/** Internal format id (stable; used in `NormalizedPlugin.format`). */\n\treadonly id: PluginFormatId;\n\t/** Public platform token (Copilot → `\"github\"`). */\n\treadonly platform: MarketplacePlatform;\n\t/**\n\t * Precedence when several formats coexist in one directory (lower wins).\n\t * Native `.agents-plugin` beats Claude beats Copilot.\n\t */\n\treadonly precedence: number;\n\t/** Short human label for messages and scaffolding summaries. */\n\treadonly label: string;\n\t/**\n\t * Candidate locations of this format's marketplace index file, relative to a\n\t * marketplace root (POSIX), in precedence order. Formats with more than one\n\t * real-world convention (Copilot uses both `.github/plugin/marketplace.json`\n\t * and `.github/marketplace.json`) list every location they accept.\n\t */\n\treadonly marketplaceFiles: readonly string[];\n\n\t/** True if `root` carries this format's plugin manifest. */\n\tdetectPlugin(root: string): boolean;\n\t/** Parse `root` as a plugin of this format. Returns null when unparseable. */\n\tparsePlugin(root: string): NormalizedPlugin | null;\n\t/** Render `draft` into this format's on-disk layout (paths relative to the plugin root). */\n\temit(draft: PluginDraft): EmittedFile[];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG","sourcesContent":["/**\n * Plugin format adapter contract.\n *\n * A `PluginFormatAdapter` owns *everything* format-specific for one vendor\n * layout — how a plugin directory is detected and parsed (the reader), how a\n * marketplace index file is located, and how a plugin is scaffolded from a\n * normalized draft (the writer). The rest of the plugin subsystem is written\n * against {@link NormalizedPlugin} / {@link PluginDraft} and never branches on a\n * format again.\n *\n * This is the extension point the plugin system is designed around: Claude Code\n * and GitHub Copilot conventions evolve independently, so each lives in its own\n * adapter file ({@link ./claude.ts}, {@link ./copilot.ts}, {@link ./agents.ts}).\n * Supporting a new vendor — or tracking a change in an existing one — means\n * editing (or adding) exactly one adapter and registering it; no other module\n * changes.\n */\n\nimport type { NormalizedPlugin } from \"../manifest.js\";\n\n/** Internal format id — matches `NormalizedPlugin.format` / `NormalizedMarketplace.format`. */\nexport type PluginFormatId = \"agents\" | \"claude\" | \"copilot\";\n\n/**\n * Public platform token surfaced to users. The Copilot format lives under\n * `.github/`, so it is surfaced as `\"github\"` rather than the internal\n * `\"copilot\"`.\n */\nexport type MarketplacePlatform = \"agents\" | \"claude\" | \"github\";\n\n/** A file produced by a format's scaffolder. `path` is relative to the plugin root (POSIX). */\nexport interface EmittedFile {\n\tpath: string;\n\tcontent: string;\n}\n\n/** An authored skill: passive instructions loaded lazily (SKILL.md-style). */\nexport interface AuthoredSkill {\n\tname: string;\n\tdescription?: string;\n\t/** Instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored slash command: a passive prompt template. */\nexport interface AuthoredCommand {\n\tname: string;\n\tdescription?: string;\n\t/** Prompt template body (markdown). */\n\tbody: string;\n}\n\n/** An authored subagent definition. `tools` is a comma-separated allowlist (Claude Code convention). */\nexport interface AuthoredAgent {\n\tname: string;\n\tdescription?: string;\n\t/** Comma-separated `allowed-tools` string, e.g. \"read, grep, glob\". */\n\ttools?: string;\n\tmodel?: string;\n\t/** System-prompt / instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored shell hook wired to a tool event. Active code — gated on authoring. */\nexport interface AuthoredHook {\n\t/** Claude Code event name: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ... */\n\tevent: string;\n\t/** Regex matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\tcommand: string;\n\t/** Timeout in seconds. */\n\ttimeout?: number;\n}\n\n/** An authored MCP server. Active code — gated on authoring. */\nexport interface AuthoredMcpServer {\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\n\n/**\n * Format-agnostic authoring spec consumed by {@link PluginFormatAdapter.emit}.\n * `ProposePlugin` builds one of these and asks each requested adapter to render\n * it, so the same authored capability lands correctly in Claude Code and\n * Copilot layouts without the authoring code knowing either.\n */\nexport interface PluginDraft {\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/**\n\t * Which platform layouts to scaffold this draft into (e.g. `[\"claude\",\n\t * \"github\"]`). Consumed as the default target set by `emitForPlatforms`;\n\t * omit to render every registered format.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n\tskills?: AuthoredSkill[];\n\tcommands?: AuthoredCommand[];\n\tagents?: AuthoredAgent[];\n\thooks?: AuthoredHook[];\n\tmcpServers?: AuthoredMcpServer[];\n}\n\n/** Reader + writer for a single plugin format. */\nexport interface PluginFormatAdapter {\n\t/** Internal format id (stable; used in `NormalizedPlugin.format`). */\n\treadonly id: PluginFormatId;\n\t/** Public platform token (Copilot → `\"github\"`). */\n\treadonly platform: MarketplacePlatform;\n\t/**\n\t * Precedence when several formats coexist in one directory (lower wins).\n\t * Native `.agents-plugin` beats Claude beats Copilot.\n\t */\n\treadonly precedence: number;\n\t/** Short human label for messages and scaffolding summaries. */\n\treadonly label: string;\n\t/** Location of this format's marketplace index file, relative to a marketplace root (POSIX). */\n\treadonly marketplaceFile: string;\n\n\t/** True if `root` carries this format's plugin manifest. */\n\tdetectPlugin(root: string): boolean;\n\t/** Parse `root` as a plugin of this format. Returns null when unparseable. */\n\tparsePlugin(root: string): NormalizedPlugin | null;\n\t/** Render `draft` into this format's on-disk layout (paths relative to the plugin root). */\n\temit(draft: PluginDraft): EmittedFile[];\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG","sourcesContent":["/**\n * Plugin format adapter contract.\n *\n * A `PluginFormatAdapter` owns *everything* format-specific for one vendor\n * layout — how a plugin directory is detected and parsed (the reader), how a\n * marketplace index file is located, and how a plugin is scaffolded from a\n * normalized draft (the writer). The rest of the plugin subsystem is written\n * against {@link NormalizedPlugin} / {@link PluginDraft} and never branches on a\n * format again.\n *\n * This is the extension point the plugin system is designed around: Claude Code\n * and GitHub Copilot conventions evolve independently, so each lives in its own\n * adapter file ({@link ./claude.ts}, {@link ./copilot.ts}, {@link ./agents.ts}).\n * Supporting a new vendor — or tracking a change in an existing one — means\n * editing (or adding) exactly one adapter and registering it; no other module\n * changes.\n */\n\nimport type { NormalizedPlugin } from \"../manifest.js\";\n\n/** Internal format id — matches `NormalizedPlugin.format` / `NormalizedMarketplace.format`. */\nexport type PluginFormatId = \"agents\" | \"claude\" | \"copilot\";\n\n/**\n * Public platform token surfaced to users. The Copilot format lives under\n * `.github/`, so it is surfaced as `\"github\"` rather than the internal\n * `\"copilot\"`.\n */\nexport type MarketplacePlatform = \"agents\" | \"claude\" | \"github\";\n\n/** A file produced by a format's scaffolder. `path` is relative to the plugin root (POSIX). */\nexport interface EmittedFile {\n\tpath: string;\n\tcontent: string;\n}\n\n/** An authored skill: passive instructions loaded lazily (SKILL.md-style). */\nexport interface AuthoredSkill {\n\tname: string;\n\tdescription?: string;\n\t/** Instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored slash command: a passive prompt template. */\nexport interface AuthoredCommand {\n\tname: string;\n\tdescription?: string;\n\t/** Prompt template body (markdown). */\n\tbody: string;\n}\n\n/** An authored subagent definition. `tools` is a comma-separated allowlist (Claude Code convention). */\nexport interface AuthoredAgent {\n\tname: string;\n\tdescription?: string;\n\t/** Comma-separated `allowed-tools` string, e.g. \"read, grep, glob\". */\n\ttools?: string;\n\tmodel?: string;\n\t/** System-prompt / instruction body (markdown). */\n\tbody: string;\n}\n\n/** An authored shell hook wired to a tool event. Active code — gated on authoring. */\nexport interface AuthoredHook {\n\t/** Claude Code event name: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ... */\n\tevent: string;\n\t/** Regex matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\tcommand: string;\n\t/** Timeout in seconds. */\n\ttimeout?: number;\n}\n\n/** An authored MCP server. Active code — gated on authoring. */\nexport interface AuthoredMcpServer {\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\n\n/**\n * Format-agnostic authoring spec consumed by {@link PluginFormatAdapter.emit}.\n * `ProposePlugin` builds one of these and asks each requested adapter to render\n * it, so the same authored capability lands correctly in Claude Code and\n * Copilot layouts without the authoring code knowing either.\n */\nexport interface PluginDraft {\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/**\n\t * Which platform layouts to scaffold this draft into (e.g. `[\"claude\",\n\t * \"github\"]`). Consumed as the default target set by `emitForPlatforms`;\n\t * omit to render every registered format.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n\tskills?: AuthoredSkill[];\n\tcommands?: AuthoredCommand[];\n\tagents?: AuthoredAgent[];\n\thooks?: AuthoredHook[];\n\tmcpServers?: AuthoredMcpServer[];\n}\n\n/** Reader + writer for a single plugin format. */\nexport interface PluginFormatAdapter {\n\t/** Internal format id (stable; used in `NormalizedPlugin.format`). */\n\treadonly id: PluginFormatId;\n\t/** Public platform token (Copilot → `\"github\"`). */\n\treadonly platform: MarketplacePlatform;\n\t/**\n\t * Precedence when several formats coexist in one directory (lower wins).\n\t * Native `.agents-plugin` beats Claude beats Copilot.\n\t */\n\treadonly precedence: number;\n\t/** Short human label for messages and scaffolding summaries. */\n\treadonly label: string;\n\t/**\n\t * Candidate locations of this format's marketplace index file, relative to a\n\t * marketplace root (POSIX), in precedence order. Formats with more than one\n\t * real-world convention (Copilot uses both `.github/plugin/marketplace.json`\n\t * and `.github/marketplace.json`) list every location they accept.\n\t */\n\treadonly marketplaceFiles: readonly string[];\n\n\t/** True if `root` carries this format's plugin manifest. */\n\tdetectPlugin(root: string): boolean;\n\t/** Parse `root` as a plugin of this format. Returns null when unparseable. */\n\tparsePlugin(root: string): NormalizedPlugin | null;\n\t/** Render `draft` into this format's on-disk layout (paths relative to the plugin root). */\n\temit(draft: PluginDraft): EmittedFile[];\n}\n"]}
@@ -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;AA6BtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAuBxE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAkC7E","sourcesContent":["/**\n * Plugin discovery and wiring.\n *\n * Discovers plugin directories under the `plugins/` folders, parses their\n * manifests (see {@link parsePluginDir}), and turns each into a synthetic\n * {@link ExtensionFactory} that registers the plugin's capabilities through the\n * existing ExtensionAPI. The factory is loaded by the standard extension loader,\n * so plugins are just extensions assembled from a manifest instead of code.\n *\n * Capability wiring (minimum):\n * - skills / themes → `resources_discover` skill/theme paths\n * - commands → `resources_discover` slash-command paths (`.agents/commands`)\n * - agents → `resources_discover` agent paths (`.agents/agents` subagents)\n * - providers (native only) → `registerProvider`\n * - hooks → shell-protocol bridge (see hooks-bridge.ts)\n * - mcpServers → parsed; wiring deferred (see design doc)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { type ExtensionMcpServerConfig, registerExtensionMcpServers } from \"../../extension-mcp-servers.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"../types.js\";\nimport { installPluginHooks } from \"./hooks-bridge.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Substitute ${CLAUDE_PLUGIN_ROOT} / ${AGENTS_PLUGIN_ROOT} with the plugin root in a string. */\nfunction substituteRoot(value: string, root: string): string {\n\treturn value.replace(/\\$\\{(?:CLAUDE|AGENTS)_PLUGIN_ROOT\\}/g, root);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\troot: string,\n): Record<string, ExtensionMcpServerConfig> {\n\tconst out: Record<string, ExtensionMcpServerConfig> = {};\n\tfor (const [name, raw] of Object.entries(mcpServers)) {\n\t\tif (!raw || typeof raw !== \"object\") continue;\n\t\tconst cfg = raw as ExtensionMcpServerConfig;\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteRoot(cfg.command, root),\n\t\t\targs: cfg.args?.map((a) => substituteRoot(String(a), root)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteRoot(String(v), root)]))\n\t\t\t\t: undefined,\n\t\t\tbackground: cfg.background,\n\t\t};\n\t}\n\treturn out;\n}\n\nexport type { NormalizedPlugin } from \"./manifest.js\";\nexport { parsePluginDir } from \"./manifest.js\";\n\n/**\n * Discover plugins across the given `plugins/` directories.\n * First-wins on duplicate ids (project dirs should be listed before global).\n */\nexport function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[] {\n\tconst plugins: NormalizedPlugin[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const dir of pluginDirs) {\n\t\tif (!fs.existsSync(dir)) continue;\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\t\tconst plugin = parsePluginDir(path.join(dir, entry.name));\n\t\t\tif (plugin && !seen.has(plugin.id)) {\n\t\t\t\tseen.add(plugin.id);\n\t\t\t\tplugins.push(plugin);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn plugins;\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin): ExtensionFactory {\n\tconst factory: ExtensionFactory = (pi: ExtensionAPI) => {\n\t\t// Resources: contribute the plugin's capability directories. Commands map to\n\t\t// the slash-command surface (`.agents/commands`) and agents to subagent\n\t\t// definitions (`.agents/agents`), matching hoocode's native conventions.\n\t\tif (plugin.skillsDir || plugin.commandsDir || plugin.themesDir || plugin.agentsDir) {\n\t\t\tpi.on(\"resources_discover\", () => ({\n\t\t\t\tskillPaths: plugin.skillsDir ? [plugin.skillsDir] : undefined,\n\t\t\t\tthemePaths: plugin.themesDir ? [plugin.themesDir] : undefined,\n\t\t\t\tslashCommandPaths: plugin.commandsDir ? [plugin.commandsDir] : undefined,\n\t\t\t\tagentPaths: plugin.agentsDir ? [plugin.agentsDir] : undefined,\n\t\t\t}));\n\t\t}\n\n\t\t// Providers (native plugins only).\n\t\tfor (const provider of plugin.providers ?? []) {\n\t\t\tpi.registerProvider(provider.name, provider.config);\n\t\t}\n\n\t\t// Hooks: true-parity shell bridge.\n\t\tif (plugin.hooks) {\n\t\t\tinstallPluginHooks(pi, plugin.hooks, plugin.root, () => {\n\t\t\t\t// Non-blocking hook failures are intentionally quiet (Claude Code parity).\n\t\t\t});\n\t\t}\n\n\t\t// MCP servers: register for 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;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"]}
@@ -31,6 +31,19 @@ function resolveMcpServers(mcpServers, root) {
31
31
  if (!raw || typeof raw !== "object")
32
32
  continue;
33
33
  const cfg = raw;
34
+ // Remote servers ({ type: "http" | "sse", url }): pass through for the
35
+ // MCP loader's HTTP transports.
36
+ if (typeof cfg.url === "string" && typeof cfg.command !== "string") {
37
+ out[name] = {
38
+ type: cfg.type === "sse" ? "sse" : "http",
39
+ url: cfg.url,
40
+ headers: cfg.headers
41
+ ? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteRoot(String(v), root)]))
42
+ : undefined,
43
+ background: cfg.background,
44
+ };
45
+ continue;
46
+ }
34
47
  if (typeof cfg.command !== "string")
35
48
  continue;
36
49
  out[name] = {
@@ -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\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 { 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"]}
@@ -21,6 +21,27 @@ export declare function sanitizeForDir(s: string): string;
21
21
  export declare function defaultMarketplaceDir(): string;
22
22
  /** The default marketplace record — always present so source-level trust is meaningful out of the box. */
23
23
  export declare function defaultMarketplaceRecord(): MarketplaceRecord;
24
+ /**
25
+ * Curated well-known marketplaces, trusted out of the box. Shipping an entry
26
+ * here is a maintainer-level trust decision (the human half of the "adding a
27
+ * marketplace is the human trust boundary" rule) — keep this list short and
28
+ * high-trust. Indices are cloned lazily into the marketplace cache on first
29
+ * search and never auto-updated afterwards (plugin sources inside the official
30
+ * index are sha-pinned).
31
+ */
32
+ export declare const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{
33
+ name: string;
34
+ url: string;
35
+ }>;
36
+ /** Local cache directory for a marketplace fetched from `url` (same convention as `/plugin marketplace add`). */
37
+ export declare function marketplaceCacheDir(cwd: string, url: string): string;
38
+ /**
39
+ * Clone any well-known marketplace index that is not in the local cache yet.
40
+ * No-op (and no network) when every index is already cached. Returns one error
41
+ * string per marketplace that could not be fetched (offline is non-fatal —
42
+ * search degrades to the marketplaces already available).
43
+ */
44
+ export declare function ensureWellKnownMarketplaces(cwd: string): Promise<string[]>;
24
45
  /**
25
46
  * All marketplace records in effect: the bundled default first (curated,
26
47
  * trusted), then user-added ones from `.agents/` (falling back to legacy
@@ -61,7 +82,8 @@ export interface InstallOutcome {
61
82
  * Install an available plugin by name into `.agents/plugins`. Copies local
62
83
  * sources; clones git sources. Transparent + reversible by construction — the
63
84
  * plugin lands in a named directory and {@link uninstallPlugin} removes it.
64
- * Active after the next reload.
85
+ * Callers activate the result (live activation via AgentSession.activatePlugin,
86
+ * or a reload).
65
87
  */
66
88
  export declare function installAvailablePlugin(cwd: string, name: string): Promise<InstallOutcome>;
67
89
  export interface UninstallOutcome {
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EAItB,MAAM,kBAAkB,CAAC;AAE1B,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAUD,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,4GAA0G;AAC1G,wBAAgB,wBAAwB,IAAI,iBAAiB,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAcvE;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,MAAM,EAAE,uBAAuB,CAAC;IAChC,oDAAoD;IACpD,UAAU,EAAE,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,CAAC;IACnD,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,eAAe,EAAE,mBAAmB,EAAE,CAAC;CACvC;AAED,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBnE;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE1F;AAED,sEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAEtG;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAEtG;AAqED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA4C/F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAS3E","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/** `.agents/` is the primary, cross-vendor home for installed plugins and the added-marketplace registry. */\nexport function installedPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\nfunction marketplaceStorePath(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"marketplaces.json\");\n}\n\nfunction legacyStorePath(cwd: string): string {\n\treturn path.join(cwd, \".hoocode\", \"marketplaces.json\");\n}\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nexport function defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\tconst primary = readMarketplaceStore(marketplaceStorePath(cwd));\n\tconst user =\n\t\tprimary.length > 0 || existsSync(marketplaceStorePath(cwd))\n\t\t\t? primary\n\t\t\t: readMarketplaceStore(legacyStorePath(cwd));\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(cwd: string, name: string): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\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\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Clone a git repository into `dest`.\n * If `ref` is provided, does a shallow clone of that branch/tag.\n * If only `sha` is provided, does a full clone and checks out the commit.\n * Returns the clone result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (ref) {\n\t\treturn execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\t}\n\tif (sha) {\n\t\tconst cloneRes = await execGit([\"clone\", \"--\", url, dest]);\n\t\tif (cloneRes.code !== 0) return cloneRes;\n\t\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n\t}\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into `.agents/plugins`. Copies local\n * sources; clones git sources. Transparent + reversible by construction — the\n * plugin lands in a named directory and {@link uninstallPlugin} removes it.\n * Active after the next reload.\n */\nexport async function installAvailablePlugin(cwd: string, name: string): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(installedPluginsDir(cwd), { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tconst parsed = parsePluginDir(dest);\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${path.relative(cwd, dest) || dest}. ` +\n\t\t\t`Active after the next reload; remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** Remove an installed plugin from `.agents/plugins` (and the legacy `.hoocode/plugins`). */\nexport function uninstallPlugin(cwd: string, name: string): UninstallOutcome {\n\tconst candidates = [\n\t\tpath.join(installedPluginsDir(cwd), sanitizeForDir(name)),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\". Effective after the next reload.` };\n}\n"]}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EAItB,MAAM,kBAAkB,CAAC;AAE1B,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAUD,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,4GAA0G;AAC1G,wBAAgB,wBAAwB,IAAI,iBAAiB,CAE5D;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMhF,CAAC;AAEF,iHAAiH;AACjH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAahF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAuBvE;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,MAAM,EAAE,uBAAuB,CAAC;IAChC,oDAAoD;IACpD,UAAU,EAAE,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,CAAC;IACnD,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,eAAe,EAAE,mBAAmB,EAAE,CAAC;CACvC;AAED,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBnE;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE1F;AAED,sEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAEtG;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAEtG;AAqED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA6D/F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAS3E","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/** `.agents/` is the primary, cross-vendor home for installed plugins and the added-marketplace registry. */\nexport function installedPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\nfunction marketplaceStorePath(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"marketplaces.json\");\n}\n\nfunction legacyStorePath(cwd: string): string {\n\treturn path.join(cwd, \".hoocode\", \"marketplaces.json\");\n}\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nexport function defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * Curated well-known marketplaces, trusted out of the box. Shipping an entry\n * here is a maintainer-level trust decision (the human half of the \"adding a\n * marketplace is the human trust boundary\" rule) — keep this list short and\n * high-trust. Indices are cloned lazily into the marketplace cache on first\n * search and never auto-updated afterwards (plugin sources inside the official\n * index are sha-pinned).\n */\nexport const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string }> = [\n\t{ name: \"claude-plugins-official\", url: \"https://github.com/anthropics/claude-plugins-official\" },\n\t// The Copilot plugin directory ships both a `.github/plugin/marketplace.json`\n\t// and a `.claude-plugin/marketplace.json`, so it parses with\n\t// supportPlatform [\"claude\", \"github\"] — the default `github` source.\n\t{ name: \"copilot-plugins\", url: \"https://github.com/github/copilot-plugins\" },\n];\n\n/** Local cache directory for a marketplace fetched from `url` (same convention as `/plugin marketplace add`). */\nexport function marketplaceCacheDir(cwd: string, url: string): string {\n\treturn path.join(cwd, \".agents\", \"marketplace-cache\", sanitizeForDir(url));\n}\n\n/**\n * Clone any well-known marketplace index that is not in the local cache yet.\n * No-op (and no network) when every index is already cached. Returns one error\n * string per marketplace that could not be fetched (offline is non-fatal —\n * search degrades to the marketplaces already available).\n */\nexport async function ensureWellKnownMarketplaces(cwd: string): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(cwd, wk.url);\n\t\tif (existsSync(dir)) continue;\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tconst res = await cloneGitRepo(wk.url, dir);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\terrors.push(`${wk.name}: ${(res.stderr || res.stdout).trim()}`);\n\t\t}\n\t}\n\treturn errors;\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\t// Well-known marketplaces participate once their index is cached locally\n\t// (see ensureWellKnownMarketplaces; SearchPlugins fetches lazily).\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(cwd, wk.url);\n\t\tif (existsSync(dir) && !records.some((x) => x.dir === dir)) {\n\t\t\trecords.push({ location: wk.url, dir });\n\t\t}\n\t}\n\n\tconst primary = readMarketplaceStore(marketplaceStorePath(cwd));\n\tconst user =\n\t\tprimary.length > 0 || existsSync(marketplaceStorePath(cwd))\n\t\t\t? primary\n\t\t\t: readMarketplaceStore(legacyStorePath(cwd));\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(cwd: string, name: string): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\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\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Clone a git repository into `dest`.\n * If `ref` is provided, does a shallow clone of that branch/tag.\n * If only `sha` is provided, does a full clone and checks out the commit.\n * Returns the clone result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (ref) {\n\t\treturn execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\t}\n\tif (sha) {\n\t\tconst cloneRes = await execGit([\"clone\", \"--\", url, dest]);\n\t\tif (cloneRes.code !== 0) return cloneRes;\n\t\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n\t}\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into `.agents/plugins`. Copies local\n * sources; clones git sources. Transparent + reversible by construction — the\n * plugin lands in a named directory and {@link uninstallPlugin} removes it.\n * Callers activate the result (live activation via AgentSession.activatePlugin,\n * or a reload).\n */\nexport async function installAvailablePlugin(cwd: string, name: string): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(installedPluginsDir(cwd), { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tlet parsed = parsePluginDir(dest);\n\tif (!parsed) {\n\t\t// Manifest-less plugin dir (some marketplaces index bare capability trees,\n\t\t// e.g. a plugin that is just a `skills/` directory): synthesize a native\n\t\t// manifest from the marketplace entry so the standard loader can carry it.\n\t\tconst manifestDir = path.join(dest, \".agents-plugin\");\n\t\tmkdirSync(manifestDir, { recursive: true });\n\t\twriteFileSync(\n\t\t\tpath.join(manifestDir, \"plugin.json\"),\n\t\t\t`${JSON.stringify(\n\t\t\t\t{ name: found.name, ...(found.description ? { description: found.description } : {}) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)}\\n`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tparsed = parsePluginDir(dest);\n\t}\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${path.relative(cwd, dest) || dest}. ` +\n\t\t\t`Remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** Remove an installed plugin from `.agents/plugins` (and the legacy `.hoocode/plugins`). */\nexport function uninstallPlugin(cwd: string, name: string): UninstallOutcome {\n\tconst candidates = [\n\t\tpath.join(installedPluginsDir(cwd), sanitizeForDir(name)),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\".` };\n}\n"]}