@nocoo/pika 0.2.1

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.
Files changed (2) hide show
  1. package/dist/bin.js +3429 -0
  2. package/package.json +32 -0
package/dist/bin.js ADDED
@@ -0,0 +1,3429 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+ var __require = import.meta.require;
15
+
16
+ // ../core/src/types.ts
17
+ var SOURCES;
18
+ var init_types = __esm(() => {
19
+ SOURCES = [
20
+ "claude-code",
21
+ "codex",
22
+ "gemini-cli",
23
+ "opencode",
24
+ "vscode-copilot"
25
+ ];
26
+ });
27
+
28
+ // ../../package.json
29
+ var package_default;
30
+ var init_package = __esm(() => {
31
+ package_default = {
32
+ name: "pika",
33
+ version: "0.2.1",
34
+ private: true,
35
+ workspaces: [
36
+ "packages/*"
37
+ ],
38
+ scripts: {
39
+ dev: "bun run --cwd packages/web dev",
40
+ build: "bun run --filter '*' build",
41
+ test: "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:coverage": "vitest run --coverage",
44
+ lint: "tsc --noEmit && tsc --noEmit -p packages/web/tsconfig.json",
45
+ "lint:typecheck": "tsc --noEmit && tsc --noEmit -p packages/web/tsconfig.json",
46
+ "sync-versions": "bun scripts/sync-versions.ts --write",
47
+ prepare: "husky"
48
+ },
49
+ devDependencies: {
50
+ "@vitest/coverage-v8": "^3.1.1",
51
+ husky: "^9.1.7",
52
+ typescript: "^5.8.2",
53
+ vitest: "^3.1.1"
54
+ },
55
+ overrides: {
56
+ undici: ">=7.24.0",
57
+ cookie: ">=0.7.0"
58
+ }
59
+ };
60
+ });
61
+
62
+ // ../core/src/version.ts
63
+ var PIKA_VERSION;
64
+ var init_version = __esm(() => {
65
+ init_package();
66
+ PIKA_VERSION = package_default.version;
67
+ });
68
+
69
+ // ../core/src/constants.ts
70
+ var PARSER_REVISION = 2, SCHEMA_VERSION = 1, METADATA_BATCH_SIZE = 50, LOGIN_TIMEOUT_MS = 120000, CONFIG_DIR = "pika", CONFIG_FILE = "config.json", DEV_CONFIG_FILE = "config.dev.json", PARSE_ERRORS_FILE = "parse-errors.jsonl", MAX_UPLOAD_RETRIES = 2, INITIAL_BACKOFF_MS = 1000, CONTENT_UPLOAD_CONCURRENCY = 2, MAX_CONTENT_UPLOAD_BYTES, MAX_METADATA_BODY_BYTES, MAX_DECOMPRESSED_CONTENT_BYTES;
71
+ var init_constants = __esm(() => {
72
+ MAX_CONTENT_UPLOAD_BYTES = 50 * 1024 * 1024;
73
+ MAX_METADATA_BODY_BYTES = 2 * 1024 * 1024;
74
+ MAX_DECOMPRESSED_CONTENT_BYTES = 256 * 1024 * 1024;
75
+ });
76
+
77
+ // ../core/src/validation.ts
78
+ var init_validation = __esm(() => {
79
+ init_types();
80
+ init_constants();
81
+ });
82
+
83
+ // ../core/src/chunking.ts
84
+ var init_chunking = __esm(() => {
85
+ init_constants();
86
+ });
87
+
88
+ // ../core/src/title.ts
89
+ function getFirstUserMessage(messages) {
90
+ for (const msg of messages) {
91
+ if (msg.role === "user" && msg.content.trim().length > 0) {
92
+ return msg.content.trim();
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ function generateTitle(projectName, firstUserMessage) {
98
+ if (!firstUserMessage)
99
+ return null;
100
+ const normalized = firstUserMessage.replace(/\s+/g, " ").trim();
101
+ if (normalized.length === 0)
102
+ return null;
103
+ let prefix = "";
104
+ if (projectName) {
105
+ const segments = projectName.split("/").filter(Boolean);
106
+ const lastSegment = segments[segments.length - 1];
107
+ if (lastSegment) {
108
+ prefix = `[${lastSegment}] `;
109
+ }
110
+ }
111
+ const budget = MAX_TITLE_LENGTH - prefix.length;
112
+ if (budget <= 0) {
113
+ return truncateAtWord(normalized, MAX_TITLE_LENGTH);
114
+ }
115
+ const truncated = truncateAtWord(normalized, budget);
116
+ return `${prefix}${truncated}`;
117
+ }
118
+ function truncateAtWord(text, maxLen) {
119
+ if (text.length <= maxLen)
120
+ return text;
121
+ const cutoff = maxLen - 1;
122
+ const lastSpace = text.lastIndexOf(" ", cutoff);
123
+ if (lastSpace > 0) {
124
+ return text.slice(0, lastSpace) + "\u2026";
125
+ }
126
+ return text.slice(0, cutoff) + "\u2026";
127
+ }
128
+ var MAX_TITLE_LENGTH = 80;
129
+
130
+ // ../core/src/index.ts
131
+ var init_src = __esm(() => {
132
+ init_types();
133
+ init_version();
134
+ init_constants();
135
+ init_validation();
136
+ init_chunking();
137
+ });
138
+
139
+ // src/config/manager.ts
140
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
141
+ import { join } from "path";
142
+ import { randomUUID } from "crypto";
143
+
144
+ class ConfigManager {
145
+ configDir;
146
+ isDev;
147
+ constructor(configDir, isDev = false) {
148
+ this.configDir = configDir;
149
+ this.isDev = isDev;
150
+ }
151
+ get configPath() {
152
+ const fileName = this.isDev ? DEV_CONFIG_FILE : CONFIG_FILE;
153
+ return join(this.configDir, fileName);
154
+ }
155
+ read() {
156
+ try {
157
+ const content = readFileSync(this.configPath, "utf-8");
158
+ return JSON.parse(content);
159
+ } catch {
160
+ return {};
161
+ }
162
+ }
163
+ write(partial) {
164
+ if (!existsSync(this.configDir)) {
165
+ mkdirSync(this.configDir, { recursive: true });
166
+ }
167
+ const existing = this.read();
168
+ const merged = { ...existing, ...partial };
169
+ writeFileSync(this.configPath, JSON.stringify(merged, null, 2) + `
170
+ `, {
171
+ mode: 384
172
+ });
173
+ }
174
+ getToken() {
175
+ return this.read().token;
176
+ }
177
+ isLoggedIn() {
178
+ return !!this.getToken();
179
+ }
180
+ getApiUrl() {
181
+ return this.isDev ? DEV_API_URL : PROD_API_URL;
182
+ }
183
+ getDeviceId() {
184
+ const config = this.read();
185
+ if (config.deviceId)
186
+ return config.deviceId;
187
+ const deviceId = randomUUID();
188
+ this.write({ deviceId });
189
+ return deviceId;
190
+ }
191
+ }
192
+ var PROD_API_URL = "https://pika.hexly.ai", DEV_API_URL = "https://pika.dev.hexly.ai";
193
+ var init_manager = __esm(() => {
194
+ init_src();
195
+ });
196
+
197
+ // src/storage/cursor-store.ts
198
+ import { readFile, writeFile, mkdir } from "fs/promises";
199
+ import { join as join2, dirname } from "path";
200
+ function emptyState() {
201
+ return { version: 1, files: {}, updatedAt: null };
202
+ }
203
+
204
+ class CursorStore {
205
+ filePath;
206
+ constructor(storeDir) {
207
+ this.filePath = join2(storeDir, CURSORS_FILE2);
208
+ }
209
+ async load() {
210
+ try {
211
+ const raw = await readFile(this.filePath, "utf-8");
212
+ return JSON.parse(raw);
213
+ } catch {
214
+ return emptyState();
215
+ }
216
+ }
217
+ async save(state) {
218
+ const dir = dirname(this.filePath);
219
+ await mkdir(dir, { recursive: true });
220
+ await writeFile(this.filePath, JSON.stringify(state, null, 2) + `
221
+ `);
222
+ }
223
+ }
224
+ var CURSORS_FILE2 = "cursors.json";
225
+ var init_cursor_store = () => {};
226
+
227
+ // src/utils/file-changed.ts
228
+ function fileUnchanged(prev, curr) {
229
+ if (!prev)
230
+ return false;
231
+ if (prev.mtimeMs === undefined || prev.size === undefined)
232
+ return false;
233
+ return prev.inode === curr.inode && prev.mtimeMs === curr.mtimeMs && prev.size === curr.size;
234
+ }
235
+
236
+ // src/utils/hash-project-ref.ts
237
+ import { createHash } from "crypto";
238
+ function hashProjectRef(raw) {
239
+ if (!raw)
240
+ return null;
241
+ return createHash("sha256").update(raw).digest("hex").slice(0, PROJECT_REF_HASH_LENGTH);
242
+ }
243
+ var PROJECT_REF_HASH_LENGTH = 16;
244
+ var init_hash_project_ref = () => {};
245
+
246
+ // src/parsers/claude.ts
247
+ import { createReadStream } from "fs";
248
+ import { stat } from "fs/promises";
249
+ import { createInterface } from "readline";
250
+ function extractUserContent(content) {
251
+ if (typeof content === "string")
252
+ return content;
253
+ if (!Array.isArray(content))
254
+ return "";
255
+ const textParts = [];
256
+ for (const block of content) {
257
+ if (block.type === "text" && typeof block.text === "string") {
258
+ textParts.push(block.text);
259
+ }
260
+ }
261
+ return textParts.join(`
262
+ `);
263
+ }
264
+ function toNonNegInt(value) {
265
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
266
+ return 0;
267
+ return Math.floor(value);
268
+ }
269
+ function processAssistantContent(content, timestamp, model, usage) {
270
+ const messages = [];
271
+ const textParts = [];
272
+ for (const block of content) {
273
+ if (block.type === "text" && typeof block.text === "string") {
274
+ textParts.push(block.text);
275
+ } else if (block.type === "tool_use") {
276
+ if (textParts.length > 0) {
277
+ messages.push({
278
+ role: "assistant",
279
+ content: textParts.join(`
280
+ `),
281
+ model,
282
+ inputTokens: usage ? toNonNegInt(usage.input_tokens) : undefined,
283
+ outputTokens: usage ? toNonNegInt(usage.output_tokens) : undefined,
284
+ cachedTokens: usage ? toNonNegInt(usage.cache_read_input_tokens) : undefined,
285
+ timestamp
286
+ });
287
+ textParts.length = 0;
288
+ }
289
+ messages.push({
290
+ role: "tool",
291
+ content: "",
292
+ toolName: typeof block.name === "string" ? block.name : undefined,
293
+ toolInput: block.input != null ? JSON.stringify(block.input) : undefined,
294
+ timestamp
295
+ });
296
+ }
297
+ }
298
+ if (textParts.length > 0 || messages.length === 0) {
299
+ messages.push({
300
+ role: "assistant",
301
+ content: textParts.join(`
302
+ `),
303
+ model,
304
+ inputTokens: usage ? toNonNegInt(usage.input_tokens) : undefined,
305
+ outputTokens: usage ? toNonNegInt(usage.output_tokens) : undefined,
306
+ cachedTokens: usage ? toNonNegInt(usage.cache_read_input_tokens) : undefined,
307
+ timestamp
308
+ });
309
+ }
310
+ return messages;
311
+ }
312
+ function processToolResults(content, timestamp) {
313
+ const messages = [];
314
+ for (const block of content) {
315
+ if (block.type === "tool_result") {
316
+ const resultContent = typeof block.content === "string" ? block.content : block.content != null ? JSON.stringify(block.content) : "";
317
+ messages.push({
318
+ role: "tool",
319
+ content: resultContent,
320
+ toolResult: resultContent,
321
+ timestamp
322
+ });
323
+ }
324
+ }
325
+ return messages;
326
+ }
327
+ function processLine(line, sessions) {
328
+ let obj;
329
+ try {
330
+ obj = JSON.parse(line);
331
+ } catch {
332
+ return;
333
+ }
334
+ const sessionId = obj.sessionId;
335
+ if (!sessionId || typeof sessionId !== "string")
336
+ return;
337
+ const type = obj.type;
338
+ if (type !== "user" && type !== "assistant")
339
+ return;
340
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
341
+ if (!timestamp)
342
+ return;
343
+ const message = obj.message;
344
+ if (!message)
345
+ return;
346
+ let accum = sessions.get(sessionId);
347
+ if (!accum) {
348
+ accum = {
349
+ sessionId,
350
+ messages: [],
351
+ lines: [],
352
+ startedAt: null,
353
+ lastMessageAt: null,
354
+ lastModel: null,
355
+ totalInputTokens: 0,
356
+ totalOutputTokens: 0,
357
+ totalCachedTokens: 0
358
+ };
359
+ sessions.set(sessionId, accum);
360
+ }
361
+ accum.lines.push(line);
362
+ if (!accum.startedAt || timestamp < accum.startedAt) {
363
+ accum.startedAt = timestamp;
364
+ }
365
+ if (!accum.lastMessageAt || timestamp > accum.lastMessageAt) {
366
+ accum.lastMessageAt = timestamp;
367
+ }
368
+ if (type === "user") {
369
+ const content = message.content;
370
+ if (Array.isArray(content)) {
371
+ const hasToolResults = content.some((b) => b.type === "tool_result");
372
+ if (hasToolResults) {
373
+ accum.messages.push(...processToolResults(content, timestamp));
374
+ } else {
375
+ accum.messages.push({
376
+ role: "user",
377
+ content: extractUserContent(content),
378
+ timestamp
379
+ });
380
+ }
381
+ } else {
382
+ accum.messages.push({
383
+ role: "user",
384
+ content: extractUserContent(content),
385
+ timestamp
386
+ });
387
+ }
388
+ } else if (type === "assistant") {
389
+ const model = typeof message.model === "string" ? message.model.trim() : undefined;
390
+ if (model)
391
+ accum.lastModel = model;
392
+ const content = message.content;
393
+ if (Array.isArray(content)) {
394
+ const msgs = processAssistantContent(content, timestamp, model, message.usage);
395
+ accum.messages.push(...msgs);
396
+ }
397
+ if (message.usage) {
398
+ accum.totalInputTokens += toNonNegInt(message.usage.input_tokens);
399
+ accum.totalOutputTokens += toNonNegInt(message.usage.output_tokens);
400
+ accum.totalCachedTokens += toNonNegInt(message.usage.cache_read_input_tokens);
401
+ }
402
+ }
403
+ }
404
+ function buildParseResult(accum, filePath) {
405
+ const startedAt = accum.startedAt ?? new Date().toISOString();
406
+ const lastMessageAt = accum.lastMessageAt ?? startedAt;
407
+ const durationMs = new Date(lastMessageAt).getTime() - new Date(startedAt).getTime();
408
+ const canonical = {
409
+ sessionKey: `claude:${accum.sessionId}`,
410
+ source: "claude-code",
411
+ parserRevision: PARSER_REVISION,
412
+ schemaVersion: SCHEMA_VERSION,
413
+ startedAt,
414
+ lastMessageAt,
415
+ durationSeconds: Math.max(0, Math.floor(durationMs / 1000)),
416
+ projectRef: extractProjectRef(filePath),
417
+ projectName: extractProjectName(filePath),
418
+ model: accum.lastModel,
419
+ title: generateTitle(extractProjectName(filePath), getFirstUserMessage(accum.messages)),
420
+ messages: accum.messages,
421
+ totalInputTokens: accum.totalInputTokens,
422
+ totalOutputTokens: accum.totalOutputTokens,
423
+ totalCachedTokens: accum.totalCachedTokens,
424
+ snapshotAt: new Date().toISOString()
425
+ };
426
+ const raw = {
427
+ sessionKey: `claude:${accum.sessionId}`,
428
+ source: "claude-code",
429
+ parserRevision: PARSER_REVISION,
430
+ collectedAt: new Date().toISOString(),
431
+ sourceFiles: [
432
+ {
433
+ path: filePath,
434
+ format: "jsonl",
435
+ content: accum.lines.join(`
436
+ `)
437
+ }
438
+ ]
439
+ };
440
+ return { canonical, raw };
441
+ }
442
+ function extractProjectRef(filePath) {
443
+ const parts = filePath.split("/");
444
+ const projectsIdx = parts.lastIndexOf("projects");
445
+ if (projectsIdx < 0 || projectsIdx + 1 >= parts.length - 1)
446
+ return null;
447
+ const dirName = parts[projectsIdx + 1];
448
+ if (!dirName)
449
+ return null;
450
+ return hashProjectRef(dirName);
451
+ }
452
+ function extractProjectName(filePath) {
453
+ const parts = filePath.split("/");
454
+ const projectsIdx = parts.lastIndexOf("projects");
455
+ if (projectsIdx < 0 || projectsIdx + 1 >= parts.length - 1)
456
+ return null;
457
+ const dirName = parts[projectsIdx + 1];
458
+ if (!dirName)
459
+ return null;
460
+ return dirName.replace(/-/g, "/");
461
+ }
462
+ async function parseClaudeFileMulti(filePath, startOffset = 0) {
463
+ const st = await stat(filePath).catch(() => null);
464
+ if (!st || !st.isFile() || st.size === 0)
465
+ return [];
466
+ if (startOffset >= st.size)
467
+ return [];
468
+ const sessions = new Map;
469
+ const stream = createReadStream(filePath, {
470
+ encoding: "utf8",
471
+ start: 0
472
+ });
473
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
474
+ try {
475
+ for await (const line of rl) {
476
+ if (!line)
477
+ continue;
478
+ processLine(line, sessions);
479
+ }
480
+ } finally {
481
+ rl.close();
482
+ stream.destroy();
483
+ }
484
+ const results = [];
485
+ for (const accum of sessions.values()) {
486
+ if (accum.messages.length === 0)
487
+ continue;
488
+ results.push(buildParseResult(accum, filePath));
489
+ }
490
+ return results;
491
+ }
492
+ var init_claude = __esm(() => {
493
+ init_src();
494
+ init_hash_project_ref();
495
+ });
496
+
497
+ // src/drivers/session/claude.ts
498
+ import { readdir, stat as stat2 } from "fs/promises";
499
+ import { join as join3 } from "path";
500
+ async function discoverClaudeFiles(claudeDir) {
501
+ const projectsDir = join3(claudeDir, "projects");
502
+ try {
503
+ await stat2(projectsDir);
504
+ } catch {
505
+ return [];
506
+ }
507
+ const results = [];
508
+ await walkJsonl(projectsDir, results);
509
+ return results;
510
+ }
511
+ async function walkJsonl(dir, results) {
512
+ let entries;
513
+ try {
514
+ entries = await readdir(dir, { withFileTypes: true });
515
+ } catch {
516
+ return;
517
+ }
518
+ for (const entry of entries) {
519
+ const fullPath = join3(dir, entry.name);
520
+ if (entry.isDirectory()) {
521
+ await walkJsonl(fullPath, results);
522
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
523
+ results.push(fullPath);
524
+ }
525
+ }
526
+ }
527
+ var claudeSessionDriver;
528
+ var init_claude2 = __esm(() => {
529
+ init_claude();
530
+ claudeSessionDriver = {
531
+ source: "claude-code",
532
+ async discover(opts) {
533
+ if (!opts.claudeDir)
534
+ return [];
535
+ return discoverClaudeFiles(opts.claudeDir);
536
+ },
537
+ shouldSkip(cursor, fingerprint) {
538
+ return fileUnchanged(cursor, fingerprint);
539
+ },
540
+ resumeState(cursor, fingerprint) {
541
+ if (!cursor || cursor.inode !== fingerprint.inode) {
542
+ return { kind: "byte-offset", startOffset: 0 };
543
+ }
544
+ if (cursor.offset > fingerprint.size) {
545
+ return { kind: "byte-offset", startOffset: 0 };
546
+ }
547
+ return { kind: "byte-offset", startOffset: cursor.offset };
548
+ },
549
+ async parse(filePath, resume) {
550
+ return parseClaudeFileMulti(filePath, resume.startOffset);
551
+ },
552
+ buildCursor(fingerprint, _results) {
553
+ return {
554
+ inode: fingerprint.inode,
555
+ mtimeMs: fingerprint.mtimeMs,
556
+ size: fingerprint.size,
557
+ offset: fingerprint.size,
558
+ updatedAt: new Date().toISOString()
559
+ };
560
+ }
561
+ };
562
+ });
563
+
564
+ // src/parsers/codex.ts
565
+ import { createReadStream as createReadStream2 } from "fs";
566
+ import { stat as stat3 } from "fs/promises";
567
+ import { basename } from "path";
568
+ import { createInterface as createInterface2 } from "readline";
569
+ function toNonNegInt2(value) {
570
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
571
+ return 0;
572
+ return Math.floor(value);
573
+ }
574
+ function extractSessionIdFromFilename(filePath) {
575
+ const name = basename(filePath);
576
+ const match = name.match(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/);
577
+ if (!match)
578
+ return null;
579
+ return match[1];
580
+ }
581
+ function extractProjectRef2(cwd) {
582
+ if (!cwd)
583
+ return null;
584
+ return hashProjectRef(cwd);
585
+ }
586
+ function extractProjectName2(cwd) {
587
+ if (!cwd)
588
+ return null;
589
+ const parts = cwd.split("/").filter(Boolean);
590
+ return parts.length > 0 ? parts[parts.length - 1] : null;
591
+ }
592
+ function extractContentFromBlocks(blocks) {
593
+ const parts = [];
594
+ for (const block of blocks) {
595
+ if (typeof block === "object" && block !== null && "text" in block && typeof block.text === "string") {
596
+ parts.push(block.text);
597
+ }
598
+ }
599
+ return parts.join(`
600
+ `);
601
+ }
602
+ function processLine2(line, accum) {
603
+ let obj;
604
+ try {
605
+ obj = JSON.parse(line);
606
+ } catch {
607
+ return;
608
+ }
609
+ if (!obj || typeof obj.type !== "string")
610
+ return;
611
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
612
+ accum.lines.push(line);
613
+ if (timestamp) {
614
+ if (!accum.startedAt || timestamp < accum.startedAt) {
615
+ accum.startedAt = timestamp;
616
+ }
617
+ if (!accum.lastMessageAt || timestamp > accum.lastMessageAt) {
618
+ accum.lastMessageAt = timestamp;
619
+ }
620
+ }
621
+ const payload = obj.payload;
622
+ if (!payload || typeof payload !== "object")
623
+ return;
624
+ switch (obj.type) {
625
+ case "session_meta":
626
+ processSessionMeta(payload, accum);
627
+ break;
628
+ case "turn_context":
629
+ processTurnContext(payload, accum);
630
+ break;
631
+ case "event_msg":
632
+ processEventMsg(payload, timestamp, accum);
633
+ break;
634
+ case "response_item":
635
+ processResponseItem(payload, timestamp, accum);
636
+ break;
637
+ }
638
+ }
639
+ function processSessionMeta(payload, accum) {
640
+ if (typeof payload.id === "string") {
641
+ accum.sessionId = payload.id;
642
+ }
643
+ if (typeof payload.cwd === "string") {
644
+ accum.cwd = payload.cwd;
645
+ }
646
+ if (typeof payload.timestamp === "string") {
647
+ if (!accum.startedAt || payload.timestamp < accum.startedAt) {
648
+ accum.startedAt = payload.timestamp;
649
+ }
650
+ }
651
+ }
652
+ function processTurnContext(payload, accum) {
653
+ if (typeof payload.model === "string") {
654
+ accum.lastModel = payload.model;
655
+ }
656
+ if (typeof payload.cwd === "string" && !accum.cwd) {
657
+ accum.cwd = payload.cwd;
658
+ }
659
+ }
660
+ function processEventMsg(payload, timestamp, accum) {
661
+ const subtype = payload.type;
662
+ if (typeof subtype !== "string")
663
+ return;
664
+ const ts = timestamp ?? new Date().toISOString();
665
+ switch (subtype) {
666
+ case "user_message": {
667
+ const message = typeof payload.message === "string" ? payload.message : null;
668
+ if (message) {
669
+ accum.messages.push({
670
+ role: "user",
671
+ content: message,
672
+ timestamp: ts
673
+ });
674
+ }
675
+ break;
676
+ }
677
+ case "agent_message": {
678
+ const message = typeof payload.message === "string" ? payload.message : null;
679
+ if (message) {
680
+ accum.messages.push({
681
+ role: "assistant",
682
+ content: message,
683
+ model: accum.lastModel ?? undefined,
684
+ timestamp: ts
685
+ });
686
+ }
687
+ break;
688
+ }
689
+ case "token_count": {
690
+ const info = payload.info;
691
+ if (info?.total_token_usage) {
692
+ const usage = info.total_token_usage;
693
+ accum.totalInputTokens = toNonNegInt2(usage.input_tokens);
694
+ accum.totalOutputTokens = toNonNegInt2(usage.output_tokens);
695
+ accum.totalCachedTokens = toNonNegInt2(usage.cached_input_tokens);
696
+ }
697
+ break;
698
+ }
699
+ }
700
+ }
701
+ function processResponseItem(payload, timestamp, accum) {
702
+ const subtype = payload.type;
703
+ if (typeof subtype !== "string")
704
+ return;
705
+ const ts = timestamp ?? new Date().toISOString();
706
+ switch (subtype) {
707
+ case "message": {
708
+ const role = payload.role;
709
+ const content = payload.content;
710
+ if (role === "user" || role === "assistant") {
711
+ let text = "";
712
+ if (Array.isArray(content)) {
713
+ text = extractContentFromBlocks(content);
714
+ } else if (typeof content === "string") {
715
+ text = content;
716
+ }
717
+ if (text) {
718
+ accum.messages.push({
719
+ role: role === "user" ? "user" : "assistant",
720
+ content: text,
721
+ model: role === "assistant" ? accum.lastModel ?? undefined : undefined,
722
+ timestamp: ts
723
+ });
724
+ }
725
+ }
726
+ break;
727
+ }
728
+ case "function_call": {
729
+ const name = typeof payload.name === "string" ? payload.name : undefined;
730
+ const args = typeof payload.arguments === "string" ? payload.arguments : undefined;
731
+ accum.messages.push({
732
+ role: "tool",
733
+ content: "",
734
+ toolName: name,
735
+ toolInput: args,
736
+ timestamp: ts
737
+ });
738
+ break;
739
+ }
740
+ case "function_call_output": {
741
+ const output = typeof payload.output === "string" ? payload.output : "";
742
+ accum.messages.push({
743
+ role: "tool",
744
+ content: output,
745
+ toolResult: output,
746
+ timestamp: ts
747
+ });
748
+ break;
749
+ }
750
+ }
751
+ }
752
+ function buildParseResult2(accum, filePath) {
753
+ const startedAt = accum.startedAt ?? new Date().toISOString();
754
+ const lastMessageAt = accum.lastMessageAt ?? startedAt;
755
+ const durationMs = new Date(lastMessageAt).getTime() - new Date(startedAt).getTime();
756
+ const sessionKey = `codex:${accum.sessionId}`;
757
+ const canonical = {
758
+ sessionKey,
759
+ source: "codex",
760
+ parserRevision: PARSER_REVISION,
761
+ schemaVersion: SCHEMA_VERSION,
762
+ startedAt,
763
+ lastMessageAt,
764
+ durationSeconds: Math.max(0, Math.floor(durationMs / 1000)),
765
+ projectRef: extractProjectRef2(accum.cwd),
766
+ projectName: extractProjectName2(accum.cwd),
767
+ model: accum.lastModel,
768
+ title: generateTitle(extractProjectName2(accum.cwd), getFirstUserMessage(accum.messages)),
769
+ messages: accum.messages,
770
+ totalInputTokens: accum.totalInputTokens,
771
+ totalOutputTokens: accum.totalOutputTokens,
772
+ totalCachedTokens: accum.totalCachedTokens,
773
+ snapshotAt: new Date().toISOString()
774
+ };
775
+ const raw = {
776
+ sessionKey,
777
+ source: "codex",
778
+ parserRevision: PARSER_REVISION,
779
+ collectedAt: new Date().toISOString(),
780
+ sourceFiles: [
781
+ {
782
+ path: filePath,
783
+ format: "jsonl",
784
+ content: accum.lines.join(`
785
+ `)
786
+ }
787
+ ]
788
+ };
789
+ return { canonical, raw };
790
+ }
791
+ function buildEmptyResult(filePath) {
792
+ const now = new Date().toISOString();
793
+ const sessionId = extractSessionIdFromFilename(filePath) ?? "unknown";
794
+ const sessionKey = `codex:${sessionId}`;
795
+ return {
796
+ canonical: {
797
+ sessionKey,
798
+ source: "codex",
799
+ parserRevision: PARSER_REVISION,
800
+ schemaVersion: SCHEMA_VERSION,
801
+ startedAt: now,
802
+ lastMessageAt: now,
803
+ durationSeconds: 0,
804
+ projectRef: null,
805
+ projectName: null,
806
+ model: null,
807
+ title: null,
808
+ messages: [],
809
+ totalInputTokens: 0,
810
+ totalOutputTokens: 0,
811
+ totalCachedTokens: 0,
812
+ snapshotAt: now
813
+ },
814
+ raw: {
815
+ sessionKey,
816
+ source: "codex",
817
+ parserRevision: PARSER_REVISION,
818
+ collectedAt: now,
819
+ sourceFiles: [{ path: filePath, format: "jsonl", content: "" }]
820
+ }
821
+ };
822
+ }
823
+ async function parseCodexFile(filePath, startOffset = 0) {
824
+ const st = await stat3(filePath).catch(() => null);
825
+ if (!st || !st.isFile() || st.size === 0)
826
+ return buildEmptyResult(filePath);
827
+ if (startOffset >= st.size)
828
+ return buildEmptyResult(filePath);
829
+ const filenameId = extractSessionIdFromFilename(filePath) ?? "unknown";
830
+ const accum = {
831
+ sessionId: filenameId,
832
+ messages: [],
833
+ lines: [],
834
+ startedAt: null,
835
+ lastMessageAt: null,
836
+ lastModel: null,
837
+ cwd: null,
838
+ totalInputTokens: 0,
839
+ totalOutputTokens: 0,
840
+ totalCachedTokens: 0
841
+ };
842
+ const stream = createReadStream2(filePath, {
843
+ encoding: "utf8",
844
+ start: 0
845
+ });
846
+ const rl = createInterface2({ input: stream, crlfDelay: Infinity });
847
+ try {
848
+ for await (const line of rl) {
849
+ if (!line)
850
+ continue;
851
+ processLine2(line, accum);
852
+ }
853
+ } finally {
854
+ rl.close();
855
+ stream.destroy();
856
+ }
857
+ if (accum.messages.length === 0)
858
+ return buildEmptyResult(filePath);
859
+ return buildParseResult2(accum, filePath);
860
+ }
861
+ var init_codex = __esm(() => {
862
+ init_src();
863
+ init_hash_project_ref();
864
+ });
865
+
866
+ // src/drivers/session/codex.ts
867
+ import { readdir as readdir2, stat as stat4 } from "fs/promises";
868
+ import { join as join4 } from "path";
869
+ async function walkFiltered(dir, results, filter) {
870
+ let entries;
871
+ try {
872
+ entries = await readdir2(dir, { withFileTypes: true });
873
+ } catch {
874
+ return;
875
+ }
876
+ for (const entry of entries) {
877
+ const fullPath = join4(dir, entry.name);
878
+ if (entry.isDirectory()) {
879
+ await walkFiltered(fullPath, results, filter);
880
+ } else if (entry.isFile() && filter(entry.name)) {
881
+ results.push(fullPath);
882
+ }
883
+ }
884
+ }
885
+ async function discoverCodexFiles(codexSessionsDir) {
886
+ try {
887
+ await stat4(codexSessionsDir);
888
+ } catch {
889
+ return [];
890
+ }
891
+ const results = [];
892
+ await walkFiltered(codexSessionsDir, results, (name) => name.startsWith("rollout-") && name.endsWith(".jsonl"));
893
+ return results;
894
+ }
895
+ var codexSessionDriver;
896
+ var init_codex2 = __esm(() => {
897
+ init_codex();
898
+ codexSessionDriver = {
899
+ source: "codex",
900
+ async discover(opts) {
901
+ if (!opts.codexSessionsDir)
902
+ return [];
903
+ return discoverCodexFiles(opts.codexSessionsDir);
904
+ },
905
+ shouldSkip(cursor, fingerprint) {
906
+ return fileUnchanged(cursor, fingerprint);
907
+ },
908
+ resumeState(cursor, fingerprint) {
909
+ if (!cursor || cursor.inode !== fingerprint.inode) {
910
+ return { kind: "codex", startOffset: 0, lastTotalTokens: 0, lastModel: null };
911
+ }
912
+ if (cursor.offset > fingerprint.size) {
913
+ return { kind: "codex", startOffset: 0, lastTotalTokens: 0, lastModel: null };
914
+ }
915
+ return {
916
+ kind: "codex",
917
+ startOffset: cursor.offset,
918
+ lastTotalTokens: cursor.lastTotalTokens,
919
+ lastModel: cursor.lastModel
920
+ };
921
+ },
922
+ async parse(filePath, resume) {
923
+ const result = await parseCodexFile(filePath, resume.startOffset);
924
+ return [result];
925
+ },
926
+ buildCursor(fingerprint, results) {
927
+ let lastTotalTokens = 0;
928
+ let lastModel = null;
929
+ if (results.length > 0) {
930
+ const session = results[0].canonical;
931
+ lastTotalTokens = session.totalInputTokens + session.totalOutputTokens;
932
+ lastModel = session.model;
933
+ }
934
+ return {
935
+ inode: fingerprint.inode,
936
+ mtimeMs: fingerprint.mtimeMs,
937
+ size: fingerprint.size,
938
+ offset: fingerprint.size,
939
+ lastTotalTokens,
940
+ lastModel,
941
+ updatedAt: new Date().toISOString()
942
+ };
943
+ }
944
+ };
945
+ });
946
+
947
+ // src/parsers/gemini.ts
948
+ import { readFile as readFile2, stat as stat5 } from "fs/promises";
949
+ function toNonNegInt3(value) {
950
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
951
+ return 0;
952
+ return Math.floor(value);
953
+ }
954
+ function extractUserContent2(content) {
955
+ if (!Array.isArray(content))
956
+ return "";
957
+ const parts = [];
958
+ for (const item of content) {
959
+ if (typeof item === "object" && item !== null && "text" in item && typeof item.text === "string") {
960
+ parts.push(item.text);
961
+ }
962
+ }
963
+ return parts.join(`
964
+ `);
965
+ }
966
+ function extractProjectRef3(projectHash) {
967
+ if (!projectHash)
968
+ return null;
969
+ return hashProjectRef(projectHash);
970
+ }
971
+ function extractProjectName3(_projectHash) {
972
+ return null;
973
+ }
974
+ function processUserMessage(msg, accum) {
975
+ const text = extractUserContent2(msg.content);
976
+ if (!text)
977
+ return;
978
+ accum.messages.push({
979
+ role: "user",
980
+ content: text,
981
+ timestamp: msg.timestamp ?? new Date().toISOString()
982
+ });
983
+ }
984
+ function processGeminiMessage(msg, accum) {
985
+ const ts = msg.timestamp ?? new Date().toISOString();
986
+ if (typeof msg.model === "string") {
987
+ accum.lastModel = msg.model;
988
+ }
989
+ if (typeof msg.content === "string" && msg.content.length > 0) {
990
+ accum.messages.push({
991
+ role: "assistant",
992
+ content: msg.content,
993
+ model: msg.model ?? accum.lastModel ?? undefined,
994
+ timestamp: ts
995
+ });
996
+ }
997
+ if (Array.isArray(msg.toolCalls)) {
998
+ for (const tc of msg.toolCalls) {
999
+ processToolCall(tc, accum, ts);
1000
+ }
1001
+ }
1002
+ if (msg.tokens) {
1003
+ accum.totalInputTokens += toNonNegInt3(msg.tokens.input);
1004
+ accum.totalOutputTokens += toNonNegInt3(msg.tokens.output);
1005
+ accum.totalCachedTokens += toNonNegInt3(msg.tokens.cached);
1006
+ }
1007
+ }
1008
+ function processToolCall(tc, accum, parentTs) {
1009
+ const ts = tc.timestamp ?? parentTs;
1010
+ const toolName = tc.name ?? tc.displayName;
1011
+ accum.messages.push({
1012
+ role: "tool",
1013
+ content: "",
1014
+ toolName: toolName ?? undefined,
1015
+ toolInput: tc.args ? JSON.stringify(tc.args) : undefined,
1016
+ timestamp: ts
1017
+ });
1018
+ if (Array.isArray(tc.result)) {
1019
+ for (const item of tc.result) {
1020
+ const output = item.functionResponse?.response?.output;
1021
+ if (typeof output === "string") {
1022
+ accum.messages.push({
1023
+ role: "tool",
1024
+ content: output,
1025
+ toolName: toolName ?? undefined,
1026
+ toolResult: output,
1027
+ timestamp: ts
1028
+ });
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ function buildParseResult3(session, accum, filePath, rawContent, sourceMessageCount) {
1034
+ const sessionId = session.sessionId ?? "unknown";
1035
+ const sessionKey = `gemini:${sessionId}`;
1036
+ const startedAt = session.startTime ?? new Date().toISOString();
1037
+ const lastMessageAt = session.lastUpdated ?? startedAt;
1038
+ const durationMs = new Date(lastMessageAt).getTime() - new Date(startedAt).getTime();
1039
+ const canonical = {
1040
+ sessionKey,
1041
+ source: "gemini-cli",
1042
+ parserRevision: PARSER_REVISION,
1043
+ schemaVersion: SCHEMA_VERSION,
1044
+ startedAt,
1045
+ lastMessageAt,
1046
+ durationSeconds: Math.max(0, Math.floor(durationMs / 1000)),
1047
+ projectRef: extractProjectRef3(session.projectHash),
1048
+ projectName: extractProjectName3(session.projectHash),
1049
+ model: accum.lastModel,
1050
+ title: generateTitle(extractProjectName3(session.projectHash), getFirstUserMessage(accum.messages)),
1051
+ messages: accum.messages,
1052
+ totalInputTokens: accum.totalInputTokens,
1053
+ totalOutputTokens: accum.totalOutputTokens,
1054
+ totalCachedTokens: accum.totalCachedTokens,
1055
+ snapshotAt: new Date().toISOString()
1056
+ };
1057
+ const raw = {
1058
+ sessionKey,
1059
+ source: "gemini-cli",
1060
+ parserRevision: PARSER_REVISION,
1061
+ collectedAt: new Date().toISOString(),
1062
+ sourceFiles: [
1063
+ {
1064
+ path: filePath,
1065
+ format: "json",
1066
+ content: rawContent
1067
+ }
1068
+ ]
1069
+ };
1070
+ return { canonical, raw, sourceMessageCount };
1071
+ }
1072
+ function buildEmptyResult2(filePath) {
1073
+ const now = new Date().toISOString();
1074
+ const sessionKey = "gemini:unknown";
1075
+ return {
1076
+ canonical: {
1077
+ sessionKey,
1078
+ source: "gemini-cli",
1079
+ parserRevision: PARSER_REVISION,
1080
+ schemaVersion: SCHEMA_VERSION,
1081
+ startedAt: now,
1082
+ lastMessageAt: now,
1083
+ durationSeconds: 0,
1084
+ projectRef: null,
1085
+ projectName: null,
1086
+ model: null,
1087
+ title: null,
1088
+ messages: [],
1089
+ totalInputTokens: 0,
1090
+ totalOutputTokens: 0,
1091
+ totalCachedTokens: 0,
1092
+ snapshotAt: now
1093
+ },
1094
+ raw: {
1095
+ sessionKey,
1096
+ source: "gemini-cli",
1097
+ parserRevision: PARSER_REVISION,
1098
+ collectedAt: now,
1099
+ sourceFiles: [{ path: filePath, format: "json", content: "" }]
1100
+ },
1101
+ sourceMessageCount: 0
1102
+ };
1103
+ }
1104
+ async function parseGeminiFile(filePath, startIndex = 0) {
1105
+ const st = await stat5(filePath).catch(() => null);
1106
+ if (!st || !st.isFile() || st.size === 0)
1107
+ return buildEmptyResult2(filePath);
1108
+ let rawContent;
1109
+ try {
1110
+ rawContent = await readFile2(filePath, "utf8");
1111
+ } catch {
1112
+ return buildEmptyResult2(filePath);
1113
+ }
1114
+ let session;
1115
+ try {
1116
+ session = JSON.parse(rawContent);
1117
+ } catch {
1118
+ return buildEmptyResult2(filePath);
1119
+ }
1120
+ if (!session || typeof session !== "object")
1121
+ return buildEmptyResult2(filePath);
1122
+ const messages = session.messages;
1123
+ if (!Array.isArray(messages) || messages.length === 0) {
1124
+ return buildEmptyResult2(filePath);
1125
+ }
1126
+ if (startIndex >= messages.length)
1127
+ return buildEmptyResult2(filePath);
1128
+ const accum = {
1129
+ messages: [],
1130
+ totalInputTokens: 0,
1131
+ totalOutputTokens: 0,
1132
+ totalCachedTokens: 0,
1133
+ lastModel: null
1134
+ };
1135
+ for (let i = 0;i < messages.length; i++) {
1136
+ const msg = messages[i];
1137
+ if (!msg || typeof msg !== "object" || typeof msg.type !== "string") {
1138
+ continue;
1139
+ }
1140
+ switch (msg.type) {
1141
+ case "user":
1142
+ processUserMessage(msg, accum);
1143
+ break;
1144
+ case "gemini":
1145
+ processGeminiMessage(msg, accum);
1146
+ break;
1147
+ }
1148
+ }
1149
+ if (accum.messages.length === 0)
1150
+ return buildEmptyResult2(filePath);
1151
+ return buildParseResult3(session, accum, filePath, rawContent, messages.length);
1152
+ }
1153
+ var init_gemini = __esm(() => {
1154
+ init_src();
1155
+ init_hash_project_ref();
1156
+ });
1157
+
1158
+ // src/drivers/session/gemini.ts
1159
+ import { readdir as readdir3, stat as stat6 } from "fs/promises";
1160
+ import { join as join5 } from "path";
1161
+ async function discoverGeminiFiles(geminiDir) {
1162
+ const tmpDir = join5(geminiDir, "tmp");
1163
+ try {
1164
+ await stat6(tmpDir);
1165
+ } catch {
1166
+ return [];
1167
+ }
1168
+ const results = [];
1169
+ let projectDirs;
1170
+ try {
1171
+ projectDirs = await readdir3(tmpDir, { withFileTypes: true });
1172
+ } catch {
1173
+ return [];
1174
+ }
1175
+ for (const projEntry of projectDirs) {
1176
+ if (!projEntry.isDirectory())
1177
+ continue;
1178
+ const chatsDir = join5(tmpDir, projEntry.name, "chats");
1179
+ let chatFiles;
1180
+ try {
1181
+ chatFiles = await readdir3(chatsDir, { withFileTypes: true });
1182
+ } catch {
1183
+ continue;
1184
+ }
1185
+ for (const chatEntry of chatFiles) {
1186
+ if (chatEntry.isFile() && chatEntry.name.startsWith("session-") && chatEntry.name.endsWith(".json")) {
1187
+ results.push(join5(chatsDir, chatEntry.name));
1188
+ }
1189
+ }
1190
+ }
1191
+ return results;
1192
+ }
1193
+ var geminiSessionDriver;
1194
+ var init_gemini2 = __esm(() => {
1195
+ init_gemini();
1196
+ geminiSessionDriver = {
1197
+ source: "gemini-cli",
1198
+ async discover(opts) {
1199
+ if (!opts.geminiDir)
1200
+ return [];
1201
+ return discoverGeminiFiles(opts.geminiDir);
1202
+ },
1203
+ shouldSkip(cursor, fingerprint) {
1204
+ return fileUnchanged(cursor, fingerprint);
1205
+ },
1206
+ resumeState(cursor, fingerprint) {
1207
+ if (!cursor || cursor.inode !== fingerprint.inode) {
1208
+ return {
1209
+ kind: "array-index",
1210
+ startIndex: 0,
1211
+ lastTotalTokens: 0,
1212
+ lastModel: null
1213
+ };
1214
+ }
1215
+ if (cursor.size > fingerprint.size) {
1216
+ return {
1217
+ kind: "array-index",
1218
+ startIndex: 0,
1219
+ lastTotalTokens: 0,
1220
+ lastModel: null
1221
+ };
1222
+ }
1223
+ return {
1224
+ kind: "array-index",
1225
+ startIndex: cursor.messageIndex,
1226
+ lastTotalTokens: cursor.lastTotalTokens,
1227
+ lastModel: cursor.lastModel
1228
+ };
1229
+ },
1230
+ async parse(filePath, resume) {
1231
+ const result = await parseGeminiFile(filePath, resume.startIndex);
1232
+ return [result];
1233
+ },
1234
+ buildCursor(fingerprint, results) {
1235
+ let lastTotalTokens = 0;
1236
+ let lastModel = null;
1237
+ let messageIndex = 0;
1238
+ if (results.length > 0) {
1239
+ const session = results[0].canonical;
1240
+ lastTotalTokens = session.totalInputTokens + session.totalOutputTokens;
1241
+ lastModel = session.model;
1242
+ const geminiResult = results[0];
1243
+ messageIndex = geminiResult.sourceMessageCount;
1244
+ }
1245
+ return {
1246
+ inode: fingerprint.inode,
1247
+ mtimeMs: fingerprint.mtimeMs,
1248
+ size: fingerprint.size,
1249
+ messageIndex,
1250
+ lastTotalTokens,
1251
+ lastModel,
1252
+ updatedAt: new Date().toISOString()
1253
+ };
1254
+ }
1255
+ };
1256
+ });
1257
+
1258
+ // src/parsers/opencode.ts
1259
+ import { readFile as readFile3, readdir as readdir4 } from "fs/promises";
1260
+ import { join as join6 } from "path";
1261
+ function toNonNegInt4(value) {
1262
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
1263
+ return 0;
1264
+ return Math.floor(value);
1265
+ }
1266
+ function msToIso(ms) {
1267
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) {
1268
+ return new Date().toISOString();
1269
+ }
1270
+ return new Date(ms).toISOString();
1271
+ }
1272
+ function extractProjectRef4(projectId) {
1273
+ if (!projectId)
1274
+ return null;
1275
+ return hashProjectRef(projectId);
1276
+ }
1277
+ function extractProjectName4(directory) {
1278
+ if (!directory || typeof directory !== "string")
1279
+ return null;
1280
+ return directory;
1281
+ }
1282
+ function processTextPart(part, role, model, ts, accum) {
1283
+ if (!part.text || typeof part.text !== "string" || part.text.length === 0)
1284
+ return;
1285
+ if (part.synthetic)
1286
+ return;
1287
+ const canonicalRole = role === "user" ? "user" : "assistant";
1288
+ accum.messages.push({
1289
+ role: canonicalRole,
1290
+ content: part.text,
1291
+ model: canonicalRole === "assistant" ? model : undefined,
1292
+ timestamp: ts
1293
+ });
1294
+ }
1295
+ function processToolPart(part, ts, accum) {
1296
+ if (!part.state)
1297
+ return;
1298
+ const toolName = part.tool ?? undefined;
1299
+ const input = part.state.input ? JSON.stringify(part.state.input) : undefined;
1300
+ if (part.state.status === "completed") {
1301
+ const output = part.state.output ?? part.state.metadata?.output ?? "";
1302
+ accum.messages.push({
1303
+ role: "tool",
1304
+ content: "",
1305
+ toolName,
1306
+ toolInput: input,
1307
+ timestamp: ts
1308
+ });
1309
+ if (typeof output === "string" && output.length > 0) {
1310
+ accum.messages.push({
1311
+ role: "tool",
1312
+ content: output,
1313
+ toolName,
1314
+ toolResult: output,
1315
+ timestamp: ts
1316
+ });
1317
+ }
1318
+ } else if (part.state.status === "running") {
1319
+ accum.messages.push({
1320
+ role: "tool",
1321
+ content: "",
1322
+ toolName,
1323
+ toolInput: input,
1324
+ timestamp: ts
1325
+ });
1326
+ }
1327
+ }
1328
+ function processMessage(msg, parts, accum) {
1329
+ const ts = msToIso(msg.time?.created);
1330
+ const role = msg.role;
1331
+ if (role === "assistant" && typeof msg.modelID === "string") {
1332
+ accum.lastModel = msg.modelID;
1333
+ }
1334
+ const model = msg.modelID ?? accum.lastModel ?? undefined;
1335
+ for (const part of parts) {
1336
+ if (!part || typeof part !== "object" || typeof part.type !== "string")
1337
+ continue;
1338
+ switch (part.type) {
1339
+ case "text":
1340
+ processTextPart(part, role, model, ts, accum);
1341
+ break;
1342
+ case "tool":
1343
+ processToolPart(part, ts, accum);
1344
+ break;
1345
+ }
1346
+ }
1347
+ if (role === "assistant" && msg.tokens) {
1348
+ accum.totalInputTokens += toNonNegInt4(msg.tokens.input);
1349
+ accum.totalOutputTokens += toNonNegInt4(msg.tokens.output);
1350
+ accum.totalCachedTokens += toNonNegInt4(msg.tokens.cache?.read);
1351
+ }
1352
+ }
1353
+ function buildParseResult4(session, accum, rawContent, rawFormat, rawPath) {
1354
+ const sessionKey = `opencode:${session.id}`;
1355
+ const startedAt = msToIso(session.time?.created);
1356
+ const lastMessageAt = msToIso(session.time?.updated);
1357
+ const durationMs = new Date(lastMessageAt).getTime() - new Date(startedAt).getTime();
1358
+ const canonical = {
1359
+ sessionKey,
1360
+ source: "opencode",
1361
+ parserRevision: PARSER_REVISION,
1362
+ schemaVersion: SCHEMA_VERSION,
1363
+ startedAt,
1364
+ lastMessageAt,
1365
+ durationSeconds: Math.max(0, Math.floor(durationMs / 1000)),
1366
+ projectRef: extractProjectRef4(session.projectID),
1367
+ projectName: extractProjectName4(session.directory),
1368
+ model: accum.lastModel,
1369
+ title: session.title ?? null,
1370
+ messages: accum.messages,
1371
+ totalInputTokens: accum.totalInputTokens,
1372
+ totalOutputTokens: accum.totalOutputTokens,
1373
+ totalCachedTokens: accum.totalCachedTokens,
1374
+ snapshotAt: new Date().toISOString()
1375
+ };
1376
+ const raw = {
1377
+ sessionKey,
1378
+ source: "opencode",
1379
+ parserRevision: PARSER_REVISION,
1380
+ collectedAt: new Date().toISOString(),
1381
+ sourceFiles: [
1382
+ {
1383
+ path: rawPath,
1384
+ format: rawFormat,
1385
+ content: rawContent
1386
+ }
1387
+ ]
1388
+ };
1389
+ return { canonical, raw };
1390
+ }
1391
+ function buildEmptyResult3(sessionId, rawPath) {
1392
+ const now = new Date().toISOString();
1393
+ const sessionKey = `opencode:${sessionId}`;
1394
+ return {
1395
+ canonical: {
1396
+ sessionKey,
1397
+ source: "opencode",
1398
+ parserRevision: PARSER_REVISION,
1399
+ schemaVersion: SCHEMA_VERSION,
1400
+ startedAt: now,
1401
+ lastMessageAt: now,
1402
+ durationSeconds: 0,
1403
+ projectRef: null,
1404
+ projectName: null,
1405
+ model: null,
1406
+ title: null,
1407
+ messages: [],
1408
+ totalInputTokens: 0,
1409
+ totalOutputTokens: 0,
1410
+ totalCachedTokens: 0,
1411
+ snapshotAt: now
1412
+ },
1413
+ raw: {
1414
+ sessionKey,
1415
+ source: "opencode",
1416
+ parserRevision: PARSER_REVISION,
1417
+ collectedAt: now,
1418
+ sourceFiles: [{ path: rawPath, format: "json", content: "" }]
1419
+ }
1420
+ };
1421
+ }
1422
+ async function loadJsonFileWithRaw(filePath) {
1423
+ try {
1424
+ const raw = await readFile3(filePath, "utf8");
1425
+ return { parsed: JSON.parse(raw), raw };
1426
+ } catch {
1427
+ return null;
1428
+ }
1429
+ }
1430
+ async function loadPartsForMessageWithRaw(partDir, messageId) {
1431
+ const msgPartDir = join6(partDir, messageId);
1432
+ let entries;
1433
+ try {
1434
+ entries = await readdir4(msgPartDir);
1435
+ } catch {
1436
+ return { parts: [], sourceFiles: [] };
1437
+ }
1438
+ const parts = [];
1439
+ const sourceFiles = [];
1440
+ for (const entry of entries.sort()) {
1441
+ if (!entry.endsWith(".json"))
1442
+ continue;
1443
+ const filePath = join6(msgPartDir, entry);
1444
+ const loaded = await loadJsonFileWithRaw(filePath);
1445
+ if (loaded && typeof loaded.parsed.type === "string") {
1446
+ parts.push(loaded.parsed);
1447
+ sourceFiles.push({ path: filePath, format: "json", content: loaded.raw });
1448
+ }
1449
+ }
1450
+ return { parts, sourceFiles };
1451
+ }
1452
+ function parseOpenCodeMessages(session, messages, rawFormat, rawPath) {
1453
+ if (!messages || messages.length === 0) {
1454
+ return buildEmptyResult3(session.id, rawPath);
1455
+ }
1456
+ const accum = {
1457
+ messages: [],
1458
+ totalInputTokens: 0,
1459
+ totalOutputTokens: 0,
1460
+ totalCachedTokens: 0,
1461
+ lastModel: null
1462
+ };
1463
+ for (const msg of messages) {
1464
+ if (!msg || typeof msg !== "object" || typeof msg.role !== "string")
1465
+ continue;
1466
+ const parts = msg.parts ?? [];
1467
+ processMessage(msg, parts, accum);
1468
+ }
1469
+ if (accum.messages.length === 0) {
1470
+ return buildEmptyResult3(session.id, rawPath);
1471
+ }
1472
+ const rawContent = JSON.stringify(messages);
1473
+ return buildParseResult4(session, accum, rawContent, rawFormat, rawPath);
1474
+ }
1475
+ async function parseOpenCodeJsonSession(sessionJsonPath, messageDir, partDir) {
1476
+ const sessionLoaded = await loadJsonFileWithRaw(sessionJsonPath);
1477
+ if (!sessionLoaded || typeof sessionLoaded.parsed !== "object" || !sessionLoaded.parsed.id) {
1478
+ return buildEmptyResult3("unknown", sessionJsonPath);
1479
+ }
1480
+ const session = sessionLoaded.parsed;
1481
+ const rawSourceFiles = [
1482
+ { path: sessionJsonPath, format: "json", content: sessionLoaded.raw }
1483
+ ];
1484
+ const sessionMsgDir = join6(messageDir, session.id);
1485
+ let msgEntries;
1486
+ try {
1487
+ msgEntries = await readdir4(sessionMsgDir);
1488
+ } catch {
1489
+ return buildEmptyResult3(session.id, sessionJsonPath);
1490
+ }
1491
+ const messages = [];
1492
+ for (const entry of msgEntries.sort()) {
1493
+ if (!entry.endsWith(".json"))
1494
+ continue;
1495
+ const msgPath = join6(sessionMsgDir, entry);
1496
+ const msgLoaded = await loadJsonFileWithRaw(msgPath);
1497
+ if (!msgLoaded || typeof msgLoaded.parsed.role !== "string")
1498
+ continue;
1499
+ const msg = msgLoaded.parsed;
1500
+ rawSourceFiles.push({ path: msgPath, format: "json", content: msgLoaded.raw });
1501
+ const partsResult = await loadPartsForMessageWithRaw(partDir, msg.id);
1502
+ msg.parts = partsResult.parts;
1503
+ rawSourceFiles.push(...partsResult.sourceFiles);
1504
+ messages.push(msg);
1505
+ }
1506
+ messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
1507
+ const result = parseOpenCodeMessages(session, messages, "json", sessionJsonPath);
1508
+ result.raw.sourceFiles = rawSourceFiles;
1509
+ return result;
1510
+ }
1511
+ function parseOpenCodeSqliteSession(session, messages, dbPath, rawSourceFiles) {
1512
+ const result = parseOpenCodeMessages(session, messages, "sqlite-export", dbPath);
1513
+ if (rawSourceFiles && rawSourceFiles.length > 0) {
1514
+ result.raw.sourceFiles = rawSourceFiles;
1515
+ }
1516
+ return result;
1517
+ }
1518
+ var init_opencode = __esm(() => {
1519
+ init_src();
1520
+ init_hash_project_ref();
1521
+ });
1522
+
1523
+ // src/drivers/session/opencode.ts
1524
+ import { readdir as readdir5, stat as stat8 } from "fs/promises";
1525
+ import { join as join7, dirname as dirname2, basename as basename2 } from "path";
1526
+ async function discoverOpenCodeJsonFiles(messageDir, ctx, inodeToFilePath) {
1527
+ const storageDir = dirname2(messageDir);
1528
+ const sessionDir = join7(storageDir, "session");
1529
+ try {
1530
+ await stat8(sessionDir);
1531
+ } catch {
1532
+ return [];
1533
+ }
1534
+ let projectDirs;
1535
+ try {
1536
+ projectDirs = await readdir5(sessionDir, { withFileTypes: true });
1537
+ } catch {
1538
+ return [];
1539
+ }
1540
+ const results = [];
1541
+ const msgDirMtimes = {};
1542
+ for (const projEntry of projectDirs) {
1543
+ if (!projEntry.isDirectory())
1544
+ continue;
1545
+ const projDir = join7(sessionDir, projEntry.name);
1546
+ let sessionFiles;
1547
+ try {
1548
+ sessionFiles = await readdir5(projDir, { withFileTypes: true });
1549
+ } catch {
1550
+ continue;
1551
+ }
1552
+ for (const fileEntry of sessionFiles) {
1553
+ if (fileEntry.isFile() && fileEntry.name.startsWith("ses_") && fileEntry.name.endsWith(".json")) {
1554
+ const filePath = join7(projDir, fileEntry.name);
1555
+ results.push(filePath);
1556
+ if (inodeToFilePath) {
1557
+ try {
1558
+ const fileStat = await stat8(filePath);
1559
+ inodeToFilePath.set(fileStat.ino, filePath);
1560
+ } catch {}
1561
+ }
1562
+ const sessionId = basename2(fileEntry.name, ".json");
1563
+ const sessionMsgDir = join7(messageDir, sessionId);
1564
+ try {
1565
+ const msgStat = await stat8(sessionMsgDir);
1566
+ msgDirMtimes[filePath] = msgStat.mtimeMs;
1567
+ } catch {}
1568
+ }
1569
+ }
1570
+ }
1571
+ if (ctx) {
1572
+ ctx.openCodeMsgDirMtimes = msgDirMtimes;
1573
+ }
1574
+ return results;
1575
+ }
1576
+ function createOpenCodeJsonDriver(syncCtx) {
1577
+ const inodeToFilePath = new Map;
1578
+ return {
1579
+ source: "opencode",
1580
+ async discover(opts) {
1581
+ if (!opts.openCodeMessageDir)
1582
+ return [];
1583
+ inodeToFilePath.clear();
1584
+ return discoverOpenCodeJsonFiles(opts.openCodeMessageDir, syncCtx, inodeToFilePath);
1585
+ },
1586
+ shouldSkip(cursor, fingerprint) {
1587
+ if (!fileUnchanged(cursor, fingerprint))
1588
+ return false;
1589
+ if (cursor && syncCtx?.openCodeMsgDirMtimes) {
1590
+ const filePath = inodeToFilePath.get(fingerprint.inode);
1591
+ if (filePath) {
1592
+ const currentMsgMtime = syncCtx.openCodeMsgDirMtimes[filePath];
1593
+ if (cursor.messageDirMtimeMs === undefined)
1594
+ return false;
1595
+ if (currentMsgMtime !== undefined && currentMsgMtime !== cursor.messageDirMtimeMs) {
1596
+ return false;
1597
+ }
1598
+ if (cursor.messageDirMtimeMs === undefined && currentMsgMtime !== undefined) {
1599
+ return false;
1600
+ }
1601
+ }
1602
+ }
1603
+ return true;
1604
+ },
1605
+ resumeState(_cursor, _fingerprint) {
1606
+ return { kind: "opencode-json" };
1607
+ },
1608
+ async parse(filePath, _resume) {
1609
+ const sessionDir = dirname2(filePath);
1610
+ const projectSessionDir = dirname2(sessionDir);
1611
+ const storageDir = dirname2(projectSessionDir);
1612
+ const messageDir = join7(storageDir, "message");
1613
+ const partDir = join7(storageDir, "part");
1614
+ const result = await parseOpenCodeJsonSession(filePath, messageDir, partDir);
1615
+ if (syncCtx) {
1616
+ if (!syncCtx.openCodeSessionState) {
1617
+ syncCtx.openCodeSessionState = new Map;
1618
+ }
1619
+ syncCtx.openCodeSessionState.set(result.canonical.sessionKey, {
1620
+ lastMessageAt: result.canonical.lastMessageAt,
1621
+ totalMessages: result.canonical.messages.length
1622
+ });
1623
+ }
1624
+ return [result];
1625
+ },
1626
+ buildCursor(fingerprint, _results) {
1627
+ const filePath = inodeToFilePath.get(fingerprint.inode);
1628
+ const messageDirMtimeMs = filePath && syncCtx?.openCodeMsgDirMtimes ? syncCtx.openCodeMsgDirMtimes[filePath] : undefined;
1629
+ return {
1630
+ inode: fingerprint.inode,
1631
+ mtimeMs: fingerprint.mtimeMs,
1632
+ size: fingerprint.size,
1633
+ messageDirMtimeMs,
1634
+ updatedAt: new Date().toISOString()
1635
+ };
1636
+ }
1637
+ };
1638
+ }
1639
+ var init_opencode2 = __esm(() => {
1640
+ init_opencode();
1641
+ });
1642
+
1643
+ // src/parsers/vscode-copilot.ts
1644
+ import { readFile as readFile4, stat as stat9 } from "fs/promises";
1645
+ import { dirname as dirname3, join as join8 } from "path";
1646
+ function toNonNegInt5(value) {
1647
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
1648
+ return 0;
1649
+ return Math.floor(value);
1650
+ }
1651
+ function normalizeTimestamp(value) {
1652
+ if (typeof value === "string") {
1653
+ const d = new Date(value);
1654
+ return Number.isNaN(d.getTime()) ? null : value;
1655
+ }
1656
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1657
+ return new Date(value).toISOString();
1658
+ }
1659
+ return null;
1660
+ }
1661
+ function applySnapshot(op, state) {
1662
+ const v = op.v;
1663
+ if (!v || typeof v !== "object")
1664
+ return;
1665
+ if (typeof v.sessionId === "string") {
1666
+ state.sessionId = v.sessionId;
1667
+ }
1668
+ if (typeof v.creationDate === "string") {
1669
+ state.creationDate = v.creationDate;
1670
+ }
1671
+ if (typeof v.customTitle === "string") {
1672
+ state.customTitle = v.customTitle;
1673
+ }
1674
+ if (Array.isArray(v.requests)) {
1675
+ for (const req of v.requests) {
1676
+ if (req && typeof req === "object") {
1677
+ state.requests.push(normalizeRequest(req));
1678
+ }
1679
+ }
1680
+ }
1681
+ const inputState = v.inputState;
1682
+ if (inputState && typeof inputState.selectedModel === "string") {
1683
+ state.selectedModel = inputState.selectedModel;
1684
+ }
1685
+ }
1686
+ function normalizeRequest(raw) {
1687
+ return {
1688
+ requestId: typeof raw.requestId === "string" ? raw.requestId : undefined,
1689
+ timestamp: typeof raw.timestamp === "number" ? raw.timestamp : undefined,
1690
+ modelId: typeof raw.modelId === "string" ? raw.modelId : undefined,
1691
+ message: raw.message,
1692
+ response: Array.isArray(raw.response) ? raw.response : [],
1693
+ result: raw.result,
1694
+ modelState: raw.modelState
1695
+ };
1696
+ }
1697
+ function applySet(op, state) {
1698
+ const k = op.k;
1699
+ if (!Array.isArray(k) || k.length === 0)
1700
+ return;
1701
+ if (k.length === 1 && k[0] === "customTitle") {
1702
+ if (typeof op.v === "string") {
1703
+ state.customTitle = op.v;
1704
+ }
1705
+ return;
1706
+ }
1707
+ if (k.length === 2 && k[0] === "inputState" && k[1] === "selectedModel") {
1708
+ if (typeof op.v === "string") {
1709
+ state.selectedModel = op.v;
1710
+ }
1711
+ return;
1712
+ }
1713
+ if (k.length === 3 && k[0] === "requests" && typeof k[1] === "number") {
1714
+ const idx = k[1];
1715
+ if (idx < 0 || idx >= state.requests.length)
1716
+ return;
1717
+ const field = k[2];
1718
+ if (field === "result") {
1719
+ state.requests[idx].result = op.v;
1720
+ } else if (field === "modelState") {
1721
+ state.requests[idx].modelState = op.v;
1722
+ }
1723
+ return;
1724
+ }
1725
+ }
1726
+ function applyAppend(op, state) {
1727
+ const k = op.k;
1728
+ if (!Array.isArray(k) || k.length === 0)
1729
+ return;
1730
+ if (k.length === 1 && k[0] === "requests") {
1731
+ if (op.v && typeof op.v === "object" && !Array.isArray(op.v)) {
1732
+ state.requests.push(normalizeRequest(op.v));
1733
+ }
1734
+ return;
1735
+ }
1736
+ if (k.length === 3 && k[0] === "requests" && typeof k[1] === "number" && k[2] === "response") {
1737
+ const idx = k[1];
1738
+ if (idx < 0 || idx >= state.requests.length)
1739
+ return;
1740
+ const req = state.requests[idx];
1741
+ if (!req.response)
1742
+ req.response = [];
1743
+ if (op.v && typeof op.v === "object") {
1744
+ req.response.push(op.v);
1745
+ }
1746
+ return;
1747
+ }
1748
+ }
1749
+ function replayCrdt(ops) {
1750
+ const state = {
1751
+ sessionId: null,
1752
+ creationDate: null,
1753
+ customTitle: null,
1754
+ selectedModel: null,
1755
+ requests: []
1756
+ };
1757
+ for (const op of ops) {
1758
+ switch (op.kind) {
1759
+ case 0:
1760
+ applySnapshot(op, state);
1761
+ break;
1762
+ case 1:
1763
+ applySet(op, state);
1764
+ break;
1765
+ case 2:
1766
+ applyAppend(op, state);
1767
+ break;
1768
+ }
1769
+ }
1770
+ return state;
1771
+ }
1772
+ function extractRequestMessages(req, fallbackModel, accum) {
1773
+ const ts = req.timestamp ? new Date(req.timestamp).toISOString() : new Date().toISOString();
1774
+ const model = req.modelId ?? fallbackModel ?? undefined;
1775
+ if (req.modelId)
1776
+ accum.lastModel = req.modelId;
1777
+ const userText = req.message?.text;
1778
+ if (typeof userText === "string" && userText.length > 0) {
1779
+ accum.messages.push({
1780
+ role: "user",
1781
+ content: userText,
1782
+ timestamp: ts
1783
+ });
1784
+ }
1785
+ if (Array.isArray(req.response)) {
1786
+ const textParts = [];
1787
+ for (const chunk of req.response) {
1788
+ if (!chunk || typeof chunk !== "object")
1789
+ continue;
1790
+ if (chunk.kind === "thinking") {
1791
+ continue;
1792
+ }
1793
+ if (chunk.kind === "toolInvocationSerialized") {
1794
+ if (textParts.length > 0) {
1795
+ accum.messages.push({
1796
+ role: "assistant",
1797
+ content: textParts.join(""),
1798
+ model,
1799
+ timestamp: ts
1800
+ });
1801
+ textParts.length = 0;
1802
+ }
1803
+ accum.messages.push({
1804
+ role: "tool",
1805
+ content: "",
1806
+ toolName: typeof chunk.toolId === "string" ? chunk.toolId : undefined,
1807
+ toolInput: typeof chunk.invocationMessage === "string" ? chunk.invocationMessage : undefined,
1808
+ timestamp: ts
1809
+ });
1810
+ if (typeof chunk.result === "string" && chunk.result.length > 0) {
1811
+ accum.messages.push({
1812
+ role: "tool",
1813
+ content: chunk.result,
1814
+ toolName: typeof chunk.toolId === "string" ? chunk.toolId : undefined,
1815
+ toolResult: chunk.result,
1816
+ timestamp: ts
1817
+ });
1818
+ }
1819
+ continue;
1820
+ }
1821
+ if (typeof chunk.value === "string") {
1822
+ textParts.push(chunk.value);
1823
+ }
1824
+ }
1825
+ if (textParts.length > 0) {
1826
+ accum.messages.push({
1827
+ role: "assistant",
1828
+ content: textParts.join(""),
1829
+ model,
1830
+ timestamp: ts
1831
+ });
1832
+ }
1833
+ }
1834
+ if (req.result?.metadata) {
1835
+ accum.totalInputTokens += toNonNegInt5(req.result.metadata.promptTokens);
1836
+ accum.totalOutputTokens += toNonNegInt5(req.result.metadata.outputTokens);
1837
+ }
1838
+ }
1839
+ async function extractWorkspaceFolder(sessionFilePath) {
1840
+ try {
1841
+ const chatSessionsDir = dirname3(sessionFilePath);
1842
+ const workspaceDir = dirname3(chatSessionsDir);
1843
+ const workspaceJsonPath = join8(workspaceDir, "workspace.json");
1844
+ const content = await readFile4(workspaceJsonPath, "utf8");
1845
+ const data = JSON.parse(content);
1846
+ if (typeof data?.folder === "string") {
1847
+ const folder = data.folder.replace(/^file:\/\//, "");
1848
+ return decodeURIComponent(folder) || null;
1849
+ }
1850
+ return null;
1851
+ } catch {
1852
+ return null;
1853
+ }
1854
+ }
1855
+ function extractProjectRef5(folder) {
1856
+ if (!folder)
1857
+ return null;
1858
+ return hashProjectRef(folder);
1859
+ }
1860
+ function extractProjectName5(folder) {
1861
+ if (!folder)
1862
+ return null;
1863
+ const parts = folder.split("/").filter(Boolean);
1864
+ return parts.length > 0 ? parts[parts.length - 1] : null;
1865
+ }
1866
+ function buildParseResult5(state, accum, filePath, rawContent, workspaceFolder) {
1867
+ const sessionId = state.sessionId ?? "unknown";
1868
+ const sessionKey = `copilot:${sessionId}`;
1869
+ let startedAt;
1870
+ let lastMessageAt;
1871
+ if (state.creationDate) {
1872
+ startedAt = state.creationDate;
1873
+ } else if (state.requests.length > 0 && state.requests[0].timestamp) {
1874
+ startedAt = new Date(state.requests[0].timestamp).toISOString();
1875
+ } else {
1876
+ startedAt = new Date().toISOString();
1877
+ }
1878
+ const lastReq = state.requests[state.requests.length - 1];
1879
+ const normalizedCompletedAt = normalizeTimestamp(lastReq?.modelState?.completedAt);
1880
+ if (normalizedCompletedAt) {
1881
+ lastMessageAt = normalizedCompletedAt;
1882
+ } else if (lastReq?.timestamp) {
1883
+ lastMessageAt = new Date(lastReq.timestamp).toISOString();
1884
+ } else {
1885
+ lastMessageAt = startedAt;
1886
+ }
1887
+ const durationMs = new Date(lastMessageAt).getTime() - new Date(startedAt).getTime();
1888
+ const canonical = {
1889
+ sessionKey,
1890
+ source: "vscode-copilot",
1891
+ parserRevision: PARSER_REVISION,
1892
+ schemaVersion: SCHEMA_VERSION,
1893
+ startedAt,
1894
+ lastMessageAt,
1895
+ durationSeconds: Math.max(0, Math.floor(durationMs / 1000)),
1896
+ projectRef: extractProjectRef5(workspaceFolder),
1897
+ projectName: extractProjectName5(workspaceFolder),
1898
+ model: accum.lastModel ?? state.selectedModel,
1899
+ title: state.customTitle,
1900
+ messages: accum.messages,
1901
+ totalInputTokens: accum.totalInputTokens,
1902
+ totalOutputTokens: accum.totalOutputTokens,
1903
+ totalCachedTokens: 0,
1904
+ snapshotAt: new Date().toISOString()
1905
+ };
1906
+ const raw = {
1907
+ sessionKey,
1908
+ source: "vscode-copilot",
1909
+ parserRevision: PARSER_REVISION,
1910
+ collectedAt: new Date().toISOString(),
1911
+ sourceFiles: [
1912
+ {
1913
+ path: filePath,
1914
+ format: "jsonl",
1915
+ content: rawContent
1916
+ }
1917
+ ]
1918
+ };
1919
+ return { canonical, raw };
1920
+ }
1921
+ function buildEmptyResult4(filePath) {
1922
+ const now = new Date().toISOString();
1923
+ const sessionKey = "copilot:unknown";
1924
+ return {
1925
+ canonical: {
1926
+ sessionKey,
1927
+ source: "vscode-copilot",
1928
+ parserRevision: PARSER_REVISION,
1929
+ schemaVersion: SCHEMA_VERSION,
1930
+ startedAt: now,
1931
+ lastMessageAt: now,
1932
+ durationSeconds: 0,
1933
+ projectRef: null,
1934
+ projectName: null,
1935
+ model: null,
1936
+ title: null,
1937
+ messages: [],
1938
+ totalInputTokens: 0,
1939
+ totalOutputTokens: 0,
1940
+ totalCachedTokens: 0,
1941
+ snapshotAt: now
1942
+ },
1943
+ raw: {
1944
+ sessionKey,
1945
+ source: "vscode-copilot",
1946
+ parserRevision: PARSER_REVISION,
1947
+ collectedAt: now,
1948
+ sourceFiles: [{ path: filePath, format: "jsonl", content: "" }]
1949
+ }
1950
+ };
1951
+ }
1952
+ function parseCrdtOps(content) {
1953
+ const ops = [];
1954
+ const lines = content.split(`
1955
+ `);
1956
+ for (const line of lines) {
1957
+ const trimmed = line.trim();
1958
+ if (!trimmed)
1959
+ continue;
1960
+ try {
1961
+ const parsed = JSON.parse(trimmed);
1962
+ if (typeof parsed?.kind === "number") {
1963
+ ops.push(parsed);
1964
+ }
1965
+ } catch {}
1966
+ }
1967
+ return ops;
1968
+ }
1969
+ function extractMessages(state, processedIds = new Set) {
1970
+ const accum = {
1971
+ messages: [],
1972
+ totalInputTokens: 0,
1973
+ totalOutputTokens: 0,
1974
+ lastModel: state.selectedModel
1975
+ };
1976
+ const newRequestIds = [];
1977
+ for (const req of state.requests) {
1978
+ const id = req.requestId;
1979
+ extractRequestMessages(req, state.selectedModel, accum);
1980
+ if (id && !processedIds.has(id))
1981
+ newRequestIds.push(id);
1982
+ }
1983
+ return { accum, newRequestIds };
1984
+ }
1985
+ async function parseVscodeCopilotFile(filePath, startOffset = 0, processedRequestIds = [], workspaceFolder = null) {
1986
+ const st = await stat9(filePath).catch(() => null);
1987
+ if (!st || !st.isFile() || st.size === 0)
1988
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
1989
+ if (startOffset >= st.size)
1990
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
1991
+ let rawContent;
1992
+ try {
1993
+ rawContent = await readFile4(filePath, "utf8");
1994
+ } catch {
1995
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
1996
+ }
1997
+ const ops = parseCrdtOps(rawContent);
1998
+ if (ops.length === 0)
1999
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
2000
+ const state = replayCrdt(ops);
2001
+ if (state.requests.length === 0)
2002
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
2003
+ const processedSet = new Set(processedRequestIds);
2004
+ const { accum, newRequestIds } = extractMessages(state, processedSet);
2005
+ if (accum.messages.length === 0)
2006
+ return { ...buildEmptyResult4(filePath), newRequestIds: [] };
2007
+ const folder = workspaceFolder ?? await extractWorkspaceFolder(filePath);
2008
+ return { ...buildParseResult5(state, accum, filePath, rawContent, folder), newRequestIds };
2009
+ }
2010
+ var init_vscode_copilot = __esm(() => {
2011
+ init_src();
2012
+ init_hash_project_ref();
2013
+ });
2014
+
2015
+ // src/drivers/session/vscode-copilot.ts
2016
+ import { readdir as readdir6 } from "fs/promises";
2017
+ import { join as join9 } from "path";
2018
+ async function collectJsonl(dir) {
2019
+ let entries;
2020
+ try {
2021
+ entries = await readdir6(dir, { withFileTypes: true });
2022
+ } catch {
2023
+ return [];
2024
+ }
2025
+ const results = [];
2026
+ for (const entry of entries) {
2027
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2028
+ results.push(join9(dir, entry.name));
2029
+ }
2030
+ }
2031
+ return results;
2032
+ }
2033
+ async function discoverInBaseDir(baseDir) {
2034
+ const results = [];
2035
+ const wsDir = join9(baseDir, "workspaceStorage");
2036
+ try {
2037
+ const wsDirEntries = await readdir6(wsDir, { withFileTypes: true });
2038
+ for (const entry of wsDirEntries) {
2039
+ if (!entry.isDirectory())
2040
+ continue;
2041
+ const chatDir = join9(wsDir, entry.name, "chatSessions");
2042
+ const files = await collectJsonl(chatDir);
2043
+ results.push(...files);
2044
+ }
2045
+ } catch {}
2046
+ const globalDir = join9(baseDir, "globalStorage", "emptyWindowChatSessions");
2047
+ const globalFiles = await collectJsonl(globalDir);
2048
+ results.push(...globalFiles);
2049
+ return results;
2050
+ }
2051
+ var lastNewRequestIds, lastProcessedRequestIds, vscodeCopilotSessionDriver;
2052
+ var init_vscode_copilot2 = __esm(() => {
2053
+ init_vscode_copilot();
2054
+ lastNewRequestIds = [];
2055
+ lastProcessedRequestIds = [];
2056
+ vscodeCopilotSessionDriver = {
2057
+ source: "vscode-copilot",
2058
+ async discover(opts) {
2059
+ if (!opts.vscodeCopilotDirs || opts.vscodeCopilotDirs.length === 0) {
2060
+ return [];
2061
+ }
2062
+ const results = [];
2063
+ for (const baseDir of opts.vscodeCopilotDirs) {
2064
+ const files = await discoverInBaseDir(baseDir);
2065
+ results.push(...files);
2066
+ }
2067
+ return results;
2068
+ },
2069
+ shouldSkip(cursor, fingerprint) {
2070
+ return fileUnchanged(cursor, fingerprint);
2071
+ },
2072
+ resumeState(cursor, fingerprint) {
2073
+ if (!cursor || cursor.inode !== fingerprint.inode) {
2074
+ return { kind: "vscode-copilot", startOffset: 0, processedRequestIds: [] };
2075
+ }
2076
+ if (cursor.offset > fingerprint.size) {
2077
+ return { kind: "vscode-copilot", startOffset: 0, processedRequestIds: [] };
2078
+ }
2079
+ return {
2080
+ kind: "vscode-copilot",
2081
+ startOffset: cursor.offset,
2082
+ processedRequestIds: cursor.processedRequestIds
2083
+ };
2084
+ },
2085
+ async parse(filePath, resume) {
2086
+ const workspaceFolder = await extractWorkspaceFolder(filePath);
2087
+ const result = await parseVscodeCopilotFile(filePath, resume.startOffset, resume.processedRequestIds, workspaceFolder);
2088
+ lastNewRequestIds = result.newRequestIds;
2089
+ lastProcessedRequestIds = resume.processedRequestIds;
2090
+ if (result.canonical.messages.length === 0)
2091
+ return [];
2092
+ return [result];
2093
+ },
2094
+ buildCursor(fingerprint, _results) {
2095
+ const allIds = [...lastProcessedRequestIds, ...lastNewRequestIds];
2096
+ return {
2097
+ inode: fingerprint.inode,
2098
+ mtimeMs: fingerprint.mtimeMs,
2099
+ size: fingerprint.size,
2100
+ offset: fingerprint.size,
2101
+ processedRequestIds: allIds,
2102
+ updatedAt: new Date().toISOString()
2103
+ };
2104
+ }
2105
+ };
2106
+ });
2107
+
2108
+ // src/drivers/registry.ts
2109
+ import { stat as stat11 } from "fs/promises";
2110
+ import { homedir } from "os";
2111
+ import { join as join10 } from "path";
2112
+ function resolveDefaultPaths(home) {
2113
+ const h = home ?? homedir();
2114
+ return {
2115
+ claudeDir: join10(h, ".claude"),
2116
+ codexSessionsDir: join10(h, ".codex", "sessions"),
2117
+ geminiDir: join10(h, ".gemini"),
2118
+ openCodeDir: join10(h, ".local", "share", "opencode"),
2119
+ vscodeCopilotDirs: [
2120
+ join10(h, "Library", "Application Support", "Code", "User"),
2121
+ join10(h, "Library", "Application Support", "Code - Insiders", "User")
2122
+ ]
2123
+ };
2124
+ }
2125
+ async function exists(path) {
2126
+ try {
2127
+ await stat11(path);
2128
+ return true;
2129
+ } catch {
2130
+ return false;
2131
+ }
2132
+ }
2133
+ async function buildDriverSet(overrides, syncCtx, sourceFilter) {
2134
+ const paths = { ...resolveDefaultPaths(), ...overrides };
2135
+ const [
2136
+ claudeExists,
2137
+ codexExists,
2138
+ geminiExists,
2139
+ openCodeExists,
2140
+ openCodeDbExists,
2141
+ ...vscodeDirExists
2142
+ ] = await Promise.all([
2143
+ exists(paths.claudeDir),
2144
+ exists(paths.codexSessionsDir),
2145
+ exists(paths.geminiDir),
2146
+ exists(paths.openCodeDir),
2147
+ exists(join10(paths.openCodeDir, "opencode.db")),
2148
+ ...paths.vscodeCopilotDirs.map((d) => exists(d))
2149
+ ]);
2150
+ const discoverOpts = {};
2151
+ if (claudeExists)
2152
+ discoverOpts.claudeDir = paths.claudeDir;
2153
+ if (codexExists)
2154
+ discoverOpts.codexSessionsDir = paths.codexSessionsDir;
2155
+ if (geminiExists)
2156
+ discoverOpts.geminiDir = paths.geminiDir;
2157
+ if (openCodeExists) {
2158
+ discoverOpts.openCodeMessageDir = join10(paths.openCodeDir, "storage", "message");
2159
+ }
2160
+ if (openCodeDbExists) {
2161
+ discoverOpts.openCodeDbPath = join10(paths.openCodeDir, "opencode.db");
2162
+ }
2163
+ const activeVscodeDirs = paths.vscodeCopilotDirs.filter((_, i) => vscodeDirExists[i]);
2164
+ if (activeVscodeDirs.length > 0) {
2165
+ discoverOpts.vscodeCopilotDirs = activeVscodeDirs;
2166
+ }
2167
+ const allowed = (source) => !sourceFilter || sourceFilter.has(source);
2168
+ const fileDrivers = [];
2169
+ if (claudeExists && allowed("claude-code"))
2170
+ fileDrivers.push(claudeSessionDriver);
2171
+ if (codexExists && allowed("codex"))
2172
+ fileDrivers.push(codexSessionDriver);
2173
+ if (geminiExists && allowed("gemini-cli"))
2174
+ fileDrivers.push(geminiSessionDriver);
2175
+ if (openCodeExists && allowed("opencode"))
2176
+ fileDrivers.push(createOpenCodeJsonDriver(syncCtx));
2177
+ if (activeVscodeDirs.length > 0 && allowed("vscode-copilot")) {
2178
+ fileDrivers.push(vscodeCopilotSessionDriver);
2179
+ }
2180
+ return {
2181
+ fileDrivers,
2182
+ dbDriversAvailable: openCodeDbExists && allowed("opencode"),
2183
+ discoverOpts,
2184
+ paths
2185
+ };
2186
+ }
2187
+ var init_registry = __esm(() => {
2188
+ init_claude2();
2189
+ init_codex2();
2190
+ init_gemini2();
2191
+ init_opencode2();
2192
+ init_vscode_copilot2();
2193
+ });
2194
+
2195
+ // src/drivers/session/opencode-sqlite.ts
2196
+ import { stat as stat12 } from "fs/promises";
2197
+ function shouldSkipForJson(sessionKey, sqliteLastMessageAt, sqliteTotalMessages, jsonState) {
2198
+ if (!jsonState)
2199
+ return false;
2200
+ const info = jsonState.get(sessionKey);
2201
+ if (!info)
2202
+ return false;
2203
+ return info.lastMessageAt >= sqliteLastMessageAt && info.totalMessages >= sqliteTotalMessages;
2204
+ }
2205
+ function querySessions(db) {
2206
+ const rows = db.prepare("SELECT * FROM session ORDER BY rowid").all();
2207
+ const sessions = [];
2208
+ const rawRows = [];
2209
+ for (const row of rows) {
2210
+ if (!row.id)
2211
+ continue;
2212
+ const session = {
2213
+ id: row.id,
2214
+ projectID: row.project_id,
2215
+ directory: row.directory,
2216
+ title: row.title,
2217
+ time: {
2218
+ created: row.time_created,
2219
+ updated: row.time_updated
2220
+ }
2221
+ };
2222
+ sessions.push(session);
2223
+ rawRows.push(row);
2224
+ }
2225
+ return { sessions, rawRows };
2226
+ }
2227
+ function queryMessagesForSession(db, sessionId, watermark, lastMessageIds) {
2228
+ let rows;
2229
+ if (watermark) {
2230
+ rows = db.prepare("SELECT id, session_id, data, time_created FROM message WHERE session_id = ? AND time_created >= ? ORDER BY time_created").all(sessionId, watermark);
2231
+ } else {
2232
+ rows = db.prepare("SELECT id, session_id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created").all(sessionId);
2233
+ }
2234
+ const messages = [];
2235
+ for (const row of rows) {
2236
+ if (lastMessageIds?.has(row.id))
2237
+ continue;
2238
+ try {
2239
+ const data = JSON.parse(row.data);
2240
+ if (!data || typeof data.role !== "string")
2241
+ continue;
2242
+ if (!data.id)
2243
+ data.id = row.id;
2244
+ messages.push(data);
2245
+ } catch {
2246
+ continue;
2247
+ }
2248
+ }
2249
+ return messages;
2250
+ }
2251
+ function queryAllMessagesForSession(db, sessionId) {
2252
+ const rows = db.prepare("SELECT id, session_id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created").all(sessionId);
2253
+ const messages = [];
2254
+ const rawDataStrings = [];
2255
+ for (const row of rows) {
2256
+ try {
2257
+ const data = JSON.parse(row.data);
2258
+ if (!data || typeof data.role !== "string")
2259
+ continue;
2260
+ if (!data.id)
2261
+ data.id = row.id;
2262
+ messages.push(data);
2263
+ rawDataStrings.push(row.data);
2264
+ } catch {
2265
+ continue;
2266
+ }
2267
+ }
2268
+ return { messages, rawDataStrings };
2269
+ }
2270
+ function queryPartsForMessageWithRaw(db, messageId) {
2271
+ const rows = db.prepare("SELECT data, message_id FROM part WHERE message_id = ? ORDER BY rowid").all(messageId);
2272
+ const parts = [];
2273
+ const rawDataStrings = [];
2274
+ for (const row of rows) {
2275
+ try {
2276
+ const data = JSON.parse(row.data);
2277
+ if (!data || typeof data.type !== "string")
2278
+ continue;
2279
+ parts.push(data);
2280
+ rawDataStrings.push(row.data);
2281
+ } catch {
2282
+ continue;
2283
+ }
2284
+ }
2285
+ return { parts, rawDataStrings };
2286
+ }
2287
+ function buildRawSourceFiles(session, sessionRow, messageRawStrings, messageIds, partRawStrings, dbPath) {
2288
+ const files = [];
2289
+ files.push({
2290
+ path: `${dbPath}#session/${session.id}`,
2291
+ format: "sqlite-export",
2292
+ content: JSON.stringify(sessionRow)
2293
+ });
2294
+ for (let i = 0;i < messageIds.length; i++) {
2295
+ const msgId = messageIds[i];
2296
+ files.push({
2297
+ path: `${dbPath}#message/${msgId}`,
2298
+ format: "sqlite-export",
2299
+ content: messageRawStrings[i]
2300
+ });
2301
+ const partStrings = partRawStrings.get(msgId);
2302
+ if (partStrings) {
2303
+ for (let j = 0;j < partStrings.length; j++) {
2304
+ files.push({
2305
+ path: `${dbPath}#part/${msgId}/${j}`,
2306
+ format: "sqlite-export",
2307
+ content: partStrings[j]
2308
+ });
2309
+ }
2310
+ }
2311
+ }
2312
+ return files;
2313
+ }
2314
+ function createOpenCodeSqliteDriver(openDb, dbPath) {
2315
+ return {
2316
+ source: "opencode",
2317
+ async run(prevCursor, ctx) {
2318
+ let dbStat;
2319
+ try {
2320
+ dbStat = await stat12(dbPath);
2321
+ } catch {
2322
+ return {
2323
+ results: [],
2324
+ cursor: prevCursor ?? {
2325
+ inode: 0,
2326
+ lastTimeCreated: "",
2327
+ lastMessageIds: [],
2328
+ updatedAt: new Date().toISOString()
2329
+ },
2330
+ rowCount: 0
2331
+ };
2332
+ }
2333
+ const inodeMatch = prevCursor && prevCursor.inode === dbStat.ino;
2334
+ const watermark = inodeMatch ? prevCursor.lastTimeCreated || null : null;
2335
+ const prevMessageIds = inodeMatch && prevCursor.lastMessageIds ? new Set(prevCursor.lastMessageIds) : null;
2336
+ let db;
2337
+ try {
2338
+ db = openDb(dbPath, { readonly: true });
2339
+ } catch {
2340
+ return {
2341
+ results: [],
2342
+ cursor: prevCursor ?? {
2343
+ inode: dbStat.ino,
2344
+ lastTimeCreated: "",
2345
+ lastMessageIds: [],
2346
+ updatedAt: new Date().toISOString()
2347
+ },
2348
+ rowCount: 0
2349
+ };
2350
+ }
2351
+ try {
2352
+ const { sessions, rawRows: sessionRawRows } = querySessions(db);
2353
+ const results = [];
2354
+ let totalRows = 0;
2355
+ let maxTimeCreated = watermark ?? "";
2356
+ for (let si = 0;si < sessions.length; si++) {
2357
+ const session = sessions[si];
2358
+ const sessionRawRow = sessionRawRows[si];
2359
+ const newMessages = queryMessagesForSession(db, session.id, watermark, prevMessageIds);
2360
+ totalRows += newMessages.length;
2361
+ if (newMessages.length === 0 && watermark) {
2362
+ continue;
2363
+ }
2364
+ const { messages: allMessages, rawDataStrings: messageRawStrings } = queryAllMessagesForSession(db, session.id);
2365
+ const messageIds = [];
2366
+ const partRawMap = new Map;
2367
+ for (const msg of allMessages) {
2368
+ const { parts, rawDataStrings: partRawStrings } = queryPartsForMessageWithRaw(db, msg.id);
2369
+ msg.parts = parts;
2370
+ messageIds.push(msg.id);
2371
+ if (partRawStrings.length > 0) {
2372
+ partRawMap.set(msg.id, partRawStrings);
2373
+ }
2374
+ }
2375
+ const rawSourceFiles = buildRawSourceFiles(session, sessionRawRow, messageRawStrings, messageIds, partRawMap, dbPath);
2376
+ const result = parseOpenCodeSqliteSession(session, allMessages, dbPath, rawSourceFiles);
2377
+ const sessionKey = result.canonical.sessionKey;
2378
+ if (shouldSkipForJson(sessionKey, result.canonical.lastMessageAt, result.canonical.messages.length, ctx.openCodeSessionState)) {
2379
+ continue;
2380
+ }
2381
+ results.push(result);
2382
+ if (!ctx.openCodeSessionState) {
2383
+ ctx.openCodeSessionState = new Map;
2384
+ }
2385
+ ctx.openCodeSessionState.set(sessionKey, {
2386
+ lastMessageAt: result.canonical.lastMessageAt,
2387
+ totalMessages: result.canonical.messages.length
2388
+ });
2389
+ for (const msg of newMessages) {
2390
+ const tc = msg.time?.created;
2391
+ if (typeof tc === "number" && tc > 0) {
2392
+ const iso = new Date(tc).toISOString();
2393
+ if (iso > maxTimeCreated) {
2394
+ maxTimeCreated = iso;
2395
+ }
2396
+ }
2397
+ }
2398
+ }
2399
+ const boundaryIds = [];
2400
+ if (maxTimeCreated) {
2401
+ for (const session of sessions) {
2402
+ const rows = db.prepare("SELECT id FROM message WHERE session_id = ? AND time_created = ?").all(session.id, maxTimeCreated);
2403
+ for (const row of rows) {
2404
+ boundaryIds.push(row.id);
2405
+ }
2406
+ }
2407
+ }
2408
+ const newCursor = {
2409
+ inode: dbStat.ino,
2410
+ lastTimeCreated: maxTimeCreated,
2411
+ lastMessageIds: boundaryIds,
2412
+ updatedAt: new Date().toISOString()
2413
+ };
2414
+ return { results, cursor: newCursor, rowCount: totalRows };
2415
+ } finally {
2416
+ try {
2417
+ db.close();
2418
+ } catch {}
2419
+ }
2420
+ }
2421
+ };
2422
+ }
2423
+ var init_opencode_sqlite = __esm(() => {
2424
+ init_opencode();
2425
+ });
2426
+
2427
+ // src/upload/engine.ts
2428
+ import { createHash as createHash2 } from "crypto";
2429
+ function sha256(input) {
2430
+ return createHash2("sha256").update(input, "utf-8").digest("hex");
2431
+ }
2432
+ function toSessionSnapshot(canonical, raw) {
2433
+ const canonicalJson = JSON.stringify(canonical);
2434
+ const rawJson = JSON.stringify(raw);
2435
+ const contentHash = sha256(canonicalJson);
2436
+ const rawHash = sha256(rawJson);
2437
+ let userMessages = 0;
2438
+ let assistantMessages = 0;
2439
+ for (const msg of canonical.messages) {
2440
+ if (msg.role === "user")
2441
+ userMessages++;
2442
+ else if (msg.role === "assistant")
2443
+ assistantMessages++;
2444
+ }
2445
+ const snapshot = {
2446
+ sessionKey: canonical.sessionKey,
2447
+ source: canonical.source,
2448
+ startedAt: canonical.startedAt,
2449
+ lastMessageAt: canonical.lastMessageAt,
2450
+ durationSeconds: canonical.durationSeconds,
2451
+ userMessages,
2452
+ assistantMessages,
2453
+ totalMessages: canonical.messages.length,
2454
+ totalInputTokens: canonical.totalInputTokens,
2455
+ totalOutputTokens: canonical.totalOutputTokens,
2456
+ totalCachedTokens: canonical.totalCachedTokens,
2457
+ projectRef: canonical.projectRef,
2458
+ projectName: canonical.projectName,
2459
+ model: canonical.model,
2460
+ title: canonical.title,
2461
+ contentHash,
2462
+ rawHash,
2463
+ parserRevision: canonical.parserRevision,
2464
+ schemaVersion: canonical.schemaVersion,
2465
+ snapshotAt: canonical.snapshotAt
2466
+ };
2467
+ return {
2468
+ snapshot,
2469
+ precomputed: { canonicalJson, rawJson, contentHash, rawHash }
2470
+ };
2471
+ }
2472
+ function splitBatches(items, size) {
2473
+ const batches = [];
2474
+ for (let i = 0;i < items.length; i += size) {
2475
+ batches.push(items.slice(i, i + size));
2476
+ }
2477
+ return batches;
2478
+ }
2479
+ function defaultSleep(ms) {
2480
+ return new Promise((resolve) => setTimeout(resolve, ms));
2481
+ }
2482
+ function parseRetryAfter(value) {
2483
+ if (!value)
2484
+ return INITIAL_BACKOFF_MS;
2485
+ const seconds = parseInt(value, 10);
2486
+ if (!isNaN(seconds) && seconds >= 0) {
2487
+ return seconds * 1000;
2488
+ }
2489
+ const date = new Date(value);
2490
+ if (!isNaN(date.getTime())) {
2491
+ const ms = date.getTime() - Date.now();
2492
+ return Math.max(0, ms);
2493
+ }
2494
+ return INITIAL_BACKOFF_MS;
2495
+ }
2496
+ async function uploadBatch(snapshots, opts) {
2497
+ const fetchFn = opts.fetch ?? globalThis.fetch;
2498
+ const sleepFn = opts.sleep ?? defaultSleep;
2499
+ const url = `${opts.apiUrl}/api/ingest/sessions`;
2500
+ let lastStatus = 0;
2501
+ for (let attempt = 0;attempt <= MAX_UPLOAD_RETRIES; attempt++) {
2502
+ const response = await fetchFn(url, {
2503
+ method: "POST",
2504
+ headers: {
2505
+ "Content-Type": "application/json",
2506
+ Authorization: `Bearer ${opts.apiKey}`
2507
+ },
2508
+ body: JSON.stringify({ userId: opts.userId, sessions: snapshots })
2509
+ });
2510
+ lastStatus = response.status;
2511
+ if (response.ok) {
2512
+ const body = await response.json();
2513
+ return { ingested: body.ingested, conflicts: 0, errors: [] };
2514
+ }
2515
+ if (response.status === 401) {
2516
+ throw new AuthError;
2517
+ }
2518
+ if (response.status === 409) {
2519
+ const body = await response.json();
2520
+ return {
2521
+ ingested: 0,
2522
+ conflicts: body.conflicts?.length ?? snapshots.length,
2523
+ errors: []
2524
+ };
2525
+ }
2526
+ if (response.status === 429) {
2527
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
2528
+ await sleepFn(retryAfter);
2529
+ continue;
2530
+ }
2531
+ if (response.status >= 400 && response.status < 500) {
2532
+ const body = await response.text();
2533
+ throw new ClientError(response.status, body);
2534
+ }
2535
+ if (response.status >= 500) {
2536
+ if (attempt < MAX_UPLOAD_RETRIES) {
2537
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
2538
+ await sleepFn(backoff);
2539
+ continue;
2540
+ }
2541
+ }
2542
+ }
2543
+ throw new RetryExhaustedError(lastStatus, MAX_UPLOAD_RETRIES + 1);
2544
+ }
2545
+ async function uploadMetadataBatches(snapshots, opts) {
2546
+ if (snapshots.length === 0) {
2547
+ return { totalIngested: 0, totalConflicts: 0, totalBatches: 0, errors: [] };
2548
+ }
2549
+ const batches = splitBatches(snapshots, METADATA_BATCH_SIZE);
2550
+ const result = {
2551
+ totalIngested: 0,
2552
+ totalConflicts: 0,
2553
+ totalBatches: batches.length,
2554
+ errors: []
2555
+ };
2556
+ let nextIndex = 0;
2557
+ async function worker() {
2558
+ while (true) {
2559
+ const idx = nextIndex++;
2560
+ if (idx >= batches.length)
2561
+ break;
2562
+ const batchResult = await uploadBatch(batches[idx], opts);
2563
+ result.totalIngested += batchResult.ingested;
2564
+ result.totalConflicts += batchResult.conflicts;
2565
+ result.errors.push(...batchResult.errors);
2566
+ }
2567
+ }
2568
+ const workers = Array.from({ length: Math.min(METADATA_BATCH_CONCURRENCY, batches.length) }, () => worker());
2569
+ await Promise.all(workers);
2570
+ return result;
2571
+ }
2572
+ var AuthError, RetryExhaustedError, ClientError, METADATA_BATCH_CONCURRENCY = 4;
2573
+ var init_engine = __esm(() => {
2574
+ init_src();
2575
+ AuthError = class AuthError extends Error {
2576
+ constructor(message = "API key invalid or expired. Run: pika login --force") {
2577
+ super(message);
2578
+ this.name = "AuthError";
2579
+ }
2580
+ };
2581
+ RetryExhaustedError = class RetryExhaustedError extends Error {
2582
+ statusCode;
2583
+ attempts;
2584
+ constructor(statusCode, attempts) {
2585
+ super(`Upload failed after ${attempts} attempts (last status: ${statusCode})`);
2586
+ this.statusCode = statusCode;
2587
+ this.attempts = attempts;
2588
+ this.name = "RetryExhaustedError";
2589
+ }
2590
+ };
2591
+ ClientError = class ClientError extends Error {
2592
+ statusCode;
2593
+ body;
2594
+ constructor(statusCode, body) {
2595
+ super(`Upload failed with status ${statusCode}: ${body}`);
2596
+ this.statusCode = statusCode;
2597
+ this.body = body;
2598
+ this.name = "ClientError";
2599
+ }
2600
+ };
2601
+ });
2602
+
2603
+ // src/upload/content.ts
2604
+ import { gzip } from "zlib";
2605
+ import { promisify } from "util";
2606
+ async function gzipCompress(input) {
2607
+ return gzipAsync(Buffer.from(input, "utf-8"));
2608
+ }
2609
+ function defaultSleep2(ms) {
2610
+ return new Promise((resolve) => setTimeout(resolve, ms));
2611
+ }
2612
+ async function putWithRetry(url, body, headers, opts) {
2613
+ const fetchFn = opts.fetch ?? globalThis.fetch;
2614
+ const sleepFn = opts.sleep ?? defaultSleep2;
2615
+ let lastStatus = 0;
2616
+ for (let attempt = 0;attempt <= MAX_UPLOAD_RETRIES; attempt++) {
2617
+ const response = await fetchFn(url, {
2618
+ method: "PUT",
2619
+ headers: {
2620
+ "Content-Type": "application/octet-stream",
2621
+ "Content-Encoding": "gzip",
2622
+ Authorization: `Bearer ${opts.apiKey}`,
2623
+ ...headers
2624
+ },
2625
+ body
2626
+ });
2627
+ lastStatus = response.status;
2628
+ if (response.status === 200 || response.status === 201) {
2629
+ return { uploaded: true };
2630
+ }
2631
+ if (response.status === 204) {
2632
+ return { uploaded: false };
2633
+ }
2634
+ if (response.status === 401) {
2635
+ throw new AuthError;
2636
+ }
2637
+ if (response.status === 409) {
2638
+ const text = await response.text();
2639
+ throw new ClientError(409, text);
2640
+ }
2641
+ if (response.status === 429) {
2642
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
2643
+ await sleepFn(retryAfter);
2644
+ continue;
2645
+ }
2646
+ if (response.status >= 400 && response.status < 500) {
2647
+ const text = await response.text();
2648
+ throw new ClientError(response.status, text);
2649
+ }
2650
+ if (response.status >= 500) {
2651
+ if (attempt < MAX_UPLOAD_RETRIES) {
2652
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
2653
+ await sleepFn(backoff);
2654
+ continue;
2655
+ }
2656
+ }
2657
+ }
2658
+ throw new RetryExhaustedError(lastStatus, MAX_UPLOAD_RETRIES + 1);
2659
+ }
2660
+ async function requestPresignedUrl(sessionKey, rawHash, opts) {
2661
+ const fetchFn = opts.fetch ?? globalThis.fetch;
2662
+ const response = await fetchFn(`${opts.apiUrl}/api/ingest/presign`, {
2663
+ method: "POST",
2664
+ headers: {
2665
+ "Content-Type": "application/json",
2666
+ Authorization: `Bearer ${opts.apiKey}`
2667
+ },
2668
+ body: JSON.stringify({ sessionKey, rawHash })
2669
+ });
2670
+ if (response.status === 401)
2671
+ throw new AuthError;
2672
+ if (!response.ok) {
2673
+ const text = await response.text();
2674
+ throw new ClientError(response.status, text);
2675
+ }
2676
+ const data = await response.json();
2677
+ if (!data.url || !data.key) {
2678
+ throw new ClientError(response.status, "Invalid presign response: missing url or key");
2679
+ }
2680
+ return data;
2681
+ }
2682
+ async function uploadToPresignedUrl(presignedUrl, body, opts) {
2683
+ const fetchFn = opts.fetch ?? globalThis.fetch;
2684
+ const sleepFn = opts.sleep ?? defaultSleep2;
2685
+ let lastStatus = 0;
2686
+ for (let attempt = 0;attempt <= MAX_UPLOAD_RETRIES; attempt++) {
2687
+ const response = await fetchFn(presignedUrl, {
2688
+ method: "PUT",
2689
+ headers: {
2690
+ "Content-Type": "application/gzip"
2691
+ },
2692
+ body
2693
+ });
2694
+ lastStatus = response.status;
2695
+ if (response.status === 200 || response.status === 201) {
2696
+ return;
2697
+ }
2698
+ if (response.status >= 500) {
2699
+ if (attempt < MAX_UPLOAD_RETRIES) {
2700
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
2701
+ await sleepFn(backoff);
2702
+ continue;
2703
+ }
2704
+ }
2705
+ if (response.status >= 400 && response.status < 500) {
2706
+ const text = await response.text();
2707
+ throw new ClientError(response.status, text);
2708
+ }
2709
+ }
2710
+ throw new RetryExhaustedError(lastStatus, MAX_UPLOAD_RETRIES + 1);
2711
+ }
2712
+ async function confirmRawUpload(sessionKey, rawHash, rawSize, opts) {
2713
+ const fetchFn = opts.fetch ?? globalThis.fetch;
2714
+ const response = await fetchFn(`${opts.apiUrl}/api/ingest/confirm-raw`, {
2715
+ method: "POST",
2716
+ headers: {
2717
+ "Content-Type": "application/json",
2718
+ Authorization: `Bearer ${opts.apiKey}`
2719
+ },
2720
+ body: JSON.stringify({ sessionKey, rawHash, rawSize })
2721
+ });
2722
+ if (response.status === 401)
2723
+ throw new AuthError;
2724
+ if (!response.ok) {
2725
+ const text = await response.text();
2726
+ throw new ClientError(response.status, text);
2727
+ }
2728
+ }
2729
+ async function uploadRawDirect(sessionKey, rawHash, rawGzip, opts) {
2730
+ const presign = await requestPresignedUrl(sessionKey, rawHash, opts);
2731
+ await uploadToPresignedUrl(presign.url, rawGzip, opts);
2732
+ await confirmRawUpload(sessionKey, rawHash, rawGzip.length, opts);
2733
+ return true;
2734
+ }
2735
+ async function uploadSessionContent(canonical, raw, opts, precomputed) {
2736
+ const canonicalJson = precomputed?.canonicalJson ?? JSON.stringify(canonical);
2737
+ const rawJson = precomputed?.rawJson ?? JSON.stringify(raw);
2738
+ const contentHash = precomputed?.contentHash ?? sha256(canonicalJson);
2739
+ const rawHash = precomputed?.rawHash ?? sha256(rawJson);
2740
+ const [canonicalGzip, rawGzip] = await Promise.all([
2741
+ gzipCompress(canonicalJson),
2742
+ gzipCompress(rawJson)
2743
+ ]);
2744
+ const sessionKey = encodeURIComponent(canonical.sessionKey);
2745
+ const canonicalPromise = putWithRetry(`${opts.apiUrl}/api/ingest/content/${sessionKey}/canonical`, canonicalGzip, {
2746
+ "X-Content-Hash": contentHash,
2747
+ "X-Parser-Revision": String(canonical.parserRevision),
2748
+ "X-Schema-Version": String(canonical.schemaVersion)
2749
+ }, opts);
2750
+ const rawPromise = (async () => {
2751
+ try {
2752
+ return await uploadRawDirect(canonical.sessionKey, rawHash, rawGzip, opts);
2753
+ } catch (err) {
2754
+ if (err instanceof AuthError)
2755
+ throw err;
2756
+ const rawResult = await putWithRetry(`${opts.apiUrl}/api/ingest/content/${sessionKey}/raw`, rawGzip, { "X-Raw-Hash": rawHash }, opts);
2757
+ return rawResult.uploaded;
2758
+ }
2759
+ })();
2760
+ const [canonicalResult, rawUploaded] = await Promise.all([
2761
+ canonicalPromise,
2762
+ rawPromise
2763
+ ]);
2764
+ return {
2765
+ canonicalUploaded: canonicalResult.uploaded,
2766
+ rawUploaded,
2767
+ contentHash,
2768
+ rawHash
2769
+ };
2770
+ }
2771
+ async function uploadContentBatch(sessions, opts, concurrency = CONTENT_UPLOAD_CONCURRENCY) {
2772
+ const result = {
2773
+ uploaded: 0,
2774
+ skipped: 0,
2775
+ errors: []
2776
+ };
2777
+ if (sessions.length === 0)
2778
+ return result;
2779
+ let nextIndex = 0;
2780
+ let authError = null;
2781
+ async function worker() {
2782
+ while (!authError) {
2783
+ const idx = nextIndex++;
2784
+ if (idx >= sessions.length)
2785
+ break;
2786
+ const { canonical, raw, precomputed } = sessions[idx];
2787
+ try {
2788
+ const contentResult = await uploadSessionContent(canonical, raw, opts, precomputed);
2789
+ if (contentResult.canonicalUploaded || contentResult.rawUploaded) {
2790
+ result.uploaded++;
2791
+ } else {
2792
+ result.skipped++;
2793
+ }
2794
+ } catch (err) {
2795
+ if (err instanceof AuthError) {
2796
+ authError = err;
2797
+ return;
2798
+ }
2799
+ result.errors.push({
2800
+ sessionKey: canonical.sessionKey,
2801
+ error: err instanceof Error ? err.message : String(err)
2802
+ });
2803
+ }
2804
+ }
2805
+ }
2806
+ const workers = Array.from({ length: Math.min(concurrency, sessions.length) }, () => worker());
2807
+ await Promise.all(workers);
2808
+ if (authError)
2809
+ throw authError;
2810
+ return result;
2811
+ }
2812
+ var gzipAsync;
2813
+ var init_content = __esm(() => {
2814
+ init_src();
2815
+ init_engine();
2816
+ gzipAsync = promisify(gzip);
2817
+ });
2818
+
2819
+ // src/commands/sync-pipeline.ts
2820
+ import { stat as stat13 } from "fs/promises";
2821
+ async function getFingerprint(filePath) {
2822
+ const s = await stat13(filePath);
2823
+ return {
2824
+ inode: s.ino,
2825
+ mtimeMs: s.mtimeMs,
2826
+ size: s.size
2827
+ };
2828
+ }
2829
+ async function runSyncPipeline(input, opts) {
2830
+ const {
2831
+ fileDrivers,
2832
+ dbDriver,
2833
+ discoverOpts,
2834
+ syncCtx
2835
+ } = input;
2836
+ let cursorState = { ...input.cursorState, files: { ...input.cursorState.files } };
2837
+ const allResults = [];
2838
+ const parseErrors = [];
2839
+ let totalFiles = 0;
2840
+ let totalSkipped = 0;
2841
+ const sessionKeyToFile = new Map;
2842
+ const prevCursors = new Map;
2843
+ const dbSourcedSessionKeys = new Set;
2844
+ let prevDbCursor;
2845
+ for (const driver of fileDrivers) {
2846
+ const files = await driver.discover(discoverOpts);
2847
+ for (const filePath of files) {
2848
+ totalFiles++;
2849
+ let fingerprint;
2850
+ try {
2851
+ fingerprint = await getFingerprint(filePath);
2852
+ } catch {
2853
+ continue;
2854
+ }
2855
+ const existingCursor = cursorState.files[filePath];
2856
+ if (driver.shouldSkip(existingCursor, fingerprint)) {
2857
+ totalSkipped++;
2858
+ continue;
2859
+ }
2860
+ const resume = driver.resumeState(existingCursor, fingerprint);
2861
+ try {
2862
+ const results = await driver.parse(filePath, resume);
2863
+ if (results.length > 0) {
2864
+ allResults.push(...results);
2865
+ prevCursors.set(filePath, cursorState.files[filePath]);
2866
+ for (const r of results) {
2867
+ sessionKeyToFile.set(r.canonical.sessionKey, filePath);
2868
+ }
2869
+ const newCursor = driver.buildCursor(fingerprint, results);
2870
+ cursorState.files[filePath] = newCursor;
2871
+ }
2872
+ } catch (err) {
2873
+ parseErrors.push({
2874
+ timestamp: new Date().toISOString(),
2875
+ source: driver.source,
2876
+ filePath,
2877
+ error: err instanceof Error ? err.message : String(err)
2878
+ });
2879
+ }
2880
+ }
2881
+ }
2882
+ if (dbDriver) {
2883
+ try {
2884
+ prevDbCursor = cursorState.openCodeSqlite;
2885
+ const dbResult = await dbDriver.run(prevDbCursor, syncCtx);
2886
+ allResults.push(...dbResult.results);
2887
+ for (const r of dbResult.results) {
2888
+ dbSourcedSessionKeys.add(r.canonical.sessionKey);
2889
+ }
2890
+ cursorState.openCodeSqlite = dbResult.cursor;
2891
+ } catch (err) {
2892
+ parseErrors.push({
2893
+ timestamp: new Date().toISOString(),
2894
+ source: dbDriver.source,
2895
+ filePath: discoverOpts.openCodeDbPath ?? "opencode.db",
2896
+ error: err instanceof Error ? err.message : String(err)
2897
+ });
2898
+ }
2899
+ }
2900
+ cursorState.updatedAt = new Date().toISOString();
2901
+ let uploadResult;
2902
+ let contentResult;
2903
+ if (opts.upload && allResults.length > 0) {
2904
+ const transformed = allResults.map((r) => toSessionSnapshot(r.canonical, r.raw));
2905
+ const snapshots = transformed.map((t) => t.snapshot);
2906
+ const precomputedMap = new Map;
2907
+ for (const t of transformed) {
2908
+ precomputedMap.set(t.snapshot.sessionKey, t.precomputed);
2909
+ }
2910
+ const uploadOpts = {
2911
+ apiUrl: opts.apiUrl,
2912
+ apiKey: opts.apiKey,
2913
+ userId: opts.userId,
2914
+ fetch: opts.fetch,
2915
+ sleep: opts.sleep
2916
+ };
2917
+ uploadResult = await uploadMetadataBatches(snapshots, uploadOpts);
2918
+ const contentOpts = {
2919
+ apiUrl: opts.apiUrl,
2920
+ apiKey: opts.apiKey,
2921
+ fetch: opts.fetch,
2922
+ sleep: opts.sleep
2923
+ };
2924
+ contentResult = await uploadContentBatch(allResults.map((r) => ({
2925
+ canonical: r.canonical,
2926
+ raw: r.raw,
2927
+ precomputed: precomputedMap.get(r.canonical.sessionKey)
2928
+ })), contentOpts, opts.contentConcurrency);
2929
+ if (contentResult.errors.length > 0) {
2930
+ const rolledBackFiles = new Set;
2931
+ let rollbackDbCursor = false;
2932
+ for (const { sessionKey } of contentResult.errors) {
2933
+ const filePath = sessionKeyToFile.get(sessionKey);
2934
+ if (filePath && !rolledBackFiles.has(filePath)) {
2935
+ rolledBackFiles.add(filePath);
2936
+ const prev = prevCursors.get(filePath);
2937
+ if (prev === undefined) {
2938
+ delete cursorState.files[filePath];
2939
+ } else {
2940
+ cursorState.files[filePath] = prev;
2941
+ }
2942
+ }
2943
+ if (dbSourcedSessionKeys.has(sessionKey)) {
2944
+ rollbackDbCursor = true;
2945
+ }
2946
+ }
2947
+ if (rollbackDbCursor) {
2948
+ cursorState.openCodeSqlite = prevDbCursor;
2949
+ }
2950
+ }
2951
+ }
2952
+ return {
2953
+ totalParsed: allResults.length,
2954
+ totalFiles,
2955
+ totalSkipped,
2956
+ parseErrors,
2957
+ uploadResult,
2958
+ contentResult,
2959
+ cursorState
2960
+ };
2961
+ }
2962
+ var init_sync_pipeline = __esm(() => {
2963
+ init_engine();
2964
+ init_content();
2965
+ });
2966
+
2967
+ // src/commands/sync.ts
2968
+ var exports_sync = {};
2969
+ __export(exports_sync, {
2970
+ default: () => sync_default,
2971
+ buildDbDriver: () => buildDbDriver
2972
+ });
2973
+ import { defineCommand } from "citty";
2974
+ import { join as join11 } from "path";
2975
+ import consola from "consola";
2976
+ async function buildDbDriver(driverSet, openDbOverride) {
2977
+ if (!driverSet.dbDriversAvailable || !driverSet.discoverOpts.openCodeDbPath) {
2978
+ return;
2979
+ }
2980
+ let openDb;
2981
+ if (openDbOverride) {
2982
+ openDb = openDbOverride;
2983
+ } else {
2984
+ const modId = "bun:sqlite";
2985
+ const bunSqlite = await import(modId);
2986
+ openDb = (path, opts) => new bunSqlite.Database(path, { readonly: opts?.readonly ?? true });
2987
+ }
2988
+ return createOpenCodeSqliteDriver(openDb, driverSet.discoverOpts.openCodeDbPath);
2989
+ }
2990
+ var sync_default;
2991
+ var init_sync = __esm(() => {
2992
+ init_src();
2993
+ init_manager();
2994
+ init_cursor_store();
2995
+ init_registry();
2996
+ init_opencode_sqlite();
2997
+ init_sync_pipeline();
2998
+ sync_default = defineCommand({
2999
+ meta: {
3000
+ name: "sync",
3001
+ description: "Parse local sessions and upload to Pika"
3002
+ },
3003
+ args: {
3004
+ upload: {
3005
+ type: "boolean",
3006
+ default: true,
3007
+ description: "Upload parsed sessions (default: true)"
3008
+ },
3009
+ dev: {
3010
+ type: "boolean",
3011
+ default: false,
3012
+ description: "Use local dev server"
3013
+ },
3014
+ source: {
3015
+ type: "string",
3016
+ description: `Filter sources (comma-separated). Valid: ${SOURCES.join(", ")}`
3017
+ }
3018
+ },
3019
+ async run({ args }) {
3020
+ const isDev = args.dev;
3021
+ const doUpload = args.upload;
3022
+ let sourceFilter;
3023
+ if (args.source) {
3024
+ const requested = args.source.split(",").map((s) => s.trim());
3025
+ const invalid = requested.filter((s) => !SOURCES.includes(s));
3026
+ if (invalid.length > 0) {
3027
+ consola.error(`Unknown source(s): ${invalid.join(", ")}. Valid: ${SOURCES.join(", ")}`);
3028
+ process.exitCode = 1;
3029
+ return;
3030
+ }
3031
+ sourceFilter = new Set(requested);
3032
+ consola.info(`Filtering to source(s): ${requested.join(", ")}`);
3033
+ }
3034
+ const configDir = join11(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".config", CONFIG_DIR);
3035
+ const config = new ConfigManager(configDir, isDev);
3036
+ if (doUpload && !config.isLoggedIn()) {
3037
+ consola.error("Not logged in. Run: pika login");
3038
+ process.exitCode = 1;
3039
+ return;
3040
+ }
3041
+ const cursorStore = new CursorStore(configDir);
3042
+ const cursorState = await cursorStore.load();
3043
+ const syncCtx = {};
3044
+ const driverSet = await buildDriverSet(undefined, syncCtx, sourceFilter);
3045
+ if (driverSet.fileDrivers.length === 0 && !driverSet.dbDriversAvailable) {
3046
+ consola.info("No AI tool sessions found");
3047
+ return;
3048
+ }
3049
+ const dbDriver = await buildDbDriver(driverSet);
3050
+ const sourceCount = driverSet.fileDrivers.length + (dbDriver ? 1 : 0);
3051
+ consola.start(`Syncing ${sourceCount} source(s)...`);
3052
+ const result = await runSyncPipeline({
3053
+ fileDrivers: driverSet.fileDrivers,
3054
+ dbDriver,
3055
+ discoverOpts: driverSet.discoverOpts,
3056
+ cursorState,
3057
+ syncCtx
3058
+ }, {
3059
+ upload: doUpload,
3060
+ apiUrl: config.getApiUrl(),
3061
+ apiKey: config.getToken() ?? "",
3062
+ userId: "cli"
3063
+ });
3064
+ await cursorStore.save(result.cursorState);
3065
+ consola.success(`Parsed ${result.totalParsed} session(s) from ${result.totalFiles} file(s) (${result.totalSkipped} unchanged)`);
3066
+ if (result.uploadResult) {
3067
+ consola.success(`Uploaded ${result.uploadResult.totalIngested} session(s) in ${result.uploadResult.totalBatches} batch(es)`);
3068
+ if (result.uploadResult.totalConflicts > 0) {
3069
+ consola.warn(`${result.uploadResult.totalConflicts} session(s) had version conflicts (skipped)`);
3070
+ }
3071
+ }
3072
+ if (result.contentResult) {
3073
+ consola.success(`Content: ${result.contentResult.uploaded} uploaded, ${result.contentResult.skipped} skipped`);
3074
+ if (result.contentResult.errors.length > 0) {
3075
+ consola.warn(`${result.contentResult.errors.length} content upload error(s)`);
3076
+ }
3077
+ }
3078
+ if (result.parseErrors.length > 0) {
3079
+ consola.warn(`${result.parseErrors.length} parse error(s) in this run`);
3080
+ }
3081
+ }
3082
+ });
3083
+ });
3084
+
3085
+ // src/commands/login-flow.ts
3086
+ import http from "http";
3087
+ function performLogin(deps) {
3088
+ return new Promise((resolve) => {
3089
+ const { openBrowser, log, config, apiUrl, timeoutMs } = deps;
3090
+ const server = http.createServer((req, res) => {
3091
+ const url = new URL(req.url, `http://localhost`);
3092
+ if (url.pathname !== "/callback") {
3093
+ res.writeHead(404, { "Content-Type": "text/plain" });
3094
+ res.end("Not found");
3095
+ return;
3096
+ }
3097
+ const apiKey = url.searchParams.get("api_key");
3098
+ const email = url.searchParams.get("email");
3099
+ if (!apiKey) {
3100
+ res.writeHead(400, { "Content-Type": "text/plain" });
3101
+ res.end("Missing api_key");
3102
+ cleanup();
3103
+ resolve({ success: false, error: "No api_key received" });
3104
+ return;
3105
+ }
3106
+ config.write({ token: apiKey });
3107
+ res.writeHead(200, { "Content-Type": "text/html" });
3108
+ res.end("<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>");
3109
+ cleanup();
3110
+ resolve({ success: true, email: email || undefined });
3111
+ });
3112
+ server.listen(0, "127.0.0.1", () => {
3113
+ const addr = server.address();
3114
+ if (!addr || typeof addr === "string") {
3115
+ cleanup();
3116
+ resolve({ success: false, error: "Failed to start local server" });
3117
+ return;
3118
+ }
3119
+ const callbackUrl = `http://127.0.0.1:${addr.port}/callback`;
3120
+ const loginUrl = `${apiUrl}/api/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
3121
+ openBrowser(loginUrl).catch(() => {
3122
+ if (log) {
3123
+ log(`Could not open browser. Open this URL manually:
3124
+ ${loginUrl}`);
3125
+ }
3126
+ });
3127
+ });
3128
+ server.on("error", (err) => {
3129
+ cleanup();
3130
+ resolve({ success: false, error: `Local server error: ${err.message}` });
3131
+ });
3132
+ const timer = setTimeout(() => {
3133
+ cleanup();
3134
+ resolve({ success: false, error: "Login timeout \u2014 no response received" });
3135
+ }, timeoutMs);
3136
+ function cleanup() {
3137
+ clearTimeout(timer);
3138
+ server.close();
3139
+ }
3140
+ });
3141
+ }
3142
+ var init_login_flow = () => {};
3143
+
3144
+ // src/commands/login.ts
3145
+ var exports_login = {};
3146
+ __export(exports_login, {
3147
+ default: () => login_default
3148
+ });
3149
+ import { defineCommand as defineCommand2 } from "citty";
3150
+ import consola2 from "consola";
3151
+ import { join as join12 } from "path";
3152
+ import { homedir as homedir2, platform } from "os";
3153
+ function getBrowserCommand() {
3154
+ switch (platform()) {
3155
+ case "darwin":
3156
+ return "open";
3157
+ case "win32":
3158
+ return "start";
3159
+ default:
3160
+ return "xdg-open";
3161
+ }
3162
+ }
3163
+ var login_default;
3164
+ var init_login = __esm(() => {
3165
+ init_src();
3166
+ init_manager();
3167
+ init_login_flow();
3168
+ login_default = defineCommand2({
3169
+ meta: {
3170
+ name: "login",
3171
+ description: "Connect CLI to dashboard via browser OAuth"
3172
+ },
3173
+ args: {
3174
+ force: {
3175
+ type: "boolean",
3176
+ default: false,
3177
+ description: "Force re-login even if already authenticated"
3178
+ },
3179
+ dev: {
3180
+ type: "boolean",
3181
+ default: false,
3182
+ description: "Use local dev server"
3183
+ }
3184
+ },
3185
+ async run({ args }) {
3186
+ const configDir = join12(homedir2(), ".config", CONFIG_DIR);
3187
+ const config = new ConfigManager(configDir, args.dev);
3188
+ if (config.isLoggedIn() && !args.force) {
3189
+ consola2.info("Already logged in. Use --force to re-authenticate.");
3190
+ return;
3191
+ }
3192
+ consola2.start("Opening browser for authentication...");
3193
+ const result = await performLogin({
3194
+ openBrowser: async (url) => {
3195
+ const { exec } = await import("child_process");
3196
+ const cmd = getBrowserCommand();
3197
+ return new Promise((resolve, reject) => {
3198
+ exec(`${cmd} "${url}"`, (error) => {
3199
+ if (error)
3200
+ reject(error);
3201
+ else
3202
+ resolve();
3203
+ });
3204
+ });
3205
+ },
3206
+ log: (msg) => consola2.info(msg),
3207
+ config,
3208
+ apiUrl: config.getApiUrl(),
3209
+ timeoutMs: LOGIN_TIMEOUT_MS
3210
+ });
3211
+ if (result.success) {
3212
+ consola2.success(`Logged in as ${result.email || "unknown"}`);
3213
+ } else {
3214
+ consola2.error(result.error || "Login failed");
3215
+ }
3216
+ }
3217
+ });
3218
+ });
3219
+
3220
+ // src/commands/status-display.ts
3221
+ function inferSource(filePath) {
3222
+ if (filePath.includes(".claude/"))
3223
+ return "claude-code";
3224
+ if (filePath.includes(".codex/"))
3225
+ return "codex";
3226
+ if (filePath.includes(".gemini/"))
3227
+ return "gemini-cli";
3228
+ if (filePath.includes("opencode/") || filePath.includes("opencode\\"))
3229
+ return "opencode";
3230
+ if (filePath.includes("workspaceStorage/") || filePath.includes("globalStorage/"))
3231
+ return "vscode-copilot";
3232
+ return null;
3233
+ }
3234
+ function formatTimeAgo(ms) {
3235
+ if (ms < 0)
3236
+ return "just now";
3237
+ const seconds = Math.floor(ms / 1000);
3238
+ if (seconds < 60)
3239
+ return `${seconds}s ago`;
3240
+ const minutes = Math.floor(seconds / 60);
3241
+ if (minutes < 60)
3242
+ return `${minutes}m ago`;
3243
+ const hours = Math.floor(minutes / 60);
3244
+ if (hours < 24)
3245
+ return `${hours}h ago`;
3246
+ const days = Math.floor(hours / 24);
3247
+ return `${days}d ago`;
3248
+ }
3249
+ function buildStatus(input, now = new Date) {
3250
+ const { loggedIn, cursorState, parseErrors } = input;
3251
+ const lastSyncAt = cursorState.updatedAt;
3252
+ let lastSyncAgo = null;
3253
+ if (lastSyncAt) {
3254
+ const elapsed = now.getTime() - new Date(lastSyncAt).getTime();
3255
+ lastSyncAgo = formatTimeAgo(elapsed);
3256
+ }
3257
+ const sourceCounts = new Map;
3258
+ for (const source of SOURCES) {
3259
+ sourceCounts.set(source, 0);
3260
+ }
3261
+ for (const filePath of Object.keys(cursorState.files)) {
3262
+ const source = inferSource(filePath);
3263
+ if (source) {
3264
+ sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);
3265
+ }
3266
+ }
3267
+ const sourceStats = [];
3268
+ for (const source of SOURCES) {
3269
+ const count = sourceCounts.get(source) ?? 0;
3270
+ if (count > 0) {
3271
+ sourceStats.push({ source, fileCount: count });
3272
+ }
3273
+ }
3274
+ const totalFiles = Object.keys(cursorState.files).length;
3275
+ const hasOpenCodeDb = !!cursorState.openCodeSqlite;
3276
+ return {
3277
+ loggedIn,
3278
+ lastSyncAt,
3279
+ lastSyncAgo,
3280
+ sourceStats,
3281
+ totalFiles,
3282
+ hasOpenCodeDb,
3283
+ parseErrorCount: parseErrors.length,
3284
+ recentErrors: parseErrors
3285
+ };
3286
+ }
3287
+ function loadParseErrors(content) {
3288
+ if (!content.trim())
3289
+ return [];
3290
+ const lines = content.trim().split(`
3291
+ `);
3292
+ const errors = [];
3293
+ for (const line of lines) {
3294
+ if (!line.trim())
3295
+ continue;
3296
+ try {
3297
+ const parsed = JSON.parse(line);
3298
+ if (parsed.timestamp && parsed.source && parsed.error) {
3299
+ errors.push(parsed);
3300
+ }
3301
+ } catch {}
3302
+ }
3303
+ return errors.slice(-MAX_RECENT_ERRORS).reverse();
3304
+ }
3305
+ function formatSourceLabel(source) {
3306
+ const labels = {
3307
+ "claude-code": "Claude Code",
3308
+ codex: "Codex CLI",
3309
+ "gemini-cli": "Gemini CLI",
3310
+ opencode: "OpenCode",
3311
+ "vscode-copilot": "VS Code Copilot"
3312
+ };
3313
+ return labels[source];
3314
+ }
3315
+ function formatStatusLines(output) {
3316
+ const lines = [];
3317
+ lines.push(output.loggedIn ? "Logged in: yes" : "Logged in: no");
3318
+ if (output.lastSyncAt) {
3319
+ lines.push(`Last sync: ${output.lastSyncAgo} (${output.lastSyncAt})`);
3320
+ } else {
3321
+ lines.push("Last sync: never");
3322
+ }
3323
+ if (output.sourceStats.length > 0 || output.hasOpenCodeDb) {
3324
+ lines.push("");
3325
+ lines.push("Sources:");
3326
+ for (const s of output.sourceStats) {
3327
+ lines.push(` ${formatSourceLabel(s.source)}: ${s.fileCount} file(s)`);
3328
+ }
3329
+ if (output.hasOpenCodeDb) {
3330
+ lines.push(" OpenCode (SQLite): active");
3331
+ }
3332
+ lines.push(` Total tracked: ${output.totalFiles} file(s)`);
3333
+ } else {
3334
+ lines.push("");
3335
+ lines.push("Sources: none tracked");
3336
+ }
3337
+ if (output.parseErrorCount > 0) {
3338
+ lines.push("");
3339
+ lines.push(`Parse errors: ${output.parseErrorCount}`);
3340
+ for (const err of output.recentErrors) {
3341
+ const ts = err.timestamp.slice(0, 19).replace("T", " ");
3342
+ lines.push(` [${ts}] ${err.source}: ${err.error}`);
3343
+ if (err.filePath) {
3344
+ lines.push(` ${err.filePath}`);
3345
+ }
3346
+ }
3347
+ }
3348
+ return lines;
3349
+ }
3350
+ var MAX_RECENT_ERRORS = 5;
3351
+ var init_status_display = __esm(() => {
3352
+ init_src();
3353
+ });
3354
+
3355
+ // src/commands/status.ts
3356
+ var exports_status = {};
3357
+ __export(exports_status, {
3358
+ default: () => status_default
3359
+ });
3360
+ import { defineCommand as defineCommand3 } from "citty";
3361
+ import { readFile as readFile5 } from "fs/promises";
3362
+ import { join as join13 } from "path";
3363
+ import consola3 from "consola";
3364
+ var status_default;
3365
+ var init_status = __esm(() => {
3366
+ init_src();
3367
+ init_manager();
3368
+ init_cursor_store();
3369
+ init_status_display();
3370
+ status_default = defineCommand3({
3371
+ meta: {
3372
+ name: "status",
3373
+ description: "Show sync status and session stats"
3374
+ },
3375
+ args: {
3376
+ dev: {
3377
+ type: "boolean",
3378
+ default: false,
3379
+ description: "Use local dev server"
3380
+ }
3381
+ },
3382
+ async run({ args }) {
3383
+ const isDev = args.dev;
3384
+ const configDir = join13(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".config", CONFIG_DIR);
3385
+ const config = new ConfigManager(configDir, isDev);
3386
+ const loggedIn = config.isLoggedIn();
3387
+ const cursorStore = new CursorStore(configDir);
3388
+ const cursorState = await cursorStore.load();
3389
+ let parseErrors;
3390
+ try {
3391
+ const content = await readFile5(join13(configDir, PARSE_ERRORS_FILE), "utf-8");
3392
+ parseErrors = loadParseErrors(content);
3393
+ } catch {
3394
+ parseErrors = [];
3395
+ }
3396
+ const output = buildStatus({ loggedIn, cursorState, parseErrors });
3397
+ const lines = formatStatusLines(output);
3398
+ for (const line of lines) {
3399
+ if (line === "") {
3400
+ consola3.log("");
3401
+ } else {
3402
+ consola3.log(line);
3403
+ }
3404
+ }
3405
+ }
3406
+ });
3407
+ });
3408
+
3409
+ // src/bin.ts
3410
+ import { runMain } from "citty";
3411
+
3412
+ // src/cli.ts
3413
+ init_src();
3414
+ import { defineCommand as defineCommand4 } from "citty";
3415
+ var main = defineCommand4({
3416
+ meta: {
3417
+ name: "pika",
3418
+ version: PIKA_VERSION,
3419
+ description: "Replay and search coding agent sessions"
3420
+ },
3421
+ subCommands: {
3422
+ sync: () => Promise.resolve().then(() => (init_sync(), exports_sync)).then((m) => m.default),
3423
+ login: () => Promise.resolve().then(() => (init_login(), exports_login)).then((m) => m.default),
3424
+ status: () => Promise.resolve().then(() => (init_status(), exports_status)).then((m) => m.default)
3425
+ }
3426
+ });
3427
+
3428
+ // src/bin.ts
3429
+ runMain(main);