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.
- package/LICENSE +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- 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;
|