dr-claw 1.1.2

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 (287) hide show
  1. package/LICENSE +1380 -0
  2. package/NOTICE +17 -0
  3. package/README.md +875 -0
  4. package/README.zh-CN.md +857 -0
  5. package/dist/api-docs.html +878 -0
  6. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
  66. package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
  67. package/dist/assets/arc-DOMBmC_A.js +1 -0
  68. package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
  71. package/dist/assets/channel-BFOE7OyC.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
  76. package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
  77. package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
  82. package/dist/assets/clone-BulQSblH.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
  84. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  85. package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
  94. package/dist/assets/graph-Blm-emkR.js +1 -0
  95. package/dist/assets/index-B8Yutsnm.css +1 -0
  96. package/dist/assets/index-Cf6gyhc3.js +654 -0
  97. package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
  98. package/dist/assets/init-Gi6I4Gst.js +1 -0
  99. package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
  100. package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
  101. package/dist/assets/layout-Ct2kFIId.js +1 -0
  102. package/dist/assets/linear-CKG9EwIJ.js +1 -0
  103. package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
  105. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
  107. package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
  108. package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
  109. package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
  110. package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
  111. package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
  113. package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
  114. package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
  115. package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
  116. package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
  117. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
  119. package/dist/clear-cache.html +85 -0
  120. package/dist/convert-icons.md +53 -0
  121. package/dist/dr-claw.png +0 -0
  122. package/dist/favicon.png +0 -0
  123. package/dist/generate-icons.js +49 -0
  124. package/dist/icons/claude-ai-icon.svg +1 -0
  125. package/dist/icons/codex-white.svg +3 -0
  126. package/dist/icons/codex.svg +3 -0
  127. package/dist/icons/cursor-white.svg +12 -0
  128. package/dist/icons/cursor.svg +1 -0
  129. package/dist/icons/file.svg +16997 -0
  130. package/dist/icons/generate-icons.md +19 -0
  131. package/dist/icons/icon-128x128.png +0 -0
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-152x152.png +0 -0
  134. package/dist/icons/icon-192x192.png +0 -0
  135. package/dist/icons/icon-384x384.png +0 -0
  136. package/dist/icons/icon-512x512.png +0 -0
  137. package/dist/icons/icon-72x72.png +0 -0
  138. package/dist/icons/icon-96x96.png +0 -0
  139. package/dist/icons/news/arxiv.svg +1 -0
  140. package/dist/icons/news/huggingface.svg +8 -0
  141. package/dist/icons/news/x-black.png +0 -0
  142. package/dist/icons/news/x-white.png +0 -0
  143. package/dist/icons/news/xiaohongshu.png +0 -0
  144. package/dist/index.html +51 -0
  145. package/dist/leveraged-cognition.png +0 -0
  146. package/dist/manifest.json +61 -0
  147. package/dist/screenshots/chat.png +0 -0
  148. package/dist/screenshots/new_claw.png +0 -0
  149. package/dist/screenshots/news_dashboard.png +0 -0
  150. package/dist/screenshots/project_dashboard.png +0 -0
  151. package/dist/screenshots/research_lab.png +0 -0
  152. package/dist/screenshots/skill_library.png +0 -0
  153. package/dist/screenshots/task_list.png +0 -0
  154. package/dist/screenshots/terminal_example.png +0 -0
  155. package/dist/screenshots/terminal_example1.png +0 -0
  156. package/dist/sw.js +58 -0
  157. package/dist/wechat-group-qr.jpg +0 -0
  158. package/package.json +133 -0
  159. package/public/api-docs.html +878 -0
  160. package/public/clear-cache.html +85 -0
  161. package/public/convert-icons.md +53 -0
  162. package/public/dr-claw.png +0 -0
  163. package/public/favicon.png +0 -0
  164. package/public/generate-icons.js +49 -0
  165. package/public/icons/claude-ai-icon.svg +1 -0
  166. package/public/icons/codex-white.svg +3 -0
  167. package/public/icons/codex.svg +3 -0
  168. package/public/icons/cursor-white.svg +12 -0
  169. package/public/icons/cursor.svg +1 -0
  170. package/public/icons/file.svg +16997 -0
  171. package/public/icons/generate-icons.md +19 -0
  172. package/public/icons/icon-128x128.png +0 -0
  173. package/public/icons/icon-144x144.png +0 -0
  174. package/public/icons/icon-152x152.png +0 -0
  175. package/public/icons/icon-192x192.png +0 -0
  176. package/public/icons/icon-384x384.png +0 -0
  177. package/public/icons/icon-512x512.png +0 -0
  178. package/public/icons/icon-72x72.png +0 -0
  179. package/public/icons/icon-96x96.png +0 -0
  180. package/public/icons/news/arxiv.svg +1 -0
  181. package/public/icons/news/huggingface.svg +8 -0
  182. package/public/icons/news/x-black.png +0 -0
  183. package/public/icons/news/x-white.png +0 -0
  184. package/public/icons/news/xiaohongshu.png +0 -0
  185. package/public/leveraged-cognition.png +0 -0
  186. package/public/manifest.json +61 -0
  187. package/public/screenshots/chat.png +0 -0
  188. package/public/screenshots/new_claw.png +0 -0
  189. package/public/screenshots/news_dashboard.png +0 -0
  190. package/public/screenshots/project_dashboard.png +0 -0
  191. package/public/screenshots/research_lab.png +0 -0
  192. package/public/screenshots/skill_library.png +0 -0
  193. package/public/screenshots/task_list.png +0 -0
  194. package/public/screenshots/terminal_example.png +0 -0
  195. package/public/screenshots/terminal_example1.png +0 -0
  196. package/public/sw.js +58 -0
  197. package/public/wechat-group-qr.jpg +0 -0
  198. package/scripts/export-skills-catalog-v2.mjs +746 -0
  199. package/scripts/fix-node-pty.js +45 -0
  200. package/scripts/gemini_image_core.py +470 -0
  201. package/scripts/install-python-tools.js +64 -0
  202. package/server/AGENTS.md +73 -0
  203. package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
  204. package/server/__tests__/runtimePorts.test.mjs +115 -0
  205. package/server/__tests__/session-delete.test.mjs +166 -0
  206. package/server/bin/compute-node +70 -0
  207. package/server/claude-sdk.js +799 -0
  208. package/server/cli-chat.js +488 -0
  209. package/server/cli.js +354 -0
  210. package/server/compute-node.js +439 -0
  211. package/server/constants/config.js +5 -0
  212. package/server/cursor-cli.js +340 -0
  213. package/server/database/auth.db +0 -0
  214. package/server/database/db.js +1852 -0
  215. package/server/database/init.sql +194 -0
  216. package/server/gemini-cli.js +1352 -0
  217. package/server/index.js +3154 -0
  218. package/server/load-env.js +71 -0
  219. package/server/local-gpu.js +860 -0
  220. package/server/middleware/auth.js +100 -0
  221. package/server/openai-codex.js +724 -0
  222. package/server/openrouter.js +890 -0
  223. package/server/project-token-usage.js +407 -0
  224. package/server/projects.js +3973 -0
  225. package/server/routes/agent.js +1272 -0
  226. package/server/routes/auth.js +128 -0
  227. package/server/routes/auto-research.js +558 -0
  228. package/server/routes/cli-auth.js +958 -0
  229. package/server/routes/codex.js +344 -0
  230. package/server/routes/commands.js +601 -0
  231. package/server/routes/compute.js +358 -0
  232. package/server/routes/cursor.js +795 -0
  233. package/server/routes/git.js +1348 -0
  234. package/server/routes/mcp-utils.js +48 -0
  235. package/server/routes/mcp.js +618 -0
  236. package/server/routes/news.js +711 -0
  237. package/server/routes/projects.js +633 -0
  238. package/server/routes/references.js +338 -0
  239. package/server/routes/settings.js +285 -0
  240. package/server/routes/skills.js +914 -0
  241. package/server/routes/taskmaster.js +2566 -0
  242. package/server/routes/telemetry.js +89 -0
  243. package/server/routes/user.js +155 -0
  244. package/server/scripts/fix_session_names.js +57 -0
  245. package/server/scripts/research-news/common_words.py +19 -0
  246. package/server/scripts/research-news/link_keywords.py +298 -0
  247. package/server/scripts/research-news/scan_existing_notes.py +266 -0
  248. package/server/scripts/research-news/scoring_utils.py +227 -0
  249. package/server/scripts/research-news/search_arxiv.py +842 -0
  250. package/server/scripts/research-news/search_huggingface.py +438 -0
  251. package/server/scripts/research-news/search_x.py +490 -0
  252. package/server/scripts/research-news/search_xiaohongshu.py +610 -0
  253. package/server/taskmaster-templates/README.md +150 -0
  254. package/server/taskmaster-templates/README.zh-CN.md +150 -0
  255. package/server/taskmaster-templates/ai-research-dataset.json +230 -0
  256. package/server/taskmaster-templates/ai-research-method-model.json +230 -0
  257. package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
  258. package/server/telemetry.js +121 -0
  259. package/server/templates/AGENTS.md +103 -0
  260. package/server/templates/CLAUDE.md +104 -0
  261. package/server/templates/GEMINI.md +103 -0
  262. package/server/templates/cursor-project.md +108 -0
  263. package/server/templates/index.js +37 -0
  264. package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
  265. package/server/utils/auto-research-mailer.js +57 -0
  266. package/server/utils/cliResolution.js +170 -0
  267. package/server/utils/commandParser.js +303 -0
  268. package/server/utils/cursorCommand.js +61 -0
  269. package/server/utils/geminiApiKey.js +28 -0
  270. package/server/utils/gitConfig.js +24 -0
  271. package/server/utils/imageAttachmentFiles.js +35 -0
  272. package/server/utils/mcp-detector.js +198 -0
  273. package/server/utils/parsers/bibtex-parser.js +207 -0
  274. package/server/utils/permissions.js +100 -0
  275. package/server/utils/runtimePorts.js +212 -0
  276. package/server/utils/sessionFormatting.js +68 -0
  277. package/server/utils/sessionIndex.js +100 -0
  278. package/server/utils/sessionMode.js +95 -0
  279. package/server/utils/sessionTokenUsage.js +105 -0
  280. package/server/utils/taskmaster-websocket.js +129 -0
  281. package/server/utils/zotero-client.js +136 -0
  282. package/shared/errorClassifier.js +102 -0
  283. package/shared/geminiThinkingSupport.d.ts +22 -0
  284. package/shared/geminiThinkingSupport.js +96 -0
  285. package/shared/geminiThoughtParser.d.ts +8 -0
  286. package/shared/geminiThoughtParser.js +30 -0
  287. package/shared/modelConstants.js +139 -0
@@ -0,0 +1,914 @@
1
+ import express from 'express';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ import { extractProjectDirectory, ensureProjectSkillLinks } from '../projects.js';
7
+ import { FORBIDDEN_PATHS } from './projects.js';
8
+
9
+ const GLOBAL_SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
10
+
11
+ const router = express.Router();
12
+
13
+ /**
14
+ * Sync a skill directory + tag config to the global skills/ folder.
15
+ * Failures are logged but never throw — local upload must always succeed.
16
+ */
17
+ async function syncSkillToGlobal(name, srcDir, tags) {
18
+ try {
19
+ await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
20
+ const destDir = path.join(GLOBAL_SKILLS_DIR, name);
21
+ // Remove stale copy then copy fresh
22
+ await fs.rm(destDir, { recursive: true, force: true });
23
+ await fs.cp(srcDir, destDir, { recursive: true });
24
+
25
+ // Update global skill-tag-mapping.json
26
+ const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
27
+ let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
28
+ try {
29
+ tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
30
+ } catch { /* use defaults */ }
31
+
32
+ if (tags && tags.stageOverride) {
33
+ tagMapping.stageOverrides = tagMapping.stageOverrides || {};
34
+ tagMapping.stageOverrides[name] = tags.stageOverride;
35
+ }
36
+ if (tags && tags.domainOverride) {
37
+ tagMapping.domainOverrides = tagMapping.domainOverrides || {};
38
+ tagMapping.domainOverrides[name] = tags.domainOverride;
39
+ }
40
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
41
+
42
+ // Update global stage-skill-map.json
43
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
44
+ let stageSkillMap = { skillOrigins: {} };
45
+ try {
46
+ stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
47
+ } catch { /* use defaults */ }
48
+
49
+ stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
50
+ stageSkillMap.skillOrigins[name] = (tags && tags.origin) || 'downloaded';
51
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
52
+ } catch (err) {
53
+ console.warn('[skills] syncSkillToGlobal failed (non-fatal):', err.message);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Remove a skill directory + clean its entries from global config files.
59
+ */
60
+ async function removeSkillFromGlobal(name) {
61
+ try {
62
+ const destDir = path.join(GLOBAL_SKILLS_DIR, name);
63
+ await fs.rm(destDir, { recursive: true, force: true });
64
+
65
+ // Clean global skill-tag-mapping.json
66
+ const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
67
+ try {
68
+ const tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
69
+ if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[name];
70
+ if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[name];
71
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
72
+ } catch { /* file missing or unparseable — nothing to clean */ }
73
+
74
+ // Clean global stage-skill-map.json
75
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
76
+ try {
77
+ const stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
78
+ if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[name];
79
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
80
+ } catch { /* file missing or unparseable — nothing to clean */ }
81
+ } catch (err) {
82
+ console.warn('[skills] removeSkillFromGlobal failed (non-fatal):', err.message);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Update tag overrides for a skill in the global config.
88
+ */
89
+ async function updateGlobalTags(name, tags) {
90
+ try {
91
+ const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
92
+ let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
93
+ try {
94
+ tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
95
+ } catch { /* use defaults */ }
96
+
97
+ const { stageOverride, domainOverride } = tags;
98
+
99
+ if (stageOverride !== undefined) {
100
+ tagMapping.stageOverrides = tagMapping.stageOverrides || {};
101
+ if (stageOverride === null) {
102
+ delete tagMapping.stageOverrides[name];
103
+ } else {
104
+ tagMapping.stageOverrides[name] = stageOverride;
105
+ }
106
+ }
107
+
108
+ if (domainOverride !== undefined) {
109
+ tagMapping.domainOverrides = tagMapping.domainOverrides || {};
110
+ if (domainOverride === null) {
111
+ delete tagMapping.domainOverrides[name];
112
+ } else {
113
+ tagMapping.domainOverrides[name] = domainOverride;
114
+ }
115
+ }
116
+
117
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
118
+
119
+ // Also sync origin if provided
120
+ if (tags.origin !== undefined) {
121
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
122
+ let stageSkillMap = { skillOrigins: {} };
123
+ try {
124
+ stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
125
+ } catch { /* use defaults */ }
126
+
127
+ stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
128
+ if (tags.origin === null) {
129
+ delete stageSkillMap.skillOrigins[name];
130
+ } else {
131
+ stageSkillMap.skillOrigins[name] = tags.origin;
132
+ }
133
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
134
+ }
135
+ } catch (err) {
136
+ console.warn('[skills] updateGlobalTags failed (non-fatal):', err.message);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Locate SKILL.md inside a zip (root or one level deep).
142
+ * Returns { entry, prefix } where prefix is '' or 'dirname/'.
143
+ */
144
+ function findSkillMd(zip) {
145
+ // Check root
146
+ const rootEntry = zip.getEntry('SKILL.md');
147
+ if (rootEntry) return { entry: rootEntry, prefix: '' };
148
+
149
+ // Check one level deep
150
+ for (const entry of zip.getEntries()) {
151
+ const parts = entry.entryName.split('/').filter(Boolean);
152
+ if (parts.length === 2 && parts[1] === 'SKILL.md' && entry.isDirectory === false) {
153
+ return { entry, prefix: parts[0] + '/' };
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * Parse YAML frontmatter from SKILL.md content using gray-matter.
161
+ */
162
+ async function parseFrontmatter(content) {
163
+ const matter = (await import('gray-matter')).default;
164
+ const { data, content: body } = matter(content);
165
+ return { data, body };
166
+ }
167
+
168
+ /**
169
+ * Sanitize skill name for use as directory name.
170
+ */
171
+ function safeDirName(name) {
172
+ return name
173
+ .toLowerCase()
174
+ .replace(/[^a-z0-9_-]+/g, '-')
175
+ .replace(/^-+|-+$/g, '')
176
+ .slice(0, 80);
177
+ }
178
+
179
+ /**
180
+ * Validate that a zip buffer contains a valid skill package.
181
+ */
182
+ async function validateZipBuffer(buffer) {
183
+ const AdmZip = (await import('adm-zip')).default;
184
+ const zip = new AdmZip(buffer);
185
+
186
+ const result = findSkillMd(zip);
187
+ if (!result) {
188
+ return { valid: false, error: 'No SKILL.md found at root or one directory deep.' };
189
+ }
190
+
191
+ const content = result.entry.getData().toString('utf8');
192
+ const { data: frontmatter, body } = await parseFrontmatter(content);
193
+
194
+ if (!frontmatter || !frontmatter.name) {
195
+ return { valid: false, error: 'SKILL.md must contain YAML frontmatter with a "name" field.' };
196
+ }
197
+
198
+ const entries = zip.getEntries();
199
+ const prefix = result.prefix;
200
+ const relevantEntries = prefix
201
+ ? entries.filter(e => e.entryName.startsWith(prefix))
202
+ : entries;
203
+ const fileCount = relevantEntries.filter(e => !e.isDirectory).length;
204
+ const hasPrompts = relevantEntries.some(e => {
205
+ const rel = prefix ? e.entryName.slice(prefix.length) : e.entryName;
206
+ return rel.startsWith('prompts/') || rel.startsWith('prompt/');
207
+ });
208
+ const hasReferences = relevantEntries.some(e => {
209
+ const rel = prefix ? e.entryName.slice(prefix.length) : e.entryName;
210
+ return rel.startsWith('references/') || rel.startsWith('reference/');
211
+ });
212
+
213
+ return {
214
+ valid: true,
215
+ skillName: frontmatter.name,
216
+ frontmatter,
217
+ description: body.trim().split('\n')[0] || '',
218
+ fileCount,
219
+ hasPrompts,
220
+ hasReferences,
221
+ prefix,
222
+ zip,
223
+ };
224
+ }
225
+
226
+ // GET / — return the file tree of the global skills/ directory
227
+ router.get('/', async (req, res) => {
228
+ try {
229
+ await fs.access(GLOBAL_SKILLS_DIR);
230
+
231
+ const { getFileTree } = await import('../file-tree.js').catch(() => ({}));
232
+ // Prefer shared helper when available, but fall back if it throws.
233
+ if (typeof getFileTree === 'function') {
234
+ try {
235
+ const tree = await getFileTree(GLOBAL_SKILLS_DIR, 5, 0, false);
236
+ return res.json(tree);
237
+ } catch (helperError) {
238
+ console.warn('[skills] getFileTree failed, falling back to local walker:', helperError.message);
239
+ }
240
+ }
241
+ // Manual recursive tree building (mirrors getFileTree from index.js)
242
+ async function buildTree(dirPath, maxDepth, depth) {
243
+ const items = [];
244
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ if (entry.name.startsWith('.')) continue;
247
+ if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue;
248
+ const itemPath = path.join(dirPath, entry.name);
249
+ let isDir = entry.isDirectory();
250
+ if (!isDir && entry.isSymbolicLink()) {
251
+ try { isDir = (await fs.stat(itemPath)).isDirectory(); } catch { /* ignore */ }
252
+ }
253
+ const item = { name: entry.name, path: itemPath, type: isDir ? 'directory' : 'file' };
254
+ if (isDir && depth < maxDepth) {
255
+ try {
256
+ item.children = await buildTree(itemPath, maxDepth, depth + 1);
257
+ } catch (childError) {
258
+ console.warn('[skills] Skipping unreadable directory:', itemPath, childError.message);
259
+ item.children = [];
260
+ }
261
+ }
262
+ items.push(item);
263
+ }
264
+ return items;
265
+ }
266
+ const tree = await buildTree(GLOBAL_SKILLS_DIR, 5, 0);
267
+ res.json(tree);
268
+ } catch (error) {
269
+ if (error.code === 'ENOENT') {
270
+ return res.status(404).json({ error: 'Global skills directory not found' });
271
+ }
272
+ console.error('[ERROR] Skills tree error:', error.message);
273
+ res.status(500).json({ error: error.message });
274
+ }
275
+ });
276
+
277
+ // GET /file — read a single file from the global skills/ directory
278
+ router.get('/file', async (req, res) => {
279
+ try {
280
+ const { filePath } = req.query;
281
+ if (!filePath) {
282
+ return res.status(400).json({ error: 'filePath is required' });
283
+ }
284
+ const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
285
+ const resolved = path.resolve(skillsDir, filePath);
286
+ if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
287
+ return res.status(403).json({ error: 'Path must be under skills root' });
288
+ }
289
+ const content = await fs.readFile(resolved, 'utf8');
290
+ res.json({ content });
291
+ } catch (error) {
292
+ if (error.code === 'ENOENT') {
293
+ res.status(404).json({ error: 'File not found' });
294
+ } else {
295
+ console.error('[ERROR] Skill file read error:', error.message);
296
+ res.status(500).json({ error: error.message });
297
+ }
298
+ }
299
+ });
300
+
301
+ // PUT /file — write a single file in the global skills/ directory
302
+ router.put('/file', async (req, res) => {
303
+ try {
304
+ const { filePath, content } = req.body || {};
305
+ if (!filePath || typeof content !== 'string') {
306
+ return res.status(400).json({ error: 'filePath and content are required' });
307
+ }
308
+ const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
309
+ const resolved = path.resolve(skillsDir, filePath);
310
+ if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
311
+ return res.status(403).json({ error: 'Path must be under skills root' });
312
+ }
313
+ await fs.writeFile(resolved, content, 'utf8');
314
+ res.json({ success: true });
315
+ } catch (error) {
316
+ console.error('[ERROR] Skill file write error:', error.message);
317
+ res.status(500).json({ error: error.message });
318
+ }
319
+ });
320
+
321
+ // GET /scan-local — scan a local directory for skill subdirectories
322
+ router.get('/scan-local', async (req, res) => {
323
+ try {
324
+ const rawPath = req.query.path || '~/.claude/skills';
325
+ const resolvedPath = rawPath.replace(/^~/, os.homedir());
326
+ const absolutePath = path.resolve(resolvedPath);
327
+
328
+ // Security: reject forbidden system paths
329
+ const normalizedPath = path.normalize(absolutePath);
330
+ for (const forbidden of FORBIDDEN_PATHS) {
331
+ if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) {
332
+ return res.status(403).json({ error: `Scanning system directory "${forbidden}" is not allowed.` });
333
+ }
334
+ }
335
+
336
+ // Validate the path exists and is a directory
337
+ let stat;
338
+ try {
339
+ stat = await fs.stat(absolutePath);
340
+ } catch {
341
+ return res.status(404).json({ error: `Path does not exist: ${rawPath}` });
342
+ }
343
+ if (!stat.isDirectory()) {
344
+ return res.status(400).json({ error: `Path is not a directory: ${rawPath}` });
345
+ }
346
+
347
+ // Scan for subdirectories (1-level deep)
348
+ const entries = await fs.readdir(absolutePath, { withFileTypes: true });
349
+ const skills = [];
350
+
351
+ for (const entry of entries) {
352
+ if (!entry.isDirectory()) continue;
353
+ if (entry.name.startsWith('.')) continue;
354
+
355
+ const skillDir = path.join(absolutePath, entry.name);
356
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
357
+ let hasSkillMd = false;
358
+ try {
359
+ await fs.access(skillMdPath);
360
+ hasSkillMd = true;
361
+ } catch {
362
+ // No SKILL.md
363
+ }
364
+
365
+ // Check if already imported in GLOBAL_SKILLS_DIR
366
+ let alreadyImported = false;
367
+ try {
368
+ await fs.access(path.join(GLOBAL_SKILLS_DIR, entry.name));
369
+ alreadyImported = true;
370
+ } catch {
371
+ // Not imported
372
+ }
373
+
374
+ skills.push({
375
+ name: entry.name,
376
+ hasSkillMd,
377
+ alreadyImported,
378
+ sourcePath: skillDir,
379
+ });
380
+ }
381
+
382
+ res.json({ path: rawPath, resolvedPath: absolutePath, skills });
383
+ } catch (error) {
384
+ console.error('[skills] scan-local error:', error);
385
+ res.status(500).json({ error: error.message });
386
+ }
387
+ });
388
+
389
+ // POST /import-from-local — copy selected skills from a local directory into GLOBAL_SKILLS_DIR
390
+ router.post('/import-from-local', async (req, res) => {
391
+ try {
392
+ const { sourcePath: rawPath, skillNames } = req.body || {};
393
+ const resolvedPath = (rawPath || '~/.claude/skills').replace(/^~/, os.homedir());
394
+ const absolutePath = path.resolve(resolvedPath);
395
+
396
+ // Security: reject forbidden system paths
397
+ const normalizedPath = path.normalize(absolutePath);
398
+ for (const forbidden of FORBIDDEN_PATHS) {
399
+ if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) {
400
+ return res.status(403).json({ error: `Importing from system directory "${forbidden}" is not allowed.` });
401
+ }
402
+ }
403
+
404
+ // Validate the path exists and is a directory
405
+ let stat;
406
+ try {
407
+ stat = await fs.stat(absolutePath);
408
+ } catch {
409
+ return res.status(404).json({ error: `Path does not exist: ${rawPath || '~/.claude/skills'}` });
410
+ }
411
+ if (!stat.isDirectory()) {
412
+ return res.status(400).json({ error: `Path is not a directory: ${rawPath || '~/.claude/skills'}` });
413
+ }
414
+
415
+ // If skillNames is provided, only import those; otherwise scan all subdirectories
416
+ let dirsToImport = [];
417
+ if (Array.isArray(skillNames) && skillNames.length > 0) {
418
+ for (const name of skillNames) {
419
+ const skillDir = path.join(absolutePath, name);
420
+ try {
421
+ const s = await fs.stat(skillDir);
422
+ if (s.isDirectory()) {
423
+ dirsToImport.push({ name, sourcePath: skillDir });
424
+ }
425
+ } catch {
426
+ // Skip missing
427
+ }
428
+ }
429
+ } else {
430
+ const entries = await fs.readdir(absolutePath, { withFileTypes: true });
431
+ for (const entry of entries) {
432
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
433
+ dirsToImport.push({ name: entry.name, sourcePath: path.join(absolutePath, entry.name) });
434
+ }
435
+ }
436
+
437
+ await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
438
+
439
+ const imported = [];
440
+ const skipped = [];
441
+ const errors = [];
442
+
443
+ for (const { name, sourcePath: srcDir } of dirsToImport) {
444
+ const destDir = path.join(GLOBAL_SKILLS_DIR, name);
445
+ try {
446
+ // Check if already exists
447
+ try {
448
+ await fs.access(destDir);
449
+ skipped.push(name);
450
+ continue;
451
+ } catch {
452
+ // Does not exist — proceed with copy
453
+ }
454
+
455
+ await fs.cp(srcDir, destDir, { recursive: true });
456
+
457
+ // Update stage-skill-map.json with origin 'local-import'
458
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
459
+ let stageSkillMap = { skillOrigins: {} };
460
+ try {
461
+ stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
462
+ } catch { /* use defaults */ }
463
+ stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
464
+ stageSkillMap.skillOrigins[name] = 'local-import';
465
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
466
+
467
+ imported.push(name);
468
+ } catch (err) {
469
+ errors.push(`${name}: ${err.message}`);
470
+ }
471
+ }
472
+
473
+ res.json({ imported, skipped, errors });
474
+ } catch (error) {
475
+ console.error('[skills] import-from-local error:', error);
476
+ res.status(500).json({ error: error.message });
477
+ }
478
+ });
479
+
480
+ // POST /:projectName/import-user-skills — import from ~/.claude/skills/ into GLOBAL_SKILLS_DIR then re-sync symlinks
481
+ router.post('/:projectName/import-user-skills', async (req, res) => {
482
+ try {
483
+ const projectName = req.params.projectName;
484
+ const projectRoot = await extractProjectDirectory(projectName);
485
+ if (!projectRoot) {
486
+ return res.status(404).json({ error: 'Project not found.' });
487
+ }
488
+
489
+ // Resolve ~/.claude/skills/
490
+ const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
491
+ let stat;
492
+ try {
493
+ stat = await fs.stat(userSkillsDir);
494
+ } catch {
495
+ return res.status(404).json({ error: 'User skills directory not found (~/.claude/skills/).' });
496
+ }
497
+ if (!stat.isDirectory()) {
498
+ return res.status(400).json({ error: '~/.claude/skills/ is not a directory.' });
499
+ }
500
+
501
+ // Scan for subdirs containing SKILL.md
502
+ const entries = await fs.readdir(userSkillsDir, { withFileTypes: true });
503
+ await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
504
+
505
+ const imported = [];
506
+ const skipped = [];
507
+ const errors = [];
508
+
509
+ for (const entry of entries) {
510
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
511
+
512
+ const srcDir = path.join(userSkillsDir, entry.name);
513
+
514
+ // Verify SKILL.md exists
515
+ try {
516
+ await fs.access(path.join(srcDir, 'SKILL.md'));
517
+ } catch {
518
+ continue; // Skip dirs without SKILL.md
519
+ }
520
+
521
+ const destDir = path.join(GLOBAL_SKILLS_DIR, entry.name);
522
+
523
+ try {
524
+ // Check if already exists in global skills
525
+ try {
526
+ await fs.access(destDir);
527
+ skipped.push(entry.name);
528
+ continue;
529
+ } catch {
530
+ // Does not exist — proceed
531
+ }
532
+
533
+ await fs.cp(srcDir, destDir, { recursive: true });
534
+
535
+ // Parse frontmatter from SKILL.md for tag extraction
536
+ let frontmatter = {};
537
+ try {
538
+ const skillMdContent = await fs.readFile(path.join(srcDir, 'SKILL.md'), 'utf8');
539
+ const parsed = await parseFrontmatter(skillMdContent);
540
+ frontmatter = parsed.data || {};
541
+ } catch { /* ignore parse errors */ }
542
+
543
+ // Update skill-tag-mapping.json with tag overrides from frontmatter
544
+ const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
545
+ let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
546
+ try {
547
+ tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
548
+ } catch { /* use defaults */ }
549
+ if (frontmatter.stage) {
550
+ tagMapping.stageOverrides = tagMapping.stageOverrides || {};
551
+ tagMapping.stageOverrides[entry.name] = typeof frontmatter.stage === 'string'
552
+ ? { en: `Stage: ${frontmatter.stage}`, zh: `阶段: ${frontmatter.stage}` }
553
+ : frontmatter.stage;
554
+ }
555
+ if (frontmatter.domain) {
556
+ tagMapping.domainOverrides = tagMapping.domainOverrides || {};
557
+ tagMapping.domainOverrides[entry.name] = typeof frontmatter.domain === 'string'
558
+ ? { en: `Domain: ${frontmatter.domain}`, zh: `领域: ${frontmatter.domain}` }
559
+ : frontmatter.domain;
560
+ }
561
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
562
+
563
+ // Update stage-skill-map.json with origin
564
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
565
+ let stageSkillMap = { skillOrigins: {} };
566
+ try {
567
+ stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
568
+ } catch { /* use defaults */ }
569
+ stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
570
+ stageSkillMap.skillOrigins[entry.name] = 'user-import';
571
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
572
+
573
+ imported.push(entry.name);
574
+ } catch (err) {
575
+ errors.push(`${entry.name}: ${err.message}`);
576
+ }
577
+ }
578
+
579
+ // Re-sync symlinks for the requesting project
580
+ if (imported.length > 0) {
581
+ try {
582
+ await ensureProjectSkillLinks(projectRoot);
583
+ } catch (err) {
584
+ console.warn('[skills] ensureProjectSkillLinks after import failed (non-fatal):', err.message);
585
+ }
586
+ }
587
+
588
+ res.json({ imported, skipped, errors });
589
+ } catch (error) {
590
+ console.error('[skills] import-user-skills error:', error);
591
+ res.status(500).json({ error: error.message });
592
+ }
593
+ });
594
+
595
+ // POST /:projectName/validate-skill-zip
596
+ router.post('/:projectName/validate-skill-zip', async (req, res) => {
597
+ try {
598
+ const multer = (await import('multer')).default;
599
+ const upload = multer({
600
+ storage: multer.memoryStorage(),
601
+ limits: { fileSize: 50 * 1024 * 1024 },
602
+ });
603
+
604
+ upload.single('file')(req, res, async (err) => {
605
+ if (err) {
606
+ return res.status(400).json({ error: 'Failed to process uploaded file: ' + err.message });
607
+ }
608
+ if (!req.file) {
609
+ return res.status(400).json({ error: 'No file provided.' });
610
+ }
611
+
612
+ try {
613
+ const result = await validateZipBuffer(req.file.buffer);
614
+ if (!result.valid) {
615
+ return res.status(400).json({ valid: false, error: result.error });
616
+ }
617
+
618
+ return res.json({
619
+ valid: true,
620
+ skillName: result.skillName,
621
+ frontmatter: result.frontmatter,
622
+ description: result.description,
623
+ fileCount: result.fileCount,
624
+ hasPrompts: result.hasPrompts,
625
+ hasReferences: result.hasReferences,
626
+ });
627
+ } catch (parseErr) {
628
+ return res.status(400).json({ valid: false, error: 'Failed to parse zip: ' + parseErr.message });
629
+ }
630
+ });
631
+ } catch (error) {
632
+ console.error('[skills] validate-skill-zip error:', error);
633
+ res.status(500).json({ error: 'Internal server error' });
634
+ }
635
+ });
636
+
637
+ // POST /:projectName/upload-skill
638
+ router.post('/:projectName/upload-skill', async (req, res) => {
639
+ try {
640
+ const multer = (await import('multer')).default;
641
+ const upload = multer({
642
+ storage: multer.memoryStorage(),
643
+ limits: { fileSize: 50 * 1024 * 1024 },
644
+ });
645
+
646
+ upload.single('file')(req, res, async (err) => {
647
+ if (err) {
648
+ return res.status(400).json({ error: 'Failed to process uploaded file: ' + err.message });
649
+ }
650
+ if (!req.file) {
651
+ return res.status(400).json({ error: 'No file provided.' });
652
+ }
653
+
654
+ let extractedDir = null;
655
+
656
+ try {
657
+ // Resolve project root
658
+ const projectName = req.params.projectName;
659
+ const projectRoot = await extractProjectDirectory(projectName);
660
+ if (!projectRoot) {
661
+ return res.status(404).json({ error: 'Project not found.' });
662
+ }
663
+
664
+ // Validate zip
665
+ const result = await validateZipBuffer(req.file.buffer);
666
+ if (!result.valid) {
667
+ return res.status(400).json({ error: result.error });
668
+ }
669
+
670
+ const safeSkillName = safeDirName(result.skillName);
671
+ if (!safeSkillName) {
672
+ return res.status(400).json({ error: 'Invalid skill name.' });
673
+ }
674
+
675
+ // Determine skills directory (use skills/ relative to project root)
676
+ const skillsDir = path.join(projectRoot, 'skills');
677
+ await fs.mkdir(skillsDir, { recursive: true });
678
+
679
+ extractedDir = path.join(skillsDir, safeSkillName);
680
+
681
+ // Check for conflicts
682
+ try {
683
+ await fs.access(extractedDir);
684
+ return res.status(409).json({ error: `Skill directory "${safeSkillName}" already exists.` });
685
+ } catch {
686
+ // Directory doesn't exist — good
687
+ }
688
+
689
+ await fs.mkdir(extractedDir, { recursive: true });
690
+
691
+ // Extract files with path traversal protection
692
+ const { zip, prefix } = result;
693
+ for (const entry of zip.getEntries()) {
694
+ if (entry.isDirectory) continue;
695
+
696
+ let relativePath = entry.entryName;
697
+ if (prefix && relativePath.startsWith(prefix)) {
698
+ relativePath = relativePath.slice(prefix.length);
699
+ } else if (prefix) {
700
+ continue; // Skip files outside the skill directory
701
+ }
702
+
703
+ // Path traversal protection
704
+ const resolved = path.resolve(extractedDir, relativePath);
705
+ if (!resolved.startsWith(extractedDir)) {
706
+ continue; // Skip dangerous paths
707
+ }
708
+
709
+ const dir = path.dirname(resolved);
710
+ await fs.mkdir(dir, { recursive: true });
711
+ await fs.writeFile(resolved, entry.getData());
712
+ }
713
+
714
+ // Parse tags from request body
715
+ let tags = {};
716
+ try {
717
+ if (req.body && req.body.tags) {
718
+ tags = typeof req.body.tags === 'string' ? JSON.parse(req.body.tags) : req.body.tags;
719
+ }
720
+ } catch {
721
+ // Ignore invalid tags JSON
722
+ }
723
+
724
+ // Update skill-tag-mapping.json
725
+ const tagMappingPath = path.join(projectRoot, 'skills', 'skill-tag-mapping.json');
726
+ let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
727
+ try {
728
+ const raw = await fs.readFile(tagMappingPath, 'utf8');
729
+ tagMapping = JSON.parse(raw);
730
+ } catch {
731
+ // Use defaults
732
+ }
733
+
734
+ if (tags.stageOverride) {
735
+ tagMapping.stageOverrides = tagMapping.stageOverrides || {};
736
+ tagMapping.stageOverrides[safeSkillName] = tags.stageOverride;
737
+ }
738
+ if (tags.domainOverride) {
739
+ tagMapping.domainOverrides = tagMapping.domainOverrides || {};
740
+ tagMapping.domainOverrides[safeSkillName] = tags.domainOverride;
741
+ }
742
+
743
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
744
+
745
+ // Update stage-skill-map.json with origin
746
+ const stageSkillMapPath = path.join(projectRoot, 'skills', 'stage-skill-map.json');
747
+ let stageSkillMap = { skillOrigins: {} };
748
+ try {
749
+ const raw = await fs.readFile(stageSkillMapPath, 'utf8');
750
+ stageSkillMap = JSON.parse(raw);
751
+ } catch {
752
+ // Use defaults
753
+ }
754
+
755
+ stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
756
+ stageSkillMap.skillOrigins[safeSkillName] = tags.origin || 'downloaded';
757
+
758
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
759
+
760
+ // Sync to global skills directory
761
+ await syncSkillToGlobal(safeSkillName, extractedDir, tags);
762
+
763
+ return res.json({
764
+ success: true,
765
+ skillName: result.skillName,
766
+ dirName: safeSkillName,
767
+ path: extractedDir,
768
+ frontmatter: result.frontmatter,
769
+ });
770
+ } catch (uploadErr) {
771
+ // Cleanup on failure
772
+ if (extractedDir) {
773
+ try {
774
+ await fs.rm(extractedDir, { recursive: true, force: true });
775
+ } catch {
776
+ // Ignore cleanup errors
777
+ }
778
+ }
779
+ console.error('[skills] upload-skill error:', uploadErr);
780
+ return res.status(500).json({ error: 'Failed to extract skill: ' + uploadErr.message });
781
+ }
782
+ });
783
+ } catch (error) {
784
+ console.error('[skills] upload-skill error:', error);
785
+ res.status(500).json({ error: 'Internal server error' });
786
+ }
787
+ });
788
+
789
+ // DELETE /global-skill — delete a skill from the global GLOBAL_SKILLS_DIR by relative dirPath
790
+ router.delete('/global-skill', async (req, res) => {
791
+ try {
792
+ const { dirPath } = req.body || {};
793
+ if (!dirPath || typeof dirPath !== 'string') {
794
+ return res.status(400).json({ error: 'dirPath is required' });
795
+ }
796
+ const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
797
+ const resolved = path.resolve(skillsDir, dirPath);
798
+ if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
799
+ return res.status(403).json({ error: 'Path must be under skills root' });
800
+ }
801
+ try {
802
+ const stat = await fs.stat(resolved);
803
+ if (!stat.isDirectory()) {
804
+ return res.status(400).json({ error: 'Path is not a directory' });
805
+ }
806
+ } catch {
807
+ return res.status(404).json({ error: 'Skill directory not found' });
808
+ }
809
+ await fs.rm(resolved, { recursive: true, force: true });
810
+
811
+ // Clean config files using the skill folder name
812
+ const skillName = path.basename(resolved);
813
+ const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
814
+ try {
815
+ const tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
816
+ if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[skillName];
817
+ if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[skillName];
818
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
819
+ } catch { /* file missing or unparseable */ }
820
+
821
+ const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
822
+ try {
823
+ const stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
824
+ if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[skillName];
825
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
826
+ } catch { /* file missing or unparseable */ }
827
+
828
+ res.json({ success: true, deleted: dirPath });
829
+ } catch (error) {
830
+ console.error('[skills] delete-global-skill error:', error);
831
+ res.status(500).json({ error: 'Failed to delete skill: ' + error.message });
832
+ }
833
+ });
834
+
835
+ // DELETE /:projectName/:skillDirName
836
+ router.delete('/:projectName/:skillDirName', async (req, res) => {
837
+ try {
838
+ const { projectName, skillDirName } = req.params;
839
+ const projectRoot = await extractProjectDirectory(projectName);
840
+ if (!projectRoot) {
841
+ return res.status(404).json({ error: 'Project not found.' });
842
+ }
843
+
844
+ // Search candidate locations for the skill directory
845
+ const candidateBases = ['skills', '.claude/skills', '.agents/skills', '.cursor/skills'];
846
+ let foundDir = null;
847
+ for (const base of candidateBases) {
848
+ const candidate = path.join(projectRoot, base, skillDirName);
849
+ try {
850
+ const stat = await fs.stat(candidate);
851
+ if (stat.isDirectory()) {
852
+ foundDir = candidate;
853
+ break;
854
+ }
855
+ } catch {
856
+ // Not found in this location
857
+ }
858
+ }
859
+
860
+ if (!foundDir) {
861
+ return res.status(404).json({ error: `Skill directory "${skillDirName}" not found.` });
862
+ }
863
+
864
+ // Delete the skill directory
865
+ await fs.rm(foundDir, { recursive: true, force: true });
866
+
867
+ // Determine the parent skills dir from the found location for config cleanup
868
+ const foundSkillsBase = path.dirname(foundDir);
869
+
870
+ // Clean up skill-tag-mapping.json in the skills base where the skill was found
871
+ // Also clean from the legacy skills/ location for backwards compat
872
+ const tagMappingCandidates = [
873
+ path.join(foundSkillsBase, 'skill-tag-mapping.json'),
874
+ path.join(projectRoot, 'skills', 'skill-tag-mapping.json'),
875
+ ];
876
+ for (const tagMappingPath of [...new Set(tagMappingCandidates)]) {
877
+ try {
878
+ const raw = await fs.readFile(tagMappingPath, 'utf8');
879
+ const tagMapping = JSON.parse(raw);
880
+ if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[skillDirName];
881
+ if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[skillDirName];
882
+ await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
883
+ } catch {
884
+ // Tag mapping file doesn't exist or can't be parsed; skip cleanup
885
+ }
886
+ }
887
+
888
+ // Clean up stage-skill-map.json in the skills base + legacy location
889
+ const stageMapCandidates = [
890
+ path.join(foundSkillsBase, 'stage-skill-map.json'),
891
+ path.join(projectRoot, 'skills', 'stage-skill-map.json'),
892
+ ];
893
+ for (const stageSkillMapPath of [...new Set(stageMapCandidates)]) {
894
+ try {
895
+ const raw = await fs.readFile(stageSkillMapPath, 'utf8');
896
+ const stageSkillMap = JSON.parse(raw);
897
+ if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[skillDirName];
898
+ await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
899
+ } catch {
900
+ // Stage-skill-map file doesn't exist or can't be parsed; skip cleanup
901
+ }
902
+ }
903
+
904
+ // Remove from global skills directory
905
+ await removeSkillFromGlobal(skillDirName);
906
+
907
+ return res.json({ success: true, deleted: skillDirName });
908
+ } catch (error) {
909
+ console.error('[skills] delete-skill error:', error);
910
+ res.status(500).json({ error: 'Failed to delete skill: ' + error.message });
911
+ }
912
+ });
913
+
914
+ export default router;