agentel 0.2.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/LICENSE +21 -0
- package/README.md +452 -0
- package/agentlog-spec.md +551 -0
- package/bin/agentlog-recall.js +8 -0
- package/bin/agentlog.js +14 -0
- package/docs/code-reference.md +1108 -0
- package/docs/history-source-handling.md +837 -0
- package/docs/release.md +69 -0
- package/package.json +57 -0
- package/src/archive.js +1130 -0
- package/src/autostart.js +182 -0
- package/src/canonical-events.js +575 -0
- package/src/cli.js +7928 -0
- package/src/collector.js +113 -0
- package/src/commands/logs.js +51 -0
- package/src/commands/server.js +11 -0
- package/src/config.js +240 -0
- package/src/doctor.js +102 -0
- package/src/importers/aider.js +553 -0
- package/src/importers/claude.js +349 -0
- package/src/importers/cline.js +471 -0
- package/src/importers/gemini.js +795 -0
- package/src/importers/providers.js +149 -0
- package/src/importers/shared.js +15 -0
- package/src/importers.js +7063 -0
- package/src/mcp.js +148 -0
- package/src/parser-versions.js +62 -0
- package/src/paths.js +61 -0
- package/src/redaction.js +228 -0
- package/src/repo.js +106 -0
- package/src/search.js +619 -0
- package/src/sources.js +86 -0
- package/src/supervisor.js +217 -0
- package/src/sync.js +677 -0
- package/src/version.js +7 -0
- package/src/web-accounts.js +122 -0
package/src/search.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
const { ensureConversationMarkdown, listSessions, readEvents, readTranscript, sessionHistoryTime } = require("./archive");
|
|
8
|
+
const { EVENT_KINDS, renderEventText } = require("./canonical-events");
|
|
9
|
+
const { loadConfig } = require("./config");
|
|
10
|
+
const { paths, ensureDir, readJson, writeJson } = require("./paths");
|
|
11
|
+
const { canonicalRepo } = require("./repo");
|
|
12
|
+
|
|
13
|
+
function buildIndex(env = process.env) {
|
|
14
|
+
const sessions = listSessions(env);
|
|
15
|
+
const docs = [];
|
|
16
|
+
const df = {};
|
|
17
|
+
let totalLength = 0;
|
|
18
|
+
|
|
19
|
+
for (const session of sessions) {
|
|
20
|
+
if (session.conversationPath && !fs.existsSync(session.conversationPath)) ensureConversationMarkdown(session, env);
|
|
21
|
+
const events = readEvents(session);
|
|
22
|
+
const eventDocs = events.length ? docsForEvents(session, events) : [];
|
|
23
|
+
if (!eventDocs.length) ensureConversationMarkdown(session, env);
|
|
24
|
+
const sourceDocs = eventDocs.length ? eventDocs : docsForTranscript(session, readTranscript(session.transcriptPath));
|
|
25
|
+
for (const sourceDoc of sourceDocs) {
|
|
26
|
+
const indexText = normalizeIndexText(sourceDoc.text);
|
|
27
|
+
if (!indexText) continue;
|
|
28
|
+
for (const chunk of chunkText(indexText)) {
|
|
29
|
+
const tokens = tokenize(chunk);
|
|
30
|
+
if (!tokens.length) continue;
|
|
31
|
+
const tf = {};
|
|
32
|
+
for (const token of tokens) tf[token] = (tf[token] || 0) + 1;
|
|
33
|
+
for (const token of new Set(tokens)) df[token] = (df[token] || 0) + 1;
|
|
34
|
+
totalLength += tokens.length;
|
|
35
|
+
docs.push({
|
|
36
|
+
...sourceDoc,
|
|
37
|
+
id: sourceDoc.id || `${session.sessionId}:${sourceDoc.messageIndex ?? docs.length}:${docs.length}`,
|
|
38
|
+
text: chunk,
|
|
39
|
+
matchedText: chunk,
|
|
40
|
+
tf,
|
|
41
|
+
length: tokens.length
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const index = {
|
|
48
|
+
version: 2,
|
|
49
|
+
builtAt: new Date().toISOString(),
|
|
50
|
+
docCount: docs.length,
|
|
51
|
+
avgDocLength: docs.length ? totalLength / docs.length : 0,
|
|
52
|
+
df,
|
|
53
|
+
docs
|
|
54
|
+
};
|
|
55
|
+
const indexPath = paths(env).index;
|
|
56
|
+
ensureDir(path.dirname(indexPath));
|
|
57
|
+
writeJson(indexPath, index);
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function docsForEvents(session, events) {
|
|
62
|
+
const indexedKinds = new Set([
|
|
63
|
+
EVENT_KINDS.PROMPT_SUBMITTED,
|
|
64
|
+
EVENT_KINDS.RESPONSE_GENERATED,
|
|
65
|
+
EVENT_KINDS.TOOL_CALLED
|
|
66
|
+
]);
|
|
67
|
+
return events
|
|
68
|
+
.filter((event) => indexedKinds.has(event.kind))
|
|
69
|
+
.map((event) => {
|
|
70
|
+
const renderedText = renderEventText(event);
|
|
71
|
+
if (!String(renderedText || "").trim()) return null;
|
|
72
|
+
return {
|
|
73
|
+
id: event.eventId,
|
|
74
|
+
eventId: event.eventId,
|
|
75
|
+
eventKind: event.kind,
|
|
76
|
+
messageIndex: event.messageIndex,
|
|
77
|
+
sessionId: session.sessionId,
|
|
78
|
+
provider: event.provider || session.provider,
|
|
79
|
+
sourceType: event.sourceType || session.sourceType || "",
|
|
80
|
+
repoCanonical: event.repoCanonical || session.repoCanonical || "",
|
|
81
|
+
repoDisplay: displayRepoLabel(session),
|
|
82
|
+
scopeCanonical: event.scopeCanonical || session.scopeCanonical || "",
|
|
83
|
+
cwd: session.cwd || "",
|
|
84
|
+
title: session.title || event.indexed?.title || "",
|
|
85
|
+
eventTitle: event.indexed?.title || "",
|
|
86
|
+
startedAt: session.startedAt,
|
|
87
|
+
occurredAt: event.occurredAt,
|
|
88
|
+
role: event.role || "",
|
|
89
|
+
text: renderedText,
|
|
90
|
+
matchedText: renderedText,
|
|
91
|
+
path: session.conversationPath || session.transcriptPath,
|
|
92
|
+
eventPath: session.eventPath || ""
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function docsForTranscript(session, messages) {
|
|
99
|
+
const docs = [];
|
|
100
|
+
for (const message of messages) {
|
|
101
|
+
docs.push({
|
|
102
|
+
id: `${session.sessionId}:${message.index}:${docs.length}`,
|
|
103
|
+
sessionId: session.sessionId,
|
|
104
|
+
provider: session.provider,
|
|
105
|
+
sourceType: session.sourceType || "",
|
|
106
|
+
repoCanonical: session.repoCanonical || "",
|
|
107
|
+
repoDisplay: displayRepoLabel(session),
|
|
108
|
+
scopeCanonical: session.scopeCanonical || "",
|
|
109
|
+
cwd: session.cwd || "",
|
|
110
|
+
title: session.title || "",
|
|
111
|
+
startedAt: session.startedAt,
|
|
112
|
+
occurredAt: message.timestamp || session.startedAt,
|
|
113
|
+
messageIndex: message.index,
|
|
114
|
+
role: message.role,
|
|
115
|
+
text: message.content,
|
|
116
|
+
matchedText: message.content,
|
|
117
|
+
path: session.transcriptPath
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return docs;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function loadIndex(env = process.env) {
|
|
124
|
+
const indexPath = paths(env).index;
|
|
125
|
+
const existing = readJson(indexPath, null);
|
|
126
|
+
if (existing && existing.version === 2 && !indexIsStale(indexPath, env)) return existing;
|
|
127
|
+
return buildIndex(env);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function searchPastSessions(query, options = {}, env = process.env) {
|
|
131
|
+
try {
|
|
132
|
+
const eventResults = searchIndexedSessions(query, options, env);
|
|
133
|
+
if (eventResults.length) return eventResults;
|
|
134
|
+
} catch {
|
|
135
|
+
// Fall through to the legacy markdown path below.
|
|
136
|
+
}
|
|
137
|
+
return searchMarkdownSessions(query, options, env);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
141
|
+
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
142
|
+
const includeWebChats = Boolean(options.includeWebChats);
|
|
143
|
+
const filter = normalizeSessionFilter(options);
|
|
144
|
+
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
145
|
+
const since = parseSinceFilter(options.since);
|
|
146
|
+
const queryTokens = tokenize(query);
|
|
147
|
+
const phrase = String(query || "").trim().toLowerCase();
|
|
148
|
+
if (!queryTokens.length && !phrase) return [];
|
|
149
|
+
|
|
150
|
+
const sessions = listSessions(env).filter((session) => {
|
|
151
|
+
if (!matchesSessionFilter(session, { ...filter, includeWebChats, since })) return false;
|
|
152
|
+
const conversationPath = ensureConversationMarkdown(session, env);
|
|
153
|
+
const searchPath = conversationPath || session.transcriptPath;
|
|
154
|
+
session._searchPath = searchPath;
|
|
155
|
+
return Boolean(searchPath && fs.existsSync(searchPath));
|
|
156
|
+
});
|
|
157
|
+
if (!sessions.length) return [];
|
|
158
|
+
|
|
159
|
+
const sessionByPath = new Map();
|
|
160
|
+
for (const session of sessions) {
|
|
161
|
+
sessionByPath.set(path.resolve(session._searchPath), session);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const matches = ripgrepMatches(queryTokens, [...sessionByPath.keys()]) || jsLineMatches(queryTokens, [...sessionByPath.keys()]);
|
|
165
|
+
const ranked = [];
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
for (const match of matches) {
|
|
168
|
+
const session = sessionByPath.get(path.resolve(match.path));
|
|
169
|
+
if (!session) continue;
|
|
170
|
+
const key = `${session.sessionId}:${match.line}`;
|
|
171
|
+
if (seen.has(key)) continue;
|
|
172
|
+
seen.add(key);
|
|
173
|
+
const excerptText = readLineWindow(match.path, match.line, 4);
|
|
174
|
+
const lower = excerptText.toLowerCase();
|
|
175
|
+
const matchedTokens = queryTokens.filter((token) => lower.includes(token));
|
|
176
|
+
let score = matchedTokens.length * 5 + (phrase && lower.includes(phrase) ? 10 : 0);
|
|
177
|
+
if (!options.repo && repo && session.repoCanonical === repo) score *= 1.25;
|
|
178
|
+
ranked.push({
|
|
179
|
+
session_id: session.sessionId,
|
|
180
|
+
provider: session.provider,
|
|
181
|
+
source_type: session.sourceType || undefined,
|
|
182
|
+
repo: session.repoCanonical || undefined,
|
|
183
|
+
repo_display: displayRepoLabel(session),
|
|
184
|
+
scope: session.scopeCanonical || undefined,
|
|
185
|
+
cwd: session.cwd || undefined,
|
|
186
|
+
title: session.title || undefined,
|
|
187
|
+
started_at: session.startedAt,
|
|
188
|
+
role: inferRoleFromMarkdown(match.path, match.line),
|
|
189
|
+
excerpt: excerptText.replace(/\s+/g, " ").trim(),
|
|
190
|
+
score: Number(score.toFixed(4)),
|
|
191
|
+
session_link: session._searchPath
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ranked.sort((a, b) => b.score - a.score || String(b.started_at).localeCompare(String(a.started_at)));
|
|
196
|
+
return ranked.slice(0, limit);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function searchIndexedSessions(query, options = {}, env = process.env) {
|
|
200
|
+
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
201
|
+
const includeWebChats = Boolean(options.includeWebChats);
|
|
202
|
+
const filter = normalizeSessionFilter(options);
|
|
203
|
+
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
204
|
+
const since = parseSinceFilter(options.since);
|
|
205
|
+
const index = loadIndex(env);
|
|
206
|
+
const queryTokens = tokenize(query);
|
|
207
|
+
const phrase = String(query || "").trim().toLowerCase();
|
|
208
|
+
if (!queryTokens.length && !phrase) return [];
|
|
209
|
+
|
|
210
|
+
const scored = [];
|
|
211
|
+
for (const doc of index.docs || []) {
|
|
212
|
+
if (!matchesSessionFilter(doc, { ...filter, includeWebChats, since })) continue;
|
|
213
|
+
|
|
214
|
+
let score = bm25Score(doc, queryTokens, index);
|
|
215
|
+
if (phrase && doc.text.toLowerCase().includes(phrase)) score += 2.5;
|
|
216
|
+
if (!options.repo && repo && doc.repoCanonical === repo) score *= 1.25;
|
|
217
|
+
if (score > 0) scored.push({ doc, score });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
scored.sort((a, b) => b.score - a.score || String(b.doc.occurredAt || b.doc.startedAt).localeCompare(String(a.doc.occurredAt || a.doc.startedAt)));
|
|
221
|
+
const bySession = new Map();
|
|
222
|
+
for (const item of scored) {
|
|
223
|
+
if (!bySession.has(item.doc.sessionId)) bySession.set(item.doc.sessionId, item);
|
|
224
|
+
}
|
|
225
|
+
return [...bySession.values()].slice(0, limit).map(({ doc, score }) => ({
|
|
226
|
+
session_id: doc.sessionId,
|
|
227
|
+
provider: doc.provider,
|
|
228
|
+
source_type: doc.sourceType || undefined,
|
|
229
|
+
repo: doc.repoCanonical || undefined,
|
|
230
|
+
repo_display: doc.repoDisplay || doc.repoCanonical || undefined,
|
|
231
|
+
scope: doc.scopeCanonical || undefined,
|
|
232
|
+
cwd: doc.cwd || undefined,
|
|
233
|
+
title: doc.title || undefined,
|
|
234
|
+
started_at: doc.startedAt,
|
|
235
|
+
role: doc.role,
|
|
236
|
+
event_id: doc.eventId || undefined,
|
|
237
|
+
event_kind: doc.eventKind || undefined,
|
|
238
|
+
message_index: doc.messageIndex ?? undefined,
|
|
239
|
+
matched_text: doc.matchedText ? excerpt(doc.matchedText, queryTokens) : undefined,
|
|
240
|
+
excerpt: excerpt(doc.text, queryTokens),
|
|
241
|
+
score: Number(score.toFixed(4)),
|
|
242
|
+
session_link: doc.path
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ripgrepMatches(queryTokens, files) {
|
|
247
|
+
if (!queryTokens.length || !files.length) return null;
|
|
248
|
+
const pattern = queryTokens.map(escapeRegex).join("|");
|
|
249
|
+
const result = spawnSync(
|
|
250
|
+
"rg",
|
|
251
|
+
["--json", "--ignore-case", "--line-number", "-e", pattern, "--", ...files],
|
|
252
|
+
{ encoding: "utf8", maxBuffer: 1024 * 1024 * 50 }
|
|
253
|
+
);
|
|
254
|
+
if (result.error && result.error.code === "ENOENT") return null;
|
|
255
|
+
if (result.status !== 0 && result.status !== 1) return null;
|
|
256
|
+
const matches = [];
|
|
257
|
+
for (const line of String(result.stdout || "").split(/\r?\n/)) {
|
|
258
|
+
if (!line.trim()) continue;
|
|
259
|
+
let event;
|
|
260
|
+
try {
|
|
261
|
+
event = JSON.parse(line);
|
|
262
|
+
} catch {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (event.type !== "match") continue;
|
|
266
|
+
const file = event.data?.path?.text;
|
|
267
|
+
const lineNumber = event.data?.line_number;
|
|
268
|
+
if (file && lineNumber) matches.push({ path: file, line: lineNumber });
|
|
269
|
+
}
|
|
270
|
+
return matches;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function jsLineMatches(queryTokens, files) {
|
|
274
|
+
const matches = [];
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
let lines;
|
|
277
|
+
try {
|
|
278
|
+
lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (let index = 0; index < lines.length; index++) {
|
|
283
|
+
const lower = lines[index].toLowerCase();
|
|
284
|
+
if (queryTokens.some((token) => lower.includes(token))) matches.push({ path: file, line: index + 1 });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return matches;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readLineWindow(file, lineNumber, radius = 2) {
|
|
291
|
+
try {
|
|
292
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
293
|
+
const start = Math.max(0, lineNumber - 1 - radius);
|
|
294
|
+
const end = Math.min(lines.length, lineNumber + radius);
|
|
295
|
+
return lines.slice(start, end).join("\n").trim();
|
|
296
|
+
} catch {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function inferRoleFromMarkdown(file, lineNumber) {
|
|
302
|
+
try {
|
|
303
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
304
|
+
for (let index = Math.min(lineNumber - 1, lines.length - 1); index >= 0; index--) {
|
|
305
|
+
const match = lines[index].match(/^##\s+([A-Za-z_ -]+)\s+-\s+/);
|
|
306
|
+
if (match) return match[1].trim().toLowerCase().replace(/\s+/g, "_");
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// Unknown role.
|
|
310
|
+
}
|
|
311
|
+
return "unknown";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function escapeRegex(value) {
|
|
315
|
+
return String(value).replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function bm25Score(doc, queryTokens, index) {
|
|
319
|
+
const k1 = 1.2;
|
|
320
|
+
const b = 0.75;
|
|
321
|
+
const n = Math.max(1, index.docCount || 1);
|
|
322
|
+
const avgdl = Math.max(1, index.avgDocLength || 1);
|
|
323
|
+
let score = 0;
|
|
324
|
+
for (const token of queryTokens) {
|
|
325
|
+
const tf = doc.tf?.[token] || 0;
|
|
326
|
+
if (!tf) continue;
|
|
327
|
+
const df = index.df?.[token] || 0;
|
|
328
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
329
|
+
score += idf * ((tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (doc.length / avgdl))));
|
|
330
|
+
}
|
|
331
|
+
return score;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function chunkText(text, maxTokens = 220, overlap = 40) {
|
|
335
|
+
const words = String(text || "").split(/\s+/).filter(Boolean);
|
|
336
|
+
if (words.length <= maxTokens) return [words.join(" ")].filter(Boolean);
|
|
337
|
+
const chunks = [];
|
|
338
|
+
for (let start = 0; start < words.length; start += maxTokens - overlap) {
|
|
339
|
+
chunks.push(words.slice(start, start + maxTokens).join(" "));
|
|
340
|
+
}
|
|
341
|
+
return chunks;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function tokenize(text) {
|
|
345
|
+
return String(text || "")
|
|
346
|
+
.toLowerCase()
|
|
347
|
+
.match(/[a-z0-9_./:-]{2,}/g)?.filter((token) => token.length <= MAX_INDEX_TOKEN_LENGTH && !STOP_WORDS.has(token)) || [];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizeIndexText(text) {
|
|
351
|
+
let value = String(text || "");
|
|
352
|
+
if (!value.trim()) return "";
|
|
353
|
+
value = value.replace(/data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=_-]{512,}/gi, "[image data omitted]");
|
|
354
|
+
value = value.replace(/"data"\s*:\s*"[a-z0-9+/=_-]{512,}"/gi, '"data":"[binary data omitted]"');
|
|
355
|
+
value = value.replace(/\b[a-z0-9+/=_-]{2048,}\b/gi, "[encoded data omitted]");
|
|
356
|
+
if (value.length > MAX_INDEX_TEXT_CHARS) value = `${value.slice(0, MAX_INDEX_TEXT_CHARS)} [truncated]`;
|
|
357
|
+
return value.trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function excerpt(text, queryTokens) {
|
|
361
|
+
const value = String(text || "").replace(/\s+/g, " ").trim();
|
|
362
|
+
if (value.length <= 360) return value;
|
|
363
|
+
const lower = value.toLowerCase();
|
|
364
|
+
let position = -1;
|
|
365
|
+
for (const token of queryTokens) {
|
|
366
|
+
position = lower.indexOf(token);
|
|
367
|
+
if (position >= 0) break;
|
|
368
|
+
}
|
|
369
|
+
const start = Math.max(0, position - 120);
|
|
370
|
+
const end = Math.min(value.length, start + 360);
|
|
371
|
+
return `${start > 0 ? "..." : ""}${value.slice(start, end)}${end < value.length ? "..." : ""}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function inferCallingRepo(cwd) {
|
|
375
|
+
try {
|
|
376
|
+
return canonicalRepo(cwd).key;
|
|
377
|
+
} catch {
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function listHistorySessions(options = {}, env = process.env) {
|
|
383
|
+
const since = parseSinceFilter(options.since);
|
|
384
|
+
const includeWebChats = options.includeWebChats !== false;
|
|
385
|
+
const filter = normalizeSessionFilter(options);
|
|
386
|
+
return listSessions(env)
|
|
387
|
+
.filter((session) => matchesSessionFilter(session, { ...filter, includeWebChats, since }))
|
|
388
|
+
.map(historySessionSummary)
|
|
389
|
+
.sort((a, b) => String(historySessionSortTime(b)).localeCompare(String(historySessionSortTime(a))));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function historySessionSummary(session) {
|
|
393
|
+
const time = sessionHistoryTime(session);
|
|
394
|
+
return {
|
|
395
|
+
session_id: session.sessionId,
|
|
396
|
+
provider: session.provider,
|
|
397
|
+
source_type: session.sourceType || undefined,
|
|
398
|
+
repo: session.repoCanonical || undefined,
|
|
399
|
+
repo_display: displayRepoLabel(session),
|
|
400
|
+
scope: session.scopeCanonical || undefined,
|
|
401
|
+
chat_virtual_repo: session.chatVirtualRepo || undefined,
|
|
402
|
+
chat_display_path: session.chatDisplayPath || undefined,
|
|
403
|
+
chat_account_id: session.chatAccountId || undefined,
|
|
404
|
+
chat_username: session.chatUsername || undefined,
|
|
405
|
+
chat_display_name: session.chatDisplayName || undefined,
|
|
406
|
+
chat_project_path: session.chatProjectPath || undefined,
|
|
407
|
+
conversation_kind: session.conversationKind || undefined,
|
|
408
|
+
pinned: Boolean(session.pinned) || undefined,
|
|
409
|
+
cwd: session.cwd || undefined,
|
|
410
|
+
title: session.title || undefined,
|
|
411
|
+
started_at: time.startedAt || undefined,
|
|
412
|
+
ended_at: time.endedAt || undefined,
|
|
413
|
+
time_status: time.status || undefined,
|
|
414
|
+
archived_at: session.importedAt || undefined,
|
|
415
|
+
messages: session.messageCount,
|
|
416
|
+
user_messages: Number.isFinite(Number(session.userMessageCount)) ? Number(session.userMessageCount) : undefined,
|
|
417
|
+
usage: session.usage || undefined,
|
|
418
|
+
models: session.models || undefined,
|
|
419
|
+
conversation: session.conversationPath,
|
|
420
|
+
transcript: session.transcriptPath
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function historySessionSortTime(session) {
|
|
425
|
+
if (session.time_status === "recovered-time-unknown") return "";
|
|
426
|
+
return session.ended_at || session.started_at || "";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function displayRepoLabel(session) {
|
|
430
|
+
const chat = displayChatScopeLabel(session);
|
|
431
|
+
if (chat) return chat;
|
|
432
|
+
const repo = session.repoCanonical || session.repo || "";
|
|
433
|
+
if (repo && !repo.startsWith("path:")) return repo;
|
|
434
|
+
if (session.scopeCanonical || session.scope) return displayScopeLabel(session.scopeCanonical || session.scope);
|
|
435
|
+
const cwd = session.cwd || "";
|
|
436
|
+
if (cwd) return compactHomePath(cwd);
|
|
437
|
+
return repo || "unknown";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function displayScopeLabel(scope) {
|
|
441
|
+
return {
|
|
442
|
+
"claude-desktop/uncategorized": "Claude Desktop (uncategorized)",
|
|
443
|
+
"claude-code-desktop/uncategorized": "Claude Code Desktop (uncategorized)"
|
|
444
|
+
}[scope] || scope;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function displayChatScopeLabel(session) {
|
|
448
|
+
if (!isWebChatSession(session)) return "";
|
|
449
|
+
if (session.chatDisplayPath) return session.chatDisplayPath;
|
|
450
|
+
const display = session.chatDisplayName || session.chatUsername || session.chatAccountId || "account";
|
|
451
|
+
const project = session.chatProjectPath || chatProjectPathFromScope(session.scopeCanonical || session.scope || "");
|
|
452
|
+
return project ? `/${display}/${project}` : `/${display}/`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isWebChatSession(session) {
|
|
456
|
+
const provider = session?.provider || "";
|
|
457
|
+
const scope = session?.scopeCanonical || session?.scope || "";
|
|
458
|
+
return provider === "chatgpt" || provider === "claude_web" || /^\[(chatgpt|claude)\]conversations\//.test(scope);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function chatProjectPathFromScope(scope) {
|
|
462
|
+
const parts = String(scope || "").split("/").filter(Boolean);
|
|
463
|
+
return parts.length > 2 ? parts.slice(2).join("/") : "";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function compactHomePath(value) {
|
|
467
|
+
const resolved = path.resolve(String(value || ""));
|
|
468
|
+
const home = os.homedir();
|
|
469
|
+
if (resolved === home) return "~";
|
|
470
|
+
if (resolved.startsWith(`${home}${path.sep}`)) return `~/${resolved.slice(home.length + 1)}`;
|
|
471
|
+
return resolved;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function listRecentSessions(limit = 20, options = {}, env = process.env) {
|
|
475
|
+
return listHistorySessions(options, env).slice(0, limit);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function normalizeSessionFilter(options = {}) {
|
|
479
|
+
const providerInput = options.provider || options.source || "";
|
|
480
|
+
const normalizedProvider = normalizeProviderFilter(providerInput);
|
|
481
|
+
const explicitSourceType = options.sourceType || options.source_type || "";
|
|
482
|
+
return {
|
|
483
|
+
provider: normalizedProvider.provider,
|
|
484
|
+
sourceType: explicitSourceType || normalizedProvider.sourceType || "",
|
|
485
|
+
sourceTypes: explicitSourceType ? [explicitSourceType] : normalizedProvider.sourceTypes || [],
|
|
486
|
+
repo: options.repo || ""
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizeProviderFilter(value) {
|
|
491
|
+
const key = String(value || "").trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
492
|
+
const aliases = {
|
|
493
|
+
claude: { provider: "claude_code" },
|
|
494
|
+
claude_code: { provider: "claude_code" },
|
|
495
|
+
claude_cli: { provider: "claude_code" },
|
|
496
|
+
claude_desktop: { provider: "claude_desktop" },
|
|
497
|
+
claude_code_desktop: { provider: "claude_desktop", sourceType: "claude-code-desktop-metadata", sourceTypes: ["claude-code-desktop-metadata"] },
|
|
498
|
+
claude_workspace: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
499
|
+
claude_workspace_desktop: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
500
|
+
claude_sdk: { provider: "claude_sdk" },
|
|
501
|
+
codex: { provider: "codex" },
|
|
502
|
+
codex_cli: { provider: "codex", sourceType: "codex-cli-history", sourceTypes: ["codex-cli-history", "cli-history"] },
|
|
503
|
+
codex_desktop: { provider: "codex", sourceType: "codex-desktop-history", sourceTypes: ["codex-desktop-history"] },
|
|
504
|
+
cursor: { provider: "cursor" },
|
|
505
|
+
cline: { provider: "cline", sourceType: "cline-task-history", sourceTypes: ["cline-task-history"] },
|
|
506
|
+
opencode: { provider: "opencode", sourceType: "opencode-history", sourceTypes: ["opencode-history"] },
|
|
507
|
+
aider: { provider: "aider", sourceType: "aider-chat-history", sourceTypes: ["aider-chat-history"] },
|
|
508
|
+
devin: { provider: "devin" },
|
|
509
|
+
devin_cli: { provider: "devin", sourceType: "devin-cli-history", sourceTypes: ["devin-cli-history"] },
|
|
510
|
+
gemini: { provider: "gemini_cli" },
|
|
511
|
+
gemini_cli: { provider: "gemini_cli" },
|
|
512
|
+
windsurf: { provider: "windsurf" },
|
|
513
|
+
antigravity: { provider: "antigravity" },
|
|
514
|
+
chatgpt: { provider: "chatgpt" },
|
|
515
|
+
claude_web: { provider: "claude_web" },
|
|
516
|
+
claude_ai: { provider: "claude_web" }
|
|
517
|
+
};
|
|
518
|
+
return aliases[key] || { provider: key };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function matchesSessionFilter(session, filter) {
|
|
522
|
+
if (filter.provider && session.provider !== filter.provider) return false;
|
|
523
|
+
const sourceTypes = filter.sourceTypes?.length ? filter.sourceTypes : filter.sourceType ? [filter.sourceType] : [];
|
|
524
|
+
if (sourceTypes.length && !sourceTypes.includes(session.sourceType)) return false;
|
|
525
|
+
if (!filter.includeWebChats && session.scopeCanonical && session.storageScope !== "local") return false;
|
|
526
|
+
if (filter.repo && !matchesRepoFilter(session, filter.repo)) return false;
|
|
527
|
+
if (filter.since) {
|
|
528
|
+
const time = sessionHistoryTime(session);
|
|
529
|
+
const comparable = time.startedAt || time.endedAt;
|
|
530
|
+
if (!comparable || new Date(comparable) < filter.since) return false;
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function matchesRepoFilter(session, repoFilter) {
|
|
536
|
+
const wanted = String(repoFilter || "").toLowerCase();
|
|
537
|
+
if (!wanted) return true;
|
|
538
|
+
return [session.repoCanonical, session.repo, session.scopeCanonical, session.scope, session.chatVirtualRepo, session.chatDisplayPath, session.chatDisplayName, session.chatUsername, session.cwd, displayRepoLabel(session)]
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.map((value) => String(value).toLowerCase())
|
|
541
|
+
.some((value) => value === wanted || value.includes(wanted));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function reindexIfNeeded(env = process.env) {
|
|
545
|
+
const cfg = loadConfig(env);
|
|
546
|
+
if (cfg.index.paused) return { paused: true, index: readJson(paths(env).index, null) };
|
|
547
|
+
const indexPath = paths(env).index;
|
|
548
|
+
try {
|
|
549
|
+
const stat = fs.statSync(indexPath);
|
|
550
|
+
if (Date.now() - stat.mtimeMs < 10 * 60 * 1000) return { paused: false, index: readJson(indexPath, null) };
|
|
551
|
+
} catch {
|
|
552
|
+
// Missing index: build below.
|
|
553
|
+
}
|
|
554
|
+
return { paused: false, index: buildIndex(env) };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function indexIsStale(indexPath, env = process.env) {
|
|
558
|
+
let indexStat;
|
|
559
|
+
try {
|
|
560
|
+
indexStat = fs.statSync(indexPath);
|
|
561
|
+
} catch {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
for (const session of listSessions(env)) {
|
|
565
|
+
try {
|
|
566
|
+
if (fs.statSync(session.transcriptPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
567
|
+
if (fs.statSync(session.metadataPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
568
|
+
if (session.eventPath && fs.existsSync(session.eventPath) && fs.statSync(session.eventPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
569
|
+
} catch {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function parseSinceFilter(value) {
|
|
577
|
+
if (!value || value === "all") return null;
|
|
578
|
+
const match = String(value).match(/^(\d+)([dhm])$/);
|
|
579
|
+
if (!match) {
|
|
580
|
+
const date = new Date(value);
|
|
581
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
582
|
+
}
|
|
583
|
+
const amount = Number(match[1]);
|
|
584
|
+
const unit = match[2];
|
|
585
|
+
const ms = unit === "d" ? amount * 86400000 : unit === "h" ? amount * 3600000 : amount * 60000;
|
|
586
|
+
return new Date(Date.now() - ms);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const MAX_INDEX_TEXT_CHARS = 50000;
|
|
590
|
+
const MAX_INDEX_TOKEN_LENGTH = 128;
|
|
591
|
+
|
|
592
|
+
const STOP_WORDS = new Set([
|
|
593
|
+
"the",
|
|
594
|
+
"and",
|
|
595
|
+
"for",
|
|
596
|
+
"that",
|
|
597
|
+
"this",
|
|
598
|
+
"with",
|
|
599
|
+
"from",
|
|
600
|
+
"you",
|
|
601
|
+
"are",
|
|
602
|
+
"was",
|
|
603
|
+
"were",
|
|
604
|
+
"have",
|
|
605
|
+
"has",
|
|
606
|
+
"had"
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
buildIndex,
|
|
611
|
+
chunkText,
|
|
612
|
+
listHistorySessions,
|
|
613
|
+
listRecentSessions,
|
|
614
|
+
loadIndex,
|
|
615
|
+
reindexIfNeeded,
|
|
616
|
+
sessionHistoryTime,
|
|
617
|
+
searchPastSessions,
|
|
618
|
+
tokenize
|
|
619
|
+
};
|
package/src/sources.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SOURCE_GROUPS = [
|
|
4
|
+
{
|
|
5
|
+
group: "OpenAI",
|
|
6
|
+
sources: [
|
|
7
|
+
{ source: "codex-cli", provider: "codex", sourceType: "codex-cli-history", label: "Codex CLI" },
|
|
8
|
+
{ source: "codex-desktop", provider: "codex", sourceType: "codex-desktop-history", label: "Codex Desktop" },
|
|
9
|
+
{ source: "chatgpt", provider: "chatgpt", label: "ChatGPT" }
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
group: "Anthropic",
|
|
14
|
+
sources: [
|
|
15
|
+
{ source: "claude", provider: "claude_code", label: "Claude Code CLI" },
|
|
16
|
+
{ source: "claude-code-desktop", provider: "claude_desktop", sourceType: "claude-code-desktop-metadata", label: "Claude Code Desktop" },
|
|
17
|
+
{ source: "claude-workspace", provider: "claude_desktop", sourceType: "claude-workspace-desktop", label: "Claude Workspace" },
|
|
18
|
+
{ source: "claude-web", provider: "claude_web", label: "Claude.ai" },
|
|
19
|
+
{ source: "claude-sdk", provider: "claude_sdk", label: "Claude SDK jobs" }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
group: "Google",
|
|
24
|
+
sources: [
|
|
25
|
+
{ source: "gemini-cli", provider: "gemini_cli", label: "Gemini CLI" },
|
|
26
|
+
{ source: "antigravity", provider: "antigravity", label: "Antigravity" }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
group: "Cognition",
|
|
31
|
+
sources: [
|
|
32
|
+
{ source: "devin-cli", provider: "devin", sourceType: "devin-cli-history", label: "Devin CLI" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
group: "Other",
|
|
37
|
+
sources: [
|
|
38
|
+
{ source: "cursor", provider: "cursor", label: "Cursor" },
|
|
39
|
+
{ source: "cline", provider: "cline", sourceType: "cline-task-history", label: "Cline" },
|
|
40
|
+
{ source: "opencode", provider: "opencode", sourceType: "opencode-history", label: "OpenCode" },
|
|
41
|
+
{ source: "aider", provider: "aider", sourceType: "aider-chat-history", label: "Aider" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const DISABLED_IMPORT_SOURCES = new Set(["windsurf"]);
|
|
47
|
+
|
|
48
|
+
const DISABLED_IMPORT_SOURCE_MESSAGES = {
|
|
49
|
+
windsurf:
|
|
50
|
+
"Windsurf import is disabled because current Cascade transcripts are encrypted binary stores. agentlog can detect those sessions, but cannot archive readable conversation text yet."
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const IMPORT_SOURCE_ORDER = [
|
|
54
|
+
"codex-cli",
|
|
55
|
+
"codex-desktop",
|
|
56
|
+
"claude",
|
|
57
|
+
"claude-code-desktop",
|
|
58
|
+
"claude-workspace",
|
|
59
|
+
"gemini-cli",
|
|
60
|
+
"antigravity",
|
|
61
|
+
"devin-cli",
|
|
62
|
+
"cursor",
|
|
63
|
+
"cline",
|
|
64
|
+
"opencode",
|
|
65
|
+
"aider"
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const HISTORY_PROVIDER_OPTIONS = SOURCE_GROUPS.flatMap((group) => group.sources);
|
|
69
|
+
|
|
70
|
+
function enabledImportSources(sources) {
|
|
71
|
+
return (sources || []).filter((source) => !DISABLED_IMPORT_SOURCES.has(source));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sourceLabel(source) {
|
|
75
|
+
return HISTORY_PROVIDER_OPTIONS.find((item) => item.source === source)?.label || source;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
DISABLED_IMPORT_SOURCE_MESSAGES,
|
|
80
|
+
DISABLED_IMPORT_SOURCES,
|
|
81
|
+
HISTORY_PROVIDER_OPTIONS,
|
|
82
|
+
IMPORT_SOURCE_ORDER,
|
|
83
|
+
SOURCE_GROUPS,
|
|
84
|
+
enabledImportSources,
|
|
85
|
+
sourceLabel
|
|
86
|
+
};
|