dr-claw 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -177
- package/README.zh-CN.md +286 -174
- package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-Blm-emkR.js → graph-DGtZXuDY.js} +1 -1
- package/dist/assets/index-C-sL-ag0.css +1 -0
- package/dist/assets/index-VdqxIFPL.js +670 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/screenshot_v1_2.png +0 -0
- package/package.json +61 -4
- package/public/icons/nanoclaude.png +0 -0
- package/public/screenshots/screenshot_v1_2.png +0 -0
- package/scripts/build-electron-icons.mjs +40 -0
- package/scripts/check-deps.js +28 -0
- package/scripts/native-runtime.mjs +128 -0
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/__tests__/project-sync-dedup.test.mjs +173 -0
- package/server/__tests__/session-delete.test.mjs +152 -0
- package/server/claude-sdk.js +175 -14
- package/server/cli-chat.js +28 -17
- package/server/cli.js +48 -11
- package/server/data/news-log-arxiv.json +1 -0
- package/server/data/news-log-huggingface.json +1 -0
- package/server/data/news-results-arxiv.json +460 -0
- package/server/data/news-results-huggingface.json +817 -0
- package/server/data/research_interests_arxiv.json +50 -0
- package/server/data/research_interests_huggingface.json +4 -0
- package/server/database/db.js +101 -5
- package/server/database/init.sql +17 -1
- package/server/gemini-api.js +1838 -0
- package/server/gemini-cli.js +13 -2
- package/server/index.js +94 -16
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +166 -24
- package/server/mcp-compute.js +223 -0
- package/server/middleware/auth.js +25 -6
- package/server/nano-claude-code.js +340 -0
- package/server/nanoSessionPaths.js +41 -0
- package/server/openai-codex.js +6 -2
- package/server/openrouter.js +37 -22
- package/server/projects.js +516 -110
- package/server/routes/agent.js +32 -7
- package/server/routes/auto-research.js +1 -1
- package/server/routes/claude-btw.js +55 -0
- package/server/routes/cli-auth.js +422 -58
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +398 -0
- package/server/routes/compute.js +155 -0
- package/server/routes/git.js +9 -11
- package/server/routes/memory.js +143 -0
- package/server/routes/news.js +3 -0
- package/server/routes/skills.js +35 -0
- package/server/routes/user.js +3 -5
- package/server/utils/__tests__/safePath.test.js +93 -0
- package/server/utils/cliResolution.js +13 -1
- package/server/utils/codexSessionMessages.js +285 -0
- package/server/utils/geminiOAuth.js +333 -0
- package/server/utils/memoryPrompt.js +78 -0
- package/server/utils/safePath.js +51 -0
- package/server/utils/sessionIndex.js +2 -0
- package/server/utils/skillExpander.js +142 -0
- package/server/utils/spawnAsync.js +78 -0
- package/shared/modelConstants.js +19 -1
- package/dist/assets/channel-BFOE7OyC.js +0 -1
- package/dist/assets/clone-BulQSblH.js +0 -1
- package/dist/assets/index-B8Yutsnm.css +0 -1
- package/dist/assets/index-Cf6gyhc3.js +0 -654
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
- package/scripts/install-python-tools.js +0 -64
package/server/projects.js
CHANGED
|
@@ -67,6 +67,16 @@ import sqlite3 from 'sqlite3';
|
|
|
67
67
|
import { open } from 'sqlite';
|
|
68
68
|
import os from 'os';
|
|
69
69
|
import { stripInternalContextPrefix } from './utils/sessionFormatting.js';
|
|
70
|
+
import {
|
|
71
|
+
buildToolResultMessage,
|
|
72
|
+
buildToolUseMessage,
|
|
73
|
+
extractTextContent,
|
|
74
|
+
normalizeCodexCustomToolCall,
|
|
75
|
+
normalizeCodexFunctionCall,
|
|
76
|
+
normalizeExecCommandEvent,
|
|
77
|
+
normalizePatchApplyEvent,
|
|
78
|
+
normalizeWebSearchCall,
|
|
79
|
+
} from './utils/codexSessionMessages.js';
|
|
70
80
|
import {
|
|
71
81
|
extractSessionModeFromMetadata,
|
|
72
82
|
extractSessionModeFromText,
|
|
@@ -74,6 +84,7 @@ import {
|
|
|
74
84
|
normalizeSessionMode,
|
|
75
85
|
readExplicitSessionModeFromMetadata,
|
|
76
86
|
} from './utils/sessionMode.js';
|
|
87
|
+
import { resolveNanoSessionAbsPath, safeNanoSessionFilename } from './nanoSessionPaths.js';
|
|
77
88
|
|
|
78
89
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
79
90
|
const DRCLAW_SKILLS_DIR = path.join(__dirname, '..', 'skills');
|
|
@@ -84,6 +95,8 @@ const CURRENT_DEFAULT_WORKSPACES_ROOT = path.join(os.homedir(), 'dr-claw');
|
|
|
84
95
|
const DELETED_PROJECTS_CONFIG_KEY = '_deletedProjects';
|
|
85
96
|
|
|
86
97
|
let projectConfigMutationQueue = Promise.resolve();
|
|
98
|
+
const _lastBootstrapByUser = new Map(); // userId -> timestamp
|
|
99
|
+
const BOOTSTRAP_STALENESS_MS = 60_000; // Only re-scan legacy sources every 60 seconds
|
|
87
100
|
|
|
88
101
|
function isProjectTrashed(projectInfo = null, dbEntry = null) {
|
|
89
102
|
return Boolean(projectInfo?.trash?.trashedAt || dbEntry?.metadata?.trash?.trashedAt);
|
|
@@ -235,6 +248,120 @@ async function bootstrapProjectsIndexFromLegacySources(config, projectDb, userId
|
|
|
235
248
|
return seededCount;
|
|
236
249
|
}
|
|
237
250
|
|
|
251
|
+
function collectCodexProjectCandidates(sessionsByProject = new Map()) {
|
|
252
|
+
const candidatesByProject = new Map();
|
|
253
|
+
|
|
254
|
+
for (const sessions of sessionsByProject.values()) {
|
|
255
|
+
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const projectPath = sessions.find((session) => typeof session?.cwd === 'string' && session.cwd.trim())?.cwd?.trim();
|
|
260
|
+
if (!projectPath) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const projectName = encodeProjectPath(projectPath);
|
|
265
|
+
if (!projectName) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!candidatesByProject.has(projectName)) {
|
|
270
|
+
candidatesByProject.set(projectName, {
|
|
271
|
+
projectName,
|
|
272
|
+
projectPath,
|
|
273
|
+
sessions: [],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const candidate = candidatesByProject.get(projectName);
|
|
278
|
+
const sessionIds = new Set(candidate.sessions.map((session) => session.id));
|
|
279
|
+
|
|
280
|
+
for (const session of sessions) {
|
|
281
|
+
if (!session?.id || sessionIds.has(session.id)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
candidate.sessions.push(session);
|
|
286
|
+
sessionIds.add(session.id);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return Array.from(candidatesByProject.values());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const CODEX_SYNC_COOLDOWN_MS = 30_000;
|
|
294
|
+
let lastCodexSyncTimestamp = 0;
|
|
295
|
+
|
|
296
|
+
async function syncDiscoveredProjectsFromCodexSessions(config, projectDb, userId = null, visibleWorkspaceRoots = []) {
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
if (now - lastCodexSyncTimestamp < CODEX_SYNC_COOLDOWN_MS) {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { sessionDb } = await import('./database/db.js');
|
|
303
|
+
const discoveredSessions = await buildCodexSessionsIndex();
|
|
304
|
+
const candidates = collectCodexProjectCandidates(discoveredSessions);
|
|
305
|
+
let syncedProjects = 0;
|
|
306
|
+
|
|
307
|
+
for (const candidate of candidates) {
|
|
308
|
+
const { projectName, projectPath, sessions } = candidate;
|
|
309
|
+
|
|
310
|
+
if (!projectPath || !await pathExists(projectPath)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (visibleWorkspaceRoots.length > 0 && !await isPathWithinWorkspaceRoots(projectPath, visibleWorkspaceRoots)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const projectInfo = config[projectName];
|
|
319
|
+
const existing = projectDb.getProjectById(projectName);
|
|
320
|
+
if (isProjectSuppressed(projectName, config, projectInfo) || isProjectTrashed(projectInfo, existing)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const ownerUserId = existing?.user_id ?? getProjectOwnerUserId(projectInfo, existing) ?? userId ?? null;
|
|
325
|
+
const metadata = { ...(existing?.metadata || {}) };
|
|
326
|
+
|
|
327
|
+
if (projectInfo?.trash?.trashedAt) {
|
|
328
|
+
metadata.trash = {
|
|
329
|
+
...projectInfo.trash,
|
|
330
|
+
ownerUserId: projectInfo.trash.ownerUserId ?? ownerUserId,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
projectDb.upsertProject(
|
|
335
|
+
projectName,
|
|
336
|
+
ownerUserId,
|
|
337
|
+
existing?.display_name || projectInfo?.displayName || null,
|
|
338
|
+
projectPath,
|
|
339
|
+
existing?.is_starred || 0,
|
|
340
|
+
existing?.last_accessed || null,
|
|
341
|
+
Object.keys(metadata).length > 0 ? metadata : null,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
for (const session of sessions) {
|
|
345
|
+
sessionDb.upsertSessionFromSource(session.id, projectName, 'codex', {
|
|
346
|
+
displayName: session.summary || session.name || 'Codex Session',
|
|
347
|
+
lastActivity: session.lastActivity || new Date(),
|
|
348
|
+
messageCount: session.messageCount || 0,
|
|
349
|
+
createdAt: session.createdAt || session.lastActivity || new Date(),
|
|
350
|
+
metadata: {
|
|
351
|
+
projectPath,
|
|
352
|
+
sessionMode: normalizeSessionMode(session.mode),
|
|
353
|
+
indexState: 'synced',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
syncedProjects += 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
lastCodexSyncTimestamp = Date.now();
|
|
362
|
+
return syncedProjects;
|
|
363
|
+
}
|
|
364
|
+
|
|
238
365
|
function buildTrashEntry(projectName, projectInfo = null, dbEntry = null) {
|
|
239
366
|
const trashMeta = dbEntry?.metadata?.trash || projectInfo?.trash;
|
|
240
367
|
if (!trashMeta?.trashedAt) {
|
|
@@ -914,6 +1041,20 @@ function mapIndexedSessionToProjectSession(session, provider) {
|
|
|
914
1041
|
};
|
|
915
1042
|
}
|
|
916
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
|
+
|
|
917
1058
|
return {
|
|
918
1059
|
id: session.id,
|
|
919
1060
|
summary: baseName || 'New Session',
|
|
@@ -936,6 +1077,8 @@ function getSessionPlaceholderName(provider) {
|
|
|
936
1077
|
return 'Gemini Session';
|
|
937
1078
|
case 'openrouter':
|
|
938
1079
|
return 'OpenRouter Session';
|
|
1080
|
+
case 'nano':
|
|
1081
|
+
return 'Nano Claude Code Session';
|
|
939
1082
|
default:
|
|
940
1083
|
return 'New Session';
|
|
941
1084
|
}
|
|
@@ -1165,18 +1308,22 @@ async function getProjects(userId, progressCallback = null) {
|
|
|
1165
1308
|
let totalProjects = 0;
|
|
1166
1309
|
let processedProjects = 0;
|
|
1167
1310
|
|
|
1168
|
-
|
|
1169
|
-
if
|
|
1170
|
-
|
|
1311
|
+
// Sync from legacy sources so CLI-created projects appear in the web UI.
|
|
1312
|
+
// Only re-scan if the last bootstrap was more than 60 seconds ago to avoid
|
|
1313
|
+
// O(N) filesystem walks on every getProjects() call. See #86.
|
|
1314
|
+
const now = Date.now();
|
|
1315
|
+
const userKey = userId || '__anonymous__';
|
|
1316
|
+
if (now - (_lastBootstrapByUser.get(userKey) || 0) > BOOTSTRAP_STALENESS_MS) {
|
|
1317
|
+
await bootstrapProjectsIndexFromLegacySources(
|
|
1171
1318
|
config,
|
|
1172
1319
|
projectDb,
|
|
1173
1320
|
userId || null,
|
|
1174
1321
|
visibleWorkspaceRoots,
|
|
1175
1322
|
);
|
|
1176
|
-
|
|
1177
|
-
dbProjects = projectDb.getAllProjects(userId || null);
|
|
1178
|
-
}
|
|
1323
|
+
_lastBootstrapByUser.set(userKey, now);
|
|
1179
1324
|
}
|
|
1325
|
+
await syncDiscoveredProjectsFromCodexSessions(config, projectDb, userId || null, visibleWorkspaceRoots);
|
|
1326
|
+
const dbProjects = projectDb.getAllProjects(userId || null);
|
|
1180
1327
|
|
|
1181
1328
|
try {
|
|
1182
1329
|
const visibleProjects = [];
|
|
@@ -1254,6 +1401,7 @@ async function getProjects(userId, progressCallback = null) {
|
|
|
1254
1401
|
const geminiSessions = projectSessions.filter((session) => session.provider === 'gemini');
|
|
1255
1402
|
const openrouterSessions = projectSessions.filter((session) => session.provider === 'openrouter');
|
|
1256
1403
|
const localSessions = projectSessions.filter((session) => session.provider === 'local');
|
|
1404
|
+
const nanoSessions = projectSessions.filter((session) => session.provider === 'nano');
|
|
1257
1405
|
|
|
1258
1406
|
project.sessions = claudeSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'claude'));
|
|
1259
1407
|
project.sessionMeta = {
|
|
@@ -1265,6 +1413,7 @@ async function getProjects(userId, progressCallback = null) {
|
|
|
1265
1413
|
project.geminiSessions = geminiSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'gemini'));
|
|
1266
1414
|
project.openrouterSessions = openrouterSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'openrouter'));
|
|
1267
1415
|
project.localSessions = localSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'local'));
|
|
1416
|
+
project.nanoSessions = nanoSessions.slice(0, 5).map((session) => mapIndexedSessionToProjectSession(session, 'nano'));
|
|
1268
1417
|
|
|
1269
1418
|
const taskmasterResult = await detectTaskMasterFolder(actualProjectDir).catch(() => null);
|
|
1270
1419
|
|
|
@@ -1749,6 +1898,119 @@ async function parseAgentTools(filePath) {
|
|
|
1749
1898
|
return tools;
|
|
1750
1899
|
}
|
|
1751
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
|
+
|
|
1752
2014
|
// Get messages for a specific session with pagination support
|
|
1753
2015
|
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0, provider = 'claude', userId = null) {
|
|
1754
2016
|
console.log(`[DEBUG] getSessionMessages - project: ${projectName}, session: ${sessionId}, provider: ${provider}`);
|
|
@@ -1987,6 +2249,36 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
|
|
|
1987
2249
|
}
|
|
1988
2250
|
}
|
|
1989
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
|
+
|
|
1990
2282
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1991
2283
|
|
|
1992
2284
|
try {
|
|
@@ -2206,6 +2498,31 @@ async function deleteSession(projectName, sessionId, provider = 'claude') {
|
|
|
2206
2498
|
throw new Error(`Local GPU session ${sessionId} not found in file system or index`);
|
|
2207
2499
|
}
|
|
2208
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
|
+
|
|
2209
2526
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
2210
2527
|
|
|
2211
2528
|
try {
|
|
@@ -2831,38 +3148,63 @@ async function addProjectManually(projectPath, displayName = null, userId = null
|
|
|
2831
3148
|
|
|
2832
3149
|
const projectName = encodeProjectPath(absolutePath);
|
|
2833
3150
|
|
|
2834
|
-
// Check for existing project with the same path (
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3151
|
+
// Check for existing project with the same path (any user or matching user).
|
|
3152
|
+
// A single lookup handles both same-user exact match and cross-user dedup.
|
|
3153
|
+
const existing = projectDb.getProjectByPath(absolutePath, userId);
|
|
3154
|
+
|
|
3155
|
+
if (existing) {
|
|
3156
|
+
// Migrate legacy encoded IDs if needed
|
|
3157
|
+
if (existing.id !== projectName) {
|
|
3158
|
+
projectDb.migrateProjectIdentity(existing.id, projectName, absolutePath);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// If the existing record already belongs to this user, mark as manually
|
|
3162
|
+
// added and return without creating a duplicate.
|
|
3163
|
+
if (existing.user_id === userId || existing.user_id == null) {
|
|
3164
|
+
// Preserve existing values while marking as manually added
|
|
3165
|
+
const mergedMetadata = { ...(existing.metadata || {}), manuallyAdded: true };
|
|
3166
|
+
projectDb.upsertProject(
|
|
3167
|
+
projectName, userId,
|
|
3168
|
+
displayName || existing.display_name,
|
|
3169
|
+
absolutePath,
|
|
3170
|
+
existing.is_starred || 0,
|
|
3171
|
+
existing.last_accessed || new Date().toISOString(),
|
|
3172
|
+
mergedMetadata,
|
|
3173
|
+
);
|
|
3174
|
+
|
|
3175
|
+
return {
|
|
3176
|
+
name: projectName,
|
|
3177
|
+
path: absolutePath,
|
|
3178
|
+
fullPath: absolutePath,
|
|
3179
|
+
displayName: displayName || existing.display_name || await generateDisplayName(projectName, absolutePath),
|
|
3180
|
+
isManuallyAdded: true,
|
|
3181
|
+
createdAt: existing.created_at,
|
|
3182
|
+
sessions: [],
|
|
3183
|
+
cursorSessions: [],
|
|
3184
|
+
alreadyExists: true,
|
|
3185
|
+
};
|
|
2840
3186
|
}
|
|
2841
|
-
return {
|
|
2842
|
-
name: projectName,
|
|
2843
|
-
path: absolutePath,
|
|
2844
|
-
fullPath: absolutePath,
|
|
2845
|
-
displayName: displayName || existingByPath.display_name || await generateDisplayName(projectName, absolutePath),
|
|
2846
|
-
isManuallyAdded: Boolean(existingByPath.metadata?.manuallyAdded),
|
|
2847
|
-
createdAt: existingByPath.created_at,
|
|
2848
|
-
sessions: [],
|
|
2849
|
-
cursorSessions: [],
|
|
2850
|
-
alreadyExists: true,
|
|
2851
|
-
};
|
|
2852
3187
|
}
|
|
2853
3188
|
|
|
2854
|
-
|
|
3189
|
+
const effectiveId = existing ? existing.id : projectName;
|
|
3190
|
+
|
|
3191
|
+
// Preserve existing values so the upsert doesn't silently clear them
|
|
3192
|
+
const preservedStarred = existing?.is_starred || 0;
|
|
3193
|
+
const mergedMetadata = { ...(existing?.metadata || {}), manuallyAdded: true };
|
|
3194
|
+
const preservedLastAccessed = existing?.last_accessed || new Date().toISOString();
|
|
3195
|
+
|
|
3196
|
+
projectDb.upsertProject(effectiveId, userId, displayName, absolutePath, preservedStarred, preservedLastAccessed, mergedMetadata);
|
|
2855
3197
|
|
|
2856
3198
|
await mutateProjectConfig((config) => {
|
|
2857
|
-
config[
|
|
2858
|
-
...(config[
|
|
3199
|
+
config[effectiveId] = {
|
|
3200
|
+
...(config[effectiveId] || {}),
|
|
2859
3201
|
manuallyAdded: true,
|
|
2860
3202
|
originalPath: absolutePath,
|
|
2861
|
-
ownerUserId: config[
|
|
3203
|
+
ownerUserId: config[effectiveId]?.ownerUserId ?? userId ?? null,
|
|
2862
3204
|
};
|
|
2863
3205
|
|
|
2864
3206
|
if (displayName) {
|
|
2865
|
-
config[
|
|
3207
|
+
config[effectiveId].displayName = displayName;
|
|
2866
3208
|
}
|
|
2867
3209
|
});
|
|
2868
3210
|
|
|
@@ -2875,10 +3217,10 @@ async function addProjectManually(projectPath, displayName = null, userId = null
|
|
|
2875
3217
|
} catch (_) {}
|
|
2876
3218
|
|
|
2877
3219
|
return {
|
|
2878
|
-
name:
|
|
3220
|
+
name: effectiveId,
|
|
2879
3221
|
path: absolutePath,
|
|
2880
3222
|
fullPath: absolutePath,
|
|
2881
|
-
displayName: displayName || await generateDisplayName(
|
|
3223
|
+
displayName: displayName || await generateDisplayName(effectiveId, absolutePath),
|
|
2882
3224
|
isManuallyAdded: true,
|
|
2883
3225
|
createdAt: dirCreatedAt,
|
|
2884
3226
|
sessions: [],
|
|
@@ -3538,27 +3880,46 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
3538
3880
|
|
|
3539
3881
|
const messages = [];
|
|
3540
3882
|
let tokenUsage = null;
|
|
3883
|
+
const toolUseIndexByCallId = new Map();
|
|
3884
|
+
const toolResultIndexByCallId = new Map();
|
|
3541
3885
|
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
3542
3886
|
const rl = readline.createInterface({
|
|
3543
3887
|
input: fileStream,
|
|
3544
3888
|
crlfDelay: Infinity
|
|
3545
3889
|
});
|
|
3546
3890
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
if (
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
.
|
|
3561
|
-
|
|
3891
|
+
const upsertToolUse = (nextMessage) => {
|
|
3892
|
+
const callId = nextMessage?.toolCallId;
|
|
3893
|
+
if (callId && toolUseIndexByCallId.has(callId)) {
|
|
3894
|
+
const existingIndex = toolUseIndexByCallId.get(callId);
|
|
3895
|
+
messages[existingIndex] = {
|
|
3896
|
+
...messages[existingIndex],
|
|
3897
|
+
...nextMessage,
|
|
3898
|
+
};
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const nextIndex = messages.push(nextMessage) - 1;
|
|
3903
|
+
if (callId) {
|
|
3904
|
+
toolUseIndexByCallId.set(callId, nextIndex);
|
|
3905
|
+
}
|
|
3906
|
+
};
|
|
3907
|
+
|
|
3908
|
+
const upsertToolResult = (nextMessage) => {
|
|
3909
|
+
const callId = nextMessage?.toolCallId;
|
|
3910
|
+
if (callId && toolResultIndexByCallId.has(callId)) {
|
|
3911
|
+
const existingIndex = toolResultIndexByCallId.get(callId);
|
|
3912
|
+
messages[existingIndex] = {
|
|
3913
|
+
...messages[existingIndex],
|
|
3914
|
+
...nextMessage,
|
|
3915
|
+
};
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
const nextIndex = messages.push(nextMessage) - 1;
|
|
3920
|
+
if (callId) {
|
|
3921
|
+
toolResultIndexByCallId.set(callId, nextIndex);
|
|
3922
|
+
}
|
|
3562
3923
|
};
|
|
3563
3924
|
|
|
3564
3925
|
for await (const line of rl) {
|
|
@@ -3581,7 +3942,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
3581
3942
|
if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
3582
3943
|
const content = entry.payload.content;
|
|
3583
3944
|
const role = entry.payload.role || 'assistant';
|
|
3584
|
-
const textContent =
|
|
3945
|
+
const textContent = extractTextContent(content);
|
|
3585
3946
|
|
|
3586
3947
|
// Skip system context messages (environment_context)
|
|
3587
3948
|
if (textContent?.includes('<environment_context>')) {
|
|
@@ -3609,91 +3970,115 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
3609
3970
|
// Skip Codex reasoning items - they are brief status notes, not useful to display
|
|
3610
3971
|
|
|
3611
3972
|
if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
toolInput = JSON.stringify({ command: args.command });
|
|
3621
|
-
} catch (e) {
|
|
3622
|
-
// Keep original if parsing fails
|
|
3623
|
-
}
|
|
3973
|
+
const normalized = normalizeCodexFunctionCall(entry.payload.name, entry.payload.arguments);
|
|
3974
|
+
if (!normalized.skip) {
|
|
3975
|
+
upsertToolUse(buildToolUseMessage(
|
|
3976
|
+
entry.timestamp,
|
|
3977
|
+
normalized.toolName,
|
|
3978
|
+
normalized.toolInput,
|
|
3979
|
+
entry.payload.call_id,
|
|
3980
|
+
));
|
|
3624
3981
|
}
|
|
3625
|
-
|
|
3626
|
-
messages.push({
|
|
3627
|
-
type: 'tool_use',
|
|
3628
|
-
timestamp: entry.timestamp,
|
|
3629
|
-
toolName: toolName,
|
|
3630
|
-
toolInput: toolInput,
|
|
3631
|
-
toolCallId: entry.payload.call_id
|
|
3632
|
-
});
|
|
3633
3982
|
}
|
|
3634
3983
|
|
|
3635
3984
|
if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
|
|
3985
|
+
upsertToolResult(buildToolResultMessage(
|
|
3986
|
+
entry.timestamp,
|
|
3987
|
+
entry.payload.call_id,
|
|
3988
|
+
entry.payload.output,
|
|
3989
|
+
));
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
|
|
3993
|
+
const normalized = normalizeCodexCustomToolCall(entry.payload.name, entry.payload.input);
|
|
3994
|
+
upsertToolUse(buildToolUseMessage(
|
|
3995
|
+
entry.timestamp,
|
|
3996
|
+
normalized.toolName,
|
|
3997
|
+
normalized.toolInput,
|
|
3998
|
+
entry.payload.call_id,
|
|
3999
|
+
));
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
|
|
4003
|
+
upsertToolResult(buildToolResultMessage(
|
|
4004
|
+
entry.timestamp,
|
|
4005
|
+
entry.payload.call_id,
|
|
4006
|
+
entry.payload.output || '',
|
|
4007
|
+
));
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
// web_search_call events are self-contained display items — no separate result
|
|
4011
|
+
// event and no call_id in the payload. We push directly instead of upsertToolUse
|
|
4012
|
+
// because there is no toolCallId to match against a future tool_result.
|
|
4013
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'web_search_call') {
|
|
4014
|
+
const normalized = normalizeWebSearchCall(entry.payload);
|
|
3636
4015
|
messages.push({
|
|
3637
|
-
type: '
|
|
4016
|
+
type: 'tool_use',
|
|
3638
4017
|
timestamp: entry.timestamp,
|
|
3639
|
-
|
|
3640
|
-
|
|
4018
|
+
toolName: normalized.toolName,
|
|
4019
|
+
toolInput: normalized.toolInput,
|
|
3641
4020
|
});
|
|
3642
4021
|
}
|
|
3643
4022
|
|
|
3644
|
-
if (entry.type === '
|
|
3645
|
-
const
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
// Extract old and new content from patch
|
|
3654
|
-
const lines = input.split('\n');
|
|
3655
|
-
const oldLines = [];
|
|
3656
|
-
const newLines = [];
|
|
3657
|
-
|
|
3658
|
-
for (const line of lines) {
|
|
3659
|
-
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
3660
|
-
oldLines.push(line.substring(1));
|
|
3661
|
-
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
3662
|
-
newLines.push(line.substring(1));
|
|
3663
|
-
}
|
|
4023
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'exec_command_end' && entry.payload?.call_id) {
|
|
4024
|
+
const toolUseIndex = toolUseIndexByCallId.get(entry.payload.call_id);
|
|
4025
|
+
const existingToolUse = toolUseIndex !== undefined ? messages[toolUseIndex] : null;
|
|
4026
|
+
let fallbackInput = null;
|
|
4027
|
+
if (existingToolUse?.toolInput) {
|
|
4028
|
+
try {
|
|
4029
|
+
fallbackInput = JSON.parse(existingToolUse.toolInput);
|
|
4030
|
+
} catch {
|
|
4031
|
+
fallbackInput = null;
|
|
3664
4032
|
}
|
|
4033
|
+
}
|
|
4034
|
+
const normalized = normalizeExecCommandEvent(entry.payload, fallbackInput);
|
|
4035
|
+
|
|
4036
|
+
upsertToolUse(buildToolUseMessage(
|
|
4037
|
+
entry.timestamp,
|
|
4038
|
+
normalized.toolName,
|
|
4039
|
+
normalized.toolInput,
|
|
4040
|
+
entry.payload.call_id,
|
|
4041
|
+
));
|
|
4042
|
+
upsertToolResult(buildToolResultMessage(
|
|
4043
|
+
entry.timestamp,
|
|
4044
|
+
entry.payload.call_id,
|
|
4045
|
+
normalized.toolResult?.content || '',
|
|
4046
|
+
{
|
|
4047
|
+
isError: normalized.toolResult?.isError === true,
|
|
4048
|
+
toolUseResult: normalized.toolResult?.toolUseResult || null,
|
|
4049
|
+
},
|
|
4050
|
+
));
|
|
4051
|
+
}
|
|
3665
4052
|
|
|
4053
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'patch_apply_end' && entry.payload?.call_id) {
|
|
4054
|
+
const normalized = normalizePatchApplyEvent(entry.payload);
|
|
4055
|
+
upsertToolResult(buildToolResultMessage(
|
|
4056
|
+
entry.timestamp,
|
|
4057
|
+
entry.payload.call_id,
|
|
4058
|
+
normalized.toolResult.content,
|
|
4059
|
+
{
|
|
4060
|
+
isError: normalized.toolResult.isError === true,
|
|
4061
|
+
toolUseResult: normalized.toolResult.toolUseResult,
|
|
4062
|
+
},
|
|
4063
|
+
));
|
|
4064
|
+
|
|
4065
|
+
if (normalized.fileChangesToolInput) {
|
|
3666
4066
|
messages.push({
|
|
3667
4067
|
type: 'tool_use',
|
|
3668
4068
|
timestamp: entry.timestamp,
|
|
3669
|
-
toolName: '
|
|
3670
|
-
toolInput:
|
|
3671
|
-
|
|
3672
|
-
old_string: oldLines.join('\n'),
|
|
3673
|
-
new_string: newLines.join('\n')
|
|
3674
|
-
}),
|
|
3675
|
-
toolCallId: entry.payload.call_id
|
|
4069
|
+
toolName: 'FileChanges',
|
|
4070
|
+
toolInput: normalized.fileChangesToolInput,
|
|
4071
|
+
toolCallId: `${entry.payload.call_id}:files`,
|
|
3676
4072
|
});
|
|
3677
|
-
} else {
|
|
3678
4073
|
messages.push({
|
|
3679
|
-
type: '
|
|
4074
|
+
type: 'tool_result',
|
|
3680
4075
|
timestamp: entry.timestamp,
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
toolCallId: entry.payload.call_id
|
|
4076
|
+
toolCallId: `${entry.payload.call_id}:files`,
|
|
4077
|
+
output: entry.payload.status || 'completed',
|
|
3684
4078
|
});
|
|
3685
4079
|
}
|
|
3686
4080
|
}
|
|
3687
4081
|
|
|
3688
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
|
|
3689
|
-
messages.push({
|
|
3690
|
-
type: 'tool_result',
|
|
3691
|
-
timestamp: entry.timestamp,
|
|
3692
|
-
toolCallId: entry.payload.call_id,
|
|
3693
|
-
output: entry.payload.output || ''
|
|
3694
|
-
});
|
|
3695
|
-
}
|
|
3696
|
-
|
|
3697
4082
|
} catch (parseError) {
|
|
3698
4083
|
// Skip malformed lines
|
|
3699
4084
|
}
|
|
@@ -3845,6 +4230,26 @@ async function renameSession(projectName, sessionId, newSummary, provider = 'cla
|
|
|
3845
4230
|
console.error(`[Gemini] Failed to rename session ${sessionId}:`, e.message);
|
|
3846
4231
|
throw new Error(`Failed to rename Gemini session: ${e.message}`);
|
|
3847
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
|
+
}
|
|
3848
4253
|
}
|
|
3849
4254
|
// 2. Handle Cursor sessions (SQLite)
|
|
3850
4255
|
else if (provider === 'cursor') {
|
|
@@ -3945,6 +4350,7 @@ export {
|
|
|
3945
4350
|
getTrashedProjects,
|
|
3946
4351
|
getSessions,
|
|
3947
4352
|
getSessionMessages,
|
|
4353
|
+
collectCodexProjectCandidates,
|
|
3948
4354
|
parseJsonlSessions,
|
|
3949
4355
|
renameProject,
|
|
3950
4356
|
renameSession,
|