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,958 @@
1
+ import express from 'express';
2
+ import { spawn } from 'child_process';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import fetch from 'node-fetch';
7
+ import { resolveCursorCliCommand } from '../utils/cursorCommand.js';
8
+ import { resolveAvailableCliCommand } from '../utils/cliResolution.js';
9
+
10
+ const router = express.Router();
11
+
12
+ function buildCliInstallHint(agent) {
13
+ switch (agent) {
14
+ case 'claude':
15
+ return 'Claude Code CLI is not installed. Install it first, then retry login.';
16
+ case 'cursor':
17
+ return 'Cursor CLI is not installed. Install it first, then retry login.';
18
+ case 'codex':
19
+ return 'Codex CLI is not installed. Install it first, then retry login.';
20
+ case 'gemini':
21
+ return 'Gemini CLI is not installed. Install it first, then retry login.';
22
+ case 'openrouter':
23
+ return 'Set OPENROUTER_API_KEY in your .env file. Get a key at https://openrouter.ai/keys';
24
+ default:
25
+ return 'Required CLI is not installed. Install it first, then retry login.';
26
+ }
27
+ }
28
+
29
+ function isCliMockedMissing(agent) {
30
+ const raw = process.env.MOCK_MISSING_CLIS || '';
31
+ return raw
32
+ .split(',')
33
+ .map((value) => value.trim().toLowerCase())
34
+ .filter(Boolean)
35
+ .includes(agent);
36
+ }
37
+
38
+ function buildStatusPayload(result, agent) {
39
+ return {
40
+ authenticated: Boolean(result.authenticated),
41
+ email: result.email || null,
42
+ error: result.error || null,
43
+ cliAvailable: result.cliAvailable !== false,
44
+ cliCommand: result.cliCommand || null,
45
+ installHint: result.installHint || (result.cliAvailable === false ? buildCliInstallHint(agent) : null)
46
+ };
47
+ }
48
+
49
+ router.get('/claude/status', async (req, res) => {
50
+ try {
51
+ const credentialsResult = await checkClaudeCredentials();
52
+
53
+ if (credentialsResult.authenticated) {
54
+ return res.json(buildStatusPayload({
55
+ ...credentialsResult,
56
+ email: credentialsResult.email || 'Authenticated',
57
+ method: 'cli'
58
+ }, 'claude'));
59
+ }
60
+
61
+ // Check for Custom API env var (ANTHROPIC_API_KEY is the correct var for the SDK;
62
+ // also check legacy ANTHROPIC_AUTH_TOKEN for backward compatibility)
63
+ if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) {
64
+ return res.json(buildStatusPayload({
65
+ authenticated: true,
66
+ email: 'Custom API Connected',
67
+ method: 'custom_api',
68
+ cliAvailable: credentialsResult.cliAvailable,
69
+ cliCommand: credentialsResult.cliCommand
70
+ }, 'claude'));
71
+ }
72
+
73
+ return res.json(buildStatusPayload({
74
+ authenticated: false,
75
+ email: null,
76
+ error: credentialsResult.error || 'Not authenticated'
77
+ }, 'claude'));
78
+
79
+ } catch (error) {
80
+ console.error('Error checking Claude auth status:', error);
81
+ res.status(500).json({
82
+ authenticated: false,
83
+ email: null,
84
+ error: error.message
85
+ });
86
+ }
87
+ });
88
+
89
+ router.get('/cursor/status', async (req, res) => {
90
+ try {
91
+ const result = await checkCursorStatus();
92
+
93
+ res.json(buildStatusPayload(result, 'cursor'));
94
+
95
+ } catch (error) {
96
+ console.error('Error checking Cursor auth status:', error);
97
+ res.status(500).json({
98
+ authenticated: false,
99
+ email: null,
100
+ error: error.message
101
+ });
102
+ }
103
+ });
104
+
105
+ router.get('/codex/status', async (req, res) => {
106
+ try {
107
+ const result = await checkCodexCredentials();
108
+
109
+ res.json(buildStatusPayload(result, 'codex'));
110
+
111
+ } catch (error) {
112
+ console.error('Error checking Codex auth status:', error);
113
+ res.status(500).json({
114
+ authenticated: false,
115
+ email: null,
116
+ error: error.message
117
+ });
118
+ }
119
+ });
120
+
121
+ router.get('/gemini/status', async (req, res) => {
122
+ try {
123
+ const result = await checkGeminiCredentials();
124
+
125
+ res.json(buildStatusPayload(result, 'gemini'));
126
+
127
+ } catch (error) {
128
+ console.error('Error checking Gemini auth status:', error);
129
+ res.status(500).json({
130
+ authenticated: false,
131
+ email: null,
132
+ error: error.message
133
+ });
134
+ }
135
+ });
136
+
137
+ async function checkGeminiCredentials() {
138
+ console.log('[DEBUG] Checking Gemini credentials...');
139
+ let cliCommand = process.env.GEMINI_CLI_PATH || 'gemini';
140
+ try {
141
+ if (isCliMockedMissing('gemini')) {
142
+ return {
143
+ authenticated: false,
144
+ email: null,
145
+ error: 'Gemini CLI not installed (mocked)',
146
+ cliAvailable: false,
147
+ cliCommand,
148
+ installHint: buildCliInstallHint('gemini')
149
+ };
150
+ }
151
+
152
+ const resolvedCliCommand = await resolveAvailableCliCommand({
153
+ envVarName: 'GEMINI_CLI_PATH',
154
+ defaultCommands: ['gemini'],
155
+ appendWindowsSuffixes: true
156
+ });
157
+ cliCommand = resolvedCliCommand || cliCommand;
158
+
159
+ if (!resolvedCliCommand) {
160
+ if (process.env.GOOGLE_API_KEY) {
161
+ return {
162
+ authenticated: true,
163
+ email: 'API Key Connected',
164
+ method: 'custom_api',
165
+ cliAvailable: false,
166
+ cliCommand,
167
+ installHint: buildCliInstallHint('gemini')
168
+ };
169
+ }
170
+ return {
171
+ authenticated: false,
172
+ email: null,
173
+ error: 'Gemini CLI not installed',
174
+ cliAvailable: false,
175
+ cliCommand,
176
+ installHint: buildCliInstallHint('gemini')
177
+ };
178
+ }
179
+
180
+ // Check for GOOGLE_API_KEY environment variable first
181
+ if (process.env.GOOGLE_API_KEY) {
182
+ console.log('[DEBUG] Gemini: Found GOOGLE_API_KEY in environment');
183
+ return {
184
+ authenticated: true,
185
+ email: 'API Key (Env)',
186
+ cliAvailable: true,
187
+ cliCommand
188
+ };
189
+ }
190
+
191
+ // Check for OAuth credentials file (new Gemini CLI)
192
+ const oauthPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
193
+ console.log(`[DEBUG] Gemini: Checking for OAuth file at ${oauthPath}`);
194
+ try {
195
+ const content = await fs.readFile(oauthPath, 'utf8');
196
+ const creds = JSON.parse(content);
197
+
198
+ // Check for presence of refresh_token or access_token
199
+ if (creds.refresh_token || creds.access_token) {
200
+ let email = creds.email || 'OAuth (Config)';
201
+
202
+ // Try to extract email from id_token if available
203
+ if (!creds.email && creds.id_token) {
204
+ try {
205
+ const parts = creds.id_token.split('.');
206
+ if (parts.length >= 2) {
207
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
208
+ email = payload.email || email;
209
+ }
210
+ } catch (jwtError) {
211
+ console.warn('[DEBUG] Gemini: Failed to decode id_token', jwtError.message);
212
+ }
213
+ }
214
+
215
+ console.log(`[DEBUG] Gemini: Authenticated via OAuth as ${email}`);
216
+ return {
217
+ authenticated: true,
218
+ email: email,
219
+ cliAvailable: true,
220
+ cliCommand
221
+ };
222
+ } else {
223
+ console.log('[DEBUG] Gemini: OAuth file found but no tokens present');
224
+ }
225
+ } catch (e) {
226
+ if (e.code !== 'ENOENT') {
227
+ console.error('[DEBUG] Gemini: Error reading oauth_creds.json', e.message);
228
+ } else {
229
+ console.log('[DEBUG] Gemini: oauth_creds.json not found');
230
+ }
231
+ }
232
+
233
+ // Fallback to legacy config file check
234
+ const configPath = path.join(os.homedir(), '.gemini', 'config.json');
235
+ console.log(`[DEBUG] Gemini: Checking for legacy config at ${configPath}`);
236
+ try {
237
+ const content = await fs.readFile(configPath, 'utf8');
238
+ const config = JSON.parse(content);
239
+
240
+ if (config.apiKey || config.GOOGLE_API_KEY) {
241
+ console.log('[DEBUG] Gemini: Authenticated via legacy config API Key');
242
+ return {
243
+ authenticated: true,
244
+ email: 'API Key (Config)',
245
+ cliAvailable: true,
246
+ cliCommand
247
+ };
248
+ }
249
+ } catch (e) {
250
+ if (e.code !== 'ENOENT') {
251
+ console.error('[DEBUG] Gemini: Error reading config.json', e.message);
252
+ }
253
+ }
254
+
255
+ console.log('[DEBUG] Gemini: Not authenticated (no valid credentials found)');
256
+ return {
257
+ authenticated: false,
258
+ email: null,
259
+ error: 'Gemini not configured',
260
+ cliAvailable: true,
261
+ cliCommand
262
+ };
263
+ } catch (error) {
264
+ console.error('[DEBUG] Gemini: Unexpected error during auth check', error);
265
+ if (error.code === 'ENOENT') {
266
+ return {
267
+ authenticated: false,
268
+ email: null,
269
+ error: 'Gemini not configured',
270
+ cliAvailable: true,
271
+ cliCommand
272
+ };
273
+ }
274
+ return {
275
+ authenticated: false,
276
+ email: null,
277
+ error: error.message,
278
+ cliAvailable: true,
279
+ cliCommand
280
+ };
281
+ }
282
+ }
283
+
284
+ async function checkClaudeCredentials() {
285
+ const resolvedCliCommand = await resolveAvailableCliCommand({
286
+ envVarName: 'CLAUDE_CLI_PATH',
287
+ defaultCommands: ['claude'],
288
+ appendWindowsSuffixes: true
289
+ });
290
+
291
+ if (!resolvedCliCommand) {
292
+ return checkClaudeCredentialsFile({ cliAvailable: false });
293
+ }
294
+
295
+ return new Promise((resolve) => {
296
+ let processCompleted = false;
297
+
298
+ const timeout = setTimeout(() => {
299
+ if (!processCompleted) {
300
+ processCompleted = true;
301
+ if (childProcess) {
302
+ childProcess.kill();
303
+ }
304
+ // Fall back to credentials file check on timeout
305
+ checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
306
+ }
307
+ }, 5000);
308
+
309
+ let childProcess;
310
+ try {
311
+ childProcess = spawn(resolvedCliCommand, ['auth', 'status', '--json'], {
312
+ env: { ...process.env, CLAUDECODE: '' },
313
+ shell: process.platform === 'win32'
314
+ });
315
+ } catch {
316
+ clearTimeout(timeout);
317
+ checkClaudeCredentialsFile({ cliAvailable: false }).then(resolve);
318
+ return;
319
+ }
320
+
321
+ let stdout = '';
322
+ let stderr = '';
323
+
324
+ childProcess.stdout.on('data', (data) => {
325
+ stdout += data.toString();
326
+ });
327
+
328
+ childProcess.stderr.on('data', (data) => {
329
+ stderr += data.toString();
330
+ });
331
+
332
+ childProcess.on('close', (code) => {
333
+ if (processCompleted) return;
334
+ processCompleted = true;
335
+ clearTimeout(timeout);
336
+
337
+ if (code === 0 && stdout.trim()) {
338
+ try {
339
+ const status = JSON.parse(stdout.trim());
340
+ if (status.loggedIn) {
341
+ resolve({
342
+ authenticated: true,
343
+ email: status.email || null,
344
+ cliAvailable: true,
345
+ cliCommand: resolvedCliCommand
346
+ });
347
+ return;
348
+ }
349
+ } catch {
350
+ // JSON parse failed, fall through
351
+ }
352
+ }
353
+
354
+ // CLI check failed, fall back to credentials file
355
+ checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
356
+ });
357
+
358
+ childProcess.on('error', () => {
359
+ if (processCompleted) return;
360
+ processCompleted = true;
361
+ clearTimeout(timeout);
362
+ // Command was already validated by resolveAvailableCliCommand, so treat
363
+ // any spawn error as a transient failure rather than "CLI missing".
364
+ checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
365
+ });
366
+ });
367
+ }
368
+
369
+ async function checkClaudeCredentialsFile({ cliAvailable = true, cliCommand = 'claude' } = {}) {
370
+ try {
371
+ const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
372
+ const content = await fs.readFile(credPath, 'utf8');
373
+ const creds = JSON.parse(content);
374
+
375
+ const oauth = creds.claudeAiOauth;
376
+ if (oauth && oauth.accessToken) {
377
+ const isExpired = oauth.expiresAt && Date.now() >= oauth.expiresAt;
378
+
379
+ if (!isExpired) {
380
+ return {
381
+ authenticated: true,
382
+ email: creds.email || creds.user || null,
383
+ cliAvailable,
384
+ cliCommand
385
+ };
386
+ }
387
+ }
388
+
389
+ return {
390
+ authenticated: false,
391
+ email: null,
392
+ cliAvailable,
393
+ cliCommand,
394
+ error: cliAvailable ? null : 'Claude Code CLI not installed',
395
+ installHint: cliAvailable ? null : buildCliInstallHint('claude')
396
+ };
397
+ } catch (error) {
398
+ return {
399
+ authenticated: false,
400
+ email: null,
401
+ cliAvailable,
402
+ cliCommand,
403
+ error: cliAvailable ? null : 'Claude Code CLI not installed',
404
+ installHint: cliAvailable ? null : buildCliInstallHint('claude')
405
+ };
406
+ }
407
+ }
408
+
409
+ function checkCursorStatus() {
410
+ return new Promise((resolve) => {
411
+ let processCompleted = false;
412
+ const cursorCommand = resolveCursorCliCommand();
413
+
414
+ if (!cursorCommand) {
415
+ resolve({
416
+ authenticated: false,
417
+ email: null,
418
+ error: 'Cursor CLI not installed',
419
+ cliAvailable: false,
420
+ cliCommand: process.env.CURSOR_CLI_PATH || 'agent',
421
+ installHint: buildCliInstallHint('cursor')
422
+ });
423
+ return;
424
+ }
425
+
426
+ const timeout = setTimeout(() => {
427
+ if (!processCompleted) {
428
+ processCompleted = true;
429
+ if (childProcess) {
430
+ childProcess.kill();
431
+ }
432
+ resolve({
433
+ authenticated: false,
434
+ email: null,
435
+ error: 'Command timeout'
436
+ });
437
+ }
438
+ }, 5000);
439
+
440
+ let childProcess;
441
+ childProcess = spawn(cursorCommand, ['status']);
442
+
443
+ let stdout = '';
444
+ let stderr = '';
445
+
446
+ childProcess.stdout.on('data', (data) => {
447
+ stdout += data.toString();
448
+ });
449
+
450
+ childProcess.stderr.on('data', (data) => {
451
+ stderr += data.toString();
452
+ });
453
+
454
+ childProcess.on('close', (code) => {
455
+ if (processCompleted) return;
456
+ processCompleted = true;
457
+ clearTimeout(timeout);
458
+
459
+ if (code === 0) {
460
+ const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
461
+
462
+ if (emailMatch) {
463
+ resolve({
464
+ authenticated: true,
465
+ email: emailMatch[1],
466
+ output: stdout,
467
+ cliAvailable: true,
468
+ cliCommand: cursorCommand
469
+ });
470
+ } else if (stdout.includes('Logged in')) {
471
+ resolve({
472
+ authenticated: true,
473
+ email: 'Logged in',
474
+ output: stdout,
475
+ cliAvailable: true,
476
+ cliCommand: cursorCommand
477
+ });
478
+ } else {
479
+ resolve({
480
+ authenticated: false,
481
+ email: null,
482
+ error: 'Not logged in',
483
+ cliAvailable: true,
484
+ cliCommand: cursorCommand
485
+ });
486
+ }
487
+ } else {
488
+ resolve({
489
+ authenticated: false,
490
+ email: null,
491
+ error: stderr || 'Not logged in',
492
+ cliAvailable: true,
493
+ cliCommand: cursorCommand
494
+ });
495
+ }
496
+ });
497
+
498
+ childProcess.on('error', (err) => {
499
+ if (processCompleted) return;
500
+ processCompleted = true;
501
+ clearTimeout(timeout);
502
+
503
+ resolve({
504
+ authenticated: false,
505
+ email: null,
506
+ error: 'Cursor CLI not installed',
507
+ cliAvailable: false,
508
+ cliCommand: cursorCommand,
509
+ installHint: buildCliInstallHint('cursor')
510
+ });
511
+ });
512
+ });
513
+ }
514
+
515
+ async function checkCodexCredentials() {
516
+ let cliCommand = process.env.CODEX_CLI_PATH || 'codex';
517
+ try {
518
+ const resolvedCliCommand = await resolveAvailableCliCommand({
519
+ envVarName: 'CODEX_CLI_PATH',
520
+ defaultCommands: ['codex'],
521
+ appendWindowsSuffixes: true
522
+ });
523
+ cliCommand = resolvedCliCommand || cliCommand;
524
+
525
+ if (!resolvedCliCommand) {
526
+ // Even without CLI, an OPENAI_API_KEY env var means Codex SDK can work
527
+ if (process.env.OPENAI_API_KEY) {
528
+ return {
529
+ authenticated: true,
530
+ email: 'API Key Connected',
531
+ method: 'custom_api',
532
+ cliAvailable: false,
533
+ cliCommand,
534
+ installHint: buildCliInstallHint('codex')
535
+ };
536
+ }
537
+ return {
538
+ authenticated: false,
539
+ email: null,
540
+ error: 'Codex CLI not installed',
541
+ cliAvailable: false,
542
+ cliCommand,
543
+ installHint: buildCliInstallHint('codex')
544
+ };
545
+ }
546
+
547
+ const authPath = path.join(os.homedir(), '.codex', 'auth.json');
548
+ const content = await fs.readFile(authPath, 'utf8');
549
+ const auth = JSON.parse(content);
550
+
551
+ // Tokens are nested under 'tokens' key
552
+ const tokens = auth.tokens || {};
553
+
554
+ // Check for valid tokens (id_token or access_token)
555
+ if (tokens.id_token || tokens.access_token) {
556
+ // Try to extract email from id_token JWT payload
557
+ let email = 'Authenticated';
558
+ if (tokens.id_token) {
559
+ try {
560
+ // JWT is base64url encoded: header.payload.signature
561
+ const parts = tokens.id_token.split('.');
562
+ if (parts.length >= 2) {
563
+ // Decode the payload (second part)
564
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
565
+ email = payload.email || payload.user || 'Authenticated';
566
+ }
567
+ } catch {
568
+ // If JWT decoding fails, use fallback
569
+ email = 'Authenticated';
570
+ }
571
+ }
572
+
573
+ return {
574
+ authenticated: true,
575
+ email,
576
+ cliAvailable: true,
577
+ cliCommand
578
+ };
579
+ }
580
+
581
+ // Also check for OPENAI_API_KEY as fallback auth method (in auth.json or env)
582
+ if (auth.OPENAI_API_KEY || process.env.OPENAI_API_KEY) {
583
+ return {
584
+ authenticated: true,
585
+ email: 'API Key Connected',
586
+ method: 'custom_api',
587
+ cliAvailable: true,
588
+ cliCommand
589
+ };
590
+ }
591
+
592
+ return {
593
+ authenticated: false,
594
+ email: null,
595
+ error: 'No valid tokens found',
596
+ cliAvailable: true,
597
+ cliCommand
598
+ };
599
+ } catch (error) {
600
+ // File not found — check env var before giving up
601
+ if (process.env.OPENAI_API_KEY) {
602
+ return {
603
+ authenticated: true,
604
+ email: 'API Key Connected',
605
+ method: 'custom_api',
606
+ cliAvailable: true,
607
+ cliCommand
608
+ };
609
+ }
610
+ if (error.code === 'ENOENT') {
611
+ return {
612
+ authenticated: false,
613
+ email: null,
614
+ error: 'Codex not configured',
615
+ cliAvailable: true,
616
+ cliCommand
617
+ };
618
+ }
619
+ return {
620
+ authenticated: false,
621
+ email: null,
622
+ error: error.message,
623
+ cliAvailable: true,
624
+ cliCommand
625
+ };
626
+ }
627
+ }
628
+
629
+ router.get('/openrouter/status', async (req, res) => {
630
+ try {
631
+ const apiKey = process.env.OPENROUTER_API_KEY;
632
+ if (apiKey) {
633
+ return res.json(buildStatusPayload({
634
+ authenticated: true,
635
+ email: 'API Key Connected',
636
+ cliAvailable: true,
637
+ cliCommand: 'openrouter'
638
+ }, 'openrouter'));
639
+ }
640
+
641
+ return res.json(buildStatusPayload({
642
+ authenticated: false,
643
+ email: null,
644
+ error: 'OPENROUTER_API_KEY not set',
645
+ cliAvailable: true,
646
+ cliCommand: 'openrouter',
647
+ installHint: 'Set OPENROUTER_API_KEY in your .env file or environment. Get a key at https://openrouter.ai/keys'
648
+ }, 'openrouter'));
649
+ } catch (error) {
650
+ console.error('Error checking OpenRouter auth status:', error);
651
+ res.status(500).json({
652
+ authenticated: false,
653
+ email: null,
654
+ error: error.message
655
+ });
656
+ }
657
+ });
658
+
659
+ router.post('/openrouter/verify-api-key', async (req, res) => {
660
+ try {
661
+ const { apiKey } = req.body;
662
+ if (!apiKey) return res.status(400).json({ error: 'API key is required' });
663
+
664
+ const response = await fetch('https://openrouter.ai/api/v1/models', {
665
+ headers: { 'Authorization': `Bearer ${apiKey}` }
666
+ });
667
+
668
+ if (response.ok) {
669
+ const envPath = path.join(process.cwd(), '.env');
670
+ let envContent = '';
671
+ try { envContent = await fs.readFile(envPath, 'utf8'); } catch {}
672
+
673
+ const lines = envContent.split('\n');
674
+ let found = false;
675
+ const newLines = lines.map(line => {
676
+ if (line.trim().startsWith('OPENROUTER_API_KEY=')) {
677
+ found = true;
678
+ return `OPENROUTER_API_KEY=${apiKey}`;
679
+ }
680
+ return line;
681
+ }).filter(l => l.trim() !== '' || found);
682
+
683
+ if (!found) newLines.push(`OPENROUTER_API_KEY=${apiKey}`);
684
+ await fs.writeFile(envPath, newLines.join('\n') + '\n');
685
+ process.env.OPENROUTER_API_KEY = apiKey;
686
+
687
+ return res.json({ success: true, message: 'OpenRouter API key verified and saved.' });
688
+ } else {
689
+ return res.status(401).json({ error: 'Invalid API key' });
690
+ }
691
+ } catch (error) {
692
+ res.status(500).json({ error: error.message });
693
+ }
694
+ });
695
+
696
+ router.post('/gemini/verify-api-key', async (req, res) => {
697
+ try {
698
+ const { apiKey } = req.body;
699
+ if (!apiKey) return res.status(400).json({ error: 'API key is required' });
700
+
701
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
702
+
703
+ if (response.ok) {
704
+ const envPath = path.join(process.cwd(), '.env');
705
+ let envContent = '';
706
+ try { envContent = await fs.readFile(envPath, 'utf8'); } catch {}
707
+
708
+ const lines = envContent.split('\n');
709
+ let found = false;
710
+ const newLines = lines.map(line => {
711
+ if (line.trim().startsWith('GOOGLE_API_KEY=')) {
712
+ found = true;
713
+ return `GOOGLE_API_KEY=${apiKey}`;
714
+ }
715
+ return line;
716
+ }).filter(l => l.trim() !== '' || found);
717
+
718
+ if (!found) newLines.push(`GOOGLE_API_KEY=${apiKey}`);
719
+ await fs.writeFile(envPath, newLines.join('\n') + '\n');
720
+ process.env.GOOGLE_API_KEY = apiKey;
721
+
722
+ return res.json({ success: true, message: 'Google API key verified and saved.' });
723
+ } else {
724
+ return res.status(401).json({ error: 'Invalid API key' });
725
+ }
726
+ } catch (error) {
727
+ res.status(500).json({ error: error.message });
728
+ }
729
+ });
730
+
731
+ router.post('/codex/verify-api-key', async (req, res) => {
732
+ try {
733
+ const { apiKey } = req.body;
734
+ if (!apiKey) return res.status(400).json({ error: 'API key is required' });
735
+
736
+ const response = await fetch('https://api.openai.com/v1/models', {
737
+ headers: { 'Authorization': `Bearer ${apiKey}` }
738
+ });
739
+
740
+ if (response.ok) {
741
+ const envPath = path.join(process.cwd(), '.env');
742
+ let envContent = '';
743
+ try { envContent = await fs.readFile(envPath, 'utf8'); } catch {}
744
+
745
+ const lines = envContent.split('\n');
746
+ let found = false;
747
+ const newLines = lines.map(line => {
748
+ if (line.trim().startsWith('OPENAI_API_KEY=')) {
749
+ found = true;
750
+ return `OPENAI_API_KEY=${apiKey}`;
751
+ }
752
+ return line;
753
+ }).filter(l => l.trim() !== '' || found);
754
+
755
+ if (!found) newLines.push(`OPENAI_API_KEY=${apiKey}`);
756
+ await fs.writeFile(envPath, newLines.join('\n') + '\n');
757
+ process.env.OPENAI_API_KEY = apiKey;
758
+
759
+ return res.json({ success: true, message: 'OpenAI API key verified and saved.' });
760
+ } else {
761
+ return res.status(401).json({ error: 'Invalid API key' });
762
+ }
763
+ } catch (error) {
764
+ res.status(500).json({ error: error.message });
765
+ }
766
+ });
767
+
768
+ router.post('/claude/verify-custom-api', async (req, res) => {
769
+ try {
770
+ const { baseUrl, token } = req.body;
771
+ if (!token) return res.status(400).json({ error: 'Token is required' });
772
+
773
+ const verifyUrl = `${baseUrl || 'https://api.anthropic.com'}/v1/messages`;
774
+ const response = await fetch(verifyUrl, {
775
+ method: 'POST',
776
+ headers: {
777
+ 'Content-Type': 'application/json',
778
+ 'x-api-key': token,
779
+ 'anthropic-version': '2023-06-01'
780
+ },
781
+ body: JSON.stringify({
782
+ model: 'claude-sonnet-4-20250514',
783
+ max_tokens: 1,
784
+ messages: [{ role: 'user', content: 'hi' }]
785
+ })
786
+ });
787
+
788
+ // 200 = success, 401/403 = bad token, anything else with a valid JSON body means the endpoint is reachable
789
+ if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 401 && response.status !== 403)) {
790
+ const envPath = path.join(process.cwd(), '.env');
791
+ let envContent = '';
792
+ try {
793
+ envContent = await fs.readFile(envPath, 'utf8');
794
+ } catch (e) { /* ignore if not exists */ }
795
+
796
+ const keysToUpdate = {
797
+ 'ANTHROPIC_BASE_URL': baseUrl || 'https://api.anthropic.com',
798
+ 'ANTHROPIC_API_KEY': token,
799
+ 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS': '1'
800
+ };
801
+
802
+ const keysToRemove = new Set(['ANTHROPIC_AUTH_TOKEN']);
803
+
804
+ const newLines = [];
805
+ const existingKeys = new Set();
806
+ envContent.split('\n').forEach(line => {
807
+ const [key] = line.split('=');
808
+ const trimmedKey = key.trim();
809
+ if (keysToRemove.has(trimmedKey)) {
810
+ return;
811
+ }
812
+ if (keysToUpdate[trimmedKey]) {
813
+ newLines.push(`${trimmedKey}=${keysToUpdate[trimmedKey]}`);
814
+ existingKeys.add(trimmedKey);
815
+ } else if (line.trim()) {
816
+ newLines.push(line);
817
+ }
818
+ });
819
+
820
+ Object.entries(keysToUpdate).forEach(([key, val]) => {
821
+ if (!existingKeys.has(key)) {
822
+ newLines.push(`${key}=${val}`);
823
+ }
824
+ });
825
+
826
+ await fs.writeFile(envPath, newLines.join('\n') + '\n');
827
+
828
+ Object.entries(keysToUpdate).forEach(([key, val]) => {
829
+ process.env[key] = val;
830
+ });
831
+ delete process.env.ANTHROPIC_AUTH_TOKEN;
832
+
833
+ return res.json({ success: true, message: 'Custom API verified and applied.' });
834
+ } else {
835
+ const err = await response.text();
836
+ return res.status(response.status).json({ error: `Verification failed: ${err}` });
837
+ }
838
+ } catch (error) {
839
+ res.status(500).json({ error: error.message });
840
+ }
841
+ });
842
+
843
+ // ---------------------------------------------------------------------------
844
+ // Local GPU / Ollama routes
845
+ // ---------------------------------------------------------------------------
846
+
847
+ import { detectGPUs, checkOllamaStatus, pullOllamaModel } from '../local-gpu.js';
848
+
849
+ router.get('/local/gpu-info', async (req, res) => {
850
+ try {
851
+ const result = await detectGPUs();
852
+ return res.json(result);
853
+ } catch (error) {
854
+ res.status(500).json({ error: error.message });
855
+ }
856
+ });
857
+
858
+ router.get('/local/status', async (req, res) => {
859
+ try {
860
+ const serverUrl = process.env.LOCAL_GPU_SERVER_URL || 'http://localhost:11434';
861
+ const status = await checkOllamaStatus(serverUrl);
862
+
863
+ if (status.running) {
864
+ const gpus = await detectGPUs().catch(() => ({ gpus: [] }));
865
+ const hasGpu = gpus.gpus && gpus.gpus.length > 0;
866
+
867
+ return res.json(buildStatusPayload({
868
+ authenticated: true,
869
+ email: `Ollama · ${status.models.length} model${status.models.length !== 1 ? 's' : ''}${hasGpu ? ' · GPU detected' : ''}`,
870
+ cliAvailable: true,
871
+ cliCommand: null,
872
+ }, 'local'));
873
+ }
874
+
875
+ return res.json(buildStatusPayload({
876
+ authenticated: false,
877
+ email: null,
878
+ error: status.error,
879
+ cliAvailable: true,
880
+ cliCommand: null,
881
+ installHint: 'Install Ollama from https://ollama.com and run: ollama serve',
882
+ }, 'local'));
883
+ } catch (error) {
884
+ res.status(500).json({ error: error.message });
885
+ }
886
+ });
887
+
888
+ router.get('/local/models', async (req, res) => {
889
+ try {
890
+ const serverUrl = req.query.serverUrl || process.env.LOCAL_GPU_SERVER_URL || 'http://localhost:11434';
891
+ const status = await checkOllamaStatus(serverUrl);
892
+ if (!status.running) {
893
+ return res.status(503).json({ error: 'Ollama is not running', models: [] });
894
+ }
895
+
896
+ const gpus = await detectGPUs().catch(() => ({ gpus: [] }));
897
+ const hasGpu = gpus.gpus && gpus.gpus.length > 0;
898
+ let maxVramMb = 0;
899
+ if (hasGpu) {
900
+ for (const gpu of gpus.gpus) {
901
+ if (gpu.memoryTotal) maxVramMb = Math.max(maxVramMb, gpu.memoryTotal);
902
+ }
903
+ }
904
+
905
+ return res.json({
906
+ models: status.models,
907
+ hasGpu,
908
+ maxVramMb,
909
+ gpuCount: gpus.gpus.length,
910
+ });
911
+ } catch (error) {
912
+ res.status(500).json({ error: error.message });
913
+ }
914
+ });
915
+
916
+ router.post('/local/pull-model', async (req, res) => {
917
+ try {
918
+ const { modelName, serverUrl } = req.body;
919
+ if (!modelName) return res.status(400).json({ error: 'modelName is required' });
920
+
921
+ const url = serverUrl || process.env.LOCAL_GPU_SERVER_URL || 'http://localhost:11434';
922
+ await pullOllamaModel(url, modelName);
923
+ return res.json({ success: true, message: `Model "${modelName}" pulled successfully.` });
924
+ } catch (error) {
925
+ res.status(500).json({ error: error.message });
926
+ }
927
+ });
928
+
929
+ router.post('/local/save-config', async (req, res) => {
930
+ try {
931
+ const { serverUrl } = req.body;
932
+ if (serverUrl) {
933
+ const envPath = path.join(process.cwd(), '.env');
934
+ let envContent = '';
935
+ try { envContent = await fs.readFile(envPath, 'utf8'); } catch {}
936
+
937
+ const lines = envContent.split('\n');
938
+ let found = false;
939
+ const newLines = lines.map(line => {
940
+ if (line.trim().startsWith('LOCAL_GPU_SERVER_URL=')) {
941
+ found = true;
942
+ return `LOCAL_GPU_SERVER_URL=${serverUrl}`;
943
+ }
944
+ return line;
945
+ }).filter(l => l.trim() !== '' || found);
946
+
947
+ if (!found) newLines.push(`LOCAL_GPU_SERVER_URL=${serverUrl}`);
948
+ await fs.writeFile(envPath, newLines.join('\n') + '\n');
949
+ process.env.LOCAL_GPU_SERVER_URL = serverUrl;
950
+ }
951
+
952
+ return res.json({ success: true, message: 'Local GPU configuration saved.' });
953
+ } catch (error) {
954
+ res.status(500).json({ error: error.message });
955
+ }
956
+ });
957
+
958
+ export default router;