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,128 @@
1
+ import express from 'express';
2
+ import bcrypt from 'bcryptjs';
3
+ import { userDb, db } from '../database/db.js';
4
+ import { generateToken, authenticateToken } from '../middleware/auth.js';
5
+
6
+ const router = express.Router();
7
+
8
+ // Check auth status
9
+ router.get('/status', async (req, res) => {
10
+ res.json({
11
+ needsSetup: !userDb.hasUsers(),
12
+ isAuthenticated: false
13
+ });
14
+ });
15
+
16
+ // User registration (setup)
17
+ router.post('/register', async (req, res) => {
18
+ try {
19
+ const { username, password, notificationEmail } = req.body;
20
+
21
+ // Validate input
22
+ if (!username || !password || !notificationEmail) {
23
+ return res.status(400).json({ error: 'Username, password, and email are required' });
24
+ }
25
+
26
+ if (username.length < 3 || password.length < 6) {
27
+ return res.status(400).json({ error: 'Username must be at least 3 characters, password at least 6 characters' });
28
+ }
29
+
30
+ const email = String(notificationEmail).trim().toLowerCase();
31
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
32
+ if (!emailRegex.test(email)) {
33
+ return res.status(400).json({ error: 'Invalid email format' });
34
+ }
35
+
36
+ // Check if user already exists
37
+ const existingUser = userDb.getUserByUsername(username);
38
+ if (existingUser) {
39
+ return res.status(409).json({ error: 'Username already exists' });
40
+ }
41
+
42
+ // Hash password
43
+ const saltRounds = 12;
44
+ const passwordHash = await bcrypt.hash(password, saltRounds);
45
+
46
+ // Create user
47
+ const user = userDb.createUser(username, passwordHash, email);
48
+
49
+ // Generate token
50
+ const token = generateToken(user);
51
+
52
+ // Update last login outside transaction (non-fatal)
53
+ userDb.updateLastLogin(user.id);
54
+
55
+ res.json({
56
+ success: true,
57
+ user: { id: user.id, username: user.username, notificationEmail: user.notification_email },
58
+ token
59
+ });
60
+
61
+ } catch (error) {
62
+ console.error('Registration error:', error);
63
+ if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
64
+ res.status(409).json({ error: 'Username already exists' });
65
+ } else {
66
+ res.status(500).json({ error: 'Internal server error' });
67
+ }
68
+ }
69
+ });
70
+
71
+ // User login
72
+ router.post('/login', async (req, res) => {
73
+ try {
74
+ const { username, password } = req.body;
75
+
76
+ // Validate input
77
+ if (!username || !password) {
78
+ return res.status(400).json({ error: 'Username and password are required' });
79
+ }
80
+
81
+ // Get user from database
82
+ const user = userDb.getUserByUsername(username);
83
+ if (!user) {
84
+ return res.status(401).json({ error: 'Invalid username or password' });
85
+ }
86
+
87
+ // Verify password
88
+ const isValidPassword = await bcrypt.compare(password, user.password_hash);
89
+ if (!isValidPassword) {
90
+ return res.status(401).json({ error: 'Invalid username or password' });
91
+ }
92
+
93
+ // Generate token
94
+ const token = generateToken(user);
95
+
96
+ // Update last login
97
+ userDb.updateLastLogin(user.id);
98
+
99
+ res.json({
100
+ success: true,
101
+ user: { id: user.id, username: user.username },
102
+ token
103
+ });
104
+
105
+ } catch (error) {
106
+ console.error('Login error:', error);
107
+ res.status(500).json({ error: 'Internal server error' });
108
+ }
109
+ });
110
+
111
+ // Get current user (protected route)
112
+ router.get('/user', authenticateToken, (req, res) => {
113
+ res.json({
114
+ user: {
115
+ ...req.user,
116
+ notificationEmail: req.user.notification_email || null,
117
+ }
118
+ });
119
+ });
120
+
121
+ // Logout (client-side token removal, but this endpoint can be used for logging)
122
+ router.post('/logout', authenticateToken, (req, res) => {
123
+ // In a simple JWT system, logout is mainly client-side
124
+ // This endpoint exists for consistency and potential future logging
125
+ res.json({ success: true, message: 'Logged out successfully' });
126
+ });
127
+
128
+ export default router;
@@ -0,0 +1,558 @@
1
+ import express from 'express';
2
+ import crypto from 'crypto';
3
+ import path from 'path';
4
+ import { promises as fs } from 'fs';
5
+
6
+ import { appSettingsDb, autoResearchDb, userDb } from '../database/db.js';
7
+ import { extractProjectDirectory } from '../projects.js';
8
+ import { CLAUDE_MODELS, CODEX_MODELS, GEMINI_MODELS, OPENROUTER_MODELS } from '../../shared/modelConstants.js';
9
+ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive } from '../claude-sdk.js';
10
+ import { queryCodex, abortCodexSession, isCodexSessionActive } from '../openai-codex.js';
11
+ import { spawnGemini, abortGeminiSession, isGeminiSessionActive } from '../gemini-cli.js';
12
+ import { queryOpenRouter, abortOpenRouterSession, isOpenRouterSessionActive } from '../openrouter.js';
13
+ import { sendAutoResearchCompletionEmail } from '../utils/auto-research-mailer.js';
14
+ import { getGeminiApiKeyForUser, withGeminiApiKeyEnv } from '../utils/geminiApiKey.js';
15
+
16
+ const router = express.Router();
17
+
18
+ const activeRuns = new Map();
19
+ const TASK_TIMEOUT_MS = Number.parseInt(process.env.AUTO_RESEARCH_TASK_TIMEOUT_MS || '', 10) || 30 * 60 * 1000;
20
+ const AUTO_RESEARCH_SENDER_EMAIL_KEY = 'auto_research_sender_email';
21
+ const AUTO_RESEARCH_RESEND_API_KEY = 'auto_research_resend_api_key';
22
+ const AUTO_RESEARCH_DEFAULT_PERMISSION_MODE = 'bypassPermissions';
23
+ const AUTO_RESEARCH_PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
24
+
25
+ function getDefaultModelForProvider(provider) {
26
+ if (provider === 'codex') {
27
+ return CODEX_MODELS.DEFAULT || 'gpt-5.4';
28
+ }
29
+ if (provider === 'gemini') {
30
+ return GEMINI_MODELS.DEFAULT || 'gemini-3-flash-preview';
31
+ }
32
+ if (provider === 'openrouter') {
33
+ return OPENROUTER_MODELS.DEFAULT || 'anthropic/claude-sonnet-4';
34
+ }
35
+ return CLAUDE_MODELS.DEFAULT || 'sonnet';
36
+ }
37
+
38
+ function normalizeAutoResearchProvider(provider) {
39
+ if (provider === 'claude' || provider === 'codex' || provider === 'gemini' || provider === 'openrouter') {
40
+ return provider;
41
+ }
42
+ return 'claude';
43
+ }
44
+
45
+ function normalizePermissionMode(permissionMode) {
46
+ if (AUTO_RESEARCH_PERMISSION_MODES.has(permissionMode)) {
47
+ return permissionMode;
48
+ }
49
+ return AUTO_RESEARCH_DEFAULT_PERMISSION_MODE;
50
+ }
51
+
52
+ function abortActiveSession(provider, sessionId) {
53
+ if (provider === 'codex') {
54
+ return abortCodexSession(sessionId);
55
+ }
56
+ if (provider === 'gemini') {
57
+ return abortGeminiSession(sessionId);
58
+ }
59
+ if (provider === 'openrouter') {
60
+ return abortOpenRouterSession(sessionId);
61
+ }
62
+ return abortClaudeSDKSession(sessionId);
63
+ }
64
+
65
+ function isSessionActiveForProvider(provider, sessionId) {
66
+ if (provider === 'codex') {
67
+ return isCodexSessionActive(sessionId);
68
+ }
69
+ if (provider === 'gemini') {
70
+ return isGeminiSessionActive(sessionId);
71
+ }
72
+ if (provider === 'openrouter') {
73
+ return isOpenRouterSessionActive(sessionId);
74
+ }
75
+ return isClaudeSDKSessionActive(sessionId);
76
+ }
77
+
78
+ function getPipelinePaths(projectPath) {
79
+ return {
80
+ researchBriefFile: path.join(projectPath, '.pipeline', 'docs', 'research_brief.json'),
81
+ tasksFile: path.join(projectPath, '.pipeline', 'tasks', 'tasks.json'),
82
+ };
83
+ }
84
+
85
+ function extractTasksFromData(tasksData) {
86
+ let currentTag = 'master';
87
+ let tasks = [];
88
+
89
+ if (Array.isArray(tasksData)) {
90
+ tasks = tasksData;
91
+ } else if (tasksData?.tasks) {
92
+ tasks = tasksData.tasks;
93
+ } else if (tasksData && typeof tasksData === 'object') {
94
+ if (tasksData[currentTag]?.tasks) {
95
+ tasks = tasksData[currentTag].tasks;
96
+ } else {
97
+ const firstTag = Object.keys(tasksData).find((key) => Array.isArray(tasksData[key]?.tasks));
98
+ if (firstTag) {
99
+ currentTag = firstTag;
100
+ tasks = tasksData[firstTag].tasks;
101
+ }
102
+ }
103
+ }
104
+
105
+ return { currentTag, tasks: Array.isArray(tasks) ? tasks : [] };
106
+ }
107
+
108
+ function normalizeTaskStatus(status) {
109
+ const raw = String(status || '').trim().toLowerCase();
110
+ if (!raw) return 'pending';
111
+ if (raw === 'completed' || raw === 'complete') return 'done';
112
+ if (raw === 'in_progress' || raw === 'inprogress') return 'in-progress';
113
+ if (raw === 'todo' || raw === 'open') return 'pending';
114
+ return raw;
115
+ }
116
+
117
+ function normalizeTaskStage(stage) {
118
+ const raw = String(stage || '').trim().toLowerCase();
119
+ if (raw === 'presentation') return 'promotion';
120
+ if (raw === 'research') return 'survey';
121
+ return raw;
122
+ }
123
+
124
+ function normalizeTask(task) {
125
+ return {
126
+ id: task.id,
127
+ title: task.title || 'Untitled Task',
128
+ status: normalizeTaskStatus(task.status),
129
+ stage: normalizeTaskStage(task.stage),
130
+ nextActionPrompt: typeof task.nextActionPrompt === 'string' ? task.nextActionPrompt : '',
131
+ };
132
+ }
133
+
134
+ async function readPipelineState(projectPath) {
135
+ const paths = getPipelinePaths(projectPath);
136
+ const [briefStat, tasksStat] = await Promise.allSettled([
137
+ fs.access(paths.researchBriefFile),
138
+ fs.access(paths.tasksFile),
139
+ ]);
140
+
141
+ const hasResearchBrief = briefStat.status === 'fulfilled';
142
+ const hasTasksFile = tasksStat.status === 'fulfilled';
143
+ let tasks = [];
144
+
145
+ if (hasTasksFile) {
146
+ const content = await fs.readFile(paths.tasksFile, 'utf8');
147
+ const parsed = JSON.parse(content);
148
+ tasks = extractTasksFromData(parsed).tasks.map(normalizeTask);
149
+ }
150
+
151
+ const actionableTasks = tasks.filter((task) => task.status === 'pending' || task.status === 'in-progress');
152
+ const nextTask = tasks.find((task) => task.status === 'in-progress')
153
+ || tasks.find((task) => task.status === 'pending')
154
+ || null;
155
+
156
+ return {
157
+ ...paths,
158
+ hasResearchBrief,
159
+ hasTasksFile,
160
+ tasks,
161
+ nextTask,
162
+ actionableTaskCount: actionableTasks.length,
163
+ completedTaskCount: tasks.filter((task) => task.status === 'done').length,
164
+ };
165
+ }
166
+
167
+ function serializeRun(run, runtime = null) {
168
+ if (!run) return null;
169
+ return {
170
+ id: run.id,
171
+ projectName: run.project_name,
172
+ projectPath: run.project_path,
173
+ provider: runtime?.provider || run.provider,
174
+ status: run.status,
175
+ sessionId: runtime?.sessionId || run.session_id,
176
+ currentTaskId: run.current_task_id,
177
+ completedTasks: run.completed_tasks,
178
+ totalTasks: run.total_tasks,
179
+ error: run.error,
180
+ metadata: run.metadata || null,
181
+ startedAt: run.started_at,
182
+ finishedAt: run.finished_at,
183
+ emailSentAt: run.email_sent_at,
184
+ };
185
+ }
186
+
187
+ function buildEligibility(profile, pipelineState, activeRun) {
188
+ const reasons = [];
189
+
190
+ if (!profile?.notification_email) {
191
+ reasons.push('notification_email_missing');
192
+ }
193
+ if (!pipelineState.hasResearchBrief) {
194
+ reasons.push('research_brief_missing');
195
+ }
196
+ if (!pipelineState.hasTasksFile) {
197
+ reasons.push('tasks_file_missing');
198
+ }
199
+ if (pipelineState.hasTasksFile && pipelineState.actionableTaskCount === 0) {
200
+ reasons.push('no_actionable_tasks');
201
+ }
202
+ if (activeRun) {
203
+ reasons.push('run_in_progress');
204
+ }
205
+
206
+ return {
207
+ eligible: reasons.length === 0,
208
+ reasons,
209
+ };
210
+ }
211
+
212
+ class AutoResearchWriter {
213
+ constructor(onEvent) {
214
+ this.sessionId = null;
215
+ this.onEvent = onEvent;
216
+ }
217
+
218
+ send(data) {
219
+ if (data?.type === 'session-created' && data.sessionId) {
220
+ this.sessionId = data.sessionId;
221
+ }
222
+ if (typeof this.onEvent === 'function') {
223
+ this.onEvent(data, this.sessionId);
224
+ }
225
+ }
226
+
227
+ setSessionId(sessionId) {
228
+ this.sessionId = sessionId;
229
+ }
230
+
231
+ getSessionId() {
232
+ return this.sessionId;
233
+ }
234
+ }
235
+
236
+ function withTimeout(promise, timeoutMs, onTimeout) {
237
+ let timeoutId = null;
238
+ const timeoutPromise = new Promise((_, reject) => {
239
+ timeoutId = setTimeout(async () => {
240
+ try {
241
+ await onTimeout?.();
242
+ } catch (error) {
243
+ console.error('[AutoResearch] Timeout cleanup failed:', error);
244
+ }
245
+ reject(new Error(`Task timed out after ${timeoutMs}ms`));
246
+ }, timeoutMs);
247
+ });
248
+
249
+ return Promise.race([
250
+ promise.finally(() => {
251
+ if (timeoutId) {
252
+ clearTimeout(timeoutId);
253
+ }
254
+ }),
255
+ timeoutPromise,
256
+ ]);
257
+ }
258
+
259
+ async function deliverCompletionEmail(runId, userId, projectName) {
260
+ const run = autoResearchDb.getRunById(runId);
261
+ const profile = userDb.getProfile(userId);
262
+
263
+ if (!run || !profile?.notification_email) {
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const result = await sendAutoResearchCompletionEmail({
269
+ toEmail: profile.notification_email,
270
+ run,
271
+ projectName,
272
+ });
273
+ if (result?.sent) {
274
+ autoResearchDb.updateRun(runId, { emailSentAt: new Date().toISOString() });
275
+ }
276
+ } catch (error) {
277
+ console.error('[AutoResearch] Failed to send completion email:', error);
278
+ }
279
+ }
280
+
281
+ function isRunSessionStillActive(run) {
282
+ return isSessionActiveForProvider(run?.provider, run?.session_id);
283
+ }
284
+
285
+ function reconcileActiveRun(run) {
286
+ if (!run) {
287
+ return null;
288
+ }
289
+
290
+ const hasRuntime = activeRuns.has(run.id);
291
+ const sessionStillActive = isRunSessionStillActive(run);
292
+ if (hasRuntime || sessionStillActive) {
293
+ return run;
294
+ }
295
+
296
+ const staleStatus = run.status === 'cancelling' ? 'cancelled' : 'failed';
297
+ return autoResearchDb.updateRun(run.id, {
298
+ status: staleStatus,
299
+ error: run.error || 'Recovered stale Auto Research run after session interruption',
300
+ currentTaskId: null,
301
+ finishedAt: run.finished_at || new Date().toISOString(),
302
+ });
303
+ }
304
+
305
+ async function runAutoResearch(runId, userId, projectName, projectPath) {
306
+ const runState = activeRuns.get(runId);
307
+ if (!runState) {
308
+ return;
309
+ }
310
+
311
+ try {
312
+ const geminiApiKey = getGeminiApiKeyForUser(userId);
313
+ const sessionEnv = withGeminiApiKeyEnv(process.env, geminiApiKey);
314
+
315
+ let pipelineState = await readPipelineState(projectPath);
316
+ autoResearchDb.updateRun(runId, {
317
+ status: 'running',
318
+ totalTasks: pipelineState.tasks.length,
319
+ completedTasks: pipelineState.completedTaskCount,
320
+ });
321
+
322
+ while (pipelineState.nextTask) {
323
+ if (runState.cancelRequested) {
324
+ throw new Error('Run cancelled by user');
325
+ }
326
+
327
+ const task = pipelineState.nextTask;
328
+ const prompt = task.nextActionPrompt?.trim();
329
+ if (!prompt) {
330
+ throw new Error(`Task ${task.id} does not have a nextActionPrompt`);
331
+ }
332
+
333
+ autoResearchDb.updateRun(runId, {
334
+ status: 'running',
335
+ currentTaskId: String(task.id),
336
+ totalTasks: pipelineState.tasks.length,
337
+ completedTasks: pipelineState.completedTaskCount,
338
+ });
339
+
340
+ const writer = new AutoResearchWriter((event, sessionId) => {
341
+ if (event?.type === 'session-created' && sessionId) {
342
+ runState.sessionId = sessionId;
343
+ autoResearchDb.updateRun(runId, { sessionId });
344
+ }
345
+ });
346
+ const provider = runState.provider || 'claude';
347
+ const model = runState.model || getDefaultModelForProvider(provider);
348
+ const permissionMode = runState.permissionMode || AUTO_RESEARCH_DEFAULT_PERMISSION_MODE;
349
+
350
+ const agentOptions = {
351
+ cwd: projectPath,
352
+ projectPath,
353
+ sessionId: runState.sessionId,
354
+ env: sessionEnv,
355
+ model,
356
+ permissionMode,
357
+ stageTagKeys: task.stage ? [task.stage] : [],
358
+ stageTagSource: 'auto_research',
359
+ };
360
+
361
+ const agentPromise =
362
+ provider === 'codex' ? queryCodex(prompt, agentOptions, writer)
363
+ : provider === 'gemini' ? spawnGemini(prompt, agentOptions, writer)
364
+ : provider === 'openrouter' ? queryOpenRouter(prompt, agentOptions, writer)
365
+ : queryClaudeSDK(prompt, agentOptions, writer);
366
+
367
+ await withTimeout(
368
+ agentPromise,
369
+ TASK_TIMEOUT_MS,
370
+ async () => {
371
+ if (runState.sessionId) {
372
+ await abortActiveSession(provider, runState.sessionId);
373
+ }
374
+ },
375
+ );
376
+
377
+ pipelineState = await readPipelineState(projectPath);
378
+ const taskAfterRun = pipelineState.tasks.find((entry) => String(entry.id) === String(task.id));
379
+ if (!taskAfterRun || taskAfterRun.status !== 'done') {
380
+ throw new Error(`Task ${task.id} did not transition to done after execution`);
381
+ }
382
+
383
+ autoResearchDb.updateRun(runId, {
384
+ completedTasks: pipelineState.completedTaskCount,
385
+ totalTasks: pipelineState.tasks.length,
386
+ currentTaskId: null,
387
+ });
388
+ }
389
+
390
+ autoResearchDb.updateRun(runId, {
391
+ status: 'completed',
392
+ currentTaskId: null,
393
+ completedTasks: pipelineState.completedTaskCount,
394
+ totalTasks: pipelineState.tasks.length,
395
+ finishedAt: new Date().toISOString(),
396
+ });
397
+ } catch (error) {
398
+ const isCancelled = runState.cancelRequested || /cancelled by user/i.test(String(error?.message || ''));
399
+ autoResearchDb.updateRun(runId, {
400
+ status: isCancelled ? 'cancelled' : 'failed',
401
+ error: error.message,
402
+ currentTaskId: null,
403
+ finishedAt: new Date().toISOString(),
404
+ });
405
+ } finally {
406
+ activeRuns.delete(runId);
407
+ await deliverCompletionEmail(runId, userId, projectName);
408
+ }
409
+ }
410
+
411
+ router.get('/:projectName/status', async (req, res) => {
412
+ try {
413
+ const userId = req.user.id;
414
+ const { projectName } = req.params;
415
+ const projectPath = await extractProjectDirectory(projectName);
416
+ const pipelineState = await readPipelineState(projectPath);
417
+ const profile = userDb.getProfile(userId);
418
+ const activeRun = reconcileActiveRun(autoResearchDb.getActiveRunForProject(userId, projectName));
419
+ const latestRun = autoResearchDb.getLatestRunForProject(userId, projectName);
420
+ const activeRuntime = activeRun ? activeRuns.get(activeRun.id) || null : null;
421
+ const latestRuntime = latestRun ? activeRuns.get(latestRun.id) || null : null;
422
+ const eligibility = buildEligibility(profile, pipelineState, activeRun);
423
+
424
+ const activeProvider = activeRuntime?.provider || activeRun?.provider || latestRuntime?.provider || latestRun?.provider || 'claude';
425
+ res.json({
426
+ success: true,
427
+ provider: activeProvider,
428
+ eligibility,
429
+ profile: {
430
+ notificationEmail: profile?.notification_email || null,
431
+ },
432
+ mail: {
433
+ senderEmail: appSettingsDb.get(AUTO_RESEARCH_SENDER_EMAIL_KEY),
434
+ resendConfigured: Boolean(appSettingsDb.get(AUTO_RESEARCH_RESEND_API_KEY)),
435
+ },
436
+ pipeline: {
437
+ hasResearchBrief: pipelineState.hasResearchBrief,
438
+ hasTasksFile: pipelineState.hasTasksFile,
439
+ actionableTaskCount: pipelineState.actionableTaskCount,
440
+ completedTaskCount: pipelineState.completedTaskCount,
441
+ totalTaskCount: pipelineState.tasks.length,
442
+ nextTask: pipelineState.nextTask,
443
+ },
444
+ activeRun: serializeRun(activeRun, activeRuntime),
445
+ latestRun: serializeRun(latestRun, latestRuntime),
446
+ });
447
+ } catch (error) {
448
+ console.error('[AutoResearch] Failed to get status:', error);
449
+ res.status(500).json({ error: 'Failed to get Auto Research status' });
450
+ }
451
+ });
452
+
453
+ router.post('/:projectName/start', async (req, res) => {
454
+ try {
455
+ const userId = req.user.id;
456
+ const { projectName } = req.params;
457
+ const {
458
+ provider: rawProvider,
459
+ model: rawModel,
460
+ permissionMode: rawPermissionMode,
461
+ } = req.body || {};
462
+ const provider = normalizeAutoResearchProvider(rawProvider);
463
+ const permissionMode = normalizePermissionMode(rawPermissionMode);
464
+ const model = rawModel || getDefaultModelForProvider(provider);
465
+ const projectPath = await extractProjectDirectory(projectName);
466
+ const profile = userDb.getProfile(userId);
467
+ const existingRun = autoResearchDb.getActiveRunForProject(userId, projectName);
468
+ const pipelineState = await readPipelineState(projectPath);
469
+ const eligibility = buildEligibility(profile, pipelineState, existingRun);
470
+
471
+ if (!eligibility.eligible) {
472
+ return res.status(400).json({
473
+ error: 'Project is not eligible for Auto Research',
474
+ eligibility,
475
+ });
476
+ }
477
+
478
+ const runId = crypto.randomUUID();
479
+ const run = autoResearchDb.createRun({
480
+ id: runId,
481
+ userId,
482
+ projectName,
483
+ projectPath,
484
+ provider,
485
+ status: 'queued',
486
+ completedTasks: pipelineState.completedTaskCount,
487
+ totalTasks: pipelineState.tasks.length,
488
+ metadata: {
489
+ mode: 'auto_research_v1',
490
+ autoResearchModel: model,
491
+ autoResearchPermissionMode: permissionMode,
492
+ },
493
+ });
494
+
495
+ activeRuns.set(runId, {
496
+ cancelRequested: false,
497
+ sessionId: null,
498
+ provider,
499
+ model,
500
+ permissionMode,
501
+ });
502
+
503
+ void runAutoResearch(runId, userId, projectName, projectPath);
504
+
505
+ res.json({
506
+ success: true,
507
+ run: serializeRun(run),
508
+ });
509
+ } catch (error) {
510
+ console.error('[AutoResearch] Failed to start run:', error);
511
+ res.status(500).json({ error: 'Failed to start Auto Research' });
512
+ }
513
+ });
514
+
515
+ router.post('/:projectName/cancel', async (req, res) => {
516
+ try {
517
+ const userId = req.user.id;
518
+ const { projectName } = req.params;
519
+ const activeRun = reconcileActiveRun(autoResearchDb.getActiveRunForProject(userId, projectName));
520
+
521
+ if (!activeRun) {
522
+ return res.status(404).json({ error: 'No active Auto Research run found' });
523
+ }
524
+
525
+ const runtime = activeRuns.get(activeRun.id);
526
+ const sessionStillActive = isRunSessionStillActive(activeRun);
527
+ if (runtime) {
528
+ runtime.cancelRequested = true;
529
+ if (runtime.sessionId || activeRun.session_id) {
530
+ await abortActiveSession(runtime.provider || activeRun.provider || 'claude', runtime.sessionId || activeRun.session_id);
531
+ }
532
+ }
533
+
534
+ let updatedRun;
535
+ if (!runtime && !sessionStillActive) {
536
+ updatedRun = autoResearchDb.updateRun(activeRun.id, {
537
+ status: 'cancelled',
538
+ error: activeRun.error || 'Cancelled after recovering stale Auto Research run',
539
+ currentTaskId: null,
540
+ finishedAt: activeRun.finished_at || new Date().toISOString(),
541
+ });
542
+ } else {
543
+ updatedRun = autoResearchDb.updateRun(activeRun.id, {
544
+ status: 'cancelling',
545
+ });
546
+ }
547
+
548
+ res.json({
549
+ success: true,
550
+ run: serializeRun(updatedRun),
551
+ });
552
+ } catch (error) {
553
+ console.error('[AutoResearch] Failed to cancel run:', error);
554
+ res.status(500).json({ error: 'Failed to cancel Auto Research' });
555
+ }
556
+ });
557
+
558
+ export default router;