@kolisachint/hoocode-agent 0.5.12 → 0.5.14

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 (91) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/config.d.ts +9 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +16 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/context-files.d.ts +16 -3
  7. package/dist/core/context-files.d.ts.map +1 -1
  8. package/dist/core/context-files.js +77 -6
  9. package/dist/core/context-files.js.map +1 -1
  10. package/dist/core/extensions/loader.d.ts +18 -6
  11. package/dist/core/extensions/loader.d.ts.map +1 -1
  12. package/dist/core/extensions/loader.js +35 -19
  13. package/dist/core/extensions/loader.js.map +1 -1
  14. package/dist/core/extensions/plugins/install.d.ts +51 -10
  15. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  16. package/dist/core/extensions/plugins/install.js +178 -31
  17. package/dist/core/extensions/plugins/install.js.map +1 -1
  18. package/dist/core/extensions/plugins/listing.d.ts +8 -2
  19. package/dist/core/extensions/plugins/listing.d.ts.map +1 -1
  20. package/dist/core/extensions/plugins/listing.js +29 -5
  21. package/dist/core/extensions/plugins/listing.js.map +1 -1
  22. package/dist/core/extensions/plugins/locations.d.ts +62 -12
  23. package/dist/core/extensions/plugins/locations.d.ts.map +1 -1
  24. package/dist/core/extensions/plugins/locations.js +74 -18
  25. package/dist/core/extensions/plugins/locations.js.map +1 -1
  26. package/dist/core/extensions/plugins/marketplace.d.ts +29 -3
  27. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
  28. package/dist/core/extensions/plugins/marketplace.js +40 -7
  29. package/dist/core/extensions/plugins/marketplace.js.map +1 -1
  30. package/dist/core/extensions/plugins/trust.d.ts +56 -0
  31. package/dist/core/extensions/plugins/trust.d.ts.map +1 -0
  32. package/dist/core/extensions/plugins/trust.js +88 -0
  33. package/dist/core/extensions/plugins/trust.js.map +1 -0
  34. package/dist/core/learn/digest.d.ts +17 -0
  35. package/dist/core/learn/digest.d.ts.map +1 -0
  36. package/dist/core/learn/digest.js +131 -0
  37. package/dist/core/learn/digest.js.map +1 -0
  38. package/dist/core/learn/extract.d.ts +178 -0
  39. package/dist/core/learn/extract.d.ts.map +1 -0
  40. package/dist/core/learn/extract.js +705 -0
  41. package/dist/core/learn/extract.js.map +1 -0
  42. package/dist/core/learn/normalize.d.ts +65 -0
  43. package/dist/core/learn/normalize.d.ts.map +1 -0
  44. package/dist/core/learn/normalize.js +245 -0
  45. package/dist/core/learn/normalize.js.map +1 -0
  46. package/dist/core/learn/state.d.ts +115 -0
  47. package/dist/core/learn/state.d.ts.map +1 -0
  48. package/dist/core/learn/state.js +151 -0
  49. package/dist/core/learn/state.js.map +1 -0
  50. package/dist/core/settings-defaults.d.ts +6 -0
  51. package/dist/core/settings-defaults.d.ts.map +1 -1
  52. package/dist/core/settings-defaults.js +6 -0
  53. package/dist/core/settings-defaults.js.map +1 -1
  54. package/dist/core/settings-manager.d.ts +21 -0
  55. package/dist/core/settings-manager.d.ts.map +1 -1
  56. package/dist/core/settings-manager.js +31 -0
  57. package/dist/core/settings-manager.js.map +1 -1
  58. package/dist/core/settings-types.d.ts +6 -0
  59. package/dist/core/settings-types.d.ts.map +1 -1
  60. package/dist/core/settings-types.js.map +1 -1
  61. package/dist/core/tools/plugins.d.ts.map +1 -1
  62. package/dist/core/tools/plugins.js +18 -3
  63. package/dist/core/tools/plugins.js.map +1 -1
  64. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  65. package/dist/extensions/core/hoo-core.js +2 -0
  66. package/dist/extensions/core/hoo-core.js.map +1 -1
  67. package/dist/extensions/core/learn.d.ts +22 -0
  68. package/dist/extensions/core/learn.d.ts.map +1 -0
  69. package/dist/extensions/core/learn.js +167 -0
  70. package/dist/extensions/core/learn.js.map +1 -0
  71. package/dist/extensions/core/marketplace.d.ts +7 -3
  72. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  73. package/dist/extensions/core/marketplace.js +103 -11
  74. package/dist/extensions/core/marketplace.js.map +1 -1
  75. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  76. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/settings-selector.js +15 -2
  78. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.js +6 -0
  81. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  82. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  83. package/dist/modes/interactive/resource-display.js +6 -1
  84. package/dist/modes/interactive/resource-display.js.map +1 -1
  85. package/docs/settings.md +31 -0
  86. package/docs/usage.md +64 -2
  87. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  88. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  89. package/examples/extensions/sandbox/package.json +1 -1
  90. package/examples/extensions/with-deps/package.json +1 -1
  91. package/package.json +4 -4
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"marketplace.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;GAQG;AACH,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAO9D,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,EAAE,CASxE;AAED,8DAA8D;AAC9D,UAAU,0BAA0B;IACnC,MAAM,EAAE,KAAK,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,4EAA4E;AAC5E,UAAU,gCAAgC;IACzC,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,uDAAuD;AACvD,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,0BAA0B,GAAG,gCAAgC,CAAC;AAE7G,UAAU,sBAAsB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC;;;;;;OAMG;IACH,eAAe,EAAE,mBAAmB,EAAE,CAAC;IACvC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAiED,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAUjC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAwD7E;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,GAAG,oBAAoB,CAYlH;AAMD,MAAM,WAAW,iBAAiB;IACjC,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;CACZ;AAMD,8DAA8D;AAC9D,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAG3E;AAED,mDAAmD;AACnD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAG3F","sourcesContent":["/**\n * Plugin marketplaces — install plugins from a curated index hosted in a git repo\n * (or a local directory).\n *\n * A marketplace is a directory containing an index manifest in one of three formats:\n * - Native: `.agents-plugin/marketplace.json` (preferred; the `.agents` surface)\n * - Claude: `.claude-plugin/marketplace.json`\n * - Copilot: `.github/marketplace.json` (Copilot-style git index)\n *\n * All three use the same shape: `{ name?, owner?, plugins: [{ name, source, description? }] }`.\n * A plugin `source` is either a path relative to the marketplace root (a plugin\n * directory inside the repo), a git URL, or an `npm:<spec>` reference.\n *\n * When more than one is present the native `.agents-plugin` format wins, then\n * Claude, then Copilot (no merge) — matching the \"`.agents/` first\" policy used\n * across hoocode's resource surfaces.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { PLUGIN_FORMATS } from \"./formats/index.js\";\nimport { normalizePlatformToken } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/**\n * Canonical platform token used by the optional `supportPlatform` field. The\n * `.github/marketplace.json` (Copilot-style) format is surfaced as `\"github\"` —\n * friendlier than the internal `format: \"copilot\"` and matching where the file\n * lives. Authored aliases (`copilot`, `gh`) normalize to `github`.\n *\n * Re-exported from the format registry so the platform vocabulary has a single\n * source of truth.\n */\nexport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Map an internal marketplace `format` to its public platform token. */\nfunction formatToPlatform(format: NormalizedMarketplace[\"format\"]): MarketplacePlatform {\n\treturn format === \"copilot\" ? \"github\" : format;\n}\n\n/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */\nexport function normalizePlatforms(value: unknown): MarketplacePlatform[] {\n\tconst raw = Array.isArray(value) ? value : value == null ? [] : [value];\n\tconst out: MarketplacePlatform[] = [];\n\tfor (const v of raw) {\n\t\tif (typeof v !== \"string\") continue;\n\t\tconst canonical = normalizePlatformToken(v);\n\t\tif (canonical && !out.includes(canonical)) out.push(canonical);\n\t}\n\treturn out;\n}\n\n/** Structured source pointing at an entire git repository. */\ninterface MarketplacePluginSourceUrl {\n\tsource: \"url\";\n\turl: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at a subdirectory within a git repository. */\ninterface MarketplacePluginSourceGitSubdir {\n\tsource: \"git-subdir\";\n\turl: string;\n\t/** Subdirectory path inside the repository. */\n\tpath: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Source value authored in a marketplace manifest. */\nexport type MarketplacePluginSource = string | MarketplacePluginSourceUrl | MarketplacePluginSourceGitSubdir;\n\ninterface MarketplacePluginEntry {\n\tname: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\tdescription?: string;\n\t/**\n\t * Optional platform(s) this entry targets. Only set when authored on the\n\t * entry; absent means \"no per-entry restriction\" (the marketplace's\n\t * platforms apply). Purely informational today — nothing filters on it.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n}\n\nexport interface NormalizedMarketplace {\n\tname: string;\n\t/** Precedence winner when several index files conflict (agents > claude > copilot). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this marketplace directory offers, so a conflict between\n\t * multiple index files (e.g. both `.github/` and `.claude-plugin/`) is\n\t * recorded rather than hidden. Always includes the resolved `format`'s\n\t * platform; also folds in any authored top-level `supportPlatform`. Never\n\t * empty.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Absolute path to the marketplace directory. */\n\troot: string;\n\tmanifestPath: string;\n\tplugins: MarketplacePluginEntry[];\n}\n\ninterface RawMarketplace {\n\tname?: string;\n\t/** Both Claude Code and Copilot CLI define `owner` as `{ name, email? }`; a bare string is tolerated. */\n\towner?: string | { name?: string; email?: string };\n\t/** Shared optional metadata: `pluginRoot` is a base dir prepended to relative plugin source paths. */\n\tmetadata?: { description?: string; version?: string; pluginRoot?: string };\n\t/** Optional authored platform hint (string | string[]); folded into supportPlatform. */\n\tsupportPlatform?: unknown;\n\tplugins?: Array<{ name?: string; source?: unknown; description?: string; supportPlatform?: unknown }>;\n}\n\n/** True when a string source is a path (not npm:/git/URL) that `pluginRoot` should prefix. */\nfunction isRelativePathSource(source: string): boolean {\n\tconst s = source.trim();\n\treturn (\n\t\t!s.startsWith(\"npm:\") &&\n\t\t!/^https?:\\/\\//.test(s) &&\n\t\t!s.startsWith(\"git@\") &&\n\t\t!s.endsWith(\".git\") &&\n\t\t!path.isAbsolute(s)\n\t);\n}\n\n/**\n * Apply `metadata.pluginRoot` (Claude Code and Copilot CLI share the field):\n * a base directory prepended to relative plugin source paths, so\n * `\"source\": \"formatter\"` with `\"pluginRoot\": \"./plugins\"` resolves like\n * `\"./plugins/formatter\"`.\n */\nfunction applyPluginRoot(source: MarketplacePluginSource, pluginRoot: string | undefined): MarketplacePluginSource {\n\tif (!pluginRoot || typeof source !== \"string\" || !isRelativePathSource(source)) return source;\n\treturn `${pluginRoot.replace(/\\/+$/, \"\")}/${source.trim().replace(/^\\.\\//, \"\")}`;\n}\n\n/** Validate and normalize an authored source value. Returns null for unsupported/invalid shapes. */\nfunction normalizeSource(source: unknown): MarketplacePluginSource | null {\n\tif (typeof source === \"string\") return source;\n\tif (!source || typeof source !== \"object\") return null;\n\tconst obj = source as Record<string, unknown>;\n\tconst src = obj.source;\n\tconst url = obj.url;\n\tconst pin = {\n\t\t...(typeof obj.ref === \"string\" ? { ref: obj.ref } : {}),\n\t\t...(typeof obj.sha === \"string\" ? { sha: obj.sha } : {}),\n\t};\n\tif (src === \"url\" && typeof url === \"string\") {\n\t\treturn { source: \"url\", url, ...pin };\n\t}\n\tif (src === \"git-subdir\" && typeof url === \"string\" && typeof obj.path === \"string\") {\n\t\treturn { source: \"git-subdir\", url, path: obj.path, ...pin };\n\t}\n\t// Copilot marketplace shorthand: { source: \"github\", repo: \"owner/name\", path? }\n\t// (used by github/copilot-plugins). Normalize to the equivalent git source.\n\tif (src === \"github\" && typeof obj.repo === \"string\" && /^[\\w.-]+\\/[\\w.-]+$/.test(obj.repo)) {\n\t\tconst repoUrl = `https://github.com/${obj.repo}.git`;\n\t\tif (typeof obj.path === \"string\" && obj.path.length > 0) {\n\t\t\treturn { source: \"git-subdir\", url: repoUrl, path: obj.path, ...pin };\n\t\t}\n\t\treturn { source: \"url\", url: repoUrl, ...pin };\n\t}\n\treturn null;\n}\n\n/** A resolved, installable plugin source. */\nexport type ResolvedPluginSource =\n\t| { kind: \"local\"; path: string }\n\t| { kind: \"git\"; url: string; ref?: string; sha?: string }\n\t| { kind: \"git-subdir\"; url: string; path: string; ref?: string; sha?: string }\n\t| { kind: \"npm\"; spec: string };\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then\n * Copilot when more than one file is present. Returns null if no manifest exists\n * or it is invalid.\n */\nexport function parseMarketplaceDir(dir: string): NormalizedMarketplace | null {\n\t// Record every index format present so a conflict (more than one file) is\n\t// visible via supportPlatform, not silently dropped. PLUGIN_FORMATS is\n\t// precedence-ordered (native > Claude > Copilot), so `present` inherits it.\n\tconst present: Array<{ format: NormalizedMarketplace[\"format\"]; path: string }> = [];\n\tfor (const fmt of PLUGIN_FORMATS) {\n\t\tfor (const candidate of fmt.marketplaceFiles) {\n\t\t\tconst p = path.join(dir, candidate);\n\t\t\tif (fs.existsSync(p)) {\n\t\t\t\tpresent.push({ format: fmt.id, path: p });\n\t\t\t\tbreak; // first candidate wins within a format\n\t\t\t}\n\t\t}\n\t}\n\tif (present.length === 0) return null;\n\n\t// Precedence winner (present is built in agents > claude > copilot order).\n\tconst { format, path: manifestPath } = present[0];\n\n\tconst raw = readJson<RawMarketplace>(manifestPath);\n\tif (!raw) return null;\n\n\tconst pluginRoot =\n\t\ttypeof raw.metadata?.pluginRoot === \"string\" && raw.metadata.pluginRoot.trim()\n\t\t\t? raw.metadata.pluginRoot.trim()\n\t\t\t: undefined;\n\n\tconst plugins: MarketplacePluginEntry[] = [];\n\tfor (const entry of raw.plugins ?? []) {\n\t\tif (!entry?.name || !entry.source) continue;\n\t\tconst normalized = normalizeSource(entry.source);\n\t\tif (!normalized) continue;\n\t\tconst entryPlatforms = normalizePlatforms(entry.supportPlatform);\n\t\tplugins.push({\n\t\t\tname: entry.name,\n\t\t\tsource: applyPluginRoot(normalized, pluginRoot),\n\t\t\tdescription: entry.description,\n\t\t\t...(entryPlatforms.length > 0 ? { supportPlatform: entryPlatforms } : {}),\n\t\t});\n\t}\n\n\t// supportPlatform = platforms of all present index files, plus any authored\n\t// top-level hint, deduped. Always non-empty (at least the resolved format).\n\tconst supportPlatform: MarketplacePlatform[] = [];\n\tfor (const p of [...present.map((e) => formatToPlatform(e.format)), ...normalizePlatforms(raw.supportPlatform)]) {\n\t\tif (!supportPlatform.includes(p)) supportPlatform.push(p);\n\t}\n\n\treturn {\n\t\tname: (raw.name ?? path.basename(dir)).trim() || path.basename(dir),\n\t\tformat,\n\t\tsupportPlatform,\n\t\troot: dir,\n\t\tmanifestPath,\n\t\tplugins,\n\t};\n}\n\n/** Classify and resolve a plugin `source` against its marketplace root. */\nexport function resolvePluginSource(source: MarketplacePluginSource, marketplaceRoot: string): ResolvedPluginSource {\n\tif (typeof source === \"string\") {\n\t\tconst s = source.trim();\n\t\tif (s.startsWith(\"npm:\")) return { kind: \"npm\", spec: s.slice(4) };\n\t\tif (/^https?:\\/\\//.test(s) || s.startsWith(\"git@\") || s.endsWith(\".git\")) return { kind: \"git\", url: s };\n\t\t// Otherwise a path relative to (or absolute within) the marketplace repo.\n\t\treturn { kind: \"local\", path: path.isAbsolute(s) ? s : path.resolve(marketplaceRoot, s) };\n\t}\n\tif (source.source === \"url\") {\n\t\treturn { kind: \"git\", url: source.url, ref: source.ref, sha: source.sha };\n\t}\n\treturn { kind: \"git-subdir\", url: source.url, path: source.path, ref: source.ref, sha: source.sha };\n}\n\n// ============================================================================\n// Marketplace registry (persisted list of added marketplaces)\n// ============================================================================\n\nexport interface MarketplaceRecord {\n\t/** Original location the user added (git URL or local path). */\n\tlocation: string;\n\t/** Local directory to read the manifest from (clone dir for git, else location). */\n\tdir: string;\n}\n\ninterface MarketplaceStoreFile {\n\tmarketplaces?: MarketplaceRecord[];\n}\n\n/** Read the persisted marketplace list from `<storePath>`. */\nexport function readMarketplaceStore(storePath: string): MarketplaceRecord[] {\n\tconst parsed = readJson<MarketplaceStoreFile>(storePath);\n\treturn Array.isArray(parsed?.marketplaces) ? parsed.marketplaces : [];\n}\n\n/** Write the marketplace list to `<storePath>`. */\nexport function writeMarketplaceStore(storePath: string, records: MarketplaceRecord[]): void {\n\tfs.mkdirSync(path.dirname(storePath), { recursive: true });\n\tfs.writeFileSync(storePath, `${JSON.stringify({ marketplaces: records }, null, 2)}\\n`, \"utf8\");\n}\n"]}
1
+ {"version":3,"file":"marketplace.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin marketplaces — install plugins from a curated index hosted in a git repo\n * (or a local directory).\n *\n * A marketplace is a directory containing an index manifest in one of three formats:\n * - Native: `.agents-plugin/marketplace.json` (preferred; the `.agents` surface)\n * - Claude: `.claude-plugin/marketplace.json`\n * - Copilot: `.github/marketplace.json` (Copilot-style git index)\n *\n * All three use the same shape: `{ name?, owner?, plugins: [{ name, source, description? }] }`.\n * A plugin `source` is either a path relative to the marketplace root (a plugin\n * directory inside the repo), a git URL, or an `npm:<spec>` reference.\n *\n * When more than one is present the native `.agents-plugin` format wins, then\n * Claude, then Copilot (no merge) — matching the \"`.agents/` first\" policy used\n * across hoocode's resource surfaces.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { PLUGIN_FORMATS } from \"./formats/index.js\";\nimport { normalizePlatformToken } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/**\n * Canonical platform token used by the optional `supportPlatform` field. The\n * `.github/marketplace.json` (Copilot-style) format is surfaced as `\"github\"` —\n * friendlier than the internal `format: \"copilot\"` and matching where the file\n * lives. Authored aliases (`copilot`, `gh`) normalize to `github`.\n *\n * Re-exported from the format registry so the platform vocabulary has a single\n * source of truth.\n */\nexport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Map an internal marketplace `format` to its public platform token. */\nfunction formatToPlatform(format: NormalizedMarketplace[\"format\"]): MarketplacePlatform {\n\treturn format === \"copilot\" ? \"github\" : format;\n}\n\n/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */\nexport function normalizePlatforms(value: unknown): MarketplacePlatform[] {\n\tconst raw = Array.isArray(value) ? value : value == null ? [] : [value];\n\tconst out: MarketplacePlatform[] = [];\n\tfor (const v of raw) {\n\t\tif (typeof v !== \"string\") continue;\n\t\tconst canonical = normalizePlatformToken(v);\n\t\tif (canonical && !out.includes(canonical)) out.push(canonical);\n\t}\n\treturn out;\n}\n\n/** Structured source pointing at an entire git repository. */\ninterface MarketplacePluginSourceUrl {\n\tsource: \"url\";\n\turl: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at a subdirectory within a git repository. */\ninterface MarketplacePluginSourceGitSubdir {\n\tsource: \"git-subdir\";\n\turl: string;\n\t/** Subdirectory path inside the repository. */\n\tpath: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at an npm package. */\ninterface MarketplacePluginSourceNpm {\n\tsource: \"npm\";\n\tpackage: string;\n\tversion?: string;\n\tregistry?: string;\n}\n\n/** Structured source pointing at a zip archive served over HTTPS. */\ninterface MarketplacePluginSourceArchive {\n\tsource: \"archive\";\n\turl: string;\n\tsha256?: string;\n}\n\n/**\n * Source value authored in a marketplace manifest.\n *\n * `npm` and `archive` are in the union despite not being installable here. They\n * are documented vendor source types, so an entry using one is a *valid* catalog\n * entry hoocode cannot fetch — which is a different thing from a malformed entry,\n * and has to read differently. Dropping them at parse time made the plugin vanish\n * from SearchPlugins entirely, so the failure mode was \"that plugin does not\n * exist\" rather than \"hoocode cannot install that kind yet\".\n */\nexport type MarketplacePluginSource =\n\t| string\n\t| MarketplacePluginSourceUrl\n\t| MarketplacePluginSourceGitSubdir\n\t| MarketplacePluginSourceNpm\n\t| MarketplacePluginSourceArchive;\n\ninterface MarketplacePluginEntry {\n\tname: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\tdescription?: string;\n\t/**\n\t * Optional platform(s) this entry targets. Only set when authored on the\n\t * entry; absent means \"no per-entry restriction\" (the marketplace's\n\t * platforms apply). Purely informational today — nothing filters on it.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n}\n\nexport interface NormalizedMarketplace {\n\tname: string;\n\t/** Precedence winner when several index files conflict (agents > claude > copilot). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this marketplace directory offers, so a conflict between\n\t * multiple index files (e.g. both `.github/` and `.claude-plugin/`) is\n\t * recorded rather than hidden. Always includes the resolved `format`'s\n\t * platform; also folds in any authored top-level `supportPlatform`. Never\n\t * empty.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Absolute path to the marketplace directory. */\n\troot: string;\n\tmanifestPath: string;\n\tplugins: MarketplacePluginEntry[];\n}\n\ninterface RawMarketplace {\n\tname?: string;\n\t/** Both Claude Code and Copilot CLI define `owner` as `{ name, email? }`; a bare string is tolerated. */\n\towner?: string | { name?: string; email?: string };\n\t/** Shared optional metadata: `pluginRoot` is a base dir prepended to relative plugin source paths. */\n\tmetadata?: { description?: string; version?: string; pluginRoot?: string };\n\t/** Optional authored platform hint (string | string[]); folded into supportPlatform. */\n\tsupportPlatform?: unknown;\n\tplugins?: Array<{ name?: string; source?: unknown; description?: string; supportPlatform?: unknown }>;\n}\n\n/** True when a string source is a path (not npm:/git/URL) that `pluginRoot` should prefix. */\nfunction isRelativePathSource(source: string): boolean {\n\tconst s = source.trim();\n\treturn (\n\t\t!s.startsWith(\"npm:\") &&\n\t\t!/^https?:\\/\\//.test(s) &&\n\t\t!s.startsWith(\"git@\") &&\n\t\t!s.endsWith(\".git\") &&\n\t\t!path.isAbsolute(s)\n\t);\n}\n\n/**\n * Apply `metadata.pluginRoot` (Claude Code and Copilot CLI share the field):\n * a base directory prepended to relative plugin source paths, so\n * `\"source\": \"formatter\"` with `\"pluginRoot\": \"./plugins\"` resolves like\n * `\"./plugins/formatter\"`.\n */\nfunction applyPluginRoot(source: MarketplacePluginSource, pluginRoot: string | undefined): MarketplacePluginSource {\n\tif (!pluginRoot || typeof source !== \"string\" || !isRelativePathSource(source)) return source;\n\treturn `${pluginRoot.replace(/\\/+$/, \"\")}/${source.trim().replace(/^\\.\\//, \"\")}`;\n}\n\n/** Validate and normalize an authored source value. Returns null for unsupported/invalid shapes. */\nfunction normalizeSource(source: unknown): MarketplacePluginSource | null {\n\tif (typeof source === \"string\") return source;\n\tif (!source || typeof source !== \"object\") return null;\n\tconst obj = source as Record<string, unknown>;\n\tconst src = obj.source;\n\tconst url = obj.url;\n\t// `commit` is not in the documented schema but appears in the wild alongside\n\t// `sha` (anthropics/claude-plugins-official ships two such entries). It names\n\t// the same thing, so it stands in as the pin when `sha` is absent rather than\n\t// silently leaving the entry unpinned.\n\tconst shaValue = typeof obj.sha === \"string\" ? obj.sha : typeof obj.commit === \"string\" ? obj.commit : undefined;\n\tconst pin = {\n\t\t...(typeof obj.ref === \"string\" ? { ref: obj.ref } : {}),\n\t\t...(shaValue ? { sha: shaValue } : {}),\n\t};\n\tconst subdir = typeof obj.path === \"string\" && obj.path.length > 0 ? obj.path : undefined;\n\tif (src === \"url\" && typeof url === \"string\") {\n\t\t// A `url` entry carrying `path` means the plugin is a subdirectory of that\n\t\t// repo, exactly like `git-subdir`. Dropping the `path` used to install the\n\t\t// whole repository instead — six entries in the official Claude marketplace\n\t\t// resolve that way, and each produced a plugin directory with none of the\n\t\t// plugin in it.\n\t\tif (subdir) return { source: \"git-subdir\", url, path: subdir, ...pin };\n\t\treturn { source: \"url\", url, ...pin };\n\t}\n\tif (src === \"git-subdir\" && typeof url === \"string\" && subdir) {\n\t\treturn { source: \"git-subdir\", url, path: subdir, ...pin };\n\t}\n\t// Copilot marketplace shorthand: { source: \"github\", repo: \"owner/name\", path? }\n\t// (used by github/copilot-plugins and github/awesome-copilot). Normalize to the\n\t// equivalent git source.\n\tif (src === \"github\" && typeof obj.repo === \"string\" && /^[\\w.-]+\\/[\\w.-]+$/.test(obj.repo)) {\n\t\tconst repoUrl = `https://github.com/${obj.repo}.git`;\n\t\tif (subdir) return { source: \"git-subdir\", url: repoUrl, path: subdir, ...pin };\n\t\treturn { source: \"url\", url: repoUrl, ...pin };\n\t}\n\t// Documented types hoocode cannot fetch. Kept so the entry stays visible and\n\t// install can explain itself; see the MarketplacePluginSource docstring.\n\tif (src === \"npm\" && typeof obj.package === \"string\") {\n\t\treturn {\n\t\t\tsource: \"npm\",\n\t\t\tpackage: obj.package,\n\t\t\t...(typeof obj.version === \"string\" ? { version: obj.version } : {}),\n\t\t\t...(typeof obj.registry === \"string\" ? { registry: obj.registry } : {}),\n\t\t};\n\t}\n\tif (src === \"archive\" && typeof url === \"string\") {\n\t\treturn { source: \"archive\", url, ...(typeof obj.sha256 === \"string\" ? { sha256: obj.sha256 } : {}) };\n\t}\n\t// Anything else is malformed rather than merely unsupported, and stays dropped.\n\treturn null;\n}\n\n/** A resolved plugin source. The last two are recognized but not fetchable here. */\nexport type ResolvedPluginSource =\n\t| { kind: \"local\"; path: string }\n\t| { kind: \"git\"; url: string; ref?: string; sha?: string }\n\t| { kind: \"git-subdir\"; url: string; path: string; ref?: string; sha?: string }\n\t| { kind: \"npm\"; spec: string }\n\t| { kind: \"archive\"; url: string; sha256?: string };\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then\n * Copilot when more than one file is present. Returns null if no manifest exists\n * or it is invalid.\n */\nexport function parseMarketplaceDir(dir: string): NormalizedMarketplace | null {\n\t// Record every index format present so a conflict (more than one file) is\n\t// visible via supportPlatform, not silently dropped. PLUGIN_FORMATS is\n\t// precedence-ordered (native > Claude > Copilot), so `present` inherits it.\n\tconst present: Array<{ format: NormalizedMarketplace[\"format\"]; path: string }> = [];\n\tfor (const fmt of PLUGIN_FORMATS) {\n\t\tfor (const candidate of fmt.marketplaceFiles) {\n\t\t\tconst p = path.join(dir, candidate);\n\t\t\tif (fs.existsSync(p)) {\n\t\t\t\tpresent.push({ format: fmt.id, path: p });\n\t\t\t\tbreak; // first candidate wins within a format\n\t\t\t}\n\t\t}\n\t}\n\tif (present.length === 0) return null;\n\n\t// Precedence winner (present is built in agents > claude > copilot order).\n\tconst { format, path: manifestPath } = present[0];\n\n\tconst raw = readJson<RawMarketplace>(manifestPath);\n\tif (!raw) return null;\n\n\tconst pluginRoot =\n\t\ttypeof raw.metadata?.pluginRoot === \"string\" && raw.metadata.pluginRoot.trim()\n\t\t\t? raw.metadata.pluginRoot.trim()\n\t\t\t: undefined;\n\n\tconst plugins: MarketplacePluginEntry[] = [];\n\tfor (const entry of raw.plugins ?? []) {\n\t\tif (!entry?.name || !entry.source) continue;\n\t\tconst normalized = normalizeSource(entry.source);\n\t\tif (!normalized) continue;\n\t\tconst entryPlatforms = normalizePlatforms(entry.supportPlatform);\n\t\tplugins.push({\n\t\t\tname: entry.name,\n\t\t\tsource: applyPluginRoot(normalized, pluginRoot),\n\t\t\tdescription: entry.description,\n\t\t\t...(entryPlatforms.length > 0 ? { supportPlatform: entryPlatforms } : {}),\n\t\t});\n\t}\n\n\t// supportPlatform = platforms of all present index files, plus any authored\n\t// top-level hint, deduped. Always non-empty (at least the resolved format).\n\tconst supportPlatform: MarketplacePlatform[] = [];\n\tfor (const p of [...present.map((e) => formatToPlatform(e.format)), ...normalizePlatforms(raw.supportPlatform)]) {\n\t\tif (!supportPlatform.includes(p)) supportPlatform.push(p);\n\t}\n\n\treturn {\n\t\tname: (raw.name ?? path.basename(dir)).trim() || path.basename(dir),\n\t\tformat,\n\t\tsupportPlatform,\n\t\troot: dir,\n\t\tmanifestPath,\n\t\tplugins,\n\t};\n}\n\n/** Classify and resolve a plugin `source` against its marketplace root. */\nexport function resolvePluginSource(source: MarketplacePluginSource, marketplaceRoot: string): ResolvedPluginSource {\n\tif (typeof source === \"string\") {\n\t\tconst s = source.trim();\n\t\tif (s.startsWith(\"npm:\")) return { kind: \"npm\", spec: s.slice(4) };\n\t\tif (/^https?:\\/\\//.test(s) || s.startsWith(\"git@\") || s.endsWith(\".git\")) return { kind: \"git\", url: s };\n\t\t// Otherwise a path relative to (or absolute within) the marketplace repo.\n\t\treturn { kind: \"local\", path: path.isAbsolute(s) ? s : path.resolve(marketplaceRoot, s) };\n\t}\n\tif (source.source === \"url\") {\n\t\treturn { kind: \"git\", url: source.url, ref: source.ref, sha: source.sha };\n\t}\n\tif (source.source === \"npm\") {\n\t\treturn { kind: \"npm\", spec: source.version ? `${source.package}@${source.version}` : source.package };\n\t}\n\tif (source.source === \"archive\") {\n\t\treturn { kind: \"archive\", url: source.url, sha256: source.sha256 };\n\t}\n\treturn { kind: \"git-subdir\", url: source.url, path: source.path, ref: source.ref, sha: source.sha };\n}\n\n// ============================================================================\n// Marketplace registry (persisted list of added marketplaces)\n// ============================================================================\n\nexport interface MarketplaceRecord {\n\t/** Original location the user added (git URL or local path). */\n\tlocation: string;\n\t/** Local directory to read the manifest from (clone dir for git, else location). */\n\tdir: string;\n}\n\ninterface MarketplaceStoreFile {\n\tmarketplaces?: MarketplaceRecord[];\n}\n\n/** Read the persisted marketplace list from `<storePath>`. */\nexport function readMarketplaceStore(storePath: string): MarketplaceRecord[] {\n\tconst parsed = readJson<MarketplaceStoreFile>(storePath);\n\treturn Array.isArray(parsed?.marketplaces) ? parsed.marketplaces : [];\n}\n\n/** Write the marketplace list to `<storePath>`. */\nexport function writeMarketplaceStore(storePath: string, records: MarketplaceRecord[]): void {\n\tfs.mkdirSync(path.dirname(storePath), { recursive: true });\n\tfs.writeFileSync(storePath, `${JSON.stringify({ marketplaces: records }, null, 2)}\\n`, \"utf8\");\n}\n"]}
@@ -65,25 +65,52 @@ function normalizeSource(source) {
65
65
  const obj = source;
66
66
  const src = obj.source;
67
67
  const url = obj.url;
68
+ // `commit` is not in the documented schema but appears in the wild alongside
69
+ // `sha` (anthropics/claude-plugins-official ships two such entries). It names
70
+ // the same thing, so it stands in as the pin when `sha` is absent rather than
71
+ // silently leaving the entry unpinned.
72
+ const shaValue = typeof obj.sha === "string" ? obj.sha : typeof obj.commit === "string" ? obj.commit : undefined;
68
73
  const pin = {
69
74
  ...(typeof obj.ref === "string" ? { ref: obj.ref } : {}),
70
- ...(typeof obj.sha === "string" ? { sha: obj.sha } : {}),
75
+ ...(shaValue ? { sha: shaValue } : {}),
71
76
  };
77
+ const subdir = typeof obj.path === "string" && obj.path.length > 0 ? obj.path : undefined;
72
78
  if (src === "url" && typeof url === "string") {
79
+ // A `url` entry carrying `path` means the plugin is a subdirectory of that
80
+ // repo, exactly like `git-subdir`. Dropping the `path` used to install the
81
+ // whole repository instead — six entries in the official Claude marketplace
82
+ // resolve that way, and each produced a plugin directory with none of the
83
+ // plugin in it.
84
+ if (subdir)
85
+ return { source: "git-subdir", url, path: subdir, ...pin };
73
86
  return { source: "url", url, ...pin };
74
87
  }
75
- if (src === "git-subdir" && typeof url === "string" && typeof obj.path === "string") {
76
- return { source: "git-subdir", url, path: obj.path, ...pin };
88
+ if (src === "git-subdir" && typeof url === "string" && subdir) {
89
+ return { source: "git-subdir", url, path: subdir, ...pin };
77
90
  }
78
91
  // Copilot marketplace shorthand: { source: "github", repo: "owner/name", path? }
79
- // (used by github/copilot-plugins). Normalize to the equivalent git source.
92
+ // (used by github/copilot-plugins and github/awesome-copilot). Normalize to the
93
+ // equivalent git source.
80
94
  if (src === "github" && typeof obj.repo === "string" && /^[\w.-]+\/[\w.-]+$/.test(obj.repo)) {
81
95
  const repoUrl = `https://github.com/${obj.repo}.git`;
82
- if (typeof obj.path === "string" && obj.path.length > 0) {
83
- return { source: "git-subdir", url: repoUrl, path: obj.path, ...pin };
84
- }
96
+ if (subdir)
97
+ return { source: "git-subdir", url: repoUrl, path: subdir, ...pin };
85
98
  return { source: "url", url: repoUrl, ...pin };
86
99
  }
100
+ // Documented types hoocode cannot fetch. Kept so the entry stays visible and
101
+ // install can explain itself; see the MarketplacePluginSource docstring.
102
+ if (src === "npm" && typeof obj.package === "string") {
103
+ return {
104
+ source: "npm",
105
+ package: obj.package,
106
+ ...(typeof obj.version === "string" ? { version: obj.version } : {}),
107
+ ...(typeof obj.registry === "string" ? { registry: obj.registry } : {}),
108
+ };
109
+ }
110
+ if (src === "archive" && typeof url === "string") {
111
+ return { source: "archive", url, ...(typeof obj.sha256 === "string" ? { sha256: obj.sha256 } : {}) };
112
+ }
113
+ // Anything else is malformed rather than merely unsupported, and stays dropped.
87
114
  return null;
88
115
  }
89
116
  function readJson(file) {
@@ -168,6 +195,12 @@ export function resolvePluginSource(source, marketplaceRoot) {
168
195
  if (source.source === "url") {
169
196
  return { kind: "git", url: source.url, ref: source.ref, sha: source.sha };
170
197
  }
198
+ if (source.source === "npm") {
199
+ return { kind: "npm", spec: source.version ? `${source.package}@${source.version}` : source.package };
200
+ }
201
+ if (source.source === "archive") {
202
+ return { kind: "archive", url: source.url, sha256: source.sha256 };
203
+ }
171
204
  return { kind: "git-subdir", url: source.url, path: source.path, ref: source.ref, sha: source.sha };
172
205
  }
173
206
  /** Read the persisted marketplace list from `<storePath>`. */
@@ -1 +1 @@
1
- {"version":3,"file":"marketplace.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin marketplaces — install plugins from a curated index hosted in a git repo\n * (or a local directory).\n *\n * A marketplace is a directory containing an index manifest in one of three formats:\n * - Native: `.agents-plugin/marketplace.json` (preferred; the `.agents` surface)\n * - Claude: `.claude-plugin/marketplace.json`\n * - Copilot: `.github/marketplace.json` (Copilot-style git index)\n *\n * All three use the same shape: `{ name?, owner?, plugins: [{ name, source, description? }] }`.\n * A plugin `source` is either a path relative to the marketplace root (a plugin\n * directory inside the repo), a git URL, or an `npm:<spec>` reference.\n *\n * When more than one is present the native `.agents-plugin` format wins, then\n * Claude, then Copilot (no merge) — matching the \"`.agents/` first\" policy used\n * across hoocode's resource surfaces.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { PLUGIN_FORMATS } from \"./formats/index.js\";\nimport { normalizePlatformToken } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/**\n * Canonical platform token used by the optional `supportPlatform` field. The\n * `.github/marketplace.json` (Copilot-style) format is surfaced as `\"github\"` —\n * friendlier than the internal `format: \"copilot\"` and matching where the file\n * lives. Authored aliases (`copilot`, `gh`) normalize to `github`.\n *\n * Re-exported from the format registry so the platform vocabulary has a single\n * source of truth.\n */\nexport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Map an internal marketplace `format` to its public platform token. */\nfunction formatToPlatform(format: NormalizedMarketplace[\"format\"]): MarketplacePlatform {\n\treturn format === \"copilot\" ? \"github\" : format;\n}\n\n/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */\nexport function normalizePlatforms(value: unknown): MarketplacePlatform[] {\n\tconst raw = Array.isArray(value) ? value : value == null ? [] : [value];\n\tconst out: MarketplacePlatform[] = [];\n\tfor (const v of raw) {\n\t\tif (typeof v !== \"string\") continue;\n\t\tconst canonical = normalizePlatformToken(v);\n\t\tif (canonical && !out.includes(canonical)) out.push(canonical);\n\t}\n\treturn out;\n}\n\n/** Structured source pointing at an entire git repository. */\ninterface MarketplacePluginSourceUrl {\n\tsource: \"url\";\n\turl: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at a subdirectory within a git repository. */\ninterface MarketplacePluginSourceGitSubdir {\n\tsource: \"git-subdir\";\n\turl: string;\n\t/** Subdirectory path inside the repository. */\n\tpath: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Source value authored in a marketplace manifest. */\nexport type MarketplacePluginSource = string | MarketplacePluginSourceUrl | MarketplacePluginSourceGitSubdir;\n\ninterface MarketplacePluginEntry {\n\tname: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\tdescription?: string;\n\t/**\n\t * Optional platform(s) this entry targets. Only set when authored on the\n\t * entry; absent means \"no per-entry restriction\" (the marketplace's\n\t * platforms apply). Purely informational today — nothing filters on it.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n}\n\nexport interface NormalizedMarketplace {\n\tname: string;\n\t/** Precedence winner when several index files conflict (agents > claude > copilot). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this marketplace directory offers, so a conflict between\n\t * multiple index files (e.g. both `.github/` and `.claude-plugin/`) is\n\t * recorded rather than hidden. Always includes the resolved `format`'s\n\t * platform; also folds in any authored top-level `supportPlatform`. Never\n\t * empty.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Absolute path to the marketplace directory. */\n\troot: string;\n\tmanifestPath: string;\n\tplugins: MarketplacePluginEntry[];\n}\n\ninterface RawMarketplace {\n\tname?: string;\n\t/** Both Claude Code and Copilot CLI define `owner` as `{ name, email? }`; a bare string is tolerated. */\n\towner?: string | { name?: string; email?: string };\n\t/** Shared optional metadata: `pluginRoot` is a base dir prepended to relative plugin source paths. */\n\tmetadata?: { description?: string; version?: string; pluginRoot?: string };\n\t/** Optional authored platform hint (string | string[]); folded into supportPlatform. */\n\tsupportPlatform?: unknown;\n\tplugins?: Array<{ name?: string; source?: unknown; description?: string; supportPlatform?: unknown }>;\n}\n\n/** True when a string source is a path (not npm:/git/URL) that `pluginRoot` should prefix. */\nfunction isRelativePathSource(source: string): boolean {\n\tconst s = source.trim();\n\treturn (\n\t\t!s.startsWith(\"npm:\") &&\n\t\t!/^https?:\\/\\//.test(s) &&\n\t\t!s.startsWith(\"git@\") &&\n\t\t!s.endsWith(\".git\") &&\n\t\t!path.isAbsolute(s)\n\t);\n}\n\n/**\n * Apply `metadata.pluginRoot` (Claude Code and Copilot CLI share the field):\n * a base directory prepended to relative plugin source paths, so\n * `\"source\": \"formatter\"` with `\"pluginRoot\": \"./plugins\"` resolves like\n * `\"./plugins/formatter\"`.\n */\nfunction applyPluginRoot(source: MarketplacePluginSource, pluginRoot: string | undefined): MarketplacePluginSource {\n\tif (!pluginRoot || typeof source !== \"string\" || !isRelativePathSource(source)) return source;\n\treturn `${pluginRoot.replace(/\\/+$/, \"\")}/${source.trim().replace(/^\\.\\//, \"\")}`;\n}\n\n/** Validate and normalize an authored source value. Returns null for unsupported/invalid shapes. */\nfunction normalizeSource(source: unknown): MarketplacePluginSource | null {\n\tif (typeof source === \"string\") return source;\n\tif (!source || typeof source !== \"object\") return null;\n\tconst obj = source as Record<string, unknown>;\n\tconst src = obj.source;\n\tconst url = obj.url;\n\tconst pin = {\n\t\t...(typeof obj.ref === \"string\" ? { ref: obj.ref } : {}),\n\t\t...(typeof obj.sha === \"string\" ? { sha: obj.sha } : {}),\n\t};\n\tif (src === \"url\" && typeof url === \"string\") {\n\t\treturn { source: \"url\", url, ...pin };\n\t}\n\tif (src === \"git-subdir\" && typeof url === \"string\" && typeof obj.path === \"string\") {\n\t\treturn { source: \"git-subdir\", url, path: obj.path, ...pin };\n\t}\n\t// Copilot marketplace shorthand: { source: \"github\", repo: \"owner/name\", path? }\n\t// (used by github/copilot-plugins). Normalize to the equivalent git source.\n\tif (src === \"github\" && typeof obj.repo === \"string\" && /^[\\w.-]+\\/[\\w.-]+$/.test(obj.repo)) {\n\t\tconst repoUrl = `https://github.com/${obj.repo}.git`;\n\t\tif (typeof obj.path === \"string\" && obj.path.length > 0) {\n\t\t\treturn { source: \"git-subdir\", url: repoUrl, path: obj.path, ...pin };\n\t\t}\n\t\treturn { source: \"url\", url: repoUrl, ...pin };\n\t}\n\treturn null;\n}\n\n/** A resolved, installable plugin source. */\nexport type ResolvedPluginSource =\n\t| { kind: \"local\"; path: string }\n\t| { kind: \"git\"; url: string; ref?: string; sha?: string }\n\t| { kind: \"git-subdir\"; url: string; path: string; ref?: string; sha?: string }\n\t| { kind: \"npm\"; spec: string };\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then\n * Copilot when more than one file is present. Returns null if no manifest exists\n * or it is invalid.\n */\nexport function parseMarketplaceDir(dir: string): NormalizedMarketplace | null {\n\t// Record every index format present so a conflict (more than one file) is\n\t// visible via supportPlatform, not silently dropped. PLUGIN_FORMATS is\n\t// precedence-ordered (native > Claude > Copilot), so `present` inherits it.\n\tconst present: Array<{ format: NormalizedMarketplace[\"format\"]; path: string }> = [];\n\tfor (const fmt of PLUGIN_FORMATS) {\n\t\tfor (const candidate of fmt.marketplaceFiles) {\n\t\t\tconst p = path.join(dir, candidate);\n\t\t\tif (fs.existsSync(p)) {\n\t\t\t\tpresent.push({ format: fmt.id, path: p });\n\t\t\t\tbreak; // first candidate wins within a format\n\t\t\t}\n\t\t}\n\t}\n\tif (present.length === 0) return null;\n\n\t// Precedence winner (present is built in agents > claude > copilot order).\n\tconst { format, path: manifestPath } = present[0];\n\n\tconst raw = readJson<RawMarketplace>(manifestPath);\n\tif (!raw) return null;\n\n\tconst pluginRoot =\n\t\ttypeof raw.metadata?.pluginRoot === \"string\" && raw.metadata.pluginRoot.trim()\n\t\t\t? raw.metadata.pluginRoot.trim()\n\t\t\t: undefined;\n\n\tconst plugins: MarketplacePluginEntry[] = [];\n\tfor (const entry of raw.plugins ?? []) {\n\t\tif (!entry?.name || !entry.source) continue;\n\t\tconst normalized = normalizeSource(entry.source);\n\t\tif (!normalized) continue;\n\t\tconst entryPlatforms = normalizePlatforms(entry.supportPlatform);\n\t\tplugins.push({\n\t\t\tname: entry.name,\n\t\t\tsource: applyPluginRoot(normalized, pluginRoot),\n\t\t\tdescription: entry.description,\n\t\t\t...(entryPlatforms.length > 0 ? { supportPlatform: entryPlatforms } : {}),\n\t\t});\n\t}\n\n\t// supportPlatform = platforms of all present index files, plus any authored\n\t// top-level hint, deduped. Always non-empty (at least the resolved format).\n\tconst supportPlatform: MarketplacePlatform[] = [];\n\tfor (const p of [...present.map((e) => formatToPlatform(e.format)), ...normalizePlatforms(raw.supportPlatform)]) {\n\t\tif (!supportPlatform.includes(p)) supportPlatform.push(p);\n\t}\n\n\treturn {\n\t\tname: (raw.name ?? path.basename(dir)).trim() || path.basename(dir),\n\t\tformat,\n\t\tsupportPlatform,\n\t\troot: dir,\n\t\tmanifestPath,\n\t\tplugins,\n\t};\n}\n\n/** Classify and resolve a plugin `source` against its marketplace root. */\nexport function resolvePluginSource(source: MarketplacePluginSource, marketplaceRoot: string): ResolvedPluginSource {\n\tif (typeof source === \"string\") {\n\t\tconst s = source.trim();\n\t\tif (s.startsWith(\"npm:\")) return { kind: \"npm\", spec: s.slice(4) };\n\t\tif (/^https?:\\/\\//.test(s) || s.startsWith(\"git@\") || s.endsWith(\".git\")) return { kind: \"git\", url: s };\n\t\t// Otherwise a path relative to (or absolute within) the marketplace repo.\n\t\treturn { kind: \"local\", path: path.isAbsolute(s) ? s : path.resolve(marketplaceRoot, s) };\n\t}\n\tif (source.source === \"url\") {\n\t\treturn { kind: \"git\", url: source.url, ref: source.ref, sha: source.sha };\n\t}\n\treturn { kind: \"git-subdir\", url: source.url, path: source.path, ref: source.ref, sha: source.sha };\n}\n\n// ============================================================================\n// Marketplace registry (persisted list of added marketplaces)\n// ============================================================================\n\nexport interface MarketplaceRecord {\n\t/** Original location the user added (git URL or local path). */\n\tlocation: string;\n\t/** Local directory to read the manifest from (clone dir for git, else location). */\n\tdir: string;\n}\n\ninterface MarketplaceStoreFile {\n\tmarketplaces?: MarketplaceRecord[];\n}\n\n/** Read the persisted marketplace list from `<storePath>`. */\nexport function readMarketplaceStore(storePath: string): MarketplaceRecord[] {\n\tconst parsed = readJson<MarketplaceStoreFile>(storePath);\n\treturn Array.isArray(parsed?.marketplaces) ? parsed.marketplaces : [];\n}\n\n/** Write the marketplace list to `<storePath>`. */\nexport function writeMarketplaceStore(storePath: string, records: MarketplaceRecord[]): void {\n\tfs.mkdirSync(path.dirname(storePath), { recursive: true });\n\tfs.writeFileSync(storePath, `${JSON.stringify({ marketplaces: records }, null, 2)}\\n`, \"utf8\");\n}\n"]}
1
+ {"version":3,"file":"marketplace.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin marketplaces — install plugins from a curated index hosted in a git repo\n * (or a local directory).\n *\n * A marketplace is a directory containing an index manifest in one of three formats:\n * - Native: `.agents-plugin/marketplace.json` (preferred; the `.agents` surface)\n * - Claude: `.claude-plugin/marketplace.json`\n * - Copilot: `.github/marketplace.json` (Copilot-style git index)\n *\n * All three use the same shape: `{ name?, owner?, plugins: [{ name, source, description? }] }`.\n * A plugin `source` is either a path relative to the marketplace root (a plugin\n * directory inside the repo), a git URL, or an `npm:<spec>` reference.\n *\n * When more than one is present the native `.agents-plugin` format wins, then\n * Claude, then Copilot (no merge) — matching the \"`.agents/` first\" policy used\n * across hoocode's resource surfaces.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { PLUGIN_FORMATS } from \"./formats/index.js\";\nimport { normalizePlatformToken } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/**\n * Canonical platform token used by the optional `supportPlatform` field. The\n * `.github/marketplace.json` (Copilot-style) format is surfaced as `\"github\"` —\n * friendlier than the internal `format: \"copilot\"` and matching where the file\n * lives. Authored aliases (`copilot`, `gh`) normalize to `github`.\n *\n * Re-exported from the format registry so the platform vocabulary has a single\n * source of truth.\n */\nexport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Map an internal marketplace `format` to its public platform token. */\nfunction formatToPlatform(format: NormalizedMarketplace[\"format\"]): MarketplacePlatform {\n\treturn format === \"copilot\" ? \"github\" : format;\n}\n\n/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */\nexport function normalizePlatforms(value: unknown): MarketplacePlatform[] {\n\tconst raw = Array.isArray(value) ? value : value == null ? [] : [value];\n\tconst out: MarketplacePlatform[] = [];\n\tfor (const v of raw) {\n\t\tif (typeof v !== \"string\") continue;\n\t\tconst canonical = normalizePlatformToken(v);\n\t\tif (canonical && !out.includes(canonical)) out.push(canonical);\n\t}\n\treturn out;\n}\n\n/** Structured source pointing at an entire git repository. */\ninterface MarketplacePluginSourceUrl {\n\tsource: \"url\";\n\turl: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at a subdirectory within a git repository. */\ninterface MarketplacePluginSourceGitSubdir {\n\tsource: \"git-subdir\";\n\turl: string;\n\t/** Subdirectory path inside the repository. */\n\tpath: string;\n\tref?: string;\n\tsha?: string;\n}\n\n/** Structured source pointing at an npm package. */\ninterface MarketplacePluginSourceNpm {\n\tsource: \"npm\";\n\tpackage: string;\n\tversion?: string;\n\tregistry?: string;\n}\n\n/** Structured source pointing at a zip archive served over HTTPS. */\ninterface MarketplacePluginSourceArchive {\n\tsource: \"archive\";\n\turl: string;\n\tsha256?: string;\n}\n\n/**\n * Source value authored in a marketplace manifest.\n *\n * `npm` and `archive` are in the union despite not being installable here. They\n * are documented vendor source types, so an entry using one is a *valid* catalog\n * entry hoocode cannot fetch — which is a different thing from a malformed entry,\n * and has to read differently. Dropping them at parse time made the plugin vanish\n * from SearchPlugins entirely, so the failure mode was \"that plugin does not\n * exist\" rather than \"hoocode cannot install that kind yet\".\n */\nexport type MarketplacePluginSource =\n\t| string\n\t| MarketplacePluginSourceUrl\n\t| MarketplacePluginSourceGitSubdir\n\t| MarketplacePluginSourceNpm\n\t| MarketplacePluginSourceArchive;\n\ninterface MarketplacePluginEntry {\n\tname: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\tdescription?: string;\n\t/**\n\t * Optional platform(s) this entry targets. Only set when authored on the\n\t * entry; absent means \"no per-entry restriction\" (the marketplace's\n\t * platforms apply). Purely informational today — nothing filters on it.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n}\n\nexport interface NormalizedMarketplace {\n\tname: string;\n\t/** Precedence winner when several index files conflict (agents > claude > copilot). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this marketplace directory offers, so a conflict between\n\t * multiple index files (e.g. both `.github/` and `.claude-plugin/`) is\n\t * recorded rather than hidden. Always includes the resolved `format`'s\n\t * platform; also folds in any authored top-level `supportPlatform`. Never\n\t * empty.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Absolute path to the marketplace directory. */\n\troot: string;\n\tmanifestPath: string;\n\tplugins: MarketplacePluginEntry[];\n}\n\ninterface RawMarketplace {\n\tname?: string;\n\t/** Both Claude Code and Copilot CLI define `owner` as `{ name, email? }`; a bare string is tolerated. */\n\towner?: string | { name?: string; email?: string };\n\t/** Shared optional metadata: `pluginRoot` is a base dir prepended to relative plugin source paths. */\n\tmetadata?: { description?: string; version?: string; pluginRoot?: string };\n\t/** Optional authored platform hint (string | string[]); folded into supportPlatform. */\n\tsupportPlatform?: unknown;\n\tplugins?: Array<{ name?: string; source?: unknown; description?: string; supportPlatform?: unknown }>;\n}\n\n/** True when a string source is a path (not npm:/git/URL) that `pluginRoot` should prefix. */\nfunction isRelativePathSource(source: string): boolean {\n\tconst s = source.trim();\n\treturn (\n\t\t!s.startsWith(\"npm:\") &&\n\t\t!/^https?:\\/\\//.test(s) &&\n\t\t!s.startsWith(\"git@\") &&\n\t\t!s.endsWith(\".git\") &&\n\t\t!path.isAbsolute(s)\n\t);\n}\n\n/**\n * Apply `metadata.pluginRoot` (Claude Code and Copilot CLI share the field):\n * a base directory prepended to relative plugin source paths, so\n * `\"source\": \"formatter\"` with `\"pluginRoot\": \"./plugins\"` resolves like\n * `\"./plugins/formatter\"`.\n */\nfunction applyPluginRoot(source: MarketplacePluginSource, pluginRoot: string | undefined): MarketplacePluginSource {\n\tif (!pluginRoot || typeof source !== \"string\" || !isRelativePathSource(source)) return source;\n\treturn `${pluginRoot.replace(/\\/+$/, \"\")}/${source.trim().replace(/^\\.\\//, \"\")}`;\n}\n\n/** Validate and normalize an authored source value. Returns null for unsupported/invalid shapes. */\nfunction normalizeSource(source: unknown): MarketplacePluginSource | null {\n\tif (typeof source === \"string\") return source;\n\tif (!source || typeof source !== \"object\") return null;\n\tconst obj = source as Record<string, unknown>;\n\tconst src = obj.source;\n\tconst url = obj.url;\n\t// `commit` is not in the documented schema but appears in the wild alongside\n\t// `sha` (anthropics/claude-plugins-official ships two such entries). It names\n\t// the same thing, so it stands in as the pin when `sha` is absent rather than\n\t// silently leaving the entry unpinned.\n\tconst shaValue = typeof obj.sha === \"string\" ? obj.sha : typeof obj.commit === \"string\" ? obj.commit : undefined;\n\tconst pin = {\n\t\t...(typeof obj.ref === \"string\" ? { ref: obj.ref } : {}),\n\t\t...(shaValue ? { sha: shaValue } : {}),\n\t};\n\tconst subdir = typeof obj.path === \"string\" && obj.path.length > 0 ? obj.path : undefined;\n\tif (src === \"url\" && typeof url === \"string\") {\n\t\t// A `url` entry carrying `path` means the plugin is a subdirectory of that\n\t\t// repo, exactly like `git-subdir`. Dropping the `path` used to install the\n\t\t// whole repository instead — six entries in the official Claude marketplace\n\t\t// resolve that way, and each produced a plugin directory with none of the\n\t\t// plugin in it.\n\t\tif (subdir) return { source: \"git-subdir\", url, path: subdir, ...pin };\n\t\treturn { source: \"url\", url, ...pin };\n\t}\n\tif (src === \"git-subdir\" && typeof url === \"string\" && subdir) {\n\t\treturn { source: \"git-subdir\", url, path: subdir, ...pin };\n\t}\n\t// Copilot marketplace shorthand: { source: \"github\", repo: \"owner/name\", path? }\n\t// (used by github/copilot-plugins and github/awesome-copilot). Normalize to the\n\t// equivalent git source.\n\tif (src === \"github\" && typeof obj.repo === \"string\" && /^[\\w.-]+\\/[\\w.-]+$/.test(obj.repo)) {\n\t\tconst repoUrl = `https://github.com/${obj.repo}.git`;\n\t\tif (subdir) return { source: \"git-subdir\", url: repoUrl, path: subdir, ...pin };\n\t\treturn { source: \"url\", url: repoUrl, ...pin };\n\t}\n\t// Documented types hoocode cannot fetch. Kept so the entry stays visible and\n\t// install can explain itself; see the MarketplacePluginSource docstring.\n\tif (src === \"npm\" && typeof obj.package === \"string\") {\n\t\treturn {\n\t\t\tsource: \"npm\",\n\t\t\tpackage: obj.package,\n\t\t\t...(typeof obj.version === \"string\" ? { version: obj.version } : {}),\n\t\t\t...(typeof obj.registry === \"string\" ? { registry: obj.registry } : {}),\n\t\t};\n\t}\n\tif (src === \"archive\" && typeof url === \"string\") {\n\t\treturn { source: \"archive\", url, ...(typeof obj.sha256 === \"string\" ? { sha256: obj.sha256 } : {}) };\n\t}\n\t// Anything else is malformed rather than merely unsupported, and stays dropped.\n\treturn null;\n}\n\n/** A resolved plugin source. The last two are recognized but not fetchable here. */\nexport type ResolvedPluginSource =\n\t| { kind: \"local\"; path: string }\n\t| { kind: \"git\"; url: string; ref?: string; sha?: string }\n\t| { kind: \"git-subdir\"; url: string; path: string; ref?: string; sha?: string }\n\t| { kind: \"npm\"; spec: string }\n\t| { kind: \"archive\"; url: string; sha256?: string };\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then\n * Copilot when more than one file is present. Returns null if no manifest exists\n * or it is invalid.\n */\nexport function parseMarketplaceDir(dir: string): NormalizedMarketplace | null {\n\t// Record every index format present so a conflict (more than one file) is\n\t// visible via supportPlatform, not silently dropped. PLUGIN_FORMATS is\n\t// precedence-ordered (native > Claude > Copilot), so `present` inherits it.\n\tconst present: Array<{ format: NormalizedMarketplace[\"format\"]; path: string }> = [];\n\tfor (const fmt of PLUGIN_FORMATS) {\n\t\tfor (const candidate of fmt.marketplaceFiles) {\n\t\t\tconst p = path.join(dir, candidate);\n\t\t\tif (fs.existsSync(p)) {\n\t\t\t\tpresent.push({ format: fmt.id, path: p });\n\t\t\t\tbreak; // first candidate wins within a format\n\t\t\t}\n\t\t}\n\t}\n\tif (present.length === 0) return null;\n\n\t// Precedence winner (present is built in agents > claude > copilot order).\n\tconst { format, path: manifestPath } = present[0];\n\n\tconst raw = readJson<RawMarketplace>(manifestPath);\n\tif (!raw) return null;\n\n\tconst pluginRoot =\n\t\ttypeof raw.metadata?.pluginRoot === \"string\" && raw.metadata.pluginRoot.trim()\n\t\t\t? raw.metadata.pluginRoot.trim()\n\t\t\t: undefined;\n\n\tconst plugins: MarketplacePluginEntry[] = [];\n\tfor (const entry of raw.plugins ?? []) {\n\t\tif (!entry?.name || !entry.source) continue;\n\t\tconst normalized = normalizeSource(entry.source);\n\t\tif (!normalized) continue;\n\t\tconst entryPlatforms = normalizePlatforms(entry.supportPlatform);\n\t\tplugins.push({\n\t\t\tname: entry.name,\n\t\t\tsource: applyPluginRoot(normalized, pluginRoot),\n\t\t\tdescription: entry.description,\n\t\t\t...(entryPlatforms.length > 0 ? { supportPlatform: entryPlatforms } : {}),\n\t\t});\n\t}\n\n\t// supportPlatform = platforms of all present index files, plus any authored\n\t// top-level hint, deduped. Always non-empty (at least the resolved format).\n\tconst supportPlatform: MarketplacePlatform[] = [];\n\tfor (const p of [...present.map((e) => formatToPlatform(e.format)), ...normalizePlatforms(raw.supportPlatform)]) {\n\t\tif (!supportPlatform.includes(p)) supportPlatform.push(p);\n\t}\n\n\treturn {\n\t\tname: (raw.name ?? path.basename(dir)).trim() || path.basename(dir),\n\t\tformat,\n\t\tsupportPlatform,\n\t\troot: dir,\n\t\tmanifestPath,\n\t\tplugins,\n\t};\n}\n\n/** Classify and resolve a plugin `source` against its marketplace root. */\nexport function resolvePluginSource(source: MarketplacePluginSource, marketplaceRoot: string): ResolvedPluginSource {\n\tif (typeof source === \"string\") {\n\t\tconst s = source.trim();\n\t\tif (s.startsWith(\"npm:\")) return { kind: \"npm\", spec: s.slice(4) };\n\t\tif (/^https?:\\/\\//.test(s) || s.startsWith(\"git@\") || s.endsWith(\".git\")) return { kind: \"git\", url: s };\n\t\t// Otherwise a path relative to (or absolute within) the marketplace repo.\n\t\treturn { kind: \"local\", path: path.isAbsolute(s) ? s : path.resolve(marketplaceRoot, s) };\n\t}\n\tif (source.source === \"url\") {\n\t\treturn { kind: \"git\", url: source.url, ref: source.ref, sha: source.sha };\n\t}\n\tif (source.source === \"npm\") {\n\t\treturn { kind: \"npm\", spec: source.version ? `${source.package}@${source.version}` : source.package };\n\t}\n\tif (source.source === \"archive\") {\n\t\treturn { kind: \"archive\", url: source.url, sha256: source.sha256 };\n\t}\n\treturn { kind: \"git-subdir\", url: source.url, path: source.path, ref: source.ref, sha: source.sha };\n}\n\n// ============================================================================\n// Marketplace registry (persisted list of added marketplaces)\n// ============================================================================\n\nexport interface MarketplaceRecord {\n\t/** Original location the user added (git URL or local path). */\n\tlocation: string;\n\t/** Local directory to read the manifest from (clone dir for git, else location). */\n\tdir: string;\n}\n\ninterface MarketplaceStoreFile {\n\tmarketplaces?: MarketplaceRecord[];\n}\n\n/** Read the persisted marketplace list from `<storePath>`. */\nexport function readMarketplaceStore(storePath: string): MarketplaceRecord[] {\n\tconst parsed = readJson<MarketplaceStoreFile>(storePath);\n\treturn Array.isArray(parsed?.marketplaces) ? parsed.marketplaces : [];\n}\n\n/** Write the marketplace list to `<storePath>`. */\nexport function writeMarketplaceStore(storePath: string, records: MarketplaceRecord[]): void {\n\tfs.mkdirSync(path.dirname(storePath), { recursive: true });\n\tfs.writeFileSync(storePath, `${JSON.stringify({ marketplaces: records }, null, 2)}\\n`, \"utf8\");\n}\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Workspace trust — the per-machine record of which working directories the user
3
+ * has agreed to run repository-supplied plugin code from.
4
+ *
5
+ * The problem it solves (docs/plugin-system-architecture.md §5.9): a plugin
6
+ * committed to a repository is code that runs for whoever clones it next. Its
7
+ * skills and commands are text the model reads, which is no worse than reading
8
+ * the repository itself, but its **hooks and MCP servers are processes** that
9
+ * start on session load. Nothing about a plugin's location can distinguish "I
10
+ * installed this here" from "this arrived in the clone", because everything in
11
+ * the repository travels with it — including any marker a plugin might carry to
12
+ * claim otherwise.
13
+ *
14
+ * So the record lives **outside the repository**, in the agent dir, keyed by
15
+ * absolute path. That is the whole design: trust cannot be forged by repository
16
+ * content, because repository content cannot write here. It is the same shape
17
+ * Claude Code's workspace trust dialog and VS Code's trusted folders use, and it
18
+ * carries the same known consequence — once a directory is trusted, code pulled
19
+ * into it later is trusted too. Trust is a statement about a *place you work*,
20
+ * not about a specific commit.
21
+ *
22
+ * Granting is a human act (`/plugin trust`, or an explicit `/plugin install
23
+ * --scope project`, where the person is demonstrably operating in the directory
24
+ * on purpose). The autonomous install path never grants it: a model deciding
25
+ * that a workspace should execute repository code is exactly the decision this
26
+ * record exists to keep with a person.
27
+ */
28
+ /** One trusted working directory. */
29
+ export interface TrustedWorkspace {
30
+ /** Absolute, resolved path. */
31
+ path: string;
32
+ /** When trust was granted, ISO-8601. Informational — nothing expires today. */
33
+ at: string;
34
+ }
35
+ /**
36
+ * `~/.agents/trusted-workspaces.json`.
37
+ *
38
+ * Beside the marketplace registry rather than inside the repo, for the reason in
39
+ * the module docstring: a file the repository can write is not a trust record.
40
+ */
41
+ export declare function trustStorePath(agentDir?: string): string;
42
+ /** Every trusted workspace, newest grant first. */
43
+ export declare function listTrustedWorkspaces(agentDir?: string): TrustedWorkspace[];
44
+ /**
45
+ * Whether `cwd` is trusted.
46
+ *
47
+ * Exact-path only, deliberately: trusting `~/src` must not silently trust every
48
+ * repository ever cloned beneath it, which is what a prefix match would do the
49
+ * first time someone trusts a directory one level too high.
50
+ */
51
+ export declare function isWorkspaceTrusted(cwd: string, agentDir?: string): boolean;
52
+ /** Grant trust to `cwd`. Idempotent — re-granting refreshes the timestamp. */
53
+ export declare function trustWorkspace(cwd: string, agentDir?: string): TrustedWorkspace;
54
+ /** Revoke trust for `cwd`. Returns false when it was not trusted to begin with. */
55
+ export declare function untrustWorkspace(cwd: string, agentDir?: string): boolean;
56
+ //# sourceMappingURL=trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/trust.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAChC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,iFAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;CACX;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEvE;AAmBD,mDAAmD;AACnD,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAE1F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAGzF;AAED,gFAA8E;AAC9E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,gBAAgB,CAK9F;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAOvF","sourcesContent":["/**\n * Workspace trust — the per-machine record of which working directories the user\n * has agreed to run repository-supplied plugin code from.\n *\n * The problem it solves (docs/plugin-system-architecture.md §5.9): a plugin\n * committed to a repository is code that runs for whoever clones it next. Its\n * skills and commands are text the model reads, which is no worse than reading\n * the repository itself, but its **hooks and MCP servers are processes** that\n * start on session load. Nothing about a plugin's location can distinguish \"I\n * installed this here\" from \"this arrived in the clone\", because everything in\n * the repository travels with it — including any marker a plugin might carry to\n * claim otherwise.\n *\n * So the record lives **outside the repository**, in the agent dir, keyed by\n * absolute path. That is the whole design: trust cannot be forged by repository\n * content, because repository content cannot write here. It is the same shape\n * Claude Code's workspace trust dialog and VS Code's trusted folders use, and it\n * carries the same known consequence — once a directory is trusted, code pulled\n * into it later is trusted too. Trust is a statement about a *place you work*,\n * not about a specific commit.\n *\n * Granting is a human act (`/plugin trust`, or an explicit `/plugin install\n * --scope project`, where the person is demonstrably operating in the directory\n * on purpose). The autonomous install path never grants it: a model deciding\n * that a workspace should execute repository code is exactly the decision this\n * record exists to keep with a person.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../../config.js\";\n\n/** One trusted working directory. */\nexport interface TrustedWorkspace {\n\t/** Absolute, resolved path. */\n\tpath: string;\n\t/** When trust was granted, ISO-8601. Informational — nothing expires today. */\n\tat: string;\n}\n\ninterface TrustStoreFile {\n\tworkspaces?: TrustedWorkspace[];\n}\n\n/**\n * `~/.agents/trusted-workspaces.json`.\n *\n * Beside the marketplace registry rather than inside the repo, for the reason in\n * the module docstring: a file the repository can write is not a trust record.\n */\nexport function trustStorePath(agentDir: string = getAgentDir()): string {\n\treturn path.join(path.dirname(agentDir), \".agents\", \"trusted-workspaces.json\");\n}\n\nfunction readStore(agentDir: string): TrustedWorkspace[] {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(trustStorePath(agentDir), \"utf8\")) as TrustStoreFile;\n\t\treturn Array.isArray(parsed?.workspaces)\n\t\t\t? parsed.workspaces.filter((w): w is TrustedWorkspace => typeof w?.path === \"string\")\n\t\t\t: [];\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction writeStore(agentDir: string, workspaces: TrustedWorkspace[]): void {\n\tconst file = trustStorePath(agentDir);\n\tmkdirSync(path.dirname(file), { recursive: true });\n\twriteFileSync(file, `${JSON.stringify({ workspaces }, null, 2)}\\n`, \"utf8\");\n}\n\n/** Every trusted workspace, newest grant first. */\nexport function listTrustedWorkspaces(agentDir: string = getAgentDir()): TrustedWorkspace[] {\n\treturn [...readStore(agentDir)].sort((a, b) => (a.at < b.at ? 1 : -1));\n}\n\n/**\n * Whether `cwd` is trusted.\n *\n * Exact-path only, deliberately: trusting `~/src` must not silently trust every\n * repository ever cloned beneath it, which is what a prefix match would do the\n * first time someone trusts a directory one level too high.\n */\nexport function isWorkspaceTrusted(cwd: string, agentDir: string = getAgentDir()): boolean {\n\tconst target = path.resolve(cwd);\n\treturn readStore(agentDir).some((w) => path.resolve(w.path) === target);\n}\n\n/** Grant trust to `cwd`. Idempotent — re-granting refreshes the timestamp. */\nexport function trustWorkspace(cwd: string, agentDir: string = getAgentDir()): TrustedWorkspace {\n\tconst target = path.resolve(cwd);\n\tconst record: TrustedWorkspace = { path: target, at: new Date().toISOString() };\n\twriteStore(agentDir, [...readStore(agentDir).filter((w) => path.resolve(w.path) !== target), record]);\n\treturn record;\n}\n\n/** Revoke trust for `cwd`. Returns false when it was not trusted to begin with. */\nexport function untrustWorkspace(cwd: string, agentDir: string = getAgentDir()): boolean {\n\tconst target = path.resolve(cwd);\n\tconst before = readStore(agentDir);\n\tconst after = before.filter((w) => path.resolve(w.path) !== target);\n\tif (after.length === before.length) return false;\n\twriteStore(agentDir, after);\n\treturn true;\n}\n"]}