@kolisachint/hoocode-agent 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +10 -7
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +10 -2
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/capabilities/deferral.d.ts +78 -0
  10. package/dist/core/capabilities/deferral.d.ts.map +1 -0
  11. package/dist/core/capabilities/deferral.js +99 -0
  12. package/dist/core/capabilities/deferral.js.map +1 -0
  13. package/dist/core/capabilities/dense.d.ts +51 -0
  14. package/dist/core/capabilities/dense.d.ts.map +1 -0
  15. package/dist/core/capabilities/dense.js +121 -0
  16. package/dist/core/capabilities/dense.js.map +1 -0
  17. package/dist/core/capabilities/lexical.d.ts +43 -0
  18. package/dist/core/capabilities/lexical.d.ts.map +1 -0
  19. package/dist/core/capabilities/lexical.js +104 -0
  20. package/dist/core/capabilities/lexical.js.map +1 -0
  21. package/dist/core/capabilities/registry.d.ts +56 -0
  22. package/dist/core/capabilities/registry.d.ts.map +1 -0
  23. package/dist/core/capabilities/registry.js +0 -0
  24. package/dist/core/capabilities/registry.js.map +1 -0
  25. package/dist/core/capabilities/search.d.ts +47 -0
  26. package/dist/core/capabilities/search.d.ts.map +1 -0
  27. package/dist/core/capabilities/search.js +82 -0
  28. package/dist/core/capabilities/search.js.map +1 -0
  29. package/dist/core/extensions/loader.d.ts +27 -3
  30. package/dist/core/extensions/loader.d.ts.map +1 -1
  31. package/dist/core/extensions/loader.js +61 -7
  32. package/dist/core/extensions/loader.js.map +1 -1
  33. package/dist/core/extensions/plugins/authoring.d.ts +56 -9
  34. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  35. package/dist/core/extensions/plugins/authoring.js +0 -0
  36. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  37. package/dist/core/extensions/plugins/drift.d.ts +75 -0
  38. package/dist/core/extensions/plugins/drift.d.ts.map +1 -0
  39. package/dist/core/extensions/plugins/drift.js +194 -0
  40. package/dist/core/extensions/plugins/drift.js.map +1 -0
  41. package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
  42. package/dist/core/extensions/plugins/formats/claude.js +5 -0
  43. package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
  44. package/dist/core/extensions/plugins/formats/copilot.d.ts +6 -0
  45. package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
  46. package/dist/core/extensions/plugins/formats/copilot.js +57 -16
  47. package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
  48. package/dist/core/extensions/plugins/formats/index.d.ts +39 -1
  49. package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
  50. package/dist/core/extensions/plugins/formats/index.js +90 -13
  51. package/dist/core/extensions/plugins/formats/index.js.map +1 -1
  52. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +14 -0
  53. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
  54. package/dist/core/extensions/plugins/formats/jsonManifest.js +37 -4
  55. package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
  56. package/dist/core/extensions/plugins/formats/platform-targets.d.ts +48 -24
  57. package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -1
  58. package/dist/core/extensions/plugins/formats/platform-targets.js +62 -30
  59. package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -1
  60. package/dist/core/extensions/plugins/formats/shared.d.ts +22 -0
  61. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  62. package/dist/core/extensions/plugins/formats/shared.js +81 -4
  63. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  64. package/dist/core/extensions/plugins/formats/types.d.ts +10 -1
  65. package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
  66. package/dist/core/extensions/plugins/formats/types.js.map +1 -1
  67. package/dist/core/extensions/plugins/gates.d.ts +64 -0
  68. package/dist/core/extensions/plugins/gates.d.ts.map +1 -0
  69. package/dist/core/extensions/plugins/gates.js +297 -0
  70. package/dist/core/extensions/plugins/gates.js.map +1 -0
  71. package/dist/core/extensions/plugins/hooks-bridge.d.ts +1 -1
  72. package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -1
  73. package/dist/core/extensions/plugins/hooks-bridge.js +12 -8
  74. package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -1
  75. package/dist/core/extensions/plugins/index.d.ts +36 -5
  76. package/dist/core/extensions/plugins/index.d.ts.map +1 -1
  77. package/dist/core/extensions/plugins/index.js +66 -15
  78. package/dist/core/extensions/plugins/index.js.map +1 -1
  79. package/dist/core/extensions/plugins/install.d.ts +43 -24
  80. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  81. package/dist/core/extensions/plugins/install.js +167 -54
  82. package/dist/core/extensions/plugins/install.js.map +1 -1
  83. package/dist/core/extensions/plugins/locations.d.ts +84 -0
  84. package/dist/core/extensions/plugins/locations.d.ts.map +1 -0
  85. package/dist/core/extensions/plugins/locations.js +137 -0
  86. package/dist/core/extensions/plugins/locations.js.map +1 -0
  87. package/dist/core/extensions/plugins/manifest.d.ts +21 -1
  88. package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
  89. package/dist/core/extensions/plugins/manifest.js +2 -2
  90. package/dist/core/extensions/plugins/manifest.js.map +1 -1
  91. package/dist/core/extensions/plugins/packaging.d.ts +115 -0
  92. package/dist/core/extensions/plugins/packaging.d.ts.map +1 -0
  93. package/dist/core/extensions/plugins/packaging.js +362 -0
  94. package/dist/core/extensions/plugins/packaging.js.map +1 -0
  95. package/dist/core/extensions/plugins/smoke.d.ts +33 -0
  96. package/dist/core/extensions/plugins/smoke.d.ts.map +1 -0
  97. package/dist/core/extensions/plugins/smoke.js +231 -0
  98. package/dist/core/extensions/plugins/smoke.js.map +1 -0
  99. package/dist/core/extensions/plugins/trigger-eval.d.ts +136 -0
  100. package/dist/core/extensions/plugins/trigger-eval.d.ts.map +1 -0
  101. package/dist/core/extensions/plugins/trigger-eval.js +0 -0
  102. package/dist/core/extensions/plugins/trigger-eval.js.map +1 -0
  103. package/dist/core/package-manager.d.ts +6 -0
  104. package/dist/core/package-manager.d.ts.map +1 -1
  105. package/dist/core/package-manager.js.map +1 -1
  106. package/dist/core/resource-loader.d.ts +2 -0
  107. package/dist/core/resource-loader.d.ts.map +1 -1
  108. package/dist/core/resource-loader.js +15 -0
  109. package/dist/core/resource-loader.js.map +1 -1
  110. package/dist/core/settings-manager.d.ts +2 -2
  111. package/dist/core/settings-manager.d.ts.map +1 -1
  112. package/dist/core/settings-manager.js +3 -3
  113. package/dist/core/settings-manager.js.map +1 -1
  114. package/dist/core/settings-types.d.ts +1 -1
  115. package/dist/core/settings-types.d.ts.map +1 -1
  116. package/dist/core/settings-types.js.map +1 -1
  117. package/dist/core/skills.d.ts +9 -0
  118. package/dist/core/skills.d.ts.map +1 -1
  119. package/dist/core/skills.js +23 -3
  120. package/dist/core/skills.js.map +1 -1
  121. package/dist/core/tools/package-plugin.d.ts +19 -0
  122. package/dist/core/tools/package-plugin.d.ts.map +1 -0
  123. package/dist/core/tools/package-plugin.js +81 -0
  124. package/dist/core/tools/package-plugin.js.map +1 -0
  125. package/dist/core/tools/plugin-tool-names.d.ts +1 -0
  126. package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
  127. package/dist/core/tools/plugin-tool-names.js +6 -0
  128. package/dist/core/tools/plugin-tool-names.js.map +1 -1
  129. package/dist/core/tools/plugins.d.ts.map +1 -1
  130. package/dist/core/tools/plugins.js +48 -7
  131. package/dist/core/tools/plugins.js.map +1 -1
  132. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  133. package/dist/core/tools/propose-plugin.js +49 -21
  134. package/dist/core/tools/propose-plugin.js.map +1 -1
  135. package/dist/core/tools/webfetch.d.ts +3 -1
  136. package/dist/core/tools/webfetch.d.ts.map +1 -1
  137. package/dist/core/tools/webfetch.js +8 -2
  138. package/dist/core/tools/webfetch.js.map +1 -1
  139. package/dist/core/tools/websearch.d.ts +2 -0
  140. package/dist/core/tools/websearch.d.ts.map +1 -1
  141. package/dist/core/tools/websearch.js +2 -1
  142. package/dist/core/tools/websearch.js.map +1 -1
  143. package/dist/core/tools/webtools-shared.d.ts +23 -2
  144. package/dist/core/tools/webtools-shared.d.ts.map +1 -1
  145. package/dist/core/tools/webtools-shared.js +59 -9
  146. package/dist/core/tools/webtools-shared.js.map +1 -1
  147. package/dist/extensions/core/marketplace.d.ts +14 -3
  148. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  149. package/dist/extensions/core/marketplace.js +77 -18
  150. package/dist/extensions/core/marketplace.js.map +1 -1
  151. package/dist/extensions/core/mcp-deferred.d.ts +39 -2
  152. package/dist/extensions/core/mcp-deferred.d.ts.map +1 -1
  153. package/dist/extensions/core/mcp-deferred.js +74 -10
  154. package/dist/extensions/core/mcp-deferred.js.map +1 -1
  155. package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
  156. package/dist/extensions/core/mcp-loader.js +52 -9
  157. package/dist/extensions/core/mcp-loader.js.map +1 -1
  158. package/dist/extensions/core/scaffold.d.ts +3 -3
  159. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  160. package/dist/extensions/core/scaffold.js +9 -9
  161. package/dist/extensions/core/scaffold.js.map +1 -1
  162. package/dist/main.d.ts.map +1 -1
  163. package/dist/main.js +25 -7
  164. package/dist/main.js.map +1 -1
  165. package/dist/utils/tools-manager.d.ts +8 -0
  166. package/dist/utils/tools-manager.d.ts.map +1 -1
  167. package/dist/utils/tools-manager.js +12 -3
  168. package/dist/utils/tools-manager.js.map +1 -1
  169. package/docs/providers.md +11 -0
  170. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  171. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  172. package/examples/extensions/sandbox/package.json +1 -1
  173. package/examples/extensions/with-deps/package.json +1 -1
  174. package/package.json +5 -4
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tier 2 — offline drift check (§2.2).
3
+ *
4
+ * The adapters had drifted in fifteen places across two vendors and nothing
5
+ * noticed. The defect was never a missing doc-fetcher; it was a missing
6
+ * *signal*. This module is that signal: diff what the vendors document against
7
+ * what {@link declaredVocabulary} says we understand, and report the gap.
8
+ *
9
+ * Two properties are non-negotiable, both inherited from Tier 0:
10
+ *
11
+ * - **Nothing here runs at runtime.** The fetch happens in a CI job (see
12
+ * `scripts/plugin-drift-check.ts`). The network informs a human's decision to
13
+ * edit an adapter; it never informs a parse. That is the same rule
14
+ * `AGENTS.md` sets for `models.generated.ts`.
15
+ * - **A failed fetch is not a clean report.** Offline must be distinguishable
16
+ * from "no drift", or the check quietly stops checking the day the docs move
17
+ * behind a redirect.
18
+ *
19
+ * The extraction is deliberately conservative. Manifest keys come from JSON code
20
+ * fences — actual manifest examples, not prose — because a regex over English
21
+ * finds every word that happens to be backticked. Paths come from backticked
22
+ * path-shaped tokens, which is noisier, so path findings are leads for a human
23
+ * to confirm rather than assertions. A drift report nobody trusts gets muted,
24
+ * and a muted report is worse than none.
25
+ */
26
+ export interface DriftSource {
27
+ label: string;
28
+ url: string;
29
+ /** Page text, or undefined when it could not be fetched. */
30
+ text?: string;
31
+ error?: string;
32
+ }
33
+ export interface DriftFinding {
34
+ kind: "manifest-key" | "path";
35
+ value: string;
36
+ /** Which reference page it was seen in. */
37
+ source: string;
38
+ }
39
+ export interface DriftReport {
40
+ /** True only when every source was fetched *and* nothing new was found. */
41
+ clean: boolean;
42
+ /** Sources that could not be fetched. Non-empty means the report is incomplete. */
43
+ unreachable: Array<{
44
+ label: string;
45
+ error: string;
46
+ }>;
47
+ findings: DriftFinding[];
48
+ checkedSources: string[];
49
+ }
50
+ /** Top-level keys of every JSON object in a fenced ```json block. */
51
+ export declare function manifestKeysIn(markdown: string): Set<string>;
52
+ /**
53
+ * Backticked tokens that look like a **plugin-relative** file or directory path.
54
+ *
55
+ * Tight on purpose. The first cut of this accepted anything path-shaped and
56
+ * produced twenty findings against the live Claude reference, of which none were
57
+ * real drift: MCP method names (`roots/list`), repo slugs
58
+ * (`anthropics/claude-plugins-community`), workspace paths (`.claude/settings.json`),
59
+ * bare extensions (`.zip`), and example scripts. A drift report with that
60
+ * signal-to-noise ratio gets muted, and a muted report is worse than none — so
61
+ * the filter errs toward missing a real surface rather than crying wolf, and the
62
+ * manifest-key half (which reads parsed JSON, not prose) carries the precision.
63
+ */
64
+ export declare function pathsIn(markdown: string): Set<string>;
65
+ /**
66
+ * Diff the fetched references against the declared vocabulary.
67
+ *
68
+ * Everything already declared — modelled, read, or knowingly unsupported — is
69
+ * filtered out, so what remains answers one question: *is the vendor documenting
70
+ * something we have never heard of?*
71
+ */
72
+ export declare function analyzeDrift(sources: readonly DriftSource[]): DriftReport;
73
+ /** Render a report for a CI log or a human-opened issue. */
74
+ export declare function formatDriftReport(report: DriftReport): string;
75
+ //# sourceMappingURL=drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,WAAW,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqB5D;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAwBrD;AAqBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,CAmCzE;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAqB7D","sourcesContent":["/**\n * Tier 2 — offline drift check (§2.2).\n *\n * The adapters had drifted in fifteen places across two vendors and nothing\n * noticed. The defect was never a missing doc-fetcher; it was a missing\n * *signal*. This module is that signal: diff what the vendors document against\n * what {@link declaredVocabulary} says we understand, and report the gap.\n *\n * Two properties are non-negotiable, both inherited from Tier 0:\n *\n * - **Nothing here runs at runtime.** The fetch happens in a CI job (see\n * `scripts/plugin-drift-check.ts`). The network informs a human's decision to\n * edit an adapter; it never informs a parse. That is the same rule\n * `AGENTS.md` sets for `models.generated.ts`.\n * - **A failed fetch is not a clean report.** Offline must be distinguishable\n * from \"no drift\", or the check quietly stops checking the day the docs move\n * behind a redirect.\n *\n * The extraction is deliberately conservative. Manifest keys come from JSON code\n * fences — actual manifest examples, not prose — because a regex over English\n * finds every word that happens to be backticked. Paths come from backticked\n * path-shaped tokens, which is noisier, so path findings are leads for a human\n * to confirm rather than assertions. A drift report nobody trusts gets muted,\n * and a muted report is worse than none.\n */\n\nimport { declaredVocabulary } from \"./formats/index.js\";\n\nexport interface DriftSource {\n\tlabel: string;\n\turl: string;\n\t/** Page text, or undefined when it could not be fetched. */\n\ttext?: string;\n\terror?: string;\n}\n\nexport interface DriftFinding {\n\tkind: \"manifest-key\" | \"path\";\n\tvalue: string;\n\t/** Which reference page it was seen in. */\n\tsource: string;\n}\n\nexport interface DriftReport {\n\t/** True only when every source was fetched *and* nothing new was found. */\n\tclean: boolean;\n\t/** Sources that could not be fetched. Non-empty means the report is incomplete. */\n\tunreachable: Array<{ label: string; error: string }>;\n\tfindings: DriftFinding[];\n\tcheckedSources: string[];\n}\n\n/** Top-level keys of every JSON object in a fenced ```json block. */\nexport function manifestKeysIn(markdown: string): Set<string> {\n\tconst keys = new Set<string>();\n\tfor (const match of markdown.matchAll(/```(?:jsonc?|json5)\\n([\\s\\S]*?)```/g)) {\n\t\tconst body = match[1];\n\t\t// Strip comments and trailing commas so documentation JSON (which is often\n\t\t// neither) still parses.\n\t\tconst cleaned = body\n\t\t\t.replace(/^\\s*\\/\\/.*$/gm, \"\")\n\t\t\t.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\")\n\t\t\t.replace(/,(\\s*[}\\]])/g, \"$1\");\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cleaned) as unknown;\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\t\tfor (const key of Object.keys(parsed)) keys.add(key);\n\t\t\t}\n\t\t} catch {\n\t\t\t// A fence that does not parse tells us nothing; guessing at its keys with\n\t\t\t// a regex is how a drift report fills up with noise.\n\t\t}\n\t}\n\treturn keys;\n}\n\n/**\n * Plugin component directories worth noticing on their own, without a file\n * extension to identify them.\n */\n/**\n * Files every repository has. They appear in vendor references as scaffolding\n * examples, never as a surface an adapter should parse.\n */\nconst GENERIC_FILES = new Set([\"README.md\", \"CHANGELOG.md\", \"LICENSE.md\", \"CLAUDE.md\", \"AGENTS.md\", \"package.json\"]);\n\nconst COMPONENT_DIR_NAMES = new Set([\n\t\"skills\",\n\t\"commands\",\n\t\"agents\",\n\t\"hooks\",\n\t\"themes\",\n\t\"workflows\",\n\t\"output-styles\",\n\t\"monitors\",\n\t\"bin\",\n\t\"prompts\",\n\t\"chatmodes\",\n]);\n\n/**\n * Backticked tokens that look like a **plugin-relative** file or directory path.\n *\n * Tight on purpose. The first cut of this accepted anything path-shaped and\n * produced twenty findings against the live Claude reference, of which none were\n * real drift: MCP method names (`roots/list`), repo slugs\n * (`anthropics/claude-plugins-community`), workspace paths (`.claude/settings.json`),\n * bare extensions (`.zip`), and example scripts. A drift report with that\n * signal-to-noise ratio gets muted, and a muted report is worse than none — so\n * the filter errs toward missing a real surface rather than crying wolf, and the\n * manifest-key half (which reads parsed JSON, not prose) carries the precision.\n */\nexport function pathsIn(markdown: string): Set<string> {\n\tconst paths = new Set<string>();\n\tfor (const match of markdown.matchAll(/`([^`\\s]+)`/g)) {\n\t\tconst token = match[1].replace(/^\\.\\//, \"\").replace(/\\/$/, \"\");\n\t\tif (!/^[\\w.@-]+(?:\\/[\\w.@-]+)*$/.test(token)) continue;\n\t\t// Workspace surfaces, not plugin surfaces: `.claude/settings.json` and\n\t\t// friends are §1.3's territory and are not what an adapter parses.\n\t\tif (token.startsWith(\".claude/\") || token.startsWith(\".github/workflows\")) continue;\n\t\tif (token.startsWith(\"..\")) continue;\n\n\t\tconst tail = token.slice(token.lastIndexOf(\"/\") + 1);\n\t\tif (GENERIC_FILES.has(tail)) continue;\n\t\t// A real surface is a config file or a component directory. Anything else\n\t\t// path-shaped in prose is an example, a slug, or a protocol method.\n\t\tconst isConfigFile = /\\.(json|jsonc|md|toml|ya?ml)$/.test(tail);\n\t\tconst isComponentDir = COMPONENT_DIR_NAMES.has(tail) || COMPONENT_DIR_NAMES.has(token);\n\t\tif (!isConfigFile && !isComponentDir) continue;\n\t\t// A single token with no separator has to stand on its own: a named config\n\t\t// file (`.mcp.json`, `plugin.json`) or a component directory. A bare\n\t\t// extension like `.zip` is neither.\n\t\tif (!token.includes(\"/\") && !isComponentDir && !/^\\.?[\\w-]+\\.(json|jsonc|md|toml|ya?ml)$/.test(token)) continue;\n\t\tpaths.add(token);\n\t}\n\treturn paths;\n}\n\n/**\n * Whether a documented path is one we already declare.\n *\n * Suffix match, because references write paths from an example plugin root\n * (`my-plugin/hooks/hooks.json`) while adapters declare them plugin-relative\n * (`hooks/hooks.json`). Comparing the two literally reports every documented\n * example as new.\n */\nfunction isDeclaredPath(documented: string, declared: ReadonlySet<string>): boolean {\n\tif (declared.has(documented)) return true;\n\tfor (const known of declared) {\n\t\tif (documented === known || documented.endsWith(`/${known}`) || known.endsWith(`/${documented}`)) return true;\n\t\t// A documented directory whose contents we already declare: the reference\n\t\t// writes `monitors`, the adapter declares `monitors/monitors.json`.\n\t\tif (known.startsWith(`${documented}/`) || documented.startsWith(`${known}/`)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Diff the fetched references against the declared vocabulary.\n *\n * Everything already declared — modelled, read, or knowingly unsupported — is\n * filtered out, so what remains answers one question: *is the vendor documenting\n * something we have never heard of?*\n */\nexport function analyzeDrift(sources: readonly DriftSource[]): DriftReport {\n\tconst declared = declaredVocabulary();\n\tconst knownKeys = new Set([...declared.manifestKeys, ...declared.marketplaceKeys]);\n\tconst knownPaths = new Set(\n\t\t[...declared.readPaths, ...declared.marketplaceFiles, ...declared.unsupportedSurfaces].map((p) =>\n\t\t\tp.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\"),\n\t\t),\n\t);\n\n\tconst unreachable: DriftReport[\"unreachable\"] = [];\n\tconst findings: DriftFinding[] = [];\n\tconst checkedSources: string[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const source of sources) {\n\t\tif (!source.text) {\n\t\t\tunreachable.push({ label: source.label, error: source.error ?? \"not fetched\" });\n\t\t\tcontinue;\n\t\t}\n\t\tcheckedSources.push(source.label);\n\t\tfor (const key of manifestKeysIn(source.text)) {\n\t\t\tif (knownKeys.has(key) || seen.has(`k:${key}`)) continue;\n\t\t\tseen.add(`k:${key}`);\n\t\t\tfindings.push({ kind: \"manifest-key\", value: key, source: source.label });\n\t\t}\n\t\tfor (const p of pathsIn(source.text)) {\n\t\t\tif (isDeclaredPath(p, knownPaths) || seen.has(`p:${p}`)) continue;\n\t\t\tseen.add(`p:${p}`);\n\t\t\tfindings.push({ kind: \"path\", value: p, source: source.label });\n\t\t}\n\t}\n\n\t// Incomplete is never clean. A partial sweep that reports \"no drift\" is the\n\t// failure this tier exists to prevent, one level up.\n\treturn { clean: unreachable.length === 0 && findings.length === 0, unreachable, findings, checkedSources };\n}\n\n/** Render a report for a CI log or a human-opened issue. */\nexport function formatDriftReport(report: DriftReport): string {\n\tconst lines: string[] = [];\n\tif (report.checkedSources.length > 0) lines.push(`Checked: ${report.checkedSources.join(\", \")}`);\n\tfor (const u of report.unreachable) {\n\t\tlines.push(`UNREACHABLE ${u.label}: ${u.error} — this report is incomplete.`);\n\t}\n\tif (report.findings.length === 0) {\n\t\tlines.push(report.unreachable.length > 0 ? \"No drift in what could be read.\" : \"No drift.\");\n\t\treturn lines.join(\"\\n\");\n\t}\n\tlines.push(\"\", `${report.findings.length} candidate(s) documented upstream but not declared here:`);\n\tfor (const f of report.findings) {\n\t\tlines.push(` [${f.kind}] ${f.value} (${f.source})`);\n\t}\n\tlines.push(\n\t\t\"\",\n\t\t\"These are leads, not verdicts — path extraction reads prose and will surface\",\n\t\t\"examples alongside conventions. Confirm against the reference, then either\",\n\t\t\"model the surface in the adapter or add it to the knowingly-unsupported list.\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tier 2 — offline drift check (§2.2).
3
+ *
4
+ * The adapters had drifted in fifteen places across two vendors and nothing
5
+ * noticed. The defect was never a missing doc-fetcher; it was a missing
6
+ * *signal*. This module is that signal: diff what the vendors document against
7
+ * what {@link declaredVocabulary} says we understand, and report the gap.
8
+ *
9
+ * Two properties are non-negotiable, both inherited from Tier 0:
10
+ *
11
+ * - **Nothing here runs at runtime.** The fetch happens in a CI job (see
12
+ * `scripts/plugin-drift-check.ts`). The network informs a human's decision to
13
+ * edit an adapter; it never informs a parse. That is the same rule
14
+ * `AGENTS.md` sets for `models.generated.ts`.
15
+ * - **A failed fetch is not a clean report.** Offline must be distinguishable
16
+ * from "no drift", or the check quietly stops checking the day the docs move
17
+ * behind a redirect.
18
+ *
19
+ * The extraction is deliberately conservative. Manifest keys come from JSON code
20
+ * fences — actual manifest examples, not prose — because a regex over English
21
+ * finds every word that happens to be backticked. Paths come from backticked
22
+ * path-shaped tokens, which is noisier, so path findings are leads for a human
23
+ * to confirm rather than assertions. A drift report nobody trusts gets muted,
24
+ * and a muted report is worse than none.
25
+ */
26
+ import { declaredVocabulary } from "./formats/index.js";
27
+ /** Top-level keys of every JSON object in a fenced ```json block. */
28
+ export function manifestKeysIn(markdown) {
29
+ const keys = new Set();
30
+ for (const match of markdown.matchAll(/```(?:jsonc?|json5)\n([\s\S]*?)```/g)) {
31
+ const body = match[1];
32
+ // Strip comments and trailing commas so documentation JSON (which is often
33
+ // neither) still parses.
34
+ const cleaned = body
35
+ .replace(/^\s*\/\/.*$/gm, "")
36
+ .replace(/\/\*[\s\S]*?\*\//g, "")
37
+ .replace(/,(\s*[}\]])/g, "$1");
38
+ try {
39
+ const parsed = JSON.parse(cleaned);
40
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
41
+ for (const key of Object.keys(parsed))
42
+ keys.add(key);
43
+ }
44
+ }
45
+ catch {
46
+ // A fence that does not parse tells us nothing; guessing at its keys with
47
+ // a regex is how a drift report fills up with noise.
48
+ }
49
+ }
50
+ return keys;
51
+ }
52
+ /**
53
+ * Plugin component directories worth noticing on their own, without a file
54
+ * extension to identify them.
55
+ */
56
+ /**
57
+ * Files every repository has. They appear in vendor references as scaffolding
58
+ * examples, never as a surface an adapter should parse.
59
+ */
60
+ const GENERIC_FILES = new Set(["README.md", "CHANGELOG.md", "LICENSE.md", "CLAUDE.md", "AGENTS.md", "package.json"]);
61
+ const COMPONENT_DIR_NAMES = new Set([
62
+ "skills",
63
+ "commands",
64
+ "agents",
65
+ "hooks",
66
+ "themes",
67
+ "workflows",
68
+ "output-styles",
69
+ "monitors",
70
+ "bin",
71
+ "prompts",
72
+ "chatmodes",
73
+ ]);
74
+ /**
75
+ * Backticked tokens that look like a **plugin-relative** file or directory path.
76
+ *
77
+ * Tight on purpose. The first cut of this accepted anything path-shaped and
78
+ * produced twenty findings against the live Claude reference, of which none were
79
+ * real drift: MCP method names (`roots/list`), repo slugs
80
+ * (`anthropics/claude-plugins-community`), workspace paths (`.claude/settings.json`),
81
+ * bare extensions (`.zip`), and example scripts. A drift report with that
82
+ * signal-to-noise ratio gets muted, and a muted report is worse than none — so
83
+ * the filter errs toward missing a real surface rather than crying wolf, and the
84
+ * manifest-key half (which reads parsed JSON, not prose) carries the precision.
85
+ */
86
+ export function pathsIn(markdown) {
87
+ const paths = new Set();
88
+ for (const match of markdown.matchAll(/`([^`\s]+)`/g)) {
89
+ const token = match[1].replace(/^\.\//, "").replace(/\/$/, "");
90
+ if (!/^[\w.@-]+(?:\/[\w.@-]+)*$/.test(token))
91
+ continue;
92
+ // Workspace surfaces, not plugin surfaces: `.claude/settings.json` and
93
+ // friends are §1.3's territory and are not what an adapter parses.
94
+ if (token.startsWith(".claude/") || token.startsWith(".github/workflows"))
95
+ continue;
96
+ if (token.startsWith(".."))
97
+ continue;
98
+ const tail = token.slice(token.lastIndexOf("/") + 1);
99
+ if (GENERIC_FILES.has(tail))
100
+ continue;
101
+ // A real surface is a config file or a component directory. Anything else
102
+ // path-shaped in prose is an example, a slug, or a protocol method.
103
+ const isConfigFile = /\.(json|jsonc|md|toml|ya?ml)$/.test(tail);
104
+ const isComponentDir = COMPONENT_DIR_NAMES.has(tail) || COMPONENT_DIR_NAMES.has(token);
105
+ if (!isConfigFile && !isComponentDir)
106
+ continue;
107
+ // A single token with no separator has to stand on its own: a named config
108
+ // file (`.mcp.json`, `plugin.json`) or a component directory. A bare
109
+ // extension like `.zip` is neither.
110
+ if (!token.includes("/") && !isComponentDir && !/^\.?[\w-]+\.(json|jsonc|md|toml|ya?ml)$/.test(token))
111
+ continue;
112
+ paths.add(token);
113
+ }
114
+ return paths;
115
+ }
116
+ /**
117
+ * Whether a documented path is one we already declare.
118
+ *
119
+ * Suffix match, because references write paths from an example plugin root
120
+ * (`my-plugin/hooks/hooks.json`) while adapters declare them plugin-relative
121
+ * (`hooks/hooks.json`). Comparing the two literally reports every documented
122
+ * example as new.
123
+ */
124
+ function isDeclaredPath(documented, declared) {
125
+ if (declared.has(documented))
126
+ return true;
127
+ for (const known of declared) {
128
+ if (documented === known || documented.endsWith(`/${known}`) || known.endsWith(`/${documented}`))
129
+ return true;
130
+ // A documented directory whose contents we already declare: the reference
131
+ // writes `monitors`, the adapter declares `monitors/monitors.json`.
132
+ if (known.startsWith(`${documented}/`) || documented.startsWith(`${known}/`))
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ /**
138
+ * Diff the fetched references against the declared vocabulary.
139
+ *
140
+ * Everything already declared — modelled, read, or knowingly unsupported — is
141
+ * filtered out, so what remains answers one question: *is the vendor documenting
142
+ * something we have never heard of?*
143
+ */
144
+ export function analyzeDrift(sources) {
145
+ const declared = declaredVocabulary();
146
+ const knownKeys = new Set([...declared.manifestKeys, ...declared.marketplaceKeys]);
147
+ const knownPaths = new Set([...declared.readPaths, ...declared.marketplaceFiles, ...declared.unsupportedSurfaces].map((p) => p.replace(/\\/g, "/").replace(/\/$/, "")));
148
+ const unreachable = [];
149
+ const findings = [];
150
+ const checkedSources = [];
151
+ const seen = new Set();
152
+ for (const source of sources) {
153
+ if (!source.text) {
154
+ unreachable.push({ label: source.label, error: source.error ?? "not fetched" });
155
+ continue;
156
+ }
157
+ checkedSources.push(source.label);
158
+ for (const key of manifestKeysIn(source.text)) {
159
+ if (knownKeys.has(key) || seen.has(`k:${key}`))
160
+ continue;
161
+ seen.add(`k:${key}`);
162
+ findings.push({ kind: "manifest-key", value: key, source: source.label });
163
+ }
164
+ for (const p of pathsIn(source.text)) {
165
+ if (isDeclaredPath(p, knownPaths) || seen.has(`p:${p}`))
166
+ continue;
167
+ seen.add(`p:${p}`);
168
+ findings.push({ kind: "path", value: p, source: source.label });
169
+ }
170
+ }
171
+ // Incomplete is never clean. A partial sweep that reports "no drift" is the
172
+ // failure this tier exists to prevent, one level up.
173
+ return { clean: unreachable.length === 0 && findings.length === 0, unreachable, findings, checkedSources };
174
+ }
175
+ /** Render a report for a CI log or a human-opened issue. */
176
+ export function formatDriftReport(report) {
177
+ const lines = [];
178
+ if (report.checkedSources.length > 0)
179
+ lines.push(`Checked: ${report.checkedSources.join(", ")}`);
180
+ for (const u of report.unreachable) {
181
+ lines.push(`UNREACHABLE ${u.label}: ${u.error} — this report is incomplete.`);
182
+ }
183
+ if (report.findings.length === 0) {
184
+ lines.push(report.unreachable.length > 0 ? "No drift in what could be read." : "No drift.");
185
+ return lines.join("\n");
186
+ }
187
+ lines.push("", `${report.findings.length} candidate(s) documented upstream but not declared here:`);
188
+ for (const f of report.findings) {
189
+ lines.push(` [${f.kind}] ${f.value} (${f.source})`);
190
+ }
191
+ lines.push("", "These are leads, not verdicts — path extraction reads prose and will surface", "examples alongside conventions. Confirm against the reference, then either", "model the surface in the adapter or add it to the knowingly-unsupported list.");
192
+ return lines.join("\n");
193
+ }
194
+ //# sourceMappingURL=drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/drift.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tier 2 — offline drift check (§2.2).\n *\n * The adapters had drifted in fifteen places across two vendors and nothing\n * noticed. The defect was never a missing doc-fetcher; it was a missing\n * *signal*. This module is that signal: diff what the vendors document against\n * what {@link declaredVocabulary} says we understand, and report the gap.\n *\n * Two properties are non-negotiable, both inherited from Tier 0:\n *\n * - **Nothing here runs at runtime.** The fetch happens in a CI job (see\n * `scripts/plugin-drift-check.ts`). The network informs a human's decision to\n * edit an adapter; it never informs a parse. That is the same rule\n * `AGENTS.md` sets for `models.generated.ts`.\n * - **A failed fetch is not a clean report.** Offline must be distinguishable\n * from \"no drift\", or the check quietly stops checking the day the docs move\n * behind a redirect.\n *\n * The extraction is deliberately conservative. Manifest keys come from JSON code\n * fences — actual manifest examples, not prose — because a regex over English\n * finds every word that happens to be backticked. Paths come from backticked\n * path-shaped tokens, which is noisier, so path findings are leads for a human\n * to confirm rather than assertions. A drift report nobody trusts gets muted,\n * and a muted report is worse than none.\n */\n\nimport { declaredVocabulary } from \"./formats/index.js\";\n\nexport interface DriftSource {\n\tlabel: string;\n\turl: string;\n\t/** Page text, or undefined when it could not be fetched. */\n\ttext?: string;\n\terror?: string;\n}\n\nexport interface DriftFinding {\n\tkind: \"manifest-key\" | \"path\";\n\tvalue: string;\n\t/** Which reference page it was seen in. */\n\tsource: string;\n}\n\nexport interface DriftReport {\n\t/** True only when every source was fetched *and* nothing new was found. */\n\tclean: boolean;\n\t/** Sources that could not be fetched. Non-empty means the report is incomplete. */\n\tunreachable: Array<{ label: string; error: string }>;\n\tfindings: DriftFinding[];\n\tcheckedSources: string[];\n}\n\n/** Top-level keys of every JSON object in a fenced ```json block. */\nexport function manifestKeysIn(markdown: string): Set<string> {\n\tconst keys = new Set<string>();\n\tfor (const match of markdown.matchAll(/```(?:jsonc?|json5)\\n([\\s\\S]*?)```/g)) {\n\t\tconst body = match[1];\n\t\t// Strip comments and trailing commas so documentation JSON (which is often\n\t\t// neither) still parses.\n\t\tconst cleaned = body\n\t\t\t.replace(/^\\s*\\/\\/.*$/gm, \"\")\n\t\t\t.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\")\n\t\t\t.replace(/,(\\s*[}\\]])/g, \"$1\");\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cleaned) as unknown;\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\t\tfor (const key of Object.keys(parsed)) keys.add(key);\n\t\t\t}\n\t\t} catch {\n\t\t\t// A fence that does not parse tells us nothing; guessing at its keys with\n\t\t\t// a regex is how a drift report fills up with noise.\n\t\t}\n\t}\n\treturn keys;\n}\n\n/**\n * Plugin component directories worth noticing on their own, without a file\n * extension to identify them.\n */\n/**\n * Files every repository has. They appear in vendor references as scaffolding\n * examples, never as a surface an adapter should parse.\n */\nconst GENERIC_FILES = new Set([\"README.md\", \"CHANGELOG.md\", \"LICENSE.md\", \"CLAUDE.md\", \"AGENTS.md\", \"package.json\"]);\n\nconst COMPONENT_DIR_NAMES = new Set([\n\t\"skills\",\n\t\"commands\",\n\t\"agents\",\n\t\"hooks\",\n\t\"themes\",\n\t\"workflows\",\n\t\"output-styles\",\n\t\"monitors\",\n\t\"bin\",\n\t\"prompts\",\n\t\"chatmodes\",\n]);\n\n/**\n * Backticked tokens that look like a **plugin-relative** file or directory path.\n *\n * Tight on purpose. The first cut of this accepted anything path-shaped and\n * produced twenty findings against the live Claude reference, of which none were\n * real drift: MCP method names (`roots/list`), repo slugs\n * (`anthropics/claude-plugins-community`), workspace paths (`.claude/settings.json`),\n * bare extensions (`.zip`), and example scripts. A drift report with that\n * signal-to-noise ratio gets muted, and a muted report is worse than none — so\n * the filter errs toward missing a real surface rather than crying wolf, and the\n * manifest-key half (which reads parsed JSON, not prose) carries the precision.\n */\nexport function pathsIn(markdown: string): Set<string> {\n\tconst paths = new Set<string>();\n\tfor (const match of markdown.matchAll(/`([^`\\s]+)`/g)) {\n\t\tconst token = match[1].replace(/^\\.\\//, \"\").replace(/\\/$/, \"\");\n\t\tif (!/^[\\w.@-]+(?:\\/[\\w.@-]+)*$/.test(token)) continue;\n\t\t// Workspace surfaces, not plugin surfaces: `.claude/settings.json` and\n\t\t// friends are §1.3's territory and are not what an adapter parses.\n\t\tif (token.startsWith(\".claude/\") || token.startsWith(\".github/workflows\")) continue;\n\t\tif (token.startsWith(\"..\")) continue;\n\n\t\tconst tail = token.slice(token.lastIndexOf(\"/\") + 1);\n\t\tif (GENERIC_FILES.has(tail)) continue;\n\t\t// A real surface is a config file or a component directory. Anything else\n\t\t// path-shaped in prose is an example, a slug, or a protocol method.\n\t\tconst isConfigFile = /\\.(json|jsonc|md|toml|ya?ml)$/.test(tail);\n\t\tconst isComponentDir = COMPONENT_DIR_NAMES.has(tail) || COMPONENT_DIR_NAMES.has(token);\n\t\tif (!isConfigFile && !isComponentDir) continue;\n\t\t// A single token with no separator has to stand on its own: a named config\n\t\t// file (`.mcp.json`, `plugin.json`) or a component directory. A bare\n\t\t// extension like `.zip` is neither.\n\t\tif (!token.includes(\"/\") && !isComponentDir && !/^\\.?[\\w-]+\\.(json|jsonc|md|toml|ya?ml)$/.test(token)) continue;\n\t\tpaths.add(token);\n\t}\n\treturn paths;\n}\n\n/**\n * Whether a documented path is one we already declare.\n *\n * Suffix match, because references write paths from an example plugin root\n * (`my-plugin/hooks/hooks.json`) while adapters declare them plugin-relative\n * (`hooks/hooks.json`). Comparing the two literally reports every documented\n * example as new.\n */\nfunction isDeclaredPath(documented: string, declared: ReadonlySet<string>): boolean {\n\tif (declared.has(documented)) return true;\n\tfor (const known of declared) {\n\t\tif (documented === known || documented.endsWith(`/${known}`) || known.endsWith(`/${documented}`)) return true;\n\t\t// A documented directory whose contents we already declare: the reference\n\t\t// writes `monitors`, the adapter declares `monitors/monitors.json`.\n\t\tif (known.startsWith(`${documented}/`) || documented.startsWith(`${known}/`)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Diff the fetched references against the declared vocabulary.\n *\n * Everything already declared — modelled, read, or knowingly unsupported — is\n * filtered out, so what remains answers one question: *is the vendor documenting\n * something we have never heard of?*\n */\nexport function analyzeDrift(sources: readonly DriftSource[]): DriftReport {\n\tconst declared = declaredVocabulary();\n\tconst knownKeys = new Set([...declared.manifestKeys, ...declared.marketplaceKeys]);\n\tconst knownPaths = new Set(\n\t\t[...declared.readPaths, ...declared.marketplaceFiles, ...declared.unsupportedSurfaces].map((p) =>\n\t\t\tp.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\"),\n\t\t),\n\t);\n\n\tconst unreachable: DriftReport[\"unreachable\"] = [];\n\tconst findings: DriftFinding[] = [];\n\tconst checkedSources: string[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const source of sources) {\n\t\tif (!source.text) {\n\t\t\tunreachable.push({ label: source.label, error: source.error ?? \"not fetched\" });\n\t\t\tcontinue;\n\t\t}\n\t\tcheckedSources.push(source.label);\n\t\tfor (const key of manifestKeysIn(source.text)) {\n\t\t\tif (knownKeys.has(key) || seen.has(`k:${key}`)) continue;\n\t\t\tseen.add(`k:${key}`);\n\t\t\tfindings.push({ kind: \"manifest-key\", value: key, source: source.label });\n\t\t}\n\t\tfor (const p of pathsIn(source.text)) {\n\t\t\tif (isDeclaredPath(p, knownPaths) || seen.has(`p:${p}`)) continue;\n\t\t\tseen.add(`p:${p}`);\n\t\t\tfindings.push({ kind: \"path\", value: p, source: source.label });\n\t\t}\n\t}\n\n\t// Incomplete is never clean. A partial sweep that reports \"no drift\" is the\n\t// failure this tier exists to prevent, one level up.\n\treturn { clean: unreachable.length === 0 && findings.length === 0, unreachable, findings, checkedSources };\n}\n\n/** Render a report for a CI log or a human-opened issue. */\nexport function formatDriftReport(report: DriftReport): string {\n\tconst lines: string[] = [];\n\tif (report.checkedSources.length > 0) lines.push(`Checked: ${report.checkedSources.join(\", \")}`);\n\tfor (const u of report.unreachable) {\n\t\tlines.push(`UNREACHABLE ${u.label}: ${u.error} — this report is incomplete.`);\n\t}\n\tif (report.findings.length === 0) {\n\t\tlines.push(report.unreachable.length > 0 ? \"No drift in what could be read.\" : \"No drift.\");\n\t\treturn lines.join(\"\\n\");\n\t}\n\tlines.push(\"\", `${report.findings.length} candidate(s) documented upstream but not declared here:`);\n\tfor (const f of report.findings) {\n\t\tlines.push(` [${f.kind}] ${f.value} (${f.source})`);\n\t}\n\tlines.push(\n\t\t\"\",\n\t\t\"These are leads, not verdicts — path extraction reads prose and will surface\",\n\t\t\"examples alongside conventions. Confirm against the reference, then either\",\n\t\t\"model the surface in the adapter or add it to the knowingly-unsupported list.\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,YAAY,0CAOvB,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,YAAY,0CAYvB,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n\t// The Claude reference makes the manifest optional for marketplace and\n\t// --plugin-dir plugins; components are auto-discovered and the name comes\n\t// from the directory. Skills-directory plugins are the exception and are\n\t// handled by discoverPlugins, not here.\n\tallowManifestless: true,\n});\n"]}
@@ -14,5 +14,10 @@ export const claudeFormat = createJsonManifestAdapter({
14
14
  precedence: 1,
15
15
  label: "Claude Code (.claude-plugin)",
16
16
  supportsProviders: false,
17
+ // The Claude reference makes the manifest optional for marketplace and
18
+ // --plugin-dir plugins; components are auto-discovered and the name comes
19
+ // from the directory. Skills-directory plugins are the exception and are
20
+ // handled by discoverPlugins, not here.
21
+ allowManifestless: true,
17
22
  });
18
23
  //# sourceMappingURL=claude.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,8BAA8B;IACrC,iBAAiB,EAAE,KAAK;CACxB,CAAC,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,8BAA8B;IACrC,iBAAiB,EAAE,KAAK;IACxB,uEAAuE;IACvE,0EAA0E;IAC1E,yEAAyE;IACzE,wCAAwC;IACxC,iBAAiB,EAAE,IAAI;CACvB,CAAC,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n\t// The Claude reference makes the manifest optional for marketplace and\n\t// --plugin-dir plugins; components are auto-discovered and the name comes\n\t// from the directory. Skills-directory plugins are the exception and are\n\t// handled by discoverPlugins, not here.\n\tallowManifestless: true,\n});\n"]}
@@ -42,5 +42,11 @@
42
42
  * so tracking an upstream change never reaches beyond this adapter.
43
43
  */
44
44
  import type { PluginFormatAdapter } from "./types.js";
45
+ /**
46
+ * Every plugin-relative path this adapter reads, for the Tier 2 drift check
47
+ * (§2.2). Assembled from the constants above rather than restated, so a path
48
+ * added to the reader cannot be missing from the declaration.
49
+ */
50
+ export declare function copilotReadPaths(): string[];
45
51
  export declare const copilotFormat: PluginFormatAdapter;
46
52
  //# sourceMappingURL=copilot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAmBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAiDhF,eAAO,MAAM,aAAa,EAAE,mBAoJ3B,CAAC","sourcesContent":["/**\n * GitHub Copilot format.\n *\n * hoocode's preferred Copilot home is the `.github/plugin/` marker directory —\n * the convention used by the real-world plugins indexed by\n * github/copilot-plugins (e.g. microsoft/work-iq) and the documented location\n * for GitHub-hosted marketplaces. The official Copilot CLI plugin reference\n * (docs.github.com copilot/reference/copilot-cli-reference/cli-plugin-reference,\n * \"File locations\"; verified 2026-07) accepts several locations for both files\n * (`.plugin/`, plugin root, `.github/plugin/`, `.claude-plugin/`), so this\n * adapter reads them all — `.github/plugin/` first, then root, then `.plugin/`,\n * then the legacy hoocode layout — and writes `.github/plugin/`.\n * (`.claude-plugin/` belongs to the Claude adapter, which wins precedence, so a\n * `.claude-plugin`-only directory correctly parses as a Claude plugin; Copilot\n * reads those natively anyway.) The capability tree matches the Claude layout:\n *\n * skills skills/<name>/SKILL.md (or the manifest's `skills` path override)\n * commands .github/prompts/<name>.prompt.md (Copilot's prompt-file convention — the\n * analog of a slash command; a manifest\n * `commands` path or `commands/` dir is read too)\n * subagents agents/<name>.agent.md (Copilot's custom-agent convention — the\n * `.agent.md` suffix and a YAML-list `tools`\n * frontmatter are what Copilot recognizes; the\n * reader still accepts bare `.md` names)\n * hooks hooks.json or hooks/hooks.json ({ description?, hooks: { Event: [...] } })\n * MCP servers .mcp.json ({ mcpServers } | { servers })\n *\n * Workspace-level (non-plugin) conventions, per the current GitHub Copilot\n * customization docs (docs.github.com — agent skills, custom agents, prompt\n * files; verified 2026-07):\n *\n * skills .github/skills/<name>/SKILL.md (agentskills.io open standard)\n * custom agents .github/agents/<name>.agent.md (frontmatter: name, description,\n * tools as a YAML list, model, ...)\n * prompt files .github/prompts/<name>.prompt.md (VS Code / Copilot prompt files)\n *\n * Earlier hoocode releases authored a different Copilot mapping\n * (`.github/copilot-plugin.json` + `.github/prompts/*.prompt.md` +\n * `.github/chatmodes/*.chatmode.md`); the reader still accepts that layout as a\n * legacy fallback so previously authored plugins keep loading. Copilot\n * conventions move quickly; this file is the single place that encodes them,\n * so tracking an upstream change never reaches beyond this adapter.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n\ttoolsYamlList,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter } from \"./types.js\";\n\nconst MARKER_DIR = \".github\";\n/**\n * Manifest probe order: `.github/plugin/plugin.json` first — hoocode's\n * preferred Copilot home, matching the real-world plugins indexed by\n * github/copilot-plugins (e.g. microsoft/work-iq) — then the Copilot CLI\n * reference's other locations (root `plugin.json`, `.plugin/plugin.json`) and\n * hoocode's legacy `.github/copilot-plugin.json`. (`.claude-plugin/plugin.json`,\n * also probed by the CLI, belongs to the Claude adapter.)\n */\nconst MANIFEST_REL_PATHS = [\n\tpath.join(MARKER_DIR, \"plugin\", \"plugin.json\"),\n\t\"plugin.json\",\n\tpath.join(\".plugin\", \"plugin.json\"),\n\tpath.join(MARKER_DIR, \"copilot-plugin.json\"),\n] as const;\n/** Authored manifest location: the preferred `.github/plugin/` home. */\nconst emitManifestRelPath = path.join(MARKER_DIR, \"plugin\", \"plugin.json\");\n\n/**\n * Copilot prompt-file home. `.github/prompts/<name>.prompt.md` is the current\n * Copilot/VS Code prompt-file convention, and hoocode maps a plugin's commands\n * to it (both the writer and this reader), so it is the authored location for\n * commands — not merely a legacy fallback.\n */\nconst PROMPTS_DIR = path.join(MARKER_DIR, \"prompts\");\n// Legacy authored layout (read-only fallbacks).\nconst LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, \"chatmodes\");\nconst LEGACY_MCP_FILE = path.join(MARKER_DIR, \"mcp.json\");\nconst LEGACY_HOOKS_FILE = path.join(MARKER_DIR, \"hooks\", \"hooks.json\");\n\n/** Read a hooks JSON file (either `{ hooks: {...} }` or a bare event map). */\nfunction readHooksFile(root: string, rel: string): NormalizedPlugin[\"hooks\"] {\n\tconst file = path.join(root, rel);\n\tif (!fs.existsSync(file)) return undefined;\n\tconst raw = readJson<{ hooks?: Record<string, unknown> } | Record<string, unknown>>(file);\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst config = (\"hooks\" in raw && raw.hooks ? raw.hooks : raw) as NormalizedPlugin[\"hooks\"];\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction manifestPathFor(root: string): string | undefined {\n\tfor (const rel of MANIFEST_REL_PATHS) {\n\t\tif (readJson(path.join(root, rel)) != null) return path.join(root, rel);\n\t}\n\treturn undefined;\n}\n\nexport const copilotFormat: PluginFormatAdapter = {\n\tid: \"copilot\",\n\tplatform: \"github\",\n\tprecedence: 2,\n\tlabel: \"GitHub Copilot (.github)\",\n\t// .github/plugin/ first (hoocode's preferred Copilot home and the documented\n\t// location for GitHub-hosted marketplaces), then the legacy .github/ spot,\n\t// then the Copilot CLI's other probe locations (root, .plugin/).\n\tmarketplaceFiles: [\n\t\tpath.join(MARKER_DIR, \"plugin\", \"marketplace.json\"),\n\t\tpath.join(MARKER_DIR, \"marketplace.json\"),\n\t\t\"marketplace.json\",\n\t\tpath.join(\".plugin\", \"marketplace.json\"),\n\t],\n\n\tworkspace: {\n\t\troot: MARKER_DIR,\n\t\t// Copilot reads repo skills from .github/skills (also .claude/skills and\n\t\t// .agents/skills, which the sibling adapters cover).\n\t\temitSkill: (s) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t}),\n\t\t// Custom agents take `tools` as a YAML list (not the Claude comma string).\n\t\temitAgent: (a) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"agents\", `${slug(a.name)}.agent.md`),\n\t\t\tcontent: emitMarkdown(\n\t\t\t\t{ name: a.name, description: a.description, tools: toolsYamlList(a.tools), model: a.model },\n\t\t\t\ta.body,\n\t\t\t),\n\t\t}),\n\t\t// The closest Copilot equivalent of a slash command is a prompt file.\n\t\temitCommand: (c) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"prompts\", `${slug(c.name)}.prompt.md`),\n\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t}),\n\t},\n\n\tdetectPlugin(root: string): boolean {\n\t\treturn manifestPathFor(root) !== undefined;\n\t},\n\n\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\tconst manifestPath = manifestPathFor(root);\n\t\tif (!manifestPath) return null;\n\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\tif (!raw) return null;\n\n\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\tif (!id) return null;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tversion: raw.version,\n\t\t\tdescription: raw.description,\n\t\t\tauthor: parseAuthor(raw.author),\n\t\t\troot,\n\t\t\tmanifestPath,\n\t\t\tformat: \"copilot\",\n\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\tsupportPlatform: [\"github\"],\n\t\t\t// Claude-mirror layout (manifest overrides honored), with the legacy\n\t\t\t// prompts/chatmodes locations as read-only fallbacks.\n\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\t// Commands map to Copilot prompt files at `.github/prompts/`; a manifest\n\t\t\t// `commands` override or a conventional `commands/` dir still wins if present.\n\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\") ?? dirIfExists(root, PROMPTS_DIR),\n\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\") ?? dirIfExists(root, LEGACY_CHATMODES_DIR),\n\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\t// Copilot CLI plugins put hooks config at root `hooks.json`; normalizeHooks\n\t\t\t// covers the Claude-mirror `hooks/hooks.json`, then the legacy location.\n\t\t\thooks:\n\t\t\t\tnormalizeHooks(raw.hooks, root) ??\n\t\t\t\treadHooksFile(root, \"hooks.json\") ??\n\t\t\t\treadHooksFile(root, LEGACY_HOOKS_FILE),\n\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, LEGACY_MCP_FILE),\n\t\t\t// Providers are a native-only concept.\n\t\t\tproviders: undefined,\n\t\t};\n\t},\n\n\temit(draft: PluginDraft): EmittedFile[] {\n\t\t// Only the manifest is Copilot-specific — the canonical location is root\n\t\t// `plugin.json` (Copilot CLI spec) — and the capability tree mirrors the\n\t\t// Claude layout, so a plugin authored for both platforms is one tree with a\n\t\t// root manifest for Copilot and a .claude-plugin/ manifest for Claude.\n\t\tconst files: EmittedFile[] = [];\n\n\t\tfiles.push({\n\t\t\tpath: emitManifestRelPath,\n\t\t\tcontent: emitJson({\n\t\t\t\tname: draft.id,\n\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t// Spec: `author` is an object with a required `name`.\n\t\t\t\t...(draft.author ? { author: { name: draft.author } } : {}),\n\t\t\t}),\n\t\t});\n\n\t\tfor (const s of draft.skills ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t});\n\t\t}\n\t\tfor (const c of draft.commands ?? []) {\n\t\t\t// Copilot's analog of a slash command is a prompt file:\n\t\t\t// `.github/prompts/<name>.prompt.md` (docs.github.com prompt files;\n\t\t\t// verified 2026-07) — same target as this adapter's workspace.emitCommand.\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(PROMPTS_DIR, `${slug(c.name)}.prompt.md`),\n\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t});\n\t\t}\n\t\tfor (const a of draft.agents ?? []) {\n\t\t\t// Copilot recognizes custom agents by the `.agent.md` suffix, and takes\n\t\t\t// `tools` as a YAML list (not the Claude comma string) — matching this\n\t\t\t// adapter's workspace.emitAgent and the current GitHub Copilot docs\n\t\t\t// (docs.github.com custom agents / plugins-creating; verified 2026-07).\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.agent.md`),\n\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t{ name: a.name, description: a.description, tools: toolsYamlList(a.tools), model: a.model },\n\t\t\t\t\ta.body,\n\t\t\t\t),\n\t\t\t});\n\t\t}\n\t\tif (draft.mcpServers?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: \".mcp.json\",\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tmcpServers: 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}),\n\t\t\t});\n\t\t}\n\t\tif (draft.hooks?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t});\n\t\t}\n\t\treturn files;\n\t},\n};\n"]}
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAqBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA2DhF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAgB3C;AAED,eAAO,MAAM,aAAa,EAAE,mBA8J3B,CAAC","sourcesContent":["/**\n * GitHub Copilot format.\n *\n * hoocode's preferred Copilot home is the `.github/plugin/` marker directory —\n * the convention used by the real-world plugins indexed by\n * github/copilot-plugins (e.g. microsoft/work-iq) and the documented location\n * for GitHub-hosted marketplaces. The official Copilot CLI plugin reference\n * (docs.github.com copilot/reference/copilot-cli-reference/cli-plugin-reference,\n * \"File locations\"; verified 2026-07) accepts several locations for both files\n * (`.plugin/`, plugin root, `.github/plugin/`, `.claude-plugin/`), so this\n * adapter reads them all — `.github/plugin/` first, then root, then `.plugin/`,\n * then the legacy hoocode layout — and writes `.github/plugin/`.\n * (`.claude-plugin/` belongs to the Claude adapter, which wins precedence, so a\n * `.claude-plugin`-only directory correctly parses as a Claude plugin; Copilot\n * reads those natively anyway.) The capability tree matches the Claude layout:\n *\n * skills skills/<name>/SKILL.md (or the manifest's `skills` path override)\n * commands .github/prompts/<name>.prompt.md (Copilot's prompt-file convention — the\n * analog of a slash command; a manifest\n * `commands` path or `commands/` dir is read too)\n * subagents agents/<name>.agent.md (Copilot's custom-agent convention — the\n * `.agent.md` suffix and a YAML-list `tools`\n * frontmatter are what Copilot recognizes; the\n * reader still accepts bare `.md` names)\n * hooks hooks.json or hooks/hooks.json ({ description?, hooks: { Event: [...] } })\n * MCP servers .mcp.json ({ mcpServers } | { servers })\n *\n * Workspace-level (non-plugin) conventions, per the current GitHub Copilot\n * customization docs (docs.github.com — agent skills, custom agents, prompt\n * files; verified 2026-07):\n *\n * skills .github/skills/<name>/SKILL.md (agentskills.io open standard)\n * custom agents .github/agents/<name>.agent.md (frontmatter: name, description,\n * tools as a YAML list, model, ...)\n * prompt files .github/prompts/<name>.prompt.md (VS Code / Copilot prompt files)\n *\n * Earlier hoocode releases authored a different Copilot mapping\n * (`.github/copilot-plugin.json` + `.github/prompts/*.prompt.md` +\n * `.github/chatmodes/*.chatmode.md`); the reader still accepts that layout as a\n * legacy fallback so previously authored plugins keep loading. Copilot\n * conventions move quickly; this file is the single place that encodes them,\n * so tracking an upstream change never reaches beyond this adapter.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdetectUnsupportedSurfaces,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n\ttoolsYamlList,\n\tunknownManifestFields,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter } from \"./types.js\";\n\nconst MARKER_DIR = \".github\";\n/**\n * Manifest probe order, matching the Copilot CLI reference exactly:\n * `.plugin/` → root → `.github/plugin/` → (`.claude-plugin/`, which belongs to\n * the Claude adapter), with hoocode's legacy `.github/copilot-plugin.json` last.\n *\n * The order is the vendor's, not ours: a plugin carrying more than one manifest\n * must resolve to the same one under hoocode as under Copilot CLI, or the two\n * read different metadata from the same directory.\n */\nconst MANIFEST_REL_PATHS = [\n\tpath.join(\".plugin\", \"plugin.json\"),\n\t\"plugin.json\",\n\tpath.join(MARKER_DIR, \"plugin\", \"plugin.json\"),\n\tpath.join(MARKER_DIR, \"copilot-plugin.json\"),\n] as const;\n/**\n * Authored manifest location: root `plugin.json`, which is what the Copilot CLI\n * plugin-creating guide teaches and probe position #2. An earlier version emitted\n * `.github/plugin/plugin.json` while the comment here claimed root was canonical;\n * the comment was right. (`.github/plugin/` is still *read*, third in the probe\n * order above, since real marketplace-indexed plugins use it.)\n */\nconst emitManifestRelPath = \"plugin.json\";\n\n/**\n * Copilot prompt-file home. `.github/prompts/<name>.prompt.md` is the current\n * Copilot/VS Code prompt-file convention, and hoocode maps a plugin's commands\n * to it (both the writer and this reader), so it is the authored location for\n * commands — not merely a legacy fallback.\n */\nconst PROMPTS_DIR = path.join(MARKER_DIR, \"prompts\");\n// Legacy authored layout (read-only fallback).\nconst LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, \"chatmodes\");\n// Documented alternates, not legacy: the reference lists both locations for each.\nconst ALT_MCP_FILE = path.join(MARKER_DIR, \"mcp.json\");\nconst ALT_HOOKS_FILE = path.join(\"hooks\", \"hooks.json\");\n// hoocode's own older authored layout, still read so those plugins keep loading.\nconst LEGACY_HOOKS_FILE = path.join(MARKER_DIR, \"hooks\", \"hooks.json\");\n\n/** Read a hooks JSON file (either `{ hooks: {...} }` or a bare event map). */\nfunction readHooksFile(root: string, rel: string): NormalizedPlugin[\"hooks\"] {\n\tconst file = path.join(root, rel);\n\tif (!fs.existsSync(file)) return undefined;\n\tconst raw = readJson<{ hooks?: Record<string, unknown> } | Record<string, unknown>>(file);\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst config = (\"hooks\" in raw && raw.hooks ? raw.hooks : raw) as NormalizedPlugin[\"hooks\"];\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction manifestPathFor(root: string): string | undefined {\n\tfor (const rel of MANIFEST_REL_PATHS) {\n\t\tif (readJson(path.join(root, rel)) != null) return path.join(root, rel);\n\t}\n\treturn undefined;\n}\n\n/**\n * Every plugin-relative path this adapter reads, for the Tier 2 drift check\n * (§2.2). Assembled from the constants above rather than restated, so a path\n * added to the reader cannot be missing from the declaration.\n */\nexport function copilotReadPaths(): string[] {\n\treturn [\n\t\t...MANIFEST_REL_PATHS,\n\t\tPROMPTS_DIR,\n\t\tLEGACY_CHATMODES_DIR,\n\t\tALT_MCP_FILE,\n\t\tALT_HOOKS_FILE,\n\t\tLEGACY_HOOKS_FILE,\n\t\t\"skills\",\n\t\t\"agents\",\n\t\t// The `<name>.agent.md` subagent convention, which the reference writes as\n\t\t// a suffix rather than a path.\n\t\t\".agent.md\",\n\t\t\"hooks.json\",\n\t\t\".mcp.json\",\n\t].map((p) => p.replace(/\\\\/g, \"/\"));\n}\n\nexport const copilotFormat: PluginFormatAdapter = {\n\tid: \"copilot\",\n\tplatform: \"github\",\n\tprecedence: 2,\n\tlabel: \"GitHub Copilot (.github)\",\n\t// .github/plugin/ first (hoocode's preferred Copilot home and the documented\n\t// location for GitHub-hosted marketplaces), then the legacy .github/ spot,\n\t// then the Copilot CLI's other probe locations (root, .plugin/).\n\tmarketplaceFiles: [\n\t\tpath.join(MARKER_DIR, \"plugin\", \"marketplace.json\"),\n\t\tpath.join(MARKER_DIR, \"marketplace.json\"),\n\t\t\"marketplace.json\",\n\t\tpath.join(\".plugin\", \"marketplace.json\"),\n\t],\n\n\tworkspace: {\n\t\troot: MARKER_DIR,\n\t\t// Copilot reads repo skills from .github/skills (also .claude/skills and\n\t\t// .agents/skills, which the sibling adapters cover).\n\t\temitSkill: (s) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t}),\n\t\t// Custom agents take `tools` as a YAML list (not the Claude comma string).\n\t\temitAgent: (a) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"agents\", `${slug(a.name)}.agent.md`),\n\t\t\tcontent: emitMarkdown(\n\t\t\t\t{ name: a.name, description: a.description, tools: toolsYamlList(a.tools), model: a.model },\n\t\t\t\ta.body,\n\t\t\t),\n\t\t}),\n\t\t// The closest Copilot equivalent of a slash command is a prompt file.\n\t\temitCommand: (c) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"prompts\", `${slug(c.name)}.prompt.md`),\n\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t}),\n\t},\n\n\thasManifest(root: string): boolean {\n\t\treturn manifestPathFor(root) !== undefined;\n\t},\n\n\tdetectPlugin(root: string): boolean {\n\t\treturn manifestPathFor(root) !== undefined;\n\t},\n\n\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\tconst manifestPath = manifestPathFor(root);\n\t\tif (!manifestPath) return null;\n\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\tif (!raw) return null;\n\n\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\tif (!id) return null;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tversion: raw.version,\n\t\t\tdescription: raw.description,\n\t\t\tauthor: parseAuthor(raw.author),\n\t\t\troot,\n\t\t\tmanifestPath,\n\t\t\tformat: \"copilot\",\n\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\tsupportPlatform: [\"github\"],\n\t\t\t// Claude-mirror layout (manifest overrides honored), with the legacy\n\t\t\t// prompts/chatmodes locations as read-only fallbacks.\n\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\t// Commands map to Copilot prompt files at `.github/prompts/`; a manifest\n\t\t\t// `commands` override or a conventional `commands/` dir still wins if present.\n\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\") ?? dirIfExists(root, PROMPTS_DIR),\n\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\") ?? dirIfExists(root, LEGACY_CHATMODES_DIR),\n\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\t// Manifest first, then the reference's two locations (root `hooks.json`\n\t\t\t// and `hooks/hooks.json`), then hoocode's legacy `.github/hooks/`.\n\t\t\thooks:\n\t\t\t\tnormalizeHooks(raw.hooks, root) ??\n\t\t\t\treadHooksFile(root, \"hooks.json\") ??\n\t\t\t\treadHooksFile(root, ALT_HOOKS_FILE) ??\n\t\t\t\treadHooksFile(root, LEGACY_HOOKS_FILE),\n\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, ALT_MCP_FILE),\n\t\t\t// Providers are a native-only concept.\n\t\t\tproviders: undefined,\n\t\t\t// `extensions` and `lspServers` are real Copilot manifest keys we do not\n\t\t\t// model; carried here so an edit cannot drop them.\n\t\t\tunknownFields: unknownManifestFields(raw),\n\t\t\tunsupportedSurfaces: detectUnsupportedSurfaces(root),\n\t\t};\n\t},\n\n\temit(draft: PluginDraft): EmittedFile[] {\n\t\t// Only the manifest is Copilot-specific — the canonical location is root\n\t\t// `plugin.json` (Copilot CLI spec) — and the capability tree mirrors the\n\t\t// Claude layout, so a plugin authored for both platforms is one tree with a\n\t\t// root manifest for Copilot and a .claude-plugin/ manifest for Claude.\n\t\tconst files: EmittedFile[] = [];\n\n\t\tfiles.push({\n\t\t\tpath: emitManifestRelPath,\n\t\t\tcontent: emitJson({\n\t\t\t\t...(draft.unknownFields ?? {}),\n\t\t\t\tname: draft.id,\n\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t// Spec: `author` is an object with a required `name`.\n\t\t\t\t...(draft.author ? { author: { name: draft.author } } : {}),\n\t\t\t}),\n\t\t});\n\n\t\tfor (const s of draft.skills ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t});\n\t\t}\n\t\tfor (const c of draft.commands ?? []) {\n\t\t\t// Copilot's analog of a slash command is a prompt file:\n\t\t\t// `.github/prompts/<name>.prompt.md` (docs.github.com prompt files;\n\t\t\t// verified 2026-07) — same target as this adapter's workspace.emitCommand.\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(PROMPTS_DIR, `${slug(c.name)}.prompt.md`),\n\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t});\n\t\t}\n\t\tfor (const a of draft.agents ?? []) {\n\t\t\t// Copilot recognizes custom agents by the `.agent.md` suffix, and takes\n\t\t\t// `tools` as a YAML list (not the Claude comma string) — matching this\n\t\t\t// adapter's workspace.emitAgent and the current GitHub Copilot docs\n\t\t\t// (docs.github.com custom agents / plugins-creating; verified 2026-07).\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.agent.md`),\n\t\t\t\tcontent: emitMarkdown(\n\t\t\t\t\t{ name: a.name, description: a.description, tools: toolsYamlList(a.tools), model: a.model },\n\t\t\t\t\ta.body,\n\t\t\t\t),\n\t\t\t});\n\t\t}\n\t\tif (draft.mcpServers?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: \".mcp.json\",\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tmcpServers: 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}),\n\t\t\t});\n\t\t}\n\t\tif (draft.hooks?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t});\n\t\t}\n\t\treturn files;\n\t},\n};\n"]}
@@ -43,24 +43,31 @@
43
43
  */
44
44
  import * as fs from "node:fs";
45
45
  import * as path from "node:path";
46
- import { authoredHooksToConfig, dirIfExists, emitJson, emitMarkdown, normalizeHooks, normalizeMcp, parseAuthor, readJson, resolveCapabilityDir, slug, toolsYamlList, } from "./shared.js";
46
+ import { authoredHooksToConfig, detectUnsupportedSurfaces, dirIfExists, emitJson, emitMarkdown, normalizeHooks, normalizeMcp, parseAuthor, readJson, resolveCapabilityDir, slug, toolsYamlList, unknownManifestFields, } from "./shared.js";
47
47
  const MARKER_DIR = ".github";
48
48
  /**
49
- * Manifest probe order: `.github/plugin/plugin.json` first hoocode's
50
- * preferred Copilot home, matching the real-world plugins indexed by
51
- * github/copilot-plugins (e.g. microsoft/work-iq) then the Copilot CLI
52
- * reference's other locations (root `plugin.json`, `.plugin/plugin.json`) and
53
- * hoocode's legacy `.github/copilot-plugin.json`. (`.claude-plugin/plugin.json`,
54
- * also probed by the CLI, belongs to the Claude adapter.)
49
+ * Manifest probe order, matching the Copilot CLI reference exactly:
50
+ * `.plugin/` root `.github/plugin/` → (`.claude-plugin/`, which belongs to
51
+ * the Claude adapter), with hoocode's legacy `.github/copilot-plugin.json` last.
52
+ *
53
+ * The order is the vendor's, not ours: a plugin carrying more than one manifest
54
+ * must resolve to the same one under hoocode as under Copilot CLI, or the two
55
+ * read different metadata from the same directory.
55
56
  */
56
57
  const MANIFEST_REL_PATHS = [
57
- path.join(MARKER_DIR, "plugin", "plugin.json"),
58
- "plugin.json",
59
58
  path.join(".plugin", "plugin.json"),
59
+ "plugin.json",
60
+ path.join(MARKER_DIR, "plugin", "plugin.json"),
60
61
  path.join(MARKER_DIR, "copilot-plugin.json"),
61
62
  ];
62
- /** Authored manifest location: the preferred `.github/plugin/` home. */
63
- const emitManifestRelPath = path.join(MARKER_DIR, "plugin", "plugin.json");
63
+ /**
64
+ * Authored manifest location: root `plugin.json`, which is what the Copilot CLI
65
+ * plugin-creating guide teaches and probe position #2. An earlier version emitted
66
+ * `.github/plugin/plugin.json` while the comment here claimed root was canonical;
67
+ * the comment was right. (`.github/plugin/` is still *read*, third in the probe
68
+ * order above, since real marketplace-indexed plugins use it.)
69
+ */
70
+ const emitManifestRelPath = "plugin.json";
64
71
  /**
65
72
  * Copilot prompt-file home. `.github/prompts/<name>.prompt.md` is the current
66
73
  * Copilot/VS Code prompt-file convention, and hoocode maps a plugin's commands
@@ -68,9 +75,12 @@ const emitManifestRelPath = path.join(MARKER_DIR, "plugin", "plugin.json");
68
75
  * commands — not merely a legacy fallback.
69
76
  */
70
77
  const PROMPTS_DIR = path.join(MARKER_DIR, "prompts");
71
- // Legacy authored layout (read-only fallbacks).
78
+ // Legacy authored layout (read-only fallback).
72
79
  const LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, "chatmodes");
73
- const LEGACY_MCP_FILE = path.join(MARKER_DIR, "mcp.json");
80
+ // Documented alternates, not legacy: the reference lists both locations for each.
81
+ const ALT_MCP_FILE = path.join(MARKER_DIR, "mcp.json");
82
+ const ALT_HOOKS_FILE = path.join("hooks", "hooks.json");
83
+ // hoocode's own older authored layout, still read so those plugins keep loading.
74
84
  const LEGACY_HOOKS_FILE = path.join(MARKER_DIR, "hooks", "hooks.json");
75
85
  /** Read a hooks JSON file (either `{ hooks: {...} }` or a bare event map). */
76
86
  function readHooksFile(root, rel) {
@@ -90,6 +100,28 @@ function manifestPathFor(root) {
90
100
  }
91
101
  return undefined;
92
102
  }
103
+ /**
104
+ * Every plugin-relative path this adapter reads, for the Tier 2 drift check
105
+ * (§2.2). Assembled from the constants above rather than restated, so a path
106
+ * added to the reader cannot be missing from the declaration.
107
+ */
108
+ export function copilotReadPaths() {
109
+ return [
110
+ ...MANIFEST_REL_PATHS,
111
+ PROMPTS_DIR,
112
+ LEGACY_CHATMODES_DIR,
113
+ ALT_MCP_FILE,
114
+ ALT_HOOKS_FILE,
115
+ LEGACY_HOOKS_FILE,
116
+ "skills",
117
+ "agents",
118
+ // The `<name>.agent.md` subagent convention, which the reference writes as
119
+ // a suffix rather than a path.
120
+ ".agent.md",
121
+ "hooks.json",
122
+ ".mcp.json",
123
+ ].map((p) => p.replace(/\\/g, "/"));
124
+ }
93
125
  export const copilotFormat = {
94
126
  id: "copilot",
95
127
  platform: "github",
@@ -123,6 +155,9 @@ export const copilotFormat = {
123
155
  content: emitMarkdown({ description: c.description }, c.body),
124
156
  }),
125
157
  },
158
+ hasManifest(root) {
159
+ return manifestPathFor(root) !== undefined;
160
+ },
126
161
  detectPlugin(root) {
127
162
  return manifestPathFor(root) !== undefined;
128
163
  },
@@ -154,14 +189,19 @@ export const copilotFormat = {
154
189
  commandsDir: resolveCapabilityDir(root, raw.commands, "commands") ?? dirIfExists(root, PROMPTS_DIR),
155
190
  agentsDir: resolveCapabilityDir(root, raw.agents, "agents") ?? dirIfExists(root, LEGACY_CHATMODES_DIR),
156
191
  themesDir: resolveCapabilityDir(root, raw.themes, "themes"),
157
- // Copilot CLI plugins put hooks config at root `hooks.json`; normalizeHooks
158
- // covers the Claude-mirror `hooks/hooks.json`, then the legacy location.
192
+ // Manifest first, then the reference's two locations (root `hooks.json`
193
+ // and `hooks/hooks.json`), then hoocode's legacy `.github/hooks/`.
159
194
  hooks: normalizeHooks(raw.hooks, root) ??
160
195
  readHooksFile(root, "hooks.json") ??
196
+ readHooksFile(root, ALT_HOOKS_FILE) ??
161
197
  readHooksFile(root, LEGACY_HOOKS_FILE),
162
- mcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, LEGACY_MCP_FILE),
198
+ mcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, ALT_MCP_FILE),
163
199
  // Providers are a native-only concept.
164
200
  providers: undefined,
201
+ // `extensions` and `lspServers` are real Copilot manifest keys we do not
202
+ // model; carried here so an edit cannot drop them.
203
+ unknownFields: unknownManifestFields(raw),
204
+ unsupportedSurfaces: detectUnsupportedSurfaces(root),
165
205
  };
166
206
  },
167
207
  emit(draft) {
@@ -173,6 +213,7 @@ export const copilotFormat = {
173
213
  files.push({
174
214
  path: emitManifestRelPath,
175
215
  content: emitJson({
216
+ ...(draft.unknownFields ?? {}),
176
217
  name: draft.id,
177
218
  ...(draft.version ? { version: draft.version } : {}),
178
219
  ...(draft.description ? { description: draft.description } : {}),