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,724 @@
1
+ /**
2
+ * OpenAI Codex SDK Integration
3
+ * =============================
4
+ *
5
+ * This module provides integration with the OpenAI Codex SDK for non-interactive
6
+ * chat sessions. It mirrors the pattern used in claude-sdk.js for consistency.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * - queryCodex(command, options, ws) - Execute a prompt with streaming via WebSocket
11
+ * - abortCodexSession(sessionId) - Cancel an active session
12
+ * - isCodexSessionActive(sessionId) - Check if a session is running
13
+ * - getActiveCodexSessions() - List all active sessions
14
+ */
15
+
16
+ import { Codex } from '@openai/codex-sdk';
17
+ import { promises as fs } from 'fs';
18
+ import path from 'path';
19
+ import { encodeProjectPath, reconcileCodexSessionIndex } from './projects.js';
20
+ import { sessionDb } from './database/db.js';
21
+ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
22
+ import { classifyError, classifySDKError } from '../shared/errorClassifier.js';
23
+ import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
24
+ import { buildCodexRealtimeTokenBudget } from './utils/sessionTokenUsage.js';
25
+
26
+ // Track active sessions
27
+ const activeCodexSessions = new Map();
28
+
29
+ function moveActiveCodexSession(oldSessionId, newSessionId) {
30
+ if (!oldSessionId || !newSessionId || oldSessionId === newSessionId) {
31
+ return;
32
+ }
33
+
34
+ const session = activeCodexSessions.get(oldSessionId);
35
+ if (!session) {
36
+ return;
37
+ }
38
+
39
+ activeCodexSessions.set(newSessionId, session);
40
+ activeCodexSessions.delete(oldSessionId);
41
+ }
42
+
43
+ /**
44
+ * Check if an agent_message item contains system prompt / instruction content
45
+ * that should be collapsed rather than displayed as a normal message.
46
+ * @param {string} text - The message text
47
+ * @returns {boolean}
48
+ */
49
+ function isSystemPromptContent(text) {
50
+ if (!text || text.length < 200) return false;
51
+ // AGENTS.md / SKILL.md / INSTRUCTIONS headers
52
+ if (/^#\s+(AGENTS|SKILL|INSTRUCTIONS)/m.test(text)) return true;
53
+ // XML instruction tags
54
+ if (text.includes('<INSTRUCTIONS>') || text.includes('</INSTRUCTIONS>')) return true;
55
+ // "instructions for /path" pattern in a heading
56
+ if (/^#+\s+.*instructions\s+for\s+\//im.test(text)) return true;
57
+ // Skill content markers
58
+ if (text.includes('Base directory for this skill:') && text.length > 500) return true;
59
+ // Long text with numbered-list instruction patterns
60
+ if (text.length > 2000 && /^\d+\)\s/m.test(text) && /\bskill\b/i.test(text)) return true;
61
+ // Repeated SKILL.md file paths (skill listing content)
62
+ const skillPathCount = (text.match(/SKILL\.md\)/g) || []).length;
63
+ if (skillPathCount >= 3) return true;
64
+ // "How to use skills" section
65
+ if (text.includes('### How to use skills') || text.includes('## How to use skills')) return true;
66
+ // Skill discovery/trigger rules pattern
67
+ if (text.includes('Trigger rules:') && text.includes('skill') && text.length > 500) return true;
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Transform Codex SDK event to WebSocket message format
73
+ * @param {object} event - SDK event
74
+ * @returns {object|null} - Transformed event for WebSocket, or null to skip
75
+ */
76
+ function transformCodexEvent(event) {
77
+ // Map SDK event types to a consistent format
78
+ switch (event.type) {
79
+ case 'item.started':
80
+ case 'item.updated':
81
+ case 'item.completed':
82
+ const item = event.item;
83
+ if (!item) {
84
+ return null;
85
+ }
86
+
87
+ // Transform based on item type
88
+ switch (item.type) {
89
+ case 'agent_message': {
90
+ const text = item.text || '';
91
+ if (!text.trim()) return null;
92
+
93
+ // Detect and mark system prompt content
94
+ const isSysPrompt = isSystemPromptContent(text);
95
+ return {
96
+ type: 'item',
97
+ itemType: 'agent_message',
98
+ message: {
99
+ role: 'assistant',
100
+ content: text
101
+ },
102
+ isSystemPrompt: isSysPrompt
103
+ };
104
+ }
105
+
106
+ case 'reasoning': {
107
+ // Codex reasoning items are brief status notes with no real value to display
108
+ // Skip them entirely to avoid "💭 Thinking..." spam in the UI
109
+ return null;
110
+ }
111
+
112
+ case 'command_execution': {
113
+ // Codex may wrap commands in JSON: {"cmd":"...", "workdir":"...", "max_output_tokens":...}
114
+ // Extract just the command string for display
115
+ let command = item.command || '';
116
+ try {
117
+ const parsed = JSON.parse(command);
118
+ if (parsed.cmd) command = parsed.cmd;
119
+ } catch {
120
+ // Not JSON, use as-is
121
+ }
122
+ return {
123
+ type: 'item',
124
+ itemType: 'command_execution',
125
+ command,
126
+ output: item.aggregated_output || '',
127
+ exitCode: item.exit_code,
128
+ status: item.status
129
+ };
130
+ }
131
+
132
+ case 'file_change':
133
+ return {
134
+ type: 'item',
135
+ itemType: 'file_change',
136
+ changes: item.changes,
137
+ status: item.status
138
+ };
139
+
140
+ case 'mcp_tool_call':
141
+ return {
142
+ type: 'item',
143
+ itemType: 'mcp_tool_call',
144
+ server: item.server,
145
+ tool: item.tool,
146
+ arguments: item.arguments,
147
+ result: item.result,
148
+ error: item.error,
149
+ status: item.status
150
+ };
151
+
152
+ case 'web_search':
153
+ return {
154
+ type: 'item',
155
+ itemType: 'web_search',
156
+ query: item.query || ''
157
+ };
158
+
159
+ case 'todo_list':
160
+ return {
161
+ type: 'item',
162
+ itemType: 'todo_list',
163
+ items: item.items
164
+ };
165
+
166
+ case 'error':
167
+ return {
168
+ type: 'item',
169
+ itemType: 'error',
170
+ message: {
171
+ role: 'error',
172
+ content: item.message
173
+ }
174
+ };
175
+
176
+ default:
177
+ return {
178
+ type: 'item',
179
+ itemType: item.type,
180
+ item: item
181
+ };
182
+ }
183
+
184
+ case 'turn.started':
185
+ return {
186
+ type: 'turn_started'
187
+ };
188
+
189
+ case 'turn.completed':
190
+ return {
191
+ type: 'turn_complete',
192
+ usage: event.usage
193
+ };
194
+
195
+ case 'turn.failed':
196
+ return {
197
+ type: 'turn_failed',
198
+ error: event.error
199
+ };
200
+
201
+ case 'thread.started':
202
+ return {
203
+ type: 'thread_started',
204
+ threadId: event.id
205
+ };
206
+
207
+ case 'error':
208
+ return {
209
+ type: 'error',
210
+ message: event.message
211
+ };
212
+
213
+ default:
214
+ return {
215
+ type: event.type,
216
+ data: event
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Map permission mode to Codex SDK options
223
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
224
+ * @returns {object} - { sandboxMode, approvalPolicy }
225
+ */
226
+ function mapPermissionModeToCodexOptions(permissionMode) {
227
+ switch (permissionMode) {
228
+ case 'acceptEdits':
229
+ return {
230
+ sandboxMode: 'workspace-write',
231
+ approvalPolicy: 'never'
232
+ };
233
+ case 'bypassPermissions':
234
+ return {
235
+ sandboxMode: 'danger-full-access',
236
+ approvalPolicy: 'never'
237
+ };
238
+ case 'default':
239
+ default:
240
+ return {
241
+ sandboxMode: 'workspace-write',
242
+ approvalPolicy: 'untrusted'
243
+ };
244
+ }
245
+ }
246
+
247
+ function buildCodexInput(command, attachments) {
248
+ const imagePaths = Array.isArray(attachments?.imagePaths)
249
+ ? attachments.imagePaths.filter((value) => typeof value === 'string' && value.trim())
250
+ : [];
251
+ const documentPaths = Array.isArray(attachments?.documentPaths)
252
+ ? attachments.documentPaths.filter((value) => typeof value === 'string' && value.trim())
253
+ : [];
254
+
255
+ if (imagePaths.length === 0 && documentPaths.length === 0) {
256
+ return command;
257
+ }
258
+
259
+ const textSections = [command];
260
+
261
+ if (documentPaths.length > 0) {
262
+ textSections.push(
263
+ `Attached workspace PDF path(s):\n${documentPaths
264
+ .map((filePath) => `- ${filePath}`)
265
+ .join('\n')}`,
266
+ );
267
+ }
268
+
269
+ if (imagePaths.length > 0) {
270
+ textSections.push(
271
+ imagePaths.length === 1
272
+ ? 'An image is attached below.'
273
+ : `There are ${imagePaths.length} attached images below.`,
274
+ );
275
+ }
276
+
277
+ return [
278
+ { type: 'text', text: textSections.join('\n\n') },
279
+ ...imagePaths.map((filePath) => ({ type: 'local_image', path: filePath })),
280
+ ];
281
+ }
282
+
283
+ async function prepareCodexInput(command, images, cwd) {
284
+ const tempImagePaths = [];
285
+ let tempDir = null;
286
+
287
+ if (!Array.isArray(images) || images.length === 0) {
288
+ return { input: command, tempImagePaths, tempDir };
289
+ }
290
+
291
+ try {
292
+ const workingDir = cwd || process.cwd();
293
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
294
+ await fs.mkdir(tempDir, { recursive: true });
295
+
296
+ const input = [{ type: 'text', text: command }];
297
+
298
+ for (const [index, image] of images.entries()) {
299
+ const data = String(image?.data || '');
300
+ const matches = data.match(/^data:([^;]+);base64,(.+)$/);
301
+ if (!matches) {
302
+ continue;
303
+ }
304
+
305
+ const [, mimeType, base64Data] = matches;
306
+ const filename = buildTempAttachmentFilename(index, image?.name, mimeType);
307
+ const filepath = path.join(tempDir, filename);
308
+
309
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
310
+ tempImagePaths.push(filepath);
311
+ input.push({ type: 'local_image', path: filepath });
312
+ }
313
+
314
+ return {
315
+ input: input.length > 1 ? input : command,
316
+ tempImagePaths,
317
+ tempDir,
318
+ };
319
+ } catch (error) {
320
+ console.error('[Codex] Failed to prepare image inputs:', error);
321
+ return { input: command, tempImagePaths, tempDir };
322
+ }
323
+ }
324
+
325
+ async function cleanupCodexTempFiles(tempImagePaths, tempDir) {
326
+ if (!Array.isArray(tempImagePaths) || tempImagePaths.length === 0) {
327
+ return;
328
+ }
329
+
330
+ for (const filePath of tempImagePaths) {
331
+ try {
332
+ await fs.unlink(filePath);
333
+ } catch {}
334
+ }
335
+
336
+ if (tempDir) {
337
+ try {
338
+ await fs.rm(tempDir, { recursive: true, force: true });
339
+ } catch {}
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Execute a Codex query with streaming
345
+ * @param {string} command - The prompt to send
346
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode, modelReasoningEffort
347
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
348
+ */
349
+ export async function queryCodex(command, options = {}, ws) {
350
+ const {
351
+ sessionId,
352
+ cwd,
353
+ projectPath,
354
+ model,
355
+ env,
356
+ attachments,
357
+ images,
358
+ permissionMode = 'default',
359
+ modelReasoningEffort,
360
+ sessionMode,
361
+ stageTagKeys,
362
+ stageTagSource = 'task_context',
363
+ } = options;
364
+
365
+ const workingDirectory = cwd || projectPath || process.cwd();
366
+ const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
367
+
368
+ let codex;
369
+ let thread;
370
+ let currentSessionId = sessionId || null;
371
+ let provisionalSessionId = null;
372
+ const abortController = new AbortController();
373
+ let tempImagePaths = [];
374
+ let tempDir = null;
375
+
376
+ try {
377
+ // Synchronous (better-sqlite3) — no await needed.
378
+ if (sessionId && workingDirectory) {
379
+ applyStageTagsToSession({
380
+ sessionId,
381
+ projectPath: workingDirectory,
382
+ stageTagKeys,
383
+ source: stageTagSource,
384
+ });
385
+ }
386
+
387
+ // Initialize Codex SDK
388
+ codex = new Codex(env ? { env } : undefined);
389
+
390
+ // Thread options with sandbox and approval settings
391
+ const threadOptions = {
392
+ workingDirectory,
393
+ skipGitRepoCheck: true,
394
+ sandboxMode,
395
+ approvalPolicy,
396
+ model,
397
+ modelReasoningEffort,
398
+ };
399
+
400
+ // Start or resume thread
401
+ if (sessionId) {
402
+ thread = codex.resumeThread(sessionId, threadOptions);
403
+ } else {
404
+ thread = codex.startThread(threadOptions);
405
+ }
406
+
407
+ provisionalSessionId = currentSessionId || `codex-${Date.now()}`;
408
+ activeCodexSessions.set(provisionalSessionId, {
409
+ thread,
410
+ codex,
411
+ status: 'running',
412
+ abortController,
413
+ startTime: Date.now()
414
+ });
415
+
416
+ const publishSessionId = (resolvedSessionId) => {
417
+ if (!resolvedSessionId || resolvedSessionId === currentSessionId) {
418
+ return;
419
+ }
420
+
421
+ const previousSessionId = currentSessionId || provisionalSessionId;
422
+ currentSessionId = resolvedSessionId;
423
+
424
+ if (previousSessionId && previousSessionId !== currentSessionId) {
425
+ moveActiveCodexSession(previousSessionId, currentSessionId);
426
+ }
427
+
428
+ if (workingDirectory) {
429
+ recordIndexedSession({
430
+ sessionId: currentSessionId,
431
+ provider: 'codex',
432
+ projectPath: workingDirectory,
433
+ sessionMode: sessionMode || 'research',
434
+ stageTagKeys,
435
+ tagSource: stageTagSource,
436
+ });
437
+ }
438
+
439
+ sendMessage(ws, {
440
+ type: 'session-created',
441
+ sessionId: currentSessionId,
442
+ provider: 'codex',
443
+ mode: sessionMode || 'research'
444
+ });
445
+ };
446
+
447
+ publishSessionId(thread.id || sessionId || null);
448
+
449
+ const preparedInput = await prepareCodexInput(command, images, workingDirectory);
450
+ tempImagePaths = preparedInput.tempImagePaths;
451
+ tempDir = preparedInput.tempDir;
452
+
453
+ // Execute with streaming
454
+ // Prefer pre-uploaded attachments (buildCodexInput) over base64 temp images (prepareCodexInput)
455
+ const codexInput = attachments
456
+ ? buildCodexInput(command, attachments)
457
+ : preparedInput.input;
458
+ const streamedTurn = await thread.runStreamed(codexInput, {
459
+ signal: abortController.signal
460
+ });
461
+
462
+ // Track items we've already sent to avoid duplicates
463
+ const sentItems = new Map(); // itemId -> lifecycle stage
464
+
465
+ for await (const event of streamedTurn.events) {
466
+ if (event.type === 'thread.started' && event.id) {
467
+ publishSessionId(event.id);
468
+ }
469
+
470
+ // Check if session was aborted
471
+ const activeLookupId = currentSessionId || provisionalSessionId;
472
+ const session = activeCodexSessions.get(activeLookupId);
473
+ if (!session || session.status === 'aborted') {
474
+ break;
475
+ }
476
+
477
+ const itemType = event.item?.type || 'unknown';
478
+ const itemId = event.item?.id || null;
479
+
480
+ // Detailed debug logging
481
+ if (event.item) {
482
+ const preview = event.item.text ? event.item.text.substring(0, 80) : (event.item.command || '');
483
+ console.log(`[Codex] ${event.type} | ${itemType} | id=${itemId} | preview="${preview}"`);
484
+ // Extra logging for command_execution output
485
+ if (itemType === 'command_execution' && event.type === 'item.completed') {
486
+ const outLen = event.item.aggregated_output?.length || 0;
487
+ const outPreview = event.item.aggregated_output?.substring(0, 120) || '(empty)';
488
+ console.log(`[Codex] cmd output (${outLen} chars): "${outPreview}"`);
489
+ }
490
+ } else {
491
+ console.log(`[Codex] ${event.type}`);
492
+ }
493
+
494
+ // Event filtering:
495
+ // - item.updated: always skip (streaming noise)
496
+ // - item.started: forward tool-type items immediately so they appear in UI
497
+ // - item.completed: always forward (final state with results)
498
+ if (event.type === 'item.updated') {
499
+ continue;
500
+ }
501
+
502
+ if (event.type === 'item.started') {
503
+ const toolTypes = new Set(['command_execution', 'file_change', 'mcp_tool_call', 'web_search']);
504
+ if (!toolTypes.has(itemType)) {
505
+ continue;
506
+ }
507
+ if (itemId) sentItems.set(itemId, 'started');
508
+ }
509
+
510
+ if (event.type === 'item.completed' && itemId) {
511
+ sentItems.set(itemId, 'completed');
512
+ }
513
+
514
+ const transformed = transformCodexEvent(event);
515
+
516
+ // Skip null transforms (empty reasoning, etc.)
517
+ if (!transformed) {
518
+ console.log(`[Codex] Skipped null transform for ${event.type} | ${itemType}`);
519
+ continue;
520
+ }
521
+
522
+ // Add lifecycle info for frontend dedup
523
+ if (itemId) {
524
+ transformed.itemId = itemId;
525
+ transformed.lifecycle = event.type === 'item.started' ? 'started'
526
+ : event.type === 'item.completed' ? 'completed' : 'other';
527
+ }
528
+
529
+ // Add startTime for frontend timer synchronization
530
+ const activeSessionId = currentSessionId || provisionalSessionId;
531
+ const activeSession = activeSessionId ? activeCodexSessions.get(activeSessionId) : null;
532
+ if (Number.isFinite(activeSession?.startTime)) {
533
+ transformed.startTime = activeSession.startTime;
534
+ }
535
+
536
+ // For error/turn.failed events, send codex-error instead of codex-response
537
+ // to trigger the error UI with retry button (avoid sending both).
538
+ if (event.type === 'error' || event.type === 'turn.failed') {
539
+ const errorCode = event.error?.code || event.error?.type || '';
540
+ const errorMsg = event.error?.message || event.message || String(event.error || '');
541
+ const { errorType, isRetryable } = errorCode
542
+ ? classifySDKError(errorCode, 'codex')
543
+ : classifyError(errorMsg);
544
+ sendMessage(ws, {
545
+ type: 'codex-error',
546
+ error: errorMsg || errorCode,
547
+ errorType,
548
+ isRetryable,
549
+ sessionId: currentSessionId,
550
+ });
551
+ continue;
552
+ }
553
+
554
+ sendMessage(ws, {
555
+ type: 'codex-response',
556
+ data: transformed,
557
+ sessionId: currentSessionId
558
+ });
559
+
560
+ // Extract and send token usage if available (normalized to match Claude format)
561
+ if (event.type === 'turn.completed' && event.usage) {
562
+ sendMessage(ws, {
563
+ type: 'token-budget',
564
+ data: buildCodexRealtimeTokenBudget(event.usage),
565
+ sessionId: currentSessionId
566
+ });
567
+ }
568
+ }
569
+
570
+ const actualSessionId = thread.id || currentSessionId || provisionalSessionId;
571
+
572
+ // Send completion event immediately so the UI can settle
573
+ sendMessage(ws, {
574
+ type: 'codex-complete',
575
+ sessionId: currentSessionId,
576
+ actualSessionId
577
+ });
578
+
579
+ // Post-completion housekeeping — runs after the UI receives the completion signal
580
+ if (workingDirectory && actualSessionId) {
581
+ try {
582
+ await reconcileCodexSessionIndex(workingDirectory, {
583
+ sessionId: actualSessionId,
584
+ previousSessionId: currentSessionId,
585
+ projectName: encodeProjectPath(workingDirectory),
586
+ });
587
+ if (currentSessionId && actualSessionId !== currentSessionId) {
588
+ sessionDb.deleteSession(currentSessionId);
589
+ }
590
+ } catch (error) {
591
+ console.warn(`[Codex] Failed to reconcile indexed session ${actualSessionId}:`, error.message);
592
+ }
593
+ }
594
+
595
+ } catch (error) {
596
+ const lookupSessionId = currentSessionId || provisionalSessionId;
597
+ const session = lookupSessionId ? activeCodexSessions.get(lookupSessionId) : null;
598
+ const wasAborted =
599
+ session?.status === 'aborted' ||
600
+ error?.name === 'AbortError' ||
601
+ String(error?.message || '').toLowerCase().includes('aborted');
602
+
603
+ if (!wasAborted) {
604
+ console.error('[Codex] Error:', error);
605
+ const { errorType, isRetryable } = classifyError(error.message);
606
+
607
+ sendMessage(ws, {
608
+ type: 'codex-error',
609
+ error: error.message,
610
+ errorType,
611
+ isRetryable,
612
+ sessionId: currentSessionId
613
+ });
614
+ }
615
+
616
+ } finally {
617
+ await cleanupCodexTempFiles(tempImagePaths, tempDir);
618
+
619
+ // Update session status
620
+ const finalSessionId = currentSessionId || provisionalSessionId;
621
+ if (finalSessionId) {
622
+ const session = activeCodexSessions.get(finalSessionId);
623
+ if (session) {
624
+ session.status = session.status === 'aborted' ? 'aborted' : 'completed';
625
+ }
626
+ }
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Abort an active Codex session
632
+ * @param {string} sessionId - Session ID to abort
633
+ * @returns {boolean} - Whether abort was successful
634
+ */
635
+ export function abortCodexSession(sessionId) {
636
+ const session = activeCodexSessions.get(sessionId);
637
+
638
+ if (!session) {
639
+ return false;
640
+ }
641
+
642
+ session.status = 'aborted';
643
+ try {
644
+ session.abortController?.abort();
645
+ } catch (error) {
646
+ console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
647
+ }
648
+
649
+ return true;
650
+ }
651
+
652
+ /**
653
+ * Check if a session is active
654
+ * @param {string} sessionId - Session ID to check
655
+ * @returns {boolean} - Whether session is active
656
+ */
657
+ export function isCodexSessionActive(sessionId) {
658
+ const session = activeCodexSessions.get(sessionId);
659
+ return session?.status === 'running';
660
+ }
661
+
662
+ /**
663
+ * Get the start time of a Codex session
664
+ * @param {string} sessionId - Session ID
665
+ * @returns {number|null} Start time in ms or null
666
+ */
667
+ export function getCodexSessionStartTime(sessionId) {
668
+ const session = activeCodexSessions.get(sessionId);
669
+ return session ? session.startTime : null;
670
+ }
671
+
672
+ /**
673
+ * Get all active sessions
674
+ * @returns {Array} - Array of active session info
675
+ */
676
+ export function getActiveCodexSessions() {
677
+ const sessions = [];
678
+
679
+ for (const [id, session] of activeCodexSessions.entries()) {
680
+ if (session.status === 'running') {
681
+ sessions.push({
682
+ id,
683
+ status: session.status,
684
+ startTime: session.startTime
685
+ });
686
+ }
687
+ }
688
+
689
+ return sessions;
690
+ }
691
+
692
+ /**
693
+ * Helper to send message via WebSocket or writer
694
+ * @param {WebSocket|object} ws - WebSocket or response writer
695
+ * @param {object} data - Data to send
696
+ */
697
+ function sendMessage(ws, data) {
698
+ try {
699
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
700
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
701
+ ws.send(data);
702
+ } else if (typeof ws.send === 'function') {
703
+ // Raw WebSocket - stringify here
704
+ ws.send(JSON.stringify(data));
705
+ }
706
+ } catch (error) {
707
+ console.error('[Codex] Error sending message:', error);
708
+ }
709
+ }
710
+
711
+ // Clean up old completed sessions periodically
712
+ setInterval(() => {
713
+ const now = Date.now();
714
+ const maxAge = 30 * 60 * 1000; // 30 minutes
715
+
716
+ for (const [id, session] of activeCodexSessions.entries()) {
717
+ if (session.status !== 'running') {
718
+ const startTime = typeof session.startTime === 'number' ? session.startTime : Number.NaN;
719
+ if (Number.isFinite(startTime) && now - startTime > maxAge) {
720
+ activeCodexSessions.delete(id);
721
+ }
722
+ }
723
+ }
724
+ }, 5 * 60 * 1000); // Every 5 minutes