@kolisachint/hoocode-agent 0.5.11 → 0.5.13

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 (55) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/extensions/loader.d.ts +18 -6
  3. package/dist/core/extensions/loader.d.ts.map +1 -1
  4. package/dist/core/extensions/loader.js +35 -19
  5. package/dist/core/extensions/loader.js.map +1 -1
  6. package/dist/core/extensions/plugins/install.d.ts +51 -10
  7. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  8. package/dist/core/extensions/plugins/install.js +178 -31
  9. package/dist/core/extensions/plugins/install.js.map +1 -1
  10. package/dist/core/extensions/plugins/listing.d.ts +8 -2
  11. package/dist/core/extensions/plugins/listing.d.ts.map +1 -1
  12. package/dist/core/extensions/plugins/listing.js +29 -5
  13. package/dist/core/extensions/plugins/listing.js.map +1 -1
  14. package/dist/core/extensions/plugins/locations.d.ts +62 -12
  15. package/dist/core/extensions/plugins/locations.d.ts.map +1 -1
  16. package/dist/core/extensions/plugins/locations.js +74 -18
  17. package/dist/core/extensions/plugins/locations.js.map +1 -1
  18. package/dist/core/extensions/plugins/marketplace.d.ts +29 -3
  19. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
  20. package/dist/core/extensions/plugins/marketplace.js +40 -7
  21. package/dist/core/extensions/plugins/marketplace.js.map +1 -1
  22. package/dist/core/extensions/plugins/trust.d.ts +56 -0
  23. package/dist/core/extensions/plugins/trust.d.ts.map +1 -0
  24. package/dist/core/extensions/plugins/trust.js +88 -0
  25. package/dist/core/extensions/plugins/trust.js.map +1 -0
  26. package/dist/core/settings-defaults.d.ts +1 -0
  27. package/dist/core/settings-defaults.d.ts.map +1 -1
  28. package/dist/core/settings-defaults.js +1 -0
  29. package/dist/core/settings-defaults.js.map +1 -1
  30. package/dist/core/settings-manager.d.ts +7 -0
  31. package/dist/core/settings-manager.d.ts.map +1 -1
  32. package/dist/core/settings-manager.js +14 -0
  33. package/dist/core/settings-manager.js.map +1 -1
  34. package/dist/core/settings-types.d.ts +1 -0
  35. package/dist/core/settings-types.d.ts.map +1 -1
  36. package/dist/core/settings-types.js.map +1 -1
  37. package/dist/core/tools/plugins.d.ts.map +1 -1
  38. package/dist/core/tools/plugins.js +18 -3
  39. package/dist/core/tools/plugins.js.map +1 -1
  40. package/dist/extensions/core/marketplace.d.ts +7 -3
  41. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  42. package/dist/extensions/core/marketplace.js +103 -11
  43. package/dist/extensions/core/marketplace.js.map +1 -1
  44. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  45. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  46. package/dist/modes/interactive/components/settings-selector.js +15 -2
  47. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  48. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  49. package/dist/modes/interactive/interactive-mode.js +6 -0
  50. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  51. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  52. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  53. package/examples/extensions/sandbox/package.json +1 -1
  54. package/examples/extensions/with-deps/package.json +1 -1
  55. package/package.json +4 -4
@@ -29,6 +29,12 @@ export declare function pluginCapabilities(plugin: NormalizedPlugin): string[];
29
29
  export declare function availablePluginGroups(plugins: readonly AvailablePlugin[], options?: {
30
30
  installed?: ReadonlySet<string>;
31
31
  }): ListGroup[];
32
- /** Installed plugins as rows: id@version, platforms, format, capabilities. */
33
- export declare function installedPluginRows(plugins: readonly NormalizedPlugin[]): ListRow[];
32
+ export interface InstalledRowOptions {
33
+ /** Working directory, used to place each plugin in a scope. */
34
+ cwd?: string;
35
+ /** Whether `cwd` is a trusted workspace; decides if executables actually load. */
36
+ trusted?: boolean;
37
+ }
38
+ /** Installed plugins as rows: id@version, platforms, format, scope, capabilities. */
39
+ export declare function installedPluginRows(plugins: readonly NormalizedPlugin[], options?: InstalledRowOptions): ListRow[];
34
40
  //# sourceMappingURL=listing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"listing.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/listing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAUrE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAO,GAC/C,SAAS,EAAE,CAab;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,EAAE,CAWnF","sourcesContent":["/**\n * Row shapes for the plugin listings, shared by every surface that prints one.\n *\n * `/plugin list`, `SearchPlugins` and `ListPlugins` describe the same objects,\n * and each used to build its own `name [a, b] — description` string, so the same\n * plugin printed three ways and the model's view never quite matched the user's.\n * They all go through here now; only the styling and the width differ.\n */\n\nimport type { ListGroup, ListRow } from \"../../format-list.js\";\nimport type { AvailablePlugin } from \"./install.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\n\n/** Human-readable source for an available plugin (a URL, a path, an npm spec). */\nexport function formatPluginSource(source: AvailablePlugin[\"source\"]): string {\n\tif (typeof source === \"string\") return source;\n\tif (source.source === \"url\") return source.url;\n\treturn `${source.url}/${source.path}`;\n}\n\n/**\n * Capability classes a plugin actually ships.\n *\n * Covers every capability `NormalizedPlugin` can carry — themes and providers\n * included, which the old `ListPlugins` inline list omitted, so a plugin that\n * shipped only a theme or only a provider reported no capabilities at all.\n */\nexport function pluginCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"agents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && \"mcp\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((value): value is string => typeof value === \"string\");\n}\n\n/**\n * Available plugins, grouped by the marketplace offering them.\n *\n * Grouping is what makes provenance legible: a flat list cannot answer \"where\n * would this come from if I installed it\", which is the one question that\n * matters at a trust boundary. Marketplace order is preserved from the caller.\n */\nexport function availablePluginGroups(\n\tplugins: readonly AvailablePlugin[],\n\toptions: { installed?: ReadonlySet<string> } = {},\n): ListGroup[] {\n\tconst groups = new Map<string, ListRow[]>();\n\tfor (const plugin of plugins) {\n\t\tconst rows = groups.get(plugin.marketplaceName) ?? [];\n\t\trows.push({\n\t\t\tname: plugin.name,\n\t\t\tmarker: options.installed?.has(plugin.name) ? \"✓\" : undefined,\n\t\t\tfacts: [plugin.supportPlatform.join(\"/\"), plugin.sourceKind],\n\t\t\tdetail: plugin.description ?? formatPluginSource(plugin.source),\n\t\t});\n\t\tgroups.set(plugin.marketplaceName, rows);\n\t}\n\treturn Array.from(groups, ([title, rows]) => ({ title, rows }));\n}\n\n/** Installed plugins as rows: id@version, platforms, format, capabilities. */\nexport function installedPluginRows(plugins: readonly NormalizedPlugin[]): ListRow[] {\n\treturn plugins.map((plugin) => {\n\t\tconst capabilities = pluginCapabilities(plugin);\n\t\treturn {\n\t\t\tname: `${plugin.id}${plugin.version ? `@${plugin.version}` : \"\"}`,\n\t\t\t// The manifest format is parenthesised because it is usually the same\n\t\t\t// word as the platform (\"claude/github · claude\" reads as a stutter).\n\t\t\tfacts: [plugin.supportPlatform.join(\"/\"), `(${plugin.format})`, capabilities.join(\", \") || \"no capabilities\"],\n\t\t\tdetail: plugin.description,\n\t\t};\n\t});\n}\n"]}
1
+ {"version":3,"file":"listing.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/listing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,MAAM,CAO5E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAUrE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAO,GAC/C,SAAS,EAAE,CAoBb;AAED,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,OAAO,GAAE,mBAAwB,GAC/B,OAAO,EAAE,CAuBX","sourcesContent":["/**\n * Row shapes for the plugin listings, shared by every surface that prints one.\n *\n * `/plugin list`, `SearchPlugins` and `ListPlugins` describe the same objects,\n * and each used to build its own `name [a, b] — description` string, so the same\n * plugin printed three ways and the model's view never quite matched the user's.\n * They all go through here now; only the styling and the width differ.\n */\n\nimport type { ListGroup, ListRow } from \"../../format-list.js\";\nimport type { AvailablePlugin } from \"./install.js\";\nimport { pluginScopeOf } from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\n\n/** Human-readable source for an available plugin (a URL, a path, an npm spec). */\nexport function formatPluginSource(source: AvailablePlugin[\"source\"]): string {\n\tif (typeof source === \"string\") return source;\n\tif (source.source === \"npm\") return source.package;\n\t// `archive` carries a url like `url` does; without this it printed\n\t// `https://…/p.zip/undefined` through the git-subdir branch below.\n\tif (source.source === \"url\" || source.source === \"archive\") return source.url;\n\treturn `${source.url}/${source.path}`;\n}\n\n/**\n * Capability classes a plugin actually ships.\n *\n * Covers every capability `NormalizedPlugin` can carry — themes and providers\n * included, which the old `ListPlugins` inline list omitted, so a plugin that\n * shipped only a theme or only a provider reported no capabilities at all.\n */\nexport function pluginCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"agents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && \"mcp\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((value): value is string => typeof value === \"string\");\n}\n\n/**\n * Available plugins, grouped by the marketplace offering them.\n *\n * Grouping is what makes provenance legible: a flat list cannot answer \"where\n * would this come from if I installed it\", which is the one question that\n * matters at a trust boundary. Marketplace order is preserved from the caller.\n */\nexport function availablePluginGroups(\n\tplugins: readonly AvailablePlugin[],\n\toptions: { installed?: ReadonlySet<string> } = {},\n): ListGroup[] {\n\tconst groups = new Map<string, ListRow[]>();\n\tfor (const plugin of plugins) {\n\t\tconst rows = groups.get(plugin.marketplaceName) ?? [];\n\t\t// npm and archive entries are catalogued but not fetchable here, and saying\n\t\t// so in the row is cheaper than an InstallPlugin round trip that only ends\n\t\t// in the same message.\n\t\tconst installable = plugin.sourceKind !== \"npm\" && plugin.sourceKind !== \"archive\";\n\t\trows.push({\n\t\t\tname: plugin.name,\n\t\t\tmarker: options.installed?.has(plugin.name) ? \"✓\" : undefined,\n\t\t\tfacts: [\n\t\t\t\tplugin.supportPlatform.join(\"/\"),\n\t\t\t\tinstallable ? plugin.sourceKind : `${plugin.sourceKind}, not installable`,\n\t\t\t],\n\t\t\tdetail: plugin.description ?? formatPluginSource(plugin.source),\n\t\t});\n\t\tgroups.set(plugin.marketplaceName, rows);\n\t}\n\treturn Array.from(groups, ([title, rows]) => ({ title, rows }));\n}\n\nexport interface InstalledRowOptions {\n\t/** Working directory, used to place each plugin in a scope. */\n\tcwd?: string;\n\t/** Whether `cwd` is a trusted workspace; decides if executables actually load. */\n\ttrusted?: boolean;\n}\n\n/** Installed plugins as rows: id@version, platforms, format, scope, capabilities. */\nexport function installedPluginRows(\n\tplugins: readonly NormalizedPlugin[],\n\toptions: InstalledRowOptions = {},\n): ListRow[] {\n\treturn plugins.map((plugin) => {\n\t\tconst capabilities = pluginCapabilities(plugin);\n\t\t// Scope answers \"why is this loaded, and who else gets it\" — the plugin's\n\t\t// own path always knew, and nothing surfaced it.\n\t\tconst scope = options.cwd ? pluginScopeOf(plugin.root, options.cwd) : undefined;\n\t\t// A working-tree plugin's executables load only in a trusted workspace, so\n\t\t// listing \"hooks\" unqualified would overstate what is actually running.\n\t\tconst withheld = scope && scope !== \"user\" && !options.trusted && (plugin.hooks || plugin.mcpServers);\n\t\treturn {\n\t\t\tname: `${plugin.id}${plugin.version ? `@${plugin.version}` : \"\"}`,\n\t\t\t// The manifest format is parenthesised because it is usually the same\n\t\t\t// word as the platform (\"claude/github · claude\" reads as a stutter).\n\t\t\tfacts: [\n\t\t\t\tplugin.supportPlatform.join(\"/\"),\n\t\t\t\t`(${plugin.format})`,\n\t\t\t\t...(scope ? [scope] : []),\n\t\t\t\tcapabilities.join(\", \") || \"no capabilities\",\n\t\t\t\t...(withheld ? [\"hooks/mcp withheld, workspace not trusted\"] : []),\n\t\t\t],\n\t\t\tdetail: plugin.description,\n\t\t};\n\t});\n}\n"]}
@@ -6,11 +6,16 @@
6
6
  * plugin printed three ways and the model's view never quite matched the user's.
7
7
  * They all go through here now; only the styling and the width differ.
8
8
  */
9
+ import { pluginScopeOf } from "./locations.js";
9
10
  /** Human-readable source for an available plugin (a URL, a path, an npm spec). */
10
11
  export function formatPluginSource(source) {
11
12
  if (typeof source === "string")
12
13
  return source;
13
- if (source.source === "url")
14
+ if (source.source === "npm")
15
+ return source.package;
16
+ // `archive` carries a url like `url` does; without this it printed
17
+ // `https://…/p.zip/undefined` through the git-subdir branch below.
18
+ if (source.source === "url" || source.source === "archive")
14
19
  return source.url;
15
20
  return `${source.url}/${source.path}`;
16
21
  }
@@ -43,25 +48,44 @@ export function availablePluginGroups(plugins, options = {}) {
43
48
  const groups = new Map();
44
49
  for (const plugin of plugins) {
45
50
  const rows = groups.get(plugin.marketplaceName) ?? [];
51
+ // npm and archive entries are catalogued but not fetchable here, and saying
52
+ // so in the row is cheaper than an InstallPlugin round trip that only ends
53
+ // in the same message.
54
+ const installable = plugin.sourceKind !== "npm" && plugin.sourceKind !== "archive";
46
55
  rows.push({
47
56
  name: plugin.name,
48
57
  marker: options.installed?.has(plugin.name) ? "✓" : undefined,
49
- facts: [plugin.supportPlatform.join("/"), plugin.sourceKind],
58
+ facts: [
59
+ plugin.supportPlatform.join("/"),
60
+ installable ? plugin.sourceKind : `${plugin.sourceKind}, not installable`,
61
+ ],
50
62
  detail: plugin.description ?? formatPluginSource(plugin.source),
51
63
  });
52
64
  groups.set(plugin.marketplaceName, rows);
53
65
  }
54
66
  return Array.from(groups, ([title, rows]) => ({ title, rows }));
55
67
  }
56
- /** Installed plugins as rows: id@version, platforms, format, capabilities. */
57
- export function installedPluginRows(plugins) {
68
+ /** Installed plugins as rows: id@version, platforms, format, scope, capabilities. */
69
+ export function installedPluginRows(plugins, options = {}) {
58
70
  return plugins.map((plugin) => {
59
71
  const capabilities = pluginCapabilities(plugin);
72
+ // Scope answers "why is this loaded, and who else gets it" — the plugin's
73
+ // own path always knew, and nothing surfaced it.
74
+ const scope = options.cwd ? pluginScopeOf(plugin.root, options.cwd) : undefined;
75
+ // A working-tree plugin's executables load only in a trusted workspace, so
76
+ // listing "hooks" unqualified would overstate what is actually running.
77
+ const withheld = scope && scope !== "user" && !options.trusted && (plugin.hooks || plugin.mcpServers);
60
78
  return {
61
79
  name: `${plugin.id}${plugin.version ? `@${plugin.version}` : ""}`,
62
80
  // The manifest format is parenthesised because it is usually the same
63
81
  // word as the platform ("claude/github · claude" reads as a stutter).
64
- facts: [plugin.supportPlatform.join("/"), `(${plugin.format})`, capabilities.join(", ") || "no capabilities"],
82
+ facts: [
83
+ plugin.supportPlatform.join("/"),
84
+ `(${plugin.format})`,
85
+ ...(scope ? [scope] : []),
86
+ capabilities.join(", ") || "no capabilities",
87
+ ...(withheld ? ["hooks/mcp withheld, workspace not trusted"] : []),
88
+ ],
65
89
  detail: plugin.description,
66
90
  };
67
91
  });
@@ -1 +1 @@
1
- {"version":3,"file":"listing.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/listing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,MAAiC,EAAU;IAC7E,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAC/C,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;AAAA,CACtC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAwB,EAAY;IACtE,OAAO;QACN,MAAM,CAAC,SAAS,IAAI,QAAQ;QAC5B,MAAM,CAAC,WAAW,IAAI,UAAU;QAChC,MAAM,CAAC,SAAS,IAAI,QAAQ;QAC5B,MAAM,CAAC,SAAS,IAAI,QAAQ;QAC5B,MAAM,CAAC,KAAK,IAAI,OAAO;QACvB,MAAM,CAAC,UAAU,IAAI,KAAK;QAC1B,MAAM,CAAC,SAAS,EAAE,MAAM,IAAI,WAAW;KACvC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;AAAA,CAChE;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACpC,OAAmC,EACnC,OAAO,GAAwC,EAAE,EACnC;IACd,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC5C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,SAAS;YAC7D,KAAK,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;YAC5D,MAAM,EAAE,MAAM,CAAC,WAAW,IAAI,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC;SAC/D,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAAA,CAChE;AAED,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,OAAoC,EAAa;IACpF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;QAC9B,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAChD,OAAO;YACN,IAAI,EAAE,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;YACjE,sEAAsE;YACtE,uEAAsE;YACtE,KAAK,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC;YAC7G,MAAM,EAAE,MAAM,CAAC,WAAW;SAC1B,CAAC;IAAA,CACF,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Row shapes for the plugin listings, shared by every surface that prints one.\n *\n * `/plugin list`, `SearchPlugins` and `ListPlugins` describe the same objects,\n * and each used to build its own `name [a, b] — description` string, so the same\n * plugin printed three ways and the model's view never quite matched the user's.\n * They all go through here now; only the styling and the width differ.\n */\n\nimport type { ListGroup, ListRow } from \"../../format-list.js\";\nimport type { AvailablePlugin } from \"./install.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\n\n/** Human-readable source for an available plugin (a URL, a path, an npm spec). */\nexport function formatPluginSource(source: AvailablePlugin[\"source\"]): string {\n\tif (typeof source === \"string\") return source;\n\tif (source.source === \"url\") return source.url;\n\treturn `${source.url}/${source.path}`;\n}\n\n/**\n * Capability classes a plugin actually ships.\n *\n * Covers every capability `NormalizedPlugin` can carry — themes and providers\n * included, which the old `ListPlugins` inline list omitted, so a plugin that\n * shipped only a theme or only a provider reported no capabilities at all.\n */\nexport function pluginCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"agents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && \"mcp\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((value): value is string => typeof value === \"string\");\n}\n\n/**\n * Available plugins, grouped by the marketplace offering them.\n *\n * Grouping is what makes provenance legible: a flat list cannot answer \"where\n * would this come from if I installed it\", which is the one question that\n * matters at a trust boundary. Marketplace order is preserved from the caller.\n */\nexport function availablePluginGroups(\n\tplugins: readonly AvailablePlugin[],\n\toptions: { installed?: ReadonlySet<string> } = {},\n): ListGroup[] {\n\tconst groups = new Map<string, ListRow[]>();\n\tfor (const plugin of plugins) {\n\t\tconst rows = groups.get(plugin.marketplaceName) ?? [];\n\t\trows.push({\n\t\t\tname: plugin.name,\n\t\t\tmarker: options.installed?.has(plugin.name) ? \"✓\" : undefined,\n\t\t\tfacts: [plugin.supportPlatform.join(\"/\"), plugin.sourceKind],\n\t\t\tdetail: plugin.description ?? formatPluginSource(plugin.source),\n\t\t});\n\t\tgroups.set(plugin.marketplaceName, rows);\n\t}\n\treturn Array.from(groups, ([title, rows]) => ({ title, rows }));\n}\n\n/** Installed plugins as rows: id@version, platforms, format, capabilities. */\nexport function installedPluginRows(plugins: readonly NormalizedPlugin[]): ListRow[] {\n\treturn plugins.map((plugin) => {\n\t\tconst capabilities = pluginCapabilities(plugin);\n\t\treturn {\n\t\t\tname: `${plugin.id}${plugin.version ? `@${plugin.version}` : \"\"}`,\n\t\t\t// The manifest format is parenthesised because it is usually the same\n\t\t\t// word as the platform (\"claude/github · claude\" reads as a stutter).\n\t\t\tfacts: [plugin.supportPlatform.join(\"/\"), `(${plugin.format})`, capabilities.join(\", \") || \"no capabilities\"],\n\t\t\tdetail: plugin.description,\n\t\t};\n\t});\n}\n"]}
1
+ {"version":3,"file":"listing.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/listing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Row shapes for the plugin listings, shared by every surface that prints one.\n *\n * `/plugin list`, `SearchPlugins` and `ListPlugins` describe the same objects,\n * and each used to build its own `name [a, b] — description` string, so the same\n * plugin printed three ways and the model's view never quite matched the user's.\n * They all go through here now; only the styling and the width differ.\n */\n\nimport type { ListGroup, ListRow } from \"../../format-list.js\";\nimport type { AvailablePlugin } from \"./install.js\";\nimport { pluginScopeOf } from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\n\n/** Human-readable source for an available plugin (a URL, a path, an npm spec). */\nexport function formatPluginSource(source: AvailablePlugin[\"source\"]): string {\n\tif (typeof source === \"string\") return source;\n\tif (source.source === \"npm\") return source.package;\n\t// `archive` carries a url like `url` does; without this it printed\n\t// `https://…/p.zip/undefined` through the git-subdir branch below.\n\tif (source.source === \"url\" || source.source === \"archive\") return source.url;\n\treturn `${source.url}/${source.path}`;\n}\n\n/**\n * Capability classes a plugin actually ships.\n *\n * Covers every capability `NormalizedPlugin` can carry — themes and providers\n * included, which the old `ListPlugins` inline list omitted, so a plugin that\n * shipped only a theme or only a provider reported no capabilities at all.\n */\nexport function pluginCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"agents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && \"mcp\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((value): value is string => typeof value === \"string\");\n}\n\n/**\n * Available plugins, grouped by the marketplace offering them.\n *\n * Grouping is what makes provenance legible: a flat list cannot answer \"where\n * would this come from if I installed it\", which is the one question that\n * matters at a trust boundary. Marketplace order is preserved from the caller.\n */\nexport function availablePluginGroups(\n\tplugins: readonly AvailablePlugin[],\n\toptions: { installed?: ReadonlySet<string> } = {},\n): ListGroup[] {\n\tconst groups = new Map<string, ListRow[]>();\n\tfor (const plugin of plugins) {\n\t\tconst rows = groups.get(plugin.marketplaceName) ?? [];\n\t\t// npm and archive entries are catalogued but not fetchable here, and saying\n\t\t// so in the row is cheaper than an InstallPlugin round trip that only ends\n\t\t// in the same message.\n\t\tconst installable = plugin.sourceKind !== \"npm\" && plugin.sourceKind !== \"archive\";\n\t\trows.push({\n\t\t\tname: plugin.name,\n\t\t\tmarker: options.installed?.has(plugin.name) ? \"✓\" : undefined,\n\t\t\tfacts: [\n\t\t\t\tplugin.supportPlatform.join(\"/\"),\n\t\t\t\tinstallable ? plugin.sourceKind : `${plugin.sourceKind}, not installable`,\n\t\t\t],\n\t\t\tdetail: plugin.description ?? formatPluginSource(plugin.source),\n\t\t});\n\t\tgroups.set(plugin.marketplaceName, rows);\n\t}\n\treturn Array.from(groups, ([title, rows]) => ({ title, rows }));\n}\n\nexport interface InstalledRowOptions {\n\t/** Working directory, used to place each plugin in a scope. */\n\tcwd?: string;\n\t/** Whether `cwd` is a trusted workspace; decides if executables actually load. */\n\ttrusted?: boolean;\n}\n\n/** Installed plugins as rows: id@version, platforms, format, scope, capabilities. */\nexport function installedPluginRows(\n\tplugins: readonly NormalizedPlugin[],\n\toptions: InstalledRowOptions = {},\n): ListRow[] {\n\treturn plugins.map((plugin) => {\n\t\tconst capabilities = pluginCapabilities(plugin);\n\t\t// Scope answers \"why is this loaded, and who else gets it\" — the plugin's\n\t\t// own path always knew, and nothing surfaced it.\n\t\tconst scope = options.cwd ? pluginScopeOf(plugin.root, options.cwd) : undefined;\n\t\t// A working-tree plugin's executables load only in a trusted workspace, so\n\t\t// listing \"hooks\" unqualified would overstate what is actually running.\n\t\tconst withheld = scope && scope !== \"user\" && !options.trusted && (plugin.hooks || plugin.mcpServers);\n\t\treturn {\n\t\t\tname: `${plugin.id}${plugin.version ? `@${plugin.version}` : \"\"}`,\n\t\t\t// The manifest format is parenthesised because it is usually the same\n\t\t\t// word as the platform (\"claude/github · claude\" reads as a stutter).\n\t\t\tfacts: [\n\t\t\t\tplugin.supportPlatform.join(\"/\"),\n\t\t\t\t`(${plugin.format})`,\n\t\t\t\t...(scope ? [scope] : []),\n\t\t\t\tcapabilities.join(\", \") || \"no capabilities\",\n\t\t\t\t...(withheld ? [\"hooks/mcp withheld, workspace not trusted\"] : []),\n\t\t\t],\n\t\t\tdetail: plugin.description,\n\t\t};\n\t});\n}\n"]}
@@ -8,17 +8,26 @@
8
8
  * run, and is deleted on any failure. Nothing here is ever
9
9
  * loaded; it is promoted into a production home or thrown
10
10
  * away. {@link makeDraftDir}
11
- * Consumption home Persistent, global. Where marketplace installs land, in
12
- * whatever format the marketplace served. hoocode
13
- * installing for itself. {@link consumptionPluginsDir}
11
+ * Consumption home Persistent. Where marketplace installs land, in whatever
12
+ * format the marketplace served. hoocode installing for
13
+ * itself, at the scope the caller chose
14
+ * ({@link installHomeForScope}).
14
15
  * Production home Persistent, global, per platform. Where a plugin hoocode
15
16
  * *authored* lives. {@link productionPluginDir}
16
17
  *
17
- * All three are user-scoped. A plugin is portable, versioned and reusable across
18
- * projects, so writing one into the working tree means the capability is
19
- * invisible in every other repo and the agent dirties `git status` with content
20
- * unrelated to the task. The project-local home is still *read* so plugins
21
- * installed by older versions keep working ({@link legacyProjectPluginsDir}).
18
+ * Draft and production are user-scoped without exception. Consumption is the one
19
+ * that takes a scope, and `user` stays the default: a plugin is portable,
20
+ * versioned and reusable across projects, so the working tree is the wrong place
21
+ * for it *by default* — the capability would be invisible in every other repo,
22
+ * and an autonomous install would dirty `git status` with content unrelated to
23
+ * the task. `project` exists for the other case, where a team wants the plugin
24
+ * pinned in the repo and shared with collaborators, and is a deliberate choice
25
+ * someone makes per install or per setting rather than a default anything falls
26
+ * into.
27
+ *
28
+ * Authoring stays user-scoped regardless (architecture doc §5.5): a project
29
+ * destination for an *authored* plugin has no coherent production home, whereas
30
+ * an installed plugin has an obvious one.
22
31
  *
23
32
  * The two production homes are asymmetric because the vendors are. Claude Code
24
33
  * discovers `~/.claude/skills/<id>/` in place, with no install step, so an
@@ -32,10 +41,40 @@
32
41
  import type { PluginPlatform } from "./formats/platform-targets.js";
33
42
  /** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */
34
43
  export declare function sanitizeForDir(s: string): string;
35
- /** `~/.agents/plugins/` — where marketplace installs land. Format-agnostic. */
44
+ /** `~/.agents/plugins/` — where user-scoped marketplace installs land. Format-agnostic. */
36
45
  export declare function consumptionPluginsDir(agentDir?: string): string;
37
- /** The project-local home older versions installed into. Read-only now; still uninstalled from. */
38
- export declare function legacyProjectPluginsDir(cwd: string): string;
46
+ /**
47
+ * `<cwd>/.agents/plugins/` where project-scoped installs land.
48
+ *
49
+ * Also the home older versions installed *everything* into, back when scope was
50
+ * not a choice (see docs/plugin-system-architecture.md §5.4). It stayed on the
51
+ * discovery path the whole time, which is why project scope needs no loader
52
+ * change: `defaultPluginDirs` already reads it, ahead of the user home, so a
53
+ * project-scoped plugin shadows a user-scoped one of the same id.
54
+ */
55
+ export declare function projectPluginsDir(cwd: string): string;
56
+ /**
57
+ * Where a marketplace install goes.
58
+ *
59
+ * hoocode has no `enabledPlugins`-style registry — a plugin on the discovery
60
+ * path is enabled — so scope is a *destination*, not a flag written elsewhere:
61
+ *
62
+ * user `~/.agents/plugins/` portable across every checkout, invisible to collaborators
63
+ * project `<cwd>/.agents/plugins/` committed with the repo, shared, shadows the user copy
64
+ */
65
+ export type PluginInstallScope = "user" | "project";
66
+ /** Resolve the install home for `scope`. */
67
+ export declare function installHomeForScope(scope: PluginInstallScope, cwd: string, agentDir?: string): string;
68
+ /**
69
+ * Which scope a plugin already on disk belongs to, derived from where it lives.
70
+ *
71
+ * `repo` is the third answer the install scopes do not cover: a plugin under
72
+ * `<cwd>/.claude/skills` arrived with the repository rather than through an
73
+ * install, so it is project-*located* but nobody here chose it. Keeping it
74
+ * distinct from `project` is what lets `ListPlugins` say which plugins are
75
+ * running because someone installed them and which because they were cloned.
76
+ */
77
+ export declare function pluginScopeOf(pluginRoot: string, cwd: string): PluginInstallScope | "repo";
39
78
  /**
40
79
  * Root of a platform's production home — the parent that {@link productionPluginDir}
41
80
  * places plugin directories under.
@@ -57,10 +96,21 @@ export declare function marketplaceCacheDir(url: string, agentDir?: string): str
57
96
  * we last talk to the remote" is the only question the TTL is asking.
58
97
  */
59
98
  export declare function marketplaceCacheMetaPath(agentDir?: string): string;
99
+ /**
100
+ * The parent directories hoocode owns and may therefore remove a plugin from:
101
+ * the two production homes, both consumption homes (user and project), and the
102
+ * `.hoocode/plugins` home older versions installed into.
103
+ *
104
+ * Deliberately excludes `<cwd>/.claude/skills` and `<cwd>/.agents/skills`. Those
105
+ * are discovered (loader.ts `defaultPluginDirs`) but are repository content a
106
+ * team committed, so uninstall must not delete out of them — hoocode installed
107
+ * nothing there.
108
+ */
109
+ export declare function pluginHomeRoots(cwd: string, agentDir?: string): string[];
60
110
  /**
61
111
  * Every directory a plugin with `id` could occupy, in the order a lookup should
62
112
  * try them: the platform production homes, then the consumption home, then the
63
- * legacy project home. Used to find an existing plugin without knowing which
113
+ * legacy project homes. Used to find an existing plugin without knowing which
64
114
  * role wrote it.
65
115
  */
66
116
  export declare function candidatePluginDirs(cwd: string, id: string, agentDir?: string): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"locations.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/locations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAWD,iFAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE9E;AAED,mGAAmG;AACnG,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAOjG;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAElH;AAED,wEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE7E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEzF;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,EAAE,CAQvG;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAElF;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAIxF;AAED,kGAAkG;AAClG,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,oDAAoD;AACpD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD","sourcesContent":["/**\n * Where plugins live on disk.\n *\n * Three distinct roles, deliberately named apart — an earlier design called all\n * three \"staging\", which is how the wrong directory ends up in code:\n *\n * Draft dir Ephemeral. Holds an authored plugin while the eval gates\n * run, and is deleted on any failure. Nothing here is ever\n * loaded; it is promoted into a production home or thrown\n * away. {@link makeDraftDir}\n * Consumption home Persistent, global. Where marketplace installs land, in\n * whatever format the marketplace served. hoocode\n * installing for itself. {@link consumptionPluginsDir}\n * Production home Persistent, global, per platform. Where a plugin hoocode\n * *authored* lives. {@link productionPluginDir}\n *\n * All three are user-scoped. A plugin is portable, versioned and reusable across\n * projects, so writing one into the working tree means the capability is\n * invisible in every other repo and the agent dirties `git status` with content\n * unrelated to the task. The project-local home is still *read* so plugins\n * installed by older versions keep working ({@link legacyProjectPluginsDir}).\n *\n * The two production homes are asymmetric because the vendors are. Claude Code\n * discovers `~/.claude/skills/<id>/` in place, with no install step, so an\n * authored plugin is live there on its next session. Copilot CLI has no\n * equivalent — `copilot plugin install` copies into a cache it owns — so a\n * github artifact lives in a hoocode-owned home and reaches the ecosystem\n * through the publish lane instead.\n *\n * See docs/plugin-system-architecture.md §5.3 and §8.3.\n */\n\nimport { mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../../config.js\";\nimport type { PluginPlatform } from \"./formats/platform-targets.js\";\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/**\n * Home root the global surfaces hang off. Derived from the agent dir rather than\n * `homedir()` so `HOOCODE_AGENT_DIR` relocates every plugin location together —\n * which is what makes these paths testable.\n */\nfunction homeRoot(agentDir: string): string {\n\treturn path.dirname(agentDir);\n}\n\n/** `~/.agents/plugins/` — where marketplace installs land. Format-agnostic. */\nexport function consumptionPluginsDir(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugins\");\n}\n\n/** The project-local home older versions installed into. Read-only now; still uninstalled from. */\nexport function legacyProjectPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\n/**\n * Root of a platform's production home — the parent that {@link productionPluginDir}\n * places plugin directories under.\n */\nexport function productionRoot(platform: PluginPlatform, agentDir: string = getAgentDir()): string {\n\t// Claude: the documented skills-directory drop-in. A folder here carrying a\n\t// `.claude-plugin/plugin.json` loads as `<id>@skills-dir` with no install step.\n\tif (platform === \"claude\") return path.join(homeRoot(agentDir), \".claude\", \"skills\");\n\t// GitHub: no vendor drop-in exists, so this is hoocode's own home for a\n\t// github-targeted artifact. PackagePlugin works in it and publish reads from it.\n\treturn path.join(homeRoot(agentDir), \".agents\", \"publish\", \"github\");\n}\n\n/** Where an authored plugin for `platform` lives. */\nexport function productionPluginDir(platform: PluginPlatform, id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(productionRoot(platform, agentDir), sanitizeForDir(id));\n}\n\n/** `~/.agents/marketplaces.json` — the added-marketplace registry. */\nexport function marketplaceStorePath(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplaces.json\");\n}\n\n/** Root of the local marketplace clone cache. A cache is never repo content. */\nexport function marketplaceCacheRoot(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplace-cache\");\n}\n\n/** Local cache directory for a marketplace fetched from `url`. */\nexport function marketplaceCacheDir(url: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), sanitizeForDir(url));\n}\n\n/**\n * Records when each cached marketplace index was last fetched.\n *\n * Kept beside the caches rather than inferred from directory mtimes: a clone's\n * mtime moves for reasons that have nothing to do with freshness, and \"when did\n * we last talk to the remote\" is the only question the TTL is asking.\n */\nexport function marketplaceCacheMetaPath(agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), \".fetched.json\");\n}\n\n/**\n * Every directory a plugin with `id` could occupy, in the order a lookup should\n * try them: the platform production homes, then the consumption home, then the\n * legacy project home. Used to find an existing plugin without knowing which\n * role wrote it.\n */\nexport function candidatePluginDirs(cwd: string, id: string, agentDir: string = getAgentDir()): string[] {\n\tconst slug = sanitizeForDir(id);\n\treturn [\n\t\tproductionPluginDir(\"claude\", id, agentDir),\n\t\tproductionPluginDir(\"github\", id, agentDir),\n\t\tpath.join(consumptionPluginsDir(agentDir), slug),\n\t\tpath.join(legacyProjectPluginsDir(cwd), slug),\n\t];\n}\n\n/**\n * Persistent, writable directory unique to an installed plugin — the target of\n * `${CLAUDE_PLUGIN_DATA}` / `${COPILOT_PLUGIN_DATA}`, which both vendors document\n * as the place for plugin runtime state.\n *\n * Deliberately outside every plugin home. A plugin's own directory is replaced\n * wholesale on promote and deleted on uninstall, and the vendors are explicit\n * that this must not live \"inside the installed-plugins cache directory\" — state\n * that vanishes on reinstall is not state.\n */\nexport function pluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugin-data\", sanitizeForDir(id));\n}\n\n/** {@link pluginDataDir}, created if absent. */\nexport function ensurePluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\tconst dir = pluginDataDir(id, agentDir);\n\tmkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\n/** Create an ephemeral draft directory. The caller must promote or {@link discardDraftDir} it. */\nexport function makeDraftDir(): string {\n\treturn mkdtempSync(path.join(tmpdir(), \"hoo-plugin-draft-\"));\n}\n\n/** Delete a draft directory. Safe to call twice. */\nexport function discardDraftDir(dir: string): void {\n\trmSync(dir, { recursive: true, force: true });\n}\n"]}
1
+ {"version":3,"file":"locations.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/locations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAWD,6FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpD,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEpH;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,MAAM,CAS1F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAOjG;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAElH;AAED,wEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE7E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEzF;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,EAAE,CAQvF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,EAAE,CAGvG;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAElF;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAIxF;AAED,kGAAkG;AAClG,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,oDAAoD;AACpD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD","sourcesContent":["/**\n * Where plugins live on disk.\n *\n * Three distinct roles, deliberately named apart — an earlier design called all\n * three \"staging\", which is how the wrong directory ends up in code:\n *\n * Draft dir Ephemeral. Holds an authored plugin while the eval gates\n * run, and is deleted on any failure. Nothing here is ever\n * loaded; it is promoted into a production home or thrown\n * away. {@link makeDraftDir}\n * Consumption home Persistent. Where marketplace installs land, in whatever\n * format the marketplace served. hoocode installing for\n * itself, at the scope the caller chose\n * ({@link installHomeForScope}).\n * Production home Persistent, global, per platform. Where a plugin hoocode\n * *authored* lives. {@link productionPluginDir}\n *\n * Draft and production are user-scoped without exception. Consumption is the one\n * that takes a scope, and `user` stays the default: a plugin is portable,\n * versioned and reusable across projects, so the working tree is the wrong place\n * for it *by default* — the capability would be invisible in every other repo,\n * and an autonomous install would dirty `git status` with content unrelated to\n * the task. `project` exists for the other case, where a team wants the plugin\n * pinned in the repo and shared with collaborators, and is a deliberate choice\n * someone makes per install or per setting rather than a default anything falls\n * into.\n *\n * Authoring stays user-scoped regardless (architecture doc §5.5): a project\n * destination for an *authored* plugin has no coherent production home, whereas\n * an installed plugin has an obvious one.\n *\n * The two production homes are asymmetric because the vendors are. Claude Code\n * discovers `~/.claude/skills/<id>/` in place, with no install step, so an\n * authored plugin is live there on its next session. Copilot CLI has no\n * equivalent — `copilot plugin install` copies into a cache it owns — so a\n * github artifact lives in a hoocode-owned home and reaches the ecosystem\n * through the publish lane instead.\n *\n * See docs/plugin-system-architecture.md §5.3 and §8.3.\n */\n\nimport { mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../../config.js\";\nimport type { PluginPlatform } from \"./formats/platform-targets.js\";\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/**\n * Home root the global surfaces hang off. Derived from the agent dir rather than\n * `homedir()` so `HOOCODE_AGENT_DIR` relocates every plugin location together —\n * which is what makes these paths testable.\n */\nfunction homeRoot(agentDir: string): string {\n\treturn path.dirname(agentDir);\n}\n\n/** `~/.agents/plugins/` — where user-scoped marketplace installs land. Format-agnostic. */\nexport function consumptionPluginsDir(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugins\");\n}\n\n/**\n * `<cwd>/.agents/plugins/` — where project-scoped installs land.\n *\n * Also the home older versions installed *everything* into, back when scope was\n * not a choice (see docs/plugin-system-architecture.md §5.4). It stayed on the\n * discovery path the whole time, which is why project scope needs no loader\n * change: `defaultPluginDirs` already reads it, ahead of the user home, so a\n * project-scoped plugin shadows a user-scoped one of the same id.\n */\nexport function projectPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\n/**\n * Where a marketplace install goes.\n *\n * hoocode has no `enabledPlugins`-style registry — a plugin on the discovery\n * path is enabled — so scope is a *destination*, not a flag written elsewhere:\n *\n * user `~/.agents/plugins/` portable across every checkout, invisible to collaborators\n * project `<cwd>/.agents/plugins/` committed with the repo, shared, shadows the user copy\n */\nexport type PluginInstallScope = \"user\" | \"project\";\n\n/** Resolve the install home for `scope`. */\nexport function installHomeForScope(scope: PluginInstallScope, cwd: string, agentDir: string = getAgentDir()): string {\n\treturn scope === \"project\" ? projectPluginsDir(cwd) : consumptionPluginsDir(agentDir);\n}\n\n/**\n * Which scope a plugin already on disk belongs to, derived from where it lives.\n *\n * `repo` is the third answer the install scopes do not cover: a plugin under\n * `<cwd>/.claude/skills` arrived with the repository rather than through an\n * install, so it is project-*located* but nobody here chose it. Keeping it\n * distinct from `project` is what lets `ListPlugins` say which plugins are\n * running because someone installed them and which because they were cloned.\n */\nexport function pluginScopeOf(pluginRoot: string, cwd: string): PluginInstallScope | \"repo\" {\n\tconst under = (root: string): boolean => {\n\t\tconst normalized = path.resolve(root);\n\t\tconst target = path.resolve(pluginRoot);\n\t\treturn target === normalized || target.startsWith(`${normalized}${path.sep}`);\n\t};\n\tif (under(projectPluginsDir(cwd)) || under(path.join(cwd, \".hoocode\", \"plugins\"))) return \"project\";\n\tif (under(path.join(cwd, \".claude\", \"skills\")) || under(path.join(cwd, \".agents\", \"skills\"))) return \"repo\";\n\treturn \"user\";\n}\n\n/**\n * Root of a platform's production home — the parent that {@link productionPluginDir}\n * places plugin directories under.\n */\nexport function productionRoot(platform: PluginPlatform, agentDir: string = getAgentDir()): string {\n\t// Claude: the documented skills-directory drop-in. A folder here carrying a\n\t// `.claude-plugin/plugin.json` loads as `<id>@skills-dir` with no install step.\n\tif (platform === \"claude\") return path.join(homeRoot(agentDir), \".claude\", \"skills\");\n\t// GitHub: no vendor drop-in exists, so this is hoocode's own home for a\n\t// github-targeted artifact. PackagePlugin works in it and publish reads from it.\n\treturn path.join(homeRoot(agentDir), \".agents\", \"publish\", \"github\");\n}\n\n/** Where an authored plugin for `platform` lives. */\nexport function productionPluginDir(platform: PluginPlatform, id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(productionRoot(platform, agentDir), sanitizeForDir(id));\n}\n\n/** `~/.agents/marketplaces.json` — the added-marketplace registry. */\nexport function marketplaceStorePath(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplaces.json\");\n}\n\n/** Root of the local marketplace clone cache. A cache is never repo content. */\nexport function marketplaceCacheRoot(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplace-cache\");\n}\n\n/** Local cache directory for a marketplace fetched from `url`. */\nexport function marketplaceCacheDir(url: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), sanitizeForDir(url));\n}\n\n/**\n * Records when each cached marketplace index was last fetched.\n *\n * Kept beside the caches rather than inferred from directory mtimes: a clone's\n * mtime moves for reasons that have nothing to do with freshness, and \"when did\n * we last talk to the remote\" is the only question the TTL is asking.\n */\nexport function marketplaceCacheMetaPath(agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), \".fetched.json\");\n}\n\n/**\n * The parent directories hoocode owns and may therefore remove a plugin from:\n * the two production homes, both consumption homes (user and project), and the\n * `.hoocode/plugins` home older versions installed into.\n *\n * Deliberately excludes `<cwd>/.claude/skills` and `<cwd>/.agents/skills`. Those\n * are discovered (loader.ts `defaultPluginDirs`) but are repository content a\n * team committed, so uninstall must not delete out of them — hoocode installed\n * nothing there.\n */\nexport function pluginHomeRoots(cwd: string, agentDir: string = getAgentDir()): string[] {\n\treturn [\n\t\tproductionRoot(\"claude\", agentDir),\n\t\tproductionRoot(\"github\", agentDir),\n\t\tconsumptionPluginsDir(agentDir),\n\t\tprojectPluginsDir(cwd),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\"),\n\t];\n}\n\n/**\n * Every directory a plugin with `id` could occupy, in the order a lookup should\n * try them: the platform production homes, then the consumption home, then the\n * legacy project homes. Used to find an existing plugin without knowing which\n * role wrote it.\n */\nexport function candidatePluginDirs(cwd: string, id: string, agentDir: string = getAgentDir()): string[] {\n\tconst slug = sanitizeForDir(id);\n\treturn pluginHomeRoots(cwd, agentDir).map((root) => path.join(root, slug));\n}\n\n/**\n * Persistent, writable directory unique to an installed plugin — the target of\n * `${CLAUDE_PLUGIN_DATA}` / `${COPILOT_PLUGIN_DATA}`, which both vendors document\n * as the place for plugin runtime state.\n *\n * Deliberately outside every plugin home. A plugin's own directory is replaced\n * wholesale on promote and deleted on uninstall, and the vendors are explicit\n * that this must not live \"inside the installed-plugins cache directory\" — state\n * that vanishes on reinstall is not state.\n */\nexport function pluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugin-data\", sanitizeForDir(id));\n}\n\n/** {@link pluginDataDir}, created if absent. */\nexport function ensurePluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\tconst dir = pluginDataDir(id, agentDir);\n\tmkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\n/** Create an ephemeral draft directory. The caller must promote or {@link discardDraftDir} it. */\nexport function makeDraftDir(): string {\n\treturn mkdtempSync(path.join(tmpdir(), \"hoo-plugin-draft-\"));\n}\n\n/** Delete a draft directory. Safe to call twice. */\nexport function discardDraftDir(dir: string): void {\n\trmSync(dir, { recursive: true, force: true });\n}\n"]}
@@ -8,17 +8,26 @@
8
8
  * run, and is deleted on any failure. Nothing here is ever
9
9
  * loaded; it is promoted into a production home or thrown
10
10
  * away. {@link makeDraftDir}
11
- * Consumption home Persistent, global. Where marketplace installs land, in
12
- * whatever format the marketplace served. hoocode
13
- * installing for itself. {@link consumptionPluginsDir}
11
+ * Consumption home Persistent. Where marketplace installs land, in whatever
12
+ * format the marketplace served. hoocode installing for
13
+ * itself, at the scope the caller chose
14
+ * ({@link installHomeForScope}).
14
15
  * Production home Persistent, global, per platform. Where a plugin hoocode
15
16
  * *authored* lives. {@link productionPluginDir}
16
17
  *
17
- * All three are user-scoped. A plugin is portable, versioned and reusable across
18
- * projects, so writing one into the working tree means the capability is
19
- * invisible in every other repo and the agent dirties `git status` with content
20
- * unrelated to the task. The project-local home is still *read* so plugins
21
- * installed by older versions keep working ({@link legacyProjectPluginsDir}).
18
+ * Draft and production are user-scoped without exception. Consumption is the one
19
+ * that takes a scope, and `user` stays the default: a plugin is portable,
20
+ * versioned and reusable across projects, so the working tree is the wrong place
21
+ * for it *by default* — the capability would be invisible in every other repo,
22
+ * and an autonomous install would dirty `git status` with content unrelated to
23
+ * the task. `project` exists for the other case, where a team wants the plugin
24
+ * pinned in the repo and shared with collaborators, and is a deliberate choice
25
+ * someone makes per install or per setting rather than a default anything falls
26
+ * into.
27
+ *
28
+ * Authoring stays user-scoped regardless (architecture doc §5.5): a project
29
+ * destination for an *authored* plugin has no coherent production home, whereas
30
+ * an installed plugin has an obvious one.
22
31
  *
23
32
  * The two production homes are asymmetric because the vendors are. Claude Code
24
33
  * discovers `~/.claude/skills/<id>/` in place, with no install step, so an
@@ -45,14 +54,47 @@ export function sanitizeForDir(s) {
45
54
  function homeRoot(agentDir) {
46
55
  return path.dirname(agentDir);
47
56
  }
48
- /** `~/.agents/plugins/` — where marketplace installs land. Format-agnostic. */
57
+ /** `~/.agents/plugins/` — where user-scoped marketplace installs land. Format-agnostic. */
49
58
  export function consumptionPluginsDir(agentDir = getAgentDir()) {
50
59
  return path.join(homeRoot(agentDir), ".agents", "plugins");
51
60
  }
52
- /** The project-local home older versions installed into. Read-only now; still uninstalled from. */
53
- export function legacyProjectPluginsDir(cwd) {
61
+ /**
62
+ * `<cwd>/.agents/plugins/` — where project-scoped installs land.
63
+ *
64
+ * Also the home older versions installed *everything* into, back when scope was
65
+ * not a choice (see docs/plugin-system-architecture.md §5.4). It stayed on the
66
+ * discovery path the whole time, which is why project scope needs no loader
67
+ * change: `defaultPluginDirs` already reads it, ahead of the user home, so a
68
+ * project-scoped plugin shadows a user-scoped one of the same id.
69
+ */
70
+ export function projectPluginsDir(cwd) {
54
71
  return path.join(cwd, ".agents", "plugins");
55
72
  }
73
+ /** Resolve the install home for `scope`. */
74
+ export function installHomeForScope(scope, cwd, agentDir = getAgentDir()) {
75
+ return scope === "project" ? projectPluginsDir(cwd) : consumptionPluginsDir(agentDir);
76
+ }
77
+ /**
78
+ * Which scope a plugin already on disk belongs to, derived from where it lives.
79
+ *
80
+ * `repo` is the third answer the install scopes do not cover: a plugin under
81
+ * `<cwd>/.claude/skills` arrived with the repository rather than through an
82
+ * install, so it is project-*located* but nobody here chose it. Keeping it
83
+ * distinct from `project` is what lets `ListPlugins` say which plugins are
84
+ * running because someone installed them and which because they were cloned.
85
+ */
86
+ export function pluginScopeOf(pluginRoot, cwd) {
87
+ const under = (root) => {
88
+ const normalized = path.resolve(root);
89
+ const target = path.resolve(pluginRoot);
90
+ return target === normalized || target.startsWith(`${normalized}${path.sep}`);
91
+ };
92
+ if (under(projectPluginsDir(cwd)) || under(path.join(cwd, ".hoocode", "plugins")))
93
+ return "project";
94
+ if (under(path.join(cwd, ".claude", "skills")) || under(path.join(cwd, ".agents", "skills")))
95
+ return "repo";
96
+ return "user";
97
+ }
56
98
  /**
57
99
  * Root of a platform's production home — the parent that {@link productionPluginDir}
58
100
  * places plugin directories under.
@@ -92,20 +134,34 @@ export function marketplaceCacheDir(url, agentDir = getAgentDir()) {
92
134
  export function marketplaceCacheMetaPath(agentDir = getAgentDir()) {
93
135
  return path.join(marketplaceCacheRoot(agentDir), ".fetched.json");
94
136
  }
137
+ /**
138
+ * The parent directories hoocode owns and may therefore remove a plugin from:
139
+ * the two production homes, both consumption homes (user and project), and the
140
+ * `.hoocode/plugins` home older versions installed into.
141
+ *
142
+ * Deliberately excludes `<cwd>/.claude/skills` and `<cwd>/.agents/skills`. Those
143
+ * are discovered (loader.ts `defaultPluginDirs`) but are repository content a
144
+ * team committed, so uninstall must not delete out of them — hoocode installed
145
+ * nothing there.
146
+ */
147
+ export function pluginHomeRoots(cwd, agentDir = getAgentDir()) {
148
+ return [
149
+ productionRoot("claude", agentDir),
150
+ productionRoot("github", agentDir),
151
+ consumptionPluginsDir(agentDir),
152
+ projectPluginsDir(cwd),
153
+ path.join(cwd, ".hoocode", "plugins"),
154
+ ];
155
+ }
95
156
  /**
96
157
  * Every directory a plugin with `id` could occupy, in the order a lookup should
97
158
  * try them: the platform production homes, then the consumption home, then the
98
- * legacy project home. Used to find an existing plugin without knowing which
159
+ * legacy project homes. Used to find an existing plugin without knowing which
99
160
  * role wrote it.
100
161
  */
101
162
  export function candidatePluginDirs(cwd, id, agentDir = getAgentDir()) {
102
163
  const slug = sanitizeForDir(id);
103
- return [
104
- productionPluginDir("claude", id, agentDir),
105
- productionPluginDir("github", id, agentDir),
106
- path.join(consumptionPluginsDir(agentDir), slug),
107
- path.join(legacyProjectPluginsDir(cwd), slug),
108
- ];
164
+ return pluginHomeRoots(cwd, agentDir).map((root) => path.join(root, slug));
109
165
  }
110
166
  /**
111
167
  * Persistent, writable directory unique to an installed plugin — the target of
@@ -1 +1 @@
1
- {"version":3,"file":"locations.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/locations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where plugins live on disk.\n *\n * Three distinct roles, deliberately named apart — an earlier design called all\n * three \"staging\", which is how the wrong directory ends up in code:\n *\n * Draft dir Ephemeral. Holds an authored plugin while the eval gates\n * run, and is deleted on any failure. Nothing here is ever\n * loaded; it is promoted into a production home or thrown\n * away. {@link makeDraftDir}\n * Consumption home Persistent, global. Where marketplace installs land, in\n * whatever format the marketplace served. hoocode\n * installing for itself. {@link consumptionPluginsDir}\n * Production home Persistent, global, per platform. Where a plugin hoocode\n * *authored* lives. {@link productionPluginDir}\n *\n * All three are user-scoped. A plugin is portable, versioned and reusable across\n * projects, so writing one into the working tree means the capability is\n * invisible in every other repo and the agent dirties `git status` with content\n * unrelated to the task. The project-local home is still *read* so plugins\n * installed by older versions keep working ({@link legacyProjectPluginsDir}).\n *\n * The two production homes are asymmetric because the vendors are. Claude Code\n * discovers `~/.claude/skills/<id>/` in place, with no install step, so an\n * authored plugin is live there on its next session. Copilot CLI has no\n * equivalent — `copilot plugin install` copies into a cache it owns — so a\n * github artifact lives in a hoocode-owned home and reaches the ecosystem\n * through the publish lane instead.\n *\n * See docs/plugin-system-architecture.md §5.3 and §8.3.\n */\n\nimport { mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../../config.js\";\nimport type { PluginPlatform } from \"./formats/platform-targets.js\";\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/**\n * Home root the global surfaces hang off. Derived from the agent dir rather than\n * `homedir()` so `HOOCODE_AGENT_DIR` relocates every plugin location together —\n * which is what makes these paths testable.\n */\nfunction homeRoot(agentDir: string): string {\n\treturn path.dirname(agentDir);\n}\n\n/** `~/.agents/plugins/` — where marketplace installs land. Format-agnostic. */\nexport function consumptionPluginsDir(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugins\");\n}\n\n/** The project-local home older versions installed into. Read-only now; still uninstalled from. */\nexport function legacyProjectPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\n/**\n * Root of a platform's production home — the parent that {@link productionPluginDir}\n * places plugin directories under.\n */\nexport function productionRoot(platform: PluginPlatform, agentDir: string = getAgentDir()): string {\n\t// Claude: the documented skills-directory drop-in. A folder here carrying a\n\t// `.claude-plugin/plugin.json` loads as `<id>@skills-dir` with no install step.\n\tif (platform === \"claude\") return path.join(homeRoot(agentDir), \".claude\", \"skills\");\n\t// GitHub: no vendor drop-in exists, so this is hoocode's own home for a\n\t// github-targeted artifact. PackagePlugin works in it and publish reads from it.\n\treturn path.join(homeRoot(agentDir), \".agents\", \"publish\", \"github\");\n}\n\n/** Where an authored plugin for `platform` lives. */\nexport function productionPluginDir(platform: PluginPlatform, id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(productionRoot(platform, agentDir), sanitizeForDir(id));\n}\n\n/** `~/.agents/marketplaces.json` — the added-marketplace registry. */\nexport function marketplaceStorePath(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplaces.json\");\n}\n\n/** Root of the local marketplace clone cache. A cache is never repo content. */\nexport function marketplaceCacheRoot(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplace-cache\");\n}\n\n/** Local cache directory for a marketplace fetched from `url`. */\nexport function marketplaceCacheDir(url: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), sanitizeForDir(url));\n}\n\n/**\n * Records when each cached marketplace index was last fetched.\n *\n * Kept beside the caches rather than inferred from directory mtimes: a clone's\n * mtime moves for reasons that have nothing to do with freshness, and \"when did\n * we last talk to the remote\" is the only question the TTL is asking.\n */\nexport function marketplaceCacheMetaPath(agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), \".fetched.json\");\n}\n\n/**\n * Every directory a plugin with `id` could occupy, in the order a lookup should\n * try them: the platform production homes, then the consumption home, then the\n * legacy project home. Used to find an existing plugin without knowing which\n * role wrote it.\n */\nexport function candidatePluginDirs(cwd: string, id: string, agentDir: string = getAgentDir()): string[] {\n\tconst slug = sanitizeForDir(id);\n\treturn [\n\t\tproductionPluginDir(\"claude\", id, agentDir),\n\t\tproductionPluginDir(\"github\", id, agentDir),\n\t\tpath.join(consumptionPluginsDir(agentDir), slug),\n\t\tpath.join(legacyProjectPluginsDir(cwd), slug),\n\t];\n}\n\n/**\n * Persistent, writable directory unique to an installed plugin — the target of\n * `${CLAUDE_PLUGIN_DATA}` / `${COPILOT_PLUGIN_DATA}`, which both vendors document\n * as the place for plugin runtime state.\n *\n * Deliberately outside every plugin home. A plugin's own directory is replaced\n * wholesale on promote and deleted on uninstall, and the vendors are explicit\n * that this must not live \"inside the installed-plugins cache directory\" — state\n * that vanishes on reinstall is not state.\n */\nexport function pluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugin-data\", sanitizeForDir(id));\n}\n\n/** {@link pluginDataDir}, created if absent. */\nexport function ensurePluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\tconst dir = pluginDataDir(id, agentDir);\n\tmkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\n/** Create an ephemeral draft directory. The caller must promote or {@link discardDraftDir} it. */\nexport function makeDraftDir(): string {\n\treturn mkdtempSync(path.join(tmpdir(), \"hoo-plugin-draft-\"));\n}\n\n/** Delete a draft directory. Safe to call twice. */\nexport function discardDraftDir(dir: string): void {\n\trmSync(dir, { recursive: true, force: true });\n}\n"]}
1
+ {"version":3,"file":"locations.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/locations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where plugins live on disk.\n *\n * Three distinct roles, deliberately named apart — an earlier design called all\n * three \"staging\", which is how the wrong directory ends up in code:\n *\n * Draft dir Ephemeral. Holds an authored plugin while the eval gates\n * run, and is deleted on any failure. Nothing here is ever\n * loaded; it is promoted into a production home or thrown\n * away. {@link makeDraftDir}\n * Consumption home Persistent. Where marketplace installs land, in whatever\n * format the marketplace served. hoocode installing for\n * itself, at the scope the caller chose\n * ({@link installHomeForScope}).\n * Production home Persistent, global, per platform. Where a plugin hoocode\n * *authored* lives. {@link productionPluginDir}\n *\n * Draft and production are user-scoped without exception. Consumption is the one\n * that takes a scope, and `user` stays the default: a plugin is portable,\n * versioned and reusable across projects, so the working tree is the wrong place\n * for it *by default* — the capability would be invisible in every other repo,\n * and an autonomous install would dirty `git status` with content unrelated to\n * the task. `project` exists for the other case, where a team wants the plugin\n * pinned in the repo and shared with collaborators, and is a deliberate choice\n * someone makes per install or per setting rather than a default anything falls\n * into.\n *\n * Authoring stays user-scoped regardless (architecture doc §5.5): a project\n * destination for an *authored* plugin has no coherent production home, whereas\n * an installed plugin has an obvious one.\n *\n * The two production homes are asymmetric because the vendors are. Claude Code\n * discovers `~/.claude/skills/<id>/` in place, with no install step, so an\n * authored plugin is live there on its next session. Copilot CLI has no\n * equivalent — `copilot plugin install` copies into a cache it owns — so a\n * github artifact lives in a hoocode-owned home and reaches the ecosystem\n * through the publish lane instead.\n *\n * See docs/plugin-system-architecture.md §5.3 and §8.3.\n */\n\nimport { mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../../config.js\";\nimport type { PluginPlatform } from \"./formats/platform-targets.js\";\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/**\n * Home root the global surfaces hang off. Derived from the agent dir rather than\n * `homedir()` so `HOOCODE_AGENT_DIR` relocates every plugin location together —\n * which is what makes these paths testable.\n */\nfunction homeRoot(agentDir: string): string {\n\treturn path.dirname(agentDir);\n}\n\n/** `~/.agents/plugins/` — where user-scoped marketplace installs land. Format-agnostic. */\nexport function consumptionPluginsDir(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugins\");\n}\n\n/**\n * `<cwd>/.agents/plugins/` — where project-scoped installs land.\n *\n * Also the home older versions installed *everything* into, back when scope was\n * not a choice (see docs/plugin-system-architecture.md §5.4). It stayed on the\n * discovery path the whole time, which is why project scope needs no loader\n * change: `defaultPluginDirs` already reads it, ahead of the user home, so a\n * project-scoped plugin shadows a user-scoped one of the same id.\n */\nexport function projectPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\n/**\n * Where a marketplace install goes.\n *\n * hoocode has no `enabledPlugins`-style registry — a plugin on the discovery\n * path is enabled — so scope is a *destination*, not a flag written elsewhere:\n *\n * user `~/.agents/plugins/` portable across every checkout, invisible to collaborators\n * project `<cwd>/.agents/plugins/` committed with the repo, shared, shadows the user copy\n */\nexport type PluginInstallScope = \"user\" | \"project\";\n\n/** Resolve the install home for `scope`. */\nexport function installHomeForScope(scope: PluginInstallScope, cwd: string, agentDir: string = getAgentDir()): string {\n\treturn scope === \"project\" ? projectPluginsDir(cwd) : consumptionPluginsDir(agentDir);\n}\n\n/**\n * Which scope a plugin already on disk belongs to, derived from where it lives.\n *\n * `repo` is the third answer the install scopes do not cover: a plugin under\n * `<cwd>/.claude/skills` arrived with the repository rather than through an\n * install, so it is project-*located* but nobody here chose it. Keeping it\n * distinct from `project` is what lets `ListPlugins` say which plugins are\n * running because someone installed them and which because they were cloned.\n */\nexport function pluginScopeOf(pluginRoot: string, cwd: string): PluginInstallScope | \"repo\" {\n\tconst under = (root: string): boolean => {\n\t\tconst normalized = path.resolve(root);\n\t\tconst target = path.resolve(pluginRoot);\n\t\treturn target === normalized || target.startsWith(`${normalized}${path.sep}`);\n\t};\n\tif (under(projectPluginsDir(cwd)) || under(path.join(cwd, \".hoocode\", \"plugins\"))) return \"project\";\n\tif (under(path.join(cwd, \".claude\", \"skills\")) || under(path.join(cwd, \".agents\", \"skills\"))) return \"repo\";\n\treturn \"user\";\n}\n\n/**\n * Root of a platform's production home — the parent that {@link productionPluginDir}\n * places plugin directories under.\n */\nexport function productionRoot(platform: PluginPlatform, agentDir: string = getAgentDir()): string {\n\t// Claude: the documented skills-directory drop-in. A folder here carrying a\n\t// `.claude-plugin/plugin.json` loads as `<id>@skills-dir` with no install step.\n\tif (platform === \"claude\") return path.join(homeRoot(agentDir), \".claude\", \"skills\");\n\t// GitHub: no vendor drop-in exists, so this is hoocode's own home for a\n\t// github-targeted artifact. PackagePlugin works in it and publish reads from it.\n\treturn path.join(homeRoot(agentDir), \".agents\", \"publish\", \"github\");\n}\n\n/** Where an authored plugin for `platform` lives. */\nexport function productionPluginDir(platform: PluginPlatform, id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(productionRoot(platform, agentDir), sanitizeForDir(id));\n}\n\n/** `~/.agents/marketplaces.json` — the added-marketplace registry. */\nexport function marketplaceStorePath(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplaces.json\");\n}\n\n/** Root of the local marketplace clone cache. A cache is never repo content. */\nexport function marketplaceCacheRoot(agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"marketplace-cache\");\n}\n\n/** Local cache directory for a marketplace fetched from `url`. */\nexport function marketplaceCacheDir(url: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), sanitizeForDir(url));\n}\n\n/**\n * Records when each cached marketplace index was last fetched.\n *\n * Kept beside the caches rather than inferred from directory mtimes: a clone's\n * mtime moves for reasons that have nothing to do with freshness, and \"when did\n * we last talk to the remote\" is the only question the TTL is asking.\n */\nexport function marketplaceCacheMetaPath(agentDir: string = getAgentDir()): string {\n\treturn path.join(marketplaceCacheRoot(agentDir), \".fetched.json\");\n}\n\n/**\n * The parent directories hoocode owns and may therefore remove a plugin from:\n * the two production homes, both consumption homes (user and project), and the\n * `.hoocode/plugins` home older versions installed into.\n *\n * Deliberately excludes `<cwd>/.claude/skills` and `<cwd>/.agents/skills`. Those\n * are discovered (loader.ts `defaultPluginDirs`) but are repository content a\n * team committed, so uninstall must not delete out of them — hoocode installed\n * nothing there.\n */\nexport function pluginHomeRoots(cwd: string, agentDir: string = getAgentDir()): string[] {\n\treturn [\n\t\tproductionRoot(\"claude\", agentDir),\n\t\tproductionRoot(\"github\", agentDir),\n\t\tconsumptionPluginsDir(agentDir),\n\t\tprojectPluginsDir(cwd),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\"),\n\t];\n}\n\n/**\n * Every directory a plugin with `id` could occupy, in the order a lookup should\n * try them: the platform production homes, then the consumption home, then the\n * legacy project homes. Used to find an existing plugin without knowing which\n * role wrote it.\n */\nexport function candidatePluginDirs(cwd: string, id: string, agentDir: string = getAgentDir()): string[] {\n\tconst slug = sanitizeForDir(id);\n\treturn pluginHomeRoots(cwd, agentDir).map((root) => path.join(root, slug));\n}\n\n/**\n * Persistent, writable directory unique to an installed plugin — the target of\n * `${CLAUDE_PLUGIN_DATA}` / `${COPILOT_PLUGIN_DATA}`, which both vendors document\n * as the place for plugin runtime state.\n *\n * Deliberately outside every plugin home. A plugin's own directory is replaced\n * wholesale on promote and deleted on uninstall, and the vendors are explicit\n * that this must not live \"inside the installed-plugins cache directory\" — state\n * that vanishes on reinstall is not state.\n */\nexport function pluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\treturn path.join(homeRoot(agentDir), \".agents\", \"plugin-data\", sanitizeForDir(id));\n}\n\n/** {@link pluginDataDir}, created if absent. */\nexport function ensurePluginDataDir(id: string, agentDir: string = getAgentDir()): string {\n\tconst dir = pluginDataDir(id, agentDir);\n\tmkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\n/** Create an ephemeral draft directory. The caller must promote or {@link discardDraftDir} it. */\nexport function makeDraftDir(): string {\n\treturn mkdtempSync(path.join(tmpdir(), \"hoo-plugin-draft-\"));\n}\n\n/** Delete a draft directory. Safe to call twice. */\nexport function discardDraftDir(dir: string): void {\n\trmSync(dir, { recursive: true, force: true });\n}\n"]}
@@ -44,8 +44,30 @@ interface MarketplacePluginSourceGitSubdir {
44
44
  ref?: string;
45
45
  sha?: string;
46
46
  }
47
- /** Source value authored in a marketplace manifest. */
48
- export type MarketplacePluginSource = string | MarketplacePluginSourceUrl | MarketplacePluginSourceGitSubdir;
47
+ /** Structured source pointing at an npm package. */
48
+ interface MarketplacePluginSourceNpm {
49
+ source: "npm";
50
+ package: string;
51
+ version?: string;
52
+ registry?: string;
53
+ }
54
+ /** Structured source pointing at a zip archive served over HTTPS. */
55
+ interface MarketplacePluginSourceArchive {
56
+ source: "archive";
57
+ url: string;
58
+ sha256?: string;
59
+ }
60
+ /**
61
+ * Source value authored in a marketplace manifest.
62
+ *
63
+ * `npm` and `archive` are in the union despite not being installable here. They
64
+ * are documented vendor source types, so an entry using one is a *valid* catalog
65
+ * entry hoocode cannot fetch — which is a different thing from a malformed entry,
66
+ * and has to read differently. Dropping them at parse time made the plugin vanish
67
+ * from SearchPlugins entirely, so the failure mode was "that plugin does not
68
+ * exist" rather than "hoocode cannot install that kind yet".
69
+ */
70
+ export type MarketplacePluginSource = string | MarketplacePluginSourceUrl | MarketplacePluginSourceGitSubdir | MarketplacePluginSourceNpm | MarketplacePluginSourceArchive;
49
71
  interface MarketplacePluginEntry {
50
72
  name: string;
51
73
  /** Relative path, git URL, `npm:<spec>`, or structured source object. */
@@ -75,7 +97,7 @@ export interface NormalizedMarketplace {
75
97
  manifestPath: string;
76
98
  plugins: MarketplacePluginEntry[];
77
99
  }
78
- /** A resolved, installable plugin source. */
100
+ /** A resolved plugin source. The last two are recognized but not fetchable here. */
79
101
  export type ResolvedPluginSource = {
80
102
  kind: "local";
81
103
  path: string;
@@ -93,6 +115,10 @@ export type ResolvedPluginSource = {
93
115
  } | {
94
116
  kind: "npm";
95
117
  spec: string;
118
+ } | {
119
+ kind: "archive";
120
+ url: string;
121
+ sha256?: string;
96
122
  };
97
123
  /**
98
124
  * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then