@thevinci/web 1.0.0

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 (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,701 @@
1
+ import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
+
3
+ export const registerSkillRoutes = (app, dependencies) => {
4
+ const {
5
+ fs,
6
+ path,
7
+ os,
8
+ resolveProjectDirectory,
9
+ resolveOptionalProjectDirectory,
10
+ readSettingsFromDisk,
11
+ sanitizeSkillCatalogs,
12
+ isUnsafeSkillRelativePath,
13
+ refreshOpenCodeAfterConfigChange,
14
+ clientReloadDelayMs,
15
+ buildOpenCodeUrl,
16
+ getOpenCodeAuthHeaders,
17
+ getOpenCodePort,
18
+ getSkillSources,
19
+ createSkill,
20
+ updateSkill,
21
+ deleteSkill,
22
+ readSkillSupportingFile,
23
+ writeSkillSupportingFile,
24
+ deleteSkillSupportingFile,
25
+ SKILL_SCOPE,
26
+ SKILL_DIR,
27
+ getCuratedSkillsSources,
28
+ getCacheKey,
29
+ getCachedScan,
30
+ setCachedScan,
31
+ parseSkillRepoSource,
32
+ scanSkillsRepository,
33
+ installSkillsFromRepository,
34
+ scanClawdHubPage,
35
+ installSkillsFromClawdHub,
36
+ isClawdHubSource,
37
+ getProfiles,
38
+ getProfile,
39
+ } = dependencies;
40
+
41
+ const findWorktreeRootForSkills = (workingDirectory) => {
42
+ if (!workingDirectory) return null;
43
+ let current = path.resolve(workingDirectory);
44
+ while (true) {
45
+ if (fs.existsSync(path.join(current, '.git'))) {
46
+ return current;
47
+ }
48
+ const parent = path.dirname(current);
49
+ if (parent === current) {
50
+ return null;
51
+ }
52
+ current = parent;
53
+ }
54
+ };
55
+
56
+ const getSkillProjectAncestors = (workingDirectory) => {
57
+ if (!workingDirectory) return [];
58
+ const result = [];
59
+ let current = path.resolve(workingDirectory);
60
+ const stop = findWorktreeRootForSkills(workingDirectory) || current;
61
+ while (true) {
62
+ result.push(current);
63
+ if (current === stop) break;
64
+ const parent = path.dirname(current);
65
+ if (parent === current) break;
66
+ current = parent;
67
+ }
68
+ return result;
69
+ };
70
+
71
+ const isPathInside = (candidatePath, parentPath) => {
72
+ if (!candidatePath || !parentPath) return false;
73
+ const normalizedCandidate = path.resolve(candidatePath);
74
+ const normalizedParent = path.resolve(parentPath);
75
+ return normalizedCandidate === normalizedParent || normalizedCandidate.startsWith(`${normalizedParent}${path.sep}`);
76
+ };
77
+
78
+ const inferSkillScopeAndSourceFromPath = (skillPath, workingDirectory) => {
79
+ const resolvedPath = typeof skillPath === 'string' ? path.resolve(skillPath) : '';
80
+ const home = os.homedir();
81
+ const source = resolvedPath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)
82
+ ? 'agents'
83
+ : resolvedPath.includes(`${path.sep}.claude${path.sep}skills${path.sep}`)
84
+ ? 'claude'
85
+ : 'opencode';
86
+
87
+ const projectAncestors = getSkillProjectAncestors(workingDirectory);
88
+ const isProjectScoped = projectAncestors.some((ancestor) => {
89
+ const candidates = [
90
+ path.join(ancestor, '.opencode'),
91
+ path.join(ancestor, '.claude', 'skills'),
92
+ path.join(ancestor, '.agents', 'skills'),
93
+ ];
94
+ return candidates.some((candidate) => isPathInside(resolvedPath, candidate));
95
+ });
96
+
97
+ if (isProjectScoped) {
98
+ return { scope: SKILL_SCOPE.PROJECT, source };
99
+ }
100
+
101
+ const userRoots = [
102
+ path.join(home, '.vinci', '.opencode'),
103
+ path.join(home, '.opencode'),
104
+ path.join(home, '.claude', 'skills'),
105
+ path.join(home, '.agents', 'skills'),
106
+ process.env.OPENCODE_CONFIG_DIR ? path.resolve(process.env.OPENCODE_CONFIG_DIR) : null,
107
+ ].filter(Boolean);
108
+
109
+ if (userRoots.some((root) => isPathInside(resolvedPath, root))) {
110
+ return { scope: SKILL_SCOPE.USER, source };
111
+ }
112
+
113
+ return { scope: SKILL_SCOPE.USER, source };
114
+ };
115
+
116
+ const fetchOpenCodeDiscoveredSkills = async (workingDirectory) => {
117
+ if (!getOpenCodePort()) {
118
+ return [];
119
+ }
120
+
121
+ try {
122
+ const client = createOpencodeClient({
123
+ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
124
+ directory: workingDirectory || undefined,
125
+ headers: getOpenCodeAuthHeaders(),
126
+ fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
127
+ });
128
+
129
+ const response = await client.app.skills(
130
+ workingDirectory ? { directory: workingDirectory } : undefined,
131
+ );
132
+ const payload = response?.data;
133
+ if (!Array.isArray(payload)) {
134
+ return [];
135
+ }
136
+
137
+ return payload
138
+ .map((item) => {
139
+ const name = typeof item?.name === 'string' ? item.name.trim() : '';
140
+ const location = typeof item?.location === 'string' ? item.location : '';
141
+ const description = typeof item?.description === 'string' ? item.description : '';
142
+ if (!name || !location || location === '<built-in>') {
143
+ return null;
144
+ }
145
+ const inferred = inferSkillScopeAndSourceFromPath(location, workingDirectory);
146
+ return {
147
+ name,
148
+ path: location,
149
+ scope: inferred.scope,
150
+ source: inferred.source,
151
+ description,
152
+ };
153
+ })
154
+ .filter(Boolean);
155
+ } catch (error) {
156
+ console.error('Failed to list OpenCode skills:', error);
157
+ return [];
158
+ }
159
+ };
160
+
161
+ const listGitIdentitiesForResponse = () => {
162
+ try {
163
+ const profiles = getProfiles();
164
+ return profiles.map((p) => ({ id: p.id, name: p.name }));
165
+ } catch {
166
+ return [];
167
+ }
168
+ };
169
+
170
+ const resolveGitIdentity = (profileId) => {
171
+ if (!profileId) {
172
+ return null;
173
+ }
174
+ try {
175
+ const profile = getProfile(profileId);
176
+ const sshKey = profile?.sshKey;
177
+ if (typeof sshKey === 'string' && sshKey.trim()) {
178
+ return { sshKey: sshKey.trim() };
179
+ }
180
+ } catch {
181
+ // ignore
182
+ }
183
+ return null;
184
+ };
185
+
186
+ app.get('/api/config/skills', async (req, res) => {
187
+ try {
188
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
189
+ if (error) {
190
+ return res.status(400).json({ error });
191
+ }
192
+ const skills = await fetchOpenCodeDiscoveredSkills(directory);
193
+
194
+ const enrichedSkills = skills.map((skill) => {
195
+ const sources = getSkillSources(skill.name, directory, skill);
196
+ return {
197
+ ...skill,
198
+ sources
199
+ };
200
+ });
201
+
202
+ res.json({ skills: enrichedSkills });
203
+ } catch (error) {
204
+ console.error('Failed to list skills:', error);
205
+ res.status(500).json({ error: 'Failed to list skills' });
206
+ }
207
+ });
208
+
209
+ app.get('/api/config/skills/catalog', async (req, res) => {
210
+ try {
211
+ const { error } = await resolveOptionalProjectDirectory(req);
212
+ if (error) {
213
+ return res.status(400).json({ error });
214
+ }
215
+
216
+ const curatedSources = getCuratedSkillsSources();
217
+ const settings = await readSettingsFromDisk();
218
+ const customSourcesRaw = sanitizeSkillCatalogs(settings.skillCatalogs) || [];
219
+
220
+ const customSources = customSourcesRaw.map((entry) => ({
221
+ id: entry.id,
222
+ label: entry.label,
223
+ description: entry.source,
224
+ source: entry.source,
225
+ defaultSubpath: entry.subpath,
226
+ gitIdentityId: entry.gitIdentityId,
227
+ }));
228
+
229
+ const sources = [...curatedSources, ...customSources];
230
+ const sourcesForUi = sources.map(({ gitIdentityId, ...rest }) => rest);
231
+
232
+ res.json({ ok: true, sources: sourcesForUi, itemsBySource: {}, pageInfoBySource: {} });
233
+ } catch (error) {
234
+ console.error('Failed to load skills catalog:', error);
235
+ res.status(500).json({ ok: false, error: { kind: 'unknown', message: error.message || 'Failed to load catalog' } });
236
+ }
237
+ });
238
+
239
+ app.get('/api/config/skills/catalog/source', async (req, res) => {
240
+ try {
241
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
242
+ if (error) {
243
+ return res.status(400).json({ ok: false, error: { kind: 'invalidSource', message: error } });
244
+ }
245
+
246
+ const sourceId = typeof req.query.sourceId === 'string' ? req.query.sourceId : null;
247
+ if (!sourceId) {
248
+ return res.status(400).json({ ok: false, error: { kind: 'invalidSource', message: 'Missing sourceId' } });
249
+ }
250
+
251
+ const refresh = String(req.query.refresh || '').toLowerCase() === 'true';
252
+ const cursor = typeof req.query.cursor === 'string' ? req.query.cursor : null;
253
+
254
+ const curatedSources = getCuratedSkillsSources();
255
+ const settings = await readSettingsFromDisk();
256
+ const customSourcesRaw = sanitizeSkillCatalogs(settings.skillCatalogs) || [];
257
+
258
+ const customSources = customSourcesRaw.map((entry) => ({
259
+ id: entry.id,
260
+ label: entry.label,
261
+ description: entry.source,
262
+ source: entry.source,
263
+ defaultSubpath: entry.subpath,
264
+ gitIdentityId: entry.gitIdentityId,
265
+ }));
266
+
267
+ const sources = [...curatedSources, ...customSources];
268
+ const src = sources.find((entry) => entry.id === sourceId);
269
+
270
+ if (!src) {
271
+ return res.status(404).json({ ok: false, error: { kind: 'invalidSource', message: 'Unknown source' } });
272
+ }
273
+
274
+ const discovered = await fetchOpenCodeDiscoveredSkills(directory);
275
+ const installedByName = new Map(discovered.map((s) => [s.name, s]));
276
+
277
+ if (src.sourceType === 'clawdhub' || isClawdHubSource(src.source)) {
278
+ const scanned = await scanClawdHubPage({ cursor: cursor || null });
279
+ if (!scanned.ok) {
280
+ return res.status(500).json({ ok: false, error: scanned.error });
281
+ }
282
+
283
+ const items = (scanned.items || []).map((item) => {
284
+ const installed = installedByName.get(item.skillName);
285
+ return {
286
+ ...item,
287
+ sourceId: src.id,
288
+ installed: installed
289
+ ? { isInstalled: true, scope: installed.scope, source: installed.source }
290
+ : { isInstalled: false },
291
+ };
292
+ });
293
+
294
+ return res.json({ ok: true, items, nextCursor: scanned.nextCursor || null });
295
+ }
296
+
297
+ const parsed = parseSkillRepoSource(src.source);
298
+ if (!parsed.ok) {
299
+ return res.status(400).json({ ok: false, error: parsed.error });
300
+ }
301
+
302
+ const effectiveSubpath = src.defaultSubpath || parsed.effectiveSubpath || null;
303
+ const cacheKey = getCacheKey({
304
+ normalizedRepo: parsed.normalizedRepo,
305
+ subpath: effectiveSubpath || '',
306
+ identityId: src.gitIdentityId || '',
307
+ });
308
+
309
+ let scanResult = !refresh ? getCachedScan(cacheKey) : null;
310
+ if (!scanResult) {
311
+ const scanned = await scanSkillsRepository({
312
+ source: src.source,
313
+ subpath: src.defaultSubpath,
314
+ defaultSubpath: src.defaultSubpath,
315
+ identity: resolveGitIdentity(src.gitIdentityId),
316
+ });
317
+
318
+ if (!scanned.ok) {
319
+ return res.status(500).json({ ok: false, error: scanned.error });
320
+ }
321
+
322
+ scanResult = scanned;
323
+ setCachedScan(cacheKey, scanResult);
324
+ }
325
+
326
+ const items = (scanResult.items || []).map((item) => {
327
+ const installed = installedByName.get(item.skillName);
328
+ return {
329
+ sourceId: src.id,
330
+ ...item,
331
+ gitIdentityId: src.gitIdentityId,
332
+ installed: installed
333
+ ? { isInstalled: true, scope: installed.scope, source: installed.source }
334
+ : { isInstalled: false },
335
+ };
336
+ });
337
+
338
+ return res.json({ ok: true, items });
339
+ } catch (error) {
340
+ console.error('Failed to load catalog source:', error);
341
+ return res.status(500).json({
342
+ ok: false,
343
+ error: { kind: 'unknown', message: error.message || 'Failed to load catalog source' },
344
+ });
345
+ }
346
+ });
347
+
348
+ app.post('/api/config/skills/scan', async (req, res) => {
349
+ try {
350
+ const { source, subpath, gitIdentityId } = req.body || {};
351
+ const identity = resolveGitIdentity(gitIdentityId);
352
+
353
+ const result = await scanSkillsRepository({
354
+ source,
355
+ subpath,
356
+ identity,
357
+ });
358
+
359
+ if (!result.ok) {
360
+ if (result.error?.kind === 'authRequired') {
361
+ return res.status(401).json({
362
+ ok: false,
363
+ error: {
364
+ ...result.error,
365
+ identities: listGitIdentitiesForResponse(),
366
+ },
367
+ });
368
+ }
369
+
370
+ return res.status(400).json({ ok: false, error: result.error });
371
+ }
372
+
373
+ res.json({ ok: true, items: result.items });
374
+ } catch (error) {
375
+ console.error('Failed to scan skills repository:', error);
376
+ res.status(500).json({ ok: false, error: { kind: 'unknown', message: error.message || 'Failed to scan repository' } });
377
+ }
378
+ });
379
+
380
+ app.post('/api/config/skills/install', async (req, res) => {
381
+ try {
382
+ const {
383
+ source,
384
+ subpath,
385
+ gitIdentityId,
386
+ scope,
387
+ targetSource,
388
+ selections,
389
+ conflictPolicy,
390
+ conflictDecisions,
391
+ } = req.body || {};
392
+
393
+ let workingDirectory = null;
394
+ if (scope === 'project') {
395
+ const resolved = await resolveProjectDirectory(req);
396
+ if (!resolved.directory) {
397
+ return res.status(400).json({
398
+ ok: false,
399
+ error: { kind: 'invalidSource', message: resolved.error || 'Project installs require a directory parameter' },
400
+ });
401
+ }
402
+ workingDirectory = resolved.directory;
403
+ }
404
+
405
+ if (isClawdHubSource(source)) {
406
+ const result = await installSkillsFromClawdHub({
407
+ scope,
408
+ targetSource,
409
+ workingDirectory,
410
+ userSkillDir: SKILL_DIR,
411
+ selections,
412
+ conflictPolicy,
413
+ conflictDecisions,
414
+ });
415
+
416
+ if (!result.ok) {
417
+ if (result.error?.kind === 'conflicts') {
418
+ return res.status(409).json({ ok: false, error: result.error });
419
+ }
420
+ return res.status(400).json({ ok: false, error: result.error });
421
+ }
422
+
423
+ const installed = result.installed || [];
424
+ const skipped = result.skipped || [];
425
+ const requiresReload = installed.length > 0;
426
+
427
+ if (requiresReload) {
428
+ await refreshOpenCodeAfterConfigChange('skills install');
429
+ }
430
+
431
+ return res.json({
432
+ ok: true,
433
+ installed,
434
+ skipped,
435
+ requiresReload,
436
+ message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
437
+ reloadDelayMs: requiresReload ? clientReloadDelayMs : undefined,
438
+ });
439
+ }
440
+
441
+ const identity = resolveGitIdentity(gitIdentityId);
442
+
443
+ const result = await installSkillsFromRepository({
444
+ source,
445
+ subpath,
446
+ identity,
447
+ scope,
448
+ targetSource,
449
+ workingDirectory,
450
+ userSkillDir: SKILL_DIR,
451
+ selections,
452
+ conflictPolicy,
453
+ conflictDecisions,
454
+ });
455
+
456
+ if (!result.ok) {
457
+ if (result.error?.kind === 'conflicts') {
458
+ return res.status(409).json({ ok: false, error: result.error });
459
+ }
460
+
461
+ if (result.error?.kind === 'authRequired') {
462
+ return res.status(401).json({
463
+ ok: false,
464
+ error: {
465
+ ...result.error,
466
+ identities: listGitIdentitiesForResponse(),
467
+ },
468
+ });
469
+ }
470
+
471
+ return res.status(400).json({ ok: false, error: result.error });
472
+ }
473
+
474
+ const installed = result.installed || [];
475
+ const skipped = result.skipped || [];
476
+ const requiresReload = installed.length > 0;
477
+
478
+ if (requiresReload) {
479
+ await refreshOpenCodeAfterConfigChange('skills install');
480
+ }
481
+
482
+ res.json({
483
+ ok: true,
484
+ installed,
485
+ skipped,
486
+ requiresReload,
487
+ message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
488
+ reloadDelayMs: requiresReload ? clientReloadDelayMs : undefined,
489
+ });
490
+ } catch (error) {
491
+ console.error('Failed to install skills:', error);
492
+ res.status(500).json({ ok: false, error: { kind: 'unknown', message: error.message || 'Failed to install skills' } });
493
+ }
494
+ });
495
+
496
+ app.get('/api/config/skills/:name', async (req, res) => {
497
+ try {
498
+ const skillName = req.params.name;
499
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
500
+ if (error) {
501
+ return res.status(400).json({ error });
502
+ }
503
+ const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
504
+ .find((skill) => skill.name === skillName) || null;
505
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
506
+
507
+ res.json({
508
+ name: skillName,
509
+ sources: sources,
510
+ scope: sources.md.scope,
511
+ source: sources.md.source,
512
+ exists: sources.md.exists
513
+ });
514
+ } catch (error) {
515
+ console.error('Failed to get skill sources:', error);
516
+ res.status(500).json({ error: 'Failed to get skill configuration metadata' });
517
+ }
518
+ });
519
+
520
+ app.get('/api/config/skills/:name/files/*filePath', async (req, res) => {
521
+ try {
522
+ const skillName = req.params.name;
523
+ const filePath = decodeURIComponent(req.params.filePath);
524
+ if (isUnsafeSkillRelativePath(filePath)) {
525
+ return res.status(400).json({ error: 'Invalid file path' });
526
+ }
527
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
528
+ if (error) {
529
+ return res.status(400).json({ error });
530
+ }
531
+
532
+ const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
533
+ .find((skill) => skill.name === skillName) || null;
534
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
535
+ if (!sources.md.exists || !sources.md.dir) {
536
+ return res.status(404).json({ error: 'Skill not found' });
537
+ }
538
+
539
+ const content = readSkillSupportingFile(sources.md.dir, filePath);
540
+ if (content === null) {
541
+ return res.status(404).json({ error: 'File not found' });
542
+ }
543
+
544
+ res.json({ path: filePath, content });
545
+ } catch (error) {
546
+ if (error && typeof error === 'object' && (error.code === 'EACCES' || error.code === 'EPERM')) {
547
+ return res.status(403).json({ error: 'Access to file denied' });
548
+ }
549
+ console.error('Failed to read skill file:', error);
550
+ res.status(500).json({ error: 'Failed to read skill file' });
551
+ }
552
+ });
553
+
554
+ app.post('/api/config/skills/:name', async (req, res) => {
555
+ try {
556
+ const skillName = req.params.name;
557
+ const { scope, source: skillSource, ...config } = req.body;
558
+ const { directory, error } = scope === SKILL_SCOPE.PROJECT
559
+ ? await resolveProjectDirectory(req)
560
+ : await resolveOptionalProjectDirectory(req);
561
+ if (error || (scope === SKILL_SCOPE.PROJECT && !directory)) {
562
+ return res.status(400).json({ error: error || 'Project skill creation requires a directory' });
563
+ }
564
+
565
+ console.log('[Server] Creating skill:', skillName);
566
+ console.log('[Server] Scope:', scope, 'Working directory:', directory);
567
+
568
+ createSkill(skillName, { ...config, source: skillSource }, directory, scope);
569
+ await refreshOpenCodeAfterConfigChange('skill creation');
570
+
571
+ res.json({
572
+ success: true,
573
+ requiresReload: true,
574
+ message: `Skill ${skillName} created successfully. Reloading interface…`,
575
+ reloadDelayMs: clientReloadDelayMs,
576
+ });
577
+ } catch (error) {
578
+ console.error('Failed to create skill:', error);
579
+ res.status(500).json({ error: error.message || 'Failed to create skill' });
580
+ }
581
+ });
582
+
583
+ app.patch('/api/config/skills/:name', async (req, res) => {
584
+ try {
585
+ const skillName = req.params.name;
586
+ const updates = req.body;
587
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
588
+ if (error) {
589
+ return res.status(400).json({ error });
590
+ }
591
+
592
+ console.log(`[Server] Updating skill: ${skillName}`);
593
+ console.log('[Server] Working directory:', directory);
594
+
595
+ updateSkill(skillName, updates, directory, updates?.targetPath);
596
+ await refreshOpenCodeAfterConfigChange('skill update');
597
+
598
+ res.json({
599
+ success: true,
600
+ requiresReload: true,
601
+ message: `Skill ${skillName} updated successfully. Reloading interface…`,
602
+ reloadDelayMs: clientReloadDelayMs,
603
+ });
604
+ } catch (error) {
605
+ console.error('[Server] Failed to update skill:', error);
606
+ res.status(500).json({ error: error.message || 'Failed to update skill' });
607
+ }
608
+ });
609
+
610
+ app.put('/api/config/skills/:name/files/*filePath', async (req, res) => {
611
+ try {
612
+ const skillName = req.params.name;
613
+ const filePath = decodeURIComponent(req.params.filePath);
614
+ if (isUnsafeSkillRelativePath(filePath)) {
615
+ return res.status(400).json({ error: 'Invalid file path' });
616
+ }
617
+ const { content } = req.body;
618
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
619
+ if (error) {
620
+ return res.status(400).json({ error });
621
+ }
622
+
623
+ const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
624
+ .find((skill) => skill.name === skillName) || null;
625
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
626
+ if (!sources.md.exists || !sources.md.dir) {
627
+ return res.status(404).json({ error: 'Skill not found' });
628
+ }
629
+
630
+ writeSkillSupportingFile(sources.md.dir, filePath, content || '');
631
+
632
+ res.json({
633
+ success: true,
634
+ message: `File ${filePath} saved successfully`,
635
+ });
636
+ } catch (error) {
637
+ if (error && typeof error === 'object' && (error.code === 'EACCES' || error.code === 'EPERM')) {
638
+ return res.status(403).json({ error: 'Access to file denied' });
639
+ }
640
+ console.error('Failed to write skill file:', error);
641
+ res.status(500).json({ error: error.message || 'Failed to write skill file' });
642
+ }
643
+ });
644
+
645
+ app.delete('/api/config/skills/:name/files/*filePath', async (req, res) => {
646
+ try {
647
+ const skillName = req.params.name;
648
+ const filePath = decodeURIComponent(req.params.filePath);
649
+ if (isUnsafeSkillRelativePath(filePath)) {
650
+ return res.status(400).json({ error: 'Invalid file path' });
651
+ }
652
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
653
+ if (error) {
654
+ return res.status(400).json({ error });
655
+ }
656
+
657
+ const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
658
+ .find((skill) => skill.name === skillName) || null;
659
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
660
+ if (!sources.md.exists || !sources.md.dir) {
661
+ return res.status(404).json({ error: 'Skill not found' });
662
+ }
663
+
664
+ deleteSkillSupportingFile(sources.md.dir, filePath);
665
+
666
+ res.json({
667
+ success: true,
668
+ message: `File ${filePath} deleted successfully`,
669
+ });
670
+ } catch (error) {
671
+ if (error && typeof error === 'object' && (error.code === 'EACCES' || error.code === 'EPERM')) {
672
+ return res.status(403).json({ error: 'Access to file denied' });
673
+ }
674
+ console.error('Failed to delete skill file:', error);
675
+ res.status(500).json({ error: error.message || 'Failed to delete skill file' });
676
+ }
677
+ });
678
+
679
+ app.delete('/api/config/skills/:name', async (req, res) => {
680
+ try {
681
+ const skillName = req.params.name;
682
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
683
+ if (error) {
684
+ return res.status(400).json({ error });
685
+ }
686
+
687
+ deleteSkill(skillName, directory);
688
+ await refreshOpenCodeAfterConfigChange('skill deletion');
689
+
690
+ res.json({
691
+ success: true,
692
+ requiresReload: true,
693
+ message: `Skill ${skillName} deleted successfully. Reloading interface…`,
694
+ reloadDelayMs: clientReloadDelayMs,
695
+ });
696
+ } catch (error) {
697
+ console.error('Failed to delete skill:', error);
698
+ res.status(500).json({ error: error.message || 'Failed to delete skill' });
699
+ }
700
+ });
701
+ };