dr-claw 1.1.2 → 1.1.4
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/README.md +286 -177
- package/README.zh-CN.md +286 -174
- package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-Blm-emkR.js → graph-DGtZXuDY.js} +1 -1
- package/dist/assets/index-C-sL-ag0.css +1 -0
- package/dist/assets/index-VdqxIFPL.js +670 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/screenshot_v1_2.png +0 -0
- package/package.json +61 -4
- package/public/icons/nanoclaude.png +0 -0
- package/public/screenshots/screenshot_v1_2.png +0 -0
- package/scripts/build-electron-icons.mjs +40 -0
- package/scripts/check-deps.js +28 -0
- package/scripts/native-runtime.mjs +128 -0
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/__tests__/project-sync-dedup.test.mjs +173 -0
- package/server/__tests__/session-delete.test.mjs +152 -0
- package/server/claude-sdk.js +175 -14
- package/server/cli-chat.js +28 -17
- package/server/cli.js +48 -11
- package/server/data/news-log-arxiv.json +1 -0
- package/server/data/news-log-huggingface.json +1 -0
- package/server/data/news-results-arxiv.json +460 -0
- package/server/data/news-results-huggingface.json +817 -0
- package/server/data/research_interests_arxiv.json +50 -0
- package/server/data/research_interests_huggingface.json +4 -0
- package/server/database/db.js +101 -5
- package/server/database/init.sql +17 -1
- package/server/gemini-api.js +1838 -0
- package/server/gemini-cli.js +13 -2
- package/server/index.js +94 -16
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +166 -24
- package/server/mcp-compute.js +223 -0
- package/server/middleware/auth.js +25 -6
- package/server/nano-claude-code.js +340 -0
- package/server/nanoSessionPaths.js +41 -0
- package/server/openai-codex.js +6 -2
- package/server/openrouter.js +37 -22
- package/server/projects.js +516 -110
- package/server/routes/agent.js +32 -7
- package/server/routes/auto-research.js +1 -1
- package/server/routes/claude-btw.js +55 -0
- package/server/routes/cli-auth.js +422 -58
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +398 -0
- package/server/routes/compute.js +155 -0
- package/server/routes/git.js +9 -11
- package/server/routes/memory.js +143 -0
- package/server/routes/news.js +3 -0
- package/server/routes/skills.js +35 -0
- package/server/routes/user.js +3 -5
- package/server/utils/__tests__/safePath.test.js +93 -0
- package/server/utils/cliResolution.js +13 -1
- package/server/utils/codexSessionMessages.js +285 -0
- package/server/utils/geminiOAuth.js +333 -0
- package/server/utils/memoryPrompt.js +78 -0
- package/server/utils/safePath.js +51 -0
- package/server/utils/sessionIndex.js +2 -0
- package/server/utils/skillExpander.js +142 -0
- package/server/utils/spawnAsync.js +78 -0
- package/shared/modelConstants.js +19 -1
- package/dist/assets/channel-BFOE7OyC.js +0 -1
- package/dist/assets/clone-BulQSblH.js +0 -1
- package/dist/assets/index-B8Yutsnm.css +0 -1
- package/dist/assets/index-Cf6gyhc3.js +0 -654
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
- package/scripts/install-python-tools.js +0 -64
|
@@ -6,10 +6,114 @@ import os from 'os';
|
|
|
6
6
|
import fetch from 'node-fetch';
|
|
7
7
|
import { resolveCursorCliCommand } from '../utils/cursorCommand.js';
|
|
8
8
|
import { resolveAvailableCliCommand } from '../utils/cliResolution.js';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_OLLAMA_URL,
|
|
11
|
+
detectGPUs,
|
|
12
|
+
checkOllamaStatus,
|
|
13
|
+
pullOllamaModel,
|
|
14
|
+
normalizeLocalOllamaBaseUrl,
|
|
15
|
+
} from '../local-gpu.js';
|
|
9
16
|
|
|
10
17
|
const router = express.Router();
|
|
11
18
|
|
|
19
|
+
const PROVIDER_INSTALLERS = {
|
|
20
|
+
claude: {
|
|
21
|
+
displayName: 'Claude Code CLI',
|
|
22
|
+
docsUrl: 'https://docs.anthropic.com/en/docs/claude-code/overview',
|
|
23
|
+
fallbackDownloadUrl: 'https://docs.anthropic.com/en/docs/claude-code/overview',
|
|
24
|
+
commands: {
|
|
25
|
+
darwin: [
|
|
26
|
+
{ bin: 'brew', args: ['install', 'claude-code'], label: 'brew install claude-code' },
|
|
27
|
+
],
|
|
28
|
+
linux: [
|
|
29
|
+
{ bin: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'], label: 'npm install -g @anthropic-ai/claude-code' },
|
|
30
|
+
],
|
|
31
|
+
win32: [
|
|
32
|
+
{ bin: 'winget', args: ['install', '--id', 'Anthropic.ClaudeCode', '-e'], label: 'winget install Anthropic.ClaudeCode' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
cursor: {
|
|
37
|
+
displayName: 'Cursor CLI',
|
|
38
|
+
docsUrl: 'https://cursor.com/downloads',
|
|
39
|
+
fallbackDownloadUrl: 'https://cursor.com/downloads',
|
|
40
|
+
commands: {
|
|
41
|
+
darwin: [
|
|
42
|
+
{ bin: 'brew', args: ['install', '--cask', 'cursor'], label: 'brew install --cask cursor' },
|
|
43
|
+
],
|
|
44
|
+
win32: [
|
|
45
|
+
{ bin: 'winget', args: ['install', '--id', 'CursorAI,Inc.Cursor', '-e'], label: 'winget install CursorAI,Inc.Cursor' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
codex: {
|
|
50
|
+
displayName: 'OpenAI Codex CLI',
|
|
51
|
+
docsUrl: 'https://github.com/openai/codex',
|
|
52
|
+
fallbackDownloadUrl: 'https://github.com/openai/codex',
|
|
53
|
+
commands: {
|
|
54
|
+
darwin: [
|
|
55
|
+
{ bin: 'npm', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
56
|
+
],
|
|
57
|
+
linux: [
|
|
58
|
+
{ bin: 'npm', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
59
|
+
],
|
|
60
|
+
win32: [
|
|
61
|
+
{ bin: 'npm.cmd', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
gemini: {
|
|
66
|
+
displayName: 'Gemini CLI',
|
|
67
|
+
docsUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
68
|
+
fallbackDownloadUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
69
|
+
commands: {
|
|
70
|
+
darwin: [
|
|
71
|
+
{ bin: 'npm', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
72
|
+
],
|
|
73
|
+
linux: [
|
|
74
|
+
{ bin: 'npm', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
75
|
+
],
|
|
76
|
+
win32: [
|
|
77
|
+
{ bin: 'npm.cmd', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
nano: {
|
|
82
|
+
displayName: 'Nano Claude Code',
|
|
83
|
+
docsUrl: 'https://github.com/OpenLAIR/nano-claude-code',
|
|
84
|
+
fallbackDownloadUrl: 'https://github.com/OpenLAIR/nano-claude-code',
|
|
85
|
+
commands: {
|
|
86
|
+
darwin: [
|
|
87
|
+
{
|
|
88
|
+
bin: 'pip3',
|
|
89
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
90
|
+
label: 'pip3 install from GitHub (recommended)',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
linux: [
|
|
94
|
+
{
|
|
95
|
+
bin: 'pip3',
|
|
96
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
97
|
+
label: 'pip3 install from GitHub (use a venv if your distro manages system Python)',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
win32: [
|
|
101
|
+
{
|
|
102
|
+
bin: 'pip',
|
|
103
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
104
|
+
label: 'pip install from GitHub',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
12
111
|
function buildCliInstallHint(agent) {
|
|
112
|
+
const installer = PROVIDER_INSTALLERS[agent];
|
|
113
|
+
if (installer?.docsUrl) {
|
|
114
|
+
return `${installer.displayName} is not installed. Install it in Dr. Claw or open the official guide: ${installer.docsUrl}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
13
117
|
switch (agent) {
|
|
14
118
|
case 'claude':
|
|
15
119
|
return 'Claude Code CLI is not installed. Install it first, then retry login.';
|
|
@@ -21,11 +125,91 @@ function buildCliInstallHint(agent) {
|
|
|
21
125
|
return 'Gemini CLI is not installed. Install it first, then retry login.';
|
|
22
126
|
case 'openrouter':
|
|
23
127
|
return 'Set OPENROUTER_API_KEY in your .env file. Get a key at https://openrouter.ai/keys';
|
|
128
|
+
case 'nano':
|
|
129
|
+
return 'nano-claude-code is not on PATH. Install from https://github.com/OpenLAIR/nano-claude-code or set NANO_CLAUDE_CODE_COMMAND to the executable.';
|
|
24
130
|
default:
|
|
25
131
|
return 'Required CLI is not installed. Install it first, then retry login.';
|
|
26
132
|
}
|
|
27
133
|
}
|
|
28
134
|
|
|
135
|
+
function getInstallerDefinition(agent) {
|
|
136
|
+
return PROVIDER_INSTALLERS[agent] || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getPlatformInstallCommands(agent, platform = process.platform) {
|
|
140
|
+
const definition = getInstallerDefinition(agent);
|
|
141
|
+
if (!definition) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return definition.commands?.[platform] || [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runInstallCommand(command, { cwd = process.cwd(), timeoutMs = 15 * 60 * 1000 } = {}) {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
let completed = false;
|
|
151
|
+
const child = spawn(command.bin, command.args, {
|
|
152
|
+
cwd,
|
|
153
|
+
env: process.env,
|
|
154
|
+
shell: process.platform === 'win32',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let stdout = '';
|
|
158
|
+
let stderr = '';
|
|
159
|
+
|
|
160
|
+
const finish = (result) => {
|
|
161
|
+
if (completed) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
completed = true;
|
|
165
|
+
resolve(result);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
if (!completed) {
|
|
170
|
+
child.kill('SIGTERM');
|
|
171
|
+
}
|
|
172
|
+
finish({
|
|
173
|
+
ok: false,
|
|
174
|
+
code: null,
|
|
175
|
+
signal: 'SIGTERM',
|
|
176
|
+
stdout,
|
|
177
|
+
stderr: `${stderr}${stderr ? '\n' : ''}Installation timed out after ${timeoutMs}ms.`,
|
|
178
|
+
});
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
|
|
181
|
+
child.stdout.on('data', (chunk) => {
|
|
182
|
+
stdout += chunk.toString();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
child.stderr.on('data', (chunk) => {
|
|
186
|
+
stderr += chunk.toString();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
child.on('error', (error) => {
|
|
190
|
+
clearTimeout(timeout);
|
|
191
|
+
finish({
|
|
192
|
+
ok: false,
|
|
193
|
+
code: null,
|
|
194
|
+
signal: null,
|
|
195
|
+
stdout,
|
|
196
|
+
stderr: `${stderr}${stderr ? '\n' : ''}${error.message}`,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
child.on('close', (code, signal) => {
|
|
201
|
+
clearTimeout(timeout);
|
|
202
|
+
finish({
|
|
203
|
+
ok: code === 0,
|
|
204
|
+
code,
|
|
205
|
+
signal,
|
|
206
|
+
stdout,
|
|
207
|
+
stderr,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
29
213
|
function isCliMockedMissing(agent) {
|
|
30
214
|
const raw = process.env.MOCK_MISSING_CLIS || '';
|
|
31
215
|
return raw
|
|
@@ -36,16 +220,134 @@ function isCliMockedMissing(agent) {
|
|
|
36
220
|
}
|
|
37
221
|
|
|
38
222
|
function buildStatusPayload(result, agent) {
|
|
223
|
+
const installCommands = getPlatformInstallCommands(agent).map((command) => ({
|
|
224
|
+
label: command.label,
|
|
225
|
+
bin: command.bin,
|
|
226
|
+
args: command.args,
|
|
227
|
+
}));
|
|
228
|
+
const definition = getInstallerDefinition(agent);
|
|
229
|
+
|
|
39
230
|
return {
|
|
40
231
|
authenticated: Boolean(result.authenticated),
|
|
41
232
|
email: result.email || null,
|
|
42
233
|
error: result.error || null,
|
|
43
234
|
cliAvailable: result.cliAvailable !== false,
|
|
44
235
|
cliCommand: result.cliCommand || null,
|
|
45
|
-
installHint: result.installHint || (result.cliAvailable === false ? buildCliInstallHint(agent) : null)
|
|
236
|
+
installHint: result.installHint || (result.cliAvailable === false ? buildCliInstallHint(agent) : null),
|
|
237
|
+
installable: installCommands.length > 0,
|
|
238
|
+
installCommands,
|
|
239
|
+
docsUrl: definition?.docsUrl || null,
|
|
240
|
+
downloadUrl: definition?.fallbackDownloadUrl || null,
|
|
46
241
|
};
|
|
47
242
|
}
|
|
48
243
|
|
|
244
|
+
router.get('/providers', async (_req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const statuses = await Promise.all([
|
|
247
|
+
checkClaudeCredentials().then((result) => ['claude', buildStatusPayload(result, 'claude')]),
|
|
248
|
+
checkCursorStatus().then((result) => ['cursor', buildStatusPayload(result, 'cursor')]),
|
|
249
|
+
checkCodexCredentials().then((result) => ['codex', buildStatusPayload(result, 'codex')]),
|
|
250
|
+
checkGeminiCredentials().then((result) => ['gemini', buildStatusPayload(result, 'gemini')]),
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
res.json({ providers: Object.fromEntries(statuses) });
|
|
254
|
+
} catch (error) {
|
|
255
|
+
res.status(500).json({ error: error.message });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
async function checkNanoCliForInstall() {
|
|
260
|
+
const resolvedCliCommand = await resolveAvailableCliCommand({
|
|
261
|
+
envVarName: 'NANO_CLAUDE_CODE_COMMAND',
|
|
262
|
+
defaultCommands: ['nano-claude-code'],
|
|
263
|
+
args: ['--version'],
|
|
264
|
+
appendWindowsSuffixes: true,
|
|
265
|
+
});
|
|
266
|
+
if (!resolvedCliCommand) {
|
|
267
|
+
return {
|
|
268
|
+
authenticated: false,
|
|
269
|
+
email: null,
|
|
270
|
+
error: 'nano-claude-code CLI not found',
|
|
271
|
+
cliAvailable: false,
|
|
272
|
+
cliCommand: 'nano-claude-code',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
authenticated: true,
|
|
277
|
+
email: 'Nano Claude Code',
|
|
278
|
+
cliAvailable: true,
|
|
279
|
+
cliCommand: resolvedCliCommand,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
router.post('/install/:provider', async (req, res) => {
|
|
284
|
+
const provider = String(req.params.provider || '').toLowerCase();
|
|
285
|
+
const definition = getInstallerDefinition(provider);
|
|
286
|
+
|
|
287
|
+
if (!definition) {
|
|
288
|
+
return res.status(404).json({ error: `Unknown provider: ${provider}` });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const commands = getPlatformInstallCommands(provider);
|
|
292
|
+
if (commands.length === 0) {
|
|
293
|
+
return res.status(400).json({
|
|
294
|
+
ok: false,
|
|
295
|
+
provider,
|
|
296
|
+
error: `No automatic installer is configured for ${definition.displayName} on ${process.platform}. See the official docs for manual Linux steps.`,
|
|
297
|
+
docsUrl: definition.docsUrl,
|
|
298
|
+
downloadUrl: definition.fallbackDownloadUrl,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const command = commands[0];
|
|
303
|
+
const result = await runInstallCommand(command);
|
|
304
|
+
|
|
305
|
+
const checker = provider === 'claude'
|
|
306
|
+
? checkClaudeCredentials
|
|
307
|
+
: provider === 'cursor'
|
|
308
|
+
? checkCursorStatus
|
|
309
|
+
: provider === 'codex'
|
|
310
|
+
? checkCodexCredentials
|
|
311
|
+
: provider === 'nano'
|
|
312
|
+
? checkNanoCliForInstall
|
|
313
|
+
: checkGeminiCredentials;
|
|
314
|
+
|
|
315
|
+
const latestStatus = await checker();
|
|
316
|
+
|
|
317
|
+
res.status(result.ok ? 200 : 500).json({
|
|
318
|
+
ok: result.ok,
|
|
319
|
+
provider,
|
|
320
|
+
command: {
|
|
321
|
+
label: command.label,
|
|
322
|
+
bin: command.bin,
|
|
323
|
+
args: command.args,
|
|
324
|
+
},
|
|
325
|
+
stdout: result.stdout,
|
|
326
|
+
stderr: result.stderr,
|
|
327
|
+
code: result.code,
|
|
328
|
+
signal: result.signal,
|
|
329
|
+
status: buildStatusPayload(latestStatus, provider),
|
|
330
|
+
docsUrl: definition.docsUrl,
|
|
331
|
+
downloadUrl: definition.fallbackDownloadUrl,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
router.get('/download/:provider', async (req, res) => {
|
|
336
|
+
const provider = String(req.params.provider || '').toLowerCase();
|
|
337
|
+
const definition = getInstallerDefinition(provider);
|
|
338
|
+
|
|
339
|
+
if (!definition) {
|
|
340
|
+
return res.status(404).json({ error: `Unknown provider: ${provider}` });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
res.json({
|
|
344
|
+
provider,
|
|
345
|
+
displayName: definition.displayName,
|
|
346
|
+
docsUrl: definition.docsUrl,
|
|
347
|
+
downloadUrl: definition.fallbackDownloadUrl,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
49
351
|
router.get('/claude/status', async (req, res) => {
|
|
50
352
|
try {
|
|
51
353
|
const credentialsResult = await checkClaudeCredentials();
|
|
@@ -134,6 +436,42 @@ router.get('/gemini/status', async (req, res) => {
|
|
|
134
436
|
}
|
|
135
437
|
});
|
|
136
438
|
|
|
439
|
+
router.get('/nano/status', async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const resolvedCliCommand = await resolveAvailableCliCommand({
|
|
442
|
+
envVarName: 'NANO_CLAUDE_CODE_COMMAND',
|
|
443
|
+
defaultCommands: ['nano-claude-code'],
|
|
444
|
+
args: ['--version'],
|
|
445
|
+
appendWindowsSuffixes: true,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!resolvedCliCommand) {
|
|
449
|
+
return res.json(buildStatusPayload({
|
|
450
|
+
authenticated: false,
|
|
451
|
+
email: null,
|
|
452
|
+
error: 'nano-claude-code CLI not found',
|
|
453
|
+
cliAvailable: false,
|
|
454
|
+
cliCommand: 'nano-claude-code',
|
|
455
|
+
installHint: buildCliInstallHint('nano'),
|
|
456
|
+
}, 'nano'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return res.json(buildStatusPayload({
|
|
460
|
+
authenticated: true,
|
|
461
|
+
email: 'Nano Claude Code',
|
|
462
|
+
cliAvailable: true,
|
|
463
|
+
cliCommand: resolvedCliCommand,
|
|
464
|
+
}, 'nano'));
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('Error checking Nano Claude Code status:', error);
|
|
467
|
+
res.status(500).json({
|
|
468
|
+
authenticated: false,
|
|
469
|
+
email: null,
|
|
470
|
+
error: error.message,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
137
475
|
async function checkGeminiCredentials() {
|
|
138
476
|
console.log('[DEBUG] Checking Gemini credentials...');
|
|
139
477
|
let cliCommand = process.env.GEMINI_CLI_PATH || 'gemini';
|
|
@@ -177,7 +515,6 @@ async function checkGeminiCredentials() {
|
|
|
177
515
|
};
|
|
178
516
|
}
|
|
179
517
|
|
|
180
|
-
// Check for GOOGLE_API_KEY environment variable first
|
|
181
518
|
if (process.env.GOOGLE_API_KEY) {
|
|
182
519
|
console.log('[DEBUG] Gemini: Found GOOGLE_API_KEY in environment');
|
|
183
520
|
return {
|
|
@@ -188,18 +525,15 @@ async function checkGeminiCredentials() {
|
|
|
188
525
|
};
|
|
189
526
|
}
|
|
190
527
|
|
|
191
|
-
// Check for OAuth credentials file (new Gemini CLI)
|
|
192
528
|
const oauthPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
193
529
|
console.log(`[DEBUG] Gemini: Checking for OAuth file at ${oauthPath}`);
|
|
194
530
|
try {
|
|
195
531
|
const content = await fs.readFile(oauthPath, 'utf8');
|
|
196
532
|
const creds = JSON.parse(content);
|
|
197
533
|
|
|
198
|
-
// Check for presence of refresh_token or access_token
|
|
199
534
|
if (creds.refresh_token || creds.access_token) {
|
|
200
535
|
let email = creds.email || 'OAuth (Config)';
|
|
201
536
|
|
|
202
|
-
// Try to extract email from id_token if available
|
|
203
537
|
if (!creds.email && creds.id_token) {
|
|
204
538
|
try {
|
|
205
539
|
const parts = creds.id_token.split('.');
|
|
@@ -230,7 +564,6 @@ async function checkGeminiCredentials() {
|
|
|
230
564
|
}
|
|
231
565
|
}
|
|
232
566
|
|
|
233
|
-
// Fallback to legacy config file check
|
|
234
567
|
const configPath = path.join(os.homedir(), '.gemini', 'config.json');
|
|
235
568
|
console.log(`[DEBUG] Gemini: Checking for legacy config at ${configPath}`);
|
|
236
569
|
try {
|
|
@@ -301,7 +634,6 @@ async function checkClaudeCredentials() {
|
|
|
301
634
|
if (childProcess) {
|
|
302
635
|
childProcess.kill();
|
|
303
636
|
}
|
|
304
|
-
// Fall back to credentials file check on timeout
|
|
305
637
|
checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
|
|
306
638
|
}
|
|
307
639
|
}, 5000);
|
|
@@ -319,16 +651,11 @@ async function checkClaudeCredentials() {
|
|
|
319
651
|
}
|
|
320
652
|
|
|
321
653
|
let stdout = '';
|
|
322
|
-
let stderr = '';
|
|
323
654
|
|
|
324
655
|
childProcess.stdout.on('data', (data) => {
|
|
325
656
|
stdout += data.toString();
|
|
326
657
|
});
|
|
327
658
|
|
|
328
|
-
childProcess.stderr.on('data', (data) => {
|
|
329
|
-
stderr += data.toString();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
659
|
childProcess.on('close', (code) => {
|
|
333
660
|
if (processCompleted) return;
|
|
334
661
|
processCompleted = true;
|
|
@@ -347,11 +674,9 @@ async function checkClaudeCredentials() {
|
|
|
347
674
|
return;
|
|
348
675
|
}
|
|
349
676
|
} catch {
|
|
350
|
-
// JSON parse failed, fall through
|
|
351
677
|
}
|
|
352
678
|
}
|
|
353
679
|
|
|
354
|
-
// CLI check failed, fall back to credentials file
|
|
355
680
|
checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
|
|
356
681
|
});
|
|
357
682
|
|
|
@@ -359,8 +684,6 @@ async function checkClaudeCredentials() {
|
|
|
359
684
|
if (processCompleted) return;
|
|
360
685
|
processCompleted = true;
|
|
361
686
|
clearTimeout(timeout);
|
|
362
|
-
// Command was already validated by resolveAvailableCliCommand, so treat
|
|
363
|
-
// any spawn error as a transient failure rather than "CLI missing".
|
|
364
687
|
checkClaudeCredentialsFile({ cliAvailable: true, cliCommand: resolvedCliCommand }).then(resolve);
|
|
365
688
|
});
|
|
366
689
|
});
|
|
@@ -394,7 +717,7 @@ async function checkClaudeCredentialsFile({ cliAvailable = true, cliCommand = 'c
|
|
|
394
717
|
error: cliAvailable ? null : 'Claude Code CLI not installed',
|
|
395
718
|
installHint: cliAvailable ? null : buildCliInstallHint('claude')
|
|
396
719
|
};
|
|
397
|
-
} catch (
|
|
720
|
+
} catch (_error) {
|
|
398
721
|
return {
|
|
399
722
|
authenticated: false,
|
|
400
723
|
email: null,
|
|
@@ -495,7 +818,7 @@ function checkCursorStatus() {
|
|
|
495
818
|
}
|
|
496
819
|
});
|
|
497
820
|
|
|
498
|
-
childProcess.on('error', (
|
|
821
|
+
childProcess.on('error', () => {
|
|
499
822
|
if (processCompleted) return;
|
|
500
823
|
processCompleted = true;
|
|
501
824
|
clearTimeout(timeout);
|
|
@@ -512,9 +835,24 @@ function checkCursorStatus() {
|
|
|
512
835
|
});
|
|
513
836
|
}
|
|
514
837
|
|
|
838
|
+
// Auth precedence:
|
|
839
|
+
// 1. JWT tokens (id_token / access_token) from ~/.codex/auth.json
|
|
840
|
+
// 2. OPENAI_API_KEY from server environment variable
|
|
841
|
+
// 3. OPENAI_API_KEY from ~/.codex/auth.json
|
|
515
842
|
async function checkCodexCredentials() {
|
|
516
843
|
let cliCommand = process.env.CODEX_CLI_PATH || 'codex';
|
|
517
844
|
try {
|
|
845
|
+
if (isCliMockedMissing('codex')) {
|
|
846
|
+
return {
|
|
847
|
+
authenticated: false,
|
|
848
|
+
email: null,
|
|
849
|
+
error: 'Codex CLI not installed (mocked)',
|
|
850
|
+
cliAvailable: false,
|
|
851
|
+
cliCommand,
|
|
852
|
+
installHint: buildCliInstallHint('codex')
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
518
856
|
const resolvedCliCommand = await resolveAvailableCliCommand({
|
|
519
857
|
envVarName: 'CODEX_CLI_PATH',
|
|
520
858
|
defaultCommands: ['codex'],
|
|
@@ -544,28 +882,32 @@ async function checkCodexCredentials() {
|
|
|
544
882
|
};
|
|
545
883
|
}
|
|
546
884
|
|
|
885
|
+
const envApiKey = String(process.env.OPENAI_API_KEY || '').trim();
|
|
547
886
|
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
548
|
-
|
|
549
|
-
|
|
887
|
+
let auth = null;
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
const content = await fs.readFile(authPath, 'utf8');
|
|
891
|
+
auth = JSON.parse(content);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (error.code !== 'ENOENT') {
|
|
894
|
+
console.warn('[DEBUG] Codex: Failed to read auth.json, falling back to env auth if available:', error.message);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
550
897
|
|
|
551
898
|
// Tokens are nested under 'tokens' key
|
|
552
|
-
const tokens = auth
|
|
899
|
+
const tokens = auth?.tokens || {};
|
|
553
900
|
|
|
554
|
-
// Check for valid tokens (id_token or access_token)
|
|
555
901
|
if (tokens.id_token || tokens.access_token) {
|
|
556
|
-
// Try to extract email from id_token JWT payload
|
|
557
902
|
let email = 'Authenticated';
|
|
558
903
|
if (tokens.id_token) {
|
|
559
904
|
try {
|
|
560
|
-
// JWT is base64url encoded: header.payload.signature
|
|
561
905
|
const parts = tokens.id_token.split('.');
|
|
562
906
|
if (parts.length >= 2) {
|
|
563
|
-
// Decode the payload (second part)
|
|
564
907
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
565
908
|
email = payload.email || payload.user || 'Authenticated';
|
|
566
909
|
}
|
|
567
910
|
} catch {
|
|
568
|
-
// If JWT decoding fails, use fallback
|
|
569
911
|
email = 'Authenticated';
|
|
570
912
|
}
|
|
571
913
|
}
|
|
@@ -578,8 +920,8 @@ async function checkCodexCredentials() {
|
|
|
578
920
|
};
|
|
579
921
|
}
|
|
580
922
|
|
|
581
|
-
//
|
|
582
|
-
if (
|
|
923
|
+
// Service deployments may authenticate Codex through server env only.
|
|
924
|
+
if (envApiKey) {
|
|
583
925
|
return {
|
|
584
926
|
authenticated: true,
|
|
585
927
|
email: 'API Key Connected',
|
|
@@ -589,16 +931,8 @@ async function checkCodexCredentials() {
|
|
|
589
931
|
};
|
|
590
932
|
}
|
|
591
933
|
|
|
592
|
-
|
|
593
|
-
|
|
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) {
|
|
934
|
+
// Also check for OPENAI_API_KEY stored in auth.json as a fallback auth method
|
|
935
|
+
if (auth?.OPENAI_API_KEY) {
|
|
602
936
|
return {
|
|
603
937
|
authenticated: true,
|
|
604
938
|
email: 'API Key Connected',
|
|
@@ -607,15 +941,15 @@ async function checkCodexCredentials() {
|
|
|
607
941
|
cliCommand
|
|
608
942
|
};
|
|
609
943
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
authenticated: false,
|
|
947
|
+
email: null,
|
|
948
|
+
error: auth ? 'No valid tokens found' : 'Codex not configured',
|
|
949
|
+
cliAvailable: true,
|
|
950
|
+
cliCommand
|
|
951
|
+
};
|
|
952
|
+
} catch (error) {
|
|
619
953
|
return {
|
|
620
954
|
authenticated: false,
|
|
621
955
|
email: null,
|
|
@@ -785,14 +1119,20 @@ router.post('/claude/verify-custom-api', async (req, res) => {
|
|
|
785
1119
|
})
|
|
786
1120
|
});
|
|
787
1121
|
|
|
788
|
-
// 200 = success, 401/403 = bad token, anything else with a valid JSON body means the endpoint is reachable
|
|
789
1122
|
if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 401 && response.status !== 403)) {
|
|
790
1123
|
const envPath = path.join(process.cwd(), '.env');
|
|
791
1124
|
let envContent = '';
|
|
792
1125
|
try {
|
|
793
1126
|
envContent = await fs.readFile(envPath, 'utf8');
|
|
794
|
-
} catch (e) {
|
|
1127
|
+
} catch (e) {}
|
|
795
1128
|
|
|
1129
|
+
const lines = envContent ? envContent.split(/\r?\n/) : [];
|
|
1130
|
+
const nextLines = lines.filter((line) => !line.startsWith('ANTHROPIC_BASE_URL=') && !line.startsWith('ANTHROPIC_AUTH_TOKEN='));
|
|
1131
|
+
nextLines.push(`ANTHROPIC_BASE_URL=${baseUrl || 'https://api.anthropic.com'}`);
|
|
1132
|
+
nextLines.push(`ANTHROPIC_AUTH_TOKEN=${token}`);
|
|
1133
|
+
await fs.writeFile(envPath, `${nextLines.filter(Boolean).join('\n')}\n`, 'utf8');
|
|
1134
|
+
|
|
1135
|
+
return res.json({ success: true });
|
|
796
1136
|
const keysToUpdate = {
|
|
797
1137
|
'ANTHROPIC_BASE_URL': baseUrl || 'https://api.anthropic.com',
|
|
798
1138
|
'ANTHROPIC_API_KEY': token,
|
|
@@ -835,8 +1175,10 @@ router.post('/claude/verify-custom-api', async (req, res) => {
|
|
|
835
1175
|
const err = await response.text();
|
|
836
1176
|
return res.status(response.status).json({ error: `Verification failed: ${err}` });
|
|
837
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
return res.status(400).json({ error: 'Failed to verify custom API token' });
|
|
838
1180
|
} catch (error) {
|
|
839
|
-
res.status(500).json({ error: error.message });
|
|
1181
|
+
return res.status(500).json({ error: error.message });
|
|
840
1182
|
}
|
|
841
1183
|
});
|
|
842
1184
|
|
|
@@ -844,8 +1186,6 @@ router.post('/claude/verify-custom-api', async (req, res) => {
|
|
|
844
1186
|
// Local GPU / Ollama routes
|
|
845
1187
|
// ---------------------------------------------------------------------------
|
|
846
1188
|
|
|
847
|
-
import { detectGPUs, checkOllamaStatus, pullOllamaModel } from '../local-gpu.js';
|
|
848
|
-
|
|
849
1189
|
router.get('/local/gpu-info', async (req, res) => {
|
|
850
1190
|
try {
|
|
851
1191
|
const result = await detectGPUs();
|
|
@@ -857,7 +1197,7 @@ router.get('/local/gpu-info', async (req, res) => {
|
|
|
857
1197
|
|
|
858
1198
|
router.get('/local/status', async (req, res) => {
|
|
859
1199
|
try {
|
|
860
|
-
const serverUrl = process.env.LOCAL_GPU_SERVER_URL ||
|
|
1200
|
+
const serverUrl = process.env.LOCAL_GPU_SERVER_URL || DEFAULT_OLLAMA_URL;
|
|
861
1201
|
const status = await checkOllamaStatus(serverUrl);
|
|
862
1202
|
|
|
863
1203
|
if (status.running) {
|
|
@@ -887,7 +1227,14 @@ router.get('/local/status', async (req, res) => {
|
|
|
887
1227
|
|
|
888
1228
|
router.get('/local/models', async (req, res) => {
|
|
889
1229
|
try {
|
|
890
|
-
|
|
1230
|
+
let serverUrl;
|
|
1231
|
+
try {
|
|
1232
|
+
serverUrl = normalizeLocalOllamaBaseUrl(
|
|
1233
|
+
req.query.serverUrl || process.env.LOCAL_GPU_SERVER_URL || DEFAULT_OLLAMA_URL,
|
|
1234
|
+
);
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
return res.status(400).json({ error: e.message, models: [] });
|
|
1237
|
+
}
|
|
891
1238
|
const status = await checkOllamaStatus(serverUrl);
|
|
892
1239
|
if (!status.running) {
|
|
893
1240
|
return res.status(503).json({ error: 'Ollama is not running', models: [] });
|
|
@@ -918,7 +1265,14 @@ router.post('/local/pull-model', async (req, res) => {
|
|
|
918
1265
|
const { modelName, serverUrl } = req.body;
|
|
919
1266
|
if (!modelName) return res.status(400).json({ error: 'modelName is required' });
|
|
920
1267
|
|
|
921
|
-
|
|
1268
|
+
let url;
|
|
1269
|
+
try {
|
|
1270
|
+
url = normalizeLocalOllamaBaseUrl(
|
|
1271
|
+
serverUrl || process.env.LOCAL_GPU_SERVER_URL || DEFAULT_OLLAMA_URL,
|
|
1272
|
+
);
|
|
1273
|
+
} catch (e) {
|
|
1274
|
+
return res.status(400).json({ error: e.message });
|
|
1275
|
+
}
|
|
922
1276
|
await pullOllamaModel(url, modelName);
|
|
923
1277
|
return res.json({ success: true, message: `Model "${modelName}" pulled successfully.` });
|
|
924
1278
|
} catch (error) {
|
|
@@ -930,6 +1284,12 @@ router.post('/local/save-config', async (req, res) => {
|
|
|
930
1284
|
try {
|
|
931
1285
|
const { serverUrl } = req.body;
|
|
932
1286
|
if (serverUrl) {
|
|
1287
|
+
let normalizedUrl;
|
|
1288
|
+
try {
|
|
1289
|
+
normalizedUrl = normalizeLocalOllamaBaseUrl(serverUrl);
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
return res.status(400).json({ error: e.message });
|
|
1292
|
+
}
|
|
933
1293
|
const envPath = path.join(process.cwd(), '.env');
|
|
934
1294
|
let envContent = '';
|
|
935
1295
|
try { envContent = await fs.readFile(envPath, 'utf8'); } catch {}
|
|
@@ -939,14 +1299,14 @@ router.post('/local/save-config', async (req, res) => {
|
|
|
939
1299
|
const newLines = lines.map(line => {
|
|
940
1300
|
if (line.trim().startsWith('LOCAL_GPU_SERVER_URL=')) {
|
|
941
1301
|
found = true;
|
|
942
|
-
return `LOCAL_GPU_SERVER_URL=${
|
|
1302
|
+
return `LOCAL_GPU_SERVER_URL=${normalizedUrl}`;
|
|
943
1303
|
}
|
|
944
1304
|
return line;
|
|
945
1305
|
}).filter(l => l.trim() !== '' || found);
|
|
946
1306
|
|
|
947
|
-
if (!found) newLines.push(`LOCAL_GPU_SERVER_URL=${
|
|
1307
|
+
if (!found) newLines.push(`LOCAL_GPU_SERVER_URL=${normalizedUrl}`);
|
|
948
1308
|
await fs.writeFile(envPath, newLines.join('\n') + '\n');
|
|
949
|
-
process.env.LOCAL_GPU_SERVER_URL =
|
|
1309
|
+
process.env.LOCAL_GPU_SERVER_URL = normalizedUrl;
|
|
950
1310
|
}
|
|
951
1311
|
|
|
952
1312
|
return res.json({ success: true, message: 'Local GPU configuration saved.' });
|
|
@@ -955,4 +1315,8 @@ router.post('/local/save-config', async (req, res) => {
|
|
|
955
1315
|
}
|
|
956
1316
|
});
|
|
957
1317
|
|
|
1318
|
+
export {
|
|
1319
|
+
checkCodexCredentials,
|
|
1320
|
+
};
|
|
1321
|
+
|
|
958
1322
|
export default router;
|