@evomap/evolver-runtime-adapters 2.0.0-beta.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +7 -1
- package/dist/adapters.js +85 -5
- package/dist/cursorState.d.ts +7 -2
- package/dist/cursorState.js +264 -51
- package/dist/types.d.ts +6 -0
- package/package.json +5 -2
package/dist/adapters.d.ts
CHANGED
|
@@ -6,5 +6,11 @@ export declare const geminiAdapter: SessionLogAdapter;
|
|
|
6
6
|
export declare const antigravityAdapter: SessionLogAdapter;
|
|
7
7
|
export declare const genericChatAdapter: SessionLogAdapter;
|
|
8
8
|
export declare const kimiAdapter: SessionLogAdapter;
|
|
9
|
+
export declare const kiroRemovedAdapter: SessionLogAdapter;
|
|
10
|
+
export declare const opencodeRemovedAdapter: SessionLogAdapter;
|
|
11
|
+
/** Explicitly removed transcript adapters — path fail-closed only. Never walk this list for content probes. */
|
|
12
|
+
export declare const REMOVED_ADAPTERS: readonly SessionLogAdapter[];
|
|
9
13
|
export declare const ADAPTERS: readonly SessionLogAdapter[];
|
|
10
|
-
export declare function adapterForPath(path: string): SessionLogAdapter | undefined;
|
|
14
|
+
export declare function adapterForPath(path: string): SessionLogAdapter | undefined;
|
|
15
|
+
/** True when the agent id has an explicit removed transcript sentinel (see REMOVED_ADAPTERS). */
|
|
16
|
+
export declare function isRemovedAdapter(agent: string): boolean;
|
package/dist/adapters.js
CHANGED
|
@@ -291,26 +291,71 @@ function anthropicSessionSystemPrompt(chunk) {
|
|
|
291
291
|
}
|
|
292
292
|
return undefined;
|
|
293
293
|
}
|
|
294
|
-
|
|
294
|
+
const NATIVE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
295
|
+
const CLAUDE_TRANSCRIPT_PATH = /(?:^|[/\\])\.claude[/\\]projects[/\\](?:[^/\\]+[/\\])*([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.jsonl$/;
|
|
296
|
+
const CURSOR_TRANSCRIPT_PATHS = [
|
|
297
|
+
/(?:^|[/\\])\.cursor[/\\]projects[/\\](?:[^/\\]+[/\\])*agent-transcripts[/\\]([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.jsonl$/,
|
|
298
|
+
/(?:^|[/\\])\.cursor[/\\]projects[/\\](?:[^/\\]+[/\\])*agent-transcripts[/\\]([A-Za-z0-9][A-Za-z0-9._-]{0,127})[/\\]\1\.jsonl$/,
|
|
299
|
+
];
|
|
300
|
+
function transcriptSessionId(chunk, keys) {
|
|
301
|
+
const values = new Set();
|
|
302
|
+
for (const row of parseJsonlLines(chunk)) {
|
|
303
|
+
for (const key of keys) {
|
|
304
|
+
if (!hasOwn(row, key))
|
|
305
|
+
continue;
|
|
306
|
+
const value = row[key];
|
|
307
|
+
if (typeof value !== 'string' || !NATIVE_SESSION_ID.test(value))
|
|
308
|
+
return { invalid: true };
|
|
309
|
+
values.add(value);
|
|
310
|
+
if (values.size > 1)
|
|
311
|
+
return { invalid: true };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { value: values.values().next().value, invalid: false };
|
|
315
|
+
}
|
|
316
|
+
function nativeResumeIdentity(harness, path, chunk, pathPattern, recordKeys) {
|
|
317
|
+
const pathId = pathPattern.exec(path)?.[1];
|
|
318
|
+
if (!pathId || !NATIVE_SESSION_ID.test(pathId))
|
|
319
|
+
return undefined;
|
|
320
|
+
const recordId = transcriptSessionId(chunk, recordKeys);
|
|
321
|
+
if (recordId.invalid || (recordId.value !== undefined && recordId.value !== pathId))
|
|
322
|
+
return undefined;
|
|
323
|
+
return { harness, sessionId: pathId };
|
|
324
|
+
}
|
|
325
|
+
function cursorResumeIdentity(path, chunk) {
|
|
326
|
+
for (const pattern of CURSOR_TRANSCRIPT_PATHS) {
|
|
327
|
+
const identity = nativeResumeIdentity('cursor', path, chunk, pattern, ['sessionId', 'session_id', 'conversationId', 'conversation_id']);
|
|
328
|
+
if (identity)
|
|
329
|
+
return identity;
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
function anthropicStyleSession(chunk, sessionIdKeys = []) {
|
|
295
334
|
const systemPrompt = anthropicSessionSystemPrompt(chunk);
|
|
335
|
+
const sessionId = transcriptSessionId(chunk, sessionIdKeys);
|
|
296
336
|
return {
|
|
297
337
|
turns: anthropicStyleTranscript(chunk),
|
|
298
338
|
...(systemPrompt ? { systemPrompt } : {}),
|
|
339
|
+
...(!sessionId.invalid && sessionId.value ? { sessionId: sessionId.value } : {}),
|
|
299
340
|
};
|
|
300
341
|
}
|
|
301
342
|
export const claudeCodeAdapter = {
|
|
302
343
|
agent: 'claude-code',
|
|
303
344
|
detect: (p) => /\.claude[/\\]projects[/\\].*\.jsonl$/.test(p) || /claude.*\.jsonl$/i.test(p),
|
|
345
|
+
resumeIdentityFromSource: (path, chunk) => nativeResumeIdentity('claude-code', path, chunk, CLAUDE_TRANSCRIPT_PATH, ['sessionId', 'session_id']),
|
|
304
346
|
parse: anthropicStyleTranscript,
|
|
305
|
-
parseSession: anthropicStyleSession,
|
|
347
|
+
parseSession: (chunk) => anthropicStyleSession(chunk, ['sessionId', 'session_id']),
|
|
306
348
|
};
|
|
307
|
-
// Verified against real cursor agent-transcripts
|
|
349
|
+
// Verified against real cursor agent-transcripts
|
|
350
|
+
// (~/.cursor/projects/<proj>/agent-transcripts/<session-id>.jsonl, with newer nested layouts also accepted):
|
|
308
351
|
// same Anthropic content-block shape as claude-code — observed blocks are text + tool_use (no tool_result). Other
|
|
309
352
|
// .jsonl that live under .cursor (eval datasets: task_id/canonical_solution, no role) carry no turn and parse to [].
|
|
310
353
|
export const cursorAdapter = {
|
|
311
354
|
agent: 'cursor',
|
|
312
355
|
detect: (p) => /\.cursor[/\\].*\.jsonl$/.test(p) || /cursor.*\.jsonl$/i.test(p),
|
|
356
|
+
resumeIdentityFromSource: cursorResumeIdentity,
|
|
313
357
|
parse: anthropicStyleTranscript,
|
|
358
|
+
parseSession: (chunk) => anthropicStyleSession(chunk, ['sessionId', 'session_id', 'conversationId', 'conversation_id']),
|
|
314
359
|
};
|
|
315
360
|
// Verified against codex-cli 0.137.0 rollout logs (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl). Each line is
|
|
316
361
|
// {timestamp, type, payload}. Codex writes the SAME conversation twice: a high-level `event_msg` stream
|
|
@@ -1217,7 +1262,42 @@ export const kimiAdapter = {
|
|
|
1217
1262
|
parseSession: (chunk) => kimiSessions(chunk)[0] ?? { turns: [] },
|
|
1218
1263
|
parseSessions: kimiSessions,
|
|
1219
1264
|
};
|
|
1220
|
-
//
|
|
1265
|
+
// ── Removed runtime adapters ─────────────────────────────────────────────────
|
|
1266
|
+
// Kiro and OpenCode transcript adapters were removed in V2: no sanitized real-log golden fixture exists for
|
|
1267
|
+
// either runtime, so shipping a guessed schema would silently yield 0 turns on real logs while appearing
|
|
1268
|
+
// supported. The MCP config installer (setup-hooks --runtime=kiro|opencode) remains functional for injection.
|
|
1269
|
+
//
|
|
1270
|
+
// Sentinels live in REMOVED_ADAPTERS (NOT in ADAPTERS) so content probes that walk ADAPTERS and call
|
|
1271
|
+
// parse() cannot throw "kiro removed" on unrelated JSON (trajectoryExport.adapterForContent).
|
|
1272
|
+
// Path-based selection uses adapterForPath, which checks REMOVED_ADAPTERS first; matching paths then fail
|
|
1273
|
+
// closed on parse with an actionable error.
|
|
1274
|
+
const REMOVED_ADAPTER_MESSAGE = (agent) => `${agent} transcript adapter was removed from evolver v2 — no sanitized real-log golden fixture exists. `
|
|
1275
|
+
+ `Re-add with a verified golden test (see adapters.test.ts). `
|
|
1276
|
+
+ `MCP injection (setup-hooks --runtime=${agent}) is still supported; only transcript ingest is blocked.`;
|
|
1277
|
+
function removedAdapter(agent, detect) {
|
|
1278
|
+
return {
|
|
1279
|
+
agent,
|
|
1280
|
+
detect,
|
|
1281
|
+
parse: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
|
|
1282
|
+
parseSession: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
|
|
1283
|
+
parseSessions: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
export const kiroRemovedAdapter = removedAdapter('kiro', (p) => /(^|[/\\])\.kiro([/\\]|$)/i.test(p) || /(^|[/\\])kiro[^/\\]*\.jsonl?$/i.test(p));
|
|
1287
|
+
export const opencodeRemovedAdapter = removedAdapter('opencode', (p) => /(^|[/\\])\.opencode([/\\]|$)/i.test(p) || /(^|[/\\])opencode[^/\\]*\.jsonl?$/i.test(p));
|
|
1288
|
+
/** Explicitly removed transcript adapters — path fail-closed only. Never walk this list for content probes. */
|
|
1289
|
+
export const REMOVED_ADAPTERS = [kiroRemovedAdapter, opencodeRemovedAdapter];
|
|
1290
|
+
// Only verified adapters are registered for content/path discovery. Removed runtimes are listed in
|
|
1291
|
+
// REMOVED_ADAPTERS and selected only via adapterForPath path match (then parse throws).
|
|
1221
1292
|
// genericChatAdapter is LAST so any tool-specific path (claude/cursor/codex/gemini/antigravity/kimi) resolves first.
|
|
1222
1293
|
export const ADAPTERS = [claudeCodeAdapter, codexAdapter, cursorAdapter, geminiAdapter, antigravityAdapter, kimiAdapter, genericChatAdapter];
|
|
1223
|
-
export function adapterForPath(path) {
|
|
1294
|
+
export function adapterForPath(path) {
|
|
1295
|
+
const removed = REMOVED_ADAPTERS.find((a) => a.detect(path));
|
|
1296
|
+
if (removed)
|
|
1297
|
+
return removed;
|
|
1298
|
+
return ADAPTERS.find((a) => a.detect(path));
|
|
1299
|
+
}
|
|
1300
|
+
/** True when the agent id has an explicit removed transcript sentinel (see REMOVED_ADAPTERS). */
|
|
1301
|
+
export function isRemovedAdapter(agent) {
|
|
1302
|
+
return REMOVED_ADAPTERS.some((a) => a.agent === agent);
|
|
1303
|
+
}
|
package/dist/cursorState.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { NormalizedSession } from './types.js';
|
|
2
|
+
export type CursorStateVscdbErrorStage = 'open' | 'schema' | 'query';
|
|
3
|
+
export declare class CursorStateVscdbError extends Error {
|
|
4
|
+
readonly stage: CursorStateVscdbErrorStage;
|
|
5
|
+
constructor(stage: CursorStateVscdbErrorStage, message: string, cause?: unknown);
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
8
|
* Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
|
|
4
|
-
* composer that has at least one real (non-meta) turn.
|
|
5
|
-
*
|
|
9
|
+
* composer that has at least one real (non-meta) turn. A valid database with no sessions returns []; database open,
|
|
10
|
+
* query, and incompatible-schema failures throw CursorStateVscdbError. The database is always opened read-only.
|
|
6
11
|
*/
|
|
7
12
|
export declare function parseCursorStateVscdb(dbPath: string): NormalizedSession[];
|
|
8
13
|
/** True for a path that looks like a Cursor globalStorage state.vscdb. */
|
package/dist/cursorState.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { correlateToolNames, isMetaText } from './types.js';
|
|
3
3
|
const nodeRequire = createRequire(import.meta.url);
|
|
4
|
+
export class CursorStateVscdbError extends Error {
|
|
5
|
+
stage;
|
|
6
|
+
constructor(stage, message, cause) {
|
|
7
|
+
super(`Cursor state database ${stage} error: ${message}`, { cause });
|
|
8
|
+
this.name = 'CursorStateVscdbError';
|
|
9
|
+
this.stage = stage;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
4
12
|
function isBunRuntime() {
|
|
5
13
|
return typeof process.versions === 'object' && typeof process.versions.bun === 'string';
|
|
6
14
|
}
|
|
@@ -37,11 +45,9 @@ function openReadOnlySqliteDatabase(path) {
|
|
|
37
45
|
// bubbleId:<composer>:<bubble> rows; extracting user/assistant text, assistant `thinking` as a reasoning turn,
|
|
38
46
|
// and `toolFormerData` as a tool_use + tool_result pair; per-bubble token counts; session model/createdAt.
|
|
39
47
|
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// branch bubbles are read flat (no tree structure); attachment/context payloads are ignored. Extend with a real
|
|
44
|
-
// populated-DB fixture before trusting those.
|
|
48
|
+
// Composer roots can also carry an inline `conversation` array (including sub-composer roots). Attachment bodies
|
|
49
|
+
// and explicit code-block content are preserved as turns. We intentionally do not synthesize before/after diffs
|
|
50
|
+
// from codeBlockData/originalFileStates because those fields vary by Cursor version.
|
|
45
51
|
function isRecord(value) {
|
|
46
52
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
47
53
|
}
|
|
@@ -90,9 +96,61 @@ function bubbleToolTurns(bubble, toolUseId) {
|
|
|
90
96
|
}
|
|
91
97
|
return turns;
|
|
92
98
|
}
|
|
93
|
-
function
|
|
94
|
-
if (
|
|
99
|
+
function explicitBodyText(value) {
|
|
100
|
+
if (typeof value === 'string')
|
|
101
|
+
return value;
|
|
102
|
+
if (!isRecord(value))
|
|
103
|
+
return '';
|
|
104
|
+
return asString(value['text']) || asString(value['content']);
|
|
105
|
+
}
|
|
106
|
+
function bubbleAttachmentTexts(bubble) {
|
|
107
|
+
const directValue = bubble['attachments'];
|
|
108
|
+
if (directValue !== undefined && !Array.isArray(directValue)) {
|
|
109
|
+
throw new CursorStateVscdbError('schema', 'conversation attachments must be an array');
|
|
110
|
+
}
|
|
111
|
+
const direct = Array.isArray(directValue) ? directValue : [];
|
|
112
|
+
const toolValue = isRecord(bubble['toolFormerData']) ? bubble['toolFormerData']['attachments'] : undefined;
|
|
113
|
+
if (toolValue !== undefined && !Array.isArray(toolValue)) {
|
|
114
|
+
throw new CursorStateVscdbError('schema', 'tool attachments must be an array');
|
|
115
|
+
}
|
|
116
|
+
const tool = Array.isArray(toolValue) ? toolValue : [];
|
|
117
|
+
return [...direct, ...tool].map((attachment) => {
|
|
118
|
+
if (!isRecord(attachment)) {
|
|
119
|
+
throw new CursorStateVscdbError('schema', 'attachment must be an object');
|
|
120
|
+
}
|
|
121
|
+
return explicitBodyText(attachment['body']);
|
|
122
|
+
}).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
function bubbleCodeBlockTexts(bubble) {
|
|
125
|
+
const codeBlocks = bubble['codeBlocks'];
|
|
126
|
+
if (codeBlocks === undefined)
|
|
95
127
|
return [];
|
|
128
|
+
if (!Array.isArray(codeBlocks)) {
|
|
129
|
+
throw new CursorStateVscdbError('schema', 'conversation codeBlocks must be an array');
|
|
130
|
+
}
|
|
131
|
+
return codeBlocks.map(explicitContentText).filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
function explicitContentText(value) {
|
|
134
|
+
if (typeof value === 'string')
|
|
135
|
+
return value;
|
|
136
|
+
if (!isRecord(value))
|
|
137
|
+
return '';
|
|
138
|
+
return asString(value['content']) || asString(value['text']) || asString(value['code']) || explicitBodyText(value['body']);
|
|
139
|
+
}
|
|
140
|
+
function contentTurns(role, texts, existingText = '') {
|
|
141
|
+
const seen = new Set(existingText ? [existingText] : []);
|
|
142
|
+
return texts.flatMap((text) => {
|
|
143
|
+
const trimmed = text.trim();
|
|
144
|
+
if (!trimmed || seen.has(trimmed))
|
|
145
|
+
return [];
|
|
146
|
+
seen.add(trimmed);
|
|
147
|
+
return [{ role, text, isMeta: isMetaText(text) }];
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function bubbleToTurns(bubble, bubbleId) {
|
|
151
|
+
if (!isRecord(bubble)) {
|
|
152
|
+
throw new CursorStateVscdbError('schema', `conversation bubble ${bubbleId} is missing or invalid`);
|
|
153
|
+
}
|
|
96
154
|
const type = finiteNumber(bubble['type']);
|
|
97
155
|
const inputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['inputTokens']) : undefined;
|
|
98
156
|
const outputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['outputTokens']) : undefined;
|
|
@@ -101,9 +159,14 @@ function bubbleToTurns(bubble, bubbleId) {
|
|
|
101
159
|
if (type === 1) {
|
|
102
160
|
// user bubble
|
|
103
161
|
turns.push({ role: 'user', text, isMeta: isMetaText(text) });
|
|
162
|
+
turns.push(...contentTurns('user', bubbleAttachmentTexts(bubble), text));
|
|
163
|
+
turns.push(...contentTurns('user', bubbleCodeBlockTexts(bubble), text));
|
|
104
164
|
return turns;
|
|
105
165
|
}
|
|
106
|
-
|
|
166
|
+
if (type !== 2) {
|
|
167
|
+
throw new CursorStateVscdbError('schema', `conversation bubble ${bubbleId} has an unsupported type`);
|
|
168
|
+
}
|
|
169
|
+
// assistant bubble
|
|
107
170
|
const thinking = bubbleThinkingText(bubble);
|
|
108
171
|
if (thinking) {
|
|
109
172
|
turns.push({ role: 'assistant', text: thinking, reasoning: true, isMeta: false });
|
|
@@ -118,31 +181,139 @@ function bubbleToTurns(bubble, bubbleId) {
|
|
|
118
181
|
});
|
|
119
182
|
}
|
|
120
183
|
turns.push(...bubbleToolTurns(bubble, bubbleId));
|
|
184
|
+
turns.push(...contentTurns('assistant', bubbleAttachmentTexts(bubble), text));
|
|
185
|
+
turns.push(...contentTurns('assistant', bubbleCodeBlockTexts(bubble), text));
|
|
121
186
|
return turns;
|
|
122
187
|
}
|
|
123
188
|
function conversationHeaders(composer) {
|
|
124
189
|
const headers = composer['fullConversationHeadersOnly'];
|
|
125
|
-
if (
|
|
190
|
+
if (headers === undefined)
|
|
126
191
|
return [];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
192
|
+
if (!Array.isArray(headers)) {
|
|
193
|
+
throw new CursorStateVscdbError('schema', 'fullConversationHeadersOnly must be an array');
|
|
194
|
+
}
|
|
195
|
+
return headers.map((header) => {
|
|
196
|
+
if (!isRecord(header))
|
|
197
|
+
throw new CursorStateVscdbError('schema', 'conversation header must be an object');
|
|
198
|
+
const bubbleId = asString(header['bubbleId']);
|
|
199
|
+
if (!bubbleId)
|
|
200
|
+
throw new CursorStateVscdbError('schema', 'conversation header bubbleId is missing');
|
|
201
|
+
return { bubbleId, type: finiteNumber(header['type']) };
|
|
202
|
+
});
|
|
130
203
|
}
|
|
131
|
-
function
|
|
204
|
+
function inlineConversation(composer) {
|
|
205
|
+
const conversation = composer['conversation'];
|
|
206
|
+
if (conversation === undefined)
|
|
207
|
+
return [];
|
|
208
|
+
if (!Array.isArray(conversation)) {
|
|
209
|
+
throw new CursorStateVscdbError('schema', 'conversation must be an array');
|
|
210
|
+
}
|
|
211
|
+
return conversation.map((bubble, index) => ({
|
|
212
|
+
bubbleId: isRecord(bubble) ? asString(bubble['bubbleId']) || `inline-${index + 1}` : `inline-${index + 1}`,
|
|
213
|
+
value: bubble,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
function codeBlockContentByBubble(composer, readCodeBlockDiff) {
|
|
217
|
+
const byBubble = new Map();
|
|
218
|
+
const trailing = [];
|
|
219
|
+
const codeBlockData = composer['codeBlockData'];
|
|
220
|
+
if (codeBlockData === undefined)
|
|
221
|
+
return { byBubble, trailing };
|
|
222
|
+
if (!isRecord(codeBlockData)) {
|
|
223
|
+
throw new CursorStateVscdbError('schema', 'codeBlockData must be an object');
|
|
224
|
+
}
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
const add = (text, bubbleId) => {
|
|
227
|
+
const trimmed = text.trim();
|
|
228
|
+
const key = `${bubbleId}\0${trimmed}`;
|
|
229
|
+
if (!trimmed || seen.has(key))
|
|
230
|
+
return;
|
|
231
|
+
seen.add(key);
|
|
232
|
+
if (bubbleId) {
|
|
233
|
+
const texts = byBubble.get(bubbleId);
|
|
234
|
+
if (texts)
|
|
235
|
+
texts.push(text);
|
|
236
|
+
else
|
|
237
|
+
byBubble.set(bubbleId, [text]);
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
trailing.push(text);
|
|
241
|
+
};
|
|
242
|
+
const stack = Object.values(codeBlockData).reverse()
|
|
243
|
+
.map((value) => ({ value, bubbleId: '' }));
|
|
244
|
+
let visited = 0;
|
|
245
|
+
while (stack.length > 0) {
|
|
246
|
+
if (++visited > 100_000)
|
|
247
|
+
throw new CursorStateVscdbError('schema', 'codeBlockData is too deeply nested');
|
|
248
|
+
const current = stack.pop();
|
|
249
|
+
if (Array.isArray(current.value)) {
|
|
250
|
+
for (let index = current.value.length - 1; index >= 0; index--) {
|
|
251
|
+
stack.push({ value: current.value[index], bubbleId: current.bubbleId });
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!isRecord(current.value))
|
|
256
|
+
continue;
|
|
257
|
+
const bubbleId = asString(current.value['bubbleId']) || current.bubbleId;
|
|
258
|
+
const directText = asString(current.value['content']) || asString(current.value['text'])
|
|
259
|
+
|| asString(current.value['code']) || asString(current.value['body']);
|
|
260
|
+
if (directText)
|
|
261
|
+
add(directText, bubbleId);
|
|
262
|
+
const diffId = asString(current.value['diffId']);
|
|
263
|
+
if (diffId) {
|
|
264
|
+
const diff = readCodeBlockDiff(diffId);
|
|
265
|
+
if (!isRecord(diff) || !Array.isArray(diff['newModelDiffWrtV0'])) {
|
|
266
|
+
throw new CursorStateVscdbError('schema', 'code block diff must contain newModelDiffWrtV0');
|
|
267
|
+
}
|
|
268
|
+
for (const line of diff['newModelDiffWrtV0']) {
|
|
269
|
+
if (!isRecord(line)) {
|
|
270
|
+
throw new CursorStateVscdbError('schema', 'code block diff line must be an object');
|
|
271
|
+
}
|
|
272
|
+
const modified = line['modified'];
|
|
273
|
+
if (typeof modified === 'string')
|
|
274
|
+
add(modified, bubbleId);
|
|
275
|
+
else if (Array.isArray(modified) && modified.every((value) => typeof value === 'string')) {
|
|
276
|
+
add(modified.join('\n'), bubbleId);
|
|
277
|
+
}
|
|
278
|
+
else
|
|
279
|
+
throw new CursorStateVscdbError('schema', 'code block diff line must contain modified text');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const children = Object.values(current.value).filter((child) => Array.isArray(child) || isRecord(child));
|
|
283
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
284
|
+
stack.push({ value: children[index], bubbleId });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { byBubble, trailing };
|
|
288
|
+
}
|
|
289
|
+
function composerToSession(composer, composerId, readBubble, readCodeBlockDiff) {
|
|
132
290
|
const headers = conversationHeaders(composer);
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
291
|
+
const inline = inlineConversation(composer);
|
|
292
|
+
const inlineById = new Map(inline.map((bubble) => [bubble.bubbleId, bubble.value]));
|
|
293
|
+
const conversationMapValue = composer['conversationMap'];
|
|
294
|
+
if (conversationMapValue !== undefined && !isRecord(conversationMapValue)) {
|
|
295
|
+
throw new CursorStateVscdbError('schema', 'conversationMap must be an object');
|
|
296
|
+
}
|
|
297
|
+
const conversationMap = isRecord(conversationMapValue) ? conversationMapValue : undefined;
|
|
298
|
+
const codeBlocks = codeBlockContentByBubble(composer, (diffId) => readCodeBlockDiff(composerId, diffId));
|
|
299
|
+
const orderedBubbles = headers.length > 0
|
|
300
|
+
? headers.map((header) => ({ bubbleId: header.bubbleId, value: (conversationMap && conversationMap[header.bubbleId] !== undefined)
|
|
301
|
+
? conversationMap[header.bubbleId]
|
|
302
|
+
: readBubble(composerId, header.bubbleId) ?? inlineById.get(header.bubbleId) }))
|
|
303
|
+
: inline.length > 0
|
|
304
|
+
? inline
|
|
305
|
+
: (conversationMap ? Object.entries(conversationMap).map(([bubbleId, value]) => ({ bubbleId, value })) : []);
|
|
137
306
|
const turns = [];
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: readBubble(composerId, bubbleId);
|
|
142
|
-
turns.push(...bubbleToTurns(bubble, bubbleId));
|
|
307
|
+
for (const bubble of orderedBubbles) {
|
|
308
|
+
turns.push(...bubbleToTurns(bubble.value, bubble.bubbleId));
|
|
309
|
+
turns.push(...contentTurns('assistant', codeBlocks.byBubble.get(bubble.bubbleId) ?? []));
|
|
143
310
|
}
|
|
311
|
+
turns.push(...contentTurns('assistant', codeBlocks.trailing));
|
|
144
312
|
const model = isRecord(composer['modelConfig']) ? asString(composer['modelConfig']['modelName']) : '';
|
|
145
313
|
const createdAt = finiteNumber(composer['createdAt']);
|
|
314
|
+
if (createdAt !== undefined && Math.abs(createdAt) > 8_640_000_000_000_000) {
|
|
315
|
+
throw new CursorStateVscdbError('schema', 'composer createdAt is outside the supported date range');
|
|
316
|
+
}
|
|
146
317
|
return {
|
|
147
318
|
turns: correlateToolNames(turns),
|
|
148
319
|
sessionId: composerId,
|
|
@@ -155,51 +326,93 @@ function composerToSession(composer, composerId, readBubble) {
|
|
|
155
326
|
}
|
|
156
327
|
/**
|
|
157
328
|
* Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
|
|
158
|
-
* composer that has at least one real (non-meta) turn.
|
|
159
|
-
*
|
|
329
|
+
* composer that has at least one real (non-meta) turn. A valid database with no sessions returns []; database open,
|
|
330
|
+
* query, and incompatible-schema failures throw CursorStateVscdbError. The database is always opened read-only.
|
|
160
331
|
*/
|
|
161
332
|
export function parseCursorStateVscdb(dbPath) {
|
|
162
333
|
let db;
|
|
163
334
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
335
|
+
try {
|
|
336
|
+
db = openReadOnlySqliteDatabase(dbPath);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
throw new CursorStateVscdbError('open', 'unable to open the file read-only', error);
|
|
340
|
+
}
|
|
341
|
+
let columns;
|
|
342
|
+
try {
|
|
343
|
+
columns = db.prepare('PRAGMA table_info(cursorDiskKV)').all();
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new CursorStateVscdbError('query', 'unable to inspect cursorDiskKV', error);
|
|
347
|
+
}
|
|
348
|
+
const columnNames = new Set(columns.flatMap((column) => isRecord(column) ? [asString(column['name'])] : []));
|
|
349
|
+
if (!columnNames.has('key') || !columnNames.has('value')) {
|
|
350
|
+
throw new CursorStateVscdbError('schema', 'cursorDiskKV with key and value columns is required');
|
|
351
|
+
}
|
|
352
|
+
let composerRows;
|
|
353
|
+
let valueStmt;
|
|
354
|
+
try {
|
|
355
|
+
composerRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
|
|
356
|
+
valueStmt = db.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
throw new CursorStateVscdbError('query', 'unable to query Cursor conversations', error);
|
|
360
|
+
}
|
|
361
|
+
const parseJsonValue = (value, label) => {
|
|
362
|
+
const text = typeof value === 'string'
|
|
363
|
+
? value
|
|
364
|
+
: value instanceof Uint8Array
|
|
365
|
+
? new TextDecoder().decode(value)
|
|
366
|
+
: undefined;
|
|
367
|
+
if (text === undefined) {
|
|
368
|
+
throw new CursorStateVscdbError('schema', `${label} value must be JSON text or a UTF-8 blob`);
|
|
369
|
+
}
|
|
170
370
|
try {
|
|
171
|
-
|
|
172
|
-
if (!row || typeof row.value !== 'string')
|
|
173
|
-
return undefined;
|
|
174
|
-
return JSON.parse(row.value);
|
|
371
|
+
return JSON.parse(text);
|
|
175
372
|
}
|
|
176
|
-
catch {
|
|
177
|
-
|
|
373
|
+
catch (error) {
|
|
374
|
+
throw new CursorStateVscdbError('schema', `${label} contains invalid JSON`, error);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const readValue = (key, label) => {
|
|
378
|
+
let row;
|
|
379
|
+
try {
|
|
380
|
+
row = valueStmt.get(key);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
throw new CursorStateVscdbError('query', `unable to query a Cursor ${label}`, error);
|
|
178
384
|
}
|
|
385
|
+
if (!row)
|
|
386
|
+
return undefined;
|
|
387
|
+
return parseJsonValue(row.value, label);
|
|
179
388
|
};
|
|
389
|
+
const readBubble = (composerId, bubbleId) => readValue(`bubbleId:${composerId}:${bubbleId}`, 'conversation bubble');
|
|
390
|
+
const readCodeBlockDiff = (composerId, diffId) => readValue(`codeBlockDiff:${composerId}:${diffId}`, 'code block diff');
|
|
180
391
|
const sessions = [];
|
|
392
|
+
let firstComposerError;
|
|
181
393
|
for (const row of composerRows) {
|
|
182
|
-
if (typeof row.value !== 'string')
|
|
183
|
-
continue;
|
|
184
|
-
let composer;
|
|
185
394
|
try {
|
|
186
|
-
composer =
|
|
395
|
+
const composer = parseJsonValue(row.value, 'composer');
|
|
396
|
+
if (!isRecord(composer))
|
|
397
|
+
throw new CursorStateVscdbError('schema', 'composer must be a JSON object');
|
|
398
|
+
const composerId = asString(row.key).replace(/^composerData:/, '') || asString(composer['composerId']);
|
|
399
|
+
if (!composerId)
|
|
400
|
+
throw new CursorStateVscdbError('schema', 'composer id is missing');
|
|
401
|
+
const session = composerToSession(composer, composerId, readBubble, readCodeBlockDiff);
|
|
402
|
+
if (session.turns.some((turn) => turn.isMeta !== true))
|
|
403
|
+
sessions.push(session);
|
|
187
404
|
}
|
|
188
|
-
catch {
|
|
189
|
-
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (!(error instanceof CursorStateVscdbError) || error.stage !== 'schema')
|
|
407
|
+
throw error;
|
|
408
|
+
firstComposerError ??= error;
|
|
190
409
|
}
|
|
191
|
-
if (!isRecord(composer))
|
|
192
|
-
continue;
|
|
193
|
-
const composerId = asString(row.key).replace(/^composerData:/, '') || asString(composer['composerId']);
|
|
194
|
-
const session = composerToSession(composer, composerId, readBubble);
|
|
195
|
-
if (session.turns.some((turn) => turn.isMeta !== true))
|
|
196
|
-
sessions.push(session);
|
|
197
410
|
}
|
|
411
|
+
// Returning only the healthy composers would silently produce an incomplete archive.
|
|
412
|
+
if (firstComposerError)
|
|
413
|
+
throw firstComposerError;
|
|
198
414
|
return sessions;
|
|
199
415
|
}
|
|
200
|
-
catch {
|
|
201
|
-
return [];
|
|
202
|
-
}
|
|
203
416
|
finally {
|
|
204
417
|
try {
|
|
205
418
|
db?.close();
|
package/dist/types.d.ts
CHANGED
|
@@ -67,11 +67,17 @@ export interface NormalizedSession {
|
|
|
67
67
|
sourceRecord?: unknown;
|
|
68
68
|
rawRows?: unknown[];
|
|
69
69
|
}
|
|
70
|
+
export interface NativeResumeIdentity {
|
|
71
|
+
harness: 'claude-code' | 'cursor';
|
|
72
|
+
sessionId: string;
|
|
73
|
+
}
|
|
70
74
|
export interface SessionLogAdapter {
|
|
71
75
|
readonly agent: string;
|
|
72
76
|
detect(path: string): boolean;
|
|
73
77
|
/** Derive a stable runtime session id when the transcript itself does not carry one. */
|
|
74
78
|
sessionIdFromPath?(path: string): string | undefined;
|
|
79
|
+
/** Resolve a harness-bound native session identity from a verified transcript source. */
|
|
80
|
+
resumeIdentityFromSource?(path: string, rawChunk: string): NativeResumeIdentity | undefined;
|
|
75
81
|
parse(rawChunk: string): NormalizedTurn[];
|
|
76
82
|
parseSession?(rawChunk: string): NormalizedSession;
|
|
77
83
|
parseSessions?(rawChunk: string): NormalizedSession[];
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-runtime-adapters",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.13.0 || >=23.4.0"
|
|
8
|
+
},
|
|
6
9
|
"description": "五 runtime 会话日志适配器 (CC/codex/cursor/kiro/opencode)",
|
|
7
10
|
"main": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
},
|
|
19
22
|
"publishConfig": {
|
|
20
23
|
"access": "public",
|
|
21
|
-
"tag": "
|
|
24
|
+
"tag": "latest"
|
|
22
25
|
},
|
|
23
26
|
"files": [
|
|
24
27
|
"dist/",
|