dr-claw 1.1.3 → 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/dist/assets/{_basePickBy-CTZ7OJiJ.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-LYG5erFT.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-CYhqYtN8.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-B7xb9vYd.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-Dz6eN3Yc.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DIa9rHl5.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-JZVAPeYL.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BoNiAnP-.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-BURhBXxZ.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-Bxa-FsXa.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Dn_6iLa4.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-B0N2-K2K.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Bd7csSYW.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-DV5aJTr6.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-iZL_J_xq.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-iZL_J_xq.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-C_Uhc5Rn.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-BiW_P9KL.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-uGQgzEbt.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-qox_ZDR7.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-RKFFDPoU.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-11Go9j0k.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-DBAiHvAx.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-Dzc11a-7.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-DJVXfOqP.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-CJ95S3XQ.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-DpdkGgBK.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CSGpNz7D.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-BvPB3vMM.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-C96BbhFQ.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-HjDfr5-H.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-J7RjAKTD.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-C6MlEWCC.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-Bi5BLoNG.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-Bo14V3Q1.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-CP3_EwJ4.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-B3wFfI_W.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-fZVhVt_s.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-BooCitAX.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-CM0zUx-Q.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-B-8ph3e8.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-Q2k0EFpn.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/icons/nanoclaude.png +0 -0
- package/scripts/native-runtime.mjs +5 -3
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/claude-sdk.js +82 -3
- package/server/cli-chat.js +28 -17
- package/server/cli.js +13 -1
- 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 +92 -0
- 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 -17
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +3 -1
- 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 +207 -0
- 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 +103 -2
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +26 -21
- 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/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-Dk-4X8XT.js +0 -1
- package/dist/assets/clone-DbPu_xVW.js +0 -1
- package/dist/assets/index-1MRmplD1.css +0 -1
- package/dist/assets/index-CFbkRzqw.js +0 -664
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BzH7qQMD.js +0 -1
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
|
@@ -44,12 +44,13 @@ import mime from 'mime-types';
|
|
|
44
44
|
|
|
45
45
|
import { getProjects, getTrashedProjects, getSessions, getSessionMessages, renameProject, renameSession, deleteSession, deleteProject, restoreProject, deleteTrashedProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
46
46
|
import { getProjectTokenUsageSummary } from './project-token-usage.js';
|
|
47
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getClaudeSDKSessionStartTime, getActiveClaudeSDKSessions, resolveToolApproval
|
|
47
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getClaudeSDKSessionStartTime, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
|
|
48
48
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getCursorSessionStartTime, getActiveCursorSessions } from './cursor-cli.js';
|
|
49
49
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getCodexSessionStartTime, getActiveCodexSessions } from './openai-codex.js';
|
|
50
50
|
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getGeminiSessionStartTime, getActiveGeminiSessions } from './gemini-cli.js';
|
|
51
51
|
import { queryOpenRouter, abortOpenRouterSession, isOpenRouterSessionActive, getOpenRouterSessionStartTime, getActiveOpenRouterSessions } from './openrouter.js';
|
|
52
52
|
import { queryLocalGPU, abortLocalGPUSession, isLocalGPUSessionActive, getLocalGPUSessionStartTime, getActiveLocalGPUSessions } from './local-gpu.js';
|
|
53
|
+
import { spawnNanoClaudeCode, abortNanoClaudeCodeSession, isNanoClaudeCodeSessionActive, getNanoClaudeCodeSessionStartTime, getActiveNanoClaudeCodeSessions } from './nano-claude-code.js';
|
|
53
54
|
import gitRoutes from './routes/git.js';
|
|
54
55
|
import authRoutes from './routes/auth.js';
|
|
55
56
|
import mcpRoutes from './routes/mcp.js';
|
|
@@ -57,6 +58,7 @@ import cursorRoutes from './routes/cursor.js';
|
|
|
57
58
|
import taskmasterRoutes from './routes/taskmaster.js';
|
|
58
59
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
59
60
|
import commandsRoutes from './routes/commands.js';
|
|
61
|
+
import claudeBtwRoutes from './routes/claude-btw.js';
|
|
60
62
|
import settingsRoutes from './routes/settings.js';
|
|
61
63
|
import agentRoutes from './routes/agent.js';
|
|
62
64
|
import projectsRoutes, { WORKSPACES_ROOT, getWorkspacesRoot, validateWorkspacePath } from './routes/projects.js';
|
|
@@ -69,7 +71,6 @@ import computeRoutes from './routes/compute.js';
|
|
|
69
71
|
import newsRoutes from './routes/news.js';
|
|
70
72
|
import autoResearchRoutes from './routes/auto-research.js';
|
|
71
73
|
import referencesRoutes from './routes/references.js';
|
|
72
|
-
import communityToolsRoutes from './routes/community-tools.js';
|
|
73
74
|
import { initializeDatabase, sessionDb, tagDb } from './database/db.js';
|
|
74
75
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
75
76
|
import { IS_PLATFORM } from './constants/config.js';
|
|
@@ -85,13 +86,15 @@ import {
|
|
|
85
86
|
setRuntimePortSync,
|
|
86
87
|
} from './utils/runtimePorts.js';
|
|
87
88
|
import { buildCodexTokenUsageFromJsonl } from './utils/sessionTokenUsage.js';
|
|
89
|
+
import { getNanoDrClawSessionsRoot } from './nanoSessionPaths.js';
|
|
88
90
|
|
|
89
91
|
// File system watchers for provider project/session folders
|
|
90
92
|
const PROVIDER_WATCH_PATHS = [
|
|
91
93
|
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
92
94
|
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
93
95
|
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
|
|
94
|
-
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
|
|
96
|
+
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'sessions') },
|
|
97
|
+
{ provider: 'nano', rootPath: getNanoDrClawSessionsRoot() },
|
|
95
98
|
];
|
|
96
99
|
const WATCHER_IGNORED_PATTERNS = [
|
|
97
100
|
'**/node_modules/**',
|
|
@@ -131,6 +134,11 @@ function shouldProcessProjectsWatcherEvent(eventType, filePath, provider) {
|
|
|
131
134
|
);
|
|
132
135
|
}
|
|
133
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
|
+
|
|
134
142
|
return true;
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -420,7 +428,14 @@ const wss = new WebSocketServer({
|
|
|
420
428
|
// Make WebSocket server available to routes
|
|
421
429
|
app.locals.wss = wss;
|
|
422
430
|
|
|
423
|
-
|
|
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
|
+
}));
|
|
424
439
|
app.use(express.json({
|
|
425
440
|
limit: '50mb',
|
|
426
441
|
type: (req) => {
|
|
@@ -480,6 +495,9 @@ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
|
|
|
480
495
|
// Commands API Routes (protected)
|
|
481
496
|
app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
482
497
|
|
|
498
|
+
// Claude ephemeral side question (/btw)
|
|
499
|
+
app.use('/api/claude', authenticateToken, claudeBtwRoutes);
|
|
500
|
+
|
|
483
501
|
// Settings API Routes (protected)
|
|
484
502
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
485
503
|
|
|
@@ -510,9 +528,6 @@ app.use('/api/auto-research', authenticateToken, autoResearchRoutes);
|
|
|
510
528
|
// References (literature library) API Routes (protected)
|
|
511
529
|
app.use('/api/references', authenticateToken, referencesRoutes);
|
|
512
530
|
|
|
513
|
-
// Community Tools API Routes (protected)
|
|
514
|
-
app.use('/api/community-tools', authenticateToken, communityToolsRoutes);
|
|
515
|
-
|
|
516
531
|
// Agent API Routes (uses API key authentication)
|
|
517
532
|
app.use('/api/agent', agentRoutes);
|
|
518
533
|
|
|
@@ -1690,6 +1705,35 @@ function handleChatConnection(ws, request) {
|
|
|
1690
1705
|
queryLocalGPU(data.command, { ...data.options, env: sessionEnv }, writer).catch(error => {
|
|
1691
1706
|
console.error('[ERROR] Local GPU query error:', error);
|
|
1692
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
|
+
});
|
|
1693
1737
|
} else if (data.type === 'cursor-resume') {
|
|
1694
1738
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
1695
1739
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -1723,6 +1767,8 @@ function handleChatConnection(ws, request) {
|
|
|
1723
1767
|
success = abortOpenRouterSession(data.sessionId);
|
|
1724
1768
|
} else if (provider === 'local') {
|
|
1725
1769
|
success = abortLocalGPUSession(data.sessionId);
|
|
1770
|
+
} else if (provider === 'nano') {
|
|
1771
|
+
success = abortNanoClaudeCodeSession(data.sessionId);
|
|
1726
1772
|
} else {
|
|
1727
1773
|
// Use Claude Agents SDK
|
|
1728
1774
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
@@ -1777,6 +1823,9 @@ function handleChatConnection(ws, request) {
|
|
|
1777
1823
|
} else if (provider === 'local') {
|
|
1778
1824
|
isActive = isLocalGPUSessionActive(sessionId);
|
|
1779
1825
|
startTime = getLocalGPUSessionStartTime(sessionId);
|
|
1826
|
+
} else if (provider === 'nano') {
|
|
1827
|
+
isActive = isNanoClaudeCodeSessionActive(sessionId);
|
|
1828
|
+
startTime = getNanoClaudeCodeSessionStartTime(sessionId);
|
|
1780
1829
|
} else {
|
|
1781
1830
|
// Use Claude Agents SDK
|
|
1782
1831
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
@@ -1797,7 +1846,8 @@ function handleChatConnection(ws, request) {
|
|
|
1797
1846
|
cursor: getActiveCursorSessions(),
|
|
1798
1847
|
codex: getActiveCodexSessions(),
|
|
1799
1848
|
gemini: getActiveGeminiSessions(),
|
|
1800
|
-
local: getActiveLocalGPUSessions()
|
|
1849
|
+
local: getActiveLocalGPUSessions(),
|
|
1850
|
+
nano: getActiveNanoClaudeCodeSessions()
|
|
1801
1851
|
};
|
|
1802
1852
|
|
|
1803
1853
|
writer.send({
|
|
@@ -2805,7 +2855,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2805
2855
|
let inputTokens = 0;
|
|
2806
2856
|
let cacheCreationTokens = 0;
|
|
2807
2857
|
let cacheReadTokens = 0;
|
|
2808
|
-
let outputTokens = 0;
|
|
2809
2858
|
let modelName = null;
|
|
2810
2859
|
|
|
2811
2860
|
// Find the latest assistant message with usage data (scan from end)
|
|
@@ -2821,7 +2870,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2821
2870
|
inputTokens = usage.input_tokens || 0;
|
|
2822
2871
|
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
2823
2872
|
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
2824
|
-
outputTokens = usage.output_tokens || 0;
|
|
2825
2873
|
modelName = entry.message.model || null;
|
|
2826
2874
|
|
|
2827
2875
|
break; // Stop after finding the latest assistant message
|
|
@@ -2832,12 +2880,42 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2832
2880
|
}
|
|
2833
2881
|
}
|
|
2834
2882
|
|
|
2835
|
-
// Determine context window from model name
|
|
2836
|
-
const
|
|
2883
|
+
// Determine context window from model name
|
|
2884
|
+
const MODEL_CONTEXT_WINDOWS = {
|
|
2885
|
+
'claude-opus-4-6': 200000,
|
|
2886
|
+
'claude-opus-4-20250918': 200000,
|
|
2887
|
+
'claude-sonnet-4-6': 200000,
|
|
2888
|
+
'claude-sonnet-4-20250514': 200000,
|
|
2889
|
+
'claude-haiku-4-5': 200000,
|
|
2890
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
2891
|
+
'claude-3-5-sonnet': 200000,
|
|
2892
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
2893
|
+
'claude-3-5-haiku': 200000,
|
|
2894
|
+
'claude-3-5-haiku-20241022': 200000,
|
|
2895
|
+
'claude-3-opus': 200000,
|
|
2896
|
+
'claude-3-opus-20240229': 200000,
|
|
2897
|
+
'claude-3-sonnet': 200000,
|
|
2898
|
+
'claude-3-haiku': 200000,
|
|
2899
|
+
};
|
|
2900
|
+
|
|
2901
|
+
// Priority: env var override > model-based lookup > default
|
|
2902
|
+
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
2903
|
+
let contextWindow;
|
|
2904
|
+
if (Number.isFinite(parsedContextWindow)) {
|
|
2905
|
+
contextWindow = parsedContextWindow;
|
|
2906
|
+
} else if (modelName) {
|
|
2907
|
+
// Try exact match first, then prefix match
|
|
2908
|
+
contextWindow = MODEL_CONTEXT_WINDOWS[modelName];
|
|
2909
|
+
if (!contextWindow) {
|
|
2910
|
+
const prefix = Object.keys(MODEL_CONTEXT_WINDOWS).find(k => modelName.startsWith(k));
|
|
2911
|
+
contextWindow = prefix ? MODEL_CONTEXT_WINDOWS[prefix] : 200000;
|
|
2912
|
+
}
|
|
2913
|
+
} else {
|
|
2914
|
+
contextWindow = 200000;
|
|
2915
|
+
}
|
|
2837
2916
|
|
|
2838
|
-
// Calculate total context usage (
|
|
2839
|
-
|
|
2840
|
-
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens + outputTokens;
|
|
2917
|
+
// Calculate total context usage (excluding output_tokens, as per ccusage)
|
|
2918
|
+
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
2841
2919
|
|
|
2842
2920
|
res.json({
|
|
2843
2921
|
used: totalUsed,
|
|
@@ -2846,8 +2924,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2846
2924
|
breakdown: {
|
|
2847
2925
|
input: inputTokens,
|
|
2848
2926
|
cacheCreation: cacheCreationTokens,
|
|
2849
|
-
cacheRead: cacheReadTokens
|
|
2850
|
-
output: outputTokens
|
|
2927
|
+
cacheRead: cacheReadTokens
|
|
2851
2928
|
}
|
|
2852
2929
|
});
|
|
2853
2930
|
} catch (error) {
|
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
|
@@ -19,6 +19,7 @@ import { writeProjectTemplates } from './templates/index.js';
|
|
|
19
19
|
import { classifyError } from '../shared/errorClassifier.js';
|
|
20
20
|
import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
|
|
21
21
|
import { createRequestId, waitForToolApproval, matchesToolPermission } from './utils/permissions.js';
|
|
22
|
+
import { buildMemoryBlock } from './utils/memoryPrompt.js';
|
|
22
23
|
|
|
23
24
|
const execAsync = promisify(exec);
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
@@ -763,7 +764,8 @@ export async function queryLocalGPU(command, options = {}, ws) {
|
|
|
763
764
|
projectName: workingDirectory ? encodeProjectPath(workingDirectory) : undefined,
|
|
764
765
|
});
|
|
765
766
|
|
|
766
|
-
const
|
|
767
|
+
const memoryBlock = options.userId ? buildMemoryBlock(options.userId) : '';
|
|
768
|
+
const systemContent = (customSystemPrompt || await buildSystemPrompt(workingDirectory)) + memoryBlock;
|
|
767
769
|
const messages = [{ role: 'system', content: systemContent }];
|
|
768
770
|
|
|
769
771
|
if (sessionId) {
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nano Claude Code provider — spawns the nano-claude-code CLI in stream-json harness mode.
|
|
3
|
+
* Uses the same WebSocket shapes as Claude (claude-response / claude-complete) for UI reuse.
|
|
4
|
+
*
|
|
5
|
+
* Requires a nano-claude-code build that supports:
|
|
6
|
+
* --output-format stream-json -p "..." --dangerously-skip-permissions
|
|
7
|
+
* --session-file <path.json> and --resume <path.json> for multi-turn persistence (absolute path under ~/.dr-claw/nano-sessions)
|
|
8
|
+
* result lines may include nano_session_file (optional)
|
|
9
|
+
* Install: https://github.com/OpenLAIR/nano-claude-code — pip install from that repo or use the published CLI name on PATH.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import crossSpawn from 'cross-spawn';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { encodeProjectPath, ensureProjectSkillLinks } from './projects.js';
|
|
16
|
+
import { writeProjectTemplates } from './templates/index.js';
|
|
17
|
+
import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
|
|
18
|
+
import { ensureNanoDrClawSessionsRoot, resolveNanoSessionAbsPath } from './nanoSessionPaths.js';
|
|
19
|
+
|
|
20
|
+
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
21
|
+
|
|
22
|
+
const activeNanoSessions = new Map();
|
|
23
|
+
|
|
24
|
+
let nanoShutdownHooksInstalled = false;
|
|
25
|
+
|
|
26
|
+
function installNanoProcessShutdownHooks() {
|
|
27
|
+
if (nanoShutdownHooksInstalled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
nanoShutdownHooksInstalled = true;
|
|
31
|
+
const shutdown = () => {
|
|
32
|
+
for (const { process: childProc } of activeNanoSessions.values()) {
|
|
33
|
+
try {
|
|
34
|
+
if (childProc && !childProc.killed) {
|
|
35
|
+
childProc.kill('SIGTERM');
|
|
36
|
+
}
|
|
37
|
+
} catch (_) {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
activeNanoSessions.clear();
|
|
42
|
+
};
|
|
43
|
+
process.on('exit', shutdown);
|
|
44
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
45
|
+
process.on(sig, shutdown);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveNanoCommand() {
|
|
50
|
+
const explicit = String(
|
|
51
|
+
process.env.NANO_CLAUDE_CODE_COMMAND || ''
|
|
52
|
+
).trim();
|
|
53
|
+
if (explicit) {
|
|
54
|
+
return explicit;
|
|
55
|
+
}
|
|
56
|
+
return 'nano-claude-code';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function persistNanoSessionMetadata(sessionId, projectPath, sessionMode) {
|
|
60
|
+
if (!sessionId || !projectPath) return;
|
|
61
|
+
try {
|
|
62
|
+
const { sessionDb } = await import('./database/db.js');
|
|
63
|
+
sessionDb.upsertSession(
|
|
64
|
+
sessionId,
|
|
65
|
+
encodeProjectPath(projectPath),
|
|
66
|
+
'nano',
|
|
67
|
+
'Nano Claude Code Session',
|
|
68
|
+
new Date().toISOString(),
|
|
69
|
+
0,
|
|
70
|
+
{ sessionMode: sessionMode || 'research', projectPath },
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn('[Nano] Failed to persist session metadata:', error.message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function spawnNanoClaudeCode(command, options = {}, ws) {
|
|
78
|
+
installNanoProcessShutdownHooks();
|
|
79
|
+
const {
|
|
80
|
+
sessionId,
|
|
81
|
+
projectPath,
|
|
82
|
+
cwd,
|
|
83
|
+
model,
|
|
84
|
+
env,
|
|
85
|
+
sessionMode,
|
|
86
|
+
stageTagKeys,
|
|
87
|
+
stageTagSource = 'task_context',
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
const workingDir = cwd || projectPath || process.cwd();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await writeProjectTemplates(workingDir);
|
|
94
|
+
await ensureProjectSkillLinks(workingDir);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn('[Nano] Project template setup:', error.message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const streaming = String(
|
|
100
|
+
process.env.NANO_CLAUDE_CODE_STREAMING || ''
|
|
101
|
+
).trim() === '1';
|
|
102
|
+
|
|
103
|
+
const isPlaceholderSession =
|
|
104
|
+
!sessionId ||
|
|
105
|
+
String(sessionId).startsWith('new-session-');
|
|
106
|
+
|
|
107
|
+
const capturedSessionId = isPlaceholderSession ? crypto.randomUUID() : String(sessionId);
|
|
108
|
+
|
|
109
|
+
await ensureNanoDrClawSessionsRoot();
|
|
110
|
+
const sessionAbsPath = resolveNanoSessionAbsPath(capturedSessionId);
|
|
111
|
+
if (!sessionAbsPath) {
|
|
112
|
+
const err = 'Invalid Nano Claude Code session id';
|
|
113
|
+
ws.send({ type: 'claude-error', error: err, sessionId: null });
|
|
114
|
+
return Promise.reject(new Error(err));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (capturedSessionId && workingDir) {
|
|
118
|
+
applyStageTagsToSession({
|
|
119
|
+
sessionId: capturedSessionId,
|
|
120
|
+
projectPath: workingDir,
|
|
121
|
+
stageTagKeys,
|
|
122
|
+
source: stageTagSource,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isPlaceholderSession) {
|
|
127
|
+
recordIndexedSession({
|
|
128
|
+
sessionId: capturedSessionId,
|
|
129
|
+
provider: 'nano',
|
|
130
|
+
projectPath: workingDir,
|
|
131
|
+
sessionMode: sessionMode || 'research',
|
|
132
|
+
stageTagKeys,
|
|
133
|
+
tagSource: stageTagSource,
|
|
134
|
+
});
|
|
135
|
+
ws.send({
|
|
136
|
+
type: 'session-created',
|
|
137
|
+
sessionId: capturedSessionId,
|
|
138
|
+
provider: 'nano',
|
|
139
|
+
mode: sessionMode || 'research',
|
|
140
|
+
projectName: encodeProjectPath(workingDir),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
145
|
+
ws.setSessionId(capturedSessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await persistNanoSessionMetadata(capturedSessionId, workingDir, sessionMode);
|
|
149
|
+
|
|
150
|
+
const nanoCmd = resolveNanoCommand();
|
|
151
|
+
const args = [
|
|
152
|
+
'--output-format', 'stream-json',
|
|
153
|
+
'-p', command,
|
|
154
|
+
'--dangerously-skip-permissions',
|
|
155
|
+
'--session-file', sessionAbsPath,
|
|
156
|
+
];
|
|
157
|
+
if (!isPlaceholderSession) {
|
|
158
|
+
args.push('--resume', sessionAbsPath);
|
|
159
|
+
}
|
|
160
|
+
if (streaming) {
|
|
161
|
+
args.push('--streaming');
|
|
162
|
+
}
|
|
163
|
+
if (model) {
|
|
164
|
+
args.push('--model', model);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('[Nano] spawn:', nanoCmd, args.join(' '));
|
|
168
|
+
console.log('[Nano] cwd:', workingDir);
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const child = spawnFunction(nanoCmd, args, {
|
|
172
|
+
cwd: workingDir,
|
|
173
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
174
|
+
env: { ...(env || process.env) },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
activeNanoSessions.set(capturedSessionId, { process: child, startTime: Date.now() });
|
|
178
|
+
|
|
179
|
+
const getSessionStartTime = () => activeNanoSessions.get(capturedSessionId)?.startTime;
|
|
180
|
+
|
|
181
|
+
let stdoutBuf = '';
|
|
182
|
+
|
|
183
|
+
child.stdout.on('data', (data) => {
|
|
184
|
+
stdoutBuf += data.toString();
|
|
185
|
+
const lines = stdoutBuf.split('\n');
|
|
186
|
+
stdoutBuf = lines.pop() || '';
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
const t = line.trim();
|
|
189
|
+
if (!t) continue;
|
|
190
|
+
try {
|
|
191
|
+
const response = JSON.parse(t);
|
|
192
|
+
switch (response.type) {
|
|
193
|
+
case 'assistant':
|
|
194
|
+
if (response.message) {
|
|
195
|
+
ws.send({
|
|
196
|
+
type: 'claude-response',
|
|
197
|
+
data: {
|
|
198
|
+
type: 'assistant',
|
|
199
|
+
message: response.message,
|
|
200
|
+
startTime: getSessionStartTime(),
|
|
201
|
+
},
|
|
202
|
+
sessionId: capturedSessionId,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'user':
|
|
207
|
+
if (response.message) {
|
|
208
|
+
ws.send({
|
|
209
|
+
type: 'claude-response',
|
|
210
|
+
data: {
|
|
211
|
+
type: 'user',
|
|
212
|
+
message: response.message,
|
|
213
|
+
startTime: getSessionStartTime(),
|
|
214
|
+
},
|
|
215
|
+
sessionId: capturedSessionId,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
case 'stream_delta':
|
|
220
|
+
if (response.content && (response.delta_type === 'text' || response.delta_type === 'thinking')) {
|
|
221
|
+
ws.send({
|
|
222
|
+
type: 'claude-response',
|
|
223
|
+
data: {
|
|
224
|
+
type: 'content_block_delta',
|
|
225
|
+
startTime: getSessionStartTime(),
|
|
226
|
+
delta: { type: 'text_delta', text: response.content },
|
|
227
|
+
},
|
|
228
|
+
sessionId: capturedSessionId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
case 'result': {
|
|
233
|
+
const u = response.usage || {};
|
|
234
|
+
const input = Number(u.input_tokens) || 0;
|
|
235
|
+
const output = Number(u.output_tokens) || 0;
|
|
236
|
+
const cacheCreate = Number(u.cache_creation_input_tokens) || 0;
|
|
237
|
+
const cacheRead = Number(u.cache_read_input_tokens) || 0;
|
|
238
|
+
if (input || output || cacheCreate || cacheRead) {
|
|
239
|
+
const total = parseInt(process.env.CONTEXT_WINDOW || process.env.VITE_CONTEXT_WINDOW || '200000', 10);
|
|
240
|
+
ws.send({
|
|
241
|
+
type: 'token-budget',
|
|
242
|
+
data: {
|
|
243
|
+
used: input + output + cacheCreate + cacheRead,
|
|
244
|
+
total,
|
|
245
|
+
breakdown: {
|
|
246
|
+
input,
|
|
247
|
+
output,
|
|
248
|
+
cacheCreation: cacheCreate,
|
|
249
|
+
cacheRead,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
sessionId: capturedSessionId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
default:
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
console.warn('[Nano] Ignoring non-JSON stdout line:', t.slice(0, 200));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
child.stderr.on('data', (data) => {
|
|
267
|
+
const msg = data.toString().trim();
|
|
268
|
+
if (!msg) return;
|
|
269
|
+
console.error('[Nano] stderr:', msg);
|
|
270
|
+
ws.send({
|
|
271
|
+
type: 'claude-error',
|
|
272
|
+
error: msg.slice(0, 2000),
|
|
273
|
+
sessionId: capturedSessionId,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
child.on('close', async (code) => {
|
|
278
|
+
activeNanoSessions.delete(capturedSessionId);
|
|
279
|
+
ws.send({
|
|
280
|
+
type: 'claude-complete',
|
|
281
|
+
sessionId: capturedSessionId,
|
|
282
|
+
exitCode: code,
|
|
283
|
+
isNewSession: isPlaceholderSession && Boolean(command && String(command).trim()),
|
|
284
|
+
});
|
|
285
|
+
if (code === 0) {
|
|
286
|
+
resolve();
|
|
287
|
+
} else {
|
|
288
|
+
reject(new Error(`nano-claude-code exited with code ${code}`));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
child.on('error', (error) => {
|
|
293
|
+
activeNanoSessions.delete(capturedSessionId);
|
|
294
|
+
ws.send({
|
|
295
|
+
type: 'claude-error',
|
|
296
|
+
error: error.message,
|
|
297
|
+
sessionId: capturedSessionId,
|
|
298
|
+
});
|
|
299
|
+
reject(error);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function abortNanoClaudeCodeSession(sessionId) {
|
|
305
|
+
const sessionData = activeNanoSessions.get(sessionId);
|
|
306
|
+
if (sessionData?.process) {
|
|
307
|
+
console.log(`[Nano] Aborting session: ${sessionId}`);
|
|
308
|
+
sessionData.process.kill('SIGTERM');
|
|
309
|
+
activeNanoSessions.delete(sessionId);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function isNanoClaudeCodeSessionActive(sessionId) {
|
|
316
|
+
return activeNanoSessions.has(sessionId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function getNanoClaudeCodeSessionStartTime(sessionId) {
|
|
320
|
+
const sessionData = activeNanoSessions.get(sessionId);
|
|
321
|
+
return sessionData ? sessionData.startTime : null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function getActiveNanoClaudeCodeSessions() {
|
|
325
|
+
return Array.from(activeNanoSessions.keys());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Kill all in-flight Nano CLI children (e.g. before process exit). */
|
|
329
|
+
export function killAllNanoClaudeCodeChildren() {
|
|
330
|
+
for (const { process: childProc } of activeNanoSessions.values()) {
|
|
331
|
+
try {
|
|
332
|
+
if (childProc && !childProc.killed) {
|
|
333
|
+
childProc.kill('SIGTERM');
|
|
334
|
+
}
|
|
335
|
+
} catch (_) {
|
|
336
|
+
// ignore
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
activeNanoSessions.clear();
|
|
340
|
+
}
|