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.
Files changed (113) hide show
  1. package/README.md +286 -177
  2. package/README.zh-CN.md +286 -174
  3. package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
  4. package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
  5. package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
  6. package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
  7. package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
  8. package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
  9. package/dist/assets/channel-BJdHHkhR.js +1 -0
  10. package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
  11. package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
  12. package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
  13. package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
  15. package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
  17. package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
  18. package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
  19. package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
  20. package/dist/assets/clone-CwXTbwxl.js +1 -0
  21. package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
  22. package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
  23. package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
  24. package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
  25. package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
  26. package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
  27. package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
  28. package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
  29. package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
  30. package/dist/assets/{graph-Blm-emkR.js → graph-DGtZXuDY.js} +1 -1
  31. package/dist/assets/index-C-sL-ag0.css +1 -0
  32. package/dist/assets/index-VdqxIFPL.js +670 -0
  33. package/dist/assets/{infoDiagram-HS3SLOUP-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
  34. package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
  35. package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
  36. package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
  37. package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
  38. package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
  39. package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
  40. package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
  41. package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
  42. package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
  45. package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
  47. package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
  48. package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
  49. package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
  50. package/dist/icons/nanoclaude.png +0 -0
  51. package/dist/index.html +2 -2
  52. package/dist/screenshots/screenshot_v1_2.png +0 -0
  53. package/package.json +61 -4
  54. package/public/icons/nanoclaude.png +0 -0
  55. package/public/screenshots/screenshot_v1_2.png +0 -0
  56. package/scripts/build-electron-icons.mjs +40 -0
  57. package/scripts/check-deps.js +28 -0
  58. package/scripts/native-runtime.mjs +128 -0
  59. package/server/__tests__/gemini-api.test.mjs +699 -0
  60. package/server/__tests__/gemini-session-index.test.mjs +95 -0
  61. package/server/__tests__/project-sync-dedup.test.mjs +173 -0
  62. package/server/__tests__/session-delete.test.mjs +152 -0
  63. package/server/claude-sdk.js +175 -14
  64. package/server/cli-chat.js +28 -17
  65. package/server/cli.js +48 -11
  66. package/server/data/news-log-arxiv.json +1 -0
  67. package/server/data/news-log-huggingface.json +1 -0
  68. package/server/data/news-results-arxiv.json +460 -0
  69. package/server/data/news-results-huggingface.json +817 -0
  70. package/server/data/research_interests_arxiv.json +50 -0
  71. package/server/data/research_interests_huggingface.json +4 -0
  72. package/server/database/db.js +101 -5
  73. package/server/database/init.sql +17 -1
  74. package/server/gemini-api.js +1838 -0
  75. package/server/gemini-cli.js +13 -2
  76. package/server/index.js +94 -16
  77. package/server/load-env.js +13 -1
  78. package/server/local-gpu.js +166 -24
  79. package/server/mcp-compute.js +223 -0
  80. package/server/middleware/auth.js +25 -6
  81. package/server/nano-claude-code.js +340 -0
  82. package/server/nanoSessionPaths.js +41 -0
  83. package/server/openai-codex.js +6 -2
  84. package/server/openrouter.js +37 -22
  85. package/server/projects.js +516 -110
  86. package/server/routes/agent.js +32 -7
  87. package/server/routes/auto-research.js +1 -1
  88. package/server/routes/claude-btw.js +55 -0
  89. package/server/routes/cli-auth.js +422 -58
  90. package/server/routes/commands.js +25 -1
  91. package/server/routes/community-tools.js +398 -0
  92. package/server/routes/compute.js +155 -0
  93. package/server/routes/git.js +9 -11
  94. package/server/routes/memory.js +143 -0
  95. package/server/routes/news.js +3 -0
  96. package/server/routes/skills.js +35 -0
  97. package/server/routes/user.js +3 -5
  98. package/server/utils/__tests__/safePath.test.js +93 -0
  99. package/server/utils/cliResolution.js +13 -1
  100. package/server/utils/codexSessionMessages.js +285 -0
  101. package/server/utils/geminiOAuth.js +333 -0
  102. package/server/utils/memoryPrompt.js +78 -0
  103. package/server/utils/safePath.js +51 -0
  104. package/server/utils/sessionIndex.js +2 -0
  105. package/server/utils/skillExpander.js +142 -0
  106. package/server/utils/spawnAsync.js +78 -0
  107. package/shared/modelConstants.js +19 -1
  108. package/dist/assets/channel-BFOE7OyC.js +0 -1
  109. package/dist/assets/clone-BulQSblH.js +0 -1
  110. package/dist/assets/index-B8Yutsnm.css +0 -1
  111. package/dist/assets/index-Cf6gyhc3.js +0 -654
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
  113. 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 (error) {
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', (err) => {
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
- const content = await fs.readFile(authPath, 'utf8');
549
- const auth = JSON.parse(content);
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.tokens || {};
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
- // 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) {
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
- 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) {
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
- if (error.code === 'ENOENT') {
611
- return {
612
- authenticated: false,
613
- email: null,
614
- error: 'Codex not configured',
615
- cliAvailable: true,
616
- cliCommand
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) { /* ignore if not exists */ }
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 || 'http://localhost:11434';
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
- const serverUrl = req.query.serverUrl || process.env.LOCAL_GPU_SERVER_URL || 'http://localhost:11434';
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
- const url = serverUrl || process.env.LOCAL_GPU_SERVER_URL || 'http://localhost:11434';
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=${serverUrl}`;
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=${serverUrl}`);
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 = serverUrl;
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;