@nothumanwork/nn 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/LICENSE +21 -0
- package/README.md +77 -0
- package/bin/nn.js +106 -0
- package/package.json +74 -0
- package/src/config/env.ts +31 -0
- package/src/config/paths.ts +50 -0
- package/src/config/runtime.ts +37 -0
- package/src/config/sync.ts +48 -0
- package/src/db/client.ts +333 -0
- package/src/db/libsql-native.ts +66 -0
- package/src/db/lock.ts +72 -0
- package/src/db/migrate.ts +246 -0
- package/src/db/replica-migrate.ts +162 -0
- package/src/db/schema.sql +99 -0
- package/src/export/claude.ts +92 -0
- package/src/export/codex.ts +86 -0
- package/src/export/cursor.ts +68 -0
- package/src/export/generic.ts +19 -0
- package/src/export/registry.ts +118 -0
- package/src/export/types.ts +44 -0
- package/src/hooks/ingest.ts +107 -0
- package/src/hooks/resolvers/antigravity.ts +44 -0
- package/src/hooks/resolvers/claude.ts +27 -0
- package/src/hooks/resolvers/codex.ts +65 -0
- package/src/hooks/resolvers/common.ts +21 -0
- package/src/hooks/resolvers/cursor.ts +31 -0
- package/src/hooks/resolvers/grok.ts +59 -0
- package/src/hooks/resolvers/index.ts +35 -0
- package/src/hooks/resolvers/pi.ts +72 -0
- package/src/hooks/types.ts +20 -0
- package/src/index.ts +247 -0
- package/src/ingest/jsonl.ts +38 -0
- package/src/ingest/pipeline.ts +101 -0
- package/src/install/index.ts +227 -0
- package/src/install/types.ts +85 -0
- package/src/ir/event-id.ts +26 -0
- package/src/ir/types.ts +84 -0
- package/src/providers/antigravity/index.ts +175 -0
- package/src/providers/claude/index.ts +228 -0
- package/src/providers/codex/index.ts +264 -0
- package/src/providers/copilot/index.ts +24 -0
- package/src/providers/cursor/index.ts +340 -0
- package/src/providers/grok/index.ts +146 -0
- package/src/providers/pi/index.ts +197 -0
- package/src/providers/registry.ts +31 -0
- package/src/providers/types.ts +53 -0
- package/src/sync/coordinator.ts +186 -0
- package/src/sync/turso.ts +64 -0
- package/src/types/assets.d.ts +4 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { homeDir, projectDir } from "../../config/paths.ts";
|
|
5
|
+
import { lineKeyFor, parseCompleteJsonlLines } from "../../ingest/jsonl.ts";
|
|
6
|
+
import type {
|
|
7
|
+
NormalizedEvent,
|
|
8
|
+
SourceImportBatch,
|
|
9
|
+
SourceSessionRef,
|
|
10
|
+
} from "../../ir/types.ts";
|
|
11
|
+
import { defaultRole } from "../../ir/types.ts";
|
|
12
|
+
import type { ProviderAdapter } from "../types.ts";
|
|
13
|
+
import { stringField, textFromUnknown, truncate } from "../types.ts";
|
|
14
|
+
|
|
15
|
+
export const PARSER_VERSION = "codex-rollout-v1";
|
|
16
|
+
|
|
17
|
+
function codexRoot(): string {
|
|
18
|
+
return join(homeDir(), ".codex", "sessions");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function discoverCodexSessions(): SourceSessionRef[] {
|
|
22
|
+
if (!existsSync(codexRoot())) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const sessions: SourceSessionRef[] = [];
|
|
26
|
+
const walk = (dir: string): void => {
|
|
27
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
const path = join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
walk(path);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
entry.isFile() &&
|
|
35
|
+
entry.name.startsWith("rollout-") &&
|
|
36
|
+
entry.name.endsWith(".jsonl")
|
|
37
|
+
) {
|
|
38
|
+
sessions.push({
|
|
39
|
+
source: "codex",
|
|
40
|
+
sessionId: basename(entry.name, ".jsonl").replace(/^rollout-/, ""),
|
|
41
|
+
path,
|
|
42
|
+
workspaceRoot: projectDir(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(codexRoot());
|
|
48
|
+
return sessions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseCodexEnvelope(
|
|
52
|
+
value: Record<string, unknown>,
|
|
53
|
+
sessionId: string,
|
|
54
|
+
timestamp: string,
|
|
55
|
+
lineKey: string,
|
|
56
|
+
rawJson: string,
|
|
57
|
+
rawLocalRef: string,
|
|
58
|
+
rowEventId: string,
|
|
59
|
+
): NormalizedEvent[] {
|
|
60
|
+
const type = stringField(value, "type") ?? "unknown";
|
|
61
|
+
const payload = (value.payload ?? value) as Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
if (type === "session_meta") {
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
source: "codex",
|
|
67
|
+
sourceSessionId: stringField(payload, "id") ?? sessionId,
|
|
68
|
+
sourceEventId: rowEventId,
|
|
69
|
+
lineKey,
|
|
70
|
+
boundary: "session_start",
|
|
71
|
+
role: "system",
|
|
72
|
+
timestamp,
|
|
73
|
+
contentText: "Codex session started",
|
|
74
|
+
parserVersion: PARSER_VERSION,
|
|
75
|
+
rawLocalRef,
|
|
76
|
+
rawJson,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (type === "event_msg" && stringField(payload, "type") === "user_message") {
|
|
82
|
+
const text =
|
|
83
|
+
stringField(payload, "message") ?? textFromUnknown(payload) ?? "";
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
source: "codex",
|
|
87
|
+
sourceSessionId: sessionId,
|
|
88
|
+
sourceEventId: rowEventId,
|
|
89
|
+
lineKey,
|
|
90
|
+
boundary: "user_turn",
|
|
91
|
+
role: "user",
|
|
92
|
+
timestamp,
|
|
93
|
+
contentText: text,
|
|
94
|
+
parserVersion: PARSER_VERSION,
|
|
95
|
+
rawLocalRef,
|
|
96
|
+
rawJson,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (type === "response_item") {
|
|
102
|
+
const itemType = stringField(payload, "type") ?? "";
|
|
103
|
+
if (itemType === "function_call") {
|
|
104
|
+
const name = stringField(payload, "name") ?? "tool";
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
source: "codex",
|
|
108
|
+
sourceSessionId: sessionId,
|
|
109
|
+
sourceEventId: rowEventId,
|
|
110
|
+
lineKey,
|
|
111
|
+
boundary: "tool_call",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
timestamp,
|
|
114
|
+
contentText: truncate(
|
|
115
|
+
`${name} ${stringField(payload, "arguments") ?? "{}"}`,
|
|
116
|
+
),
|
|
117
|
+
toolName: name,
|
|
118
|
+
toolCallId: stringField(payload, "call_id"),
|
|
119
|
+
toolInputJson: stringField(payload, "arguments") ?? "{}",
|
|
120
|
+
parserVersion: PARSER_VERSION,
|
|
121
|
+
rawLocalRef,
|
|
122
|
+
rawJson,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (itemType === "function_call_output") {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
source: "codex",
|
|
130
|
+
sourceSessionId: sessionId,
|
|
131
|
+
sourceEventId: rowEventId,
|
|
132
|
+
lineKey,
|
|
133
|
+
boundary: "tool_result",
|
|
134
|
+
role: "tool",
|
|
135
|
+
timestamp,
|
|
136
|
+
contentText: truncate(
|
|
137
|
+
textFromUnknown(payload.output ?? payload) ?? "",
|
|
138
|
+
),
|
|
139
|
+
toolCallId: stringField(payload, "call_id"),
|
|
140
|
+
toolOutputJson: JSON.stringify(payload.output ?? payload),
|
|
141
|
+
parserVersion: PARSER_VERSION,
|
|
142
|
+
rawLocalRef,
|
|
143
|
+
rawJson,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
if (stringField(payload, "role") === "assistant") {
|
|
148
|
+
const text = textFromUnknown(payload.content) ?? "";
|
|
149
|
+
if (!text.trim()) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
source: "codex",
|
|
155
|
+
sourceSessionId: sessionId,
|
|
156
|
+
sourceEventId: rowEventId,
|
|
157
|
+
lineKey,
|
|
158
|
+
boundary: "assistant_turn",
|
|
159
|
+
role: "assistant",
|
|
160
|
+
timestamp,
|
|
161
|
+
contentText: text,
|
|
162
|
+
parserVersion: PARSER_VERSION,
|
|
163
|
+
rawLocalRef,
|
|
164
|
+
rawJson,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return [
|
|
171
|
+
{
|
|
172
|
+
source: "codex",
|
|
173
|
+
sourceSessionId: sessionId,
|
|
174
|
+
sourceEventId: rowEventId,
|
|
175
|
+
lineKey,
|
|
176
|
+
boundary: "unknown_raw_event",
|
|
177
|
+
role: defaultRole("unknown_raw_event"),
|
|
178
|
+
timestamp,
|
|
179
|
+
contentText: truncate(JSON.stringify(value)),
|
|
180
|
+
parserVersion: PARSER_VERSION,
|
|
181
|
+
rawLocalRef,
|
|
182
|
+
rawJson,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseCodexFile(
|
|
188
|
+
sourcePath: string,
|
|
189
|
+
sessionId: string,
|
|
190
|
+
offset: number,
|
|
191
|
+
): SourceImportBatch {
|
|
192
|
+
const buffer = readFileSync(sourcePath);
|
|
193
|
+
const slice = buffer.subarray(offset);
|
|
194
|
+
const { lines, consumedBytes, partialTail } = parseCompleteJsonlLines(
|
|
195
|
+
slice,
|
|
196
|
+
0,
|
|
197
|
+
);
|
|
198
|
+
const events: NormalizedEvent[] = [];
|
|
199
|
+
const warnings: string[] = [];
|
|
200
|
+
let currentSessionId = sessionId;
|
|
201
|
+
let emittedLines = 0;
|
|
202
|
+
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
emittedLines = line.lineIndex;
|
|
205
|
+
const absoluteStart = offset + line.lineStart;
|
|
206
|
+
const lineKey = lineKeyFor(sourcePath, absoluteStart);
|
|
207
|
+
let value: Record<string, unknown>;
|
|
208
|
+
try {
|
|
209
|
+
value = JSON.parse(line.raw) as Record<string, unknown>;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
warnings.push(
|
|
212
|
+
`${sourcePath}:${line.lineIndex} malformed row: ${String(error)}`,
|
|
213
|
+
);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const timestamp =
|
|
218
|
+
stringField(value, "timestamp") ?? new Date().toISOString();
|
|
219
|
+
const rowEventId = `line:${line.lineIndex}`;
|
|
220
|
+
const parsed = parseCodexEnvelope(
|
|
221
|
+
value,
|
|
222
|
+
currentSessionId,
|
|
223
|
+
timestamp,
|
|
224
|
+
lineKey,
|
|
225
|
+
line.raw,
|
|
226
|
+
`${sourcePath}:${line.lineIndex}`,
|
|
227
|
+
rowEventId,
|
|
228
|
+
);
|
|
229
|
+
for (const event of parsed) {
|
|
230
|
+
currentSessionId = event.sourceSessionId;
|
|
231
|
+
events.push(event);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const absoluteConsumed =
|
|
236
|
+
partialTail ?
|
|
237
|
+
offset + (lines.at(-1)?.lineEnd ?? 0)
|
|
238
|
+
: offset + consumedBytes;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
source: "codex",
|
|
242
|
+
parserVersion: PARSER_VERSION,
|
|
243
|
+
session: {
|
|
244
|
+
source: "codex",
|
|
245
|
+
sessionId: currentSessionId,
|
|
246
|
+
path: sourcePath,
|
|
247
|
+
workspaceRoot: projectDir(),
|
|
248
|
+
},
|
|
249
|
+
events,
|
|
250
|
+
cursorKey: sourcePath,
|
|
251
|
+
consumedBytes: absoluteConsumed,
|
|
252
|
+
emittedLines,
|
|
253
|
+
warnings,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const codexProvider: ProviderAdapter = {
|
|
258
|
+
name: "codex",
|
|
259
|
+
parserVersion: PARSER_VERSION,
|
|
260
|
+
discover: discoverCodexSessions,
|
|
261
|
+
parseIncremental: parseCodexFile,
|
|
262
|
+
parseFull: (sourcePath, sessionId) =>
|
|
263
|
+
parseCodexFile(sourcePath, sessionId, 0),
|
|
264
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SourceImportBatch, SourceSessionRef } from "../../ir/types.ts";
|
|
2
|
+
import type { ProviderAdapter } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export const PARSER_VERSION = "copilot-unimplemented-v0";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GitHub Copilot Chat stores history in IDE extension globalStorage (often SQLite/state.vscdb),
|
|
8
|
+
* not plain JSONL. No stable cross-harness export path was found on this machine.
|
|
9
|
+
*
|
|
10
|
+
* See docs/providers/copilot.md for the spike outcome and future integration options.
|
|
11
|
+
*/
|
|
12
|
+
function notImplemented(): never {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"GitHub Copilot transcript ingest is not implemented yet. Copilot stores chat history in IDE globalStorage, not JSONL. See docs/providers/copilot.md.",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const copilotProvider: ProviderAdapter = {
|
|
19
|
+
name: "copilot",
|
|
20
|
+
parserVersion: PARSER_VERSION,
|
|
21
|
+
discover: (): SourceSessionRef[] => [],
|
|
22
|
+
parseIncremental: (): SourceImportBatch => notImplemented(),
|
|
23
|
+
parseFull: (): SourceImportBatch => notImplemented(),
|
|
24
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { homeDir, encodeProjectSlug, projectDir } from "../../config/paths.ts";
|
|
5
|
+
import { lineKeyFor, parseCompleteJsonlLines } from "../../ingest/jsonl.ts";
|
|
6
|
+
import type {
|
|
7
|
+
EventBoundary,
|
|
8
|
+
NormalizedEvent,
|
|
9
|
+
SourceImportBatch,
|
|
10
|
+
SourceSessionRef,
|
|
11
|
+
} from "../../ir/types.ts";
|
|
12
|
+
import { defaultRole } from "../../ir/types.ts";
|
|
13
|
+
import type { ProviderAdapter } from "../types.ts";
|
|
14
|
+
import { stringField, textFromUnknown, truncate } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
export const PARSER_VERSION = "cursor-agent-jsonl-v1";
|
|
17
|
+
|
|
18
|
+
function cursorRoot(): string {
|
|
19
|
+
return join(homeDir(), ".cursor");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decodeCursorProject(alias: string): string | null {
|
|
23
|
+
if (!alias) {
|
|
24
|
+
return "/";
|
|
25
|
+
}
|
|
26
|
+
return `/${alias.replace(/-/g, "/")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sessionMatchesProject(
|
|
30
|
+
sessionPath: string,
|
|
31
|
+
workspaceRoot?: string,
|
|
32
|
+
): boolean {
|
|
33
|
+
const project = projectDir();
|
|
34
|
+
const slug = encodeProjectSlug(project);
|
|
35
|
+
if (workspaceRoot && workspaceRoot === project) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (sessionPath.includes(`/projects/${slug}/`)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function discoverCursorSessions(): SourceSessionRef[] {
|
|
45
|
+
const projectsDir = join(cursorRoot(), "projects");
|
|
46
|
+
if (!existsSync(projectsDir)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sessions: SourceSessionRef[] = [];
|
|
51
|
+
for (const projectEntry of readdirSync(projectsDir, {
|
|
52
|
+
withFileTypes: true,
|
|
53
|
+
})) {
|
|
54
|
+
if (!projectEntry.isDirectory()) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const transcriptsDir = join(
|
|
58
|
+
projectsDir,
|
|
59
|
+
projectEntry.name,
|
|
60
|
+
"agent-transcripts",
|
|
61
|
+
);
|
|
62
|
+
if (!existsSync(transcriptsDir)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (const sessionEntry of readdirSync(transcriptsDir, {
|
|
66
|
+
withFileTypes: true,
|
|
67
|
+
})) {
|
|
68
|
+
if (!sessionEntry.isDirectory()) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const jsonlPath = join(
|
|
72
|
+
transcriptsDir,
|
|
73
|
+
sessionEntry.name,
|
|
74
|
+
`${sessionEntry.name}.jsonl`,
|
|
75
|
+
);
|
|
76
|
+
if (!existsSync(jsonlPath)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const workspaceRoot = decodeCursorProject(projectEntry.name) ?? undefined;
|
|
80
|
+
if (!sessionMatchesProject(jsonlPath, workspaceRoot)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
sessions.push({
|
|
84
|
+
source: "cursor",
|
|
85
|
+
sessionId: sessionEntry.name,
|
|
86
|
+
path: jsonlPath,
|
|
87
|
+
workspaceRoot,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return sessions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface CursorPart {
|
|
95
|
+
boundary: EventBoundary;
|
|
96
|
+
role: string;
|
|
97
|
+
contentText: string;
|
|
98
|
+
toolName?: string;
|
|
99
|
+
toolCallId?: string;
|
|
100
|
+
toolInputJson?: string;
|
|
101
|
+
toolOutputJson?: string;
|
|
102
|
+
suffix?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cursorRoleBoundary(role: string): EventBoundary {
|
|
106
|
+
if (role === "user") {
|
|
107
|
+
return "user_turn";
|
|
108
|
+
}
|
|
109
|
+
if (role === "assistant") {
|
|
110
|
+
return "assistant_turn";
|
|
111
|
+
}
|
|
112
|
+
return "unknown_raw_event";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cursorContentPart(
|
|
116
|
+
item: Record<string, unknown>,
|
|
117
|
+
rowRole: string,
|
|
118
|
+
index: number,
|
|
119
|
+
): CursorPart {
|
|
120
|
+
const blockType = stringField(item, "type") ?? "text";
|
|
121
|
+
const suffix = `block:${index}`;
|
|
122
|
+
if (
|
|
123
|
+
blockType === "tool_use" ||
|
|
124
|
+
blockType === "tool_call" ||
|
|
125
|
+
blockType === "function_call"
|
|
126
|
+
) {
|
|
127
|
+
const name = stringField(item, "name") ?? "tool";
|
|
128
|
+
const input = item.input ?? item.arguments ?? item;
|
|
129
|
+
return {
|
|
130
|
+
boundary: "tool_call",
|
|
131
|
+
role: "assistant",
|
|
132
|
+
contentText: truncate(`${name} ${JSON.stringify(input)}`),
|
|
133
|
+
toolName: name,
|
|
134
|
+
toolCallId: stringField(item, "id"),
|
|
135
|
+
toolInputJson: JSON.stringify(input),
|
|
136
|
+
suffix,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (blockType === "tool_result") {
|
|
140
|
+
const output =
|
|
141
|
+
textFromUnknown(item.content ?? item.output ?? item) ??
|
|
142
|
+
JSON.stringify(item);
|
|
143
|
+
return {
|
|
144
|
+
boundary: "tool_result",
|
|
145
|
+
role: "tool",
|
|
146
|
+
contentText: truncate(output),
|
|
147
|
+
toolCallId:
|
|
148
|
+
stringField(item, "tool_use_id") ?? stringField(item, "call_id"),
|
|
149
|
+
toolOutputJson: JSON.stringify(item),
|
|
150
|
+
suffix,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const text = textFromUnknown(item) ?? "";
|
|
154
|
+
return {
|
|
155
|
+
boundary: cursorRoleBoundary(rowRole),
|
|
156
|
+
role: rowRole,
|
|
157
|
+
contentText: text,
|
|
158
|
+
suffix,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function cursorEventParts(value: Record<string, unknown>): CursorPart[] {
|
|
163
|
+
const rowType = stringField(value, "type");
|
|
164
|
+
if (rowType === "session_start") {
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
boundary: "session_start",
|
|
168
|
+
role: "system",
|
|
169
|
+
contentText: "Cursor session started",
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
if (rowType === "session_end") {
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
boundary: "session_end",
|
|
177
|
+
role: "system",
|
|
178
|
+
contentText: "Cursor session ended",
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
if (rowType === "compaction" || rowType === "summary") {
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
boundary: "compaction",
|
|
186
|
+
role: "system",
|
|
187
|
+
contentText:
|
|
188
|
+
textFromUnknown(value.content ?? value.message) ??
|
|
189
|
+
`Cursor ${rowType}`,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const role =
|
|
195
|
+
stringField(value, "role") ?? stringField(value, "type") ?? "unknown";
|
|
196
|
+
const message = value.message;
|
|
197
|
+
const content =
|
|
198
|
+
typeof message === "object" && message !== null ?
|
|
199
|
+
(message as Record<string, unknown>).content
|
|
200
|
+
: value.content;
|
|
201
|
+
|
|
202
|
+
if (Array.isArray(content)) {
|
|
203
|
+
return content.map((item, index) =>
|
|
204
|
+
cursorContentPart(item as Record<string, unknown>, role, index),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const text = textFromUnknown(content);
|
|
209
|
+
if (text) {
|
|
210
|
+
return [{ boundary: cursorRoleBoundary(role), role, contentText: text }];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
boundary: "unknown_raw_event",
|
|
216
|
+
role: "system",
|
|
217
|
+
contentText: truncate(JSON.stringify(value)),
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseCursorBuffer(
|
|
223
|
+
sourcePath: string,
|
|
224
|
+
sessionId: string,
|
|
225
|
+
buffer: Buffer,
|
|
226
|
+
startOffset: number,
|
|
227
|
+
): SourceImportBatch {
|
|
228
|
+
const { lines, consumedBytes, partialTail } = parseCompleteJsonlLines(
|
|
229
|
+
buffer,
|
|
230
|
+
0,
|
|
231
|
+
);
|
|
232
|
+
const events: NormalizedEvent[] = [];
|
|
233
|
+
const warnings: string[] = [];
|
|
234
|
+
let currentSessionId = sessionId;
|
|
235
|
+
let emittedLines = 0;
|
|
236
|
+
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
emittedLines = line.lineIndex;
|
|
239
|
+
const absoluteStart = startOffset + line.lineStart;
|
|
240
|
+
const lineKey = lineKeyFor(sourcePath, absoluteStart);
|
|
241
|
+
let value: Record<string, unknown>;
|
|
242
|
+
try {
|
|
243
|
+
value = JSON.parse(line.raw) as Record<string, unknown>;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
warnings.push(
|
|
246
|
+
`${sourcePath}:${line.lineIndex} malformed row: ${String(error)}`,
|
|
247
|
+
);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const discoveredSession =
|
|
252
|
+
stringField(value, "sessionId") ?? stringField(value, "session_id");
|
|
253
|
+
if (discoveredSession) {
|
|
254
|
+
currentSessionId = discoveredSession;
|
|
255
|
+
}
|
|
256
|
+
const timestamp =
|
|
257
|
+
stringField(value, "timestamp") ?? new Date().toISOString();
|
|
258
|
+
const rowEventId =
|
|
259
|
+
stringField(value, "uuid") ??
|
|
260
|
+
stringField(value, "id") ??
|
|
261
|
+
`line:${line.lineIndex}`;
|
|
262
|
+
|
|
263
|
+
for (const part of cursorEventParts(value)) {
|
|
264
|
+
if (!part.contentText.trim()) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
events.push({
|
|
268
|
+
source: "cursor",
|
|
269
|
+
sourceSessionId: currentSessionId,
|
|
270
|
+
sourceEventId:
|
|
271
|
+
part.suffix ? `${rowEventId}:${part.suffix}` : rowEventId,
|
|
272
|
+
lineKey: part.suffix ? `${lineKey}:${part.suffix}` : lineKey,
|
|
273
|
+
boundary: part.boundary,
|
|
274
|
+
role: part.role || defaultRole(part.boundary),
|
|
275
|
+
timestamp,
|
|
276
|
+
contentText: part.contentText,
|
|
277
|
+
toolName: part.toolName,
|
|
278
|
+
toolCallId: part.toolCallId,
|
|
279
|
+
toolInputJson: part.toolInputJson,
|
|
280
|
+
toolOutputJson: part.toolOutputJson,
|
|
281
|
+
parserVersion: PARSER_VERSION,
|
|
282
|
+
rawLocalRef: `${sourcePath}:${line.lineIndex}`,
|
|
283
|
+
rawJson: line.raw,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const absoluteConsumed =
|
|
289
|
+
partialTail ?
|
|
290
|
+
startOffset + (lines.at(-1)?.lineEnd ?? 0)
|
|
291
|
+
: startOffset + consumedBytes;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
source: "cursor",
|
|
295
|
+
parserVersion: PARSER_VERSION,
|
|
296
|
+
session: {
|
|
297
|
+
source: "cursor",
|
|
298
|
+
sessionId: currentSessionId,
|
|
299
|
+
path: sourcePath,
|
|
300
|
+
workspaceRoot: projectDir(),
|
|
301
|
+
},
|
|
302
|
+
events,
|
|
303
|
+
cursorKey: sourcePath,
|
|
304
|
+
consumedBytes: absoluteConsumed,
|
|
305
|
+
emittedLines,
|
|
306
|
+
warnings,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseCursorFile(
|
|
311
|
+
sourcePath: string,
|
|
312
|
+
sessionId: string,
|
|
313
|
+
offset: number,
|
|
314
|
+
): SourceImportBatch {
|
|
315
|
+
const buffer = readFileSync(sourcePath);
|
|
316
|
+
return parseCursorBuffer(
|
|
317
|
+
sourcePath,
|
|
318
|
+
sessionId,
|
|
319
|
+
buffer.subarray(offset),
|
|
320
|
+
offset,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const cursorProvider: ProviderAdapter = {
|
|
325
|
+
name: "cursor",
|
|
326
|
+
parserVersion: PARSER_VERSION,
|
|
327
|
+
discover: discoverCursorSessions,
|
|
328
|
+
parseIncremental: parseCursorFile,
|
|
329
|
+
parseFull: (sourcePath, sessionId) =>
|
|
330
|
+
parseCursorFile(sourcePath, sessionId, 0),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export function cursorSessionIdFromPath(sourcePath: string): string {
|
|
334
|
+
const match = sourcePath.match(/agent-transcripts\/([^/]+)\//);
|
|
335
|
+
return match?.[1] ?? basename(sourcePath, ".jsonl");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function isTranscriptFile(path: string): boolean {
|
|
339
|
+
return existsSync(path) && statSync(path).isFile() && path.endsWith(".jsonl");
|
|
340
|
+
}
|