@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.
- package/CHANGELOG.md +31 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -7
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/capabilities/deferral.d.ts +78 -0
- package/dist/core/capabilities/deferral.d.ts.map +1 -0
- package/dist/core/capabilities/deferral.js +99 -0
- package/dist/core/capabilities/deferral.js.map +1 -0
- package/dist/core/capabilities/dense.d.ts +51 -0
- package/dist/core/capabilities/dense.d.ts.map +1 -0
- package/dist/core/capabilities/dense.js +121 -0
- package/dist/core/capabilities/dense.js.map +1 -0
- package/dist/core/capabilities/lexical.d.ts +43 -0
- package/dist/core/capabilities/lexical.d.ts.map +1 -0
- package/dist/core/capabilities/lexical.js +104 -0
- package/dist/core/capabilities/lexical.js.map +1 -0
- package/dist/core/capabilities/registry.d.ts +56 -0
- package/dist/core/capabilities/registry.d.ts.map +1 -0
- package/dist/core/capabilities/registry.js +0 -0
- package/dist/core/capabilities/registry.js.map +1 -0
- package/dist/core/capabilities/search.d.ts +47 -0
- package/dist/core/capabilities/search.d.ts.map +1 -0
- package/dist/core/capabilities/search.js +82 -0
- package/dist/core/capabilities/search.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +27 -3
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +61 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +56 -9
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
- package/dist/core/extensions/plugins/authoring.js +0 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -1
- package/dist/core/extensions/plugins/drift.d.ts +75 -0
- package/dist/core/extensions/plugins/drift.d.ts.map +1 -0
- package/dist/core/extensions/plugins/drift.js +194 -0
- package/dist/core/extensions/plugins/drift.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/claude.js +5 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.d.ts +6 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.js +57 -16
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
- package/dist/core/extensions/plugins/formats/index.d.ts +39 -1
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/index.js +90 -13
- package/dist/core/extensions/plugins/formats/index.js.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +14 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.js +37 -4
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts +48 -24
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.js +62 -30
- package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.d.ts +22 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.js +81 -4
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
- package/dist/core/extensions/plugins/formats/types.d.ts +10 -1
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/types.js.map +1 -1
- package/dist/core/extensions/plugins/gates.d.ts +64 -0
- package/dist/core/extensions/plugins/gates.d.ts.map +1 -0
- package/dist/core/extensions/plugins/gates.js +297 -0
- package/dist/core/extensions/plugins/gates.js.map +1 -0
- package/dist/core/extensions/plugins/hooks-bridge.d.ts +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.js +12 -8
- package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts +36 -5
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +66 -15
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +43 -24
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +167 -54
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/locations.d.ts +84 -0
- package/dist/core/extensions/plugins/locations.d.ts.map +1 -0
- package/dist/core/extensions/plugins/locations.js +137 -0
- package/dist/core/extensions/plugins/locations.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +21 -1
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +2 -2
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/packaging.d.ts +115 -0
- package/dist/core/extensions/plugins/packaging.d.ts.map +1 -0
- package/dist/core/extensions/plugins/packaging.js +362 -0
- package/dist/core/extensions/plugins/packaging.js.map +1 -0
- package/dist/core/extensions/plugins/smoke.d.ts +33 -0
- package/dist/core/extensions/plugins/smoke.d.ts.map +1 -0
- package/dist/core/extensions/plugins/smoke.js +231 -0
- package/dist/core/extensions/plugins/smoke.js.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts +136 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.js +0 -0
- package/dist/core/extensions/plugins/trigger-eval.js.map +1 -0
- package/dist/core/package-manager.d.ts +6 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +15 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/skills.d.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +23 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/tools/package-plugin.d.ts +19 -0
- package/dist/core/tools/package-plugin.d.ts.map +1 -0
- package/dist/core/tools/package-plugin.js +81 -0
- package/dist/core/tools/package-plugin.js.map +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
- package/dist/core/tools/plugin-tool-names.js +6 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -1
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +48 -7
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +49 -21
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/webfetch.d.ts +3 -1
- package/dist/core/tools/webfetch.d.ts.map +1 -1
- package/dist/core/tools/webfetch.js +8 -2
- package/dist/core/tools/webfetch.js.map +1 -1
- package/dist/core/tools/websearch.d.ts +2 -0
- package/dist/core/tools/websearch.d.ts.map +1 -1
- package/dist/core/tools/websearch.js +2 -1
- package/dist/core/tools/websearch.js.map +1 -1
- package/dist/core/tools/webtools-shared.d.ts +23 -2
- package/dist/core/tools/webtools-shared.d.ts.map +1 -1
- package/dist/core/tools/webtools-shared.js +59 -9
- package/dist/core/tools/webtools-shared.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +14 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +77 -18
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +39 -2
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -1
- package/dist/extensions/core/mcp-deferred.js +74 -10
- package/dist/extensions/core/mcp-deferred.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +52 -9
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts +3 -3
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +9 -9
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +25 -7
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +8 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +12 -3
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/providers.md +11 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,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,
|
|
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;
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
/**
|
|
63
|
-
|
|
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
|
|
78
|
+
// Legacy authored layout (read-only fallback).
|
|
72
79
|
const LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, "chatmodes");
|
|
73
|
-
|
|
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
|
-
//
|
|
158
|
-
//
|
|
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,
|
|
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 } : {}),
|