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.
Files changed (99) hide show
  1. package/dist/assets/{_basePickBy-CTZ7OJiJ.js → _basePickBy-7CEpiVqc.js} +1 -1
  2. package/dist/assets/{_baseUniq-LYG5erFT.js → _baseUniq-yh8GoKah.js} +1 -1
  3. package/dist/assets/{arc-CYhqYtN8.js → arc-xxYot-So.js} +1 -1
  4. package/dist/assets/{architectureDiagram-VXUJARFQ-B7xb9vYd.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
  5. package/dist/assets/{blockDiagram-VD42YOAC-Dz6eN3Yc.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
  6. package/dist/assets/{c4Diagram-YG6GDRKO-DIa9rHl5.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
  7. package/dist/assets/channel-BJdHHkhR.js +1 -0
  8. package/dist/assets/{chunk-4BX2VUAB-JZVAPeYL.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
  9. package/dist/assets/{chunk-55IACEB6-BoNiAnP-.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
  10. package/dist/assets/{chunk-B4BG7PRW-BURhBXxZ.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
  11. package/dist/assets/{chunk-DI55MBZ5-Bxa-FsXa.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
  12. package/dist/assets/{chunk-FMBD7UC4-Dn_6iLa4.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
  13. package/dist/assets/{chunk-QN33PNHL-B0N2-K2K.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
  14. package/dist/assets/{chunk-QZHKN3VN-Bd7csSYW.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
  15. package/dist/assets/{chunk-TZMSLE5B-DV5aJTr6.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
  16. package/dist/assets/{classDiagram-2ON5EDUG-iZL_J_xq.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-WZHVMYZB-iZL_J_xq.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
  18. package/dist/assets/clone-CwXTbwxl.js +1 -0
  19. package/dist/assets/{cose-bilkent-S5V4N54A-C_Uhc5Rn.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
  20. package/dist/assets/{dagre-6UL2VRFP-BiW_P9KL.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
  21. package/dist/assets/{diagram-PSM6KHXK-uGQgzEbt.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
  22. package/dist/assets/{diagram-QEK2KX5R-qox_ZDR7.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
  23. package/dist/assets/{diagram-S2PKOQOG-RKFFDPoU.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
  24. package/dist/assets/{erDiagram-Q2GNP2WA-11Go9j0k.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
  25. package/dist/assets/{flowDiagram-NV44I4VS-DBAiHvAx.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
  26. package/dist/assets/{ganttDiagram-JELNMOA3-Dzc11a-7.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
  27. package/dist/assets/{gitGraphDiagram-V2S2FVAM-DJVXfOqP.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
  28. package/dist/assets/{graph-CJ95S3XQ.js → graph-DGtZXuDY.js} +1 -1
  29. package/dist/assets/index-C-sL-ag0.css +1 -0
  30. package/dist/assets/index-VdqxIFPL.js +670 -0
  31. package/dist/assets/{infoDiagram-HS3SLOUP-DpdkGgBK.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-CSGpNz7D.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-BvPB3vMM.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
  34. package/dist/assets/{layout-C96BbhFQ.js → layout-Cnc-DCSb.js} +1 -1
  35. package/dist/assets/{linear-HjDfr5-H.js → linear-BtRgmV19.js} +1 -1
  36. package/dist/assets/{mermaid.core-J7RjAKTD.js → mermaid.core-B20uzv6N.js} +5 -5
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-C6MlEWCC.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-Bi5BLoNG.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-Bo14V3Q1.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-CP3_EwJ4.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-B3wFfI_W.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-fZVhVt_s.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-BooCitAX.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-CM0zUx-Q.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-B-8ph3e8.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-Q2k0EFpn.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
  48. package/dist/icons/nanoclaude.png +0 -0
  49. package/dist/index.html +2 -2
  50. package/package.json +1 -1
  51. package/public/icons/nanoclaude.png +0 -0
  52. package/scripts/native-runtime.mjs +5 -3
  53. package/server/__tests__/gemini-api.test.mjs +699 -0
  54. package/server/__tests__/gemini-session-index.test.mjs +95 -0
  55. package/server/claude-sdk.js +82 -3
  56. package/server/cli-chat.js +28 -17
  57. package/server/cli.js +13 -1
  58. package/server/data/news-log-arxiv.json +1 -0
  59. package/server/data/news-log-huggingface.json +1 -0
  60. package/server/data/news-results-arxiv.json +460 -0
  61. package/server/data/news-results-huggingface.json +817 -0
  62. package/server/data/research_interests_arxiv.json +50 -0
  63. package/server/data/research_interests_huggingface.json +4 -0
  64. package/server/database/db.js +92 -0
  65. package/server/database/init.sql +17 -1
  66. package/server/gemini-api.js +1838 -0
  67. package/server/gemini-cli.js +13 -2
  68. package/server/index.js +94 -17
  69. package/server/load-env.js +13 -1
  70. package/server/local-gpu.js +3 -1
  71. package/server/nano-claude-code.js +340 -0
  72. package/server/nanoSessionPaths.js +41 -0
  73. package/server/openai-codex.js +6 -2
  74. package/server/openrouter.js +37 -22
  75. package/server/projects.js +207 -0
  76. package/server/routes/agent.js +32 -7
  77. package/server/routes/auto-research.js +1 -1
  78. package/server/routes/claude-btw.js +55 -0
  79. package/server/routes/cli-auth.js +103 -2
  80. package/server/routes/commands.js +25 -1
  81. package/server/routes/community-tools.js +26 -21
  82. package/server/routes/memory.js +143 -0
  83. package/server/routes/news.js +3 -0
  84. package/server/routes/skills.js +35 -0
  85. package/server/routes/user.js +3 -5
  86. package/server/utils/__tests__/safePath.test.js +93 -0
  87. package/server/utils/cliResolution.js +13 -1
  88. package/server/utils/geminiOAuth.js +333 -0
  89. package/server/utils/memoryPrompt.js +78 -0
  90. package/server/utils/safePath.js +51 -0
  91. package/server/utils/sessionIndex.js +2 -0
  92. package/server/utils/skillExpander.js +142 -0
  93. package/server/utils/spawnAsync.js +78 -0
  94. package/shared/modelConstants.js +19 -1
  95. package/dist/assets/channel-Dk-4X8XT.js +0 -1
  96. package/dist/assets/clone-DbPu_xVW.js +0 -1
  97. package/dist/assets/index-1MRmplD1.css +0 -1
  98. package/dist/assets/index-CFbkRzqw.js +0 -664
  99. package/dist/assets/stateDiagram-v2-4FDKWEC3-BzH7qQMD.js +0 -1
@@ -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(command, effectiveAttachments, workingDir);
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
- const effectivePrompt = `${attachmentResult.modifiedCommand}`;
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, getContextWindowForModel } from './claude-sdk.js';
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
- app.use(cors());
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 (model lookup > env var > default)
2836
- const contextWindow = getContextWindowForModel(modelName);
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 (input + output share the same context window)
2839
- // This is a per-call snapshot from the latest assistant message, no cross-turn double-counting.
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) {
@@ -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('=').trim();
65
+ process.env[key] = stripOptionalEnvQuotes(valueParts.join('='));
54
66
  }
55
67
  }
56
68
  });
@@ -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 systemContent = customSystemPrompt || await buildSystemPrompt(workingDirectory);
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
+ }