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
package/server/gemini-cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { buildTempAttachmentFilename, toPortableAtPath } from './utils/imageAtta
|
|
|
12
12
|
import { splitLegacyGeminiThoughtContent } from '../shared/geminiThoughtParser.js';
|
|
13
13
|
import { classifyError } from '../shared/errorClassifier.js';
|
|
14
14
|
import { buildGeminiThinkingConfig } from '../shared/geminiThinkingSupport.js';
|
|
15
|
+
import { buildMemoryBlock } from './utils/memoryPrompt.js';
|
|
16
|
+
import { expandSkillCommand } from './utils/skillExpander.js';
|
|
15
17
|
|
|
16
18
|
// Use cross-spawn on Windows for better command execution
|
|
17
19
|
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
@@ -606,13 +608,22 @@ export async function spawnGemini(command, options = {}, ws) {
|
|
|
606
608
|
}
|
|
607
609
|
|
|
608
610
|
if (command && command.trim()) {
|
|
611
|
+
// Expand /skill-name slash commands into full SKILL.md instructions
|
|
612
|
+
// so the Gemini model receives actionable procedures.
|
|
613
|
+
const expandedCommand = await expandSkillCommand(command.trim(), workingDir);
|
|
614
|
+
|
|
609
615
|
const effectiveAttachments = attachments || images;
|
|
610
|
-
const attachmentResult = await handleGeminiAttachments(
|
|
616
|
+
const attachmentResult = await handleGeminiAttachments(expandedCommand, effectiveAttachments, workingDir);
|
|
611
617
|
tempFilePaths = attachmentResult.tempFilePaths;
|
|
612
618
|
if (attachmentResult.tempDir) {
|
|
613
619
|
tempDirs.push(attachmentResult.tempDir);
|
|
614
620
|
}
|
|
615
|
-
|
|
621
|
+
// Memory is prepended to the user prompt because Gemini CLI uses --prompt flag
|
|
622
|
+
// and does not expose a separate system instruction API.
|
|
623
|
+
const memoryBlock = options.userId ? buildMemoryBlock(options.userId) : '';
|
|
624
|
+
const effectivePrompt = memoryBlock
|
|
625
|
+
? `${memoryBlock}\n\n${attachmentResult.modifiedCommand}`
|
|
626
|
+
: `${attachmentResult.modifiedCommand}`;
|
|
616
627
|
args.push('--prompt', effectivePrompt);
|
|
617
628
|
|
|
618
629
|
// Keep Gemini CLI in yolo mode internally and enforce policy in our own approval hook.
|
package/server/index.js
CHANGED
|
@@ -31,8 +31,6 @@ const c = {
|
|
|
31
31
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
console.log('Requested PORT from env:', process.env.PORT);
|
|
35
|
-
|
|
36
34
|
import express from 'express';
|
|
37
35
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
38
36
|
import os from 'os';
|
|
@@ -52,6 +50,7 @@ import { queryCodex, abortCodexSession, isCodexSessionActive, getCodexSessionSta
|
|
|
52
50
|
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getGeminiSessionStartTime, getActiveGeminiSessions } from './gemini-cli.js';
|
|
53
51
|
import { queryOpenRouter, abortOpenRouterSession, isOpenRouterSessionActive, getOpenRouterSessionStartTime, getActiveOpenRouterSessions } from './openrouter.js';
|
|
54
52
|
import { queryLocalGPU, abortLocalGPUSession, isLocalGPUSessionActive, getLocalGPUSessionStartTime, getActiveLocalGPUSessions } from './local-gpu.js';
|
|
53
|
+
import { spawnNanoClaudeCode, abortNanoClaudeCodeSession, isNanoClaudeCodeSessionActive, getNanoClaudeCodeSessionStartTime, getActiveNanoClaudeCodeSessions } from './nano-claude-code.js';
|
|
55
54
|
import gitRoutes from './routes/git.js';
|
|
56
55
|
import authRoutes from './routes/auth.js';
|
|
57
56
|
import mcpRoutes from './routes/mcp.js';
|
|
@@ -59,6 +58,7 @@ import cursorRoutes from './routes/cursor.js';
|
|
|
59
58
|
import taskmasterRoutes from './routes/taskmaster.js';
|
|
60
59
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
61
60
|
import commandsRoutes from './routes/commands.js';
|
|
61
|
+
import claudeBtwRoutes from './routes/claude-btw.js';
|
|
62
62
|
import settingsRoutes from './routes/settings.js';
|
|
63
63
|
import agentRoutes from './routes/agent.js';
|
|
64
64
|
import projectsRoutes, { WORKSPACES_ROOT, getWorkspacesRoot, validateWorkspacePath } from './routes/projects.js';
|
|
@@ -86,13 +86,15 @@ import {
|
|
|
86
86
|
setRuntimePortSync,
|
|
87
87
|
} from './utils/runtimePorts.js';
|
|
88
88
|
import { buildCodexTokenUsageFromJsonl } from './utils/sessionTokenUsage.js';
|
|
89
|
+
import { getNanoDrClawSessionsRoot } from './nanoSessionPaths.js';
|
|
89
90
|
|
|
90
91
|
// File system watchers for provider project/session folders
|
|
91
92
|
const PROVIDER_WATCH_PATHS = [
|
|
92
93
|
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
93
94
|
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
94
95
|
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
|
|
95
|
-
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
|
|
96
|
+
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'sessions') },
|
|
97
|
+
{ provider: 'nano', rootPath: getNanoDrClawSessionsRoot() },
|
|
96
98
|
];
|
|
97
99
|
const WATCHER_IGNORED_PATTERNS = [
|
|
98
100
|
'**/node_modules/**',
|
|
@@ -132,6 +134,11 @@ function shouldProcessProjectsWatcherEvent(eventType, filePath, provider) {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
if (provider === 'nano') {
|
|
138
|
+
const base = path.basename(String(filePath || '').replace(/\\/g, '/')).toLowerCase();
|
|
139
|
+
return base.endsWith('.json') && base.startsWith('drclaw-nano-');
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
return true;
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -298,6 +305,15 @@ const SHELL_EMBEDDED_ENV_KEYS_TO_REMOVE = [
|
|
|
298
305
|
'WT_SESSION',
|
|
299
306
|
];
|
|
300
307
|
|
|
308
|
+
function safePtyKill(session, sessionKey) {
|
|
309
|
+
if (!session?.pty?.kill) return;
|
|
310
|
+
try {
|
|
311
|
+
session.pty.kill();
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.warn(`⚠️ Failed to kill PTY process (${sessionKey}):`, err);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
301
317
|
function stripAnsiSequences(value = '') {
|
|
302
318
|
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
303
319
|
}
|
|
@@ -412,7 +428,14 @@ const wss = new WebSocketServer({
|
|
|
412
428
|
// Make WebSocket server available to routes
|
|
413
429
|
app.locals.wss = wss;
|
|
414
430
|
|
|
415
|
-
|
|
431
|
+
// Parse VITE_PORT early so CORS reflects the actual frontend port.
|
|
432
|
+
const CORS_VITE_PORT = parsePortNumber(process.env.VITE_PORT, DEFAULT_FRONTEND_PORT);
|
|
433
|
+
app.use(cors({
|
|
434
|
+
origin: process.env.DR_CLAW_CORS_ORIGINS
|
|
435
|
+
? process.env.DR_CLAW_CORS_ORIGINS.split(',').map((o) => o.trim()).filter(Boolean)
|
|
436
|
+
: [`http://localhost:${getFrontendPortSync(CORS_VITE_PORT)}`, `http://127.0.0.1:${getFrontendPortSync(CORS_VITE_PORT)}`],
|
|
437
|
+
credentials: true,
|
|
438
|
+
}));
|
|
416
439
|
app.use(express.json({
|
|
417
440
|
limit: '50mb',
|
|
418
441
|
type: (req) => {
|
|
@@ -428,11 +451,21 @@ app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
|
428
451
|
|
|
429
452
|
// Public health check endpoint (no authentication required)
|
|
430
453
|
app.get('/health', (req, res) => {
|
|
431
|
-
|
|
454
|
+
const isDesktop = process.env.DR_CLAW_DESKTOP === '1';
|
|
455
|
+
const response = {
|
|
432
456
|
status: 'ok',
|
|
433
457
|
timestamp: new Date().toISOString(),
|
|
434
|
-
installMode
|
|
435
|
-
}
|
|
458
|
+
installMode: isDesktop ? 'desktop' : installMode,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
if (isDesktop) {
|
|
462
|
+
response.desktop = {
|
|
463
|
+
platform: process.platform,
|
|
464
|
+
arch: process.arch,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
res.json(response);
|
|
436
469
|
});
|
|
437
470
|
|
|
438
471
|
// Optional API key validation (if configured)
|
|
@@ -462,6 +495,9 @@ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
|
|
|
462
495
|
// Commands API Routes (protected)
|
|
463
496
|
app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
464
497
|
|
|
498
|
+
// Claude ephemeral side question (/btw)
|
|
499
|
+
app.use('/api/claude', authenticateToken, claudeBtwRoutes);
|
|
500
|
+
|
|
465
501
|
// Settings API Routes (protected)
|
|
466
502
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
467
503
|
|
|
@@ -655,6 +691,13 @@ app.use(express.static(path.join(__dirname, '../dist'), {
|
|
|
655
691
|
// System update endpoint
|
|
656
692
|
app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
657
693
|
try {
|
|
694
|
+
if (process.env.DR_CLAW_DESKTOP === '1') {
|
|
695
|
+
return res.status(400).json({
|
|
696
|
+
success: false,
|
|
697
|
+
error: 'Desktop builds do not support in-app self-update yet. Install a newer desktop package instead.'
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
658
701
|
// Get the project root directory (parent of server directory)
|
|
659
702
|
const projectRoot = path.join(__dirname, '..');
|
|
660
703
|
|
|
@@ -662,7 +705,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
662
705
|
|
|
663
706
|
// Run the update command based on installation mode
|
|
664
707
|
const updateCommand = installMode === 'git'
|
|
665
|
-
? 'git checkout main && git pull && npm install'
|
|
708
|
+
? 'git stash && git checkout main && git pull && npm install'
|
|
666
709
|
: `npm install -g ${npmPackageName}@latest`;
|
|
667
710
|
|
|
668
711
|
const child = spawn('sh', ['-c', updateCommand], {
|
|
@@ -1662,6 +1705,35 @@ function handleChatConnection(ws, request) {
|
|
|
1662
1705
|
queryLocalGPU(data.command, { ...data.options, env: sessionEnv }, writer).catch(error => {
|
|
1663
1706
|
console.error('[ERROR] Local GPU query error:', error);
|
|
1664
1707
|
});
|
|
1708
|
+
} else if (data.type === 'nano-command') {
|
|
1709
|
+
console.log('[DEBUG] Nano Claude Code message:', data.command || '[Continue/Resume]');
|
|
1710
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1711
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1712
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1713
|
+
const commandTelemetryEnabled = data.options?.telemetryEnabled !== false;
|
|
1714
|
+
const sessionId = data.options?.sessionId || data.sessionId;
|
|
1715
|
+
|
|
1716
|
+
if (sessionId && isNanoClaudeCodeSessionActive(sessionId)) {
|
|
1717
|
+
console.log(`[WARN] Nano Claude Code session ${sessionId} is already active. Ignoring concurrent request.`);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
enqueueConversationTelemetry(
|
|
1722
|
+
{
|
|
1723
|
+
name: 'agent_dialogue_meta',
|
|
1724
|
+
direction: 'user_to_agent',
|
|
1725
|
+
provider: 'nano',
|
|
1726
|
+
sessionId: sessionId || null,
|
|
1727
|
+
projectPath: data.options?.projectPath || data.options?.cwd || null,
|
|
1728
|
+
transportType: data.type,
|
|
1729
|
+
},
|
|
1730
|
+
{ ...telemetryContext, telemetryEnabled: commandTelemetryEnabled },
|
|
1731
|
+
);
|
|
1732
|
+
writer.telemetryContext = { ...telemetryContext, provider: 'nano', telemetryEnabled: commandTelemetryEnabled };
|
|
1733
|
+
writer.setProjectPath(data.options?.projectPath || data.options?.cwd || null);
|
|
1734
|
+
spawnNanoClaudeCode(data.command, { ...data.options, env: sessionEnv }, writer).catch(error => {
|
|
1735
|
+
console.error('[ERROR] Nano Claude Code error:', error);
|
|
1736
|
+
});
|
|
1665
1737
|
} else if (data.type === 'cursor-resume') {
|
|
1666
1738
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
1667
1739
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -1695,6 +1767,8 @@ function handleChatConnection(ws, request) {
|
|
|
1695
1767
|
success = abortOpenRouterSession(data.sessionId);
|
|
1696
1768
|
} else if (provider === 'local') {
|
|
1697
1769
|
success = abortLocalGPUSession(data.sessionId);
|
|
1770
|
+
} else if (provider === 'nano') {
|
|
1771
|
+
success = abortNanoClaudeCodeSession(data.sessionId);
|
|
1698
1772
|
} else {
|
|
1699
1773
|
// Use Claude Agents SDK
|
|
1700
1774
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
@@ -1749,6 +1823,9 @@ function handleChatConnection(ws, request) {
|
|
|
1749
1823
|
} else if (provider === 'local') {
|
|
1750
1824
|
isActive = isLocalGPUSessionActive(sessionId);
|
|
1751
1825
|
startTime = getLocalGPUSessionStartTime(sessionId);
|
|
1826
|
+
} else if (provider === 'nano') {
|
|
1827
|
+
isActive = isNanoClaudeCodeSessionActive(sessionId);
|
|
1828
|
+
startTime = getNanoClaudeCodeSessionStartTime(sessionId);
|
|
1752
1829
|
} else {
|
|
1753
1830
|
// Use Claude Agents SDK
|
|
1754
1831
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
@@ -1769,7 +1846,8 @@ function handleChatConnection(ws, request) {
|
|
|
1769
1846
|
cursor: getActiveCursorSessions(),
|
|
1770
1847
|
codex: getActiveCodexSessions(),
|
|
1771
1848
|
gemini: getActiveGeminiSessions(),
|
|
1772
|
-
local: getActiveLocalGPUSessions()
|
|
1849
|
+
local: getActiveLocalGPUSessions(),
|
|
1850
|
+
nano: getActiveNanoClaudeCodeSessions()
|
|
1773
1851
|
};
|
|
1774
1852
|
|
|
1775
1853
|
writer.send({
|
|
@@ -1840,7 +1918,7 @@ function handleShellConnection(ws) {
|
|
|
1840
1918
|
if (oldSession) {
|
|
1841
1919
|
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
1842
1920
|
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
|
|
1843
|
-
|
|
1921
|
+
safePtyKill(oldSession, ptySessionKey);
|
|
1844
1922
|
ptySessionsMap.delete(ptySessionKey);
|
|
1845
1923
|
}
|
|
1846
1924
|
}
|
|
@@ -2172,9 +2250,7 @@ function handleShellConnection(ws) {
|
|
|
2172
2250
|
|
|
2173
2251
|
session.timeoutId = setTimeout(() => {
|
|
2174
2252
|
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
2175
|
-
|
|
2176
|
-
session.pty.kill();
|
|
2177
|
-
}
|
|
2253
|
+
safePtyKill(session, ptySessionKey);
|
|
2178
2254
|
ptySessionsMap.delete(ptySessionKey);
|
|
2179
2255
|
}, PTY_SESSION_TIMEOUT);
|
|
2180
2256
|
}
|
|
@@ -2390,9 +2466,7 @@ function handleComputeShellConnection(ws, urlNodeId) {
|
|
|
2390
2466
|
session.ws = null;
|
|
2391
2467
|
session.timeoutId = setTimeout(() => {
|
|
2392
2468
|
console.log('⏰ Compute PTY session timeout, killing:', ptySessionKey);
|
|
2393
|
-
|
|
2394
|
-
session.pty.kill();
|
|
2395
|
-
}
|
|
2469
|
+
safePtyKill(session, ptySessionKey);
|
|
2396
2470
|
ptySessionsMap.delete(ptySessionKey);
|
|
2397
2471
|
}, PTY_SESSION_TIMEOUT);
|
|
2398
2472
|
}
|
|
@@ -3088,6 +3162,7 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
|
|
3088
3162
|
const REQUESTED_PORT = parsePortNumber(process.env.PORT, DEFAULT_BACKEND_PORT);
|
|
3089
3163
|
const REQUESTED_VITE_PORT = parsePortNumber(process.env.VITE_PORT, DEFAULT_FRONTEND_PORT);
|
|
3090
3164
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
3165
|
+
const IS_DESKTOP = process.env.DR_CLAW_DESKTOP === '1';
|
|
3091
3166
|
// Show localhost when binding to all interfaces; 0.0.0.0 is not directly connectable.
|
|
3092
3167
|
const DISPLAY_HOST = HOST === '0.0.0.0' ? 'localhost' : HOST;
|
|
3093
3168
|
|
|
@@ -3103,6 +3178,9 @@ async function startServer() {
|
|
|
3103
3178
|
|
|
3104
3179
|
// Log Claude implementation mode
|
|
3105
3180
|
console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
|
|
3181
|
+
if (IS_DESKTOP) {
|
|
3182
|
+
console.log(`${c.info('[INFO]')} Running inside Electron desktop shell`);
|
|
3183
|
+
}
|
|
3106
3184
|
console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
|
|
3107
3185
|
|
|
3108
3186
|
if (!isProduction) {
|
package/server/load-env.js
CHANGED
|
@@ -42,6 +42,18 @@ function resolveDefaultDatabasePath() {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function stripOptionalEnvQuotes(raw) {
|
|
46
|
+
const v = String(raw).trim();
|
|
47
|
+
if (v.length >= 2) {
|
|
48
|
+
const open = v[0];
|
|
49
|
+
const close = v[v.length - 1];
|
|
50
|
+
if ((open === '"' && close === '"') || (open === "'" && close === "'")) {
|
|
51
|
+
return v.slice(1, -1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
try {
|
|
46
58
|
const envPath = path.join(__dirname, '../.env');
|
|
47
59
|
const envFile = fs.readFileSync(envPath, 'utf8');
|
|
@@ -50,7 +62,7 @@ try {
|
|
|
50
62
|
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
51
63
|
const [key, ...valueParts] = trimmedLine.split('=');
|
|
52
64
|
if (key && valueParts.length > 0 && !process.env[key]) {
|
|
53
|
-
process.env[key] = valueParts.join('=')
|
|
65
|
+
process.env[key] = stripOptionalEnvQuotes(valueParts.join('='));
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
});
|
package/server/local-gpu.js
CHANGED
|
@@ -11,20 +11,26 @@ import crypto from 'crypto';
|
|
|
11
11
|
import { promises as fs } from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import os from 'os';
|
|
14
|
-
import { exec } from 'child_process';
|
|
14
|
+
import { exec, execFile } from 'child_process';
|
|
15
15
|
import { promisify } from 'util';
|
|
16
|
+
import { glob } from 'glob';
|
|
16
17
|
import { encodeProjectPath, ensureProjectSkillLinks, reconcileLocalGPUSessionIndex } from './projects.js';
|
|
17
18
|
import { writeProjectTemplates } from './templates/index.js';
|
|
18
19
|
import { classifyError } from '../shared/errorClassifier.js';
|
|
19
20
|
import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
|
|
20
21
|
import { createRequestId, waitForToolApproval, matchesToolPermission } from './utils/permissions.js';
|
|
22
|
+
import { buildMemoryBlock } from './utils/memoryPrompt.js';
|
|
21
23
|
|
|
22
24
|
const execAsync = promisify(exec);
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
23
26
|
|
|
24
|
-
const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
|
|
27
|
+
export const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
|
|
25
28
|
const MAX_AGENT_TURNS = 30;
|
|
26
29
|
const BASH_TIMEOUT_MS = 120_000;
|
|
27
30
|
const MAX_OUTPUT_CHARS = 100_000;
|
|
31
|
+
/** Match maxBuffer to capped output so large command output is not buffered in full. */
|
|
32
|
+
const MAX_TOOL_BUFFER_BYTES = Math.min(MAX_OUTPUT_CHARS * 4, 512 * 1024);
|
|
33
|
+
const GLOB_MAX_MATCHES = 200;
|
|
28
34
|
const SESSIONS_DIR_NAME = 'localgpu-sessions';
|
|
29
35
|
|
|
30
36
|
const activeLocalGPUSessions = new Map();
|
|
@@ -93,12 +99,58 @@ export async function detectGPUs() {
|
|
|
93
99
|
// Ollama helpers
|
|
94
100
|
// ---------------------------------------------------------------------------
|
|
95
101
|
|
|
102
|
+
function isLoopbackHostname(hostname) {
|
|
103
|
+
if (!hostname) return false;
|
|
104
|
+
const h = hostname.toLowerCase();
|
|
105
|
+
if (h === 'localhost' || h === '::1' || h === '[::1]') return true;
|
|
106
|
+
if (h === '127.0.0.1') return true;
|
|
107
|
+
const m = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
108
|
+
if (!m) return false;
|
|
109
|
+
return [m[1], m[2], m[3]].every((oct) => {
|
|
110
|
+
const n = parseInt(oct, 10);
|
|
111
|
+
return n >= 0 && n <= 255;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalize and validate Ollama base URL. Only http(s) loopback hosts are allowed (SSRF hardening).
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeLocalOllamaBaseUrl(input) {
|
|
119
|
+
let raw = String(input ?? '').trim().replace(/\/+$/, '');
|
|
120
|
+
if (!raw) raw = DEFAULT_OLLAMA_URL;
|
|
121
|
+
if (!/^https?:\/\//i.test(raw)) {
|
|
122
|
+
raw = `http://${raw}`;
|
|
123
|
+
}
|
|
124
|
+
let u;
|
|
125
|
+
try {
|
|
126
|
+
u = new URL(raw);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error('Invalid Ollama server URL');
|
|
129
|
+
}
|
|
130
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
131
|
+
throw new Error('Ollama URL must use http or https');
|
|
132
|
+
}
|
|
133
|
+
if (u.username || u.password) {
|
|
134
|
+
throw new Error('Ollama URL must not include credentials');
|
|
135
|
+
}
|
|
136
|
+
if (!isLoopbackHostname(u.hostname)) {
|
|
137
|
+
throw new Error('Ollama URL must use localhost or 127.0.0.1 (loopback only)');
|
|
138
|
+
}
|
|
139
|
+
return u.origin;
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
function getOllamaUrl(options) {
|
|
97
|
-
|
|
143
|
+
const raw = options?.serverUrl || process.env.LOCAL_GPU_SERVER_URL || DEFAULT_OLLAMA_URL;
|
|
144
|
+
return normalizeLocalOllamaBaseUrl(raw);
|
|
98
145
|
}
|
|
99
146
|
|
|
100
147
|
export async function checkOllamaStatus(serverUrl) {
|
|
101
|
-
|
|
148
|
+
let url;
|
|
149
|
+
try {
|
|
150
|
+
url = normalizeLocalOllamaBaseUrl(serverUrl || DEFAULT_OLLAMA_URL);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return { running: false, error: e.message };
|
|
153
|
+
}
|
|
102
154
|
try {
|
|
103
155
|
const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(5000) });
|
|
104
156
|
if (!res.ok) return { running: false, error: `Server returned ${res.status}` };
|
|
@@ -129,7 +181,7 @@ function formatOllamaModel(m) {
|
|
|
129
181
|
}
|
|
130
182
|
|
|
131
183
|
export async function pullOllamaModel(serverUrl, modelName) {
|
|
132
|
-
const url = (serverUrl || DEFAULT_OLLAMA_URL)
|
|
184
|
+
const url = normalizeLocalOllamaBaseUrl(serverUrl || DEFAULT_OLLAMA_URL);
|
|
133
185
|
const res = await fetch(`${url}/api/pull`, {
|
|
134
186
|
method: 'POST',
|
|
135
187
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -279,6 +331,78 @@ function sendMessage(ws, data) {
|
|
|
279
331
|
// Tool execution (same as OpenRouter)
|
|
280
332
|
// ---------------------------------------------------------------------------
|
|
281
333
|
|
|
334
|
+
async function runBashToolCommand(command, execCwd) {
|
|
335
|
+
if (typeof command !== 'string') {
|
|
336
|
+
return 'Error: command must be a string';
|
|
337
|
+
}
|
|
338
|
+
if (command.includes('\0')) {
|
|
339
|
+
return 'Error: command contains invalid characters';
|
|
340
|
+
}
|
|
341
|
+
const opts = {
|
|
342
|
+
cwd: execCwd,
|
|
343
|
+
timeout: BASH_TIMEOUT_MS,
|
|
344
|
+
maxBuffer: MAX_TOOL_BUFFER_BYTES,
|
|
345
|
+
windowsHide: true,
|
|
346
|
+
};
|
|
347
|
+
try {
|
|
348
|
+
if (process.platform === 'win32') {
|
|
349
|
+
const comspec = process.env.ComSpec || 'cmd.exe';
|
|
350
|
+
const { stdout, stderr } = await execFileAsync(comspec, ['/d', '/s', '/c', command], opts);
|
|
351
|
+
return (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).slice(0, MAX_OUTPUT_CHARS);
|
|
352
|
+
}
|
|
353
|
+
const { stdout, stderr } = await execFileAsync('/bin/sh', ['-c', command], opts);
|
|
354
|
+
return (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).slice(0, MAX_OUTPUT_CHARS);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return `Error: ${err.message}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isPathUnderSearchRoot(absPath, rootAbs) {
|
|
361
|
+
const rootNorm = path.resolve(rootAbs);
|
|
362
|
+
const fileNorm = path.resolve(absPath);
|
|
363
|
+
const prefix = rootNorm.endsWith(path.sep) ? rootNorm : `${rootNorm}${path.sep}`;
|
|
364
|
+
return fileNorm === rootNorm || fileNorm.startsWith(prefix);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runRipgrepSearch(pattern, dirAbs, includeGlob) {
|
|
368
|
+
if (typeof pattern !== 'string' || pattern.includes('\0')) {
|
|
369
|
+
return 'Error: invalid search pattern';
|
|
370
|
+
}
|
|
371
|
+
const rgArgs = [
|
|
372
|
+
'--max-count', '50',
|
|
373
|
+
'--line-number',
|
|
374
|
+
'--color', 'never',
|
|
375
|
+
'--no-heading',
|
|
376
|
+
];
|
|
377
|
+
if (includeGlob && typeof includeGlob === 'string' && !includeGlob.includes('\0')) {
|
|
378
|
+
rgArgs.push('--glob', includeGlob);
|
|
379
|
+
}
|
|
380
|
+
rgArgs.push('--', pattern, dirAbs);
|
|
381
|
+
const opts = {
|
|
382
|
+
cwd: dirAbs,
|
|
383
|
+
timeout: 15_000,
|
|
384
|
+
maxBuffer: MAX_TOOL_BUFFER_BYTES,
|
|
385
|
+
windowsHide: true,
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
const { stdout, stderr } = await execFileAsync('rg', rgArgs, opts);
|
|
389
|
+
const combined = (stdout || '') + (stderr ? `\n[stderr]\n${stderr}` : '');
|
|
390
|
+
const trimmed = combined.trim().slice(0, MAX_OUTPUT_CHARS);
|
|
391
|
+
return trimmed || 'No results.';
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err?.code === 'ENOENT') {
|
|
394
|
+
return 'Error: ripgrep (rg) is not installed or not in PATH. Install from https://github.com/BurntSushi/ripgrep';
|
|
395
|
+
}
|
|
396
|
+
// ripgrep exits 1 when there are zero matches (not an error).
|
|
397
|
+
if (err?.code === 1 && typeof err.stdout === 'string') {
|
|
398
|
+
const combined = (err.stdout || '') + (err.stderr ? `\n[stderr]\n${err.stderr}` : '');
|
|
399
|
+
const trimmed = combined.trim().slice(0, MAX_OUTPUT_CHARS);
|
|
400
|
+
return trimmed || 'No results.';
|
|
401
|
+
}
|
|
402
|
+
return `Error: ${err.message}`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
282
406
|
async function executeTool(name, args, cwd) {
|
|
283
407
|
try {
|
|
284
408
|
if (name === 'Read') {
|
|
@@ -313,35 +437,40 @@ async function executeTool(name, args, cwd) {
|
|
|
313
437
|
|
|
314
438
|
if (name === 'Bash') {
|
|
315
439
|
const execCwd = args.cwd ? (path.isAbsolute(args.cwd) ? args.cwd : path.resolve(cwd, args.cwd)) : cwd;
|
|
316
|
-
|
|
317
|
-
cwd: execCwd,
|
|
318
|
-
timeout: BASH_TIMEOUT_MS,
|
|
319
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
320
|
-
});
|
|
321
|
-
return (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).slice(0, MAX_OUTPUT_CHARS);
|
|
440
|
+
return await runBashToolCommand(args.command, execCwd);
|
|
322
441
|
}
|
|
323
442
|
|
|
324
443
|
if (name === 'Glob') {
|
|
325
444
|
const dir = args.directory
|
|
326
445
|
? (path.isAbsolute(args.directory) ? args.directory : path.resolve(cwd, args.directory))
|
|
327
446
|
: cwd;
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
447
|
+
const dirResolved = path.resolve(dir);
|
|
448
|
+
if (typeof args.pattern !== 'string' || args.pattern.includes('\0')) {
|
|
449
|
+
return 'Error: invalid glob pattern';
|
|
450
|
+
}
|
|
451
|
+
const matches = await glob(args.pattern, {
|
|
452
|
+
cwd: dirResolved,
|
|
453
|
+
nodir: true,
|
|
454
|
+
absolute: true,
|
|
455
|
+
followSymbolicLinks: false,
|
|
456
|
+
maxDepth: 40,
|
|
457
|
+
});
|
|
458
|
+
const safe = [];
|
|
459
|
+
for (const p of matches) {
|
|
460
|
+
if (isPathUnderSearchRoot(p, dirResolved)) {
|
|
461
|
+
safe.push(p);
|
|
462
|
+
}
|
|
463
|
+
if (safe.length >= GLOB_MAX_MATCHES) break;
|
|
464
|
+
}
|
|
465
|
+
return safe.join('\n') || 'No files matched.';
|
|
333
466
|
}
|
|
334
467
|
|
|
335
468
|
if (name === 'Search') {
|
|
336
469
|
const dir = args.directory
|
|
337
470
|
? (path.isAbsolute(args.directory) ? args.directory : path.resolve(cwd, args.directory))
|
|
338
471
|
: cwd;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
`rg --max-count 50 --line-number ${includeFlag} ${JSON.stringify(args.pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -200`,
|
|
342
|
-
{ cwd: dir, timeout: 15000 },
|
|
343
|
-
);
|
|
344
|
-
return stdout.trim() || 'No results.';
|
|
472
|
+
const dirResolved = path.resolve(dir);
|
|
473
|
+
return await runRipgrepSearch(args.pattern, dirResolved, args.include);
|
|
345
474
|
}
|
|
346
475
|
|
|
347
476
|
if (name === 'LS') {
|
|
@@ -556,7 +685,19 @@ export async function queryLocalGPU(command, options = {}, ws) {
|
|
|
556
685
|
} = options;
|
|
557
686
|
|
|
558
687
|
const workingDirectory = cwd || projectPath || process.cwd();
|
|
559
|
-
|
|
688
|
+
let ollamaUrl;
|
|
689
|
+
try {
|
|
690
|
+
ollamaUrl = getOllamaUrl(options);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
sendMessage(ws, {
|
|
693
|
+
type: 'localgpu-error',
|
|
694
|
+
error: err.message,
|
|
695
|
+
errorType: 'auth',
|
|
696
|
+
isRetryable: false,
|
|
697
|
+
sessionId,
|
|
698
|
+
});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
560
701
|
|
|
561
702
|
// Verify Ollama is reachable
|
|
562
703
|
try {
|
|
@@ -623,7 +764,8 @@ export async function queryLocalGPU(command, options = {}, ws) {
|
|
|
623
764
|
projectName: workingDirectory ? encodeProjectPath(workingDirectory) : undefined,
|
|
624
765
|
});
|
|
625
766
|
|
|
626
|
-
const
|
|
767
|
+
const memoryBlock = options.userId ? buildMemoryBlock(options.userId) : '';
|
|
768
|
+
const systemContent = (customSystemPrompt || await buildSystemPrompt(workingDirectory)) + memoryBlock;
|
|
627
769
|
const messages = [{ role: 'system', content: systemContent }];
|
|
628
770
|
|
|
629
771
|
if (sessionId) {
|