cogpit-memory 0.1.7 → 0.1.8
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/cli.js +173 -85
- package/dist/index.js +173 -85
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -951,19 +951,19 @@ function extractSessionMetadata(messages) {
|
|
|
951
951
|
}
|
|
952
952
|
return meta;
|
|
953
953
|
}
|
|
954
|
-
function parseSession(jsonlText) {
|
|
954
|
+
function parseSession(jsonlText, opts) {
|
|
955
955
|
if (isCodexSessionText(jsonlText)) {
|
|
956
956
|
return parseCodexSession(jsonlText);
|
|
957
957
|
}
|
|
958
958
|
const rawMessages = parseLines(jsonlText);
|
|
959
959
|
const metadata = extractSessionMetadata(rawMessages);
|
|
960
960
|
const turns = buildTurns(rawMessages);
|
|
961
|
-
const stats = computeStats(turns);
|
|
961
|
+
const stats = opts?.skipStats ? { totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreationTokens: 0, totalCacheReadTokens: 0, totalCostUSD: 0, toolCallCounts: {}, errorCount: 0, totalDurationMs: 0, turnCount: turns.length } : computeStats(turns);
|
|
962
962
|
return {
|
|
963
963
|
...metadata,
|
|
964
964
|
turns,
|
|
965
965
|
stats,
|
|
966
|
-
rawMessages,
|
|
966
|
+
rawMessages: opts?.skipStats ? [] : rawMessages,
|
|
967
967
|
agentKind: "claude"
|
|
968
968
|
};
|
|
969
969
|
}
|
|
@@ -974,6 +974,10 @@ function getUserMessageText(content) {
|
|
|
974
974
|
}
|
|
975
975
|
|
|
976
976
|
// src/lib/search-index.ts
|
|
977
|
+
var MAX_CONTENT_LEN = 4096;
|
|
978
|
+
function truncContent(text) {
|
|
979
|
+
return text.length > MAX_CONTENT_LEN ? text.slice(0, MAX_CONTENT_LEN) : text;
|
|
980
|
+
}
|
|
977
981
|
var SearchIndex = class {
|
|
978
982
|
db;
|
|
979
983
|
dbPath;
|
|
@@ -1007,7 +1011,7 @@ var SearchIndex = class {
|
|
|
1007
1011
|
source_file,
|
|
1008
1012
|
location,
|
|
1009
1013
|
content,
|
|
1010
|
-
tokenize = '
|
|
1014
|
+
tokenize = 'unicode61'
|
|
1011
1015
|
)`);
|
|
1012
1016
|
}
|
|
1013
1017
|
}
|
|
@@ -1034,6 +1038,60 @@ var SearchIndex = class {
|
|
|
1034
1038
|
lastUpdate: this._lastUpdate
|
|
1035
1039
|
};
|
|
1036
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Insert all searchable content from a parsed session into the FTS5 index.
|
|
1043
|
+
* Shared by both `indexFile` (single-file) and `buildFull` (batch).
|
|
1044
|
+
*/
|
|
1045
|
+
insertSessionContent(insert, sessionId, filePath, session) {
|
|
1046
|
+
for (let i = 0; i < session.turns.length; i++) {
|
|
1047
|
+
const turn = session.turns[i];
|
|
1048
|
+
const prefix = `turn/${i}`;
|
|
1049
|
+
const userText = getUserMessageText(turn.userMessage);
|
|
1050
|
+
if (userText.trim()) {
|
|
1051
|
+
insert.run(sessionId, filePath, `${prefix}/userMessage`, truncContent(userText));
|
|
1052
|
+
}
|
|
1053
|
+
const assistantJoined = turn.assistantText.join("\n\n").trim();
|
|
1054
|
+
if (assistantJoined) {
|
|
1055
|
+
insert.run(sessionId, filePath, `${prefix}/assistantMessage`, truncContent(assistantJoined));
|
|
1056
|
+
}
|
|
1057
|
+
const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
|
|
1058
|
+
if (thinkingText) {
|
|
1059
|
+
insert.run(sessionId, filePath, `${prefix}/thinking`, truncContent(thinkingText));
|
|
1060
|
+
}
|
|
1061
|
+
for (const tc of turn.toolCalls) {
|
|
1062
|
+
const inputStr = JSON.stringify(tc.input);
|
|
1063
|
+
if (inputStr && inputStr !== "{}") {
|
|
1064
|
+
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
|
|
1065
|
+
}
|
|
1066
|
+
if (tc.result) {
|
|
1067
|
+
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
for (const sa of turn.subAgentActivity) {
|
|
1071
|
+
const saPrefix = `agent/${sa.agentId}`;
|
|
1072
|
+
const saText = sa.text.join("\n\n").trim();
|
|
1073
|
+
if (saText) {
|
|
1074
|
+
insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, truncContent(saText));
|
|
1075
|
+
}
|
|
1076
|
+
const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
|
|
1077
|
+
if (saThinking) {
|
|
1078
|
+
insert.run(sessionId, filePath, `${saPrefix}/thinking`, truncContent(saThinking));
|
|
1079
|
+
}
|
|
1080
|
+
for (const tc of sa.toolCalls) {
|
|
1081
|
+
const inputStr = JSON.stringify(tc.input);
|
|
1082
|
+
if (inputStr && inputStr !== "{}") {
|
|
1083
|
+
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
|
|
1084
|
+
}
|
|
1085
|
+
if (tc.result) {
|
|
1086
|
+
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (turn.compactionSummary) {
|
|
1091
|
+
insert.run(sessionId, filePath, `${prefix}/compactionSummary`, truncContent(turn.compactionSummary));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1037
1095
|
/**
|
|
1038
1096
|
* Parse a JSONL file and insert all searchable content into the FTS5 index.
|
|
1039
1097
|
* Idempotent: deletes old data for the file before re-indexing.
|
|
@@ -1047,7 +1105,7 @@ var SearchIndex = class {
|
|
|
1047
1105
|
mtimeMs = (0, import_node_fs.statSync)(filePath).mtimeMs;
|
|
1048
1106
|
}
|
|
1049
1107
|
const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
1050
|
-
const session = parseSession(content);
|
|
1108
|
+
const session = parseSession(content, { skipStats: true });
|
|
1051
1109
|
const isSubagent = opts?.isSubagent ? 1 : 0;
|
|
1052
1110
|
const parentSessionId = opts?.parentSessionId ?? null;
|
|
1053
1111
|
const insert = this.db.prepare(
|
|
@@ -1065,54 +1123,7 @@ var SearchIndex = class {
|
|
|
1065
1123
|
const txn = this.db.transaction(() => {
|
|
1066
1124
|
deleteContent.run(filePath);
|
|
1067
1125
|
deleteFile.run(filePath);
|
|
1068
|
-
|
|
1069
|
-
const turn = session.turns[i];
|
|
1070
|
-
const prefix = `turn/${i}`;
|
|
1071
|
-
const userText = getUserMessageText(turn.userMessage);
|
|
1072
|
-
if (userText.trim()) {
|
|
1073
|
-
insert.run(sessionId, filePath, `${prefix}/userMessage`, userText);
|
|
1074
|
-
}
|
|
1075
|
-
const assistantJoined = turn.assistantText.join("\n\n").trim();
|
|
1076
|
-
if (assistantJoined) {
|
|
1077
|
-
insert.run(sessionId, filePath, `${prefix}/assistantMessage`, assistantJoined);
|
|
1078
|
-
}
|
|
1079
|
-
const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
|
|
1080
|
-
if (thinkingText) {
|
|
1081
|
-
insert.run(sessionId, filePath, `${prefix}/thinking`, thinkingText);
|
|
1082
|
-
}
|
|
1083
|
-
for (const tc of turn.toolCalls) {
|
|
1084
|
-
const inputStr = JSON.stringify(tc.input);
|
|
1085
|
-
if (inputStr && inputStr !== "{}") {
|
|
1086
|
-
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, inputStr);
|
|
1087
|
-
}
|
|
1088
|
-
if (tc.result) {
|
|
1089
|
-
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, tc.result);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
for (const sa of turn.subAgentActivity) {
|
|
1093
|
-
const saPrefix = `agent/${sa.agentId}`;
|
|
1094
|
-
const saText = sa.text.join("\n\n").trim();
|
|
1095
|
-
if (saText) {
|
|
1096
|
-
insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, saText);
|
|
1097
|
-
}
|
|
1098
|
-
const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
|
|
1099
|
-
if (saThinking) {
|
|
1100
|
-
insert.run(sessionId, filePath, `${saPrefix}/thinking`, saThinking);
|
|
1101
|
-
}
|
|
1102
|
-
for (const tc of sa.toolCalls) {
|
|
1103
|
-
const inputStr = JSON.stringify(tc.input);
|
|
1104
|
-
if (inputStr && inputStr !== "{}") {
|
|
1105
|
-
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, inputStr);
|
|
1106
|
-
}
|
|
1107
|
-
if (tc.result) {
|
|
1108
|
-
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, tc.result);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
if (turn.compactionSummary) {
|
|
1113
|
-
insert.run(sessionId, filePath, `${prefix}/compactionSummary`, turn.compactionSummary);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1126
|
+
this.insertSessionContent(insert, sessionId, filePath, session);
|
|
1116
1127
|
insertFile.run(filePath, mtimeMs, sessionId, isSubagent, parentSessionId);
|
|
1117
1128
|
});
|
|
1118
1129
|
txn();
|
|
@@ -1163,7 +1174,7 @@ var SearchIndex = class {
|
|
|
1163
1174
|
location: row.location,
|
|
1164
1175
|
snippet: row.snippet,
|
|
1165
1176
|
matchCount: 1
|
|
1166
|
-
// FTS5
|
|
1177
|
+
// FTS5 doesn't expose per-row match count; 1 = "at least one match"
|
|
1167
1178
|
}));
|
|
1168
1179
|
if (caseSensitive) {
|
|
1169
1180
|
hits = hits.filter((h) => h.snippet.includes(query));
|
|
@@ -1202,19 +1213,67 @@ var SearchIndex = class {
|
|
|
1202
1213
|
* Structure: projectsDir/{projectName}/{sessionId}.jsonl
|
|
1203
1214
|
* Subagents: projectsDir/{projectName}/{sessionId}/subagents/agent-{id}.jsonl
|
|
1204
1215
|
*
|
|
1216
|
+
* Optimized: discovers all files first, then processes them in a single
|
|
1217
|
+
* SQLite transaction with pre-prepared statements. This avoids the overhead
|
|
1218
|
+
* of 3000+ individual transactions (each forcing a disk sync).
|
|
1219
|
+
*
|
|
1205
1220
|
* Stores `projectsDir` as a class field so `rebuild()` can reuse it.
|
|
1206
1221
|
*/
|
|
1207
1222
|
buildFull(projectsDir) {
|
|
1208
1223
|
this.projectsDir = projectsDir;
|
|
1209
|
-
this.db.
|
|
1210
|
-
|
|
1211
|
-
|
|
1224
|
+
this.db.close();
|
|
1225
|
+
try {
|
|
1226
|
+
(0, import_node_fs.unlinkSync)(this.dbPath);
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
(0, import_node_fs.unlinkSync)(this.dbPath + "-wal");
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
(0, import_node_fs.unlinkSync)(this.dbPath + "-shm");
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
this.db = new Database(this.dbPath);
|
|
1238
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1239
|
+
this.db.exec("PRAGMA synchronous = OFF");
|
|
1240
|
+
this.db.exec("PRAGMA cache_size = -64000");
|
|
1241
|
+
this.db.exec("PRAGMA temp_store = MEMORY");
|
|
1242
|
+
this.db.exec("PRAGMA mmap_size = 268435456");
|
|
1243
|
+
this.initSchema();
|
|
1244
|
+
const files = [];
|
|
1245
|
+
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1246
|
+
files.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
|
|
1247
|
+
});
|
|
1248
|
+
const insert = this.db.prepare(
|
|
1249
|
+
"INSERT INTO search_content (session_id, source_file, location, content) VALUES (?, ?, ?, ?)"
|
|
1250
|
+
);
|
|
1251
|
+
const insertFile = this.db.prepare(
|
|
1252
|
+
"INSERT OR REPLACE INTO indexed_files (file_path, mtime_ms, session_id, is_subagent, parent_session_id) VALUES (?, ?, ?, ?, ?)"
|
|
1253
|
+
);
|
|
1254
|
+
const txn = this.db.transaction(() => {
|
|
1255
|
+
for (const file of files) {
|
|
1256
|
+
try {
|
|
1257
|
+
const content = (0, import_node_fs.readFileSync)(file.path, "utf-8");
|
|
1258
|
+
const session = parseSession(content, { skipStats: true });
|
|
1259
|
+
this.insertSessionContent(insert, file.sessionId, file.path, session);
|
|
1260
|
+
insertFile.run(file.path, file.mtimeMs, file.sessionId, file.isSubagent ? 1 : 0, file.parentSessionId);
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
txn();
|
|
1266
|
+
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
1212
1267
|
this._lastFullBuild = (/* @__PURE__ */ new Date()).toISOString();
|
|
1213
1268
|
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1214
1269
|
}
|
|
1215
1270
|
/**
|
|
1216
1271
|
* Incrementally re-index only files whose mtime has changed since last index.
|
|
1217
1272
|
* New files (not in indexed_files) are always indexed.
|
|
1273
|
+
*
|
|
1274
|
+
* WARNING: This walks ALL files under projectsDir and stats each one.
|
|
1275
|
+
* On large session stores (3000+ files) this can take minutes.
|
|
1276
|
+
* Prefer `updateRecent()` for CLI search paths.
|
|
1218
1277
|
*/
|
|
1219
1278
|
updateStale(projectsDir) {
|
|
1220
1279
|
this.projectsDir = projectsDir;
|
|
@@ -1241,6 +1300,45 @@ var SearchIndex = class {
|
|
|
1241
1300
|
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1242
1301
|
}
|
|
1243
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Lightweight incremental update for CLI search paths.
|
|
1305
|
+
*
|
|
1306
|
+
* Instead of stat-ing every file, only discovers files modified after the
|
|
1307
|
+
* most recent mtime in the index. Caps re-indexing to `maxFiles` to prevent
|
|
1308
|
+
* blocking on large backlogs (run `index rebuild` for a full catch-up).
|
|
1309
|
+
*/
|
|
1310
|
+
updateRecent(projectsDir, maxFiles = 50) {
|
|
1311
|
+
this.projectsDir = projectsDir;
|
|
1312
|
+
const row = this.db.prepare(
|
|
1313
|
+
"SELECT MAX(mtime_ms) as max_mtime FROM indexed_files"
|
|
1314
|
+
).get();
|
|
1315
|
+
const highWater = row?.max_mtime ?? 0;
|
|
1316
|
+
const getIndexed = this.db.prepare(
|
|
1317
|
+
"SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
|
|
1318
|
+
);
|
|
1319
|
+
const filesToIndex = [];
|
|
1320
|
+
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1321
|
+
if (mtimeMs <= highWater) {
|
|
1322
|
+
const existing = getIndexed.get(filePath);
|
|
1323
|
+
if (existing && existing.mtime_ms >= mtimeMs) return;
|
|
1324
|
+
}
|
|
1325
|
+
filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
|
|
1326
|
+
});
|
|
1327
|
+
filesToIndex.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1328
|
+
const batch = filesToIndex.slice(0, maxFiles);
|
|
1329
|
+
for (const file of batch) {
|
|
1330
|
+
try {
|
|
1331
|
+
this.indexFile(file.path, file.sessionId, file.mtimeMs, {
|
|
1332
|
+
isSubagent: file.isSubagent,
|
|
1333
|
+
parentSessionId: file.parentSessionId
|
|
1334
|
+
});
|
|
1335
|
+
} catch {
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (batch.length > 0) {
|
|
1339
|
+
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1244
1342
|
/**
|
|
1245
1343
|
* Re-run buildFull using the previously stored projectsDir.
|
|
1246
1344
|
* No-op if projectsDir was never set.
|
|
@@ -1249,18 +1347,6 @@ var SearchIndex = class {
|
|
|
1249
1347
|
if (!this.projectsDir) return;
|
|
1250
1348
|
this.buildFull(this.projectsDir);
|
|
1251
1349
|
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Walk all project directories under `projectsDir` and index every discovered
|
|
1254
|
-
* JSONL file (both sessions and subagents).
|
|
1255
|
-
*/
|
|
1256
|
-
indexProjectsDir(projectsDir) {
|
|
1257
|
-
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1258
|
-
try {
|
|
1259
|
-
this.indexFile(filePath, sessionId, mtimeMs, { isSubagent, parentSessionId });
|
|
1260
|
-
} catch {
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
1350
|
/**
|
|
1265
1351
|
* Walk the projects directory tree and invoke `callback` for every JSONL file.
|
|
1266
1352
|
*
|
|
@@ -1276,29 +1362,23 @@ var SearchIndex = class {
|
|
|
1276
1362
|
discoverFiles(projectsDir, callback) {
|
|
1277
1363
|
let entries;
|
|
1278
1364
|
try {
|
|
1279
|
-
entries = (0, import_node_fs.readdirSync)(projectsDir);
|
|
1365
|
+
entries = (0, import_node_fs.readdirSync)(projectsDir, { withFileTypes: true });
|
|
1280
1366
|
} catch {
|
|
1281
1367
|
return;
|
|
1282
1368
|
}
|
|
1283
|
-
for (const
|
|
1284
|
-
if (
|
|
1285
|
-
const projectDir = (0, import_node_path.join)(projectsDir,
|
|
1286
|
-
try {
|
|
1287
|
-
const s = (0, import_node_fs.statSync)(projectDir);
|
|
1288
|
-
if (!s.isDirectory()) continue;
|
|
1289
|
-
} catch {
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1369
|
+
for (const entry of entries) {
|
|
1370
|
+
if (entry.name === "memory" || !entry.isDirectory()) continue;
|
|
1371
|
+
const projectDir = (0, import_node_path.join)(projectsDir, entry.name);
|
|
1292
1372
|
let files;
|
|
1293
1373
|
try {
|
|
1294
|
-
files = (0, import_node_fs.readdirSync)(projectDir);
|
|
1374
|
+
files = (0, import_node_fs.readdirSync)(projectDir, { withFileTypes: true });
|
|
1295
1375
|
} catch {
|
|
1296
1376
|
continue;
|
|
1297
1377
|
}
|
|
1298
1378
|
for (const file of files) {
|
|
1299
|
-
if (!file.endsWith(".jsonl")) continue;
|
|
1300
|
-
const filePath = (0, import_node_path.join)(projectDir, file);
|
|
1301
|
-
const sessionId = (0, import_node_path.basename)(file, ".jsonl");
|
|
1379
|
+
if (!file.name.endsWith(".jsonl") || !file.isFile()) continue;
|
|
1380
|
+
const filePath = (0, import_node_path.join)(projectDir, file.name);
|
|
1381
|
+
const sessionId = (0, import_node_path.basename)(file.name, ".jsonl");
|
|
1302
1382
|
try {
|
|
1303
1383
|
const s = (0, import_node_fs.statSync)(filePath);
|
|
1304
1384
|
callback(filePath, sessionId, s.mtimeMs, false, null);
|
|
@@ -1544,11 +1624,19 @@ async function searchSessions(query, opts, searchIndex) {
|
|
|
1544
1624
|
const depth = Math.min(Math.max(1, opts.depth ?? 4), 4);
|
|
1545
1625
|
let index = searchIndex ?? null;
|
|
1546
1626
|
let ownedIndex = false;
|
|
1547
|
-
if (!index &&
|
|
1627
|
+
if (!index && searchIndex === void 0) {
|
|
1548
1628
|
try {
|
|
1629
|
+
const dbExists = (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH);
|
|
1630
|
+
if (!dbExists) {
|
|
1631
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(DEFAULT_DB_PATH), { recursive: true });
|
|
1632
|
+
}
|
|
1549
1633
|
index = new SearchIndex(DEFAULT_DB_PATH);
|
|
1550
1634
|
ownedIndex = true;
|
|
1551
|
-
|
|
1635
|
+
if (!dbExists) {
|
|
1636
|
+
index.buildFull(dirs.PROJECTS_DIR);
|
|
1637
|
+
} else {
|
|
1638
|
+
index.updateRecent(dirs.PROJECTS_DIR);
|
|
1639
|
+
}
|
|
1552
1640
|
} catch {
|
|
1553
1641
|
}
|
|
1554
1642
|
}
|
package/dist/index.js
CHANGED
|
@@ -967,19 +967,19 @@ function extractSessionMetadata(messages) {
|
|
|
967
967
|
}
|
|
968
968
|
return meta;
|
|
969
969
|
}
|
|
970
|
-
function parseSession(jsonlText) {
|
|
970
|
+
function parseSession(jsonlText, opts) {
|
|
971
971
|
if (isCodexSessionText(jsonlText)) {
|
|
972
972
|
return parseCodexSession(jsonlText);
|
|
973
973
|
}
|
|
974
974
|
const rawMessages = parseLines(jsonlText);
|
|
975
975
|
const metadata = extractSessionMetadata(rawMessages);
|
|
976
976
|
const turns = buildTurns(rawMessages);
|
|
977
|
-
const stats = computeStats(turns);
|
|
977
|
+
const stats = opts?.skipStats ? { totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreationTokens: 0, totalCacheReadTokens: 0, totalCostUSD: 0, toolCallCounts: {}, errorCount: 0, totalDurationMs: 0, turnCount: turns.length } : computeStats(turns);
|
|
978
978
|
return {
|
|
979
979
|
...metadata,
|
|
980
980
|
turns,
|
|
981
981
|
stats,
|
|
982
|
-
rawMessages,
|
|
982
|
+
rawMessages: opts?.skipStats ? [] : rawMessages,
|
|
983
983
|
agentKind: "claude"
|
|
984
984
|
};
|
|
985
985
|
}
|
|
@@ -1072,6 +1072,10 @@ function getUserMessageImages(content) {
|
|
|
1072
1072
|
}
|
|
1073
1073
|
|
|
1074
1074
|
// src/lib/search-index.ts
|
|
1075
|
+
var MAX_CONTENT_LEN = 4096;
|
|
1076
|
+
function truncContent(text) {
|
|
1077
|
+
return text.length > MAX_CONTENT_LEN ? text.slice(0, MAX_CONTENT_LEN) : text;
|
|
1078
|
+
}
|
|
1075
1079
|
var SearchIndex = class {
|
|
1076
1080
|
db;
|
|
1077
1081
|
dbPath;
|
|
@@ -1105,7 +1109,7 @@ var SearchIndex = class {
|
|
|
1105
1109
|
source_file,
|
|
1106
1110
|
location,
|
|
1107
1111
|
content,
|
|
1108
|
-
tokenize = '
|
|
1112
|
+
tokenize = 'unicode61'
|
|
1109
1113
|
)`);
|
|
1110
1114
|
}
|
|
1111
1115
|
}
|
|
@@ -1132,6 +1136,60 @@ var SearchIndex = class {
|
|
|
1132
1136
|
lastUpdate: this._lastUpdate
|
|
1133
1137
|
};
|
|
1134
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Insert all searchable content from a parsed session into the FTS5 index.
|
|
1141
|
+
* Shared by both `indexFile` (single-file) and `buildFull` (batch).
|
|
1142
|
+
*/
|
|
1143
|
+
insertSessionContent(insert, sessionId, filePath, session) {
|
|
1144
|
+
for (let i = 0; i < session.turns.length; i++) {
|
|
1145
|
+
const turn = session.turns[i];
|
|
1146
|
+
const prefix = `turn/${i}`;
|
|
1147
|
+
const userText = getUserMessageText(turn.userMessage);
|
|
1148
|
+
if (userText.trim()) {
|
|
1149
|
+
insert.run(sessionId, filePath, `${prefix}/userMessage`, truncContent(userText));
|
|
1150
|
+
}
|
|
1151
|
+
const assistantJoined = turn.assistantText.join("\n\n").trim();
|
|
1152
|
+
if (assistantJoined) {
|
|
1153
|
+
insert.run(sessionId, filePath, `${prefix}/assistantMessage`, truncContent(assistantJoined));
|
|
1154
|
+
}
|
|
1155
|
+
const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
|
|
1156
|
+
if (thinkingText) {
|
|
1157
|
+
insert.run(sessionId, filePath, `${prefix}/thinking`, truncContent(thinkingText));
|
|
1158
|
+
}
|
|
1159
|
+
for (const tc of turn.toolCalls) {
|
|
1160
|
+
const inputStr = JSON.stringify(tc.input);
|
|
1161
|
+
if (inputStr && inputStr !== "{}") {
|
|
1162
|
+
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
|
|
1163
|
+
}
|
|
1164
|
+
if (tc.result) {
|
|
1165
|
+
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
for (const sa of turn.subAgentActivity) {
|
|
1169
|
+
const saPrefix = `agent/${sa.agentId}`;
|
|
1170
|
+
const saText = sa.text.join("\n\n").trim();
|
|
1171
|
+
if (saText) {
|
|
1172
|
+
insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, truncContent(saText));
|
|
1173
|
+
}
|
|
1174
|
+
const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
|
|
1175
|
+
if (saThinking) {
|
|
1176
|
+
insert.run(sessionId, filePath, `${saPrefix}/thinking`, truncContent(saThinking));
|
|
1177
|
+
}
|
|
1178
|
+
for (const tc of sa.toolCalls) {
|
|
1179
|
+
const inputStr = JSON.stringify(tc.input);
|
|
1180
|
+
if (inputStr && inputStr !== "{}") {
|
|
1181
|
+
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
|
|
1182
|
+
}
|
|
1183
|
+
if (tc.result) {
|
|
1184
|
+
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (turn.compactionSummary) {
|
|
1189
|
+
insert.run(sessionId, filePath, `${prefix}/compactionSummary`, truncContent(turn.compactionSummary));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1135
1193
|
/**
|
|
1136
1194
|
* Parse a JSONL file and insert all searchable content into the FTS5 index.
|
|
1137
1195
|
* Idempotent: deletes old data for the file before re-indexing.
|
|
@@ -1145,7 +1203,7 @@ var SearchIndex = class {
|
|
|
1145
1203
|
mtimeMs = (0, import_node_fs.statSync)(filePath).mtimeMs;
|
|
1146
1204
|
}
|
|
1147
1205
|
const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
1148
|
-
const session = parseSession(content);
|
|
1206
|
+
const session = parseSession(content, { skipStats: true });
|
|
1149
1207
|
const isSubagent = opts?.isSubagent ? 1 : 0;
|
|
1150
1208
|
const parentSessionId = opts?.parentSessionId ?? null;
|
|
1151
1209
|
const insert = this.db.prepare(
|
|
@@ -1163,54 +1221,7 @@ var SearchIndex = class {
|
|
|
1163
1221
|
const txn = this.db.transaction(() => {
|
|
1164
1222
|
deleteContent.run(filePath);
|
|
1165
1223
|
deleteFile.run(filePath);
|
|
1166
|
-
|
|
1167
|
-
const turn = session.turns[i];
|
|
1168
|
-
const prefix = `turn/${i}`;
|
|
1169
|
-
const userText = getUserMessageText(turn.userMessage);
|
|
1170
|
-
if (userText.trim()) {
|
|
1171
|
-
insert.run(sessionId, filePath, `${prefix}/userMessage`, userText);
|
|
1172
|
-
}
|
|
1173
|
-
const assistantJoined = turn.assistantText.join("\n\n").trim();
|
|
1174
|
-
if (assistantJoined) {
|
|
1175
|
-
insert.run(sessionId, filePath, `${prefix}/assistantMessage`, assistantJoined);
|
|
1176
|
-
}
|
|
1177
|
-
const thinkingText = turn.thinking.filter((t) => t.thinking && t.thinking.length > 0).map((t) => t.thinking).join("\n\n").trim();
|
|
1178
|
-
if (thinkingText) {
|
|
1179
|
-
insert.run(sessionId, filePath, `${prefix}/thinking`, thinkingText);
|
|
1180
|
-
}
|
|
1181
|
-
for (const tc of turn.toolCalls) {
|
|
1182
|
-
const inputStr = JSON.stringify(tc.input);
|
|
1183
|
-
if (inputStr && inputStr !== "{}") {
|
|
1184
|
-
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, inputStr);
|
|
1185
|
-
}
|
|
1186
|
-
if (tc.result) {
|
|
1187
|
-
insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, tc.result);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
for (const sa of turn.subAgentActivity) {
|
|
1191
|
-
const saPrefix = `agent/${sa.agentId}`;
|
|
1192
|
-
const saText = sa.text.join("\n\n").trim();
|
|
1193
|
-
if (saText) {
|
|
1194
|
-
insert.run(sessionId, filePath, `${saPrefix}/assistantMessage`, saText);
|
|
1195
|
-
}
|
|
1196
|
-
const saThinking = sa.thinking.filter((t) => t.length > 0).join("\n\n").trim();
|
|
1197
|
-
if (saThinking) {
|
|
1198
|
-
insert.run(sessionId, filePath, `${saPrefix}/thinking`, saThinking);
|
|
1199
|
-
}
|
|
1200
|
-
for (const tc of sa.toolCalls) {
|
|
1201
|
-
const inputStr = JSON.stringify(tc.input);
|
|
1202
|
-
if (inputStr && inputStr !== "{}") {
|
|
1203
|
-
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, inputStr);
|
|
1204
|
-
}
|
|
1205
|
-
if (tc.result) {
|
|
1206
|
-
insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, tc.result);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
if (turn.compactionSummary) {
|
|
1211
|
-
insert.run(sessionId, filePath, `${prefix}/compactionSummary`, turn.compactionSummary);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1224
|
+
this.insertSessionContent(insert, sessionId, filePath, session);
|
|
1214
1225
|
insertFile.run(filePath, mtimeMs, sessionId, isSubagent, parentSessionId);
|
|
1215
1226
|
});
|
|
1216
1227
|
txn();
|
|
@@ -1261,7 +1272,7 @@ var SearchIndex = class {
|
|
|
1261
1272
|
location: row.location,
|
|
1262
1273
|
snippet: row.snippet,
|
|
1263
1274
|
matchCount: 1
|
|
1264
|
-
// FTS5
|
|
1275
|
+
// FTS5 doesn't expose per-row match count; 1 = "at least one match"
|
|
1265
1276
|
}));
|
|
1266
1277
|
if (caseSensitive) {
|
|
1267
1278
|
hits = hits.filter((h) => h.snippet.includes(query));
|
|
@@ -1300,19 +1311,67 @@ var SearchIndex = class {
|
|
|
1300
1311
|
* Structure: projectsDir/{projectName}/{sessionId}.jsonl
|
|
1301
1312
|
* Subagents: projectsDir/{projectName}/{sessionId}/subagents/agent-{id}.jsonl
|
|
1302
1313
|
*
|
|
1314
|
+
* Optimized: discovers all files first, then processes them in a single
|
|
1315
|
+
* SQLite transaction with pre-prepared statements. This avoids the overhead
|
|
1316
|
+
* of 3000+ individual transactions (each forcing a disk sync).
|
|
1317
|
+
*
|
|
1303
1318
|
* Stores `projectsDir` as a class field so `rebuild()` can reuse it.
|
|
1304
1319
|
*/
|
|
1305
1320
|
buildFull(projectsDir) {
|
|
1306
1321
|
this.projectsDir = projectsDir;
|
|
1307
|
-
this.db.
|
|
1308
|
-
|
|
1309
|
-
|
|
1322
|
+
this.db.close();
|
|
1323
|
+
try {
|
|
1324
|
+
(0, import_node_fs.unlinkSync)(this.dbPath);
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
(0, import_node_fs.unlinkSync)(this.dbPath + "-wal");
|
|
1329
|
+
} catch {
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
(0, import_node_fs.unlinkSync)(this.dbPath + "-shm");
|
|
1333
|
+
} catch {
|
|
1334
|
+
}
|
|
1335
|
+
this.db = new Database(this.dbPath);
|
|
1336
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1337
|
+
this.db.exec("PRAGMA synchronous = OFF");
|
|
1338
|
+
this.db.exec("PRAGMA cache_size = -64000");
|
|
1339
|
+
this.db.exec("PRAGMA temp_store = MEMORY");
|
|
1340
|
+
this.db.exec("PRAGMA mmap_size = 268435456");
|
|
1341
|
+
this.initSchema();
|
|
1342
|
+
const files = [];
|
|
1343
|
+
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1344
|
+
files.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
|
|
1345
|
+
});
|
|
1346
|
+
const insert = this.db.prepare(
|
|
1347
|
+
"INSERT INTO search_content (session_id, source_file, location, content) VALUES (?, ?, ?, ?)"
|
|
1348
|
+
);
|
|
1349
|
+
const insertFile = this.db.prepare(
|
|
1350
|
+
"INSERT OR REPLACE INTO indexed_files (file_path, mtime_ms, session_id, is_subagent, parent_session_id) VALUES (?, ?, ?, ?, ?)"
|
|
1351
|
+
);
|
|
1352
|
+
const txn = this.db.transaction(() => {
|
|
1353
|
+
for (const file of files) {
|
|
1354
|
+
try {
|
|
1355
|
+
const content = (0, import_node_fs.readFileSync)(file.path, "utf-8");
|
|
1356
|
+
const session = parseSession(content, { skipStats: true });
|
|
1357
|
+
this.insertSessionContent(insert, file.sessionId, file.path, session);
|
|
1358
|
+
insertFile.run(file.path, file.mtimeMs, file.sessionId, file.isSubagent ? 1 : 0, file.parentSessionId);
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
txn();
|
|
1364
|
+
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
1310
1365
|
this._lastFullBuild = (/* @__PURE__ */ new Date()).toISOString();
|
|
1311
1366
|
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1312
1367
|
}
|
|
1313
1368
|
/**
|
|
1314
1369
|
* Incrementally re-index only files whose mtime has changed since last index.
|
|
1315
1370
|
* New files (not in indexed_files) are always indexed.
|
|
1371
|
+
*
|
|
1372
|
+
* WARNING: This walks ALL files under projectsDir and stats each one.
|
|
1373
|
+
* On large session stores (3000+ files) this can take minutes.
|
|
1374
|
+
* Prefer `updateRecent()` for CLI search paths.
|
|
1316
1375
|
*/
|
|
1317
1376
|
updateStale(projectsDir) {
|
|
1318
1377
|
this.projectsDir = projectsDir;
|
|
@@ -1339,6 +1398,45 @@ var SearchIndex = class {
|
|
|
1339
1398
|
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1340
1399
|
}
|
|
1341
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Lightweight incremental update for CLI search paths.
|
|
1403
|
+
*
|
|
1404
|
+
* Instead of stat-ing every file, only discovers files modified after the
|
|
1405
|
+
* most recent mtime in the index. Caps re-indexing to `maxFiles` to prevent
|
|
1406
|
+
* blocking on large backlogs (run `index rebuild` for a full catch-up).
|
|
1407
|
+
*/
|
|
1408
|
+
updateRecent(projectsDir, maxFiles = 50) {
|
|
1409
|
+
this.projectsDir = projectsDir;
|
|
1410
|
+
const row = this.db.prepare(
|
|
1411
|
+
"SELECT MAX(mtime_ms) as max_mtime FROM indexed_files"
|
|
1412
|
+
).get();
|
|
1413
|
+
const highWater = row?.max_mtime ?? 0;
|
|
1414
|
+
const getIndexed = this.db.prepare(
|
|
1415
|
+
"SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
|
|
1416
|
+
);
|
|
1417
|
+
const filesToIndex = [];
|
|
1418
|
+
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1419
|
+
if (mtimeMs <= highWater) {
|
|
1420
|
+
const existing = getIndexed.get(filePath);
|
|
1421
|
+
if (existing && existing.mtime_ms >= mtimeMs) return;
|
|
1422
|
+
}
|
|
1423
|
+
filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
|
|
1424
|
+
});
|
|
1425
|
+
filesToIndex.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1426
|
+
const batch = filesToIndex.slice(0, maxFiles);
|
|
1427
|
+
for (const file of batch) {
|
|
1428
|
+
try {
|
|
1429
|
+
this.indexFile(file.path, file.sessionId, file.mtimeMs, {
|
|
1430
|
+
isSubagent: file.isSubagent,
|
|
1431
|
+
parentSessionId: file.parentSessionId
|
|
1432
|
+
});
|
|
1433
|
+
} catch {
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (batch.length > 0) {
|
|
1437
|
+
this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1342
1440
|
/**
|
|
1343
1441
|
* Re-run buildFull using the previously stored projectsDir.
|
|
1344
1442
|
* No-op if projectsDir was never set.
|
|
@@ -1347,18 +1445,6 @@ var SearchIndex = class {
|
|
|
1347
1445
|
if (!this.projectsDir) return;
|
|
1348
1446
|
this.buildFull(this.projectsDir);
|
|
1349
1447
|
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Walk all project directories under `projectsDir` and index every discovered
|
|
1352
|
-
* JSONL file (both sessions and subagents).
|
|
1353
|
-
*/
|
|
1354
|
-
indexProjectsDir(projectsDir) {
|
|
1355
|
-
this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
|
|
1356
|
-
try {
|
|
1357
|
-
this.indexFile(filePath, sessionId, mtimeMs, { isSubagent, parentSessionId });
|
|
1358
|
-
} catch {
|
|
1359
|
-
}
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
1448
|
/**
|
|
1363
1449
|
* Walk the projects directory tree and invoke `callback` for every JSONL file.
|
|
1364
1450
|
*
|
|
@@ -1374,29 +1460,23 @@ var SearchIndex = class {
|
|
|
1374
1460
|
discoverFiles(projectsDir, callback) {
|
|
1375
1461
|
let entries;
|
|
1376
1462
|
try {
|
|
1377
|
-
entries = (0, import_node_fs.readdirSync)(projectsDir);
|
|
1463
|
+
entries = (0, import_node_fs.readdirSync)(projectsDir, { withFileTypes: true });
|
|
1378
1464
|
} catch {
|
|
1379
1465
|
return;
|
|
1380
1466
|
}
|
|
1381
|
-
for (const
|
|
1382
|
-
if (
|
|
1383
|
-
const projectDir = (0, import_node_path.join)(projectsDir,
|
|
1384
|
-
try {
|
|
1385
|
-
const s = (0, import_node_fs.statSync)(projectDir);
|
|
1386
|
-
if (!s.isDirectory()) continue;
|
|
1387
|
-
} catch {
|
|
1388
|
-
continue;
|
|
1389
|
-
}
|
|
1467
|
+
for (const entry of entries) {
|
|
1468
|
+
if (entry.name === "memory" || !entry.isDirectory()) continue;
|
|
1469
|
+
const projectDir = (0, import_node_path.join)(projectsDir, entry.name);
|
|
1390
1470
|
let files;
|
|
1391
1471
|
try {
|
|
1392
|
-
files = (0, import_node_fs.readdirSync)(projectDir);
|
|
1472
|
+
files = (0, import_node_fs.readdirSync)(projectDir, { withFileTypes: true });
|
|
1393
1473
|
} catch {
|
|
1394
1474
|
continue;
|
|
1395
1475
|
}
|
|
1396
1476
|
for (const file of files) {
|
|
1397
|
-
if (!file.endsWith(".jsonl")) continue;
|
|
1398
|
-
const filePath = (0, import_node_path.join)(projectDir, file);
|
|
1399
|
-
const sessionId = (0, import_node_path.basename)(file, ".jsonl");
|
|
1477
|
+
if (!file.name.endsWith(".jsonl") || !file.isFile()) continue;
|
|
1478
|
+
const filePath = (0, import_node_path.join)(projectDir, file.name);
|
|
1479
|
+
const sessionId = (0, import_node_path.basename)(file.name, ".jsonl");
|
|
1400
1480
|
try {
|
|
1401
1481
|
const s = (0, import_node_fs.statSync)(filePath);
|
|
1402
1482
|
callback(filePath, sessionId, s.mtimeMs, false, null);
|
|
@@ -1647,11 +1727,19 @@ async function searchSessions(query, opts, searchIndex) {
|
|
|
1647
1727
|
const depth = Math.min(Math.max(1, opts.depth ?? 4), 4);
|
|
1648
1728
|
let index = searchIndex ?? null;
|
|
1649
1729
|
let ownedIndex = false;
|
|
1650
|
-
if (!index &&
|
|
1730
|
+
if (!index && searchIndex === void 0) {
|
|
1651
1731
|
try {
|
|
1732
|
+
const dbExists = (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH);
|
|
1733
|
+
if (!dbExists) {
|
|
1734
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(DEFAULT_DB_PATH), { recursive: true });
|
|
1735
|
+
}
|
|
1652
1736
|
index = new SearchIndex(DEFAULT_DB_PATH);
|
|
1653
1737
|
ownedIndex = true;
|
|
1654
|
-
|
|
1738
|
+
if (!dbExists) {
|
|
1739
|
+
index.buildFull(dirs.PROJECTS_DIR);
|
|
1740
|
+
} else {
|
|
1741
|
+
index.updateRecent(dirs.PROJECTS_DIR);
|
|
1742
|
+
}
|
|
1655
1743
|
} catch {
|
|
1656
1744
|
}
|
|
1657
1745
|
}
|
package/package.json
CHANGED