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
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Central Nano Claude Code session JSON location (~/.dr-claw/nano-sessions).
3
+ * Matches file watcher PROVIDER_WATCH_PATHS and avoids cwd-scattered drclaw-nano-*.json.
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import os from 'os';
9
+
10
+ export function getNanoDrClawSessionsRoot() {
11
+ return path.join(os.homedir(), '.dr-claw', 'nano-sessions');
12
+ }
13
+
14
+ export async function ensureNanoDrClawSessionsRoot() {
15
+ const root = getNanoDrClawSessionsRoot();
16
+ await fs.mkdir(root, { recursive: true });
17
+ return root;
18
+ }
19
+
20
+ /**
21
+ * Plain filename only: drclaw-nano-<sanitizedId>.json — no path segments / traversal.
22
+ */
23
+ export function safeNanoSessionFilename(sessionId) {
24
+ const safe = String(sessionId || '').replace(/[^a-zA-Z0-9._-]/g, '');
25
+ if (!safe || safe.includes('..')) {
26
+ return null;
27
+ }
28
+ const filename = `drclaw-nano-${safe}.json`;
29
+ if (path.basename(filename) !== filename) {
30
+ return null;
31
+ }
32
+ return filename;
33
+ }
34
+
35
+ export function resolveNanoSessionAbsPath(sessionId) {
36
+ const filename = safeNanoSessionFilename(sessionId);
37
+ if (!filename) {
38
+ return null;
39
+ }
40
+ return path.join(getNanoDrClawSessionsRoot(), filename);
41
+ }
@@ -22,6 +22,7 @@ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIn
22
22
  import { classifyError, classifySDKError } from '../shared/errorClassifier.js';
23
23
  import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
24
24
  import { buildCodexRealtimeTokenBudget } from './utils/sessionTokenUsage.js';
25
+ import { expandSkillCommand } from './utils/skillExpander.js';
25
26
 
26
27
  // Track active sessions
27
28
  const activeCodexSessions = new Map();
@@ -446,14 +447,17 @@ export async function queryCodex(command, options = {}, ws) {
446
447
 
447
448
  publishSessionId(thread.id || sessionId || null);
448
449
 
449
- const preparedInput = await prepareCodexInput(command, images, workingDirectory);
450
+ // Expand /skill-name slash commands into full SKILL.md instructions
451
+ const expandedCommand = await expandSkillCommand(command?.trim() || command, workingDirectory);
452
+
453
+ const preparedInput = await prepareCodexInput(expandedCommand, images, workingDirectory);
450
454
  tempImagePaths = preparedInput.tempImagePaths;
451
455
  tempDir = preparedInput.tempDir;
452
456
 
453
457
  // Execute with streaming
454
458
  // Prefer pre-uploaded attachments (buildCodexInput) over base64 temp images (prepareCodexInput)
455
459
  const codexInput = attachments
456
- ? buildCodexInput(command, attachments)
460
+ ? buildCodexInput(expandedCommand, attachments)
457
461
  : preparedInput.input;
458
462
  const streamedTurn = await thread.runStreamed(codexInput, {
459
463
  signal: abortController.signal
@@ -15,11 +15,15 @@ import path from 'path';
15
15
  import os from 'os';
16
16
  import { exec } from 'child_process';
17
17
  import { promisify } from 'util';
18
+ import spawnAsync from './utils/spawnAsync.js';
18
19
  import { encodeProjectPath, ensureProjectSkillLinks, reconcileOpenRouterSessionIndex } from './projects.js';
19
20
  import { writeProjectTemplates } from './templates/index.js';
20
21
  import { classifyError } from '../shared/errorClassifier.js';
21
22
  import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
22
23
  import { createRequestId, waitForToolApproval, matchesToolPermission } from './utils/permissions.js';
24
+ import { buildMemoryBlock } from './utils/memoryPrompt.js';
25
+ import { expandSkillCommand } from './utils/skillExpander.js';
26
+ import { safePath } from './utils/safePath.js';
23
27
 
24
28
  const execAsync = promisify(exec);
25
29
 
@@ -220,10 +224,9 @@ function sendMessage(ws, data) {
220
224
  // ---------------------------------------------------------------------------
221
225
 
222
226
  async function executeTool(name, args, workingDir) {
223
- const resolve = (p) => {
224
- if (!p) return workingDir;
225
- return path.isAbsolute(p) ? p : path.resolve(workingDir, p);
226
- };
227
+ // Constrain all paths to the project root to prevent LLM-driven
228
+ // prompt-injection attacks from reading/writing outside the workspace.
229
+ const resolve = (p) => safePath(p, workingDir);
227
230
  const trunc = (s) =>
228
231
  s.length <= MAX_OUTPUT_CHARS ? s : s.slice(0, MAX_OUTPUT_CHARS) + `\n…(truncated, ${s.length} total chars)`;
229
232
 
@@ -276,25 +279,33 @@ async function executeTool(name, args, workingDir) {
276
279
 
277
280
  case 'Glob': {
278
281
  const dir = resolve(args.path);
279
- const { stdout } = await execAsync(
280
- `rg --files --glob '${args.pattern}' 2>/dev/null | head -300`,
281
- { cwd: dir, timeout: 30_000, maxBuffer: 1024 * 1024 },
282
- ).catch(() =>
283
- execAsync(`find . -name '${args.pattern}' -type f 2>/dev/null | head -300`, {
284
- cwd: dir, timeout: 30_000, maxBuffer: 1024 * 1024,
285
- }),
286
- );
287
- return trunc(stdout || '(no matches)');
282
+ try {
283
+ const { stdout } = await spawnAsync('rg', ['--files', '--glob', args.pattern], {
284
+ cwd: dir, timeout: 30_000,
285
+ });
286
+ const lines = stdout.split('\n').filter(Boolean).slice(0, 300);
287
+ return trunc(lines.join('\n') || '(no matches)');
288
+ } catch {
289
+ try {
290
+ const { stdout } = await spawnAsync('find', ['.', '-name', args.pattern, '-type', 'f'], {
291
+ cwd: dir, timeout: 30_000,
292
+ });
293
+ const lines = stdout.split('\n').filter(Boolean).slice(0, 300);
294
+ return trunc(lines.join('\n') || '(no matches)');
295
+ } catch {
296
+ return '(no matches)';
297
+ }
298
+ }
288
299
  }
289
300
 
290
301
  case 'Grep': {
291
302
  const target = resolve(args.path);
292
- let cmd = `rg --line-number --max-count 100 --max-columns 200`;
293
- if (args.include) cmd += ` --glob '${args.include}'`;
294
- cmd += ` '${args.pattern.replace(/'/g, "'\\''")}' '${target}'`;
303
+ const rgArgs = ['--line-number', '--max-count', '100', '--max-columns', '200'];
304
+ if (args.include) rgArgs.push('--glob', args.include);
305
+ rgArgs.push('-e', args.pattern, '--', target);
295
306
  try {
296
- const { stdout } = await execAsync(cmd, {
297
- cwd: workingDir, timeout: 30_000, maxBuffer: 2 * 1024 * 1024,
307
+ const { stdout } = await spawnAsync('rg', rgArgs, {
308
+ cwd: workingDir, timeout: 30_000,
298
309
  });
299
310
  return trunc(stdout || '(no matches)');
300
311
  } catch (err) {
@@ -645,8 +656,9 @@ export async function queryOpenRouter(command, options = {}, ws) {
645
656
  projectName: workingDirectory ? encodeProjectPath(workingDirectory) : undefined,
646
657
  });
647
658
 
648
- // ── Build conversation ─────────────────────────────────────────────
649
- const systemContent = customSystemPrompt || await buildSystemPrompt(workingDirectory);
659
+ // ── Build conversation ───────────────────���─────────────────────────
660
+ const memoryBlock = options.userId ? buildMemoryBlock(options.userId) : '';
661
+ const systemContent = (customSystemPrompt || await buildSystemPrompt(workingDirectory)) + memoryBlock;
650
662
  const messages = [{ role: 'system', content: systemContent }];
651
663
 
652
664
  if (sessionId) {
@@ -657,8 +669,11 @@ export async function queryOpenRouter(command, options = {}, ws) {
657
669
  }
658
670
  }
659
671
 
660
- messages.push({ role: 'user', content: command });
661
- await appendSession(currentSessionId, { role: 'user', content: command }).catch(() => {});
672
+ // Expand /skill-name slash commands into full SKILL.md instructions
673
+ const expandedCommand = await expandSkillCommand(command?.trim() || command, workingDirectory);
674
+
675
+ messages.push({ role: 'user', content: expandedCommand });
676
+ await appendSession(currentSessionId, { role: 'user', content: expandedCommand }).catch(() => {});
662
677
 
663
678
  const tools = permissionMode === 'plan'
664
679
  ? TOOL_SCHEMAS.filter((t) => READ_ONLY_TOOLS.has(t.function.name))
@@ -84,6 +84,7 @@ import {
84
84
  normalizeSessionMode,
85
85
  readExplicitSessionModeFromMetadata,
86
86
  } from './utils/sessionMode.js';
87
+ import { resolveNanoSessionAbsPath, safeNanoSessionFilename } from './nanoSessionPaths.js';
87
88
 
88
89
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
89
90
  const DRCLAW_SKILLS_DIR = path.join(__dirname, '..', 'skills');
@@ -1040,6 +1041,20 @@ function mapIndexedSessionToProjectSession(session, provider) {
1040
1041
  };
1041
1042
  }
1042
1043
 
1044
+ if (provider === 'nano') {
1045
+ return {
1046
+ id: session.id,
1047
+ summary: baseName || 'Nano Claude Code Session',
1048
+ name: baseName || 'Nano Claude Code Session',
1049
+ createdAt,
1050
+ lastActivity,
1051
+ messageCount,
1052
+ mode,
1053
+ tags,
1054
+ __provider: 'nano',
1055
+ };
1056
+ }
1057
+
1043
1058
  return {
1044
1059
  id: session.id,
1045
1060
  summary: baseName || 'New Session',
@@ -1062,6 +1077,8 @@ function getSessionPlaceholderName(provider) {
1062
1077
  return 'Gemini Session';
1063
1078
  case 'openrouter':
1064
1079
  return 'OpenRouter Session';
1080
+ case 'nano':
1081
+ return 'Nano Claude Code Session';
1065
1082
  default:
1066
1083
  return 'New Session';
1067
1084
  }
@@ -1384,6 +1401,7 @@ async function getProjects(userId, progressCallback = null) {
1384
1401
  const geminiSessions = projectSessions.filter((session) => session.provider === 'gemini');
1385
1402
  const openrouterSessions = projectSessions.filter((session) => session.provider === 'openrouter');
1386
1403
  const localSessions = projectSessions.filter((session) => session.provider === 'local');
1404
+ const nanoSessions = projectSessions.filter((session) => session.provider === 'nano');
1387
1405
 
1388
1406
  project.sessions = claudeSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'claude'));
1389
1407
  project.sessionMeta = {
@@ -1395,6 +1413,7 @@ async function getProjects(userId, progressCallback = null) {
1395
1413
  project.geminiSessions = geminiSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'gemini'));
1396
1414
  project.openrouterSessions = openrouterSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'openrouter'));
1397
1415
  project.localSessions = localSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'local'));
1416
+ project.nanoSessions = nanoSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'nano'));
1398
1417
 
1399
1418
  const taskmasterResult = await detectTaskMasterFolder(actualProjectDir).catch(() => null);
1400
1419
 
@@ -1879,6 +1898,119 @@ async function parseAgentTools(filePath) {
1879
1898
  return tools;
1880
1899
  }
1881
1900
 
1901
+ function extractNanoMessageText(content) {
1902
+ if (typeof content === 'string') {
1903
+ return content;
1904
+ }
1905
+ if (!Array.isArray(content)) {
1906
+ return '';
1907
+ }
1908
+ const parts = [];
1909
+ for (const block of content) {
1910
+ if (typeof block === 'string') {
1911
+ parts.push(block);
1912
+ } else if (block && typeof block === 'object') {
1913
+ if (block.type === 'text' && block.text) {
1914
+ parts.push(String(block.text));
1915
+ }
1916
+ }
1917
+ }
1918
+ return parts.join('\n');
1919
+ }
1920
+
1921
+ /**
1922
+ * Real cwd for Nano session files. encodeProjectPath ↔ simple decode in extractProjectDirectory
1923
+ * is lossy; session metadata and projects.path store the authoritative path.
1924
+ */
1925
+ async function resolveNanoProjectDirectory(projectName, sessionId) {
1926
+ const { sessionDb, projectDb } = await import('./database/db.js');
1927
+ const session = sessionDb.getSessionById(sessionId);
1928
+ const metaPath = session?.metadata?.projectPath;
1929
+ if (typeof metaPath === 'string' && metaPath.trim()) {
1930
+ return metaPath.trim();
1931
+ }
1932
+ const projectRow = projectDb.getProjectById(projectName);
1933
+ if (projectRow?.path && typeof projectRow.path === 'string' && projectRow.path.trim()) {
1934
+ return projectRow.path.trim();
1935
+ }
1936
+ return extractProjectDirectory(projectName);
1937
+ }
1938
+
1939
+ /** Prefer ~/.dr-claw/nano-sessions; fall back to legacy <projectCwd>/drclaw-nano-*.json */
1940
+ async function findNanoSessionStoragePath(projectName, sessionId) {
1941
+ const central = resolveNanoSessionAbsPath(sessionId);
1942
+ if (central) {
1943
+ try {
1944
+ await fs.access(central);
1945
+ return central;
1946
+ } catch (_) {
1947
+ // legacy below
1948
+ }
1949
+ }
1950
+ const basename = safeNanoSessionFilename(sessionId);
1951
+ if (!basename) {
1952
+ return null;
1953
+ }
1954
+ try {
1955
+ const projectPath = await resolveNanoProjectDirectory(projectName, sessionId);
1956
+ const legacy = path.join(projectPath, basename);
1957
+ await fs.access(legacy);
1958
+ return legacy;
1959
+ } catch (_) {
1960
+ return null;
1961
+ }
1962
+ }
1963
+
1964
+ async function unlinkNanoSessionFilesEverywhere(projectName, sessionId) {
1965
+ let deleted = false;
1966
+ const central = resolveNanoSessionAbsPath(sessionId);
1967
+ if (central) {
1968
+ try {
1969
+ await fs.unlink(central);
1970
+ deleted = true;
1971
+ } catch (e) {
1972
+ if (e?.code !== 'ENOENT') {
1973
+ throw e;
1974
+ }
1975
+ }
1976
+ }
1977
+ const basename = safeNanoSessionFilename(sessionId);
1978
+ if (basename) {
1979
+ try {
1980
+ const projectPath = await resolveNanoProjectDirectory(projectName, sessionId);
1981
+ const legacy = path.join(projectPath, basename);
1982
+ await fs.unlink(legacy);
1983
+ deleted = true;
1984
+ } catch (e) {
1985
+ if (e?.code !== 'ENOENT') {
1986
+ throw e;
1987
+ }
1988
+ }
1989
+ }
1990
+ return deleted;
1991
+ }
1992
+
1993
+ /** Maps nano-claude-code session.json (title, messages[]) to Dr. Claw chat message shape. */
1994
+ function nanoSessionJsonToMessages(data) {
1995
+ const rawMessages = Array.isArray(data.messages) ? data.messages : [];
1996
+ const savedAt = data.saved_at || '';
1997
+ const out = [];
1998
+ for (const m of rawMessages) {
1999
+ const role = m.role;
2000
+ if (role !== 'user' && role !== 'assistant') {
2001
+ continue;
2002
+ }
2003
+ const text = extractNanoMessageText(m.content);
2004
+ out.push({
2005
+ type: 'message',
2006
+ role,
2007
+ content: text || '',
2008
+ timestamp: savedAt,
2009
+ });
2010
+ }
2011
+ return out;
2012
+ }
2013
+
1882
2014
  // Get messages for a specific session with pagination support
1883
2015
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0, provider = 'claude', userId = null) {
1884
2016
  console.log(`[DEBUG] getSessionMessages - project: ${projectName}, session: ${sessionId}, provider: ${provider}`);
@@ -2117,6 +2249,36 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
2117
2249
  }
2118
2250
  }
2119
2251
 
2252
+ if (String(provider || '').toLowerCase() === 'nano') {
2253
+ console.log('[DEBUG] Reading Nano Claude Code session file');
2254
+ try {
2255
+ const nanoPath = await findNanoSessionStoragePath(projectName, sessionId);
2256
+ if (!nanoPath) {
2257
+ return limit === null ? [] : { messages: [], total: 0, hasMore: false };
2258
+ }
2259
+ const raw = await fs.readFile(nanoPath, 'utf-8');
2260
+ const data = JSON.parse(raw);
2261
+ const messages = nanoSessionJsonToMessages(data);
2262
+ console.log(`[DEBUG] Found ${messages.length} messages in Nano session file at ${nanoPath}`);
2263
+ const total = messages.length;
2264
+ if (limit === null) {
2265
+ return messages;
2266
+ }
2267
+ const startIndex = Math.max(0, total - offset - limit);
2268
+ const endIndex = total - offset;
2269
+ return {
2270
+ messages: messages.slice(startIndex, endIndex),
2271
+ total,
2272
+ hasMore: startIndex > 0,
2273
+ offset,
2274
+ limit,
2275
+ };
2276
+ } catch (e) {
2277
+ console.warn(`Could not read Nano session ${sessionId}:`, e.message);
2278
+ return limit === null ? [] : { messages: [], total: 0, hasMore: false };
2279
+ }
2280
+ }
2281
+
2120
2282
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
2121
2283
 
2122
2284
  try {
@@ -2336,6 +2498,31 @@ async function deleteSession(projectName, sessionId, provider = 'claude') {
2336
2498
  throw new Error(`Local GPU session ${sessionId} not found in file system or index`);
2337
2499
  }
2338
2500
 
2501
+ if (String(provider || '').toLowerCase() === 'nano') {
2502
+ if (!safeNanoSessionFilename(sessionId)) {
2503
+ throw new Error('Invalid Nano session id');
2504
+ }
2505
+ let deletedFile = false;
2506
+ try {
2507
+ deletedFile = await unlinkNanoSessionFilesEverywhere(projectName, sessionId);
2508
+ } catch (e) {
2509
+ console.error(`[Nano] Failed to delete session ${sessionId}:`, e.message);
2510
+ throw new Error(`Failed to delete Nano session: ${e.message}`);
2511
+ }
2512
+
2513
+ const deletedIndex = indexedSession?.provider === 'nano' || deletedFile;
2514
+ if (deletedIndex) {
2515
+ sessionDb.deleteSession(sessionId);
2516
+ }
2517
+
2518
+ if (deletedFile || deletedIndex) {
2519
+ console.log(`[Nano] Deleted session ${sessionId}${deletedFile ? ' (removed session file if present)' : ' from index only'}`);
2520
+ return true;
2521
+ }
2522
+
2523
+ throw new Error(`Nano session ${sessionId} not found in file system or index`);
2524
+ }
2525
+
2339
2526
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
2340
2527
 
2341
2528
  try {
@@ -4043,6 +4230,26 @@ async function renameSession(projectName, sessionId, newSummary, provider = 'cla
4043
4230
  console.error(`[Gemini] Failed to rename session ${sessionId}:`, e.message);
4044
4231
  throw new Error(`Failed to rename Gemini session: ${e.message}`);
4045
4232
  }
4233
+ } else if (String(provider || '').toLowerCase() === 'nano') {
4234
+ if (!safeNanoSessionFilename(sessionId)) {
4235
+ throw new Error('Invalid Nano session id');
4236
+ }
4237
+ const nanoPath = await findNanoSessionStoragePath(projectName, sessionId);
4238
+ if (!nanoPath) {
4239
+ throw new Error(`Nano session ${sessionId} file not found`);
4240
+ }
4241
+ try {
4242
+ const raw = await fs.readFile(nanoPath, 'utf-8');
4243
+ const data = JSON.parse(raw);
4244
+ data.title = trimmedSummary;
4245
+ await fs.writeFile(nanoPath, JSON.stringify(data, null, 2), 'utf-8');
4246
+ sessionDb.updateSessionName(sessionId, trimmedSummary);
4247
+ console.log(`[Nano] Renamed session ${sessionId} to "${trimmedSummary}"`);
4248
+ return true;
4249
+ } catch (e) {
4250
+ console.error(`[Nano] Failed to rename session ${sessionId}:`, e.message);
4251
+ throw new Error(`Failed to rename Nano session: ${e.message}`);
4252
+ }
4046
4253
  }
4047
4254
  // 2. Handle Cursor sessions (SQLite)
4048
4255
  else if (provider === 'cursor') {
@@ -10,10 +10,12 @@ import { queryClaudeSDK } from '../claude-sdk.js';
10
10
  import { spawnCursor } from '../cursor-cli.js';
11
11
  import { queryCodex } from '../openai-codex.js';
12
12
  import { spawnGemini } from '../gemini-cli.js';
13
+ import { queryGeminiApi } from '../gemini-api.js';
13
14
  import { queryOpenRouter } from '../openrouter.js';
14
15
  import { queryLocalGPU } from '../local-gpu.js';
16
+ import { spawnNanoClaudeCode } from '../nano-claude-code.js';
15
17
  import { Octokit } from '@octokit/rest';
16
- import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, LOCAL_MODELS, OPENROUTER_MODELS } from '../../shared/modelConstants.js';
18
+ import { CODEX_MODELS, GEMINI_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../shared/modelConstants.js';
17
19
  import { IS_PLATFORM } from '../constants/config.js';
18
20
  import { getGeminiApiKeyForUser, withGeminiApiKeyEnv } from '../utils/geminiApiKey.js';
19
21
 
@@ -859,8 +861,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
859
861
  return res.status(400).json({ error: 'message is required' });
860
862
  }
861
863
 
862
- if (!['claude', 'cursor', 'codex', 'gemini', 'openrouter', 'local'].includes(provider)) {
863
- return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", "openrouter", or "local"' });
864
+ if (!['claude', 'cursor', 'codex', 'gemini', 'openrouter', 'local', 'nano'].includes(provider)) {
865
+ return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", "openrouter", "local", or "nano"' });
864
866
  }
865
867
 
866
868
  // Validate GitHub branch/PR creation requirements
@@ -982,15 +984,28 @@ router.post('/', validateExternalApiKey, async (req, res) => {
982
984
  permissionMode: 'bypassPermissions'
983
985
  }, writer);
984
986
  } else if (provider === 'gemini') {
985
- console.log('🤖 Starting Gemini CLI session');
987
+ console.log('🤖 Starting Gemini session');
986
988
 
987
- await spawnGemini(message.trim(), {
989
+ const geminiOptions = {
988
990
  projectPath: finalProjectPath,
989
991
  cwd: finalProjectPath,
990
992
  sessionId: null,
991
993
  env: sessionEnv,
992
- model: model || GEMINI_MODELS.DEFAULT
993
- }, writer);
994
+ model: model || GEMINI_MODELS.DEFAULT,
995
+ userId: req.user?.id,
996
+ };
997
+
998
+ const result = await queryGeminiApi(message.trim(), geminiOptions, writer);
999
+ if (result?.authFailed) {
1000
+ console.log('🤖 Falling back to Gemini CLI harness');
1001
+ await spawnGemini(message.trim(), {
1002
+ projectPath: finalProjectPath,
1003
+ cwd: finalProjectPath,
1004
+ sessionId: null,
1005
+ env: sessionEnv,
1006
+ model: model || GEMINI_MODELS.DEFAULT
1007
+ }, writer);
1008
+ }
994
1009
  } else if (provider === 'openrouter') {
995
1010
  console.log('🤖 Starting OpenRouter session');
996
1011
 
@@ -1012,6 +1027,16 @@ router.post('/', validateExternalApiKey, async (req, res) => {
1012
1027
  env: sessionEnv,
1013
1028
  permissionMode: 'bypassPermissions',
1014
1029
  }, writer);
1030
+ } else if (provider === 'nano') {
1031
+ console.log('🤖 Starting Nano Claude Code session');
1032
+
1033
+ await spawnNanoClaudeCode(message.trim(), {
1034
+ projectPath: finalProjectPath,
1035
+ cwd: finalProjectPath,
1036
+ sessionId: null,
1037
+ model: model || NANO_CLAUDE_CODE_MODELS.DEFAULT,
1038
+ env: sessionEnv,
1039
+ }, writer);
1015
1040
  }
1016
1041
 
1017
1042
  // Handle GitHub branch and PR creation after successful agent completion
@@ -27,7 +27,7 @@ function getDefaultModelForProvider(provider) {
27
27
  return CODEX_MODELS.DEFAULT || 'gpt-5.4';
28
28
  }
29
29
  if (provider === 'gemini') {
30
- return GEMINI_MODELS.DEFAULT || 'gemini-3-flash-preview';
30
+ return GEMINI_MODELS.DEFAULT || 'gemini-2.5-flash';
31
31
  }
32
32
  if (provider === 'openrouter') {
33
33
  return OPENROUTER_MODELS.DEFAULT || 'anthropic/claude-sonnet-4';
@@ -0,0 +1,55 @@
1
+ import express from 'express';
2
+ import { runClaudeBtw } from '../claude-sdk.js';
3
+
4
+ const router = express.Router();
5
+
6
+ const MAX_QUESTION_CHARS = 2000;
7
+ const MAX_TRANSCRIPT_CHARS = 150_000;
8
+
9
+ /**
10
+ * POST /api/claude/btw
11
+ * Ephemeral side question (no tools, separate from main chat session).
12
+ */
13
+ router.post('/btw', async (req, res) => {
14
+ try {
15
+ const { question, transcript, projectPath, model } = req.body || {};
16
+ const q = typeof question === 'string' ? question.trim() : '';
17
+ if (!q) {
18
+ return res.status(400).json({ error: 'question is required' });
19
+ }
20
+ if (q.length > MAX_QUESTION_CHARS) {
21
+ return res.status(400).json({ error: `question exceeds ${MAX_QUESTION_CHARS} character limit` });
22
+ }
23
+
24
+ const raw = typeof transcript === 'string' ? transcript : '';
25
+ const text = raw.length > MAX_TRANSCRIPT_CHARS ? raw.slice(raw.length - MAX_TRANSCRIPT_CHARS) : raw;
26
+ const cwd = typeof projectPath === 'string' && projectPath.trim() ? projectPath.trim() : undefined;
27
+ const modelId = typeof model === 'string' && model.trim() ? model.trim() : undefined;
28
+
29
+ const ac = new AbortController();
30
+ res.on('close', () => {
31
+ if (!res.writableFinished) {
32
+ ac.abort();
33
+ }
34
+ });
35
+
36
+ const { answer } = await runClaudeBtw({
37
+ question: q,
38
+ transcript: text,
39
+ cwd,
40
+ model: modelId,
41
+ signal: ac.signal,
42
+ });
43
+
44
+ if (!res.headersSent) {
45
+ res.json({ answer });
46
+ }
47
+ } catch (error) {
48
+ if (!res.headersSent) {
49
+ console.error('[ERROR] /api/claude/btw:', error.message);
50
+ res.status(500).json({ error: error.message || 'btw request failed' });
51
+ }
52
+ }
53
+ });
54
+
55
+ export default router;