codesesh 0.1.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/.turbo/turbo-build.log +16 -0
- package/dist/chunk-H3D3GNQK.js +3574 -0
- package/dist/chunk-H3D3GNQK.js.map +1 -0
- package/dist/dist-KGHAVLGF.js +52 -0
- package/dist/dist-KGHAVLGF.js.map +1 -0
- package/dist/index.js +325 -0
- package/dist/index.js.map +1 -0
- package/dist/web/assets/index-Cbmq6KvN.js +67 -0
- package/dist/web/assets/index-vSqmWltx.css +2 -0
- package/dist/web/icon/agent/antigravity.svg +1 -0
- package/dist/web/icon/agent/claudecode.svg +1 -0
- package/dist/web/icon/agent/codex.svg +1 -0
- package/dist/web/icon/agent/cursor.svg +12 -0
- package/dist/web/icon/agent/kilocode.svg +1 -0
- package/dist/web/icon/agent/kimi.svg +1 -0
- package/dist/web/icon/agent/opencode.svg +1 -0
- package/dist/web/index.html +18 -0
- package/dist/web/logo.svg +29 -0
- package/package.json +34 -0
- package/scripts/copy-web-dist.js +31 -0
- package/src/api/__tests__/handlers.test.ts +191 -0
- package/src/api/__tests__/routes.test.ts +16 -0
- package/src/api/handlers.ts +76 -0
- package/src/api/routes.ts +13 -0
- package/src/commands/serve.ts +78 -0
- package/src/index.ts +203 -0
- package/src/output.ts +47 -0
- package/src/server.ts +56 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,3574 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/dist/index.mjs
|
|
4
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
5
|
+
import { join as join2, basename as basename2 } from "path";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { homedir, platform } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { basename } from "path";
|
|
11
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
12
|
+
import { join as join3 } from "path";
|
|
13
|
+
import { createRequire } from "module";
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
16
|
+
import { join as join4, basename as basename3, dirname } from "path";
|
|
17
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
18
|
+
import { join as join5, basename as basename4 } from "path";
|
|
19
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
|
|
20
|
+
import { join as join6, normalize } from "path";
|
|
21
|
+
import { resolve, sep } from "path";
|
|
22
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync, mkdirSync } from "fs";
|
|
23
|
+
import { join as join7 } from "path";
|
|
24
|
+
import { homedir as homedir2 } from "os";
|
|
25
|
+
var registrations = [];
|
|
26
|
+
function registerAgent(reg) {
|
|
27
|
+
registrations.push(reg);
|
|
28
|
+
}
|
|
29
|
+
function createRegisteredAgents() {
|
|
30
|
+
return registrations.map((r) => r.create());
|
|
31
|
+
}
|
|
32
|
+
function getRegisteredAgents() {
|
|
33
|
+
return registrations;
|
|
34
|
+
}
|
|
35
|
+
function getAgentInfoMap(sessionsByAgent) {
|
|
36
|
+
return registrations.map((r) => ({
|
|
37
|
+
name: r.name,
|
|
38
|
+
displayName: r.displayName,
|
|
39
|
+
icon: r.icon,
|
|
40
|
+
count: sessionsByAgent[r.name] ?? 0
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function getAgentByName(name) {
|
|
44
|
+
return registrations.find((r) => r.name === name);
|
|
45
|
+
}
|
|
46
|
+
var BaseAgent = class {
|
|
47
|
+
getUri(sessionId) {
|
|
48
|
+
return `${this.name}://${sessionId}`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function envPath(name) {
|
|
52
|
+
const value = process.env[name];
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function firstExisting(...paths) {
|
|
57
|
+
for (const p of paths) {
|
|
58
|
+
if (existsSync(p)) return p;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function getDataHome() {
|
|
63
|
+
const xdg = envPath("XDG_DATA_HOME");
|
|
64
|
+
if (xdg) return xdg;
|
|
65
|
+
const p = platform();
|
|
66
|
+
if (p === "win32") {
|
|
67
|
+
return envPath("LOCALAPPDATA") ?? envPath("APPDATA") ?? join(homedir(), "AppData", "Local");
|
|
68
|
+
}
|
|
69
|
+
return join(homedir(), ".local", "share");
|
|
70
|
+
}
|
|
71
|
+
function resolveProviderRoots() {
|
|
72
|
+
const home = homedir();
|
|
73
|
+
return {
|
|
74
|
+
codexRoot: envPath("CODEX_HOME") ?? join(home, ".codex"),
|
|
75
|
+
claudeRoot: envPath("CLAUDE_CONFIG_DIR") ?? join(home, ".claude"),
|
|
76
|
+
kimiRoot: envPath("KIMI_SHARE_DIR") ?? join(home, ".kimi"),
|
|
77
|
+
opencodeRoot: join(getDataHome(), "opencode")
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getCursorDataPath() {
|
|
81
|
+
const override = envPath("CURSOR_DATA_PATH");
|
|
82
|
+
if (override) return override;
|
|
83
|
+
const p = platform();
|
|
84
|
+
if (p === "darwin") {
|
|
85
|
+
return firstExisting(join(homedir(), "Library", "Application Support", "Cursor", "User"));
|
|
86
|
+
}
|
|
87
|
+
if (p === "linux") {
|
|
88
|
+
const xdg = envPath("XDG_CONFIG_HOME") ?? join(homedir(), ".config");
|
|
89
|
+
return firstExisting(join(xdg, "Cursor", "User"));
|
|
90
|
+
}
|
|
91
|
+
if (p === "win32") {
|
|
92
|
+
const appData = envPath("APPDATA") ?? join(homedir(), "AppData", "Roaming");
|
|
93
|
+
return firstExisting(join(appData, "Cursor", "User"));
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function* parseJsonlLines(content) {
|
|
98
|
+
for (const line of content.split("\n")) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed) continue;
|
|
101
|
+
try {
|
|
102
|
+
yield JSON.parse(trimmed);
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function readJsonlFile(filePath) {
|
|
108
|
+
const content = readFileSync(filePath, "utf-8");
|
|
109
|
+
return parseJsonlLines(content);
|
|
110
|
+
}
|
|
111
|
+
var TITLE_MAX_LENGTH = 100;
|
|
112
|
+
var UNTITLED_SESSION = "Untitled Session";
|
|
113
|
+
function normalizeTitleText(text) {
|
|
114
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
115
|
+
if (!cleaned) return null;
|
|
116
|
+
return cleaned.slice(0, TITLE_MAX_LENGTH);
|
|
117
|
+
}
|
|
118
|
+
function basenameTitle(path) {
|
|
119
|
+
if (!path) return null;
|
|
120
|
+
const normalized = path.trim().replace(/[/\\]+$/, "");
|
|
121
|
+
if (!normalized) return null;
|
|
122
|
+
const name = basename(normalized).trim();
|
|
123
|
+
return name || null;
|
|
124
|
+
}
|
|
125
|
+
function resolveSessionTitle(explicit, message, directory) {
|
|
126
|
+
for (const candidate of [explicit, message, directory]) {
|
|
127
|
+
if (candidate) {
|
|
128
|
+
const normalized = normalizeTitleText(candidate);
|
|
129
|
+
if (normalized) return normalized;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return UNTITLED_SESSION;
|
|
133
|
+
}
|
|
134
|
+
var PerfTracer = class {
|
|
135
|
+
rootMarkers = [];
|
|
136
|
+
activeStack = [];
|
|
137
|
+
enabled = false;
|
|
138
|
+
enable() {
|
|
139
|
+
this.enabled = true;
|
|
140
|
+
}
|
|
141
|
+
start(name) {
|
|
142
|
+
const marker = {
|
|
143
|
+
name,
|
|
144
|
+
startTime: performance.now(),
|
|
145
|
+
children: []
|
|
146
|
+
};
|
|
147
|
+
if (!this.enabled) return marker;
|
|
148
|
+
const parent = this.activeStack[this.activeStack.length - 1];
|
|
149
|
+
if (parent) {
|
|
150
|
+
marker.parent = parent;
|
|
151
|
+
parent.children.push(marker);
|
|
152
|
+
} else {
|
|
153
|
+
this.rootMarkers.push(marker);
|
|
154
|
+
}
|
|
155
|
+
this.activeStack.push(marker);
|
|
156
|
+
return marker;
|
|
157
|
+
}
|
|
158
|
+
end(marker) {
|
|
159
|
+
if (!this.enabled) return;
|
|
160
|
+
const target = marker ?? this.activeStack[this.activeStack.length - 1];
|
|
161
|
+
if (!target) return;
|
|
162
|
+
target.endTime = performance.now();
|
|
163
|
+
target.duration = target.endTime - target.startTime;
|
|
164
|
+
while (this.activeStack.length > 0) {
|
|
165
|
+
const popped = this.activeStack.pop();
|
|
166
|
+
if (popped === target) break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
measure(name, fn) {
|
|
170
|
+
const marker = this.start(name);
|
|
171
|
+
try {
|
|
172
|
+
return fn();
|
|
173
|
+
} finally {
|
|
174
|
+
this.end(marker);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async measureAsync(name, fn) {
|
|
178
|
+
const marker = this.start(name);
|
|
179
|
+
try {
|
|
180
|
+
return await fn();
|
|
181
|
+
} finally {
|
|
182
|
+
this.end(marker);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
getReport() {
|
|
186
|
+
if (!this.enabled) return "Performance tracing disabled";
|
|
187
|
+
const lines = [];
|
|
188
|
+
lines.push("\n=== Performance Report ===\n");
|
|
189
|
+
for (const marker of this.rootMarkers) {
|
|
190
|
+
this.formatMarker(marker, 0, lines);
|
|
191
|
+
}
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
formatMarker(marker, depth, lines) {
|
|
195
|
+
const indent = " ".repeat(depth);
|
|
196
|
+
const duration = marker.duration?.toFixed(2) ?? "?";
|
|
197
|
+
lines.push(`${indent}${marker.name}: ${duration}ms`);
|
|
198
|
+
for (const child of marker.children) {
|
|
199
|
+
this.formatMarker(child, depth + 1, lines);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
reset() {
|
|
203
|
+
this.rootMarkers = [];
|
|
204
|
+
this.activeStack = [];
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var perf = new PerfTracer();
|
|
208
|
+
function parseTimestampMs(data) {
|
|
209
|
+
const raw = String(data["timestamp"] ?? "").trim();
|
|
210
|
+
if (!raw) return 0;
|
|
211
|
+
try {
|
|
212
|
+
return new Date(raw.includes("Z") ? raw : raw + "Z").getTime();
|
|
213
|
+
} catch {
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function normalizeTitleText2(text) {
|
|
218
|
+
const line = text.split("\n").find((l) => l.trim());
|
|
219
|
+
return line?.trim().slice(0, 80) || "";
|
|
220
|
+
}
|
|
221
|
+
var ClaudeCodeAgent = class extends BaseAgent {
|
|
222
|
+
name = "claudecode";
|
|
223
|
+
displayName = "Claude Code";
|
|
224
|
+
basePath = null;
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
226
|
+
sessionsIndexCache = {};
|
|
227
|
+
sessionMetaMap = /* @__PURE__ */ new Map();
|
|
228
|
+
findBasePath() {
|
|
229
|
+
const roots = resolveProviderRoots();
|
|
230
|
+
return firstExisting(join2(roots.claudeRoot, "projects"), "data/claudecode");
|
|
231
|
+
}
|
|
232
|
+
isAvailable() {
|
|
233
|
+
this.basePath = this.findBasePath();
|
|
234
|
+
if (!this.basePath) return false;
|
|
235
|
+
try {
|
|
236
|
+
for (const entry of readdirSync(this.basePath)) {
|
|
237
|
+
const dir = join2(this.basePath, entry);
|
|
238
|
+
if (existsSync2(dir) && readdirSync(dir).some((f) => f.endsWith(".jsonl"))) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
scan() {
|
|
247
|
+
if (!this.basePath) return [];
|
|
248
|
+
const scanMarker = perf.start("claudecode:scan");
|
|
249
|
+
const heads = [];
|
|
250
|
+
const listMarker = perf.start("listProjectDirs");
|
|
251
|
+
const projectDirs = this.listProjectDirs();
|
|
252
|
+
perf.end(listMarker);
|
|
253
|
+
for (const projectDir of projectDirs) {
|
|
254
|
+
const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
|
|
255
|
+
const files = this.listJsonlFiles(projectDir);
|
|
256
|
+
perf.end(fileMarker);
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
try {
|
|
259
|
+
const parseMarker = perf.start(`parseSessionHead:${basename2(file)}`);
|
|
260
|
+
const head = this.parseSessionHead(file, projectDir);
|
|
261
|
+
perf.end(parseMarker);
|
|
262
|
+
if (head) {
|
|
263
|
+
heads.push(head);
|
|
264
|
+
this.sessionMetaMap.set(head.id, {
|
|
265
|
+
id: head.id,
|
|
266
|
+
title: head.title,
|
|
267
|
+
sourcePath: file,
|
|
268
|
+
directory: head.directory,
|
|
269
|
+
model: head.stats.total_tokens ? "unknown" : void 0,
|
|
270
|
+
messageCount: head.stats.message_count,
|
|
271
|
+
createdAt: head.time_created,
|
|
272
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
perf.end(scanMarker);
|
|
280
|
+
return heads;
|
|
281
|
+
}
|
|
282
|
+
getSessionMetaMap() {
|
|
283
|
+
return this.sessionMetaMap;
|
|
284
|
+
}
|
|
285
|
+
setSessionMetaMap(meta) {
|
|
286
|
+
this.sessionMetaMap = meta;
|
|
287
|
+
}
|
|
288
|
+
getSessionData(sessionId) {
|
|
289
|
+
const meta = this.sessionMetaMap.get(sessionId);
|
|
290
|
+
if (!meta) {
|
|
291
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
292
|
+
}
|
|
293
|
+
if (!existsSync2(meta.sourcePath)) {
|
|
294
|
+
throw new Error(`Session file missing: ${meta.sourcePath}`);
|
|
295
|
+
}
|
|
296
|
+
const content = readFileSync2(meta.sourcePath, "utf-8");
|
|
297
|
+
const messages = [];
|
|
298
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
299
|
+
const ignoredToolCallIds = /* @__PURE__ */ new Set();
|
|
300
|
+
const assistantUuidToToolCalls = /* @__PURE__ */ new Map();
|
|
301
|
+
const assistantState = {
|
|
302
|
+
currentIndex: null,
|
|
303
|
+
latestTextIndex: null
|
|
304
|
+
};
|
|
305
|
+
let totalCost = 0;
|
|
306
|
+
let totalInputTokens = 0;
|
|
307
|
+
let totalOutputTokens = 0;
|
|
308
|
+
for (const record of parseJsonlLines(content)) {
|
|
309
|
+
try {
|
|
310
|
+
this.convertRecord(
|
|
311
|
+
record,
|
|
312
|
+
messages,
|
|
313
|
+
pendingToolCalls,
|
|
314
|
+
ignoredToolCallIds,
|
|
315
|
+
assistantUuidToToolCalls,
|
|
316
|
+
assistantState
|
|
317
|
+
);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const msg of messages) {
|
|
322
|
+
totalCost += msg.cost ?? 0;
|
|
323
|
+
totalInputTokens += msg.tokens?.input ?? 0;
|
|
324
|
+
totalOutputTokens += msg.tokens?.output ?? 0;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
id: meta.id,
|
|
328
|
+
title: meta.title,
|
|
329
|
+
slug: `claudecode/${meta.id}`,
|
|
330
|
+
directory: meta.directory,
|
|
331
|
+
version: void 0,
|
|
332
|
+
time_created: meta.createdAt,
|
|
333
|
+
time_updated: meta.updatedAt,
|
|
334
|
+
stats: {
|
|
335
|
+
message_count: messages.length,
|
|
336
|
+
total_input_tokens: totalInputTokens,
|
|
337
|
+
total_output_tokens: totalOutputTokens,
|
|
338
|
+
total_cost: totalCost
|
|
339
|
+
},
|
|
340
|
+
messages
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 检测文件系统变更
|
|
345
|
+
* 通过比较文件修改时间判断是否有新内容
|
|
346
|
+
*/
|
|
347
|
+
checkForChanges(sinceTimestamp, cachedSessions) {
|
|
348
|
+
if (!this.basePath) {
|
|
349
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
350
|
+
}
|
|
351
|
+
const changedIds = [];
|
|
352
|
+
for (const session of cachedSessions) {
|
|
353
|
+
const meta = this.sessionMetaMap.get(session.id);
|
|
354
|
+
if (!meta) continue;
|
|
355
|
+
try {
|
|
356
|
+
const stat = statSync(meta.sourcePath);
|
|
357
|
+
if (stat.mtimeMs > sinceTimestamp) {
|
|
358
|
+
changedIds.push(session.id);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
changedIds.push(session.id);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
let totalFiles = 0;
|
|
366
|
+
for (const dir of this.listProjectDirs()) {
|
|
367
|
+
totalFiles += this.listJsonlFiles(dir).length;
|
|
368
|
+
}
|
|
369
|
+
const hasNewFiles = totalFiles > cachedSessions.length;
|
|
370
|
+
return {
|
|
371
|
+
hasChanges: changedIds.length > 0 || hasNewFiles,
|
|
372
|
+
changedIds,
|
|
373
|
+
timestamp: Date.now()
|
|
374
|
+
};
|
|
375
|
+
} catch {
|
|
376
|
+
return {
|
|
377
|
+
hasChanges: changedIds.length > 0,
|
|
378
|
+
changedIds,
|
|
379
|
+
timestamp: Date.now()
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 增量扫描 - 只扫描变更的会话
|
|
385
|
+
*/
|
|
386
|
+
incrementalScan(cachedSessions, changedIds) {
|
|
387
|
+
if (!this.basePath) return cachedSessions;
|
|
388
|
+
const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
|
|
389
|
+
for (const projectDir of this.listProjectDirs()) {
|
|
390
|
+
for (const file of this.listJsonlFiles(projectDir)) {
|
|
391
|
+
try {
|
|
392
|
+
const sessionId = basename2(file, ".jsonl");
|
|
393
|
+
if (changedIds.includes(sessionId)) {
|
|
394
|
+
const head = this.parseSessionHead(file, projectDir);
|
|
395
|
+
if (head) {
|
|
396
|
+
sessionMap.set(head.id, head);
|
|
397
|
+
this.sessionMetaMap.set(head.id, {
|
|
398
|
+
id: head.id,
|
|
399
|
+
title: head.title,
|
|
400
|
+
sourcePath: file,
|
|
401
|
+
directory: head.directory,
|
|
402
|
+
model: head.stats.total_tokens ? "unknown" : void 0,
|
|
403
|
+
messageCount: head.stats.message_count,
|
|
404
|
+
createdAt: head.time_created,
|
|
405
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const projectDir of this.listProjectDirs()) {
|
|
414
|
+
for (const file of this.listJsonlFiles(projectDir)) {
|
|
415
|
+
try {
|
|
416
|
+
const sessionId = basename2(file, ".jsonl");
|
|
417
|
+
if (!sessionMap.has(sessionId)) {
|
|
418
|
+
const head = this.parseSessionHead(file, projectDir);
|
|
419
|
+
if (head) {
|
|
420
|
+
sessionMap.set(head.id, head);
|
|
421
|
+
this.sessionMetaMap.set(head.id, {
|
|
422
|
+
id: head.id,
|
|
423
|
+
title: head.title,
|
|
424
|
+
sourcePath: file,
|
|
425
|
+
directory: head.directory,
|
|
426
|
+
model: head.stats.total_tokens ? "unknown" : void 0,
|
|
427
|
+
messageCount: head.stats.message_count,
|
|
428
|
+
createdAt: head.time_created,
|
|
429
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return Array.from(sessionMap.values());
|
|
438
|
+
}
|
|
439
|
+
// --- Private helpers ---
|
|
440
|
+
listProjectDirs() {
|
|
441
|
+
if (!this.basePath) return [];
|
|
442
|
+
try {
|
|
443
|
+
return readdirSync(this.basePath).map((e) => join2(this.basePath, e)).filter((p) => existsSync2(p));
|
|
444
|
+
} catch {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
listJsonlFiles(dir) {
|
|
449
|
+
try {
|
|
450
|
+
return readdirSync(dir).filter((f) => f.endsWith(".jsonl") && f !== "sessions-index.json").map((f) => join2(dir, f));
|
|
451
|
+
} catch {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
loadSessionsIndex(projectDir) {
|
|
456
|
+
const cacheKey = basename2(projectDir);
|
|
457
|
+
if (cacheKey in this.sessionsIndexCache) {
|
|
458
|
+
return this.sessionsIndexCache[cacheKey];
|
|
459
|
+
}
|
|
460
|
+
const indexPath = join2(projectDir, "sessions-index.json");
|
|
461
|
+
const map = /* @__PURE__ */ new Map();
|
|
462
|
+
if (existsSync2(indexPath)) {
|
|
463
|
+
try {
|
|
464
|
+
const data = JSON.parse(readFileSync2(indexPath, "utf-8"));
|
|
465
|
+
const entries = data?.entries ?? [];
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
const sid = entry?.sessionId;
|
|
468
|
+
if (typeof sid === "string") {
|
|
469
|
+
map.set(sid, entry);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
this.sessionsIndexCache[cacheKey] = map;
|
|
476
|
+
return map;
|
|
477
|
+
}
|
|
478
|
+
parseSessionHead(filePath, projectDir) {
|
|
479
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
480
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
481
|
+
if (lines.length === 0) return null;
|
|
482
|
+
const sessionId = basename2(filePath, ".jsonl");
|
|
483
|
+
let firstRecord;
|
|
484
|
+
try {
|
|
485
|
+
firstRecord = JSON.parse(lines[0]);
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const createdAt = parseTimestampMs(firstRecord) || statSync(filePath).mtimeMs;
|
|
490
|
+
const index = this.loadSessionsIndex(projectDir);
|
|
491
|
+
const indexEntry = index.get(sessionId);
|
|
492
|
+
const explicitTitle = indexEntry?.summary ? String(indexEntry.summary) : null;
|
|
493
|
+
let updatedAt = createdAt;
|
|
494
|
+
let messageCount = 0;
|
|
495
|
+
let model = null;
|
|
496
|
+
let cwd = null;
|
|
497
|
+
for (const line of lines) {
|
|
498
|
+
try {
|
|
499
|
+
const data = JSON.parse(line);
|
|
500
|
+
const ts = parseTimestampMs(data);
|
|
501
|
+
if (ts > updatedAt) updatedAt = ts;
|
|
502
|
+
if (!cwd && data["cwd"] && typeof data["cwd"] === "string") {
|
|
503
|
+
cwd = data["cwd"];
|
|
504
|
+
}
|
|
505
|
+
const msg = data["message"];
|
|
506
|
+
if (msg && typeof msg === "object") {
|
|
507
|
+
const role = msg["role"];
|
|
508
|
+
if (typeof role === "string" && role.trim()) {
|
|
509
|
+
messageCount++;
|
|
510
|
+
}
|
|
511
|
+
if (!model) {
|
|
512
|
+
const m = msg["model"];
|
|
513
|
+
if (typeof m === "string" && m.trim()) model = m.trim();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const directory = cwd ?? projectDir;
|
|
520
|
+
const messageTitle = this.extractTitle(lines);
|
|
521
|
+
const directoryTitle = basenameTitle(directory) || basenameTitle(projectDir);
|
|
522
|
+
const title = resolveSessionTitle(explicitTitle, messageTitle, directoryTitle);
|
|
523
|
+
return {
|
|
524
|
+
id: sessionId,
|
|
525
|
+
slug: `claudecode/${sessionId}`,
|
|
526
|
+
title,
|
|
527
|
+
directory,
|
|
528
|
+
time_created: createdAt,
|
|
529
|
+
time_updated: updatedAt,
|
|
530
|
+
stats: {
|
|
531
|
+
message_count: messageCount,
|
|
532
|
+
total_input_tokens: 0,
|
|
533
|
+
total_output_tokens: 0,
|
|
534
|
+
total_cost: 0
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
extractTitle(lines) {
|
|
539
|
+
for (const line of lines.slice(0, 20)) {
|
|
540
|
+
try {
|
|
541
|
+
const data = JSON.parse(line);
|
|
542
|
+
const msg = data["message"];
|
|
543
|
+
if (!msg || typeof msg !== "object") continue;
|
|
544
|
+
if (msg["role"] !== "user") continue;
|
|
545
|
+
const content = msg["content"];
|
|
546
|
+
if (!content) continue;
|
|
547
|
+
if (typeof content === "string") {
|
|
548
|
+
return normalizeTitleText2(content);
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(content)) {
|
|
551
|
+
const texts = content.filter((item) => typeof item === "object" && item !== null && "text" in item).map((item) => String(item["text"] ?? "")).join(" ");
|
|
552
|
+
return normalizeTitleText2(texts);
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
// --- Record conversion ---
|
|
560
|
+
convertRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
|
|
561
|
+
if (data["isMeta"] === true) return;
|
|
562
|
+
const msgType = String(data["type"] ?? "");
|
|
563
|
+
if (msgType === "assistant") {
|
|
564
|
+
this.convertAssistantRecord(
|
|
565
|
+
data,
|
|
566
|
+
messages,
|
|
567
|
+
pendingToolCalls,
|
|
568
|
+
ignoredToolCallIds,
|
|
569
|
+
assistantUuidToToolCalls,
|
|
570
|
+
assistantState
|
|
571
|
+
);
|
|
572
|
+
} else if (msgType === "user") {
|
|
573
|
+
this.convertUserRecord(
|
|
574
|
+
data,
|
|
575
|
+
messages,
|
|
576
|
+
pendingToolCalls,
|
|
577
|
+
ignoredToolCallIds,
|
|
578
|
+
assistantUuidToToolCalls,
|
|
579
|
+
assistantState
|
|
580
|
+
);
|
|
581
|
+
} else if (msgType === "tool_result") {
|
|
582
|
+
this.convertToolResultRecord(data, messages, assistantState);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
convertAssistantRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
|
|
586
|
+
const msg = data["message"] ?? {};
|
|
587
|
+
const timestampMs = parseTimestampMs(data);
|
|
588
|
+
const rawContent = msg["content"] ?? [];
|
|
589
|
+
const uuid = String(data["uuid"] ?? "");
|
|
590
|
+
const toolCallIds = [];
|
|
591
|
+
let currentAssistantIndex = assistantState.currentIndex;
|
|
592
|
+
let latestAssistantTextIndex = assistantState.latestTextIndex;
|
|
593
|
+
if (Array.isArray(rawContent)) {
|
|
594
|
+
for (const item of rawContent) {
|
|
595
|
+
if (!item || typeof item !== "object") continue;
|
|
596
|
+
const part = item;
|
|
597
|
+
const partType = String(part["type"] ?? "");
|
|
598
|
+
if (partType === "thinking") {
|
|
599
|
+
const text = String(part["thinking"] ?? "");
|
|
600
|
+
if (text.trim()) {
|
|
601
|
+
currentAssistantIndex = this.appendAssistantReasoning(
|
|
602
|
+
messages,
|
|
603
|
+
{ messageId: uuid, msg, timestampMs, text },
|
|
604
|
+
currentAssistantIndex
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (partType === "text") {
|
|
610
|
+
const text = String(part["text"] ?? "");
|
|
611
|
+
if (text.trim()) {
|
|
612
|
+
currentAssistantIndex = this.appendAssistantText(
|
|
613
|
+
messages,
|
|
614
|
+
{ messageId: uuid, msg, timestampMs, text },
|
|
615
|
+
currentAssistantIndex
|
|
616
|
+
);
|
|
617
|
+
latestAssistantTextIndex = currentAssistantIndex;
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (partType !== "tool_use") continue;
|
|
622
|
+
const toolName = String(part["name"] ?? "").trim();
|
|
623
|
+
const toolCallId = String(part["id"] ?? "").trim();
|
|
624
|
+
if (toolName && toolCallId && this.shouldIgnoreTool(toolName)) {
|
|
625
|
+
ignoredToolCallIds.add(toolCallId);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const toolPart = this.buildToolPart(part, timestampMs);
|
|
629
|
+
const [msgIndex, partIndex] = this.attachToolCallToLatestAssistant(messages, {
|
|
630
|
+
messageId: uuid,
|
|
631
|
+
msg,
|
|
632
|
+
timestampMs,
|
|
633
|
+
toolPart,
|
|
634
|
+
latestTextIndex: latestAssistantTextIndex
|
|
635
|
+
});
|
|
636
|
+
currentAssistantIndex = msgIndex;
|
|
637
|
+
if (toolCallId) {
|
|
638
|
+
pendingToolCalls.set(toolCallId, [msgIndex, partIndex]);
|
|
639
|
+
toolCallIds.push(toolCallId);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (toolCallIds.length > 0) {
|
|
644
|
+
assistantUuidToToolCalls.set(uuid, toolCallIds);
|
|
645
|
+
}
|
|
646
|
+
assistantState.currentIndex = currentAssistantIndex;
|
|
647
|
+
assistantState.latestTextIndex = latestAssistantTextIndex;
|
|
648
|
+
}
|
|
649
|
+
convertUserRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
|
|
650
|
+
const msg = data["message"] ?? {};
|
|
651
|
+
const timestampMs = parseTimestampMs(data);
|
|
652
|
+
const content = msg["content"] ?? "";
|
|
653
|
+
const uuid = String(data["uuid"] ?? "");
|
|
654
|
+
if (typeof content === "string") {
|
|
655
|
+
const parts = this.normalizeUserTextParts(content, timestampMs);
|
|
656
|
+
if (parts.length === 0) {
|
|
657
|
+
assistantState.currentIndex = null;
|
|
658
|
+
assistantState.latestTextIndex = null;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
messages.push(this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts }));
|
|
662
|
+
assistantState.currentIndex = null;
|
|
663
|
+
assistantState.latestTextIndex = null;
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!Array.isArray(content)) {
|
|
667
|
+
assistantState.currentIndex = null;
|
|
668
|
+
assistantState.latestTextIndex = null;
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const visibleParts = this.normalizeUserTextParts(content, timestampMs);
|
|
672
|
+
const toolStateUpdates = this.extractToolStateUpdates(data["toolUseResult"]);
|
|
673
|
+
for (const item of content) {
|
|
674
|
+
if (!item || typeof item !== "object") continue;
|
|
675
|
+
const ci = item;
|
|
676
|
+
if (ci["type"] !== "tool_result") continue;
|
|
677
|
+
const toolCallId = this.resolveToolCallId(data, ci, assistantUuidToToolCalls);
|
|
678
|
+
if (toolCallId && ignoredToolCallIds.has(toolCallId)) continue;
|
|
679
|
+
const outputParts = this.normalizeClaudeToolOutput(ci["content"], timestampMs);
|
|
680
|
+
if (this.backfillToolOutput(
|
|
681
|
+
messages,
|
|
682
|
+
pendingToolCalls,
|
|
683
|
+
toolCallId,
|
|
684
|
+
outputParts,
|
|
685
|
+
toolStateUpdates
|
|
686
|
+
)) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const fallback = this.buildFallbackToolMessage({
|
|
690
|
+
messageId: uuid,
|
|
691
|
+
timestampMs,
|
|
692
|
+
toolCallId,
|
|
693
|
+
outputParts
|
|
694
|
+
});
|
|
695
|
+
if (fallback) messages.push(fallback);
|
|
696
|
+
}
|
|
697
|
+
if (visibleParts.length > 0) {
|
|
698
|
+
messages.push(
|
|
699
|
+
this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts: visibleParts })
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
assistantState.currentIndex = null;
|
|
703
|
+
assistantState.latestTextIndex = null;
|
|
704
|
+
}
|
|
705
|
+
convertToolResultRecord(data, messages, assistantState) {
|
|
706
|
+
const timestampMs = parseTimestampMs(data);
|
|
707
|
+
const msg = data["message"] ?? {};
|
|
708
|
+
const outputParts = this.normalizeClaudeToolOutput(msg["content"], timestampMs);
|
|
709
|
+
const uuid = String(data["uuid"] ?? "");
|
|
710
|
+
const fallback = this.buildFallbackToolMessage({
|
|
711
|
+
messageId: uuid,
|
|
712
|
+
timestampMs,
|
|
713
|
+
toolCallId: null,
|
|
714
|
+
outputParts
|
|
715
|
+
});
|
|
716
|
+
if (fallback) messages.push(fallback);
|
|
717
|
+
assistantState.currentIndex = null;
|
|
718
|
+
assistantState.latestTextIndex = null;
|
|
719
|
+
}
|
|
720
|
+
// --- Message building ---
|
|
721
|
+
buildMessage(opts) {
|
|
722
|
+
return {
|
|
723
|
+
id: opts.messageId,
|
|
724
|
+
role: opts.role,
|
|
725
|
+
agent: opts.agent ?? null,
|
|
726
|
+
time_created: opts.timestampMs,
|
|
727
|
+
mode: opts.mode ?? null,
|
|
728
|
+
model: opts.model ?? null,
|
|
729
|
+
provider: opts.provider ?? null,
|
|
730
|
+
tokens: opts.tokens ? opts.tokens : void 0,
|
|
731
|
+
cost: opts.cost ?? 0,
|
|
732
|
+
parts: opts.parts
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
buildTextPart(text, timestampMs) {
|
|
736
|
+
return { type: "text", text, time_created: timestampMs };
|
|
737
|
+
}
|
|
738
|
+
buildReasoningPart(text, timestampMs) {
|
|
739
|
+
return { type: "reasoning", text, time_created: timestampMs };
|
|
740
|
+
}
|
|
741
|
+
buildToolPart(part, timestampMs) {
|
|
742
|
+
const toolName = String(part["name"] ?? "");
|
|
743
|
+
return {
|
|
744
|
+
type: "tool",
|
|
745
|
+
tool: toolName,
|
|
746
|
+
callID: String(part["id"] ?? ""),
|
|
747
|
+
title: `Tool: ${toolName}`,
|
|
748
|
+
state: {
|
|
749
|
+
input: part["input"] ?? {},
|
|
750
|
+
output: null
|
|
751
|
+
},
|
|
752
|
+
time_created: timestampMs
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
applyAssistantMetadata(message, msg) {
|
|
756
|
+
const model = msg["model"];
|
|
757
|
+
if (model && typeof model === "string" && !message.model) {
|
|
758
|
+
message.model = model;
|
|
759
|
+
}
|
|
760
|
+
const usage = msg["usage"];
|
|
761
|
+
if (usage && typeof usage === "object" && !message.tokens) {
|
|
762
|
+
const u = usage;
|
|
763
|
+
message.tokens = {
|
|
764
|
+
input: (u["input_tokens"] ?? 0) + (u["cache_creation_input_tokens"] ?? 0) + (u["cache_read_input_tokens"] ?? 0),
|
|
765
|
+
output: u["output_tokens"] ?? 0
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// --- Assistant message grouping ---
|
|
770
|
+
appendAssistantReasoning(messages, opts, currentIndex) {
|
|
771
|
+
const part = this.buildReasoningPart(opts.text, opts.timestampMs);
|
|
772
|
+
if (currentIndex !== null) {
|
|
773
|
+
const message2 = messages[currentIndex];
|
|
774
|
+
const hasText = message2.parts.some((p) => p.type === "text");
|
|
775
|
+
const hasTool = message2.parts.some((p) => p.type === "tool");
|
|
776
|
+
if (!hasText && !hasTool) {
|
|
777
|
+
this.appendPartIfNew(message2, part);
|
|
778
|
+
this.applyAssistantMetadata(message2, opts.msg);
|
|
779
|
+
return currentIndex;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const message = this.buildMessage({
|
|
783
|
+
messageId: opts.messageId,
|
|
784
|
+
role: "assistant",
|
|
785
|
+
timestampMs: opts.timestampMs,
|
|
786
|
+
parts: [part],
|
|
787
|
+
agent: "claude"
|
|
788
|
+
});
|
|
789
|
+
this.applyAssistantMetadata(message, opts.msg);
|
|
790
|
+
messages.push(message);
|
|
791
|
+
return messages.length - 1;
|
|
792
|
+
}
|
|
793
|
+
appendAssistantText(messages, opts, currentIndex) {
|
|
794
|
+
const part = this.buildTextPart(opts.text, opts.timestampMs);
|
|
795
|
+
if (currentIndex !== null) {
|
|
796
|
+
const message2 = messages[currentIndex];
|
|
797
|
+
const hasTool = message2.parts.some((p) => p.type === "tool");
|
|
798
|
+
if (!hasTool) {
|
|
799
|
+
this.appendPartIfNew(message2, part);
|
|
800
|
+
this.applyAssistantMetadata(message2, opts.msg);
|
|
801
|
+
return currentIndex;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const message = this.buildMessage({
|
|
805
|
+
messageId: opts.messageId,
|
|
806
|
+
role: "assistant",
|
|
807
|
+
timestampMs: opts.timestampMs,
|
|
808
|
+
parts: [part],
|
|
809
|
+
agent: "claude"
|
|
810
|
+
});
|
|
811
|
+
this.applyAssistantMetadata(message, opts.msg);
|
|
812
|
+
messages.push(message);
|
|
813
|
+
return messages.length - 1;
|
|
814
|
+
}
|
|
815
|
+
attachToolCallToLatestAssistant(messages, opts) {
|
|
816
|
+
if (opts.latestTextIndex !== null) {
|
|
817
|
+
const message2 = messages[opts.latestTextIndex];
|
|
818
|
+
message2.parts.push(opts.toolPart);
|
|
819
|
+
this.applyAssistantMetadata(message2, opts.msg);
|
|
820
|
+
return [opts.latestTextIndex, message2.parts.length - 1];
|
|
821
|
+
}
|
|
822
|
+
const message = this.buildMessage({
|
|
823
|
+
messageId: opts.messageId,
|
|
824
|
+
role: "assistant",
|
|
825
|
+
timestampMs: opts.timestampMs,
|
|
826
|
+
parts: [opts.toolPart],
|
|
827
|
+
agent: "claude",
|
|
828
|
+
mode: "tool"
|
|
829
|
+
});
|
|
830
|
+
this.applyAssistantMetadata(message, opts.msg);
|
|
831
|
+
messages.push(message);
|
|
832
|
+
return [messages.length - 1, 0];
|
|
833
|
+
}
|
|
834
|
+
// --- User content normalization ---
|
|
835
|
+
normalizeUserTextParts(content, timestampMs) {
|
|
836
|
+
if (typeof content === "string") {
|
|
837
|
+
return content.trim() ? [this.buildTextPart(content, timestampMs)] : [];
|
|
838
|
+
}
|
|
839
|
+
if (!Array.isArray(content)) return [];
|
|
840
|
+
const parts = [];
|
|
841
|
+
for (const item of content) {
|
|
842
|
+
if (typeof item === "object" && item !== null) {
|
|
843
|
+
const ci = item;
|
|
844
|
+
if (ci["type"] === "tool_result") continue;
|
|
845
|
+
const text = String(ci["text"] ?? "");
|
|
846
|
+
if (text.trim()) parts.push(this.buildTextPart(text, timestampMs));
|
|
847
|
+
} else if (typeof item === "string" && item.trim()) {
|
|
848
|
+
parts.push(this.buildTextPart(item, timestampMs));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return parts;
|
|
852
|
+
}
|
|
853
|
+
normalizeClaudeToolOutput(content, timestampMs) {
|
|
854
|
+
if (typeof content === "string") {
|
|
855
|
+
return content.trim() ? [this.buildTextPart(content, timestampMs)] : [];
|
|
856
|
+
}
|
|
857
|
+
if (content === null || content === void 0) return [];
|
|
858
|
+
if (Array.isArray(content)) {
|
|
859
|
+
const parts = [];
|
|
860
|
+
for (const item of content) {
|
|
861
|
+
if (typeof item === "object" && item !== null) {
|
|
862
|
+
const text2 = String(
|
|
863
|
+
item["text"] ?? item["content"] ?? ""
|
|
864
|
+
);
|
|
865
|
+
if (text2.trim()) parts.push(this.buildTextPart(text2, timestampMs));
|
|
866
|
+
} else if (typeof item === "string" && item.trim()) {
|
|
867
|
+
parts.push(this.buildTextPart(item, timestampMs));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return parts;
|
|
871
|
+
}
|
|
872
|
+
const text = String(content);
|
|
873
|
+
return text.trim() ? [this.buildTextPart(text, timestampMs)] : [];
|
|
874
|
+
}
|
|
875
|
+
// --- Tool backfill ---
|
|
876
|
+
backfillToolOutput(messages, pendingToolCalls, callId, outputParts, stateUpdates) {
|
|
877
|
+
if (!callId) return false;
|
|
878
|
+
const location = pendingToolCalls.get(callId);
|
|
879
|
+
if (location === void 0) return false;
|
|
880
|
+
const [msgIndex, partIndex] = location;
|
|
881
|
+
const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
|
|
882
|
+
if (outputParts.length > 0) {
|
|
883
|
+
const existing = state.output;
|
|
884
|
+
if (Array.isArray(existing)) {
|
|
885
|
+
existing.push(...outputParts);
|
|
886
|
+
} else if (existing === null || existing === void 0) {
|
|
887
|
+
state.output = [...outputParts];
|
|
888
|
+
} else {
|
|
889
|
+
state.output = [existing, ...outputParts];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (stateUpdates) {
|
|
893
|
+
Object.assign(state, stateUpdates);
|
|
894
|
+
}
|
|
895
|
+
if (outputParts.length > 0 && !state.status) {
|
|
896
|
+
state.status = "completed";
|
|
897
|
+
}
|
|
898
|
+
return outputParts.length > 0 || !!stateUpdates;
|
|
899
|
+
}
|
|
900
|
+
resolveToolCallId(data, item, assistantUuidToToolCalls) {
|
|
901
|
+
const directId = String(item["tool_use_id"] ?? "").trim();
|
|
902
|
+
if (directId) return directId;
|
|
903
|
+
const sourceUuid = String(data["sourceToolAssistantUUID"] ?? "").trim();
|
|
904
|
+
if (!sourceUuid) return "";
|
|
905
|
+
const ids = assistantUuidToToolCalls.get(sourceUuid);
|
|
906
|
+
if (ids && ids.length === 1) return ids[0];
|
|
907
|
+
return "";
|
|
908
|
+
}
|
|
909
|
+
extractToolStateUpdates(toolUseResult) {
|
|
910
|
+
if (!toolUseResult || typeof toolUseResult !== "object") return {};
|
|
911
|
+
const result = toolUseResult;
|
|
912
|
+
const updates = {};
|
|
913
|
+
const success = result["success"];
|
|
914
|
+
if (typeof success === "boolean") {
|
|
915
|
+
updates["status"] = success ? "success" : "error";
|
|
916
|
+
}
|
|
917
|
+
const commandName = result["commandName"];
|
|
918
|
+
if (commandName) {
|
|
919
|
+
updates["meta"] = { commandName };
|
|
920
|
+
}
|
|
921
|
+
return updates;
|
|
922
|
+
}
|
|
923
|
+
// --- Fallback ---
|
|
924
|
+
buildFallbackToolMessage(opts) {
|
|
925
|
+
if (opts.outputParts.length === 0) return null;
|
|
926
|
+
return this.buildMessage({
|
|
927
|
+
messageId: opts.messageId,
|
|
928
|
+
role: "tool",
|
|
929
|
+
timestampMs: opts.timestampMs,
|
|
930
|
+
parts: opts.outputParts
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
// --- Utilities ---
|
|
934
|
+
shouldIgnoreTool(toolName) {
|
|
935
|
+
return toolName === "TodoWrite";
|
|
936
|
+
}
|
|
937
|
+
appendPartIfNew(message, part) {
|
|
938
|
+
const parts = message.parts;
|
|
939
|
+
if (parts.length > 0 && parts[parts.length - 1].type === part.type) {
|
|
940
|
+
const tail = parts[parts.length - 1];
|
|
941
|
+
if (tail.text === part.text) return;
|
|
942
|
+
}
|
|
943
|
+
parts.push(part);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
var DatabaseConstructor = null;
|
|
947
|
+
try {
|
|
948
|
+
const require2 = createRequire(import.meta.url);
|
|
949
|
+
const mod = require2("better-sqlite3");
|
|
950
|
+
DatabaseConstructor = typeof mod === "function" ? mod : mod.default;
|
|
951
|
+
} catch {
|
|
952
|
+
}
|
|
953
|
+
function openDbReadOnly(dbPath) {
|
|
954
|
+
if (!DatabaseConstructor) return null;
|
|
955
|
+
try {
|
|
956
|
+
const db = DatabaseConstructor(dbPath, { readonly: true });
|
|
957
|
+
return db;
|
|
958
|
+
} catch {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function isSqliteAvailable() {
|
|
963
|
+
return DatabaseConstructor !== null;
|
|
964
|
+
}
|
|
965
|
+
var OpenCodeAgent = class extends BaseAgent {
|
|
966
|
+
name = "opencode";
|
|
967
|
+
displayName = "OpenCode";
|
|
968
|
+
dbPath = null;
|
|
969
|
+
// Session metadata for caching
|
|
970
|
+
sessionMetaMap = /* @__PURE__ */ new Map();
|
|
971
|
+
findDbPath() {
|
|
972
|
+
if (!isSqliteAvailable()) return null;
|
|
973
|
+
const roots = resolveProviderRoots();
|
|
974
|
+
return firstExisting(join3(roots.opencodeRoot, "opencode.db"), "data/opencode/opencode.db");
|
|
975
|
+
}
|
|
976
|
+
isAvailable() {
|
|
977
|
+
this.dbPath = this.findDbPath();
|
|
978
|
+
return this.dbPath !== null;
|
|
979
|
+
}
|
|
980
|
+
scan() {
|
|
981
|
+
if (!this.dbPath) return [];
|
|
982
|
+
const db = openDbReadOnly(this.dbPath);
|
|
983
|
+
if (!db) return [];
|
|
984
|
+
try {
|
|
985
|
+
const cutoffTime = Date.now() - 3650 * 24 * 60 * 60 * 1e3;
|
|
986
|
+
const hasMessageTable = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'message'").get();
|
|
987
|
+
let rows;
|
|
988
|
+
if (hasMessageTable) {
|
|
989
|
+
rows = db.prepare(`
|
|
990
|
+
SELECT
|
|
991
|
+
s.id, s.title, s.time_created, s.time_updated, s.slug, s.directory,
|
|
992
|
+
s.version, s.summary_files,
|
|
993
|
+
(SELECT COUNT(*) FROM message m WHERE m.session_id = s.id) AS message_count,
|
|
994
|
+
(SELECT m.data FROM message m
|
|
995
|
+
WHERE m.session_id = s.id AND m.data LIKE '%"modelID"%'
|
|
996
|
+
ORDER BY m.time_created DESC LIMIT 1) AS model_message_data
|
|
997
|
+
FROM session s
|
|
998
|
+
WHERE s.time_created >= ?
|
|
999
|
+
ORDER BY s.time_created DESC
|
|
1000
|
+
`).all(cutoffTime);
|
|
1001
|
+
} else {
|
|
1002
|
+
rows = db.prepare(`
|
|
1003
|
+
SELECT s.id, s.title, s.time_created, s.time_updated, s.slug, s.directory,
|
|
1004
|
+
s.version, s.summary_files, 0 AS message_count, NULL AS model_message_data
|
|
1005
|
+
FROM session s
|
|
1006
|
+
WHERE s.time_created >= ?
|
|
1007
|
+
ORDER BY s.time_created DESC
|
|
1008
|
+
`).all(cutoffTime);
|
|
1009
|
+
}
|
|
1010
|
+
const heads = [];
|
|
1011
|
+
for (const row of rows) {
|
|
1012
|
+
const id = String(row.id ?? "");
|
|
1013
|
+
const title = String(row.title ?? "").trim() || "Untitled";
|
|
1014
|
+
const timeCreated = Number(row.time_created ?? 0);
|
|
1015
|
+
const timeUpdated = Number(row.time_updated ?? timeCreated);
|
|
1016
|
+
const slug = `opencode/${id}`;
|
|
1017
|
+
const directory = String(row.directory ?? "");
|
|
1018
|
+
heads.push({
|
|
1019
|
+
id,
|
|
1020
|
+
slug,
|
|
1021
|
+
title,
|
|
1022
|
+
directory,
|
|
1023
|
+
time_created: timeCreated,
|
|
1024
|
+
time_updated: timeUpdated,
|
|
1025
|
+
stats: {
|
|
1026
|
+
message_count: Number(row.message_count ?? 0),
|
|
1027
|
+
total_input_tokens: 0,
|
|
1028
|
+
total_output_tokens: 0,
|
|
1029
|
+
total_cost: 0
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
if (this.dbPath) {
|
|
1033
|
+
this.sessionMetaMap.set(id, {
|
|
1034
|
+
id,
|
|
1035
|
+
sourcePath: this.dbPath
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return heads;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return [];
|
|
1042
|
+
} finally {
|
|
1043
|
+
db.close();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
getSessionMetaMap() {
|
|
1047
|
+
return this.sessionMetaMap;
|
|
1048
|
+
}
|
|
1049
|
+
setSessionMetaMap(meta) {
|
|
1050
|
+
this.sessionMetaMap = meta;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* 检测数据库变更
|
|
1054
|
+
* 对于 SQLite,检测数据库文件修改时间
|
|
1055
|
+
*/
|
|
1056
|
+
checkForChanges(sinceTimestamp, cachedSessions) {
|
|
1057
|
+
if (!this.dbPath) {
|
|
1058
|
+
this.dbPath = this.findDbPath();
|
|
1059
|
+
}
|
|
1060
|
+
if (!this.dbPath || !existsSync3(this.dbPath)) {
|
|
1061
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
const stat = statSync2(this.dbPath);
|
|
1065
|
+
const hasChanges = stat.mtimeMs > sinceTimestamp;
|
|
1066
|
+
const changedIds = hasChanges ? cachedSessions.map((s) => s.id) : [];
|
|
1067
|
+
return {
|
|
1068
|
+
hasChanges,
|
|
1069
|
+
changedIds,
|
|
1070
|
+
timestamp: Date.now()
|
|
1071
|
+
};
|
|
1072
|
+
} catch {
|
|
1073
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 增量扫描 - 重新查询数据库
|
|
1078
|
+
*/
|
|
1079
|
+
incrementalScan(_cachedSessions, _changedIds) {
|
|
1080
|
+
return this.scan();
|
|
1081
|
+
}
|
|
1082
|
+
getSessionData(sessionId) {
|
|
1083
|
+
if (!this.dbPath) {
|
|
1084
|
+
this.dbPath = this.findDbPath();
|
|
1085
|
+
}
|
|
1086
|
+
if (!this.dbPath) {
|
|
1087
|
+
throw new Error("OpenCode database is missing");
|
|
1088
|
+
}
|
|
1089
|
+
const db = openDbReadOnly(this.dbPath);
|
|
1090
|
+
if (!db) {
|
|
1091
|
+
throw new Error("OpenCode database is missing");
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const sessionRow = db.prepare("SELECT * FROM session WHERE id = ?").get(sessionId);
|
|
1095
|
+
if (!sessionRow) {
|
|
1096
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1097
|
+
}
|
|
1098
|
+
const id = String(sessionRow.id ?? sessionId);
|
|
1099
|
+
const title = String(sessionRow.title ?? "Untitled");
|
|
1100
|
+
const slug = `opencode/${id}`;
|
|
1101
|
+
const directory = String(sessionRow.directory ?? "");
|
|
1102
|
+
const timeCreated = Number(sessionRow.time_created ?? 0);
|
|
1103
|
+
const timeUpdated = Number(sessionRow.time_updated ?? timeCreated);
|
|
1104
|
+
const messages = [];
|
|
1105
|
+
let totalCost = 0;
|
|
1106
|
+
let totalInputTokens = 0;
|
|
1107
|
+
let totalOutputTokens = 0;
|
|
1108
|
+
const msgRows = db.prepare("SELECT * FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
|
|
1109
|
+
for (const msgRow of msgRows) {
|
|
1110
|
+
const msgData = JSON.parse(String(msgRow.data ?? "{}"));
|
|
1111
|
+
const parts = [];
|
|
1112
|
+
const cost = Number(msgData.cost ?? 0);
|
|
1113
|
+
const tokens = msgData.tokens;
|
|
1114
|
+
const inputTokens = Number(tokens?.input ?? 0);
|
|
1115
|
+
const outputTokens = Number(tokens?.output ?? 0);
|
|
1116
|
+
totalCost += cost;
|
|
1117
|
+
totalInputTokens += inputTokens;
|
|
1118
|
+
totalOutputTokens += outputTokens;
|
|
1119
|
+
const partRows = db.prepare("SELECT * FROM part WHERE message_id = ? ORDER BY time_created ASC").all(msgRow.id);
|
|
1120
|
+
for (const partRow of partRows) {
|
|
1121
|
+
const partData = JSON.parse(String(partRow.data ?? "{}"));
|
|
1122
|
+
const partType = String(partData.type ?? "");
|
|
1123
|
+
if (partType === "text" || partType === "reasoning") {
|
|
1124
|
+
parts.push({
|
|
1125
|
+
type: partType,
|
|
1126
|
+
text: partData.text ?? "",
|
|
1127
|
+
time_created: Number(partRow.time_created ?? 0)
|
|
1128
|
+
});
|
|
1129
|
+
} else if (partType === "tool") {
|
|
1130
|
+
parts.push({
|
|
1131
|
+
type: "tool",
|
|
1132
|
+
tool: String(partData.tool ?? ""),
|
|
1133
|
+
callID: String(partData.callID ?? ""),
|
|
1134
|
+
title: String(partData.title ?? ""),
|
|
1135
|
+
state: partData.state ?? {},
|
|
1136
|
+
time_created: Number(partRow.time_created ?? 0)
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
messages.push({
|
|
1141
|
+
id: String(msgRow.id ?? ""),
|
|
1142
|
+
role: String(msgData.role ?? "assistant"),
|
|
1143
|
+
agent: msgData.agent ?? null,
|
|
1144
|
+
mode: msgData.mode ?? null,
|
|
1145
|
+
model: msgData.modelID ?? null,
|
|
1146
|
+
provider: msgData.providerID ?? null,
|
|
1147
|
+
time_created: Number(msgRow.time_created ?? 0),
|
|
1148
|
+
tokens: tokens ? { input: inputTokens, output: outputTokens } : void 0,
|
|
1149
|
+
cost,
|
|
1150
|
+
parts
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
id,
|
|
1155
|
+
title,
|
|
1156
|
+
slug,
|
|
1157
|
+
directory,
|
|
1158
|
+
version: sessionRow.version ?? void 0,
|
|
1159
|
+
time_created: timeCreated,
|
|
1160
|
+
time_updated: timeUpdated,
|
|
1161
|
+
summary_files: sessionRow.summary_files ?? void 0,
|
|
1162
|
+
stats: {
|
|
1163
|
+
message_count: messages.length,
|
|
1164
|
+
total_input_tokens: totalInputTokens,
|
|
1165
|
+
total_output_tokens: totalOutputTokens,
|
|
1166
|
+
total_cost: totalCost
|
|
1167
|
+
},
|
|
1168
|
+
messages
|
|
1169
|
+
};
|
|
1170
|
+
} finally {
|
|
1171
|
+
db.close();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
var KIMI_TOOL_TITLE_MAP = {
|
|
1176
|
+
ReadFile: "read",
|
|
1177
|
+
Glob: "glob",
|
|
1178
|
+
StrReplaceFile: "edit",
|
|
1179
|
+
Grep: "grep",
|
|
1180
|
+
WriteFile: "write",
|
|
1181
|
+
Shell: "bash"
|
|
1182
|
+
};
|
|
1183
|
+
var KIMI_IGNORED_TOOLS = /* @__PURE__ */ new Set(["SetTodoList"]);
|
|
1184
|
+
function mapToolTitle(toolName) {
|
|
1185
|
+
return KIMI_TOOL_TITLE_MAP[toolName] ?? toolName;
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeToolArguments(raw) {
|
|
1188
|
+
if (typeof raw === "string") {
|
|
1189
|
+
try {
|
|
1190
|
+
return JSON.parse(raw);
|
|
1191
|
+
} catch {
|
|
1192
|
+
return raw;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return raw;
|
|
1196
|
+
}
|
|
1197
|
+
function normalizeToolOutputParts(content, timestampMs) {
|
|
1198
|
+
if (typeof content === "string") {
|
|
1199
|
+
return content.trim() ? [{ type: "text", text: content, time_created: timestampMs }] : [];
|
|
1200
|
+
}
|
|
1201
|
+
if (Array.isArray(content)) {
|
|
1202
|
+
const parts = [];
|
|
1203
|
+
for (const item of content) {
|
|
1204
|
+
if (typeof item === "object" && item !== null && "text" in item) {
|
|
1205
|
+
const text2 = String(item.text ?? "");
|
|
1206
|
+
if (text2.trim()) parts.push({ type: "text", text: text2, time_created: timestampMs });
|
|
1207
|
+
} else if (typeof item === "string" && item.trim()) {
|
|
1208
|
+
parts.push({ type: "text", text: item, time_created: timestampMs });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return parts;
|
|
1212
|
+
}
|
|
1213
|
+
if (content == null) return [];
|
|
1214
|
+
const text = String(content);
|
|
1215
|
+
return text.trim() ? [{ type: "text", text, time_created: timestampMs }] : [];
|
|
1216
|
+
}
|
|
1217
|
+
function normalizeWireToolOutputParts(returnValue, timestampMs) {
|
|
1218
|
+
if (returnValue == null) return [];
|
|
1219
|
+
if (typeof returnValue === "string") {
|
|
1220
|
+
return returnValue.trim() ? [{ type: "text", text: returnValue, time_created: timestampMs }] : [];
|
|
1221
|
+
}
|
|
1222
|
+
if (typeof returnValue === "object") {
|
|
1223
|
+
return [
|
|
1224
|
+
{ type: "text", text: JSON.stringify(returnValue, null, 2), time_created: timestampMs }
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1227
|
+
const text = String(returnValue);
|
|
1228
|
+
return text.trim() ? [{ type: "text", text, time_created: timestampMs }] : [];
|
|
1229
|
+
}
|
|
1230
|
+
var KimiAgent = class extends BaseAgent {
|
|
1231
|
+
name = "kimi";
|
|
1232
|
+
displayName = "Kimi-Cli";
|
|
1233
|
+
basePath = null;
|
|
1234
|
+
sessionMetaMap = /* @__PURE__ */ new Map();
|
|
1235
|
+
projectMap = /* @__PURE__ */ new Map();
|
|
1236
|
+
findBasePath() {
|
|
1237
|
+
const roots = resolveProviderRoots();
|
|
1238
|
+
return firstExisting(join4(roots.kimiRoot, "sessions"), "data/kimi");
|
|
1239
|
+
}
|
|
1240
|
+
/** Parse kimi.json and build md5(project_path) → cwd mapping */
|
|
1241
|
+
loadKimiConfig() {
|
|
1242
|
+
const roots = resolveProviderRoots();
|
|
1243
|
+
const configPath = join4(roots.kimiRoot, "kimi.json");
|
|
1244
|
+
if (!existsSync4(configPath)) return;
|
|
1245
|
+
try {
|
|
1246
|
+
const raw = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1247
|
+
const workDirs = raw?.work_dirs;
|
|
1248
|
+
if (!Array.isArray(workDirs)) return;
|
|
1249
|
+
for (const wd of workDirs) {
|
|
1250
|
+
const path = wd.path;
|
|
1251
|
+
if (typeof path !== "string") continue;
|
|
1252
|
+
const hash = createHash("md5").update(path).digest("hex");
|
|
1253
|
+
this.projectMap.set(hash, path);
|
|
1254
|
+
}
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
isAvailable() {
|
|
1259
|
+
this.basePath = this.findBasePath();
|
|
1260
|
+
if (!this.basePath) return false;
|
|
1261
|
+
this.loadKimiConfig();
|
|
1262
|
+
try {
|
|
1263
|
+
return this.listSessionDirs().length > 0;
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
/** Walk sessions/{project_hash}/{session_id}/ and find valid session dirs */
|
|
1269
|
+
listSessionDirs() {
|
|
1270
|
+
if (!this.basePath) return [];
|
|
1271
|
+
const dirs = [];
|
|
1272
|
+
try {
|
|
1273
|
+
for (const hashEntry of readdirSync2(this.basePath, { withFileTypes: true })) {
|
|
1274
|
+
if (!hashEntry.isDirectory()) continue;
|
|
1275
|
+
const hashPath = join4(this.basePath, hashEntry.name);
|
|
1276
|
+
try {
|
|
1277
|
+
for (const sessionEntry of readdirSync2(hashPath, { withFileTypes: true })) {
|
|
1278
|
+
if (!sessionEntry.isDirectory()) continue;
|
|
1279
|
+
const sessionPath = join4(hashPath, sessionEntry.name);
|
|
1280
|
+
if (existsSync4(join4(sessionPath, "metadata.json")) || existsSync4(join4(sessionPath, "state.json"))) {
|
|
1281
|
+
dirs.push(sessionPath);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
return dirs;
|
|
1290
|
+
}
|
|
1291
|
+
/** Parse session directory, preferring state.json over metadata.json */
|
|
1292
|
+
parseSessionDir(sessionDir) {
|
|
1293
|
+
try {
|
|
1294
|
+
const sessionId = basename3(sessionDir);
|
|
1295
|
+
const projectHash = basename3(dirname(sessionDir));
|
|
1296
|
+
const contextFile = join4(sessionDir, "context.jsonl");
|
|
1297
|
+
const wireFile = join4(sessionDir, "wire.jsonl");
|
|
1298
|
+
if (!existsSync4(contextFile) && !existsSync4(wireFile)) return null;
|
|
1299
|
+
const statePath = join4(sessionDir, "state.json");
|
|
1300
|
+
const metaPath = join4(sessionDir, "metadata.json");
|
|
1301
|
+
let title = "";
|
|
1302
|
+
let wireMtime = null;
|
|
1303
|
+
let metaFile = "";
|
|
1304
|
+
if (existsSync4(statePath)) {
|
|
1305
|
+
const state = JSON.parse(readFileSync3(statePath, "utf-8"));
|
|
1306
|
+
title = String(state.custom_title ?? "");
|
|
1307
|
+
wireMtime = typeof state.wire_mtime === "number" ? state.wire_mtime : null;
|
|
1308
|
+
metaFile = statePath;
|
|
1309
|
+
} else if (existsSync4(metaPath)) {
|
|
1310
|
+
const meta = JSON.parse(readFileSync3(metaPath, "utf-8"));
|
|
1311
|
+
title = String(meta.title ?? "");
|
|
1312
|
+
wireMtime = typeof meta.wire_mtime === "number" ? meta.wire_mtime : null;
|
|
1313
|
+
metaFile = metaPath;
|
|
1314
|
+
}
|
|
1315
|
+
const cwd = this.projectMap.get(projectHash) || "";
|
|
1316
|
+
const createdAt = wireMtime !== null ? wireMtime * 1e3 : metaFile ? statSync3(metaFile).mtimeMs : statSync3(sessionDir).mtimeMs;
|
|
1317
|
+
return {
|
|
1318
|
+
id: sessionId,
|
|
1319
|
+
title: title || "Untitled Session",
|
|
1320
|
+
sourcePath: sessionDir,
|
|
1321
|
+
cwd,
|
|
1322
|
+
contextFile: existsSync4(contextFile) ? contextFile : null,
|
|
1323
|
+
wireFile: existsSync4(wireFile) ? wireFile : null,
|
|
1324
|
+
createdAt,
|
|
1325
|
+
metaFile
|
|
1326
|
+
};
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
scan() {
|
|
1332
|
+
if (!this.basePath) return [];
|
|
1333
|
+
const scanMarker = perf.start("kimi:scan");
|
|
1334
|
+
const listMarker = perf.start("listSessionDirs");
|
|
1335
|
+
const sessionDirs = this.listSessionDirs();
|
|
1336
|
+
perf.end(listMarker);
|
|
1337
|
+
const heads = [];
|
|
1338
|
+
for (const dir of sessionDirs) {
|
|
1339
|
+
try {
|
|
1340
|
+
const parseMarker = perf.start(`parseSessionDir:${basename3(dir)}`);
|
|
1341
|
+
const meta = this.parseSessionDir(dir);
|
|
1342
|
+
perf.end(parseMarker);
|
|
1343
|
+
if (!meta) continue;
|
|
1344
|
+
this.sessionMetaMap.set(meta.id, meta);
|
|
1345
|
+
heads.push({
|
|
1346
|
+
id: meta.id,
|
|
1347
|
+
slug: `kimi/${meta.id}`,
|
|
1348
|
+
title: meta.title,
|
|
1349
|
+
directory: meta.cwd,
|
|
1350
|
+
time_created: meta.createdAt,
|
|
1351
|
+
time_updated: meta.createdAt,
|
|
1352
|
+
stats: {
|
|
1353
|
+
message_count: 0,
|
|
1354
|
+
total_input_tokens: 0,
|
|
1355
|
+
total_output_tokens: 0,
|
|
1356
|
+
total_cost: 0
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
perf.end(scanMarker);
|
|
1363
|
+
return heads;
|
|
1364
|
+
}
|
|
1365
|
+
getSessionMetaMap() {
|
|
1366
|
+
return this.sessionMetaMap;
|
|
1367
|
+
}
|
|
1368
|
+
setSessionMetaMap(meta) {
|
|
1369
|
+
this.sessionMetaMap = meta;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* 检测文件系统变更
|
|
1373
|
+
*/
|
|
1374
|
+
checkForChanges(sinceTimestamp, cachedSessions) {
|
|
1375
|
+
const changedIds = [];
|
|
1376
|
+
for (const session of cachedSessions) {
|
|
1377
|
+
const meta = this.sessionMetaMap.get(session.id);
|
|
1378
|
+
if (!meta) continue;
|
|
1379
|
+
try {
|
|
1380
|
+
if (meta.metaFile) {
|
|
1381
|
+
const stat = statSync3(meta.metaFile);
|
|
1382
|
+
if (stat.mtimeMs > sinceTimestamp) {
|
|
1383
|
+
changedIds.push(session.id);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const dataFile = meta.wireFile || meta.contextFile;
|
|
1388
|
+
if (dataFile) {
|
|
1389
|
+
const dataStat = statSync3(dataFile);
|
|
1390
|
+
if (dataStat.mtimeMs > sinceTimestamp) {
|
|
1391
|
+
changedIds.push(session.id);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
} catch {
|
|
1395
|
+
changedIds.push(session.id);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
hasChanges: changedIds.length > 0,
|
|
1400
|
+
changedIds,
|
|
1401
|
+
timestamp: Date.now()
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* 增量扫描
|
|
1406
|
+
*/
|
|
1407
|
+
incrementalScan(cachedSessions, changedIds) {
|
|
1408
|
+
const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
|
|
1409
|
+
for (const dir of this.listSessionDirs()) {
|
|
1410
|
+
try {
|
|
1411
|
+
const meta = this.parseSessionDir(dir);
|
|
1412
|
+
if (!meta) continue;
|
|
1413
|
+
if (changedIds.includes(meta.id)) {
|
|
1414
|
+
this.sessionMetaMap.set(meta.id, meta);
|
|
1415
|
+
sessionMap.set(meta.id, {
|
|
1416
|
+
id: meta.id,
|
|
1417
|
+
slug: `kimi/${meta.id}`,
|
|
1418
|
+
title: meta.title,
|
|
1419
|
+
directory: meta.cwd,
|
|
1420
|
+
time_created: meta.createdAt,
|
|
1421
|
+
time_updated: meta.createdAt,
|
|
1422
|
+
stats: {
|
|
1423
|
+
message_count: 0,
|
|
1424
|
+
total_input_tokens: 0,
|
|
1425
|
+
total_output_tokens: 0,
|
|
1426
|
+
total_cost: 0
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return Array.from(sessionMap.values());
|
|
1434
|
+
}
|
|
1435
|
+
getSessionData(sessionId) {
|
|
1436
|
+
const meta = this.sessionMetaMap.get(sessionId);
|
|
1437
|
+
if (!meta) throw new Error(`Session not found: ${sessionId}`);
|
|
1438
|
+
if (meta.contextFile) {
|
|
1439
|
+
return this.getSessionDataFromContext(meta);
|
|
1440
|
+
}
|
|
1441
|
+
return this.getSessionDataFromWire(meta);
|
|
1442
|
+
}
|
|
1443
|
+
getSessionDataFromContext(meta) {
|
|
1444
|
+
if (!meta.contextFile) throw new Error("context.jsonl is missing");
|
|
1445
|
+
const content = readFileSync3(meta.contextFile, "utf-8");
|
|
1446
|
+
const messages = [];
|
|
1447
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
1448
|
+
const ignoredToolCallIds = /* @__PURE__ */ new Set();
|
|
1449
|
+
let seq = 0;
|
|
1450
|
+
for (const record of parseJsonlLines(content)) {
|
|
1451
|
+
seq++;
|
|
1452
|
+
try {
|
|
1453
|
+
const role = String(record.role ?? "");
|
|
1454
|
+
if (role === "_checkpoint" || role === "_usage") continue;
|
|
1455
|
+
if (role === "user") {
|
|
1456
|
+
const text = String(record.content ?? "");
|
|
1457
|
+
if (text.trim()) {
|
|
1458
|
+
messages.push(
|
|
1459
|
+
this.buildMessage({
|
|
1460
|
+
messageId: `context-${seq}`,
|
|
1461
|
+
role: "user",
|
|
1462
|
+
timestampMs: 0,
|
|
1463
|
+
parts: [{ type: "text", text, time_created: 0 }]
|
|
1464
|
+
})
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (role === "assistant") {
|
|
1470
|
+
const { message, toolIndexes } = this.buildContextAssistantMessage(
|
|
1471
|
+
record,
|
|
1472
|
+
seq,
|
|
1473
|
+
ignoredToolCallIds
|
|
1474
|
+
);
|
|
1475
|
+
if (!message) continue;
|
|
1476
|
+
const msgIndex = messages.length;
|
|
1477
|
+
messages.push(message);
|
|
1478
|
+
for (const [callId, partIndex] of toolIndexes) {
|
|
1479
|
+
pendingToolCalls.set(callId, [msgIndex, partIndex]);
|
|
1480
|
+
}
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (role === "tool") {
|
|
1484
|
+
const callId = String(record.tool_call_id ?? "").trim();
|
|
1485
|
+
if (callId && ignoredToolCallIds.has(callId)) continue;
|
|
1486
|
+
const outputParts = normalizeToolOutputParts(record.content, 0);
|
|
1487
|
+
if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (outputParts.length > 0) {
|
|
1491
|
+
messages.push(
|
|
1492
|
+
this.buildMessage({
|
|
1493
|
+
messageId: `context-${seq}`,
|
|
1494
|
+
role: "tool",
|
|
1495
|
+
timestampMs: 0,
|
|
1496
|
+
parts: outputParts
|
|
1497
|
+
})
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const stats = this.extractStats(meta.sourcePath);
|
|
1505
|
+
return this.buildSessionData(meta, messages, stats);
|
|
1506
|
+
}
|
|
1507
|
+
getSessionDataFromWire(meta) {
|
|
1508
|
+
const wirePath = meta.wireFile ?? join4(meta.sourcePath, "wire.jsonl");
|
|
1509
|
+
if (!existsSync4(wirePath)) throw new Error("wire.jsonl is missing");
|
|
1510
|
+
const content = readFileSync3(wirePath, "utf-8");
|
|
1511
|
+
const messages = [];
|
|
1512
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
1513
|
+
const ignoredToolCallIds = /* @__PURE__ */ new Set();
|
|
1514
|
+
const openToolArgumentBuffer = /* @__PURE__ */ new Map();
|
|
1515
|
+
let currentAssistantIndex = null;
|
|
1516
|
+
let openToolCallId = null;
|
|
1517
|
+
let seq = 0;
|
|
1518
|
+
for (const record of parseJsonlLines(content)) {
|
|
1519
|
+
seq++;
|
|
1520
|
+
try {
|
|
1521
|
+
const message = record.message ?? {};
|
|
1522
|
+
const msgType = String(message.type ?? "");
|
|
1523
|
+
const payload = message.payload ?? {};
|
|
1524
|
+
const timestamp = Number(record.timestamp ?? 0);
|
|
1525
|
+
const timestampMs = Number.isFinite(timestamp) ? Math.floor(timestamp * 1e3) : 0;
|
|
1526
|
+
if (msgType === "TurnBegin") {
|
|
1527
|
+
const userInput = payload.user_input;
|
|
1528
|
+
if (Array.isArray(userInput) && userInput.length > 0) {
|
|
1529
|
+
const text = String(userInput[0]?.text ?? "");
|
|
1530
|
+
if (text.trim()) {
|
|
1531
|
+
messages.push(
|
|
1532
|
+
this.buildMessage({
|
|
1533
|
+
messageId: `wire-${seq}`,
|
|
1534
|
+
role: "user",
|
|
1535
|
+
timestampMs,
|
|
1536
|
+
parts: [{ type: "text", text, time_created: timestampMs }]
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
currentAssistantIndex = null;
|
|
1542
|
+
openToolCallId = null;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (msgType === "ContentPart") {
|
|
1546
|
+
currentAssistantIndex = this.getOrCreateWireAssistant(
|
|
1547
|
+
messages,
|
|
1548
|
+
currentAssistantIndex,
|
|
1549
|
+
`wire-${seq}`
|
|
1550
|
+
);
|
|
1551
|
+
const assistant = messages[currentAssistantIndex];
|
|
1552
|
+
const partType = String(payload.type ?? "");
|
|
1553
|
+
if (partType === "think") {
|
|
1554
|
+
const text = String(payload.think ?? "");
|
|
1555
|
+
if (text.trim()) {
|
|
1556
|
+
assistant.parts.push({ type: "reasoning", text, time_created: timestampMs });
|
|
1557
|
+
}
|
|
1558
|
+
} else if (partType === "text") {
|
|
1559
|
+
const text = String(payload.text ?? "");
|
|
1560
|
+
if (text.trim()) {
|
|
1561
|
+
assistant.parts.push({ type: "text", text, time_created: timestampMs });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
if (msgType === "ToolCall") {
|
|
1567
|
+
const function_ = payload.function;
|
|
1568
|
+
const toolName = String(function_?.name ?? "").trim();
|
|
1569
|
+
const callId = String(payload.id ?? "").trim();
|
|
1570
|
+
if (toolName && callId && KIMI_IGNORED_TOOLS.has(toolName)) {
|
|
1571
|
+
ignoredToolCallIds.add(callId);
|
|
1572
|
+
openToolCallId = callId;
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (!function_ || !callId || !toolName) continue;
|
|
1576
|
+
currentAssistantIndex = this.getOrCreateWireAssistant(
|
|
1577
|
+
messages,
|
|
1578
|
+
currentAssistantIndex,
|
|
1579
|
+
`wire-${seq}`
|
|
1580
|
+
);
|
|
1581
|
+
const assistant = messages[currentAssistantIndex];
|
|
1582
|
+
const rawArgs = function_.arguments;
|
|
1583
|
+
const normalizedArgs = normalizeToolArguments(rawArgs);
|
|
1584
|
+
const buffer = typeof rawArgs === "string" && typeof normalizedArgs !== "string" ? rawArgs : null;
|
|
1585
|
+
const toolPart = {
|
|
1586
|
+
type: "tool",
|
|
1587
|
+
tool: toolName,
|
|
1588
|
+
callID: callId,
|
|
1589
|
+
title: mapToolTitle(toolName),
|
|
1590
|
+
state: { arguments: normalizedArgs, output: null },
|
|
1591
|
+
time_created: timestampMs
|
|
1592
|
+
};
|
|
1593
|
+
const partIndex = assistant.parts.length;
|
|
1594
|
+
assistant.parts.push(toolPart);
|
|
1595
|
+
assistant.mode = "tool";
|
|
1596
|
+
pendingToolCalls.set(callId, [currentAssistantIndex, partIndex]);
|
|
1597
|
+
openToolCallId = callId;
|
|
1598
|
+
if (buffer !== null) {
|
|
1599
|
+
openToolArgumentBuffer.set(callId, buffer);
|
|
1600
|
+
}
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (msgType === "ToolCallPart") {
|
|
1604
|
+
if (openToolCallId && ignoredToolCallIds.has(openToolCallId)) continue;
|
|
1605
|
+
const argumentsPart = String(payload.arguments_part ?? "");
|
|
1606
|
+
this.appendWireToolCallPart(
|
|
1607
|
+
argumentsPart,
|
|
1608
|
+
openToolCallId,
|
|
1609
|
+
openToolArgumentBuffer,
|
|
1610
|
+
messages,
|
|
1611
|
+
pendingToolCalls
|
|
1612
|
+
);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (msgType === "ToolResult") {
|
|
1616
|
+
const callId = String(payload.tool_call_id ?? "").trim();
|
|
1617
|
+
if (callId && ignoredToolCallIds.has(callId)) continue;
|
|
1618
|
+
const outputParts = normalizeWireToolOutputParts(payload.return_value, timestampMs);
|
|
1619
|
+
if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (outputParts.length > 0) {
|
|
1623
|
+
messages.push(
|
|
1624
|
+
this.buildMessage({
|
|
1625
|
+
messageId: `wire-${seq}`,
|
|
1626
|
+
role: "tool",
|
|
1627
|
+
timestampMs,
|
|
1628
|
+
parts: outputParts
|
|
1629
|
+
})
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
} catch {
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const filteredMessages = messages.filter((m) => m.parts.length > 0);
|
|
1638
|
+
const stats = this.extractStats(meta.sourcePath);
|
|
1639
|
+
return this.buildSessionData(meta, filteredMessages, stats);
|
|
1640
|
+
}
|
|
1641
|
+
// --- Helpers ---
|
|
1642
|
+
buildMessage(opts) {
|
|
1643
|
+
return {
|
|
1644
|
+
id: opts.messageId,
|
|
1645
|
+
role: opts.role,
|
|
1646
|
+
agent: opts.agent ?? null,
|
|
1647
|
+
time_created: opts.timestampMs,
|
|
1648
|
+
mode: opts.mode ?? null,
|
|
1649
|
+
model: opts.model ?? null,
|
|
1650
|
+
provider: opts.provider ?? null,
|
|
1651
|
+
tokens: opts.tokens ? opts.tokens : void 0,
|
|
1652
|
+
cost: opts.cost ?? 0,
|
|
1653
|
+
parts: opts.parts
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
buildContextAssistantMessage(record, seq, ignoredToolCallIds) {
|
|
1657
|
+
const parts = [];
|
|
1658
|
+
const toolIndexes = /* @__PURE__ */ new Map();
|
|
1659
|
+
const content = record.content;
|
|
1660
|
+
if (Array.isArray(content)) {
|
|
1661
|
+
for (const item of content) {
|
|
1662
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1663
|
+
const ci = item;
|
|
1664
|
+
const partType = String(ci.type ?? "");
|
|
1665
|
+
if (partType === "think") {
|
|
1666
|
+
const text = String(ci.think ?? "");
|
|
1667
|
+
if (text.trim()) parts.push({ type: "reasoning", text, time_created: 0 });
|
|
1668
|
+
} else if (partType === "text") {
|
|
1669
|
+
const text = String(ci.text ?? "");
|
|
1670
|
+
if (text.trim()) parts.push({ type: "text", text, time_created: 0 });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const toolCalls = record.tool_calls;
|
|
1675
|
+
if (Array.isArray(toolCalls)) {
|
|
1676
|
+
for (const tc of toolCalls) {
|
|
1677
|
+
if (typeof tc !== "object" || tc === null) continue;
|
|
1678
|
+
const tcRecord = tc;
|
|
1679
|
+
const function_ = tcRecord.function;
|
|
1680
|
+
if (!function_) continue;
|
|
1681
|
+
const toolName = String(function_.name ?? "").trim();
|
|
1682
|
+
const callId = String(tcRecord.id ?? "").trim();
|
|
1683
|
+
if (toolName && callId && KIMI_IGNORED_TOOLS.has(toolName)) {
|
|
1684
|
+
ignoredToolCallIds.add(callId);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (!toolName || !callId) continue;
|
|
1688
|
+
const part = {
|
|
1689
|
+
type: "tool",
|
|
1690
|
+
tool: toolName,
|
|
1691
|
+
callID: callId,
|
|
1692
|
+
title: mapToolTitle(toolName),
|
|
1693
|
+
state: { arguments: normalizeToolArguments(function_.arguments), output: null },
|
|
1694
|
+
time_created: 0
|
|
1695
|
+
};
|
|
1696
|
+
toolIndexes.set(callId, parts.length);
|
|
1697
|
+
parts.push(part);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (parts.length === 0) {
|
|
1701
|
+
return {
|
|
1702
|
+
message: this.buildMessage({
|
|
1703
|
+
messageId: `context-${seq}`,
|
|
1704
|
+
role: "assistant",
|
|
1705
|
+
timestampMs: 0,
|
|
1706
|
+
parts: []
|
|
1707
|
+
}),
|
|
1708
|
+
toolIndexes
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
const allTools = parts.every((p) => p.type === "tool");
|
|
1712
|
+
const message = this.buildMessage({
|
|
1713
|
+
messageId: `context-${seq}`,
|
|
1714
|
+
role: "assistant",
|
|
1715
|
+
timestampMs: 0,
|
|
1716
|
+
parts,
|
|
1717
|
+
agent: "kimi",
|
|
1718
|
+
mode: allTools ? "tool" : void 0
|
|
1719
|
+
});
|
|
1720
|
+
return { message, toolIndexes };
|
|
1721
|
+
}
|
|
1722
|
+
getOrCreateWireAssistant(messages, currentIndex, messageId) {
|
|
1723
|
+
if (currentIndex !== null) return currentIndex;
|
|
1724
|
+
messages.push(
|
|
1725
|
+
this.buildMessage({
|
|
1726
|
+
messageId,
|
|
1727
|
+
role: "assistant",
|
|
1728
|
+
timestampMs: 0,
|
|
1729
|
+
parts: [],
|
|
1730
|
+
agent: "kimi"
|
|
1731
|
+
})
|
|
1732
|
+
);
|
|
1733
|
+
return messages.length - 1;
|
|
1734
|
+
}
|
|
1735
|
+
appendWireToolCallPart(argumentsPart, openCallId, buffer, messages, pendingToolCalls) {
|
|
1736
|
+
if (!openCallId || !pendingToolCalls.has(openCallId)) return;
|
|
1737
|
+
const existing = buffer.get(openCallId) ?? "";
|
|
1738
|
+
const combined = existing + argumentsPart;
|
|
1739
|
+
try {
|
|
1740
|
+
const parsed = JSON.parse(combined);
|
|
1741
|
+
const location = pendingToolCalls.get(openCallId);
|
|
1742
|
+
if (!location) return;
|
|
1743
|
+
const msgPart = messages[location[0]]?.parts[location[1]];
|
|
1744
|
+
if (msgPart?.state) {
|
|
1745
|
+
msgPart.state.arguments = parsed;
|
|
1746
|
+
}
|
|
1747
|
+
buffer.delete(openCallId);
|
|
1748
|
+
} catch {
|
|
1749
|
+
buffer.set(openCallId, combined);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
backfillToolOutput(messages, pendingToolCalls, callId, outputParts) {
|
|
1753
|
+
if (!outputParts.length || !callId) return false;
|
|
1754
|
+
const location = pendingToolCalls.get(callId);
|
|
1755
|
+
if (!location) return false;
|
|
1756
|
+
const part = messages[location[0]]?.parts[location[1]];
|
|
1757
|
+
if (!part) return false;
|
|
1758
|
+
if (!part.state) part.state = {};
|
|
1759
|
+
part.state.output = [...outputParts];
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
extractStats(sessionDir) {
|
|
1763
|
+
const stats = {
|
|
1764
|
+
total_cost: 0,
|
|
1765
|
+
total_input_tokens: 0,
|
|
1766
|
+
total_output_tokens: 0,
|
|
1767
|
+
total_tokens: 0,
|
|
1768
|
+
message_count: 0
|
|
1769
|
+
};
|
|
1770
|
+
const wirePath = join4(sessionDir, "wire.jsonl");
|
|
1771
|
+
if (!existsSync4(wirePath)) return stats;
|
|
1772
|
+
try {
|
|
1773
|
+
const content = readFileSync3(wirePath, "utf-8");
|
|
1774
|
+
for (const line of content.split("\n").filter((l) => l.trim())) {
|
|
1775
|
+
try {
|
|
1776
|
+
const data = JSON.parse(line);
|
|
1777
|
+
const tokenUsage = data.message?.usage;
|
|
1778
|
+
if (!tokenUsage) continue;
|
|
1779
|
+
stats.total_input_tokens += Number(tokenUsage.input_tokens ?? 0);
|
|
1780
|
+
stats.total_output_tokens += Number(tokenUsage.output_tokens ?? 0);
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1786
|
+
const contextPath = join4(sessionDir, "context.jsonl");
|
|
1787
|
+
const rawPath = existsSync4(contextPath) ? contextPath : wirePath;
|
|
1788
|
+
if (!existsSync4(rawPath)) return stats;
|
|
1789
|
+
try {
|
|
1790
|
+
const rawContent = readFileSync3(rawPath, "utf-8");
|
|
1791
|
+
for (const line of rawContent.split("\n").filter((l) => l.trim())) {
|
|
1792
|
+
try {
|
|
1793
|
+
const data = JSON.parse(line);
|
|
1794
|
+
if (data.role === "_usage" && typeof data.token_count === "number") {
|
|
1795
|
+
stats.total_tokens = data.token_count;
|
|
1796
|
+
}
|
|
1797
|
+
} catch {
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
return stats;
|
|
1803
|
+
}
|
|
1804
|
+
buildSessionData(meta, messages, stats) {
|
|
1805
|
+
stats.message_count = messages.length;
|
|
1806
|
+
return {
|
|
1807
|
+
id: meta.id,
|
|
1808
|
+
title: meta.title,
|
|
1809
|
+
slug: `kimi/${meta.id}`,
|
|
1810
|
+
directory: meta.cwd,
|
|
1811
|
+
time_created: meta.createdAt,
|
|
1812
|
+
time_updated: meta.createdAt,
|
|
1813
|
+
stats,
|
|
1814
|
+
messages
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
var PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/;
|
|
1819
|
+
var PLAN_APPROVAL_PREFIX = "PLEASE IMPLEMENT THIS PLAN";
|
|
1820
|
+
var SUBAGENT_NOTIFICATION_PATTERN = /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/;
|
|
1821
|
+
var DEVELOPER_LIKE_USER_MARKERS = [
|
|
1822
|
+
"agents.md instructions for",
|
|
1823
|
+
"<instructions>",
|
|
1824
|
+
"<environment_context>",
|
|
1825
|
+
"<permissions instructions>",
|
|
1826
|
+
"<collaboration_mode>"
|
|
1827
|
+
];
|
|
1828
|
+
function isDeveloperLikeUserMessage(text) {
|
|
1829
|
+
const lower = text.toLowerCase();
|
|
1830
|
+
return DEVELOPER_LIKE_USER_MARKERS.some((m) => lower.includes(m));
|
|
1831
|
+
}
|
|
1832
|
+
var CODEX_TOOL_TITLE_MAP = {
|
|
1833
|
+
exec_command: "bash",
|
|
1834
|
+
apply_patch: "patch",
|
|
1835
|
+
patch: "patch",
|
|
1836
|
+
spawn_agent: "subagent",
|
|
1837
|
+
subagent: "subagent"
|
|
1838
|
+
};
|
|
1839
|
+
function extractSessionId(filename) {
|
|
1840
|
+
const stem = basename4(filename, ".jsonl");
|
|
1841
|
+
const parts = stem.split("-");
|
|
1842
|
+
if (parts.length >= 5) {
|
|
1843
|
+
return parts.slice(-5).join("-");
|
|
1844
|
+
}
|
|
1845
|
+
return stem;
|
|
1846
|
+
}
|
|
1847
|
+
function parseTimestampMs2(data) {
|
|
1848
|
+
const ts = String(data["timestamp"] ?? "").trim();
|
|
1849
|
+
if (!ts) return 0;
|
|
1850
|
+
try {
|
|
1851
|
+
return new Date(ts.includes("Z") ? ts : ts.replace(" ", "T") + "Z").getTime();
|
|
1852
|
+
} catch {
|
|
1853
|
+
return 0;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
function normalizeTitleText3(text) {
|
|
1857
|
+
const line = text.split("\n").find((l) => l.trim());
|
|
1858
|
+
return line?.trim().slice(0, 80) || "";
|
|
1859
|
+
}
|
|
1860
|
+
function mapToolTitle2(name) {
|
|
1861
|
+
return CODEX_TOOL_TITLE_MAP[name] ?? name;
|
|
1862
|
+
}
|
|
1863
|
+
function normalizeToolArguments2(raw) {
|
|
1864
|
+
if (typeof raw === "string") {
|
|
1865
|
+
try {
|
|
1866
|
+
return JSON.parse(raw);
|
|
1867
|
+
} catch {
|
|
1868
|
+
return raw;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return raw;
|
|
1872
|
+
}
|
|
1873
|
+
function normalizeCustomToolArguments(toolName, input) {
|
|
1874
|
+
if (toolName === "apply_patch") {
|
|
1875
|
+
return parseApplyPatchInput(input);
|
|
1876
|
+
}
|
|
1877
|
+
return input;
|
|
1878
|
+
}
|
|
1879
|
+
var PATCH_BEGIN_RE = /\*\*\* Begin Patch/;
|
|
1880
|
+
var PATCH_END_RE = /\*\*\* End Patch/;
|
|
1881
|
+
var PATCH_HEADER_RE = /\*\*\*\s+(Add|Delete|Update|Move)\s+File:\s*(.+)/;
|
|
1882
|
+
var PATCH_MOVE_TO_RE = /\*\*\*\s+Move to:\s*(.+)/;
|
|
1883
|
+
function parseApplyPatchInput(input) {
|
|
1884
|
+
const text = typeof input === "string" ? input : "";
|
|
1885
|
+
if (!text) return [];
|
|
1886
|
+
const blocks = [];
|
|
1887
|
+
const lines = text.split("\n");
|
|
1888
|
+
let inPatch = false;
|
|
1889
|
+
let i = 0;
|
|
1890
|
+
while (i < lines.length) {
|
|
1891
|
+
const line = lines[i];
|
|
1892
|
+
if (!inPatch && PATCH_BEGIN_RE.test(line)) {
|
|
1893
|
+
inPatch = true;
|
|
1894
|
+
i++;
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (inPatch && PATCH_END_RE.test(line)) {
|
|
1898
|
+
inPatch = false;
|
|
1899
|
+
i++;
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (inPatch) {
|
|
1903
|
+
const headerMatch = line.match(PATCH_HEADER_RE);
|
|
1904
|
+
if (headerMatch) {
|
|
1905
|
+
const action = headerMatch[1];
|
|
1906
|
+
const filePath = headerMatch[2].trim();
|
|
1907
|
+
i++;
|
|
1908
|
+
if (action === "Add") {
|
|
1909
|
+
const content = extractPatchContent(lines, i);
|
|
1910
|
+
i = content.nextLineIndex;
|
|
1911
|
+
blocks.push({ type: "write_file", path: filePath, content: content.text });
|
|
1912
|
+
} else if (action === "Update") {
|
|
1913
|
+
let moveToTarget = null;
|
|
1914
|
+
let contentStart = i;
|
|
1915
|
+
for (let j = i; j < lines.length; j++) {
|
|
1916
|
+
const l = lines[j];
|
|
1917
|
+
if (!l.trim()) continue;
|
|
1918
|
+
const moveMatch = l.match(PATCH_MOVE_TO_RE);
|
|
1919
|
+
if (moveMatch) {
|
|
1920
|
+
moveToTarget = moveMatch[1].trim();
|
|
1921
|
+
contentStart = j + 1;
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
break;
|
|
1925
|
+
}
|
|
1926
|
+
if (moveToTarget) {
|
|
1927
|
+
const content = extractPatchContent(lines, contentStart);
|
|
1928
|
+
i = content.nextLineIndex;
|
|
1929
|
+
blocks.push({
|
|
1930
|
+
type: "move_file",
|
|
1931
|
+
path: filePath,
|
|
1932
|
+
targetPath: moveToTarget,
|
|
1933
|
+
content: content.text
|
|
1934
|
+
});
|
|
1935
|
+
} else {
|
|
1936
|
+
const content = extractPatchContent(lines, i);
|
|
1937
|
+
i = content.nextLineIndex;
|
|
1938
|
+
blocks.push({ type: "edit_file", path: filePath, content: content.text });
|
|
1939
|
+
}
|
|
1940
|
+
} else if (action === "Delete") {
|
|
1941
|
+
blocks.push({ type: "delete_file", path: filePath });
|
|
1942
|
+
}
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
i++;
|
|
1947
|
+
}
|
|
1948
|
+
return blocks;
|
|
1949
|
+
}
|
|
1950
|
+
function extractPatchContent(lines, startIndex) {
|
|
1951
|
+
const contentLines = [];
|
|
1952
|
+
let i = startIndex;
|
|
1953
|
+
while (i < lines.length) {
|
|
1954
|
+
const line = lines[i];
|
|
1955
|
+
if (PATCH_HEADER_RE.test(line) || PATCH_END_RE.test(line)) break;
|
|
1956
|
+
contentLines.push(line);
|
|
1957
|
+
i++;
|
|
1958
|
+
}
|
|
1959
|
+
return { text: contentLines.join("\n"), nextLineIndex: i };
|
|
1960
|
+
}
|
|
1961
|
+
var CodexAgent = class extends BaseAgent {
|
|
1962
|
+
name = "codex";
|
|
1963
|
+
displayName = "Codex";
|
|
1964
|
+
basePath = null;
|
|
1965
|
+
sessionIndexCache = /* @__PURE__ */ new Map();
|
|
1966
|
+
sessionMetaMap = /* @__PURE__ */ new Map();
|
|
1967
|
+
// ---- BaseAgent implementation ----
|
|
1968
|
+
findBasePath() {
|
|
1969
|
+
const roots = resolveProviderRoots();
|
|
1970
|
+
return firstExisting(join5(roots.codexRoot, "sessions"));
|
|
1971
|
+
}
|
|
1972
|
+
isAvailable() {
|
|
1973
|
+
this.basePath = this.findBasePath();
|
|
1974
|
+
if (!this.basePath) return false;
|
|
1975
|
+
try {
|
|
1976
|
+
const files = this.walkDirForRolloutFiles(this.basePath);
|
|
1977
|
+
return files.length > 0;
|
|
1978
|
+
} catch {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
scan() {
|
|
1983
|
+
if (!this.basePath) return [];
|
|
1984
|
+
const scanMarker = perf.start("codex:scan");
|
|
1985
|
+
const indexMarker = perf.start("loadSessionIndex");
|
|
1986
|
+
this.loadSessionIndex();
|
|
1987
|
+
perf.end(indexMarker);
|
|
1988
|
+
const heads = [];
|
|
1989
|
+
const listMarker = perf.start("listRolloutFiles");
|
|
1990
|
+
const files = this.listRolloutFiles();
|
|
1991
|
+
perf.end(listMarker);
|
|
1992
|
+
for (const file of files) {
|
|
1993
|
+
try {
|
|
1994
|
+
const parseMarker = perf.start(`parseSessionHead:${basename4(file)}`);
|
|
1995
|
+
const head = this.parseSessionHead(file);
|
|
1996
|
+
perf.end(parseMarker);
|
|
1997
|
+
if (head) {
|
|
1998
|
+
heads.push(head);
|
|
1999
|
+
this.sessionMetaMap.set(head.id, {
|
|
2000
|
+
id: head.id,
|
|
2001
|
+
title: head.title,
|
|
2002
|
+
sourcePath: file,
|
|
2003
|
+
directory: head.directory,
|
|
2004
|
+
model: null,
|
|
2005
|
+
messageCount: head.stats.message_count,
|
|
2006
|
+
createdAt: head.time_created,
|
|
2007
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
} catch {
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
perf.end(scanMarker);
|
|
2014
|
+
return heads;
|
|
2015
|
+
}
|
|
2016
|
+
getSessionMetaMap() {
|
|
2017
|
+
return this.sessionMetaMap;
|
|
2018
|
+
}
|
|
2019
|
+
setSessionMetaMap(meta) {
|
|
2020
|
+
this.sessionMetaMap = meta;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* 检测文件系统变更
|
|
2024
|
+
*/
|
|
2025
|
+
checkForChanges(sinceTimestamp, cachedSessions) {
|
|
2026
|
+
if (!this.basePath) {
|
|
2027
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
2028
|
+
}
|
|
2029
|
+
const changedIds = [];
|
|
2030
|
+
for (const session of cachedSessions) {
|
|
2031
|
+
const meta = this.sessionMetaMap.get(session.id);
|
|
2032
|
+
if (!meta) continue;
|
|
2033
|
+
try {
|
|
2034
|
+
const stat = statSync4(meta.sourcePath);
|
|
2035
|
+
if (stat.mtimeMs > sinceTimestamp) {
|
|
2036
|
+
changedIds.push(session.id);
|
|
2037
|
+
}
|
|
2038
|
+
} catch {
|
|
2039
|
+
changedIds.push(session.id);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
const allFiles = this.listRolloutFiles();
|
|
2044
|
+
const hasNewFiles = allFiles.length > cachedSessions.length;
|
|
2045
|
+
return {
|
|
2046
|
+
hasChanges: changedIds.length > 0 || hasNewFiles,
|
|
2047
|
+
changedIds,
|
|
2048
|
+
timestamp: Date.now()
|
|
2049
|
+
};
|
|
2050
|
+
} catch {
|
|
2051
|
+
return {
|
|
2052
|
+
hasChanges: changedIds.length > 0,
|
|
2053
|
+
changedIds,
|
|
2054
|
+
timestamp: Date.now()
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* 增量扫描
|
|
2060
|
+
*/
|
|
2061
|
+
incrementalScan(cachedSessions, changedIds) {
|
|
2062
|
+
if (!this.basePath) return cachedSessions;
|
|
2063
|
+
const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
|
|
2064
|
+
for (const file of this.listRolloutFiles()) {
|
|
2065
|
+
try {
|
|
2066
|
+
const sessionId = extractSessionId(file);
|
|
2067
|
+
if (changedIds.includes(sessionId)) {
|
|
2068
|
+
const head = this.parseSessionHead(file);
|
|
2069
|
+
if (head) {
|
|
2070
|
+
sessionMap.set(head.id, head);
|
|
2071
|
+
this.sessionMetaMap.set(head.id, {
|
|
2072
|
+
id: head.id,
|
|
2073
|
+
title: head.title,
|
|
2074
|
+
sourcePath: file,
|
|
2075
|
+
directory: head.directory,
|
|
2076
|
+
model: null,
|
|
2077
|
+
messageCount: head.stats.message_count,
|
|
2078
|
+
createdAt: head.time_created,
|
|
2079
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
} catch {
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
for (const file of this.listRolloutFiles()) {
|
|
2087
|
+
try {
|
|
2088
|
+
const sessionId = extractSessionId(file);
|
|
2089
|
+
if (!sessionMap.has(sessionId)) {
|
|
2090
|
+
const head = this.parseSessionHead(file);
|
|
2091
|
+
if (head) {
|
|
2092
|
+
sessionMap.set(head.id, head);
|
|
2093
|
+
this.sessionMetaMap.set(head.id, {
|
|
2094
|
+
id: head.id,
|
|
2095
|
+
title: head.title,
|
|
2096
|
+
sourcePath: file,
|
|
2097
|
+
directory: head.directory,
|
|
2098
|
+
model: null,
|
|
2099
|
+
messageCount: head.stats.message_count,
|
|
2100
|
+
createdAt: head.time_created,
|
|
2101
|
+
updatedAt: head.time_updated ?? head.time_created
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return Array.from(sessionMap.values());
|
|
2109
|
+
}
|
|
2110
|
+
getSessionData(sessionId) {
|
|
2111
|
+
const meta = this.sessionMetaMap.get(sessionId);
|
|
2112
|
+
if (!meta) throw new Error(`Session not found: ${sessionId}`);
|
|
2113
|
+
if (!existsSync5(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
|
|
2114
|
+
const content = readFileSync4(meta.sourcePath, "utf-8");
|
|
2115
|
+
const messages = [];
|
|
2116
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2117
|
+
let totalInputTokens = 0;
|
|
2118
|
+
let totalOutputTokens = 0;
|
|
2119
|
+
let currentAssistantIndex = null;
|
|
2120
|
+
let latestAssistantTextIndex = null;
|
|
2121
|
+
let pendingPlan = null;
|
|
2122
|
+
for (const record of parseJsonlLines(content)) {
|
|
2123
|
+
try {
|
|
2124
|
+
const result = this.convertRecord(
|
|
2125
|
+
record,
|
|
2126
|
+
messages,
|
|
2127
|
+
pendingToolCalls,
|
|
2128
|
+
meta.id,
|
|
2129
|
+
currentAssistantIndex,
|
|
2130
|
+
latestAssistantTextIndex,
|
|
2131
|
+
pendingPlan
|
|
2132
|
+
);
|
|
2133
|
+
currentAssistantIndex = result.currentAssistantIndex;
|
|
2134
|
+
latestAssistantTextIndex = result.latestAssistantTextIndex;
|
|
2135
|
+
pendingPlan = result.pendingPlan;
|
|
2136
|
+
this.extractTokenUsage(record, (input, output) => {
|
|
2137
|
+
totalInputTokens += input;
|
|
2138
|
+
totalOutputTokens += output;
|
|
2139
|
+
});
|
|
2140
|
+
} catch {
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (pendingPlan && currentAssistantIndex !== null) {
|
|
2144
|
+
messages[currentAssistantIndex].parts.push(pendingPlan);
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
id: meta.id,
|
|
2148
|
+
title: meta.title,
|
|
2149
|
+
slug: `codex/${meta.id}`,
|
|
2150
|
+
directory: meta.directory,
|
|
2151
|
+
time_created: meta.createdAt,
|
|
2152
|
+
time_updated: meta.updatedAt,
|
|
2153
|
+
stats: {
|
|
2154
|
+
message_count: messages.length,
|
|
2155
|
+
total_input_tokens: totalInputTokens,
|
|
2156
|
+
total_output_tokens: totalOutputTokens,
|
|
2157
|
+
total_cost: 0
|
|
2158
|
+
},
|
|
2159
|
+
messages
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
// ---- File listing ----
|
|
2163
|
+
listRolloutFiles() {
|
|
2164
|
+
if (!this.basePath) return [];
|
|
2165
|
+
try {
|
|
2166
|
+
return this.walkDirForRolloutFiles(this.basePath);
|
|
2167
|
+
} catch {
|
|
2168
|
+
return [];
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
walkDirForRolloutFiles(dir) {
|
|
2172
|
+
const files = [];
|
|
2173
|
+
try {
|
|
2174
|
+
for (const entry of readdirSync3(dir)) {
|
|
2175
|
+
const fullPath = join5(dir, entry);
|
|
2176
|
+
const stat = statSync4(fullPath);
|
|
2177
|
+
if (stat.isDirectory()) {
|
|
2178
|
+
files.push(...this.walkDirForRolloutFiles(fullPath));
|
|
2179
|
+
} else if (entry.endsWith(".jsonl") && entry.startsWith("rollout-")) {
|
|
2180
|
+
files.push(fullPath);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
return files;
|
|
2186
|
+
}
|
|
2187
|
+
// ---- Session index ----
|
|
2188
|
+
loadSessionIndex() {
|
|
2189
|
+
if (this.sessionIndexCache.size > 0) return;
|
|
2190
|
+
const roots = resolveProviderRoots();
|
|
2191
|
+
const indexPath = join5(roots.codexRoot, "session_index.jsonl");
|
|
2192
|
+
if (!existsSync5(indexPath)) return;
|
|
2193
|
+
try {
|
|
2194
|
+
const content = readFileSync4(indexPath, "utf-8");
|
|
2195
|
+
for (const record of parseJsonlLines(content)) {
|
|
2196
|
+
const sid = String(record["id"] ?? "").trim();
|
|
2197
|
+
const threadName = String(record["thread_name"] ?? "").trim();
|
|
2198
|
+
if (sid && threadName) {
|
|
2199
|
+
this.sessionIndexCache.set(sid, threadName);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
getTitleForSession(sessionId) {
|
|
2206
|
+
return this.sessionIndexCache.get(sessionId) ?? null;
|
|
2207
|
+
}
|
|
2208
|
+
// ---- Session head parsing ----
|
|
2209
|
+
parseSessionHead(filePath) {
|
|
2210
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
2211
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
2212
|
+
if (lines.length === 0) return null;
|
|
2213
|
+
const sessionId = extractSessionId(filePath);
|
|
2214
|
+
let firstRecord;
|
|
2215
|
+
try {
|
|
2216
|
+
firstRecord = JSON.parse(lines[0]);
|
|
2217
|
+
} catch {
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
const payload = firstRecord["payload"] ?? {};
|
|
2221
|
+
const createdAt = parseTimestampMs2(payload) || statSync4(filePath).mtimeMs;
|
|
2222
|
+
const indexTitle = this.getTitleForSession(sessionId);
|
|
2223
|
+
const messageTitle = this.extractTitleFromLines(lines);
|
|
2224
|
+
const directoryTitle = basenameTitle(payload["cwd"] ? String(payload["cwd"]) : null);
|
|
2225
|
+
const title = resolveSessionTitle(indexTitle, messageTitle, directoryTitle);
|
|
2226
|
+
let updatedAt = createdAt;
|
|
2227
|
+
let messageCount = 0;
|
|
2228
|
+
let model = null;
|
|
2229
|
+
const COUNTED_TYPES = /* @__PURE__ */ new Set(["message", "function_call", "function_call_output"]);
|
|
2230
|
+
for (const line of lines) {
|
|
2231
|
+
try {
|
|
2232
|
+
const data = JSON.parse(line);
|
|
2233
|
+
const recordType = String(data["type"] ?? "");
|
|
2234
|
+
if (recordType === "session_meta") {
|
|
2235
|
+
const p = data["payload"] ?? {};
|
|
2236
|
+
const ts = parseTimestampMs2(p);
|
|
2237
|
+
if (ts > updatedAt) updatedAt = ts;
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (recordType === "response_item") {
|
|
2241
|
+
const p = data["payload"] ?? {};
|
|
2242
|
+
const pType = String(p["type"] ?? "");
|
|
2243
|
+
if (COUNTED_TYPES.has(pType)) {
|
|
2244
|
+
messageCount++;
|
|
2245
|
+
}
|
|
2246
|
+
if (!model) {
|
|
2247
|
+
const info = p["info"];
|
|
2248
|
+
const m = info?.["model"] ?? p["model"];
|
|
2249
|
+
if (typeof m === "string" && m.trim()) model = m.trim();
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const directory = payload["cwd"] ? String(payload["cwd"]) : "";
|
|
2256
|
+
return {
|
|
2257
|
+
id: sessionId,
|
|
2258
|
+
slug: `codex/${sessionId}`,
|
|
2259
|
+
title,
|
|
2260
|
+
directory,
|
|
2261
|
+
time_created: createdAt,
|
|
2262
|
+
time_updated: updatedAt,
|
|
2263
|
+
stats: {
|
|
2264
|
+
message_count: messageCount,
|
|
2265
|
+
total_input_tokens: 0,
|
|
2266
|
+
total_output_tokens: 0,
|
|
2267
|
+
total_cost: 0
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
extractTitleFromLines(lines) {
|
|
2272
|
+
let userMessageCount = 0;
|
|
2273
|
+
for (const line of lines.slice(0, 20)) {
|
|
2274
|
+
try {
|
|
2275
|
+
const data = JSON.parse(line);
|
|
2276
|
+
const recordType = String(data["type"] ?? "");
|
|
2277
|
+
if (recordType !== "response_item") continue;
|
|
2278
|
+
const payload = data["payload"] ?? {};
|
|
2279
|
+
const pType = String(payload["type"] ?? "");
|
|
2280
|
+
if (pType !== "message") continue;
|
|
2281
|
+
if (String(payload["role"] ?? "") !== "user") continue;
|
|
2282
|
+
userMessageCount++;
|
|
2283
|
+
if (userMessageCount < 2) continue;
|
|
2284
|
+
const content = payload["content"];
|
|
2285
|
+
if (Array.isArray(content)) {
|
|
2286
|
+
const texts = content.filter((item) => typeof item === "object" && item !== null && "text" in item).map((item) => String(item["text"] ?? "")).join(" ");
|
|
2287
|
+
return normalizeTitleText3(texts);
|
|
2288
|
+
}
|
|
2289
|
+
if (typeof content === "string") {
|
|
2290
|
+
return normalizeTitleText3(content);
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
// ---- Token usage extraction ----
|
|
2298
|
+
extractTokenUsage(record, accumulate) {
|
|
2299
|
+
const recordType = String(record["type"] ?? "");
|
|
2300
|
+
if (recordType !== "response_item") return;
|
|
2301
|
+
const payload = record["payload"] ?? {};
|
|
2302
|
+
const info = payload["info"];
|
|
2303
|
+
if (!info) return;
|
|
2304
|
+
const totalUsage = info["total_token_usage"];
|
|
2305
|
+
if (!totalUsage) return;
|
|
2306
|
+
const inputTokens = Number(totalUsage["input_tokens"] ?? 0);
|
|
2307
|
+
const outputTokens = Number(totalUsage["output_tokens"] ?? 0);
|
|
2308
|
+
if (inputTokens || outputTokens) {
|
|
2309
|
+
accumulate(inputTokens, outputTokens);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
// ---- Record conversion ----
|
|
2313
|
+
convertRecord(data, messages, pendingToolCalls, sessionId, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
|
|
2314
|
+
const recordType = String(data["type"] ?? "");
|
|
2315
|
+
if (recordType === "session_meta" || recordType === "event_msg") {
|
|
2316
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2317
|
+
}
|
|
2318
|
+
if (recordType !== "response_item") {
|
|
2319
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2320
|
+
}
|
|
2321
|
+
const payload = data["payload"] ?? {};
|
|
2322
|
+
const payloadType = String(payload["type"] ?? "");
|
|
2323
|
+
const timestampMs = parseTimestampMs2(payload);
|
|
2324
|
+
switch (payloadType) {
|
|
2325
|
+
case "message": {
|
|
2326
|
+
const role = String(payload["role"] ?? "");
|
|
2327
|
+
if (role === "assistant") {
|
|
2328
|
+
return this.convertAssistantMessage(
|
|
2329
|
+
payload,
|
|
2330
|
+
messages,
|
|
2331
|
+
timestampMs,
|
|
2332
|
+
currentAssistantIndex,
|
|
2333
|
+
latestAssistantTextIndex,
|
|
2334
|
+
pendingPlan
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
if (role === "user") {
|
|
2338
|
+
return this.convertUserMessage(
|
|
2339
|
+
payload,
|
|
2340
|
+
messages,
|
|
2341
|
+
timestampMs,
|
|
2342
|
+
currentAssistantIndex,
|
|
2343
|
+
latestAssistantTextIndex,
|
|
2344
|
+
pendingPlan
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
2349
|
+
case "reasoning":
|
|
2350
|
+
return this.convertReasoning(payload, messages, timestampMs, currentAssistantIndex);
|
|
2351
|
+
case "function_call":
|
|
2352
|
+
return this.convertFunctionCall(
|
|
2353
|
+
payload,
|
|
2354
|
+
messages,
|
|
2355
|
+
pendingToolCalls,
|
|
2356
|
+
timestampMs,
|
|
2357
|
+
currentAssistantIndex,
|
|
2358
|
+
latestAssistantTextIndex
|
|
2359
|
+
);
|
|
2360
|
+
case "function_call_output":
|
|
2361
|
+
this.convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs);
|
|
2362
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2363
|
+
case "custom_tool_call":
|
|
2364
|
+
return this.convertCustomToolCall(
|
|
2365
|
+
payload,
|
|
2366
|
+
messages,
|
|
2367
|
+
pendingToolCalls,
|
|
2368
|
+
timestampMs,
|
|
2369
|
+
currentAssistantIndex,
|
|
2370
|
+
latestAssistantTextIndex
|
|
2371
|
+
);
|
|
2372
|
+
case "custom_tool_call_output":
|
|
2373
|
+
this.convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs);
|
|
2374
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2375
|
+
}
|
|
2376
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2377
|
+
}
|
|
2378
|
+
// ---- Assistant message ----
|
|
2379
|
+
convertAssistantMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
|
|
2380
|
+
const content = payload["content"];
|
|
2381
|
+
if (!Array.isArray(content)) {
|
|
2382
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2383
|
+
}
|
|
2384
|
+
const textParts = [];
|
|
2385
|
+
for (const item of content) {
|
|
2386
|
+
if (typeof item !== "object" || item === null) continue;
|
|
2387
|
+
const ci = item;
|
|
2388
|
+
if (String(ci["type"] ?? "") === "output_text") {
|
|
2389
|
+
const text = String(ci["text"] ?? "");
|
|
2390
|
+
if (text.trim()) textParts.push(text);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (textParts.length === 0) {
|
|
2394
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2395
|
+
}
|
|
2396
|
+
const fullText = textParts.join("\n");
|
|
2397
|
+
const planMatch = fullText.match(PROPOSED_PLAN_PATTERN);
|
|
2398
|
+
if (planMatch) {
|
|
2399
|
+
const planText = planMatch[1].trim();
|
|
2400
|
+
const planPart = {
|
|
2401
|
+
type: "plan",
|
|
2402
|
+
text: planText,
|
|
2403
|
+
approval_status: "success",
|
|
2404
|
+
time_created: timestampMs
|
|
2405
|
+
};
|
|
2406
|
+
pendingPlan = planPart;
|
|
2407
|
+
}
|
|
2408
|
+
const displayText = fullText.replace(PROPOSED_PLAN_PATTERN, "").trim();
|
|
2409
|
+
if (!displayText) {
|
|
2410
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2411
|
+
}
|
|
2412
|
+
const textPart = { type: "text", text: displayText, time_created: timestampMs };
|
|
2413
|
+
if (currentAssistantIndex !== null) {
|
|
2414
|
+
const message = messages[currentAssistantIndex];
|
|
2415
|
+
const hasTool = message.parts.some((p) => p.type === "tool");
|
|
2416
|
+
if (!hasTool) {
|
|
2417
|
+
message.parts.push(textPart);
|
|
2418
|
+
latestAssistantTextIndex = currentAssistantIndex;
|
|
2419
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
messages.push(
|
|
2423
|
+
this.buildMessage({
|
|
2424
|
+
messageId: "",
|
|
2425
|
+
role: "assistant",
|
|
2426
|
+
timestampMs,
|
|
2427
|
+
parts: [textPart],
|
|
2428
|
+
agent: "codex"
|
|
2429
|
+
})
|
|
2430
|
+
);
|
|
2431
|
+
currentAssistantIndex = messages.length - 1;
|
|
2432
|
+
latestAssistantTextIndex = currentAssistantIndex;
|
|
2433
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2434
|
+
}
|
|
2435
|
+
// ---- User message ----
|
|
2436
|
+
convertUserMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
|
|
2437
|
+
const content = payload["content"];
|
|
2438
|
+
const text = Array.isArray(content) ? content.map(
|
|
2439
|
+
(c) => typeof c === "object" && c !== null ? String(c["text"] ?? "") : String(c ?? "")
|
|
2440
|
+
).join(" ") : String(content ?? "");
|
|
2441
|
+
if (!text.trim()) {
|
|
2442
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2443
|
+
}
|
|
2444
|
+
if (isDeveloperLikeUserMessage(text)) {
|
|
2445
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2446
|
+
}
|
|
2447
|
+
if (text.trimStart().startsWith(PLAN_APPROVAL_PREFIX)) {
|
|
2448
|
+
if (pendingPlan && currentAssistantIndex !== null) {
|
|
2449
|
+
messages[currentAssistantIndex].parts.push(pendingPlan);
|
|
2450
|
+
}
|
|
2451
|
+
pendingPlan = null;
|
|
2452
|
+
messages.push(
|
|
2453
|
+
this.buildMessage({
|
|
2454
|
+
messageId: "",
|
|
2455
|
+
role: "user",
|
|
2456
|
+
timestampMs,
|
|
2457
|
+
parts: [{ type: "text", text: text.trim(), time_created: timestampMs }]
|
|
2458
|
+
})
|
|
2459
|
+
);
|
|
2460
|
+
currentAssistantIndex = null;
|
|
2461
|
+
latestAssistantTextIndex = null;
|
|
2462
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2463
|
+
}
|
|
2464
|
+
const subagentMatch = text.match(SUBAGENT_NOTIFICATION_PATTERN);
|
|
2465
|
+
if (subagentMatch) {
|
|
2466
|
+
try {
|
|
2467
|
+
const notifPayload = JSON.parse(subagentMatch[1]);
|
|
2468
|
+
const agentId = String(notifPayload["agent_id"] ?? "");
|
|
2469
|
+
const nickname = String(notifPayload["nickname"] ?? "");
|
|
2470
|
+
const completedText = String(notifPayload["completed"] ?? "");
|
|
2471
|
+
const textPart = {
|
|
2472
|
+
type: "text",
|
|
2473
|
+
text: completedText || `Subagent ${nickname} completed`,
|
|
2474
|
+
time_created: timestampMs
|
|
2475
|
+
};
|
|
2476
|
+
messages.push(
|
|
2477
|
+
this.buildMessage({
|
|
2478
|
+
messageId: "",
|
|
2479
|
+
role: "assistant",
|
|
2480
|
+
timestampMs,
|
|
2481
|
+
parts: [textPart],
|
|
2482
|
+
agent: "codex",
|
|
2483
|
+
subagent_id: agentId || void 0,
|
|
2484
|
+
nickname: nickname || void 0
|
|
2485
|
+
})
|
|
2486
|
+
);
|
|
2487
|
+
currentAssistantIndex = null;
|
|
2488
|
+
latestAssistantTextIndex = null;
|
|
2489
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2490
|
+
} catch {
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
messages.push(
|
|
2494
|
+
this.buildMessage({
|
|
2495
|
+
messageId: "",
|
|
2496
|
+
role: "user",
|
|
2497
|
+
timestampMs,
|
|
2498
|
+
parts: [{ type: "text", text: text.trim(), time_created: timestampMs }]
|
|
2499
|
+
})
|
|
2500
|
+
);
|
|
2501
|
+
currentAssistantIndex = null;
|
|
2502
|
+
latestAssistantTextIndex = null;
|
|
2503
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
|
|
2504
|
+
}
|
|
2505
|
+
// ---- Reasoning ----
|
|
2506
|
+
convertReasoning(payload, messages, timestampMs, currentAssistantIndex) {
|
|
2507
|
+
const summary = payload["summary"];
|
|
2508
|
+
if (!Array.isArray(summary)) {
|
|
2509
|
+
return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
|
|
2510
|
+
}
|
|
2511
|
+
const texts = [];
|
|
2512
|
+
for (const item of summary) {
|
|
2513
|
+
if (typeof item === "object" && item !== null) {
|
|
2514
|
+
const ci = item;
|
|
2515
|
+
if (String(ci["type"] ?? "") === "summary_text") {
|
|
2516
|
+
const text = String(ci["text"] ?? "");
|
|
2517
|
+
if (text.trim()) texts.push(text);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (texts.length === 0) {
|
|
2522
|
+
return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
|
|
2523
|
+
}
|
|
2524
|
+
const reasoningText = texts.join("\n");
|
|
2525
|
+
const part = { type: "reasoning", text: reasoningText, time_created: timestampMs };
|
|
2526
|
+
if (currentAssistantIndex !== null) {
|
|
2527
|
+
const message = messages[currentAssistantIndex];
|
|
2528
|
+
const hasText = message.parts.some((p) => p.type === "text");
|
|
2529
|
+
const hasTool = message.parts.some((p) => p.type === "tool");
|
|
2530
|
+
if (!hasText && !hasTool) {
|
|
2531
|
+
message.parts.push(part);
|
|
2532
|
+
return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
messages.push(
|
|
2536
|
+
this.buildMessage({
|
|
2537
|
+
messageId: "",
|
|
2538
|
+
role: "assistant",
|
|
2539
|
+
timestampMs,
|
|
2540
|
+
parts: [part],
|
|
2541
|
+
agent: "codex"
|
|
2542
|
+
})
|
|
2543
|
+
);
|
|
2544
|
+
return {
|
|
2545
|
+
currentAssistantIndex: messages.length - 1,
|
|
2546
|
+
latestAssistantTextIndex: null,
|
|
2547
|
+
pendingPlan: null
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
// ---- Function call ----
|
|
2551
|
+
convertFunctionCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
|
|
2552
|
+
const callId = String(payload["call_id"] ?? "").trim();
|
|
2553
|
+
const name = String(payload["name"] ?? "").trim();
|
|
2554
|
+
if (!name) {
|
|
2555
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
|
|
2556
|
+
}
|
|
2557
|
+
const mappedName = mapToolTitle2(name);
|
|
2558
|
+
const arguments_ = normalizeToolArguments2(payload["arguments"]);
|
|
2559
|
+
const toolPart = {
|
|
2560
|
+
type: "tool",
|
|
2561
|
+
tool: mappedName,
|
|
2562
|
+
callID: callId,
|
|
2563
|
+
title: `Tool: ${mappedName}`,
|
|
2564
|
+
state: {
|
|
2565
|
+
arguments: arguments_,
|
|
2566
|
+
output: null
|
|
2567
|
+
},
|
|
2568
|
+
time_created: timestampMs
|
|
2569
|
+
};
|
|
2570
|
+
const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
|
|
2571
|
+
if (targetIndex !== null) {
|
|
2572
|
+
const message = messages[targetIndex];
|
|
2573
|
+
const partIndex = message.parts.length;
|
|
2574
|
+
message.parts.push(toolPart);
|
|
2575
|
+
message.mode = "tool";
|
|
2576
|
+
if (callId) {
|
|
2577
|
+
pendingToolCalls.set(callId, [targetIndex, partIndex]);
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
currentAssistantIndex: targetIndex,
|
|
2581
|
+
latestAssistantTextIndex: targetIndex,
|
|
2582
|
+
pendingPlan: null
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
messages.push(
|
|
2586
|
+
this.buildMessage({
|
|
2587
|
+
messageId: "",
|
|
2588
|
+
role: "assistant",
|
|
2589
|
+
timestampMs,
|
|
2590
|
+
parts: [toolPart],
|
|
2591
|
+
agent: "codex",
|
|
2592
|
+
mode: "tool"
|
|
2593
|
+
})
|
|
2594
|
+
);
|
|
2595
|
+
const newIndex = messages.length - 1;
|
|
2596
|
+
if (callId) {
|
|
2597
|
+
pendingToolCalls.set(callId, [newIndex, 0]);
|
|
2598
|
+
}
|
|
2599
|
+
return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
|
|
2600
|
+
}
|
|
2601
|
+
// ---- Function call output ----
|
|
2602
|
+
convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs) {
|
|
2603
|
+
const callId = String(payload["call_id"] ?? "").trim();
|
|
2604
|
+
if (!callId) return;
|
|
2605
|
+
const location = pendingToolCalls.get(callId);
|
|
2606
|
+
if (!location) return;
|
|
2607
|
+
const outputText = String(payload["output"] ?? "");
|
|
2608
|
+
const outputParts = outputText.trim() ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
|
|
2609
|
+
const [msgIndex, partIndex] = location;
|
|
2610
|
+
const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
|
|
2611
|
+
if (outputParts.length > 0) {
|
|
2612
|
+
state.output = [...outputParts];
|
|
2613
|
+
state.status = "completed";
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
// ---- Custom tool call ----
|
|
2617
|
+
convertCustomToolCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
|
|
2618
|
+
const callId = String(payload["call_id"] ?? "").trim();
|
|
2619
|
+
const name = String(payload["name"] ?? "").trim();
|
|
2620
|
+
if (!name) {
|
|
2621
|
+
return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
|
|
2622
|
+
}
|
|
2623
|
+
const mappedName = mapToolTitle2(name);
|
|
2624
|
+
const rawInput = payload["input"];
|
|
2625
|
+
const normalizedInput = normalizeCustomToolArguments(name, rawInput);
|
|
2626
|
+
const toolPart = {
|
|
2627
|
+
type: "tool",
|
|
2628
|
+
tool: mappedName,
|
|
2629
|
+
callID: callId,
|
|
2630
|
+
title: `Tool: ${mappedName}`,
|
|
2631
|
+
state: {
|
|
2632
|
+
arguments: normalizedInput,
|
|
2633
|
+
output: null
|
|
2634
|
+
},
|
|
2635
|
+
time_created: timestampMs
|
|
2636
|
+
};
|
|
2637
|
+
const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
|
|
2638
|
+
if (targetIndex !== null) {
|
|
2639
|
+
const message = messages[targetIndex];
|
|
2640
|
+
const partIndex = message.parts.length;
|
|
2641
|
+
message.parts.push(toolPart);
|
|
2642
|
+
message.mode = "tool";
|
|
2643
|
+
if (callId) {
|
|
2644
|
+
pendingToolCalls.set(callId, [targetIndex, partIndex]);
|
|
2645
|
+
}
|
|
2646
|
+
return {
|
|
2647
|
+
currentAssistantIndex: targetIndex,
|
|
2648
|
+
latestAssistantTextIndex: targetIndex,
|
|
2649
|
+
pendingPlan: null
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
messages.push(
|
|
2653
|
+
this.buildMessage({
|
|
2654
|
+
messageId: "",
|
|
2655
|
+
role: "assistant",
|
|
2656
|
+
timestampMs,
|
|
2657
|
+
parts: [toolPart],
|
|
2658
|
+
agent: "codex",
|
|
2659
|
+
mode: "tool"
|
|
2660
|
+
})
|
|
2661
|
+
);
|
|
2662
|
+
const newIndex = messages.length - 1;
|
|
2663
|
+
if (callId) {
|
|
2664
|
+
pendingToolCalls.set(callId, [newIndex, 0]);
|
|
2665
|
+
}
|
|
2666
|
+
return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
|
|
2667
|
+
}
|
|
2668
|
+
// ---- Custom tool call output ----
|
|
2669
|
+
convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs) {
|
|
2670
|
+
const callId = String(payload["call_id"] ?? "").trim();
|
|
2671
|
+
if (!callId) return;
|
|
2672
|
+
const location = pendingToolCalls.get(callId);
|
|
2673
|
+
if (!location) return;
|
|
2674
|
+
const outputText = String(payload["output"] ?? "");
|
|
2675
|
+
const outputParts = outputText.trim() ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
|
|
2676
|
+
const [msgIndex, partIndex] = location;
|
|
2677
|
+
const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
|
|
2678
|
+
if (outputParts.length > 0) {
|
|
2679
|
+
state.output = [...outputParts];
|
|
2680
|
+
state.status = "completed";
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
// ---- Message builder ----
|
|
2684
|
+
buildMessage(opts) {
|
|
2685
|
+
return {
|
|
2686
|
+
id: opts.messageId,
|
|
2687
|
+
role: opts.role,
|
|
2688
|
+
agent: opts.agent ?? null,
|
|
2689
|
+
time_created: opts.timestampMs,
|
|
2690
|
+
mode: opts.mode ?? null,
|
|
2691
|
+
model: opts.model ?? null,
|
|
2692
|
+
provider: opts.provider ?? null,
|
|
2693
|
+
tokens: opts.tokens ? opts.tokens : void 0,
|
|
2694
|
+
cost: opts.cost ?? 0,
|
|
2695
|
+
parts: opts.parts,
|
|
2696
|
+
subagent_id: opts.subagent_id,
|
|
2697
|
+
nickname: opts.nickname
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
var CURSOR_TOOL_TITLE_MAP = {
|
|
2702
|
+
read_file_v2: "read",
|
|
2703
|
+
edit_file_v2: "edit",
|
|
2704
|
+
run_terminal_command_v2: "bash",
|
|
2705
|
+
ripgrep_raw_search: "grep",
|
|
2706
|
+
glob_file_search: "glob"
|
|
2707
|
+
};
|
|
2708
|
+
function mapToolTitle3(toolName) {
|
|
2709
|
+
return CURSOR_TOOL_TITLE_MAP[toolName] ?? toolName;
|
|
2710
|
+
}
|
|
2711
|
+
function normalizeToolOutputParts2(output, timestampMs) {
|
|
2712
|
+
if (output == null) return [];
|
|
2713
|
+
if (typeof output === "string") {
|
|
2714
|
+
return output.trim() ? [{ type: "text", text: output, time_created: timestampMs }] : [];
|
|
2715
|
+
}
|
|
2716
|
+
if (Array.isArray(output)) {
|
|
2717
|
+
const parts = [];
|
|
2718
|
+
for (const item of output) {
|
|
2719
|
+
if (typeof item === "object" && item !== null) {
|
|
2720
|
+
const text2 = String(
|
|
2721
|
+
item.text ?? item.content ?? ""
|
|
2722
|
+
);
|
|
2723
|
+
if (text2.trim()) parts.push({ type: "text", text: text2, time_created: timestampMs });
|
|
2724
|
+
} else if (typeof item === "string" && item.trim()) {
|
|
2725
|
+
parts.push({ type: "text", text: item, time_created: timestampMs });
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return parts;
|
|
2729
|
+
}
|
|
2730
|
+
const text = String(output);
|
|
2731
|
+
return text.trim() ? [{ type: "text", text, time_created: timestampMs }] : [];
|
|
2732
|
+
}
|
|
2733
|
+
function extractTimestamp(msg) {
|
|
2734
|
+
if (msg.createdAt && typeof msg.createdAt === "number" && msg.createdAt > 0) {
|
|
2735
|
+
return msg.createdAt;
|
|
2736
|
+
}
|
|
2737
|
+
if (msg.timestamp && typeof msg.timestamp === "number" && msg.timestamp > 0) {
|
|
2738
|
+
return msg.timestamp;
|
|
2739
|
+
}
|
|
2740
|
+
return 0;
|
|
2741
|
+
}
|
|
2742
|
+
function buildToolState(action) {
|
|
2743
|
+
const state = {};
|
|
2744
|
+
if (action.input) {
|
|
2745
|
+
state.input = action.input;
|
|
2746
|
+
}
|
|
2747
|
+
if (action.output != null) {
|
|
2748
|
+
const ts = 0;
|
|
2749
|
+
const outputParts = normalizeToolOutputParts2(action.output, ts);
|
|
2750
|
+
state.output = outputParts.length > 0 ? outputParts : action.output;
|
|
2751
|
+
}
|
|
2752
|
+
if (action.state) {
|
|
2753
|
+
Object.assign(state, action.state);
|
|
2754
|
+
}
|
|
2755
|
+
if (!state.status) {
|
|
2756
|
+
if (typeof action.output === "object" && action.output !== null) {
|
|
2757
|
+
const out = action.output;
|
|
2758
|
+
if (out.success === true) state.status = "completed";
|
|
2759
|
+
else if (out.success === false) state.status = "error";
|
|
2760
|
+
else state.status = "completed";
|
|
2761
|
+
} else if (action.output != null) {
|
|
2762
|
+
state.status = "completed";
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return state;
|
|
2766
|
+
}
|
|
2767
|
+
function buildToolPart(action, timestampMs) {
|
|
2768
|
+
const toolName = action.tool ?? "unknown";
|
|
2769
|
+
return {
|
|
2770
|
+
type: "tool",
|
|
2771
|
+
tool: mapToolTitle3(toolName),
|
|
2772
|
+
callID: action.type ? `${action.type}:${String(action.input?.id ?? "")}` : "",
|
|
2773
|
+
title: `Tool: ${mapToolTitle3(toolName)}`,
|
|
2774
|
+
state: buildToolState(action),
|
|
2775
|
+
time_created: timestampMs
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
function buildTerminalToolPart(action, timestampMs) {
|
|
2779
|
+
const command = String(action.input?.command ?? "");
|
|
2780
|
+
const description = String(action.input?.commandDescription ?? "");
|
|
2781
|
+
return {
|
|
2782
|
+
type: "tool",
|
|
2783
|
+
tool: "bash",
|
|
2784
|
+
callID: "",
|
|
2785
|
+
title: description || `bash: ${command.slice(0, 60)}`,
|
|
2786
|
+
state: {
|
|
2787
|
+
input: { command },
|
|
2788
|
+
output: typeof action.output === "string" ? [{ type: "text", text: action.output, time_created: timestampMs }] : normalizeToolOutputParts2(action.output, timestampMs)
|
|
2789
|
+
},
|
|
2790
|
+
time_created: timestampMs
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function convertActionToPart(action, timestampMs) {
|
|
2794
|
+
const toolName = action.tool ?? "";
|
|
2795
|
+
if (toolName === "run_terminal_command_v2") {
|
|
2796
|
+
return buildTerminalToolPart(action, timestampMs);
|
|
2797
|
+
}
|
|
2798
|
+
if (toolName && action.type === "tool") {
|
|
2799
|
+
return buildToolPart(action, timestampMs);
|
|
2800
|
+
}
|
|
2801
|
+
return null;
|
|
2802
|
+
}
|
|
2803
|
+
var CursorAgent = class extends BaseAgent {
|
|
2804
|
+
name = "cursor";
|
|
2805
|
+
displayName = "Cursor";
|
|
2806
|
+
dbPath = null;
|
|
2807
|
+
// Cache composer data from scan so getSessionData can reuse it
|
|
2808
|
+
composerCache = /* @__PURE__ */ new Map();
|
|
2809
|
+
// Session metadata for caching
|
|
2810
|
+
sessionMetaMap = /* @__PURE__ */ new Map();
|
|
2811
|
+
findDbPath() {
|
|
2812
|
+
if (!isSqliteAvailable()) return null;
|
|
2813
|
+
const dataPath = getCursorDataPath();
|
|
2814
|
+
if (!dataPath) return null;
|
|
2815
|
+
return join6(dataPath, "globalStorage", "state.vscdb");
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Build a map of composerId → workspace folder path by reading
|
|
2819
|
+
* workspaceStorage/{id}/workspace.json and the corresponding state.vscdb.
|
|
2820
|
+
*/
|
|
2821
|
+
buildWorkspacePathMap() {
|
|
2822
|
+
const map = /* @__PURE__ */ new Map();
|
|
2823
|
+
const dataPath = getCursorDataPath();
|
|
2824
|
+
if (!dataPath) return map;
|
|
2825
|
+
const wsStoragePath = join6(dataPath, "workspaceStorage");
|
|
2826
|
+
if (!existsSync6(wsStoragePath)) return map;
|
|
2827
|
+
let entryNames;
|
|
2828
|
+
try {
|
|
2829
|
+
entryNames = readdirSync4(wsStoragePath);
|
|
2830
|
+
} catch {
|
|
2831
|
+
return map;
|
|
2832
|
+
}
|
|
2833
|
+
for (const name of entryNames) {
|
|
2834
|
+
const wsDir = join6(wsStoragePath, name);
|
|
2835
|
+
try {
|
|
2836
|
+
if (!statSync5(wsDir).isDirectory()) continue;
|
|
2837
|
+
} catch {
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
const wsJsonPath = join6(wsDir, "workspace.json");
|
|
2841
|
+
if (!existsSync6(wsJsonPath)) continue;
|
|
2842
|
+
let workspacePath;
|
|
2843
|
+
try {
|
|
2844
|
+
const data = JSON.parse(readFileSync5(wsJsonPath, "utf-8"));
|
|
2845
|
+
const uri = data.folder ?? data.workspace ?? "";
|
|
2846
|
+
if (!uri) continue;
|
|
2847
|
+
workspacePath = normalize(decodeURIComponent(uri.replace(/^file:\/\//, "")));
|
|
2848
|
+
} catch {
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
const wsDbPath = join6(wsDir, "state.vscdb");
|
|
2852
|
+
if (!existsSync6(wsDbPath)) continue;
|
|
2853
|
+
const wsDb = openDbReadOnly(wsDbPath);
|
|
2854
|
+
if (!wsDb) continue;
|
|
2855
|
+
try {
|
|
2856
|
+
const row = wsDb.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
|
|
2857
|
+
if (!row?.value) continue;
|
|
2858
|
+
const parsed = JSON.parse(row.value);
|
|
2859
|
+
let composers;
|
|
2860
|
+
if (parsed !== null && typeof parsed === "object" && "allComposers" in parsed && Array.isArray(parsed["allComposers"])) {
|
|
2861
|
+
composers = parsed.allComposers;
|
|
2862
|
+
} else if (Array.isArray(parsed)) {
|
|
2863
|
+
composers = parsed;
|
|
2864
|
+
} else {
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
for (const c of composers) {
|
|
2868
|
+
const id = c.composerId ?? c.id;
|
|
2869
|
+
if (id) map.set(id, workspacePath);
|
|
2870
|
+
}
|
|
2871
|
+
} catch {
|
|
2872
|
+
} finally {
|
|
2873
|
+
wsDb.close();
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return map;
|
|
2877
|
+
}
|
|
2878
|
+
isAvailable() {
|
|
2879
|
+
this.dbPath = this.findDbPath();
|
|
2880
|
+
return this.dbPath !== null && existsSync6(this.dbPath);
|
|
2881
|
+
}
|
|
2882
|
+
scan() {
|
|
2883
|
+
if (!this.dbPath) return [];
|
|
2884
|
+
const scanMarker = perf.start("cursor:scan");
|
|
2885
|
+
const dbMarker = perf.start("openDatabase");
|
|
2886
|
+
const db = this.openDatabase();
|
|
2887
|
+
perf.end(dbMarker);
|
|
2888
|
+
if (!db) return [];
|
|
2889
|
+
const wsMarker = perf.start("buildWorkspacePathMap");
|
|
2890
|
+
const workspacePathMap = this.buildWorkspacePathMap();
|
|
2891
|
+
perf.end(wsMarker);
|
|
2892
|
+
try {
|
|
2893
|
+
const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
|
|
2894
|
+
const heads = [];
|
|
2895
|
+
for (const row of rows) {
|
|
2896
|
+
try {
|
|
2897
|
+
const composer = JSON.parse(row.value);
|
|
2898
|
+
if (!composer.id && !composer.composerId) continue;
|
|
2899
|
+
const composerId = composer.id || composer.composerId || "";
|
|
2900
|
+
const requestId = this.extractRequestIdFromBubbles(db, composerId);
|
|
2901
|
+
const sessionId = requestId || composerId;
|
|
2902
|
+
const title = this.extractTitle(composer);
|
|
2903
|
+
const createdAt = composer.createdAt ?? 0;
|
|
2904
|
+
const updatedAt = composer.updatedAt ?? createdAt;
|
|
2905
|
+
const messageCount = this.countMessagesFromBubbles(db, composerId);
|
|
2906
|
+
const directory = workspacePathMap.get(composerId) ?? "";
|
|
2907
|
+
heads.push({
|
|
2908
|
+
id: sessionId,
|
|
2909
|
+
slug: `cursor/${sessionId}`,
|
|
2910
|
+
title,
|
|
2911
|
+
directory,
|
|
2912
|
+
time_created: createdAt,
|
|
2913
|
+
time_updated: updatedAt || void 0,
|
|
2914
|
+
stats: {
|
|
2915
|
+
message_count: messageCount,
|
|
2916
|
+
total_input_tokens: composer.inputTokenCount ?? 0,
|
|
2917
|
+
total_output_tokens: composer.outputTokenCount ?? 0,
|
|
2918
|
+
total_cost: 0
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
this.composerCache.set(sessionId, composer);
|
|
2922
|
+
this.composerCache.set(`__mapping__${composerId}`, {
|
|
2923
|
+
sessionId
|
|
2924
|
+
});
|
|
2925
|
+
if (directory) {
|
|
2926
|
+
this.composerCache.set(`__dir__${composerId}`, {
|
|
2927
|
+
directory
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
this.sessionMetaMap.set(sessionId, {
|
|
2931
|
+
id: sessionId,
|
|
2932
|
+
sourcePath: this.dbPath || ""
|
|
2933
|
+
});
|
|
2934
|
+
} catch {
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
perf.end(scanMarker);
|
|
2938
|
+
return heads;
|
|
2939
|
+
} catch {
|
|
2940
|
+
return [];
|
|
2941
|
+
} finally {
|
|
2942
|
+
db.close();
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
getSessionMetaMap() {
|
|
2946
|
+
return this.sessionMetaMap;
|
|
2947
|
+
}
|
|
2948
|
+
setSessionMetaMap(meta) {
|
|
2949
|
+
this.sessionMetaMap = meta;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* 检测数据库变更
|
|
2953
|
+
* 对于 SQLite,检测数据库文件修改时间
|
|
2954
|
+
*/
|
|
2955
|
+
checkForChanges(sinceTimestamp, cachedSessions) {
|
|
2956
|
+
if (!this.dbPath) {
|
|
2957
|
+
this.dbPath = this.findDbPath();
|
|
2958
|
+
}
|
|
2959
|
+
if (!this.dbPath || !existsSync6(this.dbPath)) {
|
|
2960
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
2961
|
+
}
|
|
2962
|
+
try {
|
|
2963
|
+
const stat = statSync5(this.dbPath);
|
|
2964
|
+
const hasChanges = stat.mtimeMs > sinceTimestamp;
|
|
2965
|
+
const changedIds = hasChanges ? cachedSessions.map((s) => s.id) : [];
|
|
2966
|
+
return {
|
|
2967
|
+
hasChanges,
|
|
2968
|
+
changedIds,
|
|
2969
|
+
timestamp: Date.now()
|
|
2970
|
+
};
|
|
2971
|
+
} catch {
|
|
2972
|
+
return { hasChanges: false, timestamp: Date.now() };
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* 增量扫描 - 重新查询数据库
|
|
2977
|
+
*/
|
|
2978
|
+
incrementalScan(_cachedSessions, _changedIds) {
|
|
2979
|
+
return this.scan();
|
|
2980
|
+
}
|
|
2981
|
+
getSessionData(sessionId) {
|
|
2982
|
+
if (!this.dbPath) {
|
|
2983
|
+
this.dbPath = this.findDbPath();
|
|
2984
|
+
}
|
|
2985
|
+
if (!this.dbPath) {
|
|
2986
|
+
throw new Error("Cursor database is missing");
|
|
2987
|
+
}
|
|
2988
|
+
const db = this.openDatabase();
|
|
2989
|
+
if (!db) {
|
|
2990
|
+
throw new Error("Cursor database is missing");
|
|
2991
|
+
}
|
|
2992
|
+
try {
|
|
2993
|
+
let composer = this.composerCache.get(sessionId);
|
|
2994
|
+
let resolvedSessionId = sessionId;
|
|
2995
|
+
if (!composer) {
|
|
2996
|
+
composer = this.loadComposer(db, sessionId) ?? void 0;
|
|
2997
|
+
}
|
|
2998
|
+
if (!composer) {
|
|
2999
|
+
const composerId2 = this.findComposerIdByRequestId(db, sessionId);
|
|
3000
|
+
if (composerId2) {
|
|
3001
|
+
composer = this.loadComposer(db, composerId2) ?? void 0;
|
|
3002
|
+
resolvedSessionId = sessionId;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
if (!composer) {
|
|
3006
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
3007
|
+
}
|
|
3008
|
+
const composerId = composer.id || composer.composerId || "";
|
|
3009
|
+
const title = this.extractTitle(composer);
|
|
3010
|
+
const createdAt = composer.createdAt ?? 0;
|
|
3011
|
+
const updatedAt = composer.updatedAt ?? createdAt;
|
|
3012
|
+
const messages = this.loadMessagesFromBubbles(db, composerId, resolvedSessionId);
|
|
3013
|
+
let totalInputTokens = 0;
|
|
3014
|
+
let totalOutputTokens = 0;
|
|
3015
|
+
for (const msg of messages) {
|
|
3016
|
+
totalInputTokens += msg.tokens?.input ?? 0;
|
|
3017
|
+
totalOutputTokens += msg.tokens?.output ?? 0;
|
|
3018
|
+
}
|
|
3019
|
+
if (totalInputTokens === 0) totalInputTokens = composer.inputTokenCount ?? 0;
|
|
3020
|
+
if (totalOutputTokens === 0) totalOutputTokens = composer.outputTokenCount ?? 0;
|
|
3021
|
+
this.appendSubagentMessages(db, composer, messages);
|
|
3022
|
+
const cachedDir = this.composerCache.get(`__dir__${composerId}`);
|
|
3023
|
+
const directory = cachedDir?.directory ?? this.buildWorkspacePathMap().get(composerId) ?? "";
|
|
3024
|
+
return {
|
|
3025
|
+
id: resolvedSessionId,
|
|
3026
|
+
title,
|
|
3027
|
+
slug: `cursor/${resolvedSessionId}`,
|
|
3028
|
+
directory,
|
|
3029
|
+
time_created: createdAt,
|
|
3030
|
+
time_updated: updatedAt || void 0,
|
|
3031
|
+
stats: {
|
|
3032
|
+
message_count: messages.length,
|
|
3033
|
+
total_input_tokens: totalInputTokens,
|
|
3034
|
+
total_output_tokens: totalOutputTokens,
|
|
3035
|
+
total_cost: 0
|
|
3036
|
+
},
|
|
3037
|
+
messages
|
|
3038
|
+
};
|
|
3039
|
+
} finally {
|
|
3040
|
+
db.close();
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
// --- Private helpers ---
|
|
3044
|
+
openDatabase() {
|
|
3045
|
+
if (!this.dbPath) return null;
|
|
3046
|
+
return openDbReadOnly(this.dbPath);
|
|
3047
|
+
}
|
|
3048
|
+
/** Extract requestId from bubbles for a composer (like agent-dump) */
|
|
3049
|
+
extractRequestIdFromBubbles(db, composerId) {
|
|
3050
|
+
try {
|
|
3051
|
+
const rows = db.prepare("SELECT value FROM cursorDiskKV WHERE key LIKE ? ORDER BY key").all(`bubbleId:${composerId}:%`);
|
|
3052
|
+
for (const row of rows) {
|
|
3053
|
+
try {
|
|
3054
|
+
const bubble = JSON.parse(row.value);
|
|
3055
|
+
if (bubble.requestId && typeof bubble.requestId === "string" && bubble.requestId.trim()) {
|
|
3056
|
+
return bubble.requestId.trim();
|
|
3057
|
+
}
|
|
3058
|
+
} catch {
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
} catch {
|
|
3062
|
+
}
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
/** Find composerId by requestId (reverse lookup) */
|
|
3066
|
+
findComposerIdByRequestId(db, requestId) {
|
|
3067
|
+
try {
|
|
3068
|
+
const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND value LIKE ?").all(`%"requestId":"${requestId}"%`);
|
|
3069
|
+
for (const row of rows) {
|
|
3070
|
+
try {
|
|
3071
|
+
const bubble = JSON.parse(row.value);
|
|
3072
|
+
if (bubble.requestId === requestId) {
|
|
3073
|
+
const keyParts = row.key.split(":");
|
|
3074
|
+
if (keyParts.length >= 2 && keyParts[1]) {
|
|
3075
|
+
return keyParts[1];
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
} catch {
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
} catch {
|
|
3082
|
+
}
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
/** Extract title from composer (like agent-dump) */
|
|
3086
|
+
extractTitle(composer) {
|
|
3087
|
+
if (composer.name && typeof composer.name === "string" && composer.name.trim()) {
|
|
3088
|
+
return composer.name.trim();
|
|
3089
|
+
}
|
|
3090
|
+
if (composer.title && typeof composer.title === "string" && composer.title.trim()) {
|
|
3091
|
+
return composer.title.trim();
|
|
3092
|
+
}
|
|
3093
|
+
if (composer.text && typeof composer.text === "string" && composer.text.trim()) {
|
|
3094
|
+
const firstLine = composer.text.split("\n").find((l) => l.trim())?.trim().slice(0, 80);
|
|
3095
|
+
if (firstLine) return firstLine;
|
|
3096
|
+
}
|
|
3097
|
+
const composerId = composer.composerId || composer.id || "";
|
|
3098
|
+
return `Cursor Session ${composerId.slice(0, 8)}`;
|
|
3099
|
+
}
|
|
3100
|
+
/** Count messages from bubbles */
|
|
3101
|
+
countMessagesFromBubbles(db, composerId) {
|
|
3102
|
+
try {
|
|
3103
|
+
const rows = db.prepare("SELECT value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
|
|
3104
|
+
let count = 0;
|
|
3105
|
+
for (const row of rows) {
|
|
3106
|
+
try {
|
|
3107
|
+
const bubble = JSON.parse(row.value);
|
|
3108
|
+
if (bubble.type === 1 || bubble.type === 2) {
|
|
3109
|
+
count++;
|
|
3110
|
+
}
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return count;
|
|
3115
|
+
} catch {
|
|
3116
|
+
return 0;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
/** Load messages from bubbles (like agent-dump) */
|
|
3120
|
+
loadMessagesFromBubbles(db, composerId, _sessionId) {
|
|
3121
|
+
const messages = [];
|
|
3122
|
+
try {
|
|
3123
|
+
const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE ? ORDER BY rowid ASC").all(`bubbleId:${composerId}:%`);
|
|
3124
|
+
let activeModelName = null;
|
|
3125
|
+
let messageIndex = 0;
|
|
3126
|
+
for (const row of rows) {
|
|
3127
|
+
try {
|
|
3128
|
+
const bubble = JSON.parse(row.value);
|
|
3129
|
+
const bubbleId = row.key.split(":").pop() || String(messageIndex);
|
|
3130
|
+
const role = bubble.type === 2 ? "assistant" : "user";
|
|
3131
|
+
let timestampMs = 0;
|
|
3132
|
+
if (bubble.timingInfo?.clientRpcSendTime) {
|
|
3133
|
+
timestampMs = Math.floor(bubble.timingInfo.clientRpcSendTime);
|
|
3134
|
+
} else if (bubble.createdAt) {
|
|
3135
|
+
timestampMs = bubble.createdAt;
|
|
3136
|
+
} else if (bubble.timestamp) {
|
|
3137
|
+
timestampMs = bubble.timestamp;
|
|
3138
|
+
}
|
|
3139
|
+
if (role === "user" && bubble.modelInfo?.modelName) {
|
|
3140
|
+
activeModelName = bubble.modelInfo.modelName;
|
|
3141
|
+
}
|
|
3142
|
+
const inputTokens = bubble.tokenCount?.inputTokens ?? 0;
|
|
3143
|
+
const outputTokens = bubble.tokenCount?.outputTokens ?? 0;
|
|
3144
|
+
const parts = [];
|
|
3145
|
+
const text = bubble.text?.trim();
|
|
3146
|
+
if (text) {
|
|
3147
|
+
parts.push({ type: "text", text, time_created: timestampMs });
|
|
3148
|
+
}
|
|
3149
|
+
if (bubble.toolFormerData) {
|
|
3150
|
+
const toolPart = this.convertToolFormerData(bubble.toolFormerData, timestampMs);
|
|
3151
|
+
if (toolPart) {
|
|
3152
|
+
parts.push(toolPart);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
if (parts.length === 0) continue;
|
|
3156
|
+
messages.push({
|
|
3157
|
+
id: `cursor-${composerId}-${bubbleId}`,
|
|
3158
|
+
role,
|
|
3159
|
+
agent: "cursor",
|
|
3160
|
+
time_created: timestampMs,
|
|
3161
|
+
time_completed: null,
|
|
3162
|
+
mode: role === "assistant" && parts.some((p) => p.type === "tool") ? "tool" : null,
|
|
3163
|
+
model: activeModelName,
|
|
3164
|
+
provider: null,
|
|
3165
|
+
tokens: { input: inputTokens, output: outputTokens },
|
|
3166
|
+
cost: 0,
|
|
3167
|
+
parts
|
|
3168
|
+
});
|
|
3169
|
+
messageIndex++;
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
} catch {
|
|
3174
|
+
}
|
|
3175
|
+
return messages;
|
|
3176
|
+
}
|
|
3177
|
+
/** Convert toolFormerData to MessagePart */
|
|
3178
|
+
convertToolFormerData(toolData, timestampMs) {
|
|
3179
|
+
if (!toolData || !toolData.name) return null;
|
|
3180
|
+
const toolName = toolData.name;
|
|
3181
|
+
const normalizedName = toolName === "create_plan" ? "plan" : mapToolTitle3(toolName);
|
|
3182
|
+
const state = {
|
|
3183
|
+
status: toolData.status === "completed" ? "completed" : "running"
|
|
3184
|
+
};
|
|
3185
|
+
if (toolData.params) {
|
|
3186
|
+
if (typeof toolData.params === "string") {
|
|
3187
|
+
try {
|
|
3188
|
+
state.input = JSON.parse(toolData.params);
|
|
3189
|
+
} catch {
|
|
3190
|
+
state.input = { _raw: toolData.params };
|
|
3191
|
+
}
|
|
3192
|
+
} else {
|
|
3193
|
+
state.input = toolData.params;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
if (toolData.result !== void 0) {
|
|
3197
|
+
if (typeof toolData.result === "string") {
|
|
3198
|
+
try {
|
|
3199
|
+
const parsed = JSON.parse(toolData.result);
|
|
3200
|
+
state.output = parsed;
|
|
3201
|
+
if (parsed.error || parsed.message || parsed.stderr) {
|
|
3202
|
+
state.error = parsed.error || parsed.message || parsed.stderr;
|
|
3203
|
+
state.status = "error";
|
|
3204
|
+
}
|
|
3205
|
+
} catch {
|
|
3206
|
+
state.output = toolData.result;
|
|
3207
|
+
}
|
|
3208
|
+
} else {
|
|
3209
|
+
state.output = toolData.result;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (toolName === "create_plan") {
|
|
3213
|
+
const planText = typeof state.input === "object" && state.input !== null ? state.input.plan : void 0;
|
|
3214
|
+
return {
|
|
3215
|
+
type: "plan",
|
|
3216
|
+
title: "Plan",
|
|
3217
|
+
input: planText,
|
|
3218
|
+
approval_status: state.status === "completed" ? "success" : "fail",
|
|
3219
|
+
state,
|
|
3220
|
+
time_created: timestampMs
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
return {
|
|
3224
|
+
type: "tool",
|
|
3225
|
+
tool: normalizedName,
|
|
3226
|
+
callID: toolData.toolCallId || "",
|
|
3227
|
+
title: `Tool: ${toolName}`,
|
|
3228
|
+
state,
|
|
3229
|
+
time_created: timestampMs
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
loadComposer(db, sessionId) {
|
|
3233
|
+
const row = db.prepare("SELECT value FROM cursorDiskKV WHERE key = ?").get(`composerData:${sessionId}`);
|
|
3234
|
+
if (!row) return null;
|
|
3235
|
+
try {
|
|
3236
|
+
return JSON.parse(row.value);
|
|
3237
|
+
} catch {
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
loadBubble(db, sessionId) {
|
|
3242
|
+
const row = db.prepare("SELECT value FROM cursorDiskKV WHERE key = ?").get(`bubble:${sessionId}`);
|
|
3243
|
+
if (!row) return null;
|
|
3244
|
+
try {
|
|
3245
|
+
return JSON.parse(row.value);
|
|
3246
|
+
} catch {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
appendSubagentMessages(db, composer, messages) {
|
|
3251
|
+
const subagentInfos = composer.subagentInfos;
|
|
3252
|
+
if (!Array.isArray(subagentInfos) || subagentInfos.length === 0) return;
|
|
3253
|
+
for (const subInfo of subagentInfos) {
|
|
3254
|
+
if (!subInfo.id) continue;
|
|
3255
|
+
const bubble = this.loadBubble(db, subInfo.id);
|
|
3256
|
+
if (!bubble || !Array.isArray(bubble.chatMessages)) continue;
|
|
3257
|
+
for (const chatMsg of bubble.chatMessages) {
|
|
3258
|
+
const role = chatMsg.role?.trim().toLowerCase();
|
|
3259
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
3260
|
+
const timestampMs = extractTimestamp(chatMsg);
|
|
3261
|
+
const parts = [];
|
|
3262
|
+
const text = chatMsg.text ?? "";
|
|
3263
|
+
if (text.trim()) {
|
|
3264
|
+
parts.push({ type: "text", text, time_created: timestampMs });
|
|
3265
|
+
}
|
|
3266
|
+
if (role === "assistant" && Array.isArray(chatMsg.actions)) {
|
|
3267
|
+
for (const action of chatMsg.actions) {
|
|
3268
|
+
const part = convertActionToPart(action, timestampMs);
|
|
3269
|
+
if (part) parts.push(part);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
if (parts.length === 0) continue;
|
|
3273
|
+
messages.push({
|
|
3274
|
+
id: `cursor-sub-${subInfo.id}`,
|
|
3275
|
+
role,
|
|
3276
|
+
agent: "cursor",
|
|
3277
|
+
time_created: timestampMs,
|
|
3278
|
+
time_completed: null,
|
|
3279
|
+
mode: null,
|
|
3280
|
+
model: null,
|
|
3281
|
+
provider: null,
|
|
3282
|
+
tokens: void 0,
|
|
3283
|
+
cost: 0,
|
|
3284
|
+
subagent_id: subInfo.id,
|
|
3285
|
+
nickname: subInfo.nickname ?? subInfo.title,
|
|
3286
|
+
parts
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
registerAgent({
|
|
3293
|
+
name: "claudecode",
|
|
3294
|
+
displayName: "Claude Code",
|
|
3295
|
+
icon: "/icon/agent/claudecode.svg",
|
|
3296
|
+
create: () => new ClaudeCodeAgent()
|
|
3297
|
+
});
|
|
3298
|
+
registerAgent({
|
|
3299
|
+
name: "opencode",
|
|
3300
|
+
displayName: "OpenCode",
|
|
3301
|
+
icon: "/icon/agent/opencode.svg",
|
|
3302
|
+
create: () => new OpenCodeAgent()
|
|
3303
|
+
});
|
|
3304
|
+
registerAgent({
|
|
3305
|
+
name: "kimi",
|
|
3306
|
+
displayName: "Kimi-Cli",
|
|
3307
|
+
icon: "/icon/agent/kimi.svg",
|
|
3308
|
+
create: () => new KimiAgent()
|
|
3309
|
+
});
|
|
3310
|
+
registerAgent({
|
|
3311
|
+
name: "codex",
|
|
3312
|
+
displayName: "Codex",
|
|
3313
|
+
icon: "/icon/agent/codex.svg",
|
|
3314
|
+
create: () => new CodexAgent()
|
|
3315
|
+
});
|
|
3316
|
+
registerAgent({
|
|
3317
|
+
name: "cursor",
|
|
3318
|
+
displayName: "Cursor",
|
|
3319
|
+
icon: "/icon/agent/cursor.svg",
|
|
3320
|
+
create: () => new CursorAgent()
|
|
3321
|
+
});
|
|
3322
|
+
var CACHE_VERSION = 2;
|
|
3323
|
+
var CACHE_FILENAME = "scan-cache.json";
|
|
3324
|
+
function getCachePath() {
|
|
3325
|
+
return join7(homedir2(), ".cache", "codesesh", CACHE_FILENAME);
|
|
3326
|
+
}
|
|
3327
|
+
function ensureCacheDir() {
|
|
3328
|
+
const cacheDir = join7(homedir2(), ".cache", "codesesh");
|
|
3329
|
+
if (!existsSync7(cacheDir)) {
|
|
3330
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
function loadCachedSessions(agentName) {
|
|
3334
|
+
try {
|
|
3335
|
+
const cachePath = getCachePath();
|
|
3336
|
+
if (!existsSync7(cachePath)) return null;
|
|
3337
|
+
const data = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
3338
|
+
if (data.version !== CACHE_VERSION) return null;
|
|
3339
|
+
const entry = data.entries[agentName];
|
|
3340
|
+
if (!entry) return null;
|
|
3341
|
+
const CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
|
|
3342
|
+
if (Date.now() - entry.timestamp > CACHE_TTL) return null;
|
|
3343
|
+
return { sessions: entry.sessions, meta: entry.meta || {}, timestamp: entry.timestamp };
|
|
3344
|
+
} catch {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
function saveCachedSessions(agentName, sessions, meta = {}) {
|
|
3349
|
+
try {
|
|
3350
|
+
ensureCacheDir();
|
|
3351
|
+
const cachePath = getCachePath();
|
|
3352
|
+
let data;
|
|
3353
|
+
if (existsSync7(cachePath)) {
|
|
3354
|
+
try {
|
|
3355
|
+
data = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
3356
|
+
if (data.version !== CACHE_VERSION) {
|
|
3357
|
+
data = { version: CACHE_VERSION, entries: {}, lastScanTime: 0 };
|
|
3358
|
+
}
|
|
3359
|
+
} catch {
|
|
3360
|
+
data = { version: CACHE_VERSION, entries: {}, lastScanTime: 0 };
|
|
3361
|
+
}
|
|
3362
|
+
} else {
|
|
3363
|
+
data = { version: CACHE_VERSION, entries: {}, lastScanTime: 0 };
|
|
3364
|
+
}
|
|
3365
|
+
data.entries[agentName] = {
|
|
3366
|
+
sessions,
|
|
3367
|
+
meta,
|
|
3368
|
+
timestamp: Date.now(),
|
|
3369
|
+
version: CACHE_VERSION
|
|
3370
|
+
};
|
|
3371
|
+
data.lastScanTime = Date.now();
|
|
3372
|
+
writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
3373
|
+
} catch {
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function clearCache() {
|
|
3377
|
+
try {
|
|
3378
|
+
const cachePath = getCachePath();
|
|
3379
|
+
if (existsSync7(cachePath)) {
|
|
3380
|
+
const data = {
|
|
3381
|
+
version: CACHE_VERSION,
|
|
3382
|
+
entries: {},
|
|
3383
|
+
lastScanTime: 0
|
|
3384
|
+
};
|
|
3385
|
+
writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
3386
|
+
}
|
|
3387
|
+
} catch {
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
function getCacheInfo() {
|
|
3391
|
+
try {
|
|
3392
|
+
const cachePath = getCachePath();
|
|
3393
|
+
if (!existsSync7(cachePath)) {
|
|
3394
|
+
return { lastScanTime: null, size: 0 };
|
|
3395
|
+
}
|
|
3396
|
+
const data = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
3397
|
+
const size = Object.values(data.entries).reduce((sum, entry) => sum + entry.sessions.length, 0);
|
|
3398
|
+
return {
|
|
3399
|
+
lastScanTime: data.lastScanTime || null,
|
|
3400
|
+
size
|
|
3401
|
+
};
|
|
3402
|
+
} catch {
|
|
3403
|
+
return { lastScanTime: null, size: 0 };
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
function isPathScopeMatch(queryPath, sessionPath) {
|
|
3407
|
+
if (!sessionPath) return false;
|
|
3408
|
+
const q = resolve(queryPath);
|
|
3409
|
+
const s = resolve(sessionPath);
|
|
3410
|
+
const sepNorm = (p) => p.replaceAll(sep, "/");
|
|
3411
|
+
const sn = sepNorm(s);
|
|
3412
|
+
const qn = sepNorm(q);
|
|
3413
|
+
return sn === qn || sn.startsWith(qn + "/") || qn.startsWith(sn + "/");
|
|
3414
|
+
}
|
|
3415
|
+
function filterSessions(sessions, options) {
|
|
3416
|
+
let result = sessions;
|
|
3417
|
+
if (options.cwd) {
|
|
3418
|
+
const cwd = options.cwd;
|
|
3419
|
+
result = result.filter((s) => isPathScopeMatch(cwd, s.directory));
|
|
3420
|
+
}
|
|
3421
|
+
if (options.from != null) {
|
|
3422
|
+
result = result.filter((s) => s.time_created >= options.from);
|
|
3423
|
+
}
|
|
3424
|
+
if (options.to != null) {
|
|
3425
|
+
result = result.filter((s) => s.time_created <= options.to);
|
|
3426
|
+
}
|
|
3427
|
+
return result;
|
|
3428
|
+
}
|
|
3429
|
+
async function scanAgentSmart(agent, options, onProgress) {
|
|
3430
|
+
const useCache = options.useCache ?? true;
|
|
3431
|
+
const smartRefresh = options.smartRefresh ?? true;
|
|
3432
|
+
if (useCache) {
|
|
3433
|
+
const cached = loadCachedSessions(agent.name);
|
|
3434
|
+
if (cached !== null) {
|
|
3435
|
+
if (agent.setSessionMetaMap) {
|
|
3436
|
+
const metaMap = /* @__PURE__ */ new Map();
|
|
3437
|
+
for (const [id, meta] of Object.entries(cached.meta)) {
|
|
3438
|
+
metaMap.set(id, meta);
|
|
3439
|
+
}
|
|
3440
|
+
agent.setSessionMetaMap(metaMap);
|
|
3441
|
+
}
|
|
3442
|
+
onProgress?.({
|
|
3443
|
+
agent: agent.name,
|
|
3444
|
+
phase: "cache",
|
|
3445
|
+
cachedCount: cached.sessions.length
|
|
3446
|
+
});
|
|
3447
|
+
if (smartRefresh && agent.checkForChanges) {
|
|
3448
|
+
setTimeout(async () => {
|
|
3449
|
+
await refreshAgentAsync(agent, cached.sessions, cached.timestamp, onProgress);
|
|
3450
|
+
}, 0);
|
|
3451
|
+
}
|
|
3452
|
+
const filtered = filterSessions(cached.sessions, options);
|
|
3453
|
+
return { agent, heads: filtered, fromCache: true };
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
return scanAgentFull(agent, options, onProgress);
|
|
3457
|
+
}
|
|
3458
|
+
async function refreshAgentAsync(agent, cachedSessions, cacheTimestamp, onProgress) {
|
|
3459
|
+
try {
|
|
3460
|
+
onProgress?.({ agent: agent.name, phase: "checking" });
|
|
3461
|
+
const checkResult = await Promise.resolve(
|
|
3462
|
+
agent.checkForChanges(cacheTimestamp, cachedSessions)
|
|
3463
|
+
);
|
|
3464
|
+
if (!checkResult.hasChanges) {
|
|
3465
|
+
onProgress?.({ agent: agent.name, phase: "complete" });
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
onProgress?.({
|
|
3469
|
+
agent: agent.name,
|
|
3470
|
+
phase: "incremental",
|
|
3471
|
+
changedCount: checkResult.changedIds?.length
|
|
3472
|
+
});
|
|
3473
|
+
const updatedSessions = await Promise.resolve(
|
|
3474
|
+
agent.incrementalScan(cachedSessions, checkResult.changedIds || [])
|
|
3475
|
+
);
|
|
3476
|
+
const metaMap = agent.getSessionMetaMap?.();
|
|
3477
|
+
const meta = {};
|
|
3478
|
+
if (metaMap) {
|
|
3479
|
+
for (const [id, data] of metaMap.entries()) {
|
|
3480
|
+
meta[id] = { id, ...data };
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
saveCachedSessions(agent.name, updatedSessions, meta);
|
|
3484
|
+
onProgress?.({
|
|
3485
|
+
agent: agent.name,
|
|
3486
|
+
phase: "complete",
|
|
3487
|
+
newCount: updatedSessions.length
|
|
3488
|
+
});
|
|
3489
|
+
} catch (err) {
|
|
3490
|
+
console.error(`[${agent.name}] Background refresh failed:`, err);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
async function scanAgentFull(agent, options, onProgress) {
|
|
3494
|
+
const availMarker = perf.start(`agent:${agent.name}:isAvailable`);
|
|
3495
|
+
const isAvail = agent.isAvailable();
|
|
3496
|
+
perf.end(availMarker);
|
|
3497
|
+
if (!isAvail) {
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
const scanMarker = perf.start(`agent:${agent.name}:scan`);
|
|
3502
|
+
const heads = agent.scan();
|
|
3503
|
+
perf.end(scanMarker);
|
|
3504
|
+
const metaMap = agent.getSessionMetaMap?.();
|
|
3505
|
+
const meta = {};
|
|
3506
|
+
if (metaMap) {
|
|
3507
|
+
for (const [id, data] of metaMap.entries()) {
|
|
3508
|
+
meta[id] = { id, ...data };
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
saveCachedSessions(agent.name, heads, meta);
|
|
3512
|
+
onProgress?.({ agent: agent.name, phase: "complete", newCount: heads.length });
|
|
3513
|
+
const filtered = filterSessions(heads, options);
|
|
3514
|
+
return { agent, heads: filtered, fromCache: false };
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
console.error(`Error scanning ${agent.name}:`, err);
|
|
3517
|
+
return { agent, heads: [], fromCache: false };
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
async function scanSessions(options = {}, onProgress) {
|
|
3521
|
+
const scanMarker = perf.start("scanSessions");
|
|
3522
|
+
const agents = createRegisteredAgents();
|
|
3523
|
+
const byAgent = {};
|
|
3524
|
+
const allSessions = [];
|
|
3525
|
+
const availableAgents = [];
|
|
3526
|
+
const agentFilter = options.agents?.length ? new Set(options.agents.map((a) => a.toLowerCase())) : null;
|
|
3527
|
+
const agentsToScan = agents.filter((agent) => {
|
|
3528
|
+
if (agentFilter && !agentFilter.has(agent.name.toLowerCase())) {
|
|
3529
|
+
return false;
|
|
3530
|
+
}
|
|
3531
|
+
return true;
|
|
3532
|
+
});
|
|
3533
|
+
const scanPromises = agentsToScan.map((agent) => scanAgentSmart(agent, options, onProgress));
|
|
3534
|
+
const results = await Promise.all(scanPromises);
|
|
3535
|
+
for (const result of results) {
|
|
3536
|
+
if (result) {
|
|
3537
|
+
availableAgents.push(result.agent);
|
|
3538
|
+
byAgent[result.agent.name] = result.heads;
|
|
3539
|
+
allSessions.push(...result.heads);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
perf.end(scanMarker);
|
|
3543
|
+
return { sessions: allSessions, byAgent, agents: availableAgents };
|
|
3544
|
+
}
|
|
3545
|
+
async function scanSessionsAsync(options = {}, onProgress) {
|
|
3546
|
+
return scanSessions(options, onProgress);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
export {
|
|
3550
|
+
registerAgent,
|
|
3551
|
+
createRegisteredAgents,
|
|
3552
|
+
getRegisteredAgents,
|
|
3553
|
+
getAgentInfoMap,
|
|
3554
|
+
getAgentByName,
|
|
3555
|
+
BaseAgent,
|
|
3556
|
+
firstExisting,
|
|
3557
|
+
resolveProviderRoots,
|
|
3558
|
+
getCursorDataPath,
|
|
3559
|
+
parseJsonlLines,
|
|
3560
|
+
readJsonlFile,
|
|
3561
|
+
normalizeTitleText,
|
|
3562
|
+
basenameTitle,
|
|
3563
|
+
resolveSessionTitle,
|
|
3564
|
+
perf,
|
|
3565
|
+
openDbReadOnly,
|
|
3566
|
+
isSqliteAvailable,
|
|
3567
|
+
loadCachedSessions,
|
|
3568
|
+
saveCachedSessions,
|
|
3569
|
+
clearCache,
|
|
3570
|
+
getCacheInfo,
|
|
3571
|
+
scanSessions,
|
|
3572
|
+
scanSessionsAsync
|
|
3573
|
+
};
|
|
3574
|
+
//# sourceMappingURL=chunk-H3D3GNQK.js.map
|