@bevel-software/platform-core-backend 0.14.0 → 0.15.1

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 (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -53,6 +53,8 @@ interface HarnessOpts {
53
53
  email?: string | null;
54
54
  skills?: SkillSummary[];
55
55
  tools?: ToolManualSummary[];
56
+ /** More plugins: folder path below `Plugins/` → manifest name. */
57
+ extraPlugins?: Record<string, string>;
56
58
  }
57
59
 
58
60
  function cr(over: Partial<ChangeRequest>): ChangeRequest {
@@ -75,17 +77,21 @@ async function makeHarness(opts: HarnessOpts = {}) {
75
77
  const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-plugins-routes-'));
76
78
  tmpDirs.push(workspaceDir);
77
79
  const kbRoot = path.join(workspaceDir, KB);
78
- // Both carry the access.md that makes a folder a plugin at all.
79
- await fs.mkdir(path.join(kbRoot, 'Plugins', 'GTM'), { recursive: true });
80
- await fs.writeFile(
81
- path.join(kbRoot, 'Plugins', 'GTM', 'access.md'),
82
- '---\nread:\n - everyone\n---\nread: []\n',
83
- );
84
- await fs.mkdir(path.join(kbRoot, 'Plugins', 'Finance'), { recursive: true });
85
- await fs.writeFile(
86
- path.join(kbRoot, 'Plugins', 'Finance', 'access.md'),
87
- '---\nread:\n - everyone\n---\nread: []\n',
88
- );
80
+ // Both carry the manifest that makes a folder a plugin to discovery, and
81
+ // the access.md that makes it exist to the index.
82
+ const fixtures: [string, string][] = [
83
+ ['GTM', 'gtm'],
84
+ ['Finance', 'finance'],
85
+ ...Object.entries(opts.extraPlugins ?? {}),
86
+ ];
87
+ for (const [folder, name] of fixtures) {
88
+ await fs.mkdir(path.join(kbRoot, 'Plugins', folder), { recursive: true });
89
+ await fs.writeFile(path.join(kbRoot, 'Plugins', folder, 'plugin.json'), `{"name":"${name}"}`);
90
+ await fs.writeFile(
91
+ path.join(kbRoot, 'Plugins', folder, 'access.md'),
92
+ '---\nread:\n - everyone\n---\nread: []\n',
93
+ );
94
+ }
89
95
 
90
96
  const workspaceService = {
91
97
  getOrCreateForBranch: async (branch: string) => ({ id: workspaceIdForBranch(branch) }),
@@ -233,11 +239,35 @@ describe('/api/plugins routes', () => {
233
239
  server = h.server;
234
240
  const { status, plugins } = await listPlugins(h.baseUrl);
235
241
  expect(status).toBe(200);
236
- expect(plugins.map((g) => g.name)).toEqual(['Finance', 'GTM']);
242
+ // Named by identity (the manifest), labelled by folder.
243
+ expect(plugins.map((g) => [g.name, g.displayName])).toEqual([['finance', 'Finance'], ['gtm', 'GTM']]);
237
244
  expect(plugins[0]).toMatchObject({ canRead: true, skillCount: 0, toolCount: 1 });
238
245
  expect(plugins[1]).toMatchObject({ canRead: true, skillCount: 1, toolCount: 0 });
239
246
  });
240
247
 
248
+ it("carries the index's broken-link count into the summary — the server's, not the caller's slice", async () => {
249
+ const entry = {
250
+ name: 'gtm',
251
+ displayName: 'GTM',
252
+ folders: ['Plugins/GTM'],
253
+ linksAreManaged: true,
254
+ skillCount: 2,
255
+ toolCount: 0,
256
+ brokenLinks: 2,
257
+ owners: { roles: [], users: [] },
258
+ writers: { roles: [], users: [] },
259
+ readers: { restricted: true, roles: [], users: [] },
260
+ };
261
+ const h = await makeHarness({
262
+ readable: MEMBER_OF_BOTH,
263
+ index: { catalog: async () => [entry], invalidate: () => {} } as unknown as IPluginIndexService,
264
+ });
265
+ server = h.server;
266
+ const { plugins } = await listPlugins(h.baseUrl);
267
+ expect(plugins).toHaveLength(1);
268
+ expect(plugins[0]).toMatchObject({ name: 'gtm', brokenLinks: 2 });
269
+ });
270
+
241
271
  it('a DISCOVERABLE plugin (access.md readable, folder not) lists locked with hasRequested from the join CR', async () => {
242
272
  const h = await makeHarness({
243
273
  readable: { [ALI]: ['Plugins/Finance/access.md'] },
@@ -245,7 +275,7 @@ describe('/api/plugins routes', () => {
245
275
  });
246
276
  server = h.server;
247
277
  const { plugins } = await listPlugins(h.baseUrl);
248
- expect(plugins.map((g) => g.name)).toEqual(['Finance']);
278
+ expect(plugins.map((g) => g.name)).toEqual(['finance']);
249
279
  expect(plugins[0]).toMatchObject({
250
280
  canRead: false,
251
281
  canWrite: false,
@@ -258,8 +288,9 @@ describe('/api/plugins routes', () => {
258
288
  const h = await makeHarness({ readable: { [ALI]: ['Plugins/Finance/access.md'] } });
259
289
  server = h.server;
260
290
  const { plugins, raw } = await listPlugins(h.baseUrl);
261
- expect(plugins.map((g) => g.name)).toEqual(['Finance']);
262
- expect(raw).not.toContain('"name":"GTM"');
291
+ expect(plugins.map((g) => g.name)).toEqual(['finance']);
292
+ expect(raw).not.toContain('"name":"gtm"');
293
+ expect(raw).not.toContain('"GTM"');
263
294
  expect(raw).not.toContain('Plugins/GTM');
264
295
  });
265
296
 
@@ -267,7 +298,7 @@ describe('/api/plugins routes', () => {
267
298
  const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
268
299
  server = h.server;
269
300
  const { plugins } = await listPlugins(h.baseUrl);
270
- expect(plugins.map((g) => g.name)).toEqual(['GTM']);
301
+ expect(plugins.map((g) => g.name)).toEqual(['gtm']);
271
302
  expect(plugins[0]).toMatchObject({ canRead: false, canWrite: true });
272
303
  });
273
304
 
@@ -278,7 +309,7 @@ describe('/api/plugins routes', () => {
278
309
  });
279
310
  server = h.server;
280
311
  const { plugins } = await listPlugins(h.baseUrl);
281
- expect(plugins[0]).toMatchObject({ name: 'GTM', canRead: true, hasRequested: false });
312
+ expect(plugins[0]).toMatchObject({ name: 'gtm', canRead: true, hasRequested: false });
282
313
  });
283
314
 
284
315
  it('lists the owner verdict per caller — the folder verdict, not the manager one', async () => {
@@ -288,8 +319,8 @@ describe('/api/plugins routes', () => {
288
319
  });
289
320
  server = h.server;
290
321
  const { plugins } = await listPlugins(h.baseUrl);
291
- expect(plugins.find((g) => g.name === 'GTM')).toMatchObject({ isOwner: true });
292
- expect(plugins.find((g) => g.name === 'Finance')).toMatchObject({ isOwner: false });
322
+ expect(plugins.find((g) => g.name === 'gtm')).toMatchObject({ isOwner: true });
323
+ expect(plugins.find((g) => g.name === 'finance')).toMatchObject({ isOwner: false });
293
324
  });
294
325
 
295
326
  it('delete: 404 for unknown AND for a non-owner — a manager included (identical, fail-closed)', async () => {
@@ -301,7 +332,7 @@ describe('/api/plugins routes', () => {
301
332
  writable: { [ALI]: ['Plugins/GTM/access.md'] },
302
333
  });
303
334
  server = h.server;
304
- for (const name of ['Nope', 'GTM']) {
335
+ for (const name of ['Nope', 'gtm']) {
305
336
  const res = await fetch(`${h.baseUrl}/api/plugins/${name}`, { method: 'DELETE' });
306
337
  expect(res.status, name).toBe(404);
307
338
  expect(await res.json()).toEqual({ error: 'Unknown plugin', kind: 'unknown-plugin' });
@@ -309,12 +340,13 @@ describe('/api/plugins routes', () => {
309
340
  expect(h.provision.deletePlugin).not.toHaveBeenCalled();
310
341
  });
311
342
 
312
- it('delete: an OWNER deletes through the provision door, by the catalog name', async () => {
343
+ it('delete: an OWNER deletes through the provision door — found by identity, deleted by folder', async () => {
313
344
  const h = await makeHarness({ owner: { [ALI]: ['Plugins/GTM'] } });
314
345
  server = h.server;
315
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
346
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm`, { method: 'DELETE' });
316
347
  expect(res.status).toBe(200);
317
348
  expect(await res.json()).toEqual({ ok: true });
349
+ // Provisioning owns folders, so it is handed the folder, not the name.
318
350
  expect(h.provision.deletePlugin).toHaveBeenCalledWith(
319
351
  expect.objectContaining({ email: ALI }),
320
352
  'GTM',
@@ -326,7 +358,7 @@ describe('/api/plugins routes', () => {
326
358
  const { PluginProvisionError } = await import('../plugin-provision.service.js');
327
359
  h.provision.deletePlugin.mockRejectedValueOnce(new PluginProvisionError('Unknown plugin', 404));
328
360
  server = h.server;
329
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
361
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm`, { method: 'DELETE' });
330
362
  expect(res.status).toBe(404);
331
363
  expect(await res.json()).toEqual({ error: 'Unknown plugin' });
332
364
  });
@@ -336,7 +368,7 @@ describe('/api/plugins routes', () => {
336
368
  const h = await makeHarness({ owner: { [ALI]: ['Plugins/GTM'] } });
337
369
  h.provision.deletePlugin.mockRejectedValueOnce(new Error('push refused'));
338
370
  server = h.server;
339
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
371
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm`, { method: 'DELETE' });
340
372
  expect(res.status).toBe(500);
341
373
  expect(await res.json()).toEqual({ error: 'Failed to delete the plugin' });
342
374
  error.mockRestore();
@@ -345,7 +377,7 @@ describe('/api/plugins routes', () => {
345
377
  it('join-request: 404 for unknown AND for undiscoverable (identical, fail-closed)', async () => {
346
378
  const h = await makeHarness({});
347
379
  server = h.server;
348
- for (const name of ['Nope', 'GTM']) {
380
+ for (const name of ['Nope', 'gtm']) {
349
381
  const res = await fetch(`${h.baseUrl}/api/plugins/${name}/join-request`, { method: 'POST' });
350
382
  expect(res.status, name).toBe(404);
351
383
  expect(await res.json()).toEqual({ error: 'Unknown plugin', kind: 'unknown-plugin' });
@@ -355,7 +387,7 @@ describe('/api/plugins routes', () => {
355
387
  it('join-request: 409 when the caller can already read the folder', async () => {
356
388
  const h = await makeHarness({ readable: MEMBER_OF_BOTH });
357
389
  server = h.server;
358
- const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
390
+ const res = await fetch(`${h.baseUrl}/api/plugins/finance/join-request`, { method: 'POST' });
359
391
  expect(res.status).toBe(409);
360
392
  expect((await res.json()).kind).toBe('already-readable');
361
393
  });
@@ -363,10 +395,12 @@ describe('/api/plugins routes', () => {
363
395
  it('join-request: branch + splice + commit + CR, on the deterministic join branch', async () => {
364
396
  const h = await makeHarness({ readable: { [ALI]: ['Plugins/Finance/access.md'] } });
365
397
  server = h.server;
366
- const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
398
+ const res = await fetch(`${h.baseUrl}/api/plugins/finance/join-request`, { method: 'POST' });
367
399
  expect(res.status).toBe(200);
368
400
  expect(await res.json()).toEqual({ ok: true, number: 42 });
369
401
 
402
+ // The branch is cut from the FOLDER name, as every join branch before the
403
+ // manifest became the identity was — so none of them is orphaned.
370
404
  const branch = joinBranchFor(ALI, 'Finance');
371
405
  expect(h.workflow.createBranch).toHaveBeenCalledWith(wsId, branch, DEFAULT_BRANCH);
372
406
  // The write went to the plugin's access.md and added the caller to the
@@ -389,13 +423,26 @@ describe('/api/plugins routes', () => {
389
423
  );
390
424
  });
391
425
 
426
+ it('keys a join request by the folder PATH below the root — two folders sharing a basename never share a branch', async () => {
427
+ const h = await makeHarness({
428
+ extraPlugins: { 'teams/GTM': 'team-gtm' },
429
+ readable: { [ALI]: ['Plugins/teams/GTM/access.md'] },
430
+ });
431
+ server = h.server;
432
+ const res = await fetch(`${h.baseUrl}/api/plugins/team-gtm/join-request`, { method: 'POST' });
433
+ expect(res.status).toBe(200);
434
+ const branch = joinBranchFor(ALI, 'teams/GTM');
435
+ expect(branch).not.toBe(joinBranchFor(ALI, 'GTM'));
436
+ expect(h.workflow.createBranch).toHaveBeenCalledWith(wsId, branch, DEFAULT_BRANCH);
437
+ });
438
+
392
439
  it('join-request is idempotent: an existing open join CR is returned, nothing new is created', async () => {
393
440
  const h = await makeHarness({
394
441
  readable: { [ALI]: ['Plugins/Finance/access.md'] },
395
442
  authoredCrs: [cr({ branch: joinBranchFor(ALI, 'Finance'), number: 9 })],
396
443
  });
397
444
  server = h.server;
398
- const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
445
+ const res = await fetch(`${h.baseUrl}/api/plugins/finance/join-request`, { method: 'POST' });
399
446
  expect(res.status).toBe(200);
400
447
  expect(await res.json()).toEqual({ ok: true, number: 9 });
401
448
  expect(h.workflow.createBranch).not.toHaveBeenCalled();
@@ -418,7 +465,7 @@ describe('/api/plugins routes', () => {
418
465
  it('join-requests: a MANAGER gets the service\'s list for the plugin', async () => {
419
466
  const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
420
467
  server = h.server;
421
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests`);
468
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm/join-requests`);
422
469
  expect(res.status).toBe(200);
423
470
  expect(await res.json()).toEqual({ requests: [] });
424
471
  expect(h.joinRequests.list).toHaveBeenCalledWith(
@@ -434,7 +481,7 @@ describe('/api/plugins routes', () => {
434
481
  // question only the server answers.
435
482
  const h = await makeHarness({ readable: MEMBER_OF_BOTH });
436
483
  server = h.server;
437
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests`);
484
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm/join-requests`);
438
485
  expect(res.status).toBe(200);
439
486
  expect(await res.json()).toEqual({ requests: [] });
440
487
  expect(h.joinRequests.list).not.toHaveBeenCalled();
@@ -443,7 +490,7 @@ describe('/api/plugins routes', () => {
443
490
  it('reconcile: 404 for a non-manager — indistinguishable from a missing request', async () => {
444
491
  const h = await makeHarness({ readable: MEMBER_OF_BOTH, authoredCrs: [cr({ number: 7 })] });
445
492
  server = h.server;
446
- const denied = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/7/reconcile`, {
493
+ const denied = await fetch(`${h.baseUrl}/api/plugins/gtm/join-requests/7/reconcile`, {
447
494
  method: 'POST',
448
495
  });
449
496
  expect(denied.status).toBe(404);
@@ -453,7 +500,7 @@ describe('/api/plugins routes', () => {
453
500
  it('reconcile: 404 for a manager when the change request does not exist', async () => {
454
501
  const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
455
502
  server = h.server;
456
- const missing = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/999/reconcile`, {
503
+ const missing = await fetch(`${h.baseUrl}/api/plugins/gtm/join-requests/999/reconcile`, {
457
504
  method: 'POST',
458
505
  });
459
506
  expect(missing.status).toBe(404);
@@ -467,7 +514,7 @@ describe('/api/plugins routes', () => {
467
514
  });
468
515
  (h.joinRequests.reconcile as ReturnType<typeof vi.fn>).mockResolvedValue(true);
469
516
  server = h.server;
470
- const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/7/reconcile`, {
517
+ const res = await fetch(`${h.baseUrl}/api/plugins/gtm/join-requests/7/reconcile`, {
471
518
  method: 'POST',
472
519
  });
473
520
  expect(res.status).toBe(200);
@@ -0,0 +1,403 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ PLUGIN_MANIFEST_FILE,
5
+ PLUGIN_MCP_FILE,
6
+ PLUGIN_MANIFEST_SCHEMA,
7
+ PLUGIN_MCP_SCHEMA,
8
+ pluginManifestName,
9
+ } from '@bevel-software/platform-shared';
10
+ import { walkFiles } from '../../../shared/fs-walk.js';
11
+ import { containsVariableReference } from '../../../shared/variable-refs.js';
12
+ import { judgeMcpServerEntry } from '../../tool-manuals/mcp-json-discovery.js';
13
+ import type { SkillSummary } from '../../skills/skills.contract.js';
14
+ import type { LinkMembership } from '../plugin-links.js';
15
+ import type { DiscoveredPlugin } from '../discovery/plugin-source.js';
16
+
17
+ /**
18
+ * The compiler: SOURCE layout in, DISTRIBUTION layout out.
19
+ *
20
+ * Source is what lives in the knowledge base — shared skills under
21
+ * `Skills/`, plugin folders under `Plugins/` whose manifests LINK to them.
22
+ * Distribution is what an agent installs: a marketplace of self-contained
23
+ * plugins, each physically holding its skills (Claude Code refuses paths
24
+ * that escape a plugin's directory, and symlinks are out), in the vendor
25
+ * layouts the clients read today — `.claude-plugin/plugin.json`,
26
+ * `.codex-plugin/plugin.json`, `.mcp.json` — beside the Agent Plugins
27
+ * `plugin.json` and `mcp.json`.
28
+ *
29
+ * What comes out, for one caller:
30
+ *
31
+ * .claude-plugin/marketplace.json Claude Code's catalogue
32
+ * .agents/plugins/marketplace.json Codex's catalogue, every entry
33
+ * INSTALLED_BY_DEFAULT
34
+ * plugins/<slug>/… one per plugin the caller may read
35
+ * anything of: inline + linked skills,
36
+ * the portable half of mcp.json
37
+ * plugins/skills-and-knowledge/… every readable skill no plugin above
38
+ * ships — how a standalone skill, or one
39
+ * only in plugins the caller cannot see,
40
+ * is still one install away — plus the
41
+ * knowledge base's own MCP endpoint
42
+ * plugins/hexis-all/… a bundle whose only content is a
43
+ * dependency on every plugin above —
44
+ * Claude Code installs them all from
45
+ * one `claude plugin install`
46
+ * skills/<name>/… every readable skill once, flat, for
47
+ * `npx skills add <url> --all`
48
+ * README.md names the source commit
49
+ *
50
+ * FAIL CLOSED, per file: a skill is copied only when `readable` says yes for
51
+ * its SKILL.md — the same per-path verdict the catalog filters on — so the
52
+ * compiled tree for a caller holds exactly what that caller may read, and
53
+ * nothing about a skill they may not. Plugins with nothing readable are
54
+ * left out entirely. `access.md` and `.bevelignore` never ship; neither do
55
+ * the hexis-only `.tool` manuals (no other client can run them), nor an
56
+ * `mcp.json` header carrying a `${VAR}` vault reference (the extension block
57
+ * that resolves it is ours, and a header a client would send verbatim must
58
+ * not name a secret it cannot expand).
59
+ *
60
+ * A pure function of its inputs — it reads the KB tree it is pointed at and
61
+ * returns a virtual tree; writing it somewhere (a git namespace, a folder, a
62
+ * remote) is the sink's business.
63
+ */
64
+
65
+ export interface MarketplaceOptions {
66
+ /** The marketplace's name — a slug the clients key it on. */
67
+ name: string;
68
+ /** Shown as the marketplace owner. */
69
+ owner: string;
70
+ description?: string;
71
+ /** The default-branch commit the tree was compiled from; also the bundle's version. */
72
+ sourceCommit: string;
73
+ /** The name of the one-install bundle plugin. */
74
+ bundleName?: string;
75
+ /** The name of the leftovers plugin holding skills no other plugin ships. */
76
+ skillsPluginName?: string;
77
+ /**
78
+ * This deployment's hosted MCP endpoint — the knowledge base as a tool.
79
+ * Shipped in the leftovers plugin's `mcp.json` so an agent that installs
80
+ * from the marketplace can read the knowledge its skills refer to. URL only,
81
+ * never a credential: the endpoint challenges with OAuth metadata, and the
82
+ * client signs the person in on first use.
83
+ */
84
+ knowledgeBaseMcp?: { name: string; url: string };
85
+ }
86
+
87
+ export interface CompileInput {
88
+ /** Absolute path of the KB checkout (the folder holding `Plugins/`, `Skills/`, …). */
89
+ kbRoot: string;
90
+ /** The released catalog, UNFILTERED — `readable` does the filtering. */
91
+ skills: SkillSummary[];
92
+ /** The plugins as the configured source discovered them (manifests and servers already parsed). */
93
+ plugins: DiscoveredPlugin[];
94
+ /** Which plugins hold which skills, from the link index. */
95
+ membership: LinkMembership;
96
+ /** The caller's read verdict on a repo-relative path (`<skillFolder>/SKILL.md`). */
97
+ readable: (repoPath: string) => Promise<boolean>;
98
+ options: MarketplaceOptions;
99
+ }
100
+
101
+ export interface VirtualTree {
102
+ /** repo-relative POSIX path → file bytes. */
103
+ files: Map<string, Buffer>;
104
+ /** What was left out and why — name clashes, unparsable manifests. */
105
+ warnings: string[];
106
+ /** The plugin slugs the tree carries, in marketplace order. */
107
+ plugins: string[];
108
+ }
109
+
110
+ const BUNDLE_NAME = 'hexis-all';
111
+ const SKILLS_PLUGIN_NAME = 'skills-and-knowledge';
112
+ /** Slugs the compiler emits itself; a real plugin may not take them. */
113
+ const RESERVED_SLUGS = new Set([BUNDLE_NAME, SKILLS_PLUGIN_NAME]);
114
+ const NEVER_SHIPPED = new Set(['access.md', '.bevelignore']);
115
+
116
+ export async function compileMarketplace(input: CompileInput): Promise<VirtualTree> {
117
+ const { kbRoot, skills, plugins, membership, readable, options } = input;
118
+ const files = new Map<string, Buffer>();
119
+ const warnings: string[] = [];
120
+ const put = (rel: string, content: string | Buffer) =>
121
+ files.set(rel, typeof content === 'string' ? Buffer.from(content, 'utf-8') : content);
122
+
123
+ // One verdict per skill, resolved once: every plugin below reads from it. A
124
+ // RETIRED skill (its governance lifecycle) stays in the catalog for its
125
+ // owners but never ships — retiring is how a skill leaves every agent.
126
+ const readableSkills = new Map<string, SkillSummary>();
127
+ for (const s of skills) {
128
+ if (s.lifecycle === 'retired') continue;
129
+ if (await readable(`${s.path}/SKILL.md`)) readableSkills.set(s.path, s);
130
+ }
131
+
132
+ interface PluginOut {
133
+ slug: string;
134
+ displayName: string;
135
+ description?: string;
136
+ version?: string;
137
+ skills: SkillSummary[];
138
+ /** Source folder (real plugins only). */
139
+ folder?: string;
140
+ /** The portable `mcpServers` map to ship, when the plugin has servers. */
141
+ mcp?: Record<string, Record<string, unknown>>;
142
+ }
143
+ const out: PluginOut[] = [];
144
+
145
+ // 1. Real plugins: inline skills (in the folder) + linked skills (from the
146
+ // manifest), each only if readable. Personal folders are places, not
147
+ // plugins; a plugin the index does not count as existing is not shipped.
148
+ const byName = new Map(plugins.map((p) => [p.name, p]));
149
+ for (const [name, links] of [...membership.byPlugin.entries()].sort(([a], [b]) => a.localeCompare(b))) {
150
+ const plugin = byName.get(name);
151
+ if (!plugin || plugin.personal || !plugin.exists) continue;
152
+ const manifest = plugin.manifest;
153
+ const inline = [...readableSkills.values()].filter((s) => s.path.startsWith(`${plugin.folder}/`));
154
+ const linked = links.linkedSkills
155
+ .map((p) => readableSkills.get(p))
156
+ .filter((s): s is SkillSummary => s !== undefined);
157
+ const dedup = dedupeByName([...inline, ...linked], (s, other) =>
158
+ warnings.push(
159
+ `${name}: skill "${s.name}" at ${s.path} shares its name with ${other.path} — the second is left out`,
160
+ ),
161
+ );
162
+ const mcp = portableMcp(plugin.mcpServers, (name, reason) =>
163
+ warnings.push(`${plugin.folder}: mcp server "${name}" not shipped — ${reason}`),
164
+ );
165
+ if (dedup.length === 0 && mcp === null) continue; // nothing this caller may see
166
+ // The plugin's identity IS the marketplace slug. Folded once more here
167
+ // all the same: it becomes a path segment in the compiled tree, and a
168
+ // checked-in manifest is repository content, not trusted input — the
169
+ // folding (`[a-z0-9.-]`, no runs, no leading dots) leaves nothing that
170
+ // can be a separator or a parent reference.
171
+ const slug = pluginManifestName(name);
172
+ if (RESERVED_SLUGS.has(slug)) {
173
+ warnings.push(`${plugin.folder}: manifest name "${slug}" is reserved by the marketplace and cannot be a plugin — plugin skipped`);
174
+ continue;
175
+ }
176
+ if (out.some((p) => p.slug === slug)) {
177
+ warnings.push(`${plugin.folder}: manifest name "${slug}" is already taken in this marketplace — plugin skipped`);
178
+ continue;
179
+ }
180
+ out.push({
181
+ slug,
182
+ displayName: plugin.displayName,
183
+ description: typeof manifest?.description === 'string' ? manifest.description : undefined,
184
+ version: typeof manifest?.version === 'string' ? manifest.version : undefined,
185
+ skills: dedup,
186
+ folder: plugin.folder,
187
+ mcp: mcp ?? undefined,
188
+ });
189
+ }
190
+
191
+ // 2. The leftovers plugin: every readable skill that no plugin above ships.
192
+ // Agents install plugins, not loose skills, so a skill in no plugin — or
193
+ // only in plugins this caller cannot see anything of — still has to be one
194
+ // install away. ONE plugin per caller, not one per scope: there is nothing
195
+ // to choose between (the caller may read all of it) and the bundle installs
196
+ // it regardless, so scope plugins would only add names and duplicate what
197
+ // the real plugins already carry.
198
+ // It also carries the knowledge base's own MCP endpoint (URL only — the
199
+ // client's OAuth flow signs the person in), so an agent installing from
200
+ // the marketplace can read the knowledge its skills point at. With the
201
+ // endpoint configured the plugin exists even for a caller with no
202
+ // leftover skill: the knowledge is the point.
203
+ const covered = new Set(out.flatMap((p) => p.skills.map((s) => s.path)));
204
+ const leftovers = [...readableSkills.values()].filter((s) => !covered.has(s.path));
205
+ const kbMcp = options.knowledgeBaseMcp
206
+ ? { [options.knowledgeBaseMcp.name]: { type: 'streamable-http', url: options.knowledgeBaseMcp.url } }
207
+ : undefined;
208
+ if (leftovers.length > 0 || kbMcp) {
209
+ const slug = pluginManifestName(options.skillsPluginName ?? SKILLS_PLUGIN_NAME);
210
+ if (out.some((p) => p.slug === slug)) {
211
+ warnings.push(`the skills plugin "${slug}" collides with a plugin's manifest name — standalone skills not shipped`);
212
+ } else {
213
+ out.push({
214
+ slug,
215
+ displayName: 'Skills and knowledge',
216
+ description: kbMcp
217
+ ? 'Every skill you may read that no other plugin here ships, and the knowledge base as an MCP server'
218
+ : 'Every skill you may read that no other plugin here ships',
219
+ skills: dedupeByName(leftovers, (s, other) =>
220
+ warnings.push(`${slug}: "${s.name}" at ${s.path} shares its name with ${other.path} — left out`),
221
+ ),
222
+ mcp: kbMcp,
223
+ });
224
+ }
225
+ }
226
+
227
+ // 3. Materialise every plugin: the three manifests + copied skill folders.
228
+ for (const p of out) {
229
+ const base = `plugins/${p.slug}`;
230
+ const spec: Record<string, unknown> = { $schema: PLUGIN_MANIFEST_SCHEMA, name: p.slug };
231
+ if (p.version) spec.version = p.version;
232
+ if (p.description) spec.description = p.description;
233
+ put(`${base}/${PLUGIN_MANIFEST_FILE}`, `${JSON.stringify(spec, null, 2)}\n`);
234
+ const vendor: Record<string, unknown> = { name: p.slug };
235
+ if (p.version) vendor.version = p.version;
236
+ vendor.description = p.description ?? p.displayName;
237
+ put(`${base}/.claude-plugin/plugin.json`, `${JSON.stringify(vendor, null, 2)}\n`);
238
+ put(`${base}/.codex-plugin/plugin.json`, `${JSON.stringify(vendor, null, 2)}\n`);
239
+ if (p.mcp) {
240
+ put(`${base}/${PLUGIN_MCP_FILE}`, `${JSON.stringify({ $schema: PLUGIN_MCP_SCHEMA, mcpServers: p.mcp }, null, 2)}\n`);
241
+ put(`${base}/.mcp.json`, `${JSON.stringify({ mcpServers: p.mcp }, null, 2)}\n`);
242
+ }
243
+ for (const s of p.skills) {
244
+ await copySkill(kbRoot, s, `${base}/skills/${s.name}`, put);
245
+ }
246
+ }
247
+
248
+ // 4. The one-install bundle: a Claude manifest with nothing but dependencies.
249
+ const bundle = pluginManifestName(options.bundleName ?? BUNDLE_NAME);
250
+ const slugs = out.map((p) => p.slug);
251
+ if (slugs.includes(bundle)) {
252
+ // Only reachable with a non-default bundle name that folds to a real
253
+ // plugin's slug: the default is reserved above. Skipping the bundle
254
+ // beats overwriting that plugin's manifests with a dependency list.
255
+ warnings.push(`the bundle name "${bundle}" collides with a plugin's manifest name — no bundle plugin emitted`);
256
+ }
257
+ if (slugs.length > 0 && !slugs.includes(bundle)) {
258
+ const shortSha = options.sourceCommit.slice(0, 12) || '0';
259
+ const description = `Everything in ${options.owner}'s marketplace you may read — one install.`;
260
+ put(
261
+ `plugins/${bundle}/.claude-plugin/plugin.json`,
262
+ `${JSON.stringify({ name: bundle, version: `0.0.0-${shortSha}`, description, dependencies: slugs }, null, 2)}\n`,
263
+ );
264
+ put(
265
+ `plugins/${bundle}/${PLUGIN_MANIFEST_FILE}`,
266
+ `${JSON.stringify({ $schema: PLUGIN_MANIFEST_SCHEMA, name: bundle, description }, null, 2)}\n`,
267
+ );
268
+ put(
269
+ `plugins/${bundle}/.codex-plugin/plugin.json`,
270
+ `${JSON.stringify({ name: bundle, description }, null, 2)}\n`,
271
+ );
272
+ }
273
+
274
+ // 5. The flat root skills/ folder, every readable skill once.
275
+ const flat = dedupeByName(
276
+ [...readableSkills.values()].sort((a, b) => a.path.localeCompare(b.path)),
277
+ (s, other) => warnings.push(`skills/: "${s.name}" at ${s.path} shares its name with ${other.path} — left out`),
278
+ );
279
+ for (const s of flat) await copySkill(kbRoot, s, `skills/${s.name}`, put);
280
+
281
+ // 6. The two catalogues + a README naming the source.
282
+ const bundleEmitted = slugs.length > 0 && !slugs.includes(bundle);
283
+ const entries = [...out.map((p) => ({ slug: p.slug, description: p.description ?? p.displayName })), ...(bundleEmitted ? [{ slug: bundle, description: 'Everything you may read, one install' }] : [])];
284
+ put(
285
+ '.claude-plugin/marketplace.json',
286
+ `${JSON.stringify(
287
+ {
288
+ name: options.name,
289
+ owner: { name: options.owner },
290
+ ...(options.description ? { description: options.description } : {}),
291
+ plugins: entries.map((e) => ({ name: e.slug, source: `./plugins/${e.slug}`, description: e.description })),
292
+ },
293
+ null,
294
+ 2,
295
+ )}\n`,
296
+ );
297
+ put(
298
+ '.agents/plugins/marketplace.json',
299
+ `${JSON.stringify(
300
+ {
301
+ name: options.name,
302
+ plugins: entries.map((e) => ({
303
+ name: e.slug,
304
+ source: { source: 'local', path: `./plugins/${e.slug}` },
305
+ policy: { installation: 'INSTALLED_BY_DEFAULT' },
306
+ description: e.description,
307
+ })),
308
+ },
309
+ null,
310
+ 2,
311
+ )}\n`,
312
+ );
313
+ put(
314
+ 'README.md',
315
+ [
316
+ `# ${options.name}`,
317
+ '',
318
+ `Compiled from ${options.owner}'s knowledge base at commit \`${options.sourceCommit}\`.`,
319
+ 'This tree is generated: edit the knowledge base, not these files.',
320
+ '',
321
+ `Plugins: ${entries.map((e) => e.slug).join(', ') || 'none'}.`,
322
+ '',
323
+ ].join('\n'),
324
+ );
325
+
326
+ return { files, warnings, plugins: entries.map((e) => e.slug) };
327
+ }
328
+
329
+ // --- helpers ------------------------------------------------------------------
330
+
331
+ /** Copy a skill folder (minus what never ships) under `dest`. */
332
+ async function copySkill(
333
+ kbRoot: string,
334
+ skill: SkillSummary,
335
+ dest: string,
336
+ put: (rel: string, content: Buffer) => void,
337
+ ): Promise<void> {
338
+ const abs = path.join(kbRoot, skill.path);
339
+ for (const rel of await walkFiles(abs, (name) => !NEVER_SHIPPED.has(name))) {
340
+ put(`${dest}/${rel}`, await fs.readFile(path.join(abs, rel)));
341
+ }
342
+ }
343
+
344
+ /** First by (name, path) order wins; the loser is reported to `onClash`. */
345
+ function dedupeByName(
346
+ list: SkillSummary[],
347
+ onClash: (loser: SkillSummary, winner: SkillSummary) => void,
348
+ ): SkillSummary[] {
349
+ const sorted = [...list].sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
350
+ const seen = new Map<string, SkillSummary>();
351
+ const out: SkillSummary[] = [];
352
+ for (const s of sorted) {
353
+ const winner = seen.get(s.name);
354
+ if (winner) {
355
+ if (winner.path !== s.path) onClash(s, winner);
356
+ continue;
357
+ }
358
+ seen.set(s.name, s);
359
+ out.push(s);
360
+ }
361
+ return out;
362
+ }
363
+
364
+ /**
365
+ * The portable half of a plugin's `mcpServers`: every entry the ONE
366
+ * judgement accepts (a name a client can key on, a transport it speaks, the
367
+ * field that transport needs), normalised, minus what a client cannot use —
368
+ * `${VAR}` vault references in a url or a header. Null when there is no
369
+ * server to ship; each server left out is reported with the judgement's
370
+ * reason, so an omission is diagnosable.
371
+ */
372
+ function portableMcp(
373
+ servers: Record<string, unknown> | null,
374
+ leftOut: (name: string, reason: string) => void,
375
+ ): Record<string, Record<string, unknown>> | null {
376
+ if (!servers) return null;
377
+ const out: Record<string, Record<string, unknown>> = {};
378
+ for (const [name, raw] of Object.entries(servers)) {
379
+ const verdict = judgeMcpServerEntry(name, raw);
380
+ if (!verdict.ok) {
381
+ leftOut(name, verdict.reason);
382
+ continue;
383
+ }
384
+ if (verdict.transport === 'stdio') {
385
+ out[name] = { ...verdict.entry };
386
+ continue;
387
+ }
388
+ const entry: Record<string, unknown> = { type: verdict.entry.type, url: verdict.entry.url };
389
+ if (containsVariableReference(verdict.entry.url)) {
390
+ leftOut(name, 'its url is expanded from the vault, which a client cannot do');
391
+ continue;
392
+ }
393
+ if (verdict.entry.headers) {
394
+ const headers: Record<string, string> = {};
395
+ for (const [h, v] of Object.entries(verdict.entry.headers)) {
396
+ if (!containsVariableReference(v)) headers[h] = v;
397
+ }
398
+ if (Object.keys(headers).length > 0) entry.headers = headers;
399
+ }
400
+ out[name] = entry;
401
+ }
402
+ return Object.keys(out).length > 0 ? out : null;
403
+ }