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,1295 @@
1
+ /**
2
+ * MCP Tool Definitions for cli-claw Agent Runner.
3
+ *
4
+ * Uses SDK's `tool()` helper to define in-process MCP tools.
5
+ * These tools communicate with the host process via IPC files.
6
+ *
7
+ * Context (chatJid, groupFolder, etc.) is passed via McpContext
8
+ * rather than read from environment variables, enabling in-process usage.
9
+ */
10
+
11
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
12
+ import type { SdkMcpToolDefinition } from '@anthropic-ai/claude-agent-sdk';
13
+ import { z } from 'zod';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { CronExpressionParser } from 'cron-parser';
17
+
18
+ /** Context required by MCP tools. Passed at construction time. */
19
+ export interface McpContext {
20
+ chatJid: string;
21
+ groupFolder: string;
22
+ isHome: boolean;
23
+ isAdminHome: boolean;
24
+ isScheduledTask?: boolean;
25
+ workspaceIpc: string;
26
+ workspaceGroup: string;
27
+ workspaceGlobal: string;
28
+ workspaceMemory: string;
29
+ }
30
+
31
+ function writeIpcFile(dir: string, data: object): string {
32
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
33
+ const filepath = path.join(dir, filename);
34
+ const tempPath = `${filepath}.tmp`;
35
+ try {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ // Atomic write: temp file then rename
38
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2));
39
+ fs.renameSync(tempPath, filepath);
40
+ } catch (err) {
41
+ // Clean up temp file on failure
42
+ try {
43
+ fs.unlinkSync(tempPath);
44
+ } catch {
45
+ /* ignore */
46
+ }
47
+ throw new Error(
48
+ `IPC 写入失败 (${dir}): ${err instanceof Error ? err.message : String(err)}`,
49
+ );
50
+ }
51
+ return filename;
52
+ }
53
+
54
+ /**
55
+ * Send an IPC request and poll for the result file.
56
+ * Fixes TOCTOU by directly attempting readFileSync and catching ENOENT.
57
+ * Returns the parsed JSON result, or throws on timeout.
58
+ */
59
+ async function pollIpcResult(
60
+ dir: string,
61
+ data: Record<string, unknown> & { requestId: string },
62
+ resultFilePrefix: string,
63
+ timeoutMs: number = 30_000,
64
+ ): Promise<Record<string, unknown>> {
65
+ const resultFileName = `${resultFilePrefix}_${data.requestId}.json`;
66
+ const resultFilePath = path.join(dir, resultFileName);
67
+
68
+ writeIpcFile(dir, data);
69
+
70
+ const pollInterval = 500;
71
+ const deadline = Date.now() + timeoutMs;
72
+
73
+ while (Date.now() < deadline) {
74
+ try {
75
+ const raw = fs.readFileSync(resultFilePath, 'utf-8');
76
+ fs.unlinkSync(resultFilePath);
77
+ return JSON.parse(raw) as Record<string, unknown>;
78
+ } catch (err) {
79
+ // File not ready yet — only swallow ENOENT
80
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
81
+ }
82
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
83
+ }
84
+ throw new Error(`Timeout waiting for IPC result (${timeoutMs / 1000}s)`);
85
+ }
86
+
87
+ // --- Memory helpers ---
88
+ const MEMORY_EXTENSIONS = new Set(['.md', '.txt']);
89
+ const MEMORY_SUBDIRS = new Set(['memory', 'conversations']);
90
+ const MEMORY_SKIP_DIRS = new Set(['logs', '.claude', 'node_modules', '.git']);
91
+ const MAX_MEMORY_FILE_SIZE = 512 * 1024; // 512KB per file
92
+ const MAX_MEMORY_APPEND_SIZE = 16 * 1024; // 16KB per append
93
+ const MEMORY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
94
+
95
+ function collectMemoryFiles(
96
+ baseDir: string,
97
+ out: string[],
98
+ maxDepth: number,
99
+ depth = 0,
100
+ ): void {
101
+ if (depth > maxDepth || !fs.existsSync(baseDir)) return;
102
+ try {
103
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
104
+ for (const entry of entries) {
105
+ const fullPath = path.join(baseDir, entry.name);
106
+ if (entry.isDirectory()) {
107
+ if (MEMORY_SKIP_DIRS.has(entry.name)) continue;
108
+ if (depth === 0 || MEMORY_SUBDIRS.has(entry.name)) {
109
+ collectMemoryFiles(fullPath, out, maxDepth, depth + 1);
110
+ }
111
+ } else if (entry.isFile()) {
112
+ if (
113
+ entry.name === 'AGENTS.md' ||
114
+ MEMORY_EXTENSIONS.has(path.extname(entry.name))
115
+ ) {
116
+ out.push(fullPath);
117
+ }
118
+ }
119
+ }
120
+ } catch {
121
+ /* skip unreadable */
122
+ }
123
+ }
124
+
125
+ function createToRelativePath(ctx: McpContext) {
126
+ return (filePath: string): string => {
127
+ if (
128
+ filePath === ctx.workspaceGlobal ||
129
+ filePath.startsWith(ctx.workspaceGlobal + path.sep)
130
+ ) {
131
+ return `[global] ${path.relative(ctx.workspaceGlobal, filePath)}`;
132
+ }
133
+ if (
134
+ filePath === ctx.workspaceMemory ||
135
+ filePath.startsWith(ctx.workspaceMemory + path.sep)
136
+ ) {
137
+ return `[memory] ${path.relative(ctx.workspaceMemory, filePath)}`;
138
+ }
139
+ return path.relative(ctx.workspaceGroup, filePath);
140
+ };
141
+ }
142
+
143
+ function parseMemoryFileReference(fileRef: string): {
144
+ pathRef: string;
145
+ lineFromRef?: number;
146
+ } {
147
+ const trimmed = fileRef.trim();
148
+ const lineRefMatch = trimmed.match(/^(.*?):(\d+)$/);
149
+ if (!lineRefMatch) return { pathRef: trimmed };
150
+
151
+ const lineFromRef = Number(lineRefMatch[2]);
152
+ if (!Number.isInteger(lineFromRef) || lineFromRef <= 0) {
153
+ return { pathRef: trimmed };
154
+ }
155
+ return { pathRef: lineRefMatch[1].trim(), lineFromRef };
156
+ }
157
+
158
+ /**
159
+ * Create all cli-claw MCP tool definitions for in-process SDK MCP server.
160
+ */
161
+ export function createMcpTools(ctx: McpContext): SdkMcpToolDefinition<any>[] {
162
+ const MESSAGES_DIR = path.join(ctx.workspaceIpc, 'messages');
163
+ const TASKS_DIR = path.join(ctx.workspaceIpc, 'tasks');
164
+ const hasCrossGroupAccess = ctx.isAdminHome;
165
+ const toRelativePath = createToRelativePath(ctx);
166
+
167
+ const tools: SdkMcpToolDefinition<any>[] = [
168
+ // --- send_message ---
169
+ tool(
170
+ 'send_message',
171
+ "Send a message to the user or group immediately while you're still running. Use this for progress updates or to send multiple messages. You can call this multiple times. Note: when running as a scheduled task, your final output is NOT sent to the user — use this tool if you need to communicate with the user or group.",
172
+ { text: z.string().describe('The message text to send') },
173
+ async (args) => {
174
+ const data: Record<string, unknown> = {
175
+ type: 'message',
176
+ chatJid: ctx.chatJid,
177
+ text: args.text,
178
+ groupFolder: ctx.groupFolder,
179
+ timestamp: new Date().toISOString(),
180
+ };
181
+ if (ctx.isScheduledTask) {
182
+ data.isScheduledTask = true;
183
+ }
184
+ writeIpcFile(MESSAGES_DIR, data);
185
+ return { content: [{ type: 'text' as const, text: 'Message sent.' }] };
186
+ },
187
+ ),
188
+
189
+ // --- send_image ---
190
+ tool(
191
+ 'send_image',
192
+ "Send an image file from the workspace to the user or group via IM (Feishu/Telegram/DingTalk). The file must be an image (PNG, JPEG, GIF, WebP, etc.) and must exist in the workspace. Use this when you've generated or downloaded an image and want to share it with the user. Optionally include a caption.",
193
+ {
194
+ file_path: z
195
+ .string()
196
+ .describe(
197
+ 'Path to the image file in the workspace (relative to workspace root or absolute)',
198
+ ),
199
+ caption: z
200
+ .string()
201
+ .optional()
202
+ .describe('Optional caption text to send with the image'),
203
+ },
204
+ async (args) => {
205
+ // NOTE: Web-prefixed JIDs (e.g. web:main) are no longer rejected here.
206
+ // The main process routes the image to the correct IM channel via
207
+ // activeImReplyRoutes, so the agent-runner should let the IPC
208
+ // request through regardless of JID prefix.
209
+
210
+ // Resolve path relative to workspace
211
+ const absPath = path.isAbsolute(args.file_path)
212
+ ? args.file_path
213
+ : path.join(ctx.workspaceGroup, args.file_path);
214
+
215
+ // Security: ensure path is within workspace
216
+ // Use path.sep suffix to prevent prefix-bypass (e.g. /ws/group1 matching /ws/group10/evil.png)
217
+ const resolved = path.resolve(absPath);
218
+ const safeRoot = ctx.workspaceGroup.endsWith(path.sep)
219
+ ? ctx.workspaceGroup
220
+ : ctx.workspaceGroup + path.sep;
221
+ if (resolved !== ctx.workspaceGroup && !resolved.startsWith(safeRoot)) {
222
+ return {
223
+ content: [
224
+ {
225
+ type: 'text' as const,
226
+ text: `Error: file path must be within workspace directory.`,
227
+ },
228
+ ],
229
+ isError: true,
230
+ };
231
+ }
232
+
233
+ // Check file exists
234
+ if (!fs.existsSync(resolved)) {
235
+ return {
236
+ content: [
237
+ {
238
+ type: 'text' as const,
239
+ text: `Error: file not found: ${args.file_path}`,
240
+ },
241
+ ],
242
+ isError: true,
243
+ };
244
+ }
245
+
246
+ // Read file and check size (10MB limit for both Feishu and Telegram)
247
+ const stat = fs.statSync(resolved);
248
+ if (stat.size > 10 * 1024 * 1024) {
249
+ return {
250
+ content: [
251
+ {
252
+ type: 'text' as const,
253
+ text: `Error: image file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum is 10MB.`,
254
+ },
255
+ ],
256
+ isError: true,
257
+ };
258
+ }
259
+ if (stat.size === 0) {
260
+ return {
261
+ content: [
262
+ { type: 'text' as const, text: `Error: image file is empty.` },
263
+ ],
264
+ isError: true,
265
+ };
266
+ }
267
+
268
+ const buffer = fs.readFileSync(resolved);
269
+ const base64 = buffer.toString('base64');
270
+
271
+ // Detect MIME type from magic bytes
272
+ const { detectImageMimeTypeFromBase64Strict } =
273
+ await import('./image-detector.js');
274
+ const mimeType = detectImageMimeTypeFromBase64Strict(base64);
275
+ if (!mimeType) {
276
+ return {
277
+ content: [
278
+ {
279
+ type: 'text' as const,
280
+ text: `Error: file does not appear to be a supported image format (PNG, JPEG, GIF, WebP, TIFF, BMP).`,
281
+ },
282
+ ],
283
+ isError: true,
284
+ };
285
+ }
286
+
287
+ const data: Record<string, unknown> = {
288
+ type: 'image',
289
+ chatJid: ctx.chatJid,
290
+ imageBase64: base64,
291
+ mimeType,
292
+ caption: args.caption || undefined,
293
+ fileName: path.basename(resolved),
294
+ groupFolder: ctx.groupFolder,
295
+ timestamp: new Date().toISOString(),
296
+ };
297
+ if (ctx.isScheduledTask) {
298
+ data.isScheduledTask = true;
299
+ }
300
+ writeIpcFile(MESSAGES_DIR, data);
301
+ return {
302
+ content: [
303
+ {
304
+ type: 'text' as const,
305
+ text: `Image sent: ${path.basename(resolved)} (${mimeType}, ${(stat.size / 1024).toFixed(1)}KB)`,
306
+ },
307
+ ],
308
+ };
309
+ },
310
+ ),
311
+
312
+ // --- send_file ---
313
+ tool(
314
+ 'send_file',
315
+ `Send a file to the current chat (the user you're talking to) via IM (Feishu/Telegram/DingTalk). The file path is relative to the workspace/group directory.
316
+ Supports: PDF, DOC, XLS, PPT, MP4, ZIP, SO, etc. Max file size: 30MB.`,
317
+ {
318
+ filePath: z
319
+ .string()
320
+ .describe(
321
+ 'File path relative to workspace/group (e.g., "output/report.pdf")',
322
+ ),
323
+ fileName: z
324
+ .string()
325
+ .describe('File name to display (e.g., "report.pdf")'),
326
+ },
327
+ async (args) => {
328
+ // NOTE: Web-prefixed JIDs (e.g. web:main) are no longer rejected here.
329
+ // The main process routes the file to the correct IM channel via
330
+ // activeImReplyRoutes, so the agent-runner should let the IPC
331
+ // request through regardless of JID prefix.
332
+
333
+ // Handle both absolute and relative paths
334
+ let resolvedPath: string;
335
+ let relativePath: string;
336
+
337
+ if (path.isAbsolute(args.filePath)) {
338
+ // Absolute path provided - validate and convert to relative
339
+ resolvedPath = path.resolve(args.filePath);
340
+ const safeRoot = ctx.workspaceGroup.endsWith(path.sep)
341
+ ? ctx.workspaceGroup
342
+ : ctx.workspaceGroup + path.sep;
343
+ if (
344
+ resolvedPath !== ctx.workspaceGroup &&
345
+ !resolvedPath.startsWith(safeRoot)
346
+ ) {
347
+ return {
348
+ content: [
349
+ {
350
+ type: 'text' as const,
351
+ text: 'Error: file must be within the workspace/group directory.',
352
+ },
353
+ ],
354
+ isError: true,
355
+ };
356
+ }
357
+ // Convert to relative path
358
+ relativePath = path.relative(ctx.workspaceGroup, resolvedPath);
359
+ } else {
360
+ // Relative path provided
361
+ relativePath = args.filePath;
362
+ resolvedPath = path.resolve(ctx.workspaceGroup, args.filePath);
363
+ // Validate resolved path is still within workspace
364
+ const safeRoot = ctx.workspaceGroup.endsWith(path.sep)
365
+ ? ctx.workspaceGroup
366
+ : ctx.workspaceGroup + path.sep;
367
+ if (
368
+ resolvedPath !== ctx.workspaceGroup &&
369
+ !resolvedPath.startsWith(safeRoot)
370
+ ) {
371
+ return {
372
+ content: [
373
+ {
374
+ type: 'text' as const,
375
+ text: 'Error: file must be within the workspace/group directory.',
376
+ },
377
+ ],
378
+ isError: true,
379
+ };
380
+ }
381
+ }
382
+
383
+ if (!fs.existsSync(resolvedPath)) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: 'text' as const,
388
+ text: `Error: file not found: ${args.filePath}`,
389
+ },
390
+ ],
391
+ isError: true,
392
+ };
393
+ }
394
+
395
+ const data = {
396
+ type: 'send_file',
397
+ chatJid: ctx.chatJid,
398
+ filePath: relativePath,
399
+ fileName: args.fileName,
400
+ timestamp: new Date().toISOString(),
401
+ };
402
+ writeIpcFile(TASKS_DIR, data);
403
+ return {
404
+ content: [
405
+ {
406
+ type: 'text' as const,
407
+ text: `Sending file "${args.fileName}"...`,
408
+ },
409
+ ],
410
+ };
411
+ },
412
+ ),
413
+
414
+ // --- schedule_task ---
415
+ tool(
416
+ 'schedule_task',
417
+ `Schedule a recurring or one-time task.
418
+
419
+ EXECUTION TYPE:
420
+ \u2022 "agent" (default): Task runs as a full Claude Agent with access to all tools. Consumes API tokens.
421
+ \u2022 "script" (admin only): Task runs a shell command directly on the host. Zero API token cost. Use for deterministic tasks like health checks, data collection, cURL calls, or cron-like scripts.
422
+
423
+ EXECUTION MODE:
424
+ \u2022 "host": Task runs directly on the host machine. Admin only.
425
+ \u2022 "container" (default for non-admin): Task runs in a Docker container.
426
+ Each agent task automatically gets its own dedicated workspace.
427
+
428
+ CONTEXT MODE (agent mode only) - Choose based on task type:
429
+ \u2022 "group": Task runs in the group's conversation context, with access to chat history.
430
+ \u2022 "isolated": Task runs in a fresh session with no conversation history.
431
+
432
+ MESSAGING BEHAVIOR - The task output is sent to the user or group.
433
+ \u2022 Agent mode: output is sent via MCP tool or stdout. Use <internal> tags to suppress.
434
+ \u2022 Script mode: stdout is sent as the result. stderr is included on failure.
435
+
436
+ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
437
+ \u2022 cron: Standard cron expression (e.g., "*/5 * * * *" for every 5 minutes, "0 9 * * *" for daily at 9am LOCAL time)
438
+ \u2022 interval: Milliseconds between runs (e.g., "300000" for 5 minutes, "3600000" for 1 hour)
439
+ \u2022 once: Local time WITHOUT "Z" suffix (e.g., "2026-02-01T15:30:00"). Do NOT use UTC/Z suffix.`,
440
+ {
441
+ prompt: z
442
+ .string()
443
+ .optional()
444
+ .default('')
445
+ .describe(
446
+ 'What the agent should do (agent mode) or task description (script mode, optional).',
447
+ ),
448
+ schedule_type: z
449
+ .enum(['cron', 'interval', 'once'])
450
+ .describe(
451
+ 'cron=recurring at specific times, interval=recurring every N ms, once=run once at specific time',
452
+ ),
453
+ schedule_value: z
454
+ .string()
455
+ .describe(
456
+ 'cron: "*/5 * * * *" | interval: milliseconds like "300000" | once: local timestamp like "2026-02-01T15:30:00" (no Z suffix!)',
457
+ ),
458
+ execution_type: z
459
+ .enum(['agent', 'script'])
460
+ .default('agent')
461
+ .describe(
462
+ 'agent=full Claude Agent (default), script=shell command (admin only, zero token cost)',
463
+ ),
464
+ script_command: z
465
+ .string()
466
+ .max(4096)
467
+ .optional()
468
+ .describe(
469
+ 'Shell command to execute (required for script mode). Runs in the group workspace directory.',
470
+ ),
471
+ execution_mode: z
472
+ .enum(['host', 'container'])
473
+ .optional()
474
+ .describe(
475
+ 'Execution mode: host runs directly on the server, container runs in Docker isolation',
476
+ ),
477
+ context_mode: z
478
+ .enum(['group', 'isolated'])
479
+ .default('group')
480
+ .describe(
481
+ '(agent mode only) group=runs with persistent workspace context (recommended), isolated=fresh session each time',
482
+ ),
483
+ target_group_jid: z
484
+ .string()
485
+ .optional()
486
+ .describe(
487
+ '(Admin home only) JID of the group to schedule the task for. Defaults to the current group.',
488
+ ),
489
+ },
490
+ async (args) => {
491
+ const execType = args.execution_type || 'agent';
492
+
493
+ // Validate execution_type constraints
494
+ if (execType === 'agent' && !args.prompt?.trim()) {
495
+ return {
496
+ content: [
497
+ {
498
+ type: 'text' as const,
499
+ text: 'Agent mode requires a prompt. Provide instructions for what the agent should do.',
500
+ },
501
+ ],
502
+ isError: true,
503
+ };
504
+ }
505
+ if (execType === 'script' && !args.script_command?.trim()) {
506
+ return {
507
+ content: [
508
+ {
509
+ type: 'text' as const,
510
+ text: 'Script mode requires script_command. Provide the shell command to execute.',
511
+ },
512
+ ],
513
+ isError: true,
514
+ };
515
+ }
516
+ if (execType === 'script' && !ctx.isAdminHome) {
517
+ return {
518
+ content: [
519
+ {
520
+ type: 'text' as const,
521
+ text: 'Only admin home container can create script tasks.',
522
+ },
523
+ ],
524
+ isError: true,
525
+ };
526
+ }
527
+
528
+ // Validate schedule_value before writing IPC
529
+ if (args.schedule_type === 'cron') {
530
+ try {
531
+ CronExpressionParser.parse(args.schedule_value, {
532
+ tz: process.env.TZ || 'Asia/Shanghai',
533
+ });
534
+ } catch {
535
+ return {
536
+ content: [
537
+ {
538
+ type: 'text' as const,
539
+ text: `Invalid cron: "${args.schedule_value}". Use format like "0 9 * * *" (daily 9am) or "*/5 * * * *" (every 5 min).`,
540
+ },
541
+ ],
542
+ isError: true,
543
+ };
544
+ }
545
+ } else if (args.schedule_type === 'interval') {
546
+ const ms = parseInt(args.schedule_value, 10);
547
+ if (isNaN(ms) || ms <= 0) {
548
+ return {
549
+ content: [
550
+ {
551
+ type: 'text' as const,
552
+ text: `Invalid interval: "${args.schedule_value}". Must be positive milliseconds (e.g., "300000" for 5 min).`,
553
+ },
554
+ ],
555
+ isError: true,
556
+ };
557
+ }
558
+ } else if (args.schedule_type === 'once') {
559
+ const date = new Date(args.schedule_value);
560
+ if (isNaN(date.getTime())) {
561
+ return {
562
+ content: [
563
+ {
564
+ type: 'text' as const,
565
+ text: `Invalid timestamp: "${args.schedule_value}". Use ISO 8601 format like "2026-02-01T15:30:00.000Z".`,
566
+ },
567
+ ],
568
+ isError: true,
569
+ };
570
+ }
571
+ }
572
+
573
+ const targetJid =
574
+ hasCrossGroupAccess && args.target_group_jid
575
+ ? args.target_group_jid
576
+ : ctx.chatJid;
577
+ const data: Record<string, unknown> = {
578
+ type: 'schedule_task',
579
+ prompt: args.prompt || '',
580
+ schedule_type: args.schedule_type,
581
+ schedule_value: args.schedule_value,
582
+ context_mode: args.context_mode || 'isolated',
583
+ execution_type: execType,
584
+ targetJid,
585
+ createdBy: ctx.groupFolder,
586
+ timestamp: new Date().toISOString(),
587
+ };
588
+ if (execType === 'script') {
589
+ data.script_command = args.script_command;
590
+ }
591
+ if (args.execution_mode) {
592
+ data.execution_mode = args.execution_mode;
593
+ }
594
+ const filename = writeIpcFile(TASKS_DIR, data);
595
+ const modeLabel = execType === 'script' ? 'script' : 'agent';
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text' as const,
600
+ text: `Task scheduled [${modeLabel}] (${filename}): ${args.schedule_type} - ${args.schedule_value}`,
601
+ },
602
+ ],
603
+ };
604
+ },
605
+ ),
606
+
607
+ // --- list_tasks ---
608
+ tool(
609
+ 'list_tasks',
610
+ "List all scheduled tasks. From admin home: shows all tasks. From other groups: shows only that group's tasks.",
611
+ {},
612
+ async () => {
613
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
614
+ try {
615
+ const result = await pollIpcResult(
616
+ TASKS_DIR,
617
+ {
618
+ type: 'list_tasks',
619
+ requestId,
620
+ groupFolder: ctx.groupFolder,
621
+ isAdminHome: hasCrossGroupAccess,
622
+ timestamp: new Date().toISOString(),
623
+ },
624
+ 'list_tasks_result',
625
+ );
626
+ if (!result.success) {
627
+ return {
628
+ content: [
629
+ {
630
+ type: 'text' as const,
631
+ text: `Error listing tasks: ${result.error || 'Unknown error'}`,
632
+ },
633
+ ],
634
+ isError: true,
635
+ };
636
+ }
637
+ const tasks = (result.tasks || []) as Array<{
638
+ id: string;
639
+ prompt: string;
640
+ schedule_type: string;
641
+ schedule_value: string;
642
+ status: string;
643
+ next_run: string;
644
+ }>;
645
+ if (tasks.length === 0) {
646
+ return {
647
+ content: [
648
+ { type: 'text' as const, text: 'No scheduled tasks found.' },
649
+ ],
650
+ };
651
+ }
652
+ const formatted = tasks
653
+ .map(
654
+ (t) =>
655
+ `- [${t.id}] ${t.prompt.slice(0, 50)}... (${t.schedule_type}: ${t.schedule_value}) - ${t.status}, next: ${t.next_run || 'N/A'}`,
656
+ )
657
+ .join('\n');
658
+ return {
659
+ content: [
660
+ { type: 'text' as const, text: `Scheduled tasks:\n${formatted}` },
661
+ ],
662
+ };
663
+ } catch {
664
+ return {
665
+ content: [
666
+ {
667
+ type: 'text' as const,
668
+ text: 'Timeout waiting for task list response.',
669
+ },
670
+ ],
671
+ isError: true,
672
+ };
673
+ }
674
+ },
675
+ ),
676
+
677
+ // --- pause_task ---
678
+ tool(
679
+ 'pause_task',
680
+ 'Pause a scheduled task. It will not run until resumed.',
681
+ { task_id: z.string().describe('The task ID to pause') },
682
+ async (args) => {
683
+ const data = {
684
+ type: 'pause_task',
685
+ taskId: args.task_id,
686
+ groupFolder: ctx.groupFolder,
687
+ isMain: hasCrossGroupAccess,
688
+ timestamp: new Date().toISOString(),
689
+ };
690
+ writeIpcFile(TASKS_DIR, data);
691
+ return {
692
+ content: [
693
+ {
694
+ type: 'text' as const,
695
+ text: `Task ${args.task_id} pause requested.`,
696
+ },
697
+ ],
698
+ };
699
+ },
700
+ ),
701
+
702
+ // --- resume_task ---
703
+ tool(
704
+ 'resume_task',
705
+ 'Resume a paused task.',
706
+ { task_id: z.string().describe('The task ID to resume') },
707
+ async (args) => {
708
+ const data = {
709
+ type: 'resume_task',
710
+ taskId: args.task_id,
711
+ groupFolder: ctx.groupFolder,
712
+ isMain: hasCrossGroupAccess,
713
+ timestamp: new Date().toISOString(),
714
+ };
715
+ writeIpcFile(TASKS_DIR, data);
716
+ return {
717
+ content: [
718
+ {
719
+ type: 'text' as const,
720
+ text: `Task ${args.task_id} resume requested.`,
721
+ },
722
+ ],
723
+ };
724
+ },
725
+ ),
726
+
727
+ // --- cancel_task ---
728
+ tool(
729
+ 'cancel_task',
730
+ 'Cancel and delete a scheduled task.',
731
+ { task_id: z.string().describe('The task ID to cancel') },
732
+ async (args) => {
733
+ const data = {
734
+ type: 'cancel_task',
735
+ taskId: args.task_id,
736
+ groupFolder: ctx.groupFolder,
737
+ isMain: hasCrossGroupAccess,
738
+ timestamp: new Date().toISOString(),
739
+ };
740
+ writeIpcFile(TASKS_DIR, data);
741
+ return {
742
+ content: [
743
+ {
744
+ type: 'text' as const,
745
+ text: `Task ${args.task_id} cancellation requested.`,
746
+ },
747
+ ],
748
+ };
749
+ },
750
+ ),
751
+
752
+ // --- register_group ---
753
+ tool(
754
+ 'register_group',
755
+ `Register a new group so the agent can respond to messages there. Admin home only.
756
+
757
+ Use available_groups.json to find the JID for a group. The folder name should be lowercase with hyphens (e.g., "family-chat").
758
+ You can optionally specify execution_mode: "container" (default, isolated Docker) or "host" (direct host access, admin only).`,
759
+ {
760
+ jid: z.string().describe('The chat JID (e.g., "feishu:oc_xxxx")'),
761
+ name: z.string().describe('Display name for the group'),
762
+ folder: z
763
+ .string()
764
+ .describe(
765
+ 'Folder name for group files (lowercase, hyphens, e.g., "family-chat")',
766
+ ),
767
+ execution_mode: z
768
+ .enum(['container', 'host'])
769
+ .optional()
770
+ .describe(
771
+ 'Execution mode: "container" (default, isolated Docker) or "host" (direct host access)',
772
+ ),
773
+ },
774
+ async (args) => {
775
+ if (!hasCrossGroupAccess) {
776
+ return {
777
+ content: [
778
+ {
779
+ type: 'text' as const,
780
+ text: 'Only the admin home container can register new groups.',
781
+ },
782
+ ],
783
+ isError: true,
784
+ };
785
+ }
786
+ const data = {
787
+ type: 'register_group',
788
+ jid: args.jid,
789
+ name: args.name,
790
+ folder: args.folder,
791
+ executionMode: args.execution_mode,
792
+ timestamp: new Date().toISOString(),
793
+ };
794
+ writeIpcFile(TASKS_DIR, data);
795
+ return {
796
+ content: [
797
+ {
798
+ type: 'text' as const,
799
+ text: `Group "${args.name}" registered. It will start receiving messages immediately.`,
800
+ },
801
+ ],
802
+ };
803
+ },
804
+ ),
805
+ ];
806
+
807
+ // Skill 安装/卸载仅限主容器(与 memory_* 工具一致)
808
+ if (ctx.isHome) {
809
+ tools.push(
810
+ // --- install_skill ---
811
+ tool(
812
+ 'install_skill',
813
+ `Install a skill from the skills registry (skills.sh). The skill will be available in future conversations.
814
+ Example packages: "anthropic/memory", "anthropic/think", "owner/repo", "owner/repo@skill-name".`,
815
+ {
816
+ package: z
817
+ .string()
818
+ .describe(
819
+ 'The skill package to install, format: owner/repo or owner/repo@skill',
820
+ ),
821
+ },
822
+ async (args) => {
823
+ const pkg = args.package.trim();
824
+ if (
825
+ !/^[\w\-]+\/[\w\-.]+(?:[@#][\w\-.\/]+)?$/.test(pkg) &&
826
+ !/^https?:\/\//.test(pkg)
827
+ ) {
828
+ return {
829
+ content: [
830
+ {
831
+ type: 'text' as const,
832
+ text: `Invalid package format: "${pkg}". Expected format: owner/repo or owner/repo@skill`,
833
+ },
834
+ ],
835
+ isError: true,
836
+ };
837
+ }
838
+
839
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
840
+ try {
841
+ const result = await pollIpcResult(
842
+ TASKS_DIR,
843
+ {
844
+ type: 'install_skill',
845
+ package: pkg,
846
+ requestId,
847
+ groupFolder: ctx.groupFolder,
848
+ timestamp: new Date().toISOString(),
849
+ },
850
+ 'install_skill_result',
851
+ 120_000,
852
+ );
853
+ if (result.success) {
854
+ const installed =
855
+ ((result.installed as string[]) || []).join(', ') || pkg;
856
+ return {
857
+ content: [
858
+ {
859
+ type: 'text' as const,
860
+ text: `Skill installed successfully: ${installed}\n\nNote: The skill will be available in the next conversation (new container/process).`,
861
+ },
862
+ ],
863
+ };
864
+ } else {
865
+ return {
866
+ content: [
867
+ {
868
+ type: 'text' as const,
869
+ text: `Failed to install skill "${pkg}": ${result.error || 'Unknown error'}`,
870
+ },
871
+ ],
872
+ isError: true,
873
+ };
874
+ }
875
+ } catch {
876
+ return {
877
+ content: [
878
+ {
879
+ type: 'text' as const,
880
+ text: `Timeout waiting for skill installation result (120s). The installation may still be in progress.`,
881
+ },
882
+ ],
883
+ isError: true,
884
+ };
885
+ }
886
+ },
887
+ ),
888
+
889
+ // --- uninstall_skill ---
890
+ tool(
891
+ 'uninstall_skill',
892
+ `Uninstall a user-level skill by its ID. Project-level skills cannot be uninstalled.
893
+ Use the skills panel in the UI to find the skill ID (directory name, e.g. "memory", "think").`,
894
+ {
895
+ skill_id: z
896
+ .string()
897
+ .describe(
898
+ 'The skill ID to uninstall (the directory name, e.g. "memory", "think")',
899
+ ),
900
+ },
901
+ async (args) => {
902
+ const skillId = args.skill_id.trim();
903
+ if (!skillId || !/^[\w\-]+$/.test(skillId)) {
904
+ return {
905
+ content: [
906
+ {
907
+ type: 'text' as const,
908
+ text: `Invalid skill ID: "${skillId}". Must be alphanumeric with hyphens/underscores.`,
909
+ },
910
+ ],
911
+ isError: true,
912
+ };
913
+ }
914
+
915
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
916
+ try {
917
+ const result = await pollIpcResult(
918
+ TASKS_DIR,
919
+ {
920
+ type: 'uninstall_skill',
921
+ skillId,
922
+ requestId,
923
+ groupFolder: ctx.groupFolder,
924
+ timestamp: new Date().toISOString(),
925
+ },
926
+ 'uninstall_skill_result',
927
+ );
928
+ if (result.success) {
929
+ return {
930
+ content: [
931
+ {
932
+ type: 'text' as const,
933
+ text: `Skill "${skillId}" uninstalled successfully.`,
934
+ },
935
+ ],
936
+ };
937
+ } else {
938
+ return {
939
+ content: [
940
+ {
941
+ type: 'text' as const,
942
+ text: `Failed to uninstall skill "${skillId}": ${result.error || 'Unknown error'}`,
943
+ },
944
+ ],
945
+ isError: true,
946
+ };
947
+ }
948
+ } catch {
949
+ return {
950
+ content: [
951
+ {
952
+ type: 'text' as const,
953
+ text: `Timeout waiting for skill uninstall result.`,
954
+ },
955
+ ],
956
+ isError: true,
957
+ };
958
+ }
959
+ },
960
+ ),
961
+ );
962
+ }
963
+
964
+ // --- memory_append --- (only available for home containers)
965
+ if (ctx.isHome) {
966
+ tools.push(
967
+ tool(
968
+ 'memory_append',
969
+ `\u5c06**\u65f6\u6548\u6027\u8bb0\u5fc6**\u8ffd\u52a0\u5230 memory/YYYY-MM-DD.md\uff08\u72ec\u7acb\u8bb0\u5fc6\u76ee\u5f55\uff0c\u4e0d\u5728\u5de5\u4f5c\u533a\u5185\uff09\u3002
970
+ \u4ec5\u8ffd\u52a0\u5199\u5165\uff0c\u4e0d\u4f1a\u8986\u76d6\u5df2\u6709\u5185\u5bb9\u3002
971
+
972
+ \u4ec5\u7528\u4e8e\u660e\u786e\u53ea\u8ddf\u5f53\u5929/\u77ed\u671f\u6709\u5173\u7684\u4fe1\u606f\uff1a\u4eca\u65e5\u9879\u76ee\u8fdb\u5c55\u3001\u4e34\u65f6\u6280\u672f\u51b3\u7b56\u3001\u5f85\u529e\u4e8b\u9879\u3001\u4f1a\u8bae\u8981\u70b9\u7b49\u3002
973
+
974
+ **\u91cd\u8981**\uff1a\u4e0b\u6b21\u5bf9\u8bdd\u4ecd\u53ef\u80fd\u7528\u5230\u7684\u4fe1\u606f\uff08\u7528\u6237\u8eab\u4efd\u3001\u504f\u597d\u3001\u5e38\u7528\u9879\u76ee\u3001\u7528\u6237\u8bf4\u201c\u8bb0\u4f4f\u201d\u7684\u5185\u5bb9\uff09\u5e94\u76f4\u63a5\u7528 Edit \u5de5\u5177\u7f16\u8f91 /workspace/global/AGENTS.md\uff0c\u4e0d\u8981\u7528\u6b64\u5de5\u5177\u3002`,
975
+ {
976
+ content: z
977
+ .string()
978
+ .describe('\u8981\u8ffd\u52a0\u7684\u8bb0\u5fc6\u5185\u5bb9'),
979
+ date: z
980
+ .string()
981
+ .optional()
982
+ .describe(
983
+ '\u76ee\u6807\u65e5\u671f\uff0c\u683c\u5f0f YYYY-MM-DD\uff08\u9ed8\u8ba4\uff1a\u4eca\u5929\uff09',
984
+ ),
985
+ },
986
+ async (args) => {
987
+ const normalizedContent = args.content.replace(/\r\n?/g, '\n').trim();
988
+ if (!normalizedContent) {
989
+ return {
990
+ content: [
991
+ {
992
+ type: 'text' as const,
993
+ text: '\u5185\u5bb9\u4e0d\u80fd\u4e3a\u7a7a\u3002',
994
+ },
995
+ ],
996
+ isError: true,
997
+ };
998
+ }
999
+ const appendBytes = Buffer.byteLength(normalizedContent, 'utf-8');
1000
+ if (appendBytes > MAX_MEMORY_APPEND_SIZE) {
1001
+ return {
1002
+ content: [
1003
+ {
1004
+ type: 'text' as const,
1005
+ text: `\u5185\u5bb9\u8fc7\u5927\uff1a${appendBytes} \u5b57\u8282\uff08\u4e0a\u9650 ${MAX_MEMORY_APPEND_SIZE}\uff09\u3002`,
1006
+ },
1007
+ ],
1008
+ isError: true,
1009
+ };
1010
+ }
1011
+ const date = (
1012
+ args.date ?? new Date().toISOString().split('T')[0]
1013
+ ).trim();
1014
+ if (!MEMORY_DATE_PATTERN.test(date)) {
1015
+ return {
1016
+ content: [
1017
+ {
1018
+ type: 'text' as const,
1019
+ text: `\u65e5\u671f\u683c\u5f0f\u65e0\u6548\uff1a\u201c${date}\u201d\uff0c\u8bf7\u4f7f\u7528 YYYY-MM-DD\u3002`,
1020
+ },
1021
+ ],
1022
+ isError: true,
1023
+ };
1024
+ }
1025
+ const resolvedPath = path.normalize(
1026
+ path.join(ctx.workspaceMemory, `${date}.md`),
1027
+ );
1028
+ const inMemory =
1029
+ resolvedPath === ctx.workspaceMemory ||
1030
+ resolvedPath.startsWith(ctx.workspaceMemory + path.sep);
1031
+ if (!inMemory) {
1032
+ return {
1033
+ content: [
1034
+ {
1035
+ type: 'text' as const,
1036
+ text: '\u8bbf\u95ee\u88ab\u62d2\u7edd\uff1a\u8def\u5f84\u8d85\u51fa\u5de5\u4f5c\u533a\u8303\u56f4\u3002',
1037
+ },
1038
+ ],
1039
+ isError: true,
1040
+ };
1041
+ }
1042
+ try {
1043
+ fs.mkdirSync(ctx.workspaceMemory, { recursive: true });
1044
+ const fileExists = fs.existsSync(resolvedPath);
1045
+ const currentSize = fileExists ? fs.statSync(resolvedPath).size : 0;
1046
+ const separator = currentSize > 0 ? '\n---\n\n' : '';
1047
+ const entry = `${separator}### ${new Date().toISOString()}\n${normalizedContent}\n`;
1048
+ const nextSize = currentSize + Buffer.byteLength(entry, 'utf-8');
1049
+ if (nextSize > MAX_MEMORY_FILE_SIZE) {
1050
+ return {
1051
+ content: [
1052
+ {
1053
+ type: 'text' as const,
1054
+ text: `\u8bb0\u5fc6\u6587\u4ef6\u5c06\u8d85\u8fc7 ${MAX_MEMORY_FILE_SIZE} \u5b57\u8282\u4e0a\u9650\uff0c\u8bf7\u7f29\u77ed\u5185\u5bb9\u3002`,
1055
+ },
1056
+ ],
1057
+ isError: true,
1058
+ };
1059
+ }
1060
+ fs.appendFileSync(resolvedPath, entry, 'utf-8');
1061
+ return {
1062
+ content: [
1063
+ {
1064
+ type: 'text' as const,
1065
+ text: `\u5df2\u8ffd\u52a0\u5230 memory/${date}.md\uff08${appendBytes} \u5b57\u8282\uff09\u3002`,
1066
+ },
1067
+ ],
1068
+ };
1069
+ } catch (err) {
1070
+ return {
1071
+ content: [
1072
+ {
1073
+ type: 'text' as const,
1074
+ text: `\u8ffd\u52a0\u8bb0\u5fc6\u65f6\u51fa\u9519\uff1a${err instanceof Error ? err.message : String(err)}`,
1075
+ },
1076
+ ],
1077
+ isError: true,
1078
+ };
1079
+ }
1080
+ },
1081
+ ),
1082
+ );
1083
+ }
1084
+
1085
+ // --- memory_search --- (available for all containers)
1086
+ tools.push(
1087
+ tool(
1088
+ 'memory_search',
1089
+ `\u5728\u5de5\u4f5c\u533a\u7684\u8bb0\u5fc6\u6587\u4ef6\u4e2d\u641c\u7d22\uff08AGENTS.md\u3001memory/\u3001conversations/ \u53ca\u5176\u4ed6 .md/.txt \u6587\u4ef6\uff09\u3002
1090
+ \u8fd4\u56de\u6587\u4ef6\u8def\u5f84\u3001\u884c\u53f7\u548c\u4e0a\u4e0b\u6587\u7247\u6bb5\u3002\u8d85\u8fc7 512KB \u7684\u6587\u4ef6\u4f1a\u88ab\u8df3\u8fc7\u3002
1091
+ \u7528\u4e8e\u56de\u5fc6\u8fc7\u53bb\u7684\u51b3\u7b56\u3001\u504f\u597d\u3001\u9879\u76ee\u4e0a\u4e0b\u6587\u6216\u5bf9\u8bdd\u5386\u53f2\u3002`,
1092
+ {
1093
+ query: z
1094
+ .string()
1095
+ .describe(
1096
+ '\u641c\u7d22\u5173\u952e\u8bcd\u6216\u77ed\u8bed\uff08\u4e0d\u533a\u5206\u5927\u5c0f\u5199\uff09',
1097
+ ),
1098
+ max_results: z
1099
+ .number()
1100
+ .optional()
1101
+ .default(20)
1102
+ .describe(
1103
+ '\u6700\u5927\u7ed3\u679c\u6570\uff08\u9ed8\u8ba4 20\uff0c\u4e0a\u9650 50\uff09',
1104
+ ),
1105
+ },
1106
+ async (args) => {
1107
+ if (!args.query.trim()) {
1108
+ return {
1109
+ content: [
1110
+ {
1111
+ type: 'text' as const,
1112
+ text: '\u641c\u7d22\u5173\u952e\u8bcd\u4e0d\u80fd\u4e3a\u7a7a\u3002',
1113
+ },
1114
+ ],
1115
+ isError: true,
1116
+ };
1117
+ }
1118
+ const maxResults = Math.min(Math.max(args.max_results ?? 20, 1), 50);
1119
+ const queryLower = args.query.toLowerCase();
1120
+ const files: string[] = [];
1121
+ collectMemoryFiles(ctx.workspaceMemory, files, 4);
1122
+ collectMemoryFiles(ctx.workspaceGroup, files, 4);
1123
+ collectMemoryFiles(ctx.workspaceGlobal, files, 4);
1124
+ const uniqueFiles = Array.from(new Set(files));
1125
+ if (uniqueFiles.length === 0) {
1126
+ return {
1127
+ content: [
1128
+ {
1129
+ type: 'text' as const,
1130
+ text: '\u672a\u627e\u5230\u8bb0\u5fc6\u6587\u4ef6\u3002',
1131
+ },
1132
+ ],
1133
+ };
1134
+ }
1135
+ const results: string[] = [];
1136
+ let skippedLarge = 0;
1137
+ for (const filePath of uniqueFiles) {
1138
+ if (results.length >= maxResults) break;
1139
+ try {
1140
+ const stat = fs.statSync(filePath);
1141
+ if (stat.size > MAX_MEMORY_FILE_SIZE) {
1142
+ skippedLarge++;
1143
+ continue;
1144
+ }
1145
+ const content = fs.readFileSync(filePath, 'utf-8');
1146
+ const lines = content.split('\n');
1147
+ let lastEnd = -1;
1148
+ for (let i = 0; i < lines.length; i++) {
1149
+ if (results.length >= maxResults) break;
1150
+ if (lines[i].toLowerCase().includes(queryLower)) {
1151
+ const start = Math.max(0, i - 1);
1152
+ if (start <= lastEnd) continue;
1153
+ const end = Math.min(lines.length, i + 2);
1154
+ lastEnd = end;
1155
+ const snippet = lines.slice(start, end).join('\n');
1156
+ results.push(
1157
+ `${toRelativePath(filePath)}:${i + 1}\n${snippet}`,
1158
+ );
1159
+ }
1160
+ }
1161
+ } catch {
1162
+ /* skip unreadable */
1163
+ }
1164
+ }
1165
+ const skippedNote =
1166
+ skippedLarge > 0
1167
+ ? `\uff08\u8df3\u8fc7 ${skippedLarge} \u4e2a\u5927\u6587\u4ef6\uff09`
1168
+ : '';
1169
+ if (results.length === 0) {
1170
+ return {
1171
+ content: [
1172
+ {
1173
+ type: 'text' as const,
1174
+ text: `\u5728 ${uniqueFiles.length} \u4e2a\u8bb0\u5fc6\u6587\u4ef6\u4e2d\u672a\u627e\u5230\u201c${args.query}\u201d\u7684\u5339\u914d\u3002${skippedNote}`,
1175
+ },
1176
+ ],
1177
+ };
1178
+ }
1179
+ return {
1180
+ content: [
1181
+ {
1182
+ type: 'text' as const,
1183
+ text: `\u627e\u5230 ${results.length} \u6761\u5339\u914d${skippedNote}\uff1a\n\n${results.join('\n---\n')}`,
1184
+ },
1185
+ ],
1186
+ };
1187
+ },
1188
+ ),
1189
+
1190
+ // --- memory_get ---
1191
+ tool(
1192
+ 'memory_get',
1193
+ `\u8bfb\u53d6\u8bb0\u5fc6\u6587\u4ef6\u6216\u6307\u5b9a\u884c\u8303\u56f4\u3002\u5728 memory_search \u4e4b\u540e\u4f7f\u7528\u4ee5\u83b7\u53d6\u5b8c\u6574\u4e0a\u4e0b\u6587\u3002`,
1194
+ {
1195
+ file: z
1196
+ .string()
1197
+ .describe(
1198
+ '\u76f8\u5bf9\u8def\u5f84\uff0c\u53ef\u5e26 :\u884c\u53f7\uff08\u5982 "AGENTS.md:12"\u3001"[global] AGENTS.md:8" \u6216 "[memory] 2026-01-15.md"\uff09',
1199
+ ),
1200
+ from_line: z
1201
+ .number()
1202
+ .optional()
1203
+ .describe(
1204
+ '\u8d77\u59cb\u884c\u53f7\uff08\u4ece 1 \u5f00\u59cb\uff0c\u9ed8\u8ba4\uff1a1\uff09',
1205
+ ),
1206
+ lines: z
1207
+ .number()
1208
+ .optional()
1209
+ .describe(
1210
+ '\u8bfb\u53d6\u884c\u6570\uff08\u9ed8\u8ba4\uff1a\u5168\u90e8\uff0c\u4e0a\u9650\uff1a200\uff09',
1211
+ ),
1212
+ },
1213
+ async (args) => {
1214
+ const { pathRef, lineFromRef } = parseMemoryFileReference(args.file);
1215
+ let resolvedPath: string;
1216
+ if (pathRef.startsWith('[global] ')) {
1217
+ resolvedPath = path.join(
1218
+ ctx.workspaceGlobal,
1219
+ pathRef.slice('[global] '.length),
1220
+ );
1221
+ } else if (pathRef.startsWith('[memory] ')) {
1222
+ resolvedPath = path.join(
1223
+ ctx.workspaceMemory,
1224
+ pathRef.slice('[memory] '.length),
1225
+ );
1226
+ } else {
1227
+ resolvedPath = path.join(ctx.workspaceGroup, pathRef);
1228
+ }
1229
+ resolvedPath = path.normalize(resolvedPath);
1230
+ const inGroup =
1231
+ resolvedPath === ctx.workspaceGroup ||
1232
+ resolvedPath.startsWith(ctx.workspaceGroup + path.sep);
1233
+ const inGlobal =
1234
+ resolvedPath === ctx.workspaceGlobal ||
1235
+ resolvedPath.startsWith(ctx.workspaceGlobal + path.sep);
1236
+ const inMemory =
1237
+ resolvedPath === ctx.workspaceMemory ||
1238
+ resolvedPath.startsWith(ctx.workspaceMemory + path.sep);
1239
+ if (!inGroup && !inGlobal && !inMemory) {
1240
+ return {
1241
+ content: [
1242
+ {
1243
+ type: 'text' as const,
1244
+ text: '\u8bbf\u95ee\u88ab\u62d2\u7edd\uff1a\u8def\u5f84\u8d85\u51fa\u5de5\u4f5c\u533a\u8303\u56f4\u3002',
1245
+ },
1246
+ ],
1247
+ isError: true,
1248
+ };
1249
+ }
1250
+ if (!fs.existsSync(resolvedPath)) {
1251
+ return {
1252
+ content: [
1253
+ {
1254
+ type: 'text' as const,
1255
+ text: `\u6587\u4ef6\u672a\u627e\u5230\uff1a${pathRef}`,
1256
+ },
1257
+ ],
1258
+ isError: true,
1259
+ };
1260
+ }
1261
+ try {
1262
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
1263
+ const allLines = content.split('\n');
1264
+ const fromLine = Math.max(
1265
+ (args.from_line ?? lineFromRef ?? 1) - 1,
1266
+ 0,
1267
+ );
1268
+ const maxLines = Math.min(args.lines ?? allLines.length, 200);
1269
+ const slice = allLines.slice(fromLine, fromLine + maxLines);
1270
+ const header = `${pathRef}\uff08\u7b2c ${fromLine + 1}-${fromLine + slice.length} \u884c\uff0c\u5171 ${allLines.length} \u884c\uff09`;
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: 'text' as const,
1275
+ text: `${header}\n\n${slice.join('\n')}`,
1276
+ },
1277
+ ],
1278
+ };
1279
+ } catch (err) {
1280
+ return {
1281
+ content: [
1282
+ {
1283
+ type: 'text' as const,
1284
+ text: `\u8bfb\u53d6\u6587\u4ef6\u65f6\u51fa\u9519\uff1a${err instanceof Error ? err.message : String(err)}`,
1285
+ },
1286
+ ],
1287
+ isError: true,
1288
+ };
1289
+ }
1290
+ },
1291
+ ),
1292
+ );
1293
+
1294
+ return tools;
1295
+ }