cli-claw-kit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
@@ -0,0 +1,777 @@
1
+ // Skills management routes
2
+ import { Hono } from 'hono';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { execFile } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { authMiddleware } from '../middleware/auth.js';
9
+ import { DATA_DIR } from '../config.js';
10
+ import { resolveAppPath } from '../app-root.js';
11
+ import { getSystemSettings, saveSystemSettings, } from '../runtime-config.js';
12
+ import { parseFrontmatter, validateSkillId, validateSkillPath, listFiles, scanSkillDirectory, } from '../skill-utils.js';
13
+ const execFileAsync = promisify(execFile);
14
+ let skillInstallLock = Promise.resolve();
15
+ const skillsRoutes = new Hono();
16
+ // --- Utility Functions ---
17
+ function getUserSkillsDir(userId) {
18
+ return path.join(DATA_DIR, 'skills', userId);
19
+ }
20
+ function getGlobalSkillsDir() {
21
+ return path.join(os.homedir(), '.claude', 'skills');
22
+ }
23
+ function getProjectSkillsDir() {
24
+ return resolveAppPath('container', 'skills');
25
+ }
26
+ function getHostSyncManifestPath(userId) {
27
+ return path.join(DATA_DIR, 'skills', userId, '.host-sync.json');
28
+ }
29
+ function readHostSyncManifest(userId) {
30
+ try {
31
+ const data = fs.readFileSync(getHostSyncManifestPath(userId), 'utf-8');
32
+ return JSON.parse(data);
33
+ }
34
+ catch {
35
+ return { syncedSkills: [], lastSyncAt: '' };
36
+ }
37
+ }
38
+ function writeHostSyncManifest(userId, manifest) {
39
+ const manifestPath = getHostSyncManifestPath(userId);
40
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
41
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
42
+ }
43
+ function getSkillsManifestPath(userId) {
44
+ return path.join(DATA_DIR, 'skills', userId, '.skills-manifest.json');
45
+ }
46
+ function readSkillsManifest(userId) {
47
+ try {
48
+ const data = fs.readFileSync(getSkillsManifestPath(userId), 'utf-8');
49
+ return JSON.parse(data);
50
+ }
51
+ catch {
52
+ return { skills: {} };
53
+ }
54
+ }
55
+ function writeSkillsManifest(userId, manifest) {
56
+ const manifestPath = getSkillsManifestPath(userId);
57
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
58
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
59
+ }
60
+ /**
61
+ * Update the skills manifest after installing skills.
62
+ * Records packageName, installedAt, and source for each installed skill.
63
+ */
64
+ function updateSkillsManifest(userId, packageName, installedSkillIds) {
65
+ const manifest = readSkillsManifest(userId);
66
+ const now = new Date().toISOString();
67
+ for (const id of installedSkillIds) {
68
+ manifest.skills[id] = {
69
+ packageName,
70
+ installedAt: now,
71
+ source: 'skills.sh',
72
+ };
73
+ }
74
+ writeSkillsManifest(userId, manifest);
75
+ }
76
+ /**
77
+ * Remove a skill from the manifest when it is deleted.
78
+ */
79
+ function removeFromSkillsManifest(userId, skillId) {
80
+ const manifest = readSkillsManifest(userId);
81
+ if (skillId in manifest.skills) {
82
+ delete manifest.skills[skillId];
83
+ writeSkillsManifest(userId, manifest);
84
+ }
85
+ }
86
+ // validateSkillId, validateSkillPath, parseFrontmatter, listFiles, scanSkillDirectory
87
+ // are imported from '../skill-utils.js'
88
+ function scanDirectory(rootDir, source) {
89
+ return scanSkillDirectory(rootDir, source);
90
+ }
91
+ function discoverSkills(userId) {
92
+ const userSkills = scanDirectory(getUserSkillsDir(userId), 'user');
93
+ const projectSkills = scanDirectory(getProjectSkillsDir(), 'project');
94
+ // 读取 host sync manifest 标记同步来源
95
+ const hostManifest = readHostSyncManifest(userId);
96
+ const syncedSet = new Set(hostManifest.syncedSkills);
97
+ // 读取 skills manifest 补充安装元数据
98
+ const skillsManifest = readSkillsManifest(userId);
99
+ for (const skill of userSkills) {
100
+ if (syncedSet.has(skill.id)) {
101
+ skill.syncedFromHost = true;
102
+ }
103
+ const meta = skillsManifest.skills[skill.id];
104
+ if (meta) {
105
+ skill.packageName = meta.packageName;
106
+ skill.installedAt = meta.installedAt;
107
+ }
108
+ }
109
+ return [...userSkills, ...projectSkills];
110
+ }
111
+ function getSkillDetail(skillId, userId) {
112
+ if (!validateSkillId(skillId))
113
+ return null;
114
+ const searchDirs = [
115
+ { rootDir: getUserSkillsDir(userId), source: 'user' },
116
+ { rootDir: getProjectSkillsDir(), source: 'project' },
117
+ ];
118
+ const hostManifest = readHostSyncManifest(userId);
119
+ const syncedSet = new Set(hostManifest.syncedSkills);
120
+ const skillsManifest = readSkillsManifest(userId);
121
+ for (const { rootDir, source } of searchDirs) {
122
+ const skillDir = path.join(rootDir, skillId);
123
+ if (!fs.existsSync(skillDir))
124
+ continue;
125
+ if (!validateSkillPath(rootDir, skillDir))
126
+ continue;
127
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
128
+ const skillMdDisabledPath = path.join(skillDir, 'SKILL.md.disabled');
129
+ let enabled = false;
130
+ let skillFilePath = null;
131
+ if (fs.existsSync(skillMdPath)) {
132
+ enabled = true;
133
+ skillFilePath = skillMdPath;
134
+ }
135
+ else if (fs.existsSync(skillMdDisabledPath)) {
136
+ enabled = false;
137
+ skillFilePath = skillMdDisabledPath;
138
+ }
139
+ else {
140
+ continue;
141
+ }
142
+ try {
143
+ const content = fs.readFileSync(skillFilePath, 'utf-8');
144
+ const frontmatter = parseFrontmatter(content);
145
+ const stats = fs.statSync(skillDir);
146
+ const detail = {
147
+ id: skillId,
148
+ name: frontmatter.name || skillId,
149
+ description: frontmatter.description || '',
150
+ source,
151
+ enabled,
152
+ userInvocable: frontmatter['user-invocable'] === undefined
153
+ ? true
154
+ : frontmatter['user-invocable'] !== 'false',
155
+ allowedTools: frontmatter['allowed-tools']
156
+ ? frontmatter['allowed-tools'].split(',').map((t) => t.trim())
157
+ : [],
158
+ argumentHint: frontmatter['argument-hint'] || null,
159
+ updatedAt: stats.mtime.toISOString(),
160
+ files: listFiles(skillDir),
161
+ content,
162
+ };
163
+ if (source === 'user') {
164
+ if (syncedSet.has(skillId)) {
165
+ detail.syncedFromHost = true;
166
+ }
167
+ const meta = skillsManifest.skills[skillId];
168
+ if (meta) {
169
+ detail.packageName = meta.packageName;
170
+ detail.installedAt = meta.installedAt;
171
+ }
172
+ }
173
+ return detail;
174
+ }
175
+ catch {
176
+ // Skip malformed skill
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * Parse the output of `npx skills find <query>` to extract search results.
183
+ * The output contains ANSI codes and formatted text like:
184
+ * owner/repo@skill-name
185
+ * https://skills.sh/owner/repo/skill
186
+ */
187
+ function parseSearchOutput(output) {
188
+ // Strip ANSI escape codes
189
+ const clean = output.replace(/\x1b\[[0-9;]*m/g, '');
190
+ const results = [];
191
+ const lines = clean
192
+ .split('\n')
193
+ .map((l) => l.trim())
194
+ .filter(Boolean);
195
+ for (let i = 0; i < lines.length; i++) {
196
+ const line = lines[i];
197
+ // Match package pattern: owner/repo or owner/repo@skill
198
+ const pkgMatch = line.match(/^([\w\-]+\/[\w\-.]+(?:@[\w\-.]+)?)$/);
199
+ if (pkgMatch) {
200
+ const pkg = pkgMatch[1];
201
+ // Next line might be the URL (possibly prefixed with └ or similar chars)
202
+ let url = '';
203
+ if (i + 1 < lines.length) {
204
+ const nextLine = lines[i + 1].replace(/^[└├│─\s]+/, '');
205
+ if (nextLine.startsWith('http')) {
206
+ url = nextLine;
207
+ i++;
208
+ }
209
+ }
210
+ results.push({ package: pkg, url });
211
+ }
212
+ }
213
+ return results;
214
+ }
215
+ /**
216
+ * Find skill entries under a path that were modified after the given timestamp.
217
+ * Handles both real directories and symlinks (skills CLI creates symlinks in
218
+ * ~/.claude/skills/ pointing to ~/.agents/skills/).
219
+ * Returns entry names.
220
+ */
221
+ function findModifiedEntries(dir, afterMs) {
222
+ const result = [];
223
+ if (!fs.existsSync(dir))
224
+ return result;
225
+ try {
226
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
227
+ for (const entry of entries) {
228
+ const fullPath = path.join(dir, entry.name);
229
+ try {
230
+ // Use lstat for symlinks, stat (follows symlink) for mtime of real target
231
+ const lstat = fs.lstatSync(fullPath);
232
+ if (lstat.isSymbolicLink()) {
233
+ // Symlink: check both the symlink creation time and target mtime
234
+ if (lstat.mtimeMs >= afterMs) {
235
+ result.push(entry.name);
236
+ continue;
237
+ }
238
+ // Also check the resolved target's mtime
239
+ const realStat = fs.statSync(fullPath);
240
+ if (realStat.mtimeMs >= afterMs) {
241
+ result.push(entry.name);
242
+ }
243
+ }
244
+ else if (lstat.isDirectory()) {
245
+ if (lstat.mtimeMs >= afterMs) {
246
+ result.push(entry.name);
247
+ }
248
+ }
249
+ }
250
+ catch {
251
+ // skip broken symlinks etc.
252
+ }
253
+ }
254
+ }
255
+ catch {
256
+ // ignore
257
+ }
258
+ return result;
259
+ }
260
+ /**
261
+ * Copy a skill entry (directory or symlink target) to dest.
262
+ * Resolves symlinks and copies the real content so the copy is self-contained.
263
+ */
264
+ function copySkillToUser(src, dest) {
265
+ // Resolve symlink to get the real directory
266
+ let realSrc = src;
267
+ try {
268
+ const lstat = fs.lstatSync(src);
269
+ if (lstat.isSymbolicLink()) {
270
+ realSrc = fs.realpathSync(src);
271
+ }
272
+ }
273
+ catch {
274
+ // use src as-is
275
+ }
276
+ fs.cpSync(realSrc, dest, { recursive: true });
277
+ }
278
+ const SEARCH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
279
+ const SEARCH_CACHE_MAX = 100;
280
+ const searchCache = new Map();
281
+ function getCachedSearch(key) {
282
+ const entry = searchCache.get(key);
283
+ if (!entry)
284
+ return null;
285
+ if (Date.now() > entry.expiresAt) {
286
+ searchCache.delete(key);
287
+ return null;
288
+ }
289
+ return entry.value;
290
+ }
291
+ function setCachedSearch(key, value) {
292
+ // Evict oldest if at capacity
293
+ if (searchCache.size >= SEARCH_CACHE_MAX) {
294
+ const oldest = searchCache.keys().next().value;
295
+ if (oldest !== undefined)
296
+ searchCache.delete(oldest);
297
+ }
298
+ searchCache.set(key, { value, expiresAt: Date.now() + SEARCH_CACHE_TTL });
299
+ }
300
+ /**
301
+ * Search skills via skills.sh API.
302
+ * Returns structured results with install counts.
303
+ */
304
+ async function searchSkillsApi(query) {
305
+ const cached = getCachedSearch(query);
306
+ if (cached)
307
+ return cached;
308
+ try {
309
+ const resp = await fetch(`https://skills.sh/api/search?q=${encodeURIComponent(query)}&limit=20`, { signal: AbortSignal.timeout(10_000) });
310
+ if (!resp.ok)
311
+ throw new Error(`skills.sh returned ${resp.status}`);
312
+ const data = (await resp.json());
313
+ const results = (data.skills || []).map((s) => ({
314
+ package: s.source === s.skillId || !s.skillId
315
+ ? s.source
316
+ : `${s.source}@${s.skillId}`,
317
+ url: `https://skills.sh/s/${s.id}`,
318
+ description: '',
319
+ installs: s.installs,
320
+ skillId: s.skillId,
321
+ source: s.source,
322
+ }));
323
+ setCachedSearch(query, results);
324
+ return results;
325
+ }
326
+ catch {
327
+ // Fallback to npx skills find
328
+ return searchSkillsFallback(query);
329
+ }
330
+ }
331
+ /**
332
+ * Fallback search using npx skills find CLI.
333
+ */
334
+ async function searchSkillsFallback(query) {
335
+ try {
336
+ const { stdout } = await execFileAsync('npx', ['-y', 'skills', 'find', query], { timeout: 30_000 });
337
+ return parseSearchOutput(stdout);
338
+ }
339
+ catch (error) {
340
+ if (error && typeof error === 'object' && 'stdout' in error) {
341
+ const results = parseSearchOutput(error.stdout || '');
342
+ if (results.length > 0)
343
+ return results;
344
+ }
345
+ return [];
346
+ }
347
+ }
348
+ /**
349
+ * Fetch SKILL.md content from GitHub for a given source repo and skill ID.
350
+ * Tries multiple common directory layouts.
351
+ */
352
+ async function fetchSkillMdFromGitHub(source, skillId) {
353
+ // Try common paths where SKILL.md might live
354
+ const pathCandidates = [
355
+ `skills/${skillId}/SKILL.md`,
356
+ `${skillId}/SKILL.md`,
357
+ `.claude/skills/${skillId}/SKILL.md`,
358
+ `SKILL.md`,
359
+ ];
360
+ for (const branch of ['main', 'master']) {
361
+ for (const filePath of pathCandidates) {
362
+ try {
363
+ const url = `https://raw.githubusercontent.com/${source}/${branch}/${filePath}`;
364
+ const resp = await fetch(url, { signal: AbortSignal.timeout(8_000) });
365
+ if (!resp.ok)
366
+ continue;
367
+ const content = await resp.text();
368
+ // Verify it looks like a SKILL.md (has frontmatter)
369
+ if (!content.startsWith('---'))
370
+ continue;
371
+ const frontmatter = parseFrontmatter(content);
372
+ return {
373
+ content,
374
+ description: frontmatter.description || '',
375
+ skillName: frontmatter.name || skillId,
376
+ };
377
+ }
378
+ catch {
379
+ continue;
380
+ }
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+ async function withSkillInstallLock(fn) {
386
+ const previous = skillInstallLock.catch(() => undefined);
387
+ let release = () => undefined;
388
+ const current = new Promise((resolve) => {
389
+ release = resolve;
390
+ });
391
+ skillInstallLock = previous.then(() => current);
392
+ await previous;
393
+ try {
394
+ return await fn();
395
+ }
396
+ finally {
397
+ release();
398
+ }
399
+ }
400
+ // --- Routes ---
401
+ skillsRoutes.get('/', authMiddleware, (c) => {
402
+ const authUser = c.get('user');
403
+ const skills = discoverSkills(authUser.id);
404
+ return c.json({ skills });
405
+ });
406
+ skillsRoutes.get('/search', authMiddleware, async (c) => {
407
+ const query = c.req.query('q')?.trim();
408
+ if (!query) {
409
+ return c.json({ results: [] });
410
+ }
411
+ const results = await searchSkillsApi(query);
412
+ return c.json({ results });
413
+ });
414
+ skillsRoutes.get('/search/detail', authMiddleware, async (c) => {
415
+ const source = c.req.query('source')?.trim();
416
+ const skillId = c.req.query('skillId')?.trim();
417
+ // Support legacy url-based lookup for backwards compat
418
+ const url = c.req.query('url')?.trim();
419
+ if (source && skillId) {
420
+ // New path: fetch SKILL.md from GitHub using source/skillId
421
+ const result = await fetchSkillMdFromGitHub(source, skillId);
422
+ if (!result) {
423
+ return c.json({ detail: null });
424
+ }
425
+ return c.json({
426
+ detail: {
427
+ description: result.description,
428
+ skillName: result.skillName,
429
+ readme: result.content,
430
+ installs: '',
431
+ age: '',
432
+ features: [],
433
+ },
434
+ });
435
+ }
436
+ // Legacy: extract source/skillId from skills.sh URL
437
+ if (url) {
438
+ try {
439
+ const parsed = new URL(url);
440
+ if (parsed.hostname === 'skills.sh') {
441
+ // URL pattern: https://skills.sh/s/{owner}/{repo}/{skillId}
442
+ const segments = parsed.pathname
443
+ .replace(/^\/s\//, '')
444
+ .split('/')
445
+ .filter(Boolean);
446
+ if (segments.length >= 3) {
447
+ const srcFromUrl = `${segments[0]}/${segments[1]}`;
448
+ const skillIdFromUrl = segments[2];
449
+ const result = await fetchSkillMdFromGitHub(srcFromUrl, skillIdFromUrl);
450
+ if (result) {
451
+ return c.json({
452
+ detail: {
453
+ description: result.description,
454
+ skillName: result.skillName,
455
+ readme: result.content,
456
+ installs: '',
457
+ age: '',
458
+ features: [],
459
+ },
460
+ });
461
+ }
462
+ }
463
+ }
464
+ }
465
+ catch {
466
+ // fall through
467
+ }
468
+ }
469
+ return c.json({ detail: null });
470
+ });
471
+ // Get sync status (last sync time + auto-sync config)
472
+ skillsRoutes.get('/sync-status', authMiddleware, (c) => {
473
+ const authUser = c.get('user');
474
+ const manifest = readHostSyncManifest(authUser.id);
475
+ const settings = getSystemSettings();
476
+ return c.json({
477
+ lastSyncAt: manifest.lastSyncAt || null,
478
+ syncedCount: manifest.syncedSkills.length,
479
+ autoSyncEnabled: settings.skillAutoSyncEnabled,
480
+ autoSyncIntervalMinutes: settings.skillAutoSyncIntervalMinutes,
481
+ });
482
+ });
483
+ // Toggle auto-sync on/off (admin only)
484
+ skillsRoutes.put('/sync-settings', authMiddleware, async (c) => {
485
+ const authUser = c.get('user');
486
+ if (authUser.role !== 'admin') {
487
+ return c.json({ error: 'Only admin can change sync settings' }, 403);
488
+ }
489
+ const body = await c.req.json().catch(() => ({}));
490
+ const updates = {};
491
+ if (typeof body.autoSyncEnabled === 'boolean') {
492
+ updates.skillAutoSyncEnabled = body.autoSyncEnabled;
493
+ }
494
+ if (typeof body.autoSyncIntervalMinutes === 'number' &&
495
+ body.autoSyncIntervalMinutes >= 1) {
496
+ updates.skillAutoSyncIntervalMinutes = body.autoSyncIntervalMinutes;
497
+ }
498
+ const saved = saveSystemSettings(updates);
499
+ return c.json({
500
+ autoSyncEnabled: saved.skillAutoSyncEnabled,
501
+ autoSyncIntervalMinutes: saved.skillAutoSyncIntervalMinutes,
502
+ });
503
+ });
504
+ skillsRoutes.get('/:id', authMiddleware, (c) => {
505
+ const id = c.req.param('id');
506
+ const authUser = c.get('user');
507
+ const skill = getSkillDetail(id, authUser.id);
508
+ if (!skill) {
509
+ return c.json({ error: 'Skill not found' }, 404);
510
+ }
511
+ return c.json({ skill });
512
+ });
513
+ // Toggle enable/disable for user-level skills via SKILL.md ↔ SKILL.md.disabled rename.
514
+ // Project-level skills are read-only.
515
+ skillsRoutes.patch('/:id', authMiddleware, async (c) => {
516
+ const id = c.req.param('id');
517
+ const authUser = c.get('user');
518
+ const { enabled } = await c.req.json();
519
+ if (!validateSkillId(id))
520
+ return c.json({ error: 'Invalid skill ID' }, 400);
521
+ const userDir = getUserSkillsDir(authUser.id);
522
+ const skillDir = path.join(userDir, id);
523
+ if (!fs.existsSync(skillDir)) {
524
+ return c.json({ error: 'Skill not found or is not a user-level skill' }, 404);
525
+ }
526
+ if (!validateSkillPath(userDir, skillDir)) {
527
+ return c.json({ error: 'Invalid skill path' }, 400);
528
+ }
529
+ const srcPath = path.join(skillDir, enabled ? 'SKILL.md.disabled' : 'SKILL.md');
530
+ const dstPath = path.join(skillDir, enabled ? 'SKILL.md' : 'SKILL.md.disabled');
531
+ if (!fs.existsSync(srcPath)) {
532
+ return c.json({ error: 'Skill not found or already in desired state' }, 404);
533
+ }
534
+ fs.renameSync(srcPath, dstPath);
535
+ return c.json({ success: true });
536
+ });
537
+ /**
538
+ * Delete a user-level skill by ID.
539
+ * Reusable by both the HTTP route and IPC handler.
540
+ */
541
+ function deleteSkillForUser(userId, skillId) {
542
+ if (!validateSkillId(skillId)) {
543
+ return { success: false, error: 'Invalid skill ID' };
544
+ }
545
+ const userDir = getUserSkillsDir(userId);
546
+ const skillDir = path.join(userDir, skillId);
547
+ if (!fs.existsSync(skillDir)) {
548
+ return {
549
+ success: false,
550
+ error: 'Skill not found or is a project-level skill',
551
+ };
552
+ }
553
+ if (!validateSkillPath(userDir, skillDir)) {
554
+ return { success: false, error: 'Invalid skill path' };
555
+ }
556
+ try {
557
+ fs.rmSync(skillDir, { recursive: true, force: true });
558
+ removeFromSkillsManifest(userId, skillId);
559
+ return { success: true };
560
+ }
561
+ catch (error) {
562
+ return {
563
+ success: false,
564
+ error: error instanceof Error ? error.message : 'Unknown error',
565
+ };
566
+ }
567
+ }
568
+ skillsRoutes.delete('/:id', authMiddleware, async (c) => {
569
+ const id = c.req.param('id');
570
+ const authUser = c.get('user');
571
+ const result = deleteSkillForUser(authUser.id, id);
572
+ if (!result.success) {
573
+ const status = result.error === 'Invalid skill ID' ||
574
+ result.error === 'Invalid skill path'
575
+ ? 400
576
+ : result.error?.includes('not found')
577
+ ? 404
578
+ : 500;
579
+ return c.json({ error: result.error }, status);
580
+ }
581
+ return c.json({ success: true });
582
+ });
583
+ /**
584
+ * Install a skill package for a specific user.
585
+ * Uses a temporary HOME directory to isolate `npx skills add --global` from
586
+ * the real ~/.claude/skills, eliminating race conditions across concurrent installs.
587
+ * Reusable by both the HTTP route and IPC handler.
588
+ */
589
+ async function installSkillForUser(userId, pkg) {
590
+ if (!/^[\w\-]+\/[\w\-.]+(?:[@#][\w\-.\/]+)?$/.test(pkg) &&
591
+ !/^https?:\/\//.test(pkg)) {
592
+ return { success: false, error: 'Invalid package name format' };
593
+ }
594
+ // Create an isolated temp directory to act as HOME so `--global` installs
595
+ // into tempHome/.claude/skills/ instead of the real ~/.claude/skills/.
596
+ // This avoids any race condition when multiple installs run concurrently.
597
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-install-'));
598
+ const tempSkillsDir = path.join(tempHome, '.claude', 'skills');
599
+ fs.mkdirSync(tempSkillsDir, { recursive: true });
600
+ try {
601
+ await execFileAsync('npx', ['-y', 'skills', 'add', pkg, '--global', '--yes', '-a', 'claude-code'], {
602
+ timeout: 60_000,
603
+ env: { ...process.env, HOME: tempHome },
604
+ });
605
+ // Discover all skill directories installed into the temp location
606
+ const installedEntries = [];
607
+ if (fs.existsSync(tempSkillsDir)) {
608
+ for (const entry of fs.readdirSync(tempSkillsDir, {
609
+ withFileTypes: true,
610
+ })) {
611
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
612
+ installedEntries.push(entry.name);
613
+ }
614
+ }
615
+ }
616
+ if (installedEntries.length === 0) {
617
+ return {
618
+ success: false,
619
+ error: 'No skills were installed — package may be invalid',
620
+ };
621
+ }
622
+ // Copy resolved skill content to per-user directory
623
+ const userDir = getUserSkillsDir(userId);
624
+ fs.mkdirSync(userDir, { recursive: true });
625
+ for (const name of installedEntries) {
626
+ const src = path.join(tempSkillsDir, name);
627
+ const dest = path.join(userDir, name);
628
+ if (fs.existsSync(dest)) {
629
+ fs.rmSync(dest, { recursive: true, force: true });
630
+ }
631
+ copySkillToUser(src, dest);
632
+ }
633
+ // Write manifest metadata
634
+ updateSkillsManifest(userId, pkg, installedEntries);
635
+ return { success: true, installed: installedEntries };
636
+ }
637
+ catch (error) {
638
+ return {
639
+ success: false,
640
+ error: error instanceof Error ? error.message : 'Unknown error',
641
+ };
642
+ }
643
+ finally {
644
+ // Always clean up the temp directory
645
+ try {
646
+ fs.rmSync(tempHome, { recursive: true, force: true });
647
+ }
648
+ catch {
649
+ /* ignore cleanup errors */
650
+ }
651
+ }
652
+ }
653
+ /**
654
+ * Sync host-level skills (~/.claude/skills/) to a user's directory.
655
+ * Standalone function usable from both the API route and the auto-sync timer.
656
+ */
657
+ async function syncHostSkillsForUser(userId) {
658
+ return withSkillInstallLock(async () => {
659
+ const hostDir = getGlobalSkillsDir();
660
+ const userDir = getUserSkillsDir(userId);
661
+ fs.mkdirSync(userDir, { recursive: true });
662
+ // 1. 扫描宿主机 skills
663
+ const hostSkillNames = [];
664
+ if (fs.existsSync(hostDir)) {
665
+ for (const entry of fs.readdirSync(hostDir, { withFileTypes: true })) {
666
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
667
+ continue;
668
+ const skillDir = path.join(hostDir, entry.name);
669
+ try {
670
+ const realPath = fs.realpathSync(skillDir);
671
+ if (fs.existsSync(path.join(realPath, 'SKILL.md')) ||
672
+ fs.existsSync(path.join(realPath, 'SKILL.md.disabled'))) {
673
+ hostSkillNames.push(entry.name);
674
+ }
675
+ }
676
+ catch {
677
+ // 跳过 broken symlinks
678
+ }
679
+ }
680
+ }
681
+ // 2. 读取 manifest
682
+ const manifest = readHostSyncManifest(userId);
683
+ const previouslySynced = new Set(manifest.syncedSkills);
684
+ // 3. 检测用户目录中手动安装的 skills
685
+ const existingUserSkills = new Set();
686
+ if (fs.existsSync(userDir)) {
687
+ for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
688
+ if (entry.isDirectory())
689
+ existingUserSkills.add(entry.name);
690
+ }
691
+ }
692
+ const stats = { added: 0, updated: 0, deleted: 0, skipped: 0 };
693
+ const newSyncedList = [];
694
+ // 4. 同步:新增/更新
695
+ for (const name of hostSkillNames) {
696
+ const isManuallyInstalled = existingUserSkills.has(name) && !previouslySynced.has(name);
697
+ if (isManuallyInstalled) {
698
+ stats.skipped++;
699
+ continue;
700
+ }
701
+ const src = path.join(hostDir, name);
702
+ const dest = path.join(userDir, name);
703
+ if (existingUserSkills.has(name)) {
704
+ fs.rmSync(dest, { recursive: true, force: true });
705
+ copySkillToUser(src, dest);
706
+ stats.updated++;
707
+ }
708
+ else {
709
+ copySkillToUser(src, dest);
710
+ stats.added++;
711
+ }
712
+ newSyncedList.push(name);
713
+ }
714
+ // 5. 删除宿主机已移除的(仅清理之前同步来的)
715
+ const hostSkillSet = new Set(hostSkillNames);
716
+ for (const name of previouslySynced) {
717
+ if (!hostSkillSet.has(name) && existingUserSkills.has(name)) {
718
+ const dest = path.join(userDir, name);
719
+ fs.rmSync(dest, { recursive: true, force: true });
720
+ stats.deleted++;
721
+ }
722
+ }
723
+ // 6. 更新 manifest
724
+ writeHostSyncManifest(userId, {
725
+ syncedSkills: newSyncedList,
726
+ lastSyncAt: new Date().toISOString(),
727
+ });
728
+ return { stats, total: hostSkillNames.length };
729
+ });
730
+ }
731
+ // Sync host-level skills — API endpoint (admin only).
732
+ skillsRoutes.post('/sync-host', authMiddleware, async (c) => {
733
+ const authUser = c.get('user');
734
+ if (authUser.role !== 'admin') {
735
+ return c.json({ error: 'Only admin can sync host skills' }, 403);
736
+ }
737
+ const result = await syncHostSkillsForUser(authUser.id);
738
+ return c.json(result);
739
+ });
740
+ skillsRoutes.post('/install', authMiddleware, async (c) => {
741
+ const authUser = c.get('user');
742
+ const body = await c.req.json().catch(() => ({}));
743
+ if (typeof body.package !== 'string') {
744
+ return c.json({ error: 'package field must be string' }, 400);
745
+ }
746
+ const pkg = body.package.trim();
747
+ const result = await installSkillForUser(authUser.id, pkg);
748
+ if (!result.success) {
749
+ return c.json({ error: 'Failed to install skill', details: result.error }, result.error === 'Invalid package name format' ? 400 : 500);
750
+ }
751
+ return c.json({ success: true, installed: result.installed });
752
+ });
753
+ // Reinstall a skill by its ID — requires the skill to have a packageName in the manifest.
754
+ skillsRoutes.post('/:id/reinstall', authMiddleware, async (c) => {
755
+ const id = c.req.param('id');
756
+ const authUser = c.get('user');
757
+ if (!validateSkillId(id)) {
758
+ return c.json({ error: 'Invalid skill ID' }, 400);
759
+ }
760
+ const manifest = readSkillsManifest(authUser.id);
761
+ const meta = manifest.skills[id];
762
+ if (!meta?.packageName) {
763
+ return c.json({ error: 'Skill has no package info — cannot reinstall' }, 400);
764
+ }
765
+ // Delete then reinstall
766
+ const deleteResult = deleteSkillForUser(authUser.id, id);
767
+ if (!deleteResult.success) {
768
+ return c.json({ error: 'Failed to delete old skill', details: deleteResult.error }, 500);
769
+ }
770
+ const installResult = await installSkillForUser(authUser.id, meta.packageName);
771
+ if (!installResult.success) {
772
+ return c.json({ error: 'Failed to reinstall skill', details: installResult.error }, 500);
773
+ }
774
+ return c.json({ success: true, installed: installResult.installed });
775
+ });
776
+ export { getUserSkillsDir, installSkillForUser, deleteSkillForUser, syncHostSkillsForUser, };
777
+ export default skillsRoutes;