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,210 @@
1
+ import { Hono } from 'hono';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { hasHostExecutionPermission } from '../web-context.js';
5
+ import { authMiddleware } from '../middleware/auth.js';
6
+ import { logger } from '../logger.js';
7
+ import { loadMountAllowlist, expandPath, findAllowedRoot, matchesBlockedPattern, } from '../mount-security.js';
8
+ const MAX_ENTRIES = 200;
9
+ const browseRoutes = new Hono();
10
+ /**
11
+ * List subdirectories of a given path, filtering hidden dirs and blocked patterns.
12
+ */
13
+ function listSubdirectories(dirPath, blockedPatterns) {
14
+ let entries;
15
+ try {
16
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ const dirs = [];
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory())
24
+ continue;
25
+ // Skip hidden directories
26
+ if (entry.name.startsWith('.'))
27
+ continue;
28
+ // Skip blocked patterns
29
+ const fullPath = path.join(dirPath, entry.name);
30
+ if (matchesBlockedPattern(fullPath, blockedPatterns) !== null)
31
+ continue;
32
+ // Check if has subdirectories (for expand indicator)
33
+ let hasChildren = false;
34
+ try {
35
+ const children = fs.readdirSync(fullPath, { withFileTypes: true });
36
+ hasChildren = children.some((c) => c.isDirectory() && !c.name.startsWith('.'));
37
+ }
38
+ catch {
39
+ // Permission denied or other error — treat as no children
40
+ }
41
+ dirs.push({ name: entry.name, path: fullPath, hasChildren });
42
+ if (dirs.length >= MAX_ENTRIES)
43
+ break;
44
+ }
45
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
46
+ return dirs;
47
+ }
48
+ // GET /api/browse/directories?path=xxx
49
+ browseRoutes.get('/directories', authMiddleware, (c) => {
50
+ const authUser = c.get('user');
51
+ if (!hasHostExecutionPermission(authUser)) {
52
+ return c.json({ error: 'Insufficient permissions' }, 403);
53
+ }
54
+ const requestedPath = c.req.query('path');
55
+ const allowlist = loadMountAllowlist();
56
+ const blockedPatterns = allowlist?.blockedPatterns ?? [];
57
+ const hasAllowlist = allowlist !== null && allowlist.allowedRoots.length > 0;
58
+ // No path → return root listing
59
+ if (!requestedPath) {
60
+ if (hasAllowlist) {
61
+ // Return allowlist roots as top-level entries
62
+ const roots = [];
63
+ for (const root of allowlist.allowedRoots) {
64
+ const expanded = expandPath(root.path);
65
+ let realPath;
66
+ try {
67
+ realPath = fs.realpathSync(expanded);
68
+ }
69
+ catch {
70
+ continue; // Root doesn't exist, skip
71
+ }
72
+ if (!fs.existsSync(realPath) || !fs.statSync(realPath).isDirectory())
73
+ continue;
74
+ let hasChildren = false;
75
+ try {
76
+ const children = fs.readdirSync(realPath, { withFileTypes: true });
77
+ hasChildren = children.some((ch) => ch.isDirectory() && !ch.name.startsWith('.'));
78
+ }
79
+ catch {
80
+ /* ignore */
81
+ }
82
+ roots.push({
83
+ name: root.description || path.basename(realPath),
84
+ path: realPath,
85
+ hasChildren,
86
+ });
87
+ }
88
+ return c.json({
89
+ currentPath: null,
90
+ parentPath: null,
91
+ directories: roots,
92
+ hasAllowlist: true,
93
+ });
94
+ }
95
+ // No allowlist → return HOME directory
96
+ const homeDir = process.env.HOME || '/';
97
+ return c.json({
98
+ currentPath: homeDir,
99
+ parentPath: homeDir === '/' ? null : path.dirname(homeDir),
100
+ directories: listSubdirectories(homeDir, blockedPatterns),
101
+ hasAllowlist: false,
102
+ });
103
+ }
104
+ // Validate path
105
+ if (!path.isAbsolute(requestedPath)) {
106
+ return c.json({ error: 'Path must be absolute' }, 400);
107
+ }
108
+ let realPath;
109
+ try {
110
+ realPath = fs.realpathSync(requestedPath);
111
+ }
112
+ catch {
113
+ return c.json({ error: 'Path does not exist' }, 400);
114
+ }
115
+ if (!fs.statSync(realPath).isDirectory()) {
116
+ return c.json({ error: 'Path is not a directory' }, 400);
117
+ }
118
+ // Allowlist range check
119
+ if (hasAllowlist) {
120
+ const root = findAllowedRoot(realPath, allowlist.allowedRoots);
121
+ if (!root) {
122
+ return c.json({ error: 'Path is not within allowed roots' }, 403);
123
+ }
124
+ }
125
+ // Compute parentPath
126
+ let parentPath = path.dirname(realPath);
127
+ if (parentPath === realPath) {
128
+ // At filesystem root
129
+ parentPath = null;
130
+ }
131
+ else if (hasAllowlist) {
132
+ // Check if parent is still within an allowed root
133
+ const parentRoot = findAllowedRoot(parentPath, allowlist.allowedRoots);
134
+ if (!parentRoot) {
135
+ // Parent is outside allowed roots — return null to go back to root listing
136
+ parentPath = null;
137
+ }
138
+ }
139
+ return c.json({
140
+ currentPath: realPath,
141
+ parentPath,
142
+ directories: listSubdirectories(realPath, blockedPatterns),
143
+ hasAllowlist,
144
+ });
145
+ });
146
+ // POST /api/browse/directories — create a new folder
147
+ browseRoutes.post('/directories', authMiddleware, async (c) => {
148
+ const authUser = c.get('user');
149
+ if (!hasHostExecutionPermission(authUser)) {
150
+ return c.json({ error: 'Insufficient permissions' }, 403);
151
+ }
152
+ const body = await c.req.json().catch(() => ({}));
153
+ const { parentPath, name } = body;
154
+ if (!parentPath || typeof parentPath !== 'string') {
155
+ return c.json({ error: 'parentPath is required' }, 400);
156
+ }
157
+ if (!name || typeof name !== 'string') {
158
+ return c.json({ error: 'name is required' }, 400);
159
+ }
160
+ // Validate name
161
+ if (name.includes('/') || name.includes('..') || name.startsWith('.')) {
162
+ return c.json({ error: 'Invalid folder name: must not contain /, .., or start with .' }, 400);
163
+ }
164
+ if (!path.isAbsolute(parentPath)) {
165
+ return c.json({ error: 'parentPath must be absolute' }, 400);
166
+ }
167
+ let realParent;
168
+ try {
169
+ realParent = fs.realpathSync(parentPath);
170
+ }
171
+ catch {
172
+ return c.json({ error: 'Parent path does not exist' }, 400);
173
+ }
174
+ if (!fs.statSync(realParent).isDirectory()) {
175
+ return c.json({ error: 'Parent path is not a directory' }, 400);
176
+ }
177
+ // Allowlist range check
178
+ const allowlist = loadMountAllowlist();
179
+ const hasAllowlist = allowlist !== null && allowlist.allowedRoots.length > 0;
180
+ if (hasAllowlist) {
181
+ const root = findAllowedRoot(realParent, allowlist.allowedRoots);
182
+ if (!root) {
183
+ return c.json({ error: 'Parent path is not within allowed roots' }, 403);
184
+ }
185
+ }
186
+ // Blocked patterns check
187
+ const blockedPatterns = allowlist?.blockedPatterns ?? [];
188
+ const targetPath = path.join(realParent, name);
189
+ if (matchesBlockedPattern(targetPath, blockedPatterns) !== null) {
190
+ return c.json({ error: 'Folder name matches a blocked pattern' }, 400);
191
+ }
192
+ // Check if already exists
193
+ if (fs.existsSync(targetPath)) {
194
+ return c.json({ error: 'Directory already exists' }, 400);
195
+ }
196
+ try {
197
+ fs.mkdirSync(targetPath, { recursive: false });
198
+ logger.info({ path: targetPath }, 'Directory created via browse API');
199
+ }
200
+ catch (err) {
201
+ logger.error({ err, path: targetPath }, 'Failed to create directory');
202
+ return c.json({ error: 'Failed to create directory' }, 500);
203
+ }
204
+ return c.json({
205
+ name,
206
+ path: targetPath,
207
+ hasChildren: false,
208
+ });
209
+ });
210
+ export default browseRoutes;
@@ -0,0 +1,387 @@
1
+ import { execFile } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { promisify } from 'util';
6
+ import { Hono } from 'hono';
7
+ import { APP_ROOT, resolveAppPath } from '../app-root.js';
8
+ import { DATA_DIR } from '../config.js';
9
+ import { getUserHomeGroup } from '../db.js';
10
+ import { getClaudeProviderConfig } from '../runtime-config.js';
11
+ import { sdkQuery } from '../sdk-query.js';
12
+ import { logger } from '../logger.js';
13
+ import { authMiddleware } from '../middleware/auth.js';
14
+ import { BugReportGenerateSchema, BugReportSubmitSchema } from '../schemas.js';
15
+ import { getWebDeps } from '../web-context.js';
16
+ const execFileAsync = promisify(execFile);
17
+ const bugReportRoutes = new Hono();
18
+ // --- Rate limiting (60s per user) ---
19
+ const cooldowns = new Map();
20
+ const COOLDOWN_MS = 60_000;
21
+ const generateCooldowns = new Map();
22
+ const GENERATE_COOLDOWN_MS = 30_000;
23
+ function checkCooldown(userId, map = cooldowns, cooldownMs = COOLDOWN_MS) {
24
+ const last = map.get(userId);
25
+ if (last) {
26
+ const remaining = cooldownMs - (Date.now() - last);
27
+ if (remaining > 0) {
28
+ return `请等待 ${Math.ceil(remaining / 1000)} 秒后再试`;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ // --- Capability detection (cached 5min) ---
34
+ let capCache = null;
35
+ const CAP_CACHE_TTL = 5 * 60 * 1000;
36
+ async function checkCapabilities() {
37
+ if (capCache && Date.now() - capCache.checkedAt < CAP_CACHE_TTL) {
38
+ return {
39
+ ghAvailable: capCache.ghAvailable,
40
+ ghUsername: capCache.ghUsername,
41
+ claudeAvailable: capCache.claudeAvailable,
42
+ };
43
+ }
44
+ const [gh] = await Promise.all([
45
+ execFileAsync('gh', ['auth', 'status'], { timeout: 5000 })
46
+ .then(() => true)
47
+ .catch(() => false),
48
+ ]);
49
+ // Claude availability is determined by provider config, not CLI presence
50
+ const providerConfig = getClaudeProviderConfig();
51
+ const claude = !!(providerConfig.anthropicApiKey ||
52
+ providerConfig.claudeCodeOauthToken ||
53
+ providerConfig.claudeOAuthCredentials);
54
+ // Get gh username if available
55
+ let ghUsername = null;
56
+ if (gh) {
57
+ try {
58
+ const { stdout } = await execFileAsync('gh', ['api', 'user', '--jq', '.login'], { timeout: 5000 });
59
+ ghUsername = stdout.trim() || null;
60
+ }
61
+ catch {
62
+ // gh available but can't get username
63
+ }
64
+ }
65
+ capCache = {
66
+ ghAvailable: gh,
67
+ ghUsername,
68
+ claudeAvailable: claude,
69
+ checkedAt: Date.now(),
70
+ };
71
+ return { ghAvailable: gh, ghUsername, claudeAvailable: claude };
72
+ }
73
+ // --- Helpers ---
74
+ function getVersion() {
75
+ try {
76
+ const pkg = JSON.parse(fs.readFileSync(resolveAppPath('package.json'), 'utf-8'));
77
+ return pkg.version || 'unknown';
78
+ }
79
+ catch {
80
+ return 'unknown';
81
+ }
82
+ }
83
+ function readRecentLogs(folder, maxLines = 50) {
84
+ const logsDir = path.join(DATA_DIR, 'groups', folder, 'logs');
85
+ try {
86
+ if (!fs.existsSync(logsDir))
87
+ return '(no logs directory)';
88
+ const files = fs
89
+ .readdirSync(logsDir)
90
+ .filter((f) => f.endsWith('.log'))
91
+ .sort()
92
+ .reverse();
93
+ if (files.length === 0)
94
+ return '(no log files)';
95
+ const latestFile = path.join(logsDir, files[0]);
96
+ const content = fs.readFileSync(latestFile, 'utf-8');
97
+ const lines = content.split('\n');
98
+ return lines.slice(-maxLines).join('\n');
99
+ }
100
+ catch {
101
+ return '(failed to read logs)';
102
+ }
103
+ }
104
+ /** Mask environment variable values in log text */
105
+ function sanitizeLogs(text) {
106
+ let result = text;
107
+ // Replace absolute paths to project root and home directory with placeholders
108
+ const projectRoot = APP_ROOT;
109
+ const homeDir = os.homedir();
110
+ // Replace longer path first to avoid partial replacement
111
+ if (projectRoot.startsWith(homeDir)) {
112
+ result = result.replaceAll(projectRoot, '<project>');
113
+ result = result.replaceAll(homeDir, '<home>');
114
+ }
115
+ else {
116
+ result = result.replaceAll(homeDir, '<home>');
117
+ result = result.replaceAll(projectRoot, '<project>');
118
+ }
119
+ // Generic pattern matching any env var name containing sensitive keywords
120
+ const sensitivePattern = /(\b\w*(?:token|password|passwd|secret|api[_-]?key|auth[_-]?token|authorization|cookie|credential|private[_-]?key|access[_-]?key|app[_-]?secret)\w*)[=:]\s*\S+/gi;
121
+ result = result.replace(sensitivePattern, '$1=***');
122
+ return result;
123
+ }
124
+ function buildGeneratePrompt(description, systemInfo, logs, screenshotCount) {
125
+ const sysInfoText = Object.entries(systemInfo)
126
+ .map(([k, v]) => `- ${k}: ${v}`)
127
+ .join('\n');
128
+ const screenshotNote = screenshotCount > 0
129
+ ? `\n\n## 附加截图\n用户附加了 ${screenshotCount} 张截图(截图内容无法在此展示)。请在 Issue 正文末尾添加提示:「报告者附加了 ${screenshotCount} 张截图,如需查看请联系报告者。」`
130
+ : '';
131
+ return `你是一个 Bug 报告助手,帮用户将 bug 描述整理为结构化的 GitHub Issue。
132
+
133
+ ## 用户描述
134
+ ${description}${screenshotNote}
135
+
136
+ ## 系统信息
137
+ ${sysInfoText}
138
+
139
+ ## 最近日志(最后 50 行)
140
+ \`\`\`
141
+ ${logs}
142
+ \`\`\`
143
+
144
+ 请生成一个结构化的 GitHub Issue。输出**纯 JSON**(不要 markdown 代码块),包含两个字段:
145
+ - "title": 简洁的 issue 标题,格式为 "bug: 简要描述"(小写 bug: 前缀,不超过 80 字符)
146
+ - "body": 结构化的 Markdown 正文,严格按照以下模板:
147
+
148
+ ## 用户现象
149
+ (从用户视角描述看到了什么、体验上有什么异常)
150
+
151
+ ## 问题描述
152
+ (从技术视角简要说明发生了什么)
153
+
154
+ ## 复现路径
155
+ 1. 步骤一
156
+ 2. 步骤二
157
+ 3. 期望行为 vs 实际行为
158
+ (如果能从描述和日志推断出复现步骤就写,无法推断则省略此章节)
159
+
160
+ ## 根因(可选)
161
+ (如果能从日志分析出代码层面的原因就写,否则省略)
162
+
163
+ ## 影响
164
+ (对用户体验/数据/安全的影响)
165
+
166
+ ## 环境信息
167
+ (系统信息表格)
168
+
169
+ ## 相关日志
170
+ (如有错误日志,摘录关键部分)
171
+
172
+ 只输出 JSON,不要其他内容。`;
173
+ }
174
+ function buildFallbackReport(description, systemInfo, logs) {
175
+ const sysInfoTable = Object.entries(systemInfo)
176
+ .map(([k, v]) => `| ${k} | ${v} |`)
177
+ .join('\n');
178
+ const body = `## 用户现象
179
+
180
+ ${description}
181
+
182
+ ## 问题描述
183
+
184
+ (待补充技术分析)
185
+
186
+ ## 影响
187
+
188
+ (待补充)
189
+
190
+ ## 环境信息
191
+
192
+ | 项目 | 值 |
193
+ |------|-----|
194
+ ${sysInfoTable}
195
+
196
+ ## 相关日志
197
+
198
+ \`\`\`
199
+ ${logs.slice(0, 3000)}
200
+ \`\`\`
201
+ `;
202
+ return {
203
+ title: `bug: ${description.slice(0, 70)}`,
204
+ body,
205
+ };
206
+ }
207
+ /** Try multiple strategies to extract JSON { title, body } from Claude output */
208
+ function tryParseJsonOutput(raw) {
209
+ const candidates = [];
210
+ // Strategy 1: strip markdown fencing (greedy to handle nested backticks)
211
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```\s*$/);
212
+ if (fenceMatch)
213
+ candidates.push(fenceMatch[1].trim());
214
+ // Strategy 2: extract first { ... } block
215
+ const braceMatch = raw.match(/\{[\s\S]*\}/);
216
+ if (braceMatch)
217
+ candidates.push(braceMatch[0]);
218
+ // Strategy 3: raw string as-is
219
+ candidates.push(raw.trim());
220
+ for (const candidate of candidates) {
221
+ try {
222
+ const parsed = JSON.parse(candidate);
223
+ if (typeof parsed === 'object' && parsed !== null && parsed.body) {
224
+ return parsed;
225
+ }
226
+ }
227
+ catch {
228
+ // try next candidate
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+ // ========== Routes ==========
234
+ /**
235
+ * GET /api/bug-report/capabilities
236
+ * Check what tools are available for bug reporting
237
+ */
238
+ bugReportRoutes.get('/capabilities', authMiddleware, async (c) => {
239
+ const caps = await checkCapabilities();
240
+ return c.json(caps);
241
+ });
242
+ /**
243
+ * POST /api/bug-report/generate
244
+ * Analyze the bug with Claude and generate a structured report
245
+ */
246
+ bugReportRoutes.post('/generate', authMiddleware, async (c) => {
247
+ const user = c.get('user');
248
+ // Rate limiting — 30s cooldown per user for generate
249
+ const generateCooldownMsg = checkCooldown(user.id, generateCooldowns, GENERATE_COOLDOWN_MS);
250
+ if (generateCooldownMsg) {
251
+ return c.json({ error: generateCooldownMsg }, 429);
252
+ }
253
+ const parseResult = BugReportGenerateSchema.safeParse(await c.req.json());
254
+ if (!parseResult.success) {
255
+ return c.json({ error: 'Invalid request', details: parseResult.error.issues }, 400);
256
+ }
257
+ const { description, screenshots } = parseResult.data;
258
+ // Set cooldown immediately to prevent concurrent requests
259
+ generateCooldowns.set(user.id, Date.now());
260
+ // Collect system info
261
+ const homeGroup = getUserHomeGroup(user.id);
262
+ const folder = homeGroup?.folder || 'main';
263
+ const deps = getWebDeps();
264
+ const queueStatus = deps?.queue.getStatus();
265
+ const systemInfo = {
266
+ 'cli-claw版本': getVersion(),
267
+ 'Node.js': process.version,
268
+ 操作系统: `${os.platform()} ${os.release()}`,
269
+ 架构: os.arch(),
270
+ 活跃容器数: String(queueStatus?.activeContainerCount ?? 'N/A'),
271
+ 活跃宿主机进程: String(queueStatus?.activeHostProcessCount ?? 'N/A'),
272
+ 等待队列: String(queueStatus?.waitingCount ?? 'N/A'),
273
+ 截图数量: String(screenshots?.length || 0),
274
+ };
275
+ // Read recent logs
276
+ const rawLogs = readRecentLogs(folder);
277
+ const logs = sanitizeLogs(rawLogs);
278
+ // Try Claude analysis
279
+ const caps = await checkCapabilities();
280
+ if (!caps.claudeAvailable) {
281
+ logger.info('bug-report: claude CLI not available, using fallback template');
282
+ const fallback = buildFallbackReport(description, systemInfo, logs);
283
+ return c.json({ ...fallback, systemInfo });
284
+ }
285
+ const prompt = buildGeneratePrompt(description, systemInfo, logs, screenshots?.length || 0);
286
+ try {
287
+ logger.info({ promptLen: prompt.length, userId: user.id }, 'bug-report: invoking Claude SDK');
288
+ const model = process.env.RECALL_MODEL || undefined;
289
+ const result = await sdkQuery(prompt, { model, timeout: 60_000 });
290
+ if (!result) {
291
+ const fallback = buildFallbackReport(description, systemInfo, logs);
292
+ return c.json({ ...fallback, systemInfo });
293
+ }
294
+ // Try to parse Claude's JSON output
295
+ const parsed = tryParseJsonOutput(result);
296
+ if (parsed?.body) {
297
+ return c.json({
298
+ title: parsed.title || `Bug: ${description.slice(0, 70)}`,
299
+ body: parsed.body,
300
+ systemInfo,
301
+ });
302
+ }
303
+ // Claude didn't return valid JSON, use raw output as body
304
+ logger.info('bug-report: claude output was not valid JSON, using as raw body');
305
+ return c.json({
306
+ title: `Bug: ${description.slice(0, 70)}`,
307
+ body: result,
308
+ systemInfo,
309
+ });
310
+ }
311
+ catch (err) {
312
+ logger.error({ error: err.message }, 'bug-report: unexpected error during generation');
313
+ const fallback = buildFallbackReport(description, systemInfo, logs);
314
+ return c.json({ ...fallback, systemInfo });
315
+ }
316
+ });
317
+ /**
318
+ * POST /api/bug-report/submit
319
+ * Create a GitHub issue or return a pre-filled URL
320
+ */
321
+ bugReportRoutes.post('/submit', authMiddleware, async (c) => {
322
+ const user = c.get('user');
323
+ // Rate limiting — only on actual issue submission
324
+ const cooldownMsg = checkCooldown(user.id);
325
+ if (cooldownMsg) {
326
+ return c.json({ error: cooldownMsg }, 429);
327
+ }
328
+ const parseResult = BugReportSubmitSchema.safeParse(await c.req.json());
329
+ if (!parseResult.success) {
330
+ return c.json({ error: 'Invalid request', details: parseResult.error.issues }, 400);
331
+ }
332
+ const { title, body } = parseResult.data;
333
+ // Append submitter info
334
+ const fullBody = `${body}\n\n---\n> Submitted via cli-claw by ${user.display_name || user.username}`;
335
+ // Try gh CLI first
336
+ const caps = await checkCapabilities();
337
+ if (caps.ghAvailable) {
338
+ try {
339
+ logger.info({ userId: user.id }, 'bug-report: attempting gh issue create');
340
+ const result = await new Promise((resolve, reject) => {
341
+ const child = execFile('gh', [
342
+ 'issue',
343
+ 'create',
344
+ '--repo',
345
+ 'RyanProMax/cli-claw',
346
+ '--title',
347
+ title,
348
+ '--body-file',
349
+ '-',
350
+ ], { timeout: 30000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
351
+ if (err) {
352
+ logger.warn({
353
+ message: err.message?.slice(0, 200),
354
+ stderr: stderr?.slice(0, 300),
355
+ }, 'bug-report: gh issue create failed');
356
+ reject(err);
357
+ return;
358
+ }
359
+ resolve(stdout.trim());
360
+ });
361
+ child.stdin?.write(fullBody);
362
+ child.stdin?.end();
363
+ });
364
+ // gh outputs the issue URL on success
365
+ const urlMatch = result.match(/https:\/\/github\.com\/[^\s]+\/issues\/\d+/);
366
+ const url = urlMatch ? urlMatch[0] : result;
367
+ logger.info({ url, userId: user.id }, 'bug-report: issue created via gh');
368
+ cooldowns.set(user.id, Date.now());
369
+ return c.json({ method: 'created', url });
370
+ }
371
+ catch {
372
+ // Fall through to manual URL
373
+ logger.info('bug-report: gh failed, falling back to manual URL');
374
+ }
375
+ }
376
+ // Fallback: pre-filled GitHub URL
377
+ const maxBodyLen = 6000; // conservative limit for URL length
378
+ const truncatedBody = fullBody.length > maxBodyLen
379
+ ? fullBody.slice(0, maxBodyLen) +
380
+ '\n\n...(内容过长已截断,请补充完整信息)'
381
+ : fullBody;
382
+ const url = `https://github.com/RyanProMax/cli-claw/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(truncatedBody)}`;
383
+ logger.info({ userId: user.id }, 'bug-report: returning pre-filled URL');
384
+ cooldowns.set(user.id, Date.now());
385
+ return c.json({ method: 'manual', url });
386
+ });
387
+ export default bugReportRoutes;