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,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { splitLegacyGeminiThoughtContent } from '../../shared/geminiThoughtParser.js';
3
+ import { normalizePersistedGeminiAssistantEntries } from '../gemini-cli.js';
4
+
5
+ describe('splitLegacyGeminiThoughtContent (server)', () => {
6
+ it('returns null for plain text without markers', () => {
7
+ expect(splitLegacyGeminiThoughtContent('Hello world')).toBeNull();
8
+ });
9
+
10
+ it('returns null for non-string input', () => {
11
+ expect(splitLegacyGeminiThoughtContent(null)).toBeNull();
12
+ expect(splitLegacyGeminiThoughtContent(undefined)).toBeNull();
13
+ expect(splitLegacyGeminiThoughtContent(42)).toBeNull();
14
+ });
15
+
16
+ it('returns null for empty string', () => {
17
+ expect(splitLegacyGeminiThoughtContent('')).toBeNull();
18
+ });
19
+
20
+ it('returns null when marker produces fewer than 2 segments', () => {
21
+ expect(splitLegacyGeminiThoughtContent('[Thought: true]')).toBeNull();
22
+ });
23
+
24
+ it('splits single marker into thinking + final', () => {
25
+ const result = splitLegacyGeminiThoughtContent('thinking\n[Thought: true]\nfinal');
26
+ expect(result).toEqual([
27
+ { content: 'thinking', isThinking: true },
28
+ { content: 'final', isThinking: false },
29
+ ]);
30
+ });
31
+
32
+ it('splits multiple markers correctly', () => {
33
+ const result = splitLegacyGeminiThoughtContent(
34
+ 't1\n[Thought: true]\nt2\n[Thought: true]\nfinal'
35
+ );
36
+ expect(result).toEqual([
37
+ { content: 't1', isThinking: true },
38
+ { content: 't2', isThinking: true },
39
+ { content: 'final', isThinking: false },
40
+ ]);
41
+ });
42
+
43
+ it('is case insensitive', () => {
44
+ const result = splitLegacyGeminiThoughtContent('thinking\n[thought: TRUE]\nfinal');
45
+ expect(result).toEqual([
46
+ { content: 'thinking', isThinking: true },
47
+ { content: 'final', isThinking: false },
48
+ ]);
49
+ });
50
+ });
51
+
52
+ describe('normalizePersistedGeminiAssistantEntries', () => {
53
+ it('returns single assistant entry for plain text', () => {
54
+ const result = normalizePersistedGeminiAssistantEntries('Hello world');
55
+ expect(result).toEqual([
56
+ { role: 'assistant', content: 'Hello world', type: 'message' },
57
+ ]);
58
+ });
59
+
60
+ it('splits legacy thought text into thinking + message entries', () => {
61
+ const result = normalizePersistedGeminiAssistantEntries(
62
+ 'thinking content\n[Thought: true]\nfinal answer'
63
+ );
64
+ expect(result).toEqual([
65
+ { role: 'assistant', type: 'thinking', content: 'thinking content' },
66
+ { role: 'assistant', content: 'final answer', type: 'message' },
67
+ ]);
68
+ });
69
+
70
+ it('handles multiple thinking segments', () => {
71
+ const result = normalizePersistedGeminiAssistantEntries(
72
+ 't1\n[Thought: true]\nt2\n[Thought: true]\nfinal'
73
+ );
74
+ expect(result).toEqual([
75
+ { role: 'assistant', type: 'thinking', content: 't1' },
76
+ { role: 'assistant', type: 'thinking', content: 't2' },
77
+ { role: 'assistant', content: 'final', type: 'message' },
78
+ ]);
79
+ });
80
+
81
+ it('preserves non-string content as-is', () => {
82
+ const content = { parts: [{ text: 'hello' }] };
83
+ const result = normalizePersistedGeminiAssistantEntries(content);
84
+ expect(result).toEqual([
85
+ { role: 'assistant', content, type: 'message' },
86
+ ]);
87
+ });
88
+ });
@@ -0,0 +1,115 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ const probeResults = [];
7
+
8
+ vi.mock('net', () => ({
9
+ default: {
10
+ createServer() {
11
+ let errorHandler = null;
12
+
13
+ return {
14
+ once(event, handler) {
15
+ if (event === 'error') {
16
+ errorHandler = handler;
17
+ }
18
+ },
19
+ off(event, handler) {
20
+ if (event === 'error' && errorHandler === handler) {
21
+ errorHandler = null;
22
+ }
23
+ },
24
+ listen(port, host, callback) {
25
+ const nextResult = probeResults.shift();
26
+ if (nextResult instanceof Error) {
27
+ queueMicrotask(() => errorHandler?.(nextResult));
28
+ return;
29
+ }
30
+ queueMicrotask(() => callback());
31
+ },
32
+ close(callback) {
33
+ queueMicrotask(() => callback?.());
34
+ },
35
+ };
36
+ },
37
+ },
38
+ }));
39
+
40
+ const {
41
+ DEFAULT_FRONTEND_PORT,
42
+ getBackendPortSync,
43
+ getFrontendPortSync,
44
+ listenOnAvailablePort,
45
+ setRuntimePortSync,
46
+ } = await import('../utils/runtimePorts.js');
47
+
48
+ const cleanupTasks = [];
49
+
50
+ function createFakeHttpServer() {
51
+ let errorHandler = null;
52
+ let listenedPort = null;
53
+
54
+ return {
55
+ once(event, handler) {
56
+ if (event === 'error') {
57
+ errorHandler = handler;
58
+ }
59
+ },
60
+ off(event, handler) {
61
+ if (event === 'error' && errorHandler === handler) {
62
+ errorHandler = null;
63
+ }
64
+ },
65
+ listen(port, host, callback) {
66
+ listenedPort = port;
67
+ queueMicrotask(() => callback());
68
+ },
69
+ get listenedPort() {
70
+ return listenedPort;
71
+ },
72
+ };
73
+ }
74
+
75
+ afterEach(async () => {
76
+ probeResults.length = 0;
77
+
78
+ while (cleanupTasks.length > 0) {
79
+ const task = cleanupTasks.pop();
80
+ await task();
81
+ }
82
+
83
+ delete process.env.DR_CLAW_RUNTIME_DIR;
84
+ delete process.env.DR_CLAW_RUNTIME_FILE;
85
+ });
86
+
87
+ describe('runtimePorts', () => {
88
+ it('listens on the next available backend port when the requested port is occupied', async () => {
89
+ const busyPortError = new Error('busy');
90
+ busyPortError.code = 'EADDRINUSE';
91
+ probeResults.push(busyPortError, true);
92
+
93
+ const server = createFakeHttpServer();
94
+ const activePort = await listenOnAvailablePort(server, {
95
+ startPort: 3001,
96
+ host: '0.0.0.0',
97
+ maxAttempts: 5,
98
+ });
99
+
100
+ expect(activePort).toBe(3002);
101
+ expect(server.listenedPort).toBe(3002);
102
+ });
103
+
104
+ it('reads runtime ports from an override directory and ignores stale process entries', async () => {
105
+ const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dr-claw-runtime-'));
106
+ process.env.DR_CLAW_RUNTIME_DIR = runtimeDir;
107
+ cleanupTasks.push(() => fs.rm(runtimeDir, { recursive: true, force: true }));
108
+
109
+ setRuntimePortSync('backend', 4321);
110
+ setRuntimePortSync('frontend', 9876, 999999999);
111
+
112
+ expect(getBackendPortSync(3001)).toBe(4321);
113
+ expect(getFrontendPortSync(DEFAULT_FRONTEND_PORT)).toBe(DEFAULT_FRONTEND_PORT);
114
+ });
115
+ });
@@ -0,0 +1,166 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ const originalHome = process.env.HOME;
7
+ const originalUserProfile = process.env.USERPROFILE;
8
+ const originalDatabasePath = process.env.DATABASE_PATH;
9
+
10
+ let tempRoot = null;
11
+
12
+ async function loadTestModules() {
13
+ vi.resetModules();
14
+ const projects = await import('../projects.js');
15
+ const database = await import('../database/db.js');
16
+ await database.initializeDatabase();
17
+ return { projects, database };
18
+ }
19
+
20
+ async function writeCodexSessionFile({
21
+ relativePath,
22
+ sessionId,
23
+ cwd = '/tmp/test-project',
24
+ userMessage = 'Hello from Codex',
25
+ assistantMessage = 'Hi there',
26
+ timestamp = '2026-03-30T11:00:00.000Z',
27
+ }) {
28
+ const sessionFile = path.join(tempRoot, '.codex', 'sessions', relativePath);
29
+ await mkdir(path.dirname(sessionFile), { recursive: true });
30
+
31
+ const lines = [
32
+ {
33
+ timestamp,
34
+ type: 'session_meta',
35
+ payload: {
36
+ id: sessionId,
37
+ timestamp,
38
+ cwd,
39
+ model: 'gpt-5.4',
40
+ },
41
+ },
42
+ {
43
+ timestamp,
44
+ type: 'event_msg',
45
+ payload: {
46
+ type: 'user_message',
47
+ message: userMessage,
48
+ },
49
+ },
50
+ {
51
+ timestamp,
52
+ type: 'response_item',
53
+ payload: {
54
+ type: 'message',
55
+ role: 'assistant',
56
+ content: [{ type: 'output_text', text: assistantMessage }],
57
+ },
58
+ },
59
+ ].map((entry) => JSON.stringify(entry)).join('\n');
60
+
61
+ await writeFile(sessionFile, `${lines}\n`, 'utf8');
62
+ return sessionFile;
63
+ }
64
+
65
+ describe('session deletion fallbacks', () => {
66
+ beforeEach(async () => {
67
+ tempRoot = await mkdtemp(path.join(os.tmpdir(), 'dr-claw-session-delete-'));
68
+ process.env.HOME = tempRoot;
69
+ process.env.USERPROFILE = tempRoot;
70
+ process.env.DATABASE_PATH = path.join(tempRoot, 'db', 'auth.db');
71
+ });
72
+
73
+ afterEach(async () => {
74
+ vi.resetModules();
75
+
76
+ if (originalHome === undefined) delete process.env.HOME;
77
+ else process.env.HOME = originalHome;
78
+
79
+ if (originalUserProfile === undefined) delete process.env.USERPROFILE;
80
+ else process.env.USERPROFILE = originalUserProfile;
81
+
82
+ if (originalDatabasePath === undefined) delete process.env.DATABASE_PATH;
83
+ else process.env.DATABASE_PATH = originalDatabasePath;
84
+
85
+ if (tempRoot) {
86
+ await rm(tempRoot, { recursive: true, force: true });
87
+ tempRoot = null;
88
+ }
89
+ });
90
+
91
+ it('deletes a Claude session from the index when the project directory is missing', async () => {
92
+ const { projects, database } = await loadTestModules();
93
+ const projectName = 'tmp-project';
94
+ const sessionId = 'claude-session-missing-file';
95
+
96
+ database.sessionDb.upsertSessionPlaceholder(sessionId, projectName, 'claude');
97
+ expect(database.sessionDb.getSessionById(sessionId)?.provider).toBe('claude');
98
+
99
+ await expect(projects.deleteSession(projectName, sessionId, 'claude')).resolves.toBe(true);
100
+ expect(database.sessionDb.getSessionById(sessionId)).toBeNull();
101
+ });
102
+
103
+ it('deletes a Gemini session from the index when the jsonl file is missing', async () => {
104
+ const { projects, database } = await loadTestModules();
105
+ const projectName = 'tmp-project';
106
+ const sessionId = 'gemini-session-missing-file';
107
+
108
+ database.sessionDb.upsertSessionPlaceholder(sessionId, projectName, 'gemini');
109
+ expect(database.sessionDb.getSessionById(sessionId)?.provider).toBe('gemini');
110
+
111
+ await expect(projects.deleteSession(projectName, sessionId, 'gemini')).resolves.toBe(true);
112
+ expect(database.sessionDb.getSessionById(sessionId)).toBeNull();
113
+ });
114
+
115
+ it('deletes a Codex session from the index when the jsonl file is missing', async () => {
116
+ const { projects, database } = await loadTestModules();
117
+ const projectName = 'tmp-project';
118
+ const sessionId = 'codex-session-missing-file';
119
+
120
+ database.sessionDb.upsertSessionPlaceholder(sessionId, projectName, 'codex');
121
+ expect(database.sessionDb.getSessionById(sessionId)?.provider).toBe('codex');
122
+
123
+ await expect(projects.deleteCodexSession(sessionId)).resolves.toBe(true);
124
+ expect(database.sessionDb.getSessionById(sessionId)).toBeNull();
125
+ });
126
+
127
+ it('reads Codex session messages by embedded metadata when the filename lookup key differs', async () => {
128
+ const { projects } = await loadTestModules();
129
+ const sessionId = '019d3967-fcdc-7501-8441-f443c81e2de0';
130
+
131
+ await writeCodexSessionFile({
132
+ relativePath: path.join('2026', '03', '30', 'rollout-2026-03-30T07-43-29-mismatched-name.jsonl'),
133
+ sessionId,
134
+ cwd: path.join(tempRoot, 'workspace', 'proj-a'),
135
+ userMessage: 'Hello from regression test',
136
+ assistantMessage: 'Codex responded successfully',
137
+ });
138
+
139
+ const result = await projects.getCodexSessionMessages(sessionId);
140
+ expect(result.messages.length).toBeGreaterThanOrEqual(1);
141
+
142
+ const assistantMessages = result.messages.filter((entry) => entry?.message?.role === 'assistant');
143
+
144
+ expect(assistantMessages.some((entry) => entry.message.content.includes('Codex responded successfully'))).toBe(true);
145
+ });
146
+
147
+ it('indexes Codex sessions using the real session id from metadata', async () => {
148
+ const { projects } = await loadTestModules();
149
+ const sessionId = '019d3967-a181-7171-9e9f-7b73811c0d71';
150
+ const projectPath = path.join(tempRoot, 'workspace', 'proj-b');
151
+
152
+ await writeCodexSessionFile({
153
+ relativePath: path.join('2026', '03', '30', `rollout-2026-03-30T07-43-06-${sessionId}.jsonl`),
154
+ sessionId,
155
+ cwd: projectPath,
156
+ userMessage: 'Inspect the project state',
157
+ assistantMessage: 'I found the pipeline files',
158
+ });
159
+
160
+ const sessions = await projects.getCodexSessions(projectPath, { limit: 10 });
161
+ expect(sessions).toHaveLength(1);
162
+ expect(sessions[0].id).toBe(sessionId);
163
+ expect(sessions[0].provider).toBe('codex');
164
+ expect(sessions[0].summary).toContain('Inspect the project state');
165
+ });
166
+ });
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { ComputeNode } from '../compute-node.js';
4
+ import path from 'path';
5
+
6
+ // Parse args
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+
10
+ // Check for --no-sync flag
11
+ const noSync = args.includes('--no-sync');
12
+
13
+ // Check for --node <id> flag
14
+ let nodeId = null;
15
+ const nodeIdx = args.indexOf('--node');
16
+ if (nodeIdx >= 0 && args[nodeIdx + 1]) {
17
+ nodeId = args[nodeIdx + 1];
18
+ }
19
+
20
+ // Filter out flags
21
+ const filteredArgs = args.filter((a, i) => {
22
+ if (a === '--no-sync') return false;
23
+ if (a === '--node') return false;
24
+ if (i > 0 && args[i - 1] === '--node') return false;
25
+ return true;
26
+ });
27
+
28
+ async function main() {
29
+ try {
30
+ if (command === 'configure') {
31
+ const config = {};
32
+ for (let i = 1; i < filteredArgs.length; i++) {
33
+ if (filteredArgs[i].startsWith('--')) {
34
+ const key = filteredArgs[i].substring(2);
35
+ const value = filteredArgs[i+1];
36
+ config[key] = value;
37
+ i++;
38
+ }
39
+ }
40
+ console.log(await ComputeNode.configure(config));
41
+ }
42
+ else if (command === 'sync') {
43
+ const direction = filteredArgs[1] || 'up';
44
+ console.log(await ComputeNode.sync({ nodeId, direction, cwd: process.cwd() }));
45
+ }
46
+ else if (command === 'run') {
47
+ const cmd = filteredArgs.slice(1).join(' ');
48
+ console.log(await ComputeNode.run({
49
+ nodeId,
50
+ command: cmd,
51
+ cwd: noSync ? undefined : process.cwd(),
52
+ skipSync: noSync
53
+ }));
54
+ }
55
+ else {
56
+ console.log("Usage: compute-node [configure|sync|run] [--node <id>] [--no-sync]");
57
+ console.log(" configure --host X --user Y --workDir Z");
58
+ console.log(" sync [up|down] (sync active node)");
59
+ console.log(" sync --node <id> [up|down] (sync specific node)");
60
+ console.log(" run \"command\" (auto-syncs code first)");
61
+ console.log(" run --no-sync \"command\" (run without syncing)");
62
+ console.log(" run --node <id> --no-sync \"cmd\" (run on specific node)");
63
+ }
64
+ } catch (error) {
65
+ console.error("Error:", error.message);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ main();