dr-claw 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/LICENSE +1380 -0
  2. package/NOTICE +17 -0
  3. package/README.md +875 -0
  4. package/README.zh-CN.md +857 -0
  5. package/dist/api-docs.html +878 -0
  6. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
  66. package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
  67. package/dist/assets/arc-DOMBmC_A.js +1 -0
  68. package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
  71. package/dist/assets/channel-BFOE7OyC.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
  76. package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
  77. package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
  82. package/dist/assets/clone-BulQSblH.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
  84. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  85. package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
  94. package/dist/assets/graph-Blm-emkR.js +1 -0
  95. package/dist/assets/index-B8Yutsnm.css +1 -0
  96. package/dist/assets/index-Cf6gyhc3.js +654 -0
  97. package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
  98. package/dist/assets/init-Gi6I4Gst.js +1 -0
  99. package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
  100. package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
  101. package/dist/assets/layout-Ct2kFIId.js +1 -0
  102. package/dist/assets/linear-CKG9EwIJ.js +1 -0
  103. package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
  105. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
  107. package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
  108. package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
  109. package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
  110. package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
  111. package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
  113. package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
  114. package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
  115. package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
  116. package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
  117. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
  119. package/dist/clear-cache.html +85 -0
  120. package/dist/convert-icons.md +53 -0
  121. package/dist/dr-claw.png +0 -0
  122. package/dist/favicon.png +0 -0
  123. package/dist/generate-icons.js +49 -0
  124. package/dist/icons/claude-ai-icon.svg +1 -0
  125. package/dist/icons/codex-white.svg +3 -0
  126. package/dist/icons/codex.svg +3 -0
  127. package/dist/icons/cursor-white.svg +12 -0
  128. package/dist/icons/cursor.svg +1 -0
  129. package/dist/icons/file.svg +16997 -0
  130. package/dist/icons/generate-icons.md +19 -0
  131. package/dist/icons/icon-128x128.png +0 -0
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-152x152.png +0 -0
  134. package/dist/icons/icon-192x192.png +0 -0
  135. package/dist/icons/icon-384x384.png +0 -0
  136. package/dist/icons/icon-512x512.png +0 -0
  137. package/dist/icons/icon-72x72.png +0 -0
  138. package/dist/icons/icon-96x96.png +0 -0
  139. package/dist/icons/news/arxiv.svg +1 -0
  140. package/dist/icons/news/huggingface.svg +8 -0
  141. package/dist/icons/news/x-black.png +0 -0
  142. package/dist/icons/news/x-white.png +0 -0
  143. package/dist/icons/news/xiaohongshu.png +0 -0
  144. package/dist/index.html +51 -0
  145. package/dist/leveraged-cognition.png +0 -0
  146. package/dist/manifest.json +61 -0
  147. package/dist/screenshots/chat.png +0 -0
  148. package/dist/screenshots/new_claw.png +0 -0
  149. package/dist/screenshots/news_dashboard.png +0 -0
  150. package/dist/screenshots/project_dashboard.png +0 -0
  151. package/dist/screenshots/research_lab.png +0 -0
  152. package/dist/screenshots/skill_library.png +0 -0
  153. package/dist/screenshots/task_list.png +0 -0
  154. package/dist/screenshots/terminal_example.png +0 -0
  155. package/dist/screenshots/terminal_example1.png +0 -0
  156. package/dist/sw.js +58 -0
  157. package/dist/wechat-group-qr.jpg +0 -0
  158. package/package.json +133 -0
  159. package/public/api-docs.html +878 -0
  160. package/public/clear-cache.html +85 -0
  161. package/public/convert-icons.md +53 -0
  162. package/public/dr-claw.png +0 -0
  163. package/public/favicon.png +0 -0
  164. package/public/generate-icons.js +49 -0
  165. package/public/icons/claude-ai-icon.svg +1 -0
  166. package/public/icons/codex-white.svg +3 -0
  167. package/public/icons/codex.svg +3 -0
  168. package/public/icons/cursor-white.svg +12 -0
  169. package/public/icons/cursor.svg +1 -0
  170. package/public/icons/file.svg +16997 -0
  171. package/public/icons/generate-icons.md +19 -0
  172. package/public/icons/icon-128x128.png +0 -0
  173. package/public/icons/icon-144x144.png +0 -0
  174. package/public/icons/icon-152x152.png +0 -0
  175. package/public/icons/icon-192x192.png +0 -0
  176. package/public/icons/icon-384x384.png +0 -0
  177. package/public/icons/icon-512x512.png +0 -0
  178. package/public/icons/icon-72x72.png +0 -0
  179. package/public/icons/icon-96x96.png +0 -0
  180. package/public/icons/news/arxiv.svg +1 -0
  181. package/public/icons/news/huggingface.svg +8 -0
  182. package/public/icons/news/x-black.png +0 -0
  183. package/public/icons/news/x-white.png +0 -0
  184. package/public/icons/news/xiaohongshu.png +0 -0
  185. package/public/leveraged-cognition.png +0 -0
  186. package/public/manifest.json +61 -0
  187. package/public/screenshots/chat.png +0 -0
  188. package/public/screenshots/new_claw.png +0 -0
  189. package/public/screenshots/news_dashboard.png +0 -0
  190. package/public/screenshots/project_dashboard.png +0 -0
  191. package/public/screenshots/research_lab.png +0 -0
  192. package/public/screenshots/skill_library.png +0 -0
  193. package/public/screenshots/task_list.png +0 -0
  194. package/public/screenshots/terminal_example.png +0 -0
  195. package/public/screenshots/terminal_example1.png +0 -0
  196. package/public/sw.js +58 -0
  197. package/public/wechat-group-qr.jpg +0 -0
  198. package/scripts/export-skills-catalog-v2.mjs +746 -0
  199. package/scripts/fix-node-pty.js +45 -0
  200. package/scripts/gemini_image_core.py +470 -0
  201. package/scripts/install-python-tools.js +64 -0
  202. package/server/AGENTS.md +73 -0
  203. package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
  204. package/server/__tests__/runtimePorts.test.mjs +115 -0
  205. package/server/__tests__/session-delete.test.mjs +166 -0
  206. package/server/bin/compute-node +70 -0
  207. package/server/claude-sdk.js +799 -0
  208. package/server/cli-chat.js +488 -0
  209. package/server/cli.js +354 -0
  210. package/server/compute-node.js +439 -0
  211. package/server/constants/config.js +5 -0
  212. package/server/cursor-cli.js +340 -0
  213. package/server/database/auth.db +0 -0
  214. package/server/database/db.js +1852 -0
  215. package/server/database/init.sql +194 -0
  216. package/server/gemini-cli.js +1352 -0
  217. package/server/index.js +3154 -0
  218. package/server/load-env.js +71 -0
  219. package/server/local-gpu.js +860 -0
  220. package/server/middleware/auth.js +100 -0
  221. package/server/openai-codex.js +724 -0
  222. package/server/openrouter.js +890 -0
  223. package/server/project-token-usage.js +407 -0
  224. package/server/projects.js +3973 -0
  225. package/server/routes/agent.js +1272 -0
  226. package/server/routes/auth.js +128 -0
  227. package/server/routes/auto-research.js +558 -0
  228. package/server/routes/cli-auth.js +958 -0
  229. package/server/routes/codex.js +344 -0
  230. package/server/routes/commands.js +601 -0
  231. package/server/routes/compute.js +358 -0
  232. package/server/routes/cursor.js +795 -0
  233. package/server/routes/git.js +1348 -0
  234. package/server/routes/mcp-utils.js +48 -0
  235. package/server/routes/mcp.js +618 -0
  236. package/server/routes/news.js +711 -0
  237. package/server/routes/projects.js +633 -0
  238. package/server/routes/references.js +338 -0
  239. package/server/routes/settings.js +285 -0
  240. package/server/routes/skills.js +914 -0
  241. package/server/routes/taskmaster.js +2566 -0
  242. package/server/routes/telemetry.js +89 -0
  243. package/server/routes/user.js +155 -0
  244. package/server/scripts/fix_session_names.js +57 -0
  245. package/server/scripts/research-news/common_words.py +19 -0
  246. package/server/scripts/research-news/link_keywords.py +298 -0
  247. package/server/scripts/research-news/scan_existing_notes.py +266 -0
  248. package/server/scripts/research-news/scoring_utils.py +227 -0
  249. package/server/scripts/research-news/search_arxiv.py +842 -0
  250. package/server/scripts/research-news/search_huggingface.py +438 -0
  251. package/server/scripts/research-news/search_x.py +490 -0
  252. package/server/scripts/research-news/search_xiaohongshu.py +610 -0
  253. package/server/taskmaster-templates/README.md +150 -0
  254. package/server/taskmaster-templates/README.zh-CN.md +150 -0
  255. package/server/taskmaster-templates/ai-research-dataset.json +230 -0
  256. package/server/taskmaster-templates/ai-research-method-model.json +230 -0
  257. package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
  258. package/server/telemetry.js +121 -0
  259. package/server/templates/AGENTS.md +103 -0
  260. package/server/templates/CLAUDE.md +104 -0
  261. package/server/templates/GEMINI.md +103 -0
  262. package/server/templates/cursor-project.md +108 -0
  263. package/server/templates/index.js +37 -0
  264. package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
  265. package/server/utils/auto-research-mailer.js +57 -0
  266. package/server/utils/cliResolution.js +170 -0
  267. package/server/utils/commandParser.js +303 -0
  268. package/server/utils/cursorCommand.js +61 -0
  269. package/server/utils/geminiApiKey.js +28 -0
  270. package/server/utils/gitConfig.js +24 -0
  271. package/server/utils/imageAttachmentFiles.js +35 -0
  272. package/server/utils/mcp-detector.js +198 -0
  273. package/server/utils/parsers/bibtex-parser.js +207 -0
  274. package/server/utils/permissions.js +100 -0
  275. package/server/utils/runtimePorts.js +212 -0
  276. package/server/utils/sessionFormatting.js +68 -0
  277. package/server/utils/sessionIndex.js +100 -0
  278. package/server/utils/sessionMode.js +95 -0
  279. package/server/utils/sessionTokenUsage.js +105 -0
  280. package/server/utils/taskmaster-websocket.js +129 -0
  281. package/server/utils/zotero-client.js +136 -0
  282. package/shared/errorClassifier.js +102 -0
  283. package/shared/geminiThinkingSupport.d.ts +22 -0
  284. package/shared/geminiThinkingSupport.js +96 -0
  285. package/shared/geminiThoughtParser.d.ts +8 -0
  286. package/shared/geminiThoughtParser.js +30 -0
  287. package/shared/modelConstants.js +139 -0
@@ -0,0 +1,799 @@
1
+ /**
2
+ * Claude SDK Integration
3
+ *
4
+ * This module provides SDK-based integration with Claude using the @anthropic-ai/claude-agent-sdk.
5
+ * It mirrors the interface of claude-cli.js but uses the SDK internally for better performance
6
+ * and maintainability.
7
+ *
8
+ * Key features:
9
+ * - Direct SDK integration without child processes
10
+ * - Session management with abort capability
11
+ * - Options mapping between CLI and SDK formats
12
+ * - WebSocket message streaming
13
+ */
14
+
15
+ import { query } from '@anthropic-ai/claude-agent-sdk';
16
+ import crypto from 'crypto';
17
+ import { promises as fs } from 'fs';
18
+ import path from 'path';
19
+ import os from 'os';
20
+ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
21
+ import { classifyError, classifySDKError } from '../shared/errorClassifier.js';
22
+ import { encodeProjectPath, ensureProjectSkillLinks, reconcileClaudeSessionIndex } from './projects.js';
23
+ import { writeProjectTemplates } from './templates/index.js';
24
+ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
25
+ import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
26
+
27
+ import { createRequestId, waitForToolApproval, resolveToolApproval as resolvePermApproval, matchesToolPermission } from './utils/permissions.js';
28
+
29
+ const activeSessions = new Map();
30
+ const pendingClaudeSessionIndexReconciles = new Map();
31
+
32
+ const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
33
+
34
+ function resolveToolApproval(requestId, decision) {
35
+ resolvePermApproval(requestId, decision);
36
+ }
37
+
38
+ function scheduleClaudeSessionIndexReconcile(projectPath, sessionId, delayMs = 1000) {
39
+ if (!projectPath || !sessionId) {
40
+ return;
41
+ }
42
+
43
+ const existingTimer = pendingClaudeSessionIndexReconciles.get(sessionId);
44
+ if (existingTimer) {
45
+ clearTimeout(existingTimer);
46
+ }
47
+
48
+ const timeoutId = setTimeout(async () => {
49
+ pendingClaudeSessionIndexReconciles.delete(sessionId);
50
+ try {
51
+ await reconcileClaudeSessionIndex(encodeProjectPath(projectPath), sessionId);
52
+ } catch (error) {
53
+ console.warn(`[Claude] Failed to reconcile indexed session ${sessionId}:`, error.message);
54
+ }
55
+ }, delayMs);
56
+
57
+ pendingClaudeSessionIndexReconciles.set(sessionId, timeoutId);
58
+ }
59
+
60
+ async function flushClaudeSessionIndexReconcile(projectPath, sessionId) {
61
+ if (!projectPath || !sessionId) {
62
+ return;
63
+ }
64
+
65
+ const existingTimer = pendingClaudeSessionIndexReconciles.get(sessionId);
66
+ if (existingTimer) {
67
+ clearTimeout(existingTimer);
68
+ pendingClaudeSessionIndexReconciles.delete(sessionId);
69
+ }
70
+
71
+ try {
72
+ await reconcileClaudeSessionIndex(encodeProjectPath(projectPath), sessionId);
73
+ } catch (error) {
74
+ console.warn(`[Claude] Failed to flush indexed session ${sessionId}:`, error.message);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Maps CLI options to SDK-compatible options format
80
+ * @param {Object} options - CLI options
81
+ * @returns {Object} SDK-compatible options
82
+ */
83
+ function mapCliOptionsToSDK(options = {}) {
84
+ const { sessionId, cwd, toolsSettings, permissionMode, images, env } = options;
85
+
86
+ const sdkOptions = {};
87
+
88
+ // Map working directory
89
+ if (cwd) {
90
+ sdkOptions.cwd = cwd;
91
+ }
92
+
93
+ if (env) {
94
+ sdkOptions.env = env;
95
+ }
96
+
97
+ // Map permission mode
98
+ if (permissionMode && permissionMode !== 'default') {
99
+ sdkOptions.permissionMode = permissionMode;
100
+ }
101
+
102
+ // Skip the interactive trust/bypass-permissions dialogs that the CLI shows on
103
+ // first launch in a new directory. These Ink prompts require a TTY and will
104
+ // hang when the SDK is used headlessly from a server process.
105
+ sdkOptions.allowDangerouslySkipPermissions = false;
106
+
107
+ // Map tool settings
108
+ const settings = toolsSettings || {
109
+ allowedTools: [],
110
+ disallowedTools: [],
111
+ skipPermissions: false
112
+ };
113
+
114
+ // Handle tool permissions
115
+ if (settings.skipPermissions && permissionMode !== 'plan') {
116
+ // When skipping permissions, use bypassPermissions mode
117
+ sdkOptions.permissionMode = 'bypassPermissions';
118
+ }
119
+
120
+ let allowedTools = [...(settings.allowedTools || [])];
121
+
122
+ // Add plan mode default tools
123
+ if (permissionMode === 'plan') {
124
+ const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
125
+ for (const tool of planModeTools) {
126
+ if (!allowedTools.includes(tool)) {
127
+ allowedTools.push(tool);
128
+ }
129
+ }
130
+ }
131
+
132
+ sdkOptions.allowedTools = allowedTools;
133
+
134
+ // Use the tools preset to make all default built-in tools available (including AskUserQuestion).
135
+ // This was introduced in SDK 0.1.57. Omitting this preserves existing behavior (all tools available),
136
+ // but being explicit ensures forward compatibility and clarity.
137
+ sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
138
+
139
+ sdkOptions.disallowedTools = settings.disallowedTools || [];
140
+
141
+ // Map model (default to sonnet)
142
+ // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
143
+ sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
144
+ console.log(`Using model: ${sdkOptions.model}`);
145
+
146
+ // Map system prompt configuration
147
+ sdkOptions.systemPrompt = {
148
+ type: 'preset',
149
+ preset: 'claude_code' // Required to use CLAUDE.md
150
+ };
151
+
152
+ // Map setting sources for CLAUDE.md loading
153
+ // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
154
+ sdkOptions.settingSources = ['project', 'user', 'local'];
155
+
156
+ // Map resume session
157
+ if (sessionId) {
158
+ sdkOptions.resume = sessionId;
159
+ }
160
+
161
+ return sdkOptions;
162
+ }
163
+
164
+ /**
165
+ * Adds a session to the active sessions map
166
+ * @param {string} sessionId - Session identifier
167
+ * @param {Object} queryInstance - SDK query instance
168
+ * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
169
+ * @param {string} tempDir - Temp directory for cleanup
170
+ */
171
+ function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
172
+ activeSessions.set(sessionId, {
173
+ instance: queryInstance,
174
+ startTime: Date.now(),
175
+ status: 'active',
176
+ tempImagePaths,
177
+ tempDir
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Removes a session from the active sessions map
183
+ * @param {string} sessionId - Session identifier
184
+ */
185
+ function removeSession(sessionId) {
186
+ activeSessions.delete(sessionId);
187
+ }
188
+
189
+ /**
190
+ * Gets a session from the active sessions map
191
+ * @param {string} sessionId - Session identifier
192
+ * @returns {Object|undefined} Session data or undefined
193
+ */
194
+ function getSession(sessionId) {
195
+ return activeSessions.get(sessionId);
196
+ }
197
+
198
+ /**
199
+ * Gets all active session IDs
200
+ * @returns {Array<string>} Array of active session IDs
201
+ */
202
+ function getAllSessions() {
203
+ return Array.from(activeSessions.keys());
204
+ }
205
+
206
+ /**
207
+ * Transforms SDK messages to WebSocket format expected by frontend
208
+ * @param {Object} sdkMessage - SDK message object
209
+ * @returns {Object} Transformed message ready for WebSocket
210
+ */
211
+ function transformMessage(sdkMessage) {
212
+ // Extract parent_tool_use_id for subagent tool grouping
213
+ if (sdkMessage.parent_tool_use_id) {
214
+ return {
215
+ ...sdkMessage,
216
+ parentToolUseId: sdkMessage.parent_tool_use_id
217
+ };
218
+ }
219
+ return sdkMessage;
220
+ }
221
+
222
+ /**
223
+ * Extracts token budget from the last assistant message's usage data.
224
+ * This gives us per-API-call input tokens, which represents the actual
225
+ * context window fill level (not cumulative across the agentic turn).
226
+ * @param {Object|null} usage - usage object from assistant message (message.usage)
227
+ * @returns {Object|null} Token budget object or null
228
+ */
229
+ function extractTokenBudgetFromUsage(usage) {
230
+ if (!usage) {
231
+ return null;
232
+ }
233
+
234
+ // In Claude API: input_tokens is the non-cached portion.
235
+ // Total context = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
236
+ const inputTokens = usage.input_tokens || 0;
237
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
238
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
239
+ const totalUsed = inputTokens + cacheReadTokens + cacheCreationTokens;
240
+
241
+ // Context window is the model's input limit
242
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 200000;
243
+
244
+ console.log(`Token calculation: input=${inputTokens}, cacheRead=${cacheReadTokens}, cacheCreation=${cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
245
+
246
+ return {
247
+ used: totalUsed,
248
+ total: contextWindow
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Handles image processing for SDK queries
254
+ * Saves base64 images to temporary files and returns modified prompt with file paths
255
+ * @param {string} command - Original user prompt
256
+ * @param {Array} images - Array of image objects with base64 data
257
+ * @param {string} cwd - Working directory for temp file creation
258
+ * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
259
+ */
260
+ async function handleImages(command, images, cwd) {
261
+ const tempImagePaths = [];
262
+ let tempDir = null;
263
+
264
+ if (!images || images.length === 0) {
265
+ return { modifiedCommand: command, tempImagePaths, tempDir };
266
+ }
267
+
268
+ try {
269
+ // Create temp directory in the project directory
270
+ const workingDir = cwd || process.cwd();
271
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
272
+ await fs.mkdir(tempDir, { recursive: true });
273
+
274
+ // Save each image to a temp file
275
+ for (const [index, image] of images.entries()) {
276
+ // Extract base64 data and mime type
277
+ const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
278
+ if (!matches) {
279
+ console.error('Invalid image data format');
280
+ continue;
281
+ }
282
+
283
+ const [, , base64Data] = matches;
284
+ const filename = buildTempAttachmentFilename(index, image?.name, matches[1]);
285
+ const filepath = path.join(tempDir, filename);
286
+
287
+ // Write base64 data to file
288
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
289
+ tempImagePaths.push(filepath);
290
+ }
291
+
292
+ // Include the full image paths in the prompt
293
+ let modifiedCommand = command;
294
+ if (tempImagePaths.length > 0 && command && command.trim()) {
295
+ const imageNote = `\n\n[Files provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
296
+ modifiedCommand = command + imageNote;
297
+ }
298
+
299
+ console.log(`Processed ${tempImagePaths.length} images to temp directory: ${tempDir}`);
300
+ return { modifiedCommand, tempImagePaths, tempDir };
301
+ } catch (error) {
302
+ console.error('Error processing images for SDK:', error);
303
+ return { modifiedCommand: command, tempImagePaths, tempDir };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Cleans up temporary image files
309
+ * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
310
+ * @param {string} tempDir - Temp directory to remove
311
+ */
312
+ async function cleanupTempFiles(tempImagePaths, tempDir) {
313
+ if (!tempImagePaths || tempImagePaths.length === 0) {
314
+ return;
315
+ }
316
+
317
+ try {
318
+ // Delete individual temp files
319
+ for (const imagePath of tempImagePaths) {
320
+ await fs.unlink(imagePath).catch(err =>
321
+ console.error(`Failed to delete temp image ${imagePath}:`, err)
322
+ );
323
+ }
324
+
325
+ // Delete temp directory
326
+ if (tempDir) {
327
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(err =>
328
+ console.error(`Failed to delete temp directory ${tempDir}:`, err)
329
+ );
330
+ }
331
+
332
+ console.log(`Cleaned up ${tempImagePaths.length} temp image files`);
333
+ } catch (error) {
334
+ console.error('Error during temp file cleanup:', error);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Loads MCP server configurations from ~/.claude.json
340
+ * @param {string} cwd - Current working directory for project-specific configs
341
+ * @returns {Object|null} MCP servers object or null if none found
342
+ */
343
+ async function loadMcpConfig(cwd) {
344
+ try {
345
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
346
+
347
+ // Check if config file exists
348
+ try {
349
+ await fs.access(claudeConfigPath);
350
+ } catch (error) {
351
+ // File doesn't exist, return null
352
+ console.log('No ~/.claude.json found, proceeding without MCP servers');
353
+ return null;
354
+ }
355
+
356
+ // Read and parse config file
357
+ let claudeConfig;
358
+ try {
359
+ const configContent = await fs.readFile(claudeConfigPath, 'utf8');
360
+ claudeConfig = JSON.parse(configContent);
361
+ } catch (error) {
362
+ console.error('Failed to parse ~/.claude.json:', error.message);
363
+ return null;
364
+ }
365
+
366
+ // Extract MCP servers (merge global and project-specific)
367
+ let mcpServers = {};
368
+
369
+ // Add global MCP servers
370
+ if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
371
+ mcpServers = { ...claudeConfig.mcpServers };
372
+ console.log(`Loaded ${Object.keys(mcpServers).length} global MCP servers`);
373
+ }
374
+
375
+ // Add/override with project-specific MCP servers
376
+ if (claudeConfig.claudeProjects && cwd) {
377
+ const projectConfig = claudeConfig.claudeProjects[cwd];
378
+ if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
379
+ mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
380
+ console.log(`Loaded ${Object.keys(projectConfig.mcpServers).length} project-specific MCP servers`);
381
+ }
382
+ }
383
+
384
+ // Dr. Claw no longer relies on TaskMaster MCP tooling in chat sessions.
385
+ // Filter out any TaskMaster-related MCP entries to prevent legacy "not installed" errors.
386
+ const filteredMcpServers = Object.fromEntries(
387
+ Object.entries(mcpServers).filter(([name, config]) => {
388
+ const normalizedName = String(name || '').toLowerCase();
389
+ const command = String(config?.command || '').toLowerCase();
390
+ const argsJoined = Array.isArray(config?.args)
391
+ ? config.args.map((arg) => String(arg || '').toLowerCase()).join(' ')
392
+ : '';
393
+
394
+ const isTaskMasterServer =
395
+ normalizedName.includes('task-master') ||
396
+ normalizedName.includes('taskmaster') ||
397
+ command.includes('task-master') ||
398
+ command.includes('taskmaster') ||
399
+ argsJoined.includes('task-master') ||
400
+ argsJoined.includes('taskmaster');
401
+
402
+ return !isTaskMasterServer;
403
+ })
404
+ );
405
+
406
+ // Return null if no servers found
407
+ if (Object.keys(filteredMcpServers).length === 0) {
408
+ console.log('No MCP servers configured');
409
+ return null;
410
+ }
411
+
412
+ if (Object.keys(filteredMcpServers).length !== Object.keys(mcpServers).length) {
413
+ console.log(
414
+ `Filtered legacy TaskMaster MCP servers: ${Object.keys(mcpServers).length - Object.keys(filteredMcpServers).length}`
415
+ );
416
+ }
417
+
418
+ console.log(`Total MCP servers loaded: ${Object.keys(filteredMcpServers).length}`);
419
+ return filteredMcpServers;
420
+ } catch (error) {
421
+ console.error('Error loading MCP config:', error.message);
422
+ return null;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Executes a Claude query using the SDK
428
+ * @param {string} command - User prompt/command
429
+ * @param {Object} options - Query options
430
+ * @param {Object} ws - WebSocket connection
431
+ * @returns {Promise<void>}
432
+ */
433
+ async function queryClaudeSDK(command, options = {}, ws) {
434
+ const { sessionId, sessionMode, stageTagKeys, stageTagSource = 'task_context' } = options;
435
+ let capturedSessionId = sessionId;
436
+ let sessionCreatedSent = false;
437
+ let tempImagePaths = [];
438
+ let tempDir = null;
439
+ const sessionProjectPath = options.cwd || options.projectPath || null;
440
+
441
+ try {
442
+ // Synchronous (better-sqlite3) — no await needed.
443
+ if (sessionId && sessionProjectPath) {
444
+ applyStageTagsToSession({
445
+ sessionId,
446
+ projectPath: sessionProjectPath,
447
+ stageTagKeys,
448
+ source: stageTagSource,
449
+ });
450
+ }
451
+
452
+ // Ensure skills symlinks and CLAUDE.md template exist in the project directory
453
+ const projectDir = options.cwd || options.projectPath;
454
+ if (projectDir) {
455
+ try {
456
+ await ensureProjectSkillLinks(projectDir);
457
+ await writeProjectTemplates(projectDir);
458
+ } catch (err) {
459
+ console.warn('[claude-sdk] Failed to initialize project skills/templates:', err.message);
460
+ }
461
+ }
462
+
463
+ // Map CLI options to SDK format
464
+ const sdkOptions = mapCliOptionsToSDK(options);
465
+
466
+ // Load MCP configuration
467
+ const mcpServers = await loadMcpConfig(options.cwd);
468
+ if (mcpServers) {
469
+ sdkOptions.mcpServers = mcpServers;
470
+ }
471
+
472
+ // Handle images - save to temp files and modify prompt
473
+ const imageResult = await handleImages(command, options.images, options.cwd);
474
+ const finalCommand = imageResult.modifiedCommand;
475
+ tempImagePaths = imageResult.tempImagePaths;
476
+ tempDir = imageResult.tempDir;
477
+
478
+ sdkOptions.canUseTool = async (toolName, input, context) => {
479
+ const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
480
+
481
+ if (!requiresInteraction) {
482
+ if (sdkOptions.permissionMode === 'bypassPermissions') {
483
+ return { behavior: 'allow', updatedInput: input };
484
+ }
485
+
486
+ const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
487
+ matchesToolPermission(entry, toolName, input)
488
+ );
489
+ if (isDisallowed) {
490
+ return { behavior: 'deny', message: 'Tool disallowed by settings' };
491
+ }
492
+
493
+ const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
494
+ matchesToolPermission(entry, toolName, input)
495
+ );
496
+ if (isAllowed) {
497
+ return { behavior: 'allow', updatedInput: input };
498
+ }
499
+ }
500
+
501
+ const requestId = createRequestId();
502
+ ws.send({
503
+ type: 'claude-permission-request',
504
+ requestId,
505
+ toolName,
506
+ input,
507
+ sessionId: capturedSessionId || sessionId || null
508
+ });
509
+
510
+ const decision = await waitForToolApproval(requestId, {
511
+ timeoutMs: requiresInteraction ? 0 : undefined,
512
+ signal: context?.signal,
513
+ onCancel: (reason) => {
514
+ ws.send({
515
+ type: 'claude-permission-cancelled',
516
+ requestId,
517
+ reason,
518
+ sessionId: capturedSessionId || sessionId || null
519
+ });
520
+ }
521
+ });
522
+ if (!decision) {
523
+ return { behavior: 'deny', message: 'Permission request timed out' };
524
+ }
525
+
526
+ if (decision.cancelled) {
527
+ return { behavior: 'deny', message: 'Permission request cancelled' };
528
+ }
529
+
530
+ if (decision.allow) {
531
+ if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
532
+ if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
533
+ sdkOptions.allowedTools.push(decision.rememberEntry);
534
+ }
535
+ if (Array.isArray(sdkOptions.disallowedTools)) {
536
+ sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
537
+ }
538
+ }
539
+ return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
540
+ }
541
+
542
+ return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
543
+ };
544
+
545
+ // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
546
+ const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
547
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
548
+
549
+ const queryInstance = query({
550
+ prompt: finalCommand,
551
+ options: sdkOptions
552
+ });
553
+
554
+ // Restore immediately — Query constructor already captured the value
555
+ if (prevStreamTimeout !== undefined) {
556
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
557
+ } else {
558
+ delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
559
+ }
560
+
561
+ // Track the query instance for abort capability
562
+ if (capturedSessionId) {
563
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
564
+ }
565
+
566
+ // Process streaming messages
567
+ // Track the latest assistant message's usage to get per-API-call context window usage
568
+ let lastAssistantUsage = null;
569
+ console.log('Starting async generator loop for session:', capturedSessionId || 'NEW');
570
+ for await (const message of queryInstance) {
571
+ // Capture session ID from first message
572
+ if (message.session_id && !capturedSessionId) {
573
+
574
+ capturedSessionId = message.session_id;
575
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
576
+
577
+ // Set session ID on writer
578
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
579
+ ws.setSessionId(capturedSessionId);
580
+ }
581
+
582
+ // Send session-created event only once for new sessions
583
+ if (!sessionId && !sessionCreatedSent) {
584
+ sessionCreatedSent = true;
585
+ if (options.cwd || options.projectPath) {
586
+ recordIndexedSession({
587
+ sessionId: capturedSessionId,
588
+ provider: 'claude',
589
+ projectPath: options.cwd || options.projectPath,
590
+ sessionMode: sessionMode || 'research',
591
+ stageTagKeys,
592
+ tagSource: stageTagSource,
593
+ });
594
+ }
595
+ ws.send({
596
+ type: 'session-created',
597
+ sessionId: capturedSessionId,
598
+ provider: 'claude',
599
+ mode: sessionMode || 'research'
600
+ });
601
+ } else {
602
+ console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
603
+ }
604
+ } else {
605
+ console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
606
+ }
607
+
608
+ // Track usage from assistant messages (per-API-call, not cumulative)
609
+ if (message.type === 'assistant' && message.message?.usage) {
610
+ lastAssistantUsage = message.message.usage;
611
+ }
612
+
613
+ // Detect SDK-level errors on assistant messages (e.g. rate_limit, authentication_failed)
614
+ // These come as structured enum values, not in the catch block.
615
+ if (message.type === 'assistant' && message.error) {
616
+ const { errorType, isRetryable } = classifySDKError(message.error, 'claude');
617
+ ws.send({
618
+ type: 'claude-error',
619
+ error: message.error,
620
+ errorType,
621
+ isRetryable,
622
+ sessionId: capturedSessionId || sessionId || null,
623
+ });
624
+ }
625
+
626
+ // Transform and send message to WebSocket
627
+ const transformedMessage = transformMessage(message);
628
+ const sessionData = capturedSessionId ? getSession(capturedSessionId) : null;
629
+ ws.send({
630
+ type: 'claude-response',
631
+ data: {
632
+ ...transformedMessage,
633
+ startTime: sessionData?.startTime
634
+ },
635
+ sessionId: capturedSessionId || sessionId || null
636
+ });
637
+
638
+ if (
639
+ capturedSessionId &&
640
+ sessionProjectPath &&
641
+ (message.type === 'assistant' || message.type === 'result')
642
+ ) {
643
+ scheduleClaudeSessionIndexReconcile(sessionProjectPath, capturedSessionId);
644
+ }
645
+
646
+ // Send token budget update when the turn completes
647
+ if (message.type === 'result') {
648
+ const tokenBudget = extractTokenBudgetFromUsage(lastAssistantUsage);
649
+ if (tokenBudget) {
650
+ console.log('Token budget from last assistant usage:', tokenBudget);
651
+ ws.send({
652
+ type: 'token-budget',
653
+ data: tokenBudget,
654
+ sessionId: capturedSessionId || sessionId || null
655
+ });
656
+ }
657
+ }
658
+ }
659
+
660
+ // Send completion event before removing session to avoid race with abort requests
661
+ console.log('Streaming complete, sending claude-complete event');
662
+ ws.send({
663
+ type: 'claude-complete',
664
+ sessionId: capturedSessionId,
665
+ exitCode: 0,
666
+ isNewSession: !sessionId && !!command
667
+ });
668
+ console.log('claude-complete event sent');
669
+
670
+ // Keep post-run housekeeping out of the completion critical path so the UI
671
+ // can settle immediately after the model finishes streaming.
672
+ const completionTasks = [];
673
+ if (capturedSessionId) {
674
+ removeSession(capturedSessionId);
675
+ completionTasks.push(flushClaudeSessionIndexReconcile(sessionProjectPath, capturedSessionId));
676
+ }
677
+ completionTasks.push(cleanupTempFiles(tempImagePaths, tempDir));
678
+ await Promise.allSettled(completionTasks);
679
+
680
+ } catch (error) {
681
+ console.error('SDK query error:', error);
682
+
683
+ // Record session before cleanup so it appears in sidebar even on early errors
684
+ if (capturedSessionId && !sessionId && !sessionCreatedSent && (options.cwd || options.projectPath)) {
685
+ sessionCreatedSent = true;
686
+ recordIndexedSession({
687
+ sessionId: capturedSessionId,
688
+ provider: 'claude',
689
+ projectPath: options.cwd || options.projectPath,
690
+ sessionMode: sessionMode || 'research',
691
+ });
692
+ ws.send({
693
+ type: 'session-created',
694
+ sessionId: capturedSessionId,
695
+ provider: 'claude',
696
+ mode: sessionMode || 'research',
697
+ });
698
+ }
699
+
700
+ // Clean up session on error
701
+ if (capturedSessionId) {
702
+ removeSession(capturedSessionId);
703
+ }
704
+
705
+ const { errorType, isRetryable } = classifyError(error.message);
706
+
707
+ ws.send({
708
+ type: 'claude-error',
709
+ error: error.message,
710
+ errorType,
711
+ isRetryable,
712
+ sessionId: capturedSessionId || sessionId || null
713
+ });
714
+
715
+ const errorTasks = [];
716
+ if (capturedSessionId) {
717
+ errorTasks.push(flushClaudeSessionIndexReconcile(sessionProjectPath, capturedSessionId));
718
+ }
719
+ errorTasks.push(cleanupTempFiles(tempImagePaths, tempDir));
720
+ await Promise.allSettled(errorTasks);
721
+
722
+ throw error;
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Aborts an active SDK session
728
+ * @param {string} sessionId - Session identifier
729
+ * @returns {boolean} True if session was aborted, false if not found
730
+ */
731
+ async function abortClaudeSDKSession(sessionId) {
732
+ const session = getSession(sessionId);
733
+
734
+ if (!session) {
735
+ console.log(`Session ${sessionId} not found`);
736
+ return false;
737
+ }
738
+
739
+ try {
740
+ console.log(`Aborting SDK session: ${sessionId}`);
741
+
742
+ // interrupt() can hang if the subprocess is unresponsive; race against a timeout
743
+ await Promise.race([
744
+ session.instance.interrupt(),
745
+ new Promise((_, reject) => setTimeout(() => reject(new Error('interrupt timed out after 5s')), 5000))
746
+ ]);
747
+
748
+ session.status = 'aborted';
749
+ await cleanupTempFiles(session.tempImagePaths, session.tempDir);
750
+ removeSession(sessionId);
751
+
752
+ return true;
753
+ } catch (error) {
754
+ console.error(`Error aborting session ${sessionId}:`, error);
755
+ // Still clean up the session even if interrupt failed/timed out
756
+ session.status = 'aborted';
757
+ removeSession(sessionId);
758
+ cleanupTempFiles(session.tempImagePaths, session.tempDir).catch(() => {});
759
+ return false;
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Checks if an SDK session is currently active
765
+ * @param {string} sessionId - Session identifier
766
+ * @returns {boolean} True if session is active
767
+ */
768
+ function isClaudeSDKSessionActive(sessionId) {
769
+ const session = getSession(sessionId);
770
+ return session && session.status === 'active';
771
+ }
772
+
773
+ /**
774
+ * Gets the start time of an SDK session
775
+ * @param {string} sessionId - Session identifier
776
+ * @returns {number|null} Start time in ms or null
777
+ */
778
+ function getClaudeSDKSessionStartTime(sessionId) {
779
+ const session = getSession(sessionId);
780
+ return session ? session.startTime : null;
781
+ }
782
+
783
+ /**
784
+ * Gets all active SDK session IDs
785
+ * @returns {Array<string>} Array of active session IDs
786
+ */
787
+ function getActiveClaudeSDKSessions() {
788
+ return getAllSessions();
789
+ }
790
+
791
+ // Export public API
792
+ export {
793
+ queryClaudeSDK,
794
+ abortClaudeSDKSession,
795
+ isClaudeSDKSessionActive,
796
+ getClaudeSDKSessionStartTime,
797
+ getActiveClaudeSDKSessions,
798
+ resolveToolApproval
799
+ };