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
|
@@ -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
|
+
}
|
package/server/openai-codex.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
460
|
+
? buildCodexInput(expandedCommand, attachments)
|
|
457
461
|
: preparedInput.input;
|
|
458
462
|
const streamedTurn = await thread.runStreamed(codexInput, {
|
|
459
463
|
signal: abortController.signal
|
package/server/openrouter.js
CHANGED
|
@@ -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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
if (args.include)
|
|
294
|
-
|
|
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
|
|
297
|
-
cwd: workingDir, timeout: 30_000,
|
|
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
|
|
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
|
-
|
|
661
|
-
|
|
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))
|
package/server/projects.js
CHANGED
|
@@ -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') {
|
package/server/routes/agent.js
CHANGED
|
@@ -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 {
|
|
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 "
|
|
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
|
|
987
|
+
console.log('🤖 Starting Gemini session');
|
|
986
988
|
|
|
987
|
-
|
|
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
|
-
|
|
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-
|
|
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;
|