@claude-sessions/core 0.4.6 → 0.4.7-beta.0
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/index.d.ts +16 -5
- package/dist/index.js +310 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -66,14 +66,13 @@ var extractTitle = (input) => {
|
|
|
66
66
|
text = extractTextContent(input, { stripIdeTags: true });
|
|
67
67
|
}
|
|
68
68
|
if (!text) return "Untitled";
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
if (!cleaned) return "Untitled";
|
|
73
|
-
if (cleaned.includes("\n\n")) {
|
|
74
|
-
cleaned = cleaned.split("\n\n")[0];
|
|
69
|
+
let firstParagraph = text.trim();
|
|
70
|
+
if (firstParagraph.includes("\n\n")) {
|
|
71
|
+
firstParagraph = firstParagraph.split("\n\n")[0];
|
|
75
72
|
}
|
|
76
|
-
|
|
73
|
+
const { name, args } = parseCommandMessage(firstParagraph);
|
|
74
|
+
if (name) return args ? `${name} ${args}` : name;
|
|
75
|
+
return firstParagraph || "Untitled";
|
|
77
76
|
};
|
|
78
77
|
var isInvalidApiKeyMessage = (msg) => {
|
|
79
78
|
const text = extractTextContent(msg.message);
|
|
@@ -102,11 +101,12 @@ var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallb
|
|
|
102
101
|
return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
|
|
103
102
|
}
|
|
104
103
|
if (title && title !== "Untitled") {
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const firstParagraph = title.includes("\n\n") ? title.split("\n\n")[0] : title;
|
|
105
|
+
if (firstParagraph.includes("<command-name>")) {
|
|
106
|
+
const { name, args } = parseCommandMessage(firstParagraph);
|
|
107
107
|
if (name) return args ? `${name} ${args}` : name;
|
|
108
108
|
}
|
|
109
|
-
return
|
|
109
|
+
return firstParagraph;
|
|
110
110
|
}
|
|
111
111
|
return fallback;
|
|
112
112
|
};
|
|
@@ -152,27 +152,32 @@ var tryParseJsonLine = (line, lineNumber, filePath) => {
|
|
|
152
152
|
return JSON.parse(line);
|
|
153
153
|
} catch {
|
|
154
154
|
if (filePath) {
|
|
155
|
-
|
|
155
|
+
logger.warn(`Skipping invalid JSON at line ${lineNumber} in ${filePath}`);
|
|
156
156
|
}
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
|
-
var parseJsonlLines = (lines, filePath) => {
|
|
161
|
-
|
|
160
|
+
var parseJsonlLines = (lines, filePath, { strict = false } = {}) => {
|
|
161
|
+
const results = [];
|
|
162
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
162
163
|
try {
|
|
163
|
-
|
|
164
|
+
results.push(JSON.parse(lines[idx]));
|
|
164
165
|
} catch (e) {
|
|
165
166
|
const err = e;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
if (strict) {
|
|
168
|
+
throw new Error(`Failed to parse line ${idx + 1} in ${filePath}: ${err.message}`, {
|
|
169
|
+
cause: e
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
logger.warn(`Skipping malformed line ${idx + 1} in ${filePath}: ${err.message}`);
|
|
169
173
|
}
|
|
170
|
-
}
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
171
176
|
};
|
|
172
|
-
var readJsonlFile = (filePath) => Effect.gen(function* () {
|
|
177
|
+
var readJsonlFile = (filePath, options) => Effect.gen(function* () {
|
|
173
178
|
const content = yield* Effect.tryPromise(() => fs.readFile(filePath, "utf-8"));
|
|
174
179
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
175
|
-
return parseJsonlLines(lines, filePath);
|
|
180
|
+
return parseJsonlLines(lines, filePath, options);
|
|
176
181
|
});
|
|
177
182
|
var formatRelativeTime = (timestamp) => {
|
|
178
183
|
const date = typeof timestamp === "string" ? new Date(timestamp) : new Date(timestamp);
|
|
@@ -333,14 +338,14 @@ var resolvePathFromClaudeConfig = async (folderName, fileSystem = fsp) => {
|
|
|
333
338
|
};
|
|
334
339
|
var folderNameToPath = async (folderName) => {
|
|
335
340
|
const homeDir = os.homedir();
|
|
336
|
-
const realPath = getRealPathFromSession(folderName);
|
|
337
|
-
if (realPath) {
|
|
338
|
-
return toRelativePath(realPath, homeDir);
|
|
339
|
-
}
|
|
340
341
|
const configPath = await resolvePathFromClaudeConfig(folderName);
|
|
341
342
|
if (configPath) {
|
|
342
343
|
return toRelativePath(configPath, homeDir);
|
|
343
344
|
}
|
|
345
|
+
const realPath = getRealPathFromSession(folderName);
|
|
346
|
+
if (realPath) {
|
|
347
|
+
return toRelativePath(realPath, homeDir);
|
|
348
|
+
}
|
|
344
349
|
const absolutePath = folderNameToDisplayPath(folderName);
|
|
345
350
|
return toRelativePath(absolutePath, homeDir);
|
|
346
351
|
};
|
|
@@ -1081,7 +1086,7 @@ function deleteMessageWithChainRepair(messages, targetId, targetType) {
|
|
|
1081
1086
|
// src/session/crud.ts
|
|
1082
1087
|
var updateSessionSummary = (projectName, sessionId, newSummary) => Effect5.gen(function* () {
|
|
1083
1088
|
const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
1084
|
-
const messages = yield* readJsonlFile(filePath);
|
|
1089
|
+
const messages = yield* readJsonlFile(filePath, { strict: true });
|
|
1085
1090
|
const summaryIdx = messages.findIndex((m) => m.type === "summary");
|
|
1086
1091
|
if (summaryIdx >= 0) {
|
|
1087
1092
|
messages[summaryIdx] = { ...messages[summaryIdx], summary: newSummary };
|
|
@@ -1160,7 +1165,7 @@ var readSession = (projectName, sessionId) => Effect5.gen(function* () {
|
|
|
1160
1165
|
});
|
|
1161
1166
|
var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect5.gen(function* () {
|
|
1162
1167
|
const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
1163
|
-
const messages = yield* readJsonlFile(filePath);
|
|
1168
|
+
const messages = yield* readJsonlFile(filePath, { strict: true });
|
|
1164
1169
|
const result = deleteMessageWithChainRepair(messages, messageUuid, targetType);
|
|
1165
1170
|
if (!result.deleted) {
|
|
1166
1171
|
return { success: false, error: "Message not found" };
|
|
@@ -1171,7 +1176,7 @@ var deleteMessage = (projectName, sessionId, messageUuid, targetType) => Effect5
|
|
|
1171
1176
|
});
|
|
1172
1177
|
var restoreMessage = (projectName, sessionId, message, index) => Effect5.gen(function* () {
|
|
1173
1178
|
const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
1174
|
-
const messages = yield* readJsonlFile(filePath);
|
|
1179
|
+
const messages = yield* readJsonlFile(filePath, { strict: true });
|
|
1175
1180
|
const msgUuid = message.uuid ?? message.messageId;
|
|
1176
1181
|
if (!msgUuid) {
|
|
1177
1182
|
return { success: false, error: "Message has no uuid or messageId" };
|
|
@@ -1236,7 +1241,7 @@ var renameSession = (projectName, sessionId, newTitle) => Effect5.gen(function*
|
|
|
1236
1241
|
if (lines.length === 0) {
|
|
1237
1242
|
return { success: false, error: "Empty session" };
|
|
1238
1243
|
}
|
|
1239
|
-
const messages = parseJsonlLines(lines, filePath);
|
|
1244
|
+
const messages = parseJsonlLines(lines, filePath, { strict: true });
|
|
1240
1245
|
const customTitleIdx = messages.findIndex((m) => m.type === "custom-title");
|
|
1241
1246
|
if (customTitleIdx >= 0) {
|
|
1242
1247
|
messages.splice(customTitleIdx, 1);
|
|
@@ -1286,7 +1291,7 @@ var moveSession = (sourceProject, sessionId, targetProject) => Effect5.gen(funct
|
|
|
1286
1291
|
var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(function* () {
|
|
1287
1292
|
const projectPath = path5.join(getSessionsDir(), projectName);
|
|
1288
1293
|
const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
|
|
1289
|
-
const allMessages = yield* readJsonlFile(filePath);
|
|
1294
|
+
const allMessages = yield* readJsonlFile(filePath, { strict: true });
|
|
1290
1295
|
const splitIndex = allMessages.findIndex((m) => m.uuid === splitAtMessageUuid);
|
|
1291
1296
|
if (splitIndex === -1) {
|
|
1292
1297
|
return { success: false, error: "Message not found" };
|
|
@@ -1333,7 +1338,9 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(f
|
|
|
1333
1338
|
const agentContent = yield* Effect5.tryPromise(() => fs6.readFile(agentPath, "utf-8"));
|
|
1334
1339
|
const agentLines = agentContent.trim().split("\n").filter(Boolean);
|
|
1335
1340
|
if (agentLines.length === 0) continue;
|
|
1336
|
-
const agentMessages = parseJsonlLines(agentLines, agentPath
|
|
1341
|
+
const agentMessages = parseJsonlLines(agentLines, agentPath, {
|
|
1342
|
+
strict: true
|
|
1343
|
+
});
|
|
1337
1344
|
const firstAgentMsg = agentMessages[0];
|
|
1338
1345
|
if (firstAgentMsg.sessionId === sessionId) {
|
|
1339
1346
|
const agentId = agentFile.replace("agent-", "").replace(".jsonl", "");
|
|
@@ -1359,7 +1366,7 @@ var splitSession = (projectName, sessionId, splitAtMessageUuid) => Effect5.gen(f
|
|
|
1359
1366
|
});
|
|
1360
1367
|
var repairChain = (projectName, sessionId) => Effect5.gen(function* () {
|
|
1361
1368
|
const filePath = path5.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
1362
|
-
const messages = yield* readJsonlFile(filePath);
|
|
1369
|
+
const messages = yield* readJsonlFile(filePath, { strict: true });
|
|
1363
1370
|
const beforeResult = validateChain(messages);
|
|
1364
1371
|
const repairCount = autoRepairChain(messages);
|
|
1365
1372
|
if (repairCount > 0) {
|
|
@@ -1376,8 +1383,75 @@ var repairChain = (projectName, sessionId) => Effect5.gen(function* () {
|
|
|
1376
1383
|
|
|
1377
1384
|
// src/session/tree.ts
|
|
1378
1385
|
import { Effect as Effect6 } from "effect";
|
|
1386
|
+
import * as fs8 from "fs/promises";
|
|
1387
|
+
import * as path7 from "path";
|
|
1388
|
+
|
|
1389
|
+
// src/session/cache.ts
|
|
1379
1390
|
import * as fs7 from "fs/promises";
|
|
1380
1391
|
import * as path6 from "path";
|
|
1392
|
+
var log2 = createLogger("cache");
|
|
1393
|
+
var CACHE_VERSION = 1;
|
|
1394
|
+
var CACHE_FILENAME = ".tree-cache.json";
|
|
1395
|
+
var getCachePath = (projectName) => path6.join(getSessionsDir(), projectName, CACHE_FILENAME);
|
|
1396
|
+
var loadTreeCache = async (projectName) => {
|
|
1397
|
+
const cachePath = getCachePath(projectName);
|
|
1398
|
+
try {
|
|
1399
|
+
const raw = await fs7.readFile(cachePath, "utf-8");
|
|
1400
|
+
const parsed = JSON.parse(raw);
|
|
1401
|
+
if (parsed.version !== CACHE_VERSION) {
|
|
1402
|
+
log2.debug(`cache version mismatch (${parsed.version} !== ${CACHE_VERSION}), ignoring`);
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
return parsed;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
var writeTreeCache = async (projectName, cache) => {
|
|
1411
|
+
const cachePath = getCachePath(projectName);
|
|
1412
|
+
const tmpPath = cachePath + ".tmp";
|
|
1413
|
+
try {
|
|
1414
|
+
await fs7.writeFile(tmpPath, JSON.stringify(cache), "utf-8");
|
|
1415
|
+
await fs7.rename(tmpPath, cachePath);
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
log2.debug(`failed to write cache: ${e}`);
|
|
1418
|
+
try {
|
|
1419
|
+
await fs7.unlink(tmpPath);
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
var validateCache = (cache, sessionFileIds, currentMtimes) => {
|
|
1425
|
+
const cachedIds = new Set(Object.keys(cache.sessions));
|
|
1426
|
+
const currentIds = new Set(sessionFileIds);
|
|
1427
|
+
const changedSessionIds = [];
|
|
1428
|
+
const unchangedSessionIds = [];
|
|
1429
|
+
const newSessionIds = [];
|
|
1430
|
+
const deletedSessionIds = [];
|
|
1431
|
+
for (const id of currentIds) {
|
|
1432
|
+
if (!cachedIds.has(id)) {
|
|
1433
|
+
newSessionIds.push(id);
|
|
1434
|
+
} else {
|
|
1435
|
+
const cachedMtime = cache.sessions[id].fileMtime;
|
|
1436
|
+
const currentMtime = currentMtimes.get(id) ?? 0;
|
|
1437
|
+
if (Math.abs(cachedMtime - currentMtime) <= 1) {
|
|
1438
|
+
unchangedSessionIds.push(id);
|
|
1439
|
+
} else {
|
|
1440
|
+
changedSessionIds.push(id);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
for (const id of cachedIds) {
|
|
1445
|
+
if (!currentIds.has(id)) {
|
|
1446
|
+
deletedSessionIds.push(id);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const isFullHit = changedSessionIds.length === 0 && newSessionIds.length === 0 && deletedSessionIds.length === 0;
|
|
1450
|
+
return { isFullHit, changedSessionIds, unchangedSessionIds, deletedSessionIds, newSessionIds };
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
// src/session/tree.ts
|
|
1454
|
+
var log3 = createLogger("tree");
|
|
1381
1455
|
var sortSessions = (sessions, sort) => {
|
|
1382
1456
|
return sessions.sort((a, b) => {
|
|
1383
1457
|
let comparison = 0;
|
|
@@ -1417,9 +1491,9 @@ var sortSessions = (sessions, sort) => {
|
|
|
1417
1491
|
});
|
|
1418
1492
|
};
|
|
1419
1493
|
var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSession, fileMtime) => Effect6.gen(function* () {
|
|
1420
|
-
const projectPath =
|
|
1421
|
-
const filePath =
|
|
1422
|
-
const content = yield* Effect6.tryPromise(() =>
|
|
1494
|
+
const projectPath = path7.join(getSessionsDir(), projectName);
|
|
1495
|
+
const filePath = path7.join(projectPath, `${sessionId}.jsonl`);
|
|
1496
|
+
const content = yield* Effect6.tryPromise(() => fs8.readFile(filePath, "utf-8"));
|
|
1423
1497
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1424
1498
|
const messages = parseJsonlLines(lines, filePath);
|
|
1425
1499
|
let summaries;
|
|
@@ -1437,12 +1511,12 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
|
|
|
1437
1511
|
sessionUuids.add(msg.uuid);
|
|
1438
1512
|
}
|
|
1439
1513
|
}
|
|
1440
|
-
const projectFiles = yield* Effect6.tryPromise(() =>
|
|
1514
|
+
const projectFiles = yield* Effect6.tryPromise(() => fs8.readdir(projectPath));
|
|
1441
1515
|
const allJsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
|
|
1442
1516
|
for (const file of allJsonlFiles) {
|
|
1443
1517
|
try {
|
|
1444
|
-
const otherFilePath =
|
|
1445
|
-
const otherContent = yield* Effect6.tryPromise(() =>
|
|
1518
|
+
const otherFilePath = path7.join(projectPath, file);
|
|
1519
|
+
const otherContent = yield* Effect6.tryPromise(() => fs8.readFile(otherFilePath, "utf-8"));
|
|
1446
1520
|
const otherLines = otherContent.trim().split("\n").filter(Boolean);
|
|
1447
1521
|
for (let i = 0; i < otherLines.length; i++) {
|
|
1448
1522
|
const msg = tryParseJsonLine(otherLines[i], i + 1, otherFilePath);
|
|
@@ -1486,9 +1560,9 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
|
|
|
1486
1560
|
const linkedAgentIds = yield* findLinkedAgents(projectName, sessionId);
|
|
1487
1561
|
const agents = [];
|
|
1488
1562
|
for (const agentId of linkedAgentIds) {
|
|
1489
|
-
const agentPath =
|
|
1563
|
+
const agentPath = path7.join(projectPath, `${agentId}.jsonl`);
|
|
1490
1564
|
try {
|
|
1491
|
-
const agentContent = yield* Effect6.tryPromise(() =>
|
|
1565
|
+
const agentContent = yield* Effect6.tryPromise(() => fs8.readFile(agentPath, "utf-8"));
|
|
1492
1566
|
const agentLines = agentContent.trim().split("\n").filter(Boolean);
|
|
1493
1567
|
const agentMsgs = agentLines.map((l) => JSON.parse(l));
|
|
1494
1568
|
const agentUserAssistant = agentMsgs.filter(
|
|
@@ -1536,39 +1610,16 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
|
|
|
1536
1610
|
});
|
|
1537
1611
|
var loadSessionTreeData = (projectName, sessionId) => loadSessionTreeDataInternal(projectName, sessionId, void 0);
|
|
1538
1612
|
var DEFAULT_SORT = { field: "summary", order: "desc" };
|
|
1539
|
-
var
|
|
1540
|
-
const project = (yield* listProjects).find((p) => p.name === projectName);
|
|
1541
|
-
if (!project) {
|
|
1542
|
-
return null;
|
|
1543
|
-
}
|
|
1544
|
-
const sort = sortOptions ?? DEFAULT_SORT;
|
|
1545
|
-
const projectPath = path6.join(getSessionsDir(), projectName);
|
|
1546
|
-
const files = yield* Effect6.tryPromise(() => fs7.readdir(projectPath));
|
|
1547
|
-
const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
1548
|
-
const fileMtimes = /* @__PURE__ */ new Map();
|
|
1549
|
-
yield* Effect6.all(
|
|
1550
|
-
sessionFiles.map(
|
|
1551
|
-
(file) => Effect6.gen(function* () {
|
|
1552
|
-
const filePath = path6.join(projectPath, file);
|
|
1553
|
-
try {
|
|
1554
|
-
const stat4 = yield* Effect6.tryPromise(() => fs7.stat(filePath));
|
|
1555
|
-
fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
|
|
1556
|
-
} catch {
|
|
1557
|
-
}
|
|
1558
|
-
})
|
|
1559
|
-
),
|
|
1560
|
-
{ concurrency: 20 }
|
|
1561
|
-
);
|
|
1613
|
+
var buildPhase1 = (projectPath, allJsonlFiles) => Effect6.gen(function* () {
|
|
1562
1614
|
const globalUuidMap = /* @__PURE__ */ new Map();
|
|
1563
1615
|
const allSummaries = [];
|
|
1564
|
-
const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
1565
1616
|
yield* Effect6.all(
|
|
1566
1617
|
allJsonlFiles.map(
|
|
1567
1618
|
(file) => Effect6.gen(function* () {
|
|
1568
|
-
const filePath =
|
|
1619
|
+
const filePath = path7.join(projectPath, file);
|
|
1569
1620
|
const fileSessionId = file.replace(".jsonl", "");
|
|
1570
1621
|
try {
|
|
1571
|
-
const content = yield* Effect6.tryPromise(() =>
|
|
1622
|
+
const content = yield* Effect6.tryPromise(() => fs8.readFile(filePath, "utf-8"));
|
|
1572
1623
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1573
1624
|
for (let i = 0; i < lines.length; i++) {
|
|
1574
1625
|
const msg = tryParseJsonLine(lines[i], i + 1, filePath);
|
|
@@ -1600,6 +1651,9 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
|
|
|
1600
1651
|
),
|
|
1601
1652
|
{ concurrency: 20 }
|
|
1602
1653
|
);
|
|
1654
|
+
return { globalUuidMap, allSummaries };
|
|
1655
|
+
});
|
|
1656
|
+
var buildSummariesByTargetSession = (globalUuidMap, allSummaries) => {
|
|
1603
1657
|
const summariesByTargetSession = /* @__PURE__ */ new Map();
|
|
1604
1658
|
for (const summaryData of allSummaries) {
|
|
1605
1659
|
if (summaryData.leafUuid) {
|
|
@@ -1612,21 +1666,22 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
|
|
|
1612
1666
|
summariesByTargetSession.get(targetSessionId).push({
|
|
1613
1667
|
summary: summaryData.summary,
|
|
1614
1668
|
leafUuid: summaryData.leafUuid,
|
|
1615
|
-
// Use summary's own timestamp for sorting, not the target message's timestamp
|
|
1616
1669
|
timestamp: summaryData.timestamp ?? targetInfo.timestamp,
|
|
1617
1670
|
sourceFile: summaryData.sourceFile
|
|
1618
1671
|
});
|
|
1619
1672
|
}
|
|
1620
1673
|
}
|
|
1621
1674
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
);
|
|
1675
|
+
return summariesByTargetSession;
|
|
1676
|
+
};
|
|
1677
|
+
var sortSummaries = (summaries) => {
|
|
1678
|
+
return [...summaries].sort((a, b) => {
|
|
1679
|
+
const timestampCmp = (a.timestamp ?? "").localeCompare(b.timestamp ?? "");
|
|
1680
|
+
if (timestampCmp !== 0) return timestampCmp;
|
|
1681
|
+
return (b.sourceFile ?? "").localeCompare(a.sourceFile ?? "");
|
|
1682
|
+
});
|
|
1683
|
+
};
|
|
1684
|
+
var buildProjectTreeResult = (project, sessions, sort) => {
|
|
1630
1685
|
const sortedSessions = sortSessions(sessions, sort);
|
|
1631
1686
|
const filteredSessions = sortedSessions.filter((s) => {
|
|
1632
1687
|
if (isErrorSessionTitle(s.title)) return false;
|
|
@@ -1641,12 +1696,165 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
|
|
|
1641
1696
|
sessionCount: filteredSessions.length,
|
|
1642
1697
|
sessions: filteredSessions
|
|
1643
1698
|
};
|
|
1699
|
+
};
|
|
1700
|
+
var buildTreeCache = (globalUuidMap, allSummaries, sessions, fileMtimes) => {
|
|
1701
|
+
const uuidMapRecord = {};
|
|
1702
|
+
for (const [key, val] of globalUuidMap) {
|
|
1703
|
+
uuidMapRecord[key] = val;
|
|
1704
|
+
}
|
|
1705
|
+
const sessionsRecord = {};
|
|
1706
|
+
for (const s of sessions) {
|
|
1707
|
+
sessionsRecord[s.id] = {
|
|
1708
|
+
fileMtime: fileMtimes.get(s.id) ?? 0,
|
|
1709
|
+
data: s
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
version: 1,
|
|
1714
|
+
globalUuidMap: uuidMapRecord,
|
|
1715
|
+
allSummaries,
|
|
1716
|
+
sessions: sessionsRecord
|
|
1717
|
+
};
|
|
1718
|
+
};
|
|
1719
|
+
var updateSessionSummaries = (cached, summariesByTargetSession) => {
|
|
1720
|
+
const newSummaries = sortSummaries(summariesByTargetSession.get(cached.id) ?? []);
|
|
1721
|
+
const oldJson = JSON.stringify(cached.summaries);
|
|
1722
|
+
const newJson = JSON.stringify(newSummaries);
|
|
1723
|
+
if (oldJson === newJson) return cached;
|
|
1724
|
+
const newSortTimestamp = getSessionSortTimestamp({
|
|
1725
|
+
summaries: newSummaries,
|
|
1726
|
+
createdAt: cached.createdAt
|
|
1727
|
+
});
|
|
1728
|
+
return {
|
|
1729
|
+
...cached,
|
|
1730
|
+
summaries: newSummaries,
|
|
1731
|
+
currentSummary: newSummaries[0]?.summary,
|
|
1732
|
+
sortTimestamp: newSortTimestamp
|
|
1733
|
+
};
|
|
1734
|
+
};
|
|
1735
|
+
var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* () {
|
|
1736
|
+
const projectPath = path7.join(getSessionsDir(), projectName);
|
|
1737
|
+
const exists = yield* Effect6.tryPromise(
|
|
1738
|
+
() => fs8.access(projectPath).then(() => true).catch(() => false)
|
|
1739
|
+
);
|
|
1740
|
+
if (!exists) return null;
|
|
1741
|
+
const displayName = yield* Effect6.tryPromise(() => folderNameToPath(projectName));
|
|
1742
|
+
const project = { name: projectName, displayName, path: projectPath };
|
|
1743
|
+
const sort = sortOptions ?? DEFAULT_SORT;
|
|
1744
|
+
const files = yield* Effect6.tryPromise(() => fs8.readdir(projectPath));
|
|
1745
|
+
const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
1746
|
+
const sessionFileIds = sessionFiles.map((f) => f.replace(".jsonl", ""));
|
|
1747
|
+
const fileMtimes = /* @__PURE__ */ new Map();
|
|
1748
|
+
yield* Effect6.all(
|
|
1749
|
+
sessionFiles.map(
|
|
1750
|
+
(file) => Effect6.gen(function* () {
|
|
1751
|
+
const filePath = path7.join(projectPath, file);
|
|
1752
|
+
try {
|
|
1753
|
+
const stat4 = yield* Effect6.tryPromise(() => fs8.stat(filePath));
|
|
1754
|
+
fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
|
|
1755
|
+
} catch {
|
|
1756
|
+
}
|
|
1757
|
+
})
|
|
1758
|
+
),
|
|
1759
|
+
{ concurrency: 20 }
|
|
1760
|
+
);
|
|
1761
|
+
const cache = yield* Effect6.tryPromise(() => loadTreeCache(projectName));
|
|
1762
|
+
if (cache) {
|
|
1763
|
+
const validation = validateCache(cache, sessionFileIds, fileMtimes);
|
|
1764
|
+
if (validation.isFullHit) {
|
|
1765
|
+
log3.debug(`cache hit for ${projectName} (${sessionFileIds.length} sessions)`);
|
|
1766
|
+
const sessions = sessionFileIds.map((id) => cache.sessions[id]?.data).filter((s) => s != null);
|
|
1767
|
+
return buildProjectTreeResult(project, sessions, sort);
|
|
1768
|
+
}
|
|
1769
|
+
const changedCount = validation.changedSessionIds.length + validation.newSessionIds.length + validation.deletedSessionIds.length;
|
|
1770
|
+
log3.debug(
|
|
1771
|
+
`cache partial miss for ${projectName}: ${validation.changedSessionIds.length} changed, ${validation.newSessionIds.length} new, ${validation.deletedSessionIds.length} deleted, ${validation.unchangedSessionIds.length} unchanged`
|
|
1772
|
+
);
|
|
1773
|
+
if (changedCount <= sessionFileIds.length / 2) {
|
|
1774
|
+
const result = yield* loadProjectTreeDataIncremental(
|
|
1775
|
+
projectName,
|
|
1776
|
+
projectPath,
|
|
1777
|
+
files,
|
|
1778
|
+
sessionFiles,
|
|
1779
|
+
fileMtimes,
|
|
1780
|
+
cache,
|
|
1781
|
+
validation,
|
|
1782
|
+
project,
|
|
1783
|
+
sort
|
|
1784
|
+
);
|
|
1785
|
+
return result;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
log3.debug(`full load for ${projectName} (${sessionFileIds.length} sessions)`);
|
|
1789
|
+
return yield* loadProjectTreeDataFull(
|
|
1790
|
+
projectName,
|
|
1791
|
+
projectPath,
|
|
1792
|
+
files,
|
|
1793
|
+
sessionFiles,
|
|
1794
|
+
fileMtimes,
|
|
1795
|
+
project,
|
|
1796
|
+
sort
|
|
1797
|
+
);
|
|
1798
|
+
});
|
|
1799
|
+
var loadProjectTreeDataFull = (projectName, projectPath, files, sessionFiles, fileMtimes, project, sort) => Effect6.gen(function* () {
|
|
1800
|
+
const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
1801
|
+
const { globalUuidMap, allSummaries } = yield* buildPhase1(projectPath, allJsonlFiles);
|
|
1802
|
+
const summariesByTargetSession = buildSummariesByTargetSession(globalUuidMap, allSummaries);
|
|
1803
|
+
const sessions = yield* Effect6.all(
|
|
1804
|
+
sessionFiles.map((file) => {
|
|
1805
|
+
const sessionId = file.replace(".jsonl", "");
|
|
1806
|
+
const mtime = fileMtimes.get(sessionId);
|
|
1807
|
+
return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
|
|
1808
|
+
}),
|
|
1809
|
+
{ concurrency: 10 }
|
|
1810
|
+
);
|
|
1811
|
+
const cacheData = buildTreeCache(globalUuidMap, allSummaries, sessions, fileMtimes);
|
|
1812
|
+
writeTreeCache(projectName, cacheData).catch((err) => {
|
|
1813
|
+
log3.debug(`cache write failed for ${projectName}: ${err}`);
|
|
1814
|
+
});
|
|
1815
|
+
return buildProjectTreeResult(project, sessions, sort);
|
|
1816
|
+
});
|
|
1817
|
+
var loadProjectTreeDataIncremental = (projectName, projectPath, files, sessionFiles, fileMtimes, cache, validation, project, sort) => Effect6.gen(function* () {
|
|
1818
|
+
const allJsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
1819
|
+
const { globalUuidMap, allSummaries } = yield* buildPhase1(projectPath, allJsonlFiles);
|
|
1820
|
+
const summariesByTargetSession = buildSummariesByTargetSession(globalUuidMap, allSummaries);
|
|
1821
|
+
const sessionsToLoad = [...validation.changedSessionIds, ...validation.newSessionIds];
|
|
1822
|
+
const loadedSessions = yield* Effect6.all(
|
|
1823
|
+
sessionsToLoad.map((sessionId) => {
|
|
1824
|
+
const mtime = fileMtimes.get(sessionId);
|
|
1825
|
+
return loadSessionTreeDataInternal(projectName, sessionId, summariesByTargetSession, mtime);
|
|
1826
|
+
}),
|
|
1827
|
+
{ concurrency: 10 }
|
|
1828
|
+
);
|
|
1829
|
+
const loadedMap = /* @__PURE__ */ new Map();
|
|
1830
|
+
for (const s of loadedSessions) {
|
|
1831
|
+
loadedMap.set(s.id, s);
|
|
1832
|
+
}
|
|
1833
|
+
const allSessions = [];
|
|
1834
|
+
for (const file of sessionFiles) {
|
|
1835
|
+
const sessionId = file.replace(".jsonl", "");
|
|
1836
|
+
const loaded = loadedMap.get(sessionId);
|
|
1837
|
+
if (loaded) {
|
|
1838
|
+
allSessions.push(loaded);
|
|
1839
|
+
} else if (cache.sessions[sessionId]) {
|
|
1840
|
+
const updated = updateSessionSummaries(
|
|
1841
|
+
cache.sessions[sessionId].data,
|
|
1842
|
+
summariesByTargetSession
|
|
1843
|
+
);
|
|
1844
|
+
allSessions.push({ ...updated, fileMtime: fileMtimes.get(sessionId) });
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const cacheData = buildTreeCache(globalUuidMap, allSummaries, allSessions, fileMtimes);
|
|
1848
|
+
writeTreeCache(projectName, cacheData).catch((err) => {
|
|
1849
|
+
log3.debug(`cache write failed for ${projectName}: ${err}`);
|
|
1850
|
+
});
|
|
1851
|
+
return buildProjectTreeResult(project, allSessions, sort);
|
|
1644
1852
|
});
|
|
1645
1853
|
|
|
1646
1854
|
// src/session/analysis.ts
|
|
1647
1855
|
import { Effect as Effect7 } from "effect";
|
|
1648
|
-
import * as
|
|
1649
|
-
import * as
|
|
1856
|
+
import * as fs9 from "fs/promises";
|
|
1857
|
+
import * as path8 from "path";
|
|
1650
1858
|
var isToolUse = (item) => item !== null && typeof item === "object" && "type" in item && item.type === "tool_use";
|
|
1651
1859
|
var isToolResultError = (item) => item !== null && typeof item === "object" && "type" in item && item.type === "tool_result" && "is_error" in item && item.is_error === true;
|
|
1652
1860
|
var findToolUseById = (messages, toolUseId) => {
|
|
@@ -1777,11 +1985,11 @@ var analyzeSession = (projectName, sessionId) => Effect7.gen(function* () {
|
|
|
1777
1985
|
});
|
|
1778
1986
|
var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(function* () {
|
|
1779
1987
|
const { keepSnapshots = "first_last", maxToolOutputLength = 5e3 } = options;
|
|
1780
|
-
const filePath =
|
|
1781
|
-
const content = yield* Effect7.tryPromise(() =>
|
|
1988
|
+
const filePath = path8.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
1989
|
+
const content = yield* Effect7.tryPromise(() => fs9.readFile(filePath, "utf-8"));
|
|
1782
1990
|
const originalSize = Buffer.byteLength(content, "utf-8");
|
|
1783
1991
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1784
|
-
const messages = parseJsonlLines(lines, filePath);
|
|
1992
|
+
const messages = parseJsonlLines(lines, filePath, { strict: true });
|
|
1785
1993
|
let removedCustomTitles = 0;
|
|
1786
1994
|
let removedProgress = 0;
|
|
1787
1995
|
let removedSnapshots = 0;
|
|
@@ -1843,7 +2051,7 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
|
|
|
1843
2051
|
}
|
|
1844
2052
|
const newContent = filteredMessages.map((m) => JSON.stringify(m)).join("\n") + "\n";
|
|
1845
2053
|
const compressedSize = Buffer.byteLength(newContent, "utf-8");
|
|
1846
|
-
yield* Effect7.tryPromise(() =>
|
|
2054
|
+
yield* Effect7.tryPromise(() => fs9.writeFile(filePath, newContent, "utf-8"));
|
|
1847
2055
|
return {
|
|
1848
2056
|
success: true,
|
|
1849
2057
|
originalSize,
|
|
@@ -1856,10 +2064,10 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
|
|
|
1856
2064
|
});
|
|
1857
2065
|
var extractProjectKnowledge = (projectName, sessionIds) => Effect7.gen(function* () {
|
|
1858
2066
|
const sessionsDir = getSessionsDir();
|
|
1859
|
-
const projectDir =
|
|
2067
|
+
const projectDir = path8.join(sessionsDir, projectName);
|
|
1860
2068
|
let targetSessionIds = sessionIds;
|
|
1861
2069
|
if (!targetSessionIds) {
|
|
1862
|
-
const files = yield* Effect7.tryPromise(() =>
|
|
2070
|
+
const files = yield* Effect7.tryPromise(() => fs9.readdir(projectDir));
|
|
1863
2071
|
targetSessionIds = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => f.replace(".jsonl", ""));
|
|
1864
2072
|
}
|
|
1865
2073
|
const fileModifyCount = /* @__PURE__ */ new Map();
|
|
@@ -1986,11 +2194,11 @@ var summarizeSession = (projectName, sessionId, options = {}) => Effect7.gen(fun
|
|
|
1986
2194
|
|
|
1987
2195
|
// src/session/cleanup.ts
|
|
1988
2196
|
import { Effect as Effect8 } from "effect";
|
|
1989
|
-
import * as
|
|
1990
|
-
import * as
|
|
2197
|
+
import * as fs10 from "fs/promises";
|
|
2198
|
+
import * as path9 from "path";
|
|
1991
2199
|
var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* () {
|
|
1992
|
-
const filePath =
|
|
1993
|
-
const content = yield* Effect8.tryPromise(() =>
|
|
2200
|
+
const filePath = path9.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
|
|
2201
|
+
const content = yield* Effect8.tryPromise(() => fs10.readFile(filePath, "utf-8"));
|
|
1994
2202
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1995
2203
|
if (lines.length === 0) return { removedCount: 0, remainingCount: 0 };
|
|
1996
2204
|
const messages = parseJsonlLines(lines, filePath);
|
|
@@ -2022,7 +2230,7 @@ var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* ()
|
|
|
2022
2230
|
lastValidUuid = msg.uuid;
|
|
2023
2231
|
}
|
|
2024
2232
|
const newContent = filtered.length > 0 ? filtered.map((m) => JSON.stringify(m)).join("\n") + "\n" : "";
|
|
2025
|
-
yield* Effect8.tryPromise(() =>
|
|
2233
|
+
yield* Effect8.tryPromise(() => fs10.writeFile(filePath, newContent, "utf-8"));
|
|
2026
2234
|
const remainingUserAssistant = filtered.filter(
|
|
2027
2235
|
(m) => m.type === "user" || m.type === "assistant"
|
|
2028
2236
|
).length;
|
|
@@ -2088,8 +2296,8 @@ var clearSessions = (options) => Effect8.gen(function* () {
|
|
|
2088
2296
|
const sessionsToDelete = [];
|
|
2089
2297
|
if (clearInvalid) {
|
|
2090
2298
|
for (const project of targetProjects) {
|
|
2091
|
-
const projectPath =
|
|
2092
|
-
const files = yield* Effect8.tryPromise(() =>
|
|
2299
|
+
const projectPath = path9.join(getSessionsDir(), project.name);
|
|
2300
|
+
const files = yield* Effect8.tryPromise(() => fs10.readdir(projectPath));
|
|
2093
2301
|
const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
2094
2302
|
for (const file of sessionFiles) {
|
|
2095
2303
|
const sessionId = file.replace(".jsonl", "");
|
|
@@ -2146,8 +2354,8 @@ var clearSessions = (options) => Effect8.gen(function* () {
|
|
|
2146
2354
|
|
|
2147
2355
|
// src/session/search.ts
|
|
2148
2356
|
import { Effect as Effect9, pipe as pipe2 } from "effect";
|
|
2149
|
-
import * as
|
|
2150
|
-
import * as
|
|
2357
|
+
import * as fs11 from "fs/promises";
|
|
2358
|
+
import * as path10 from "path";
|
|
2151
2359
|
var extractSnippet = (text, matchIndex, queryLength) => {
|
|
2152
2360
|
const start = Math.max(0, matchIndex - 50);
|
|
2153
2361
|
const end = Math.min(text.length, matchIndex + queryLength + 50);
|
|
@@ -2171,7 +2379,7 @@ var findContentMatch = (lines, queryLower, filePath) => {
|
|
|
2171
2379
|
return null;
|
|
2172
2380
|
};
|
|
2173
2381
|
var searchSessionContent = (projectName, sessionId, filePath, queryLower) => pipe2(
|
|
2174
|
-
Effect9.tryPromise(() =>
|
|
2382
|
+
Effect9.tryPromise(() => fs11.readFile(filePath, "utf-8")),
|
|
2175
2383
|
Effect9.map((content) => {
|
|
2176
2384
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
2177
2385
|
const match = findContentMatch(lines, queryLower, filePath);
|
|
@@ -2189,12 +2397,12 @@ var searchSessionContent = (projectName, sessionId, filePath, queryLower) => pip
|
|
|
2189
2397
|
Effect9.catchAll(() => Effect9.succeed(null))
|
|
2190
2398
|
);
|
|
2191
2399
|
var searchProjectContent = (project, queryLower, alreadyFoundIds) => Effect9.gen(function* () {
|
|
2192
|
-
const projectPath =
|
|
2193
|
-
const files = yield* Effect9.tryPromise(() =>
|
|
2400
|
+
const projectPath = path10.join(getSessionsDir(), project.name);
|
|
2401
|
+
const files = yield* Effect9.tryPromise(() => fs11.readdir(projectPath));
|
|
2194
2402
|
const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
2195
2403
|
const searchEffects = sessionFiles.map((file) => ({
|
|
2196
2404
|
sessionId: file.replace(".jsonl", ""),
|
|
2197
|
-
filePath:
|
|
2405
|
+
filePath: path10.join(projectPath, file)
|
|
2198
2406
|
})).filter(({ sessionId }) => !alreadyFoundIds.has(`${project.name}:${sessionId}`)).map(
|
|
2199
2407
|
({ sessionId, filePath }) => searchSessionContent(project.name, sessionId, filePath, queryLower)
|
|
2200
2408
|
);
|
|
@@ -2298,12 +2506,12 @@ var getSessionFiles = (projectName, sessionId) => Effect10.gen(function* () {
|
|
|
2298
2506
|
|
|
2299
2507
|
// src/session/index-file.ts
|
|
2300
2508
|
import { Effect as Effect11 } from "effect";
|
|
2301
|
-
import * as
|
|
2302
|
-
import * as
|
|
2509
|
+
import * as fs12 from "fs/promises";
|
|
2510
|
+
import * as path11 from "path";
|
|
2303
2511
|
var loadSessionsIndex = (projectName) => Effect11.gen(function* () {
|
|
2304
|
-
const indexPath =
|
|
2512
|
+
const indexPath = path11.join(getSessionsDir(), projectName, "sessions-index.json");
|
|
2305
2513
|
try {
|
|
2306
|
-
const content = yield* Effect11.tryPromise(() =>
|
|
2514
|
+
const content = yield* Effect11.tryPromise(() => fs12.readFile(indexPath, "utf-8"));
|
|
2307
2515
|
const index = JSON.parse(content);
|
|
2308
2516
|
return index;
|
|
2309
2517
|
} catch {
|
|
@@ -2331,9 +2539,9 @@ var sortIndexEntriesByModified = (entries) => {
|
|
|
2331
2539
|
});
|
|
2332
2540
|
};
|
|
2333
2541
|
var hasSessionsIndex = (projectName) => Effect11.gen(function* () {
|
|
2334
|
-
const indexPath =
|
|
2542
|
+
const indexPath = path11.join(getSessionsDir(), projectName, "sessions-index.json");
|
|
2335
2543
|
try {
|
|
2336
|
-
yield* Effect11.tryPromise(() =>
|
|
2544
|
+
yield* Effect11.tryPromise(() => fs12.access(indexPath));
|
|
2337
2545
|
return true;
|
|
2338
2546
|
} catch {
|
|
2339
2547
|
return false;
|
|
@@ -2366,6 +2574,7 @@ export {
|
|
|
2366
2574
|
folderNameToPath,
|
|
2367
2575
|
formatRelativeTime,
|
|
2368
2576
|
generateTreeNodeId,
|
|
2577
|
+
getCachePath,
|
|
2369
2578
|
getDisplayTitle,
|
|
2370
2579
|
getIndexEntryDisplayTitle,
|
|
2371
2580
|
getLogger,
|