prompt-exporter 2.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2961 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_CDP_PORT,
4
+ DEFAULT_SOURCE_ID,
5
+ activateCdpTarget,
6
+ cdpNavigate,
7
+ discoverCdpPort,
8
+ ensureChatGptSessionTab,
9
+ ensureHomeLayout,
10
+ getCdpEndpointInfo,
11
+ getDefaultDataDir,
12
+ getSourceDataDir,
13
+ getSourceTokenPath,
14
+ isCdpRunning,
15
+ isChatGptUrl,
16
+ listCdpTargets,
17
+ openCdpTab,
18
+ resolveChromiumUserDataDir
19
+ } from "./chunk-YHAS76TP.js";
20
+
21
+ // src/cli/index.ts
22
+ import { Command } from "commander";
23
+
24
+ // src/cli/commands/backup.ts
25
+ import fs6 from "fs/promises";
26
+ import path6 from "path";
27
+ import chalk2 from "chalk";
28
+ import ora from "ora";
29
+
30
+ // src/sources/chatgpt/api/client.ts
31
+ import crypto from "crypto";
32
+
33
+ // src/sources/chatgpt/api/endpoints.ts
34
+ var BASE_URL = "https://chatgpt.com";
35
+ var ENDPOINTS = {
36
+ SESSION: "/api/auth/session",
37
+ CONVERSATIONS: "/backend-api/conversations",
38
+ CONVERSATION: (id) => `/backend-api/conversation/${id}`,
39
+ PROJECTS_SIDEBAR: "/backend-api/gizmos/snorlax/sidebar",
40
+ PROJECT_CONVERSATIONS: (gizmoId) => `/backend-api/gizmos/${gizmoId}/conversations`,
41
+ FILE_DOWNLOAD: (fileId) => `/backend-api/files/download/${fileId}`
42
+ };
43
+
44
+ // src/sources/chatgpt/api/types.ts
45
+ import { z } from "zod";
46
+ var SessionSchema = z.looseObject({
47
+ accessToken: z.string(),
48
+ user: z.looseObject({
49
+ id: z.string(),
50
+ name: z.string().optional(),
51
+ email: z.string().optional(),
52
+ image: z.string().optional()
53
+ }).optional(),
54
+ expires: z.string().optional()
55
+ });
56
+ var ConversationItemSchema = z.looseObject({
57
+ id: z.string(),
58
+ title: z.string().nullable(),
59
+ create_time: z.string().or(z.number()).nullable(),
60
+ update_time: z.string().or(z.number()).nullable(),
61
+ mapping: z.record(z.string(), z.unknown()).nullable().optional(),
62
+ current_node: z.string().nullable().optional(),
63
+ conversation_template_id: z.string().nullable().optional(),
64
+ gizmo_id: z.string().nullable().optional(),
65
+ is_archived: z.boolean().optional(),
66
+ workspace_id: z.string().nullable().optional()
67
+ });
68
+ var ConversationsResponseSchema = z.looseObject({
69
+ items: z.array(ConversationItemSchema),
70
+ total: z.number(),
71
+ limit: z.number(),
72
+ offset: z.number(),
73
+ has_missing_conversations: z.boolean().optional()
74
+ });
75
+ var MessageContentSchema = z.looseObject({
76
+ content_type: z.string(),
77
+ parts: z.array(z.unknown()).optional(),
78
+ text: z.string().optional()
79
+ });
80
+ var MessageSchema = z.looseObject({
81
+ id: z.string(),
82
+ author: z.looseObject({
83
+ role: z.string(),
84
+ name: z.string().nullable().optional(),
85
+ metadata: z.record(z.string(), z.unknown()).optional()
86
+ }),
87
+ create_time: z.number().nullable().optional(),
88
+ update_time: z.number().nullable().optional(),
89
+ content: MessageContentSchema.optional(),
90
+ status: z.string().optional(),
91
+ end_turn: z.boolean().nullable().optional(),
92
+ weight: z.number().optional(),
93
+ metadata: z.record(z.string(), z.unknown()).optional(),
94
+ recipient: z.string().optional()
95
+ });
96
+ var MappingNodeSchema = z.looseObject({
97
+ id: z.string(),
98
+ message: MessageSchema.nullable().optional(),
99
+ parent: z.string().nullable().optional(),
100
+ children: z.array(z.string()).optional()
101
+ });
102
+ var ConversationDetailSchema = z.looseObject({
103
+ id: z.string().optional(),
104
+ title: z.string().nullable(),
105
+ create_time: z.number().nullable(),
106
+ update_time: z.number().nullable(),
107
+ mapping: z.record(z.string(), MappingNodeSchema),
108
+ moderation_results: z.array(z.unknown()).optional(),
109
+ current_node: z.string().nullable().optional(),
110
+ conversation_id: z.string().optional(),
111
+ is_archived: z.boolean().optional()
112
+ });
113
+ var ProjectGizmoSchema = z.looseObject({
114
+ id: z.string(),
115
+ display: z.looseObject({
116
+ name: z.string(),
117
+ theme: z.unknown().optional()
118
+ }),
119
+ created_at: z.string().optional(),
120
+ updated_at: z.string().optional(),
121
+ last_interacted_at: z.string().nullable().optional(),
122
+ num_interactions: z.number().optional(),
123
+ is_archived: z.boolean().optional(),
124
+ gizmo_type: z.string().optional()
125
+ });
126
+ var SidebarItemSchema = z.looseObject({
127
+ gizmo: ProjectGizmoSchema
128
+ });
129
+ var ProjectsSidebarResponseSchema = z.looseObject({
130
+ items: z.array(SidebarItemSchema),
131
+ cursor: z.string().nullable().optional()
132
+ });
133
+ var ProjectConversationsResponseSchema = z.looseObject({
134
+ items: z.array(ConversationItemSchema),
135
+ cursor: z.string().nullable().optional()
136
+ });
137
+ var AuthenticationError = class extends Error {
138
+ constructor(message) {
139
+ super(message);
140
+ this.name = "AuthenticationError";
141
+ }
142
+ };
143
+ var RateLimitError = class extends Error {
144
+ retryAfter;
145
+ constructor(message, retryAfter) {
146
+ super(message);
147
+ this.name = "RateLimitError";
148
+ this.retryAfter = retryAfter;
149
+ }
150
+ };
151
+ var NetworkError = class extends Error {
152
+ statusCode;
153
+ constructor(message, statusCode) {
154
+ super(message);
155
+ this.name = "NetworkError";
156
+ this.statusCode = statusCode;
157
+ }
158
+ };
159
+
160
+ // src/utils/retry.ts
161
+ var defaultOptions = {
162
+ maxRetries: 5,
163
+ baseDelay: 1e3,
164
+ maxDelay: 6e4
165
+ };
166
+ function calculateDelay(attempt, baseDelay, maxDelay) {
167
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
168
+ const jitter = Math.random() * 0.3 * exponentialDelay;
169
+ return Math.min(exponentialDelay + jitter, maxDelay);
170
+ }
171
+ async function withRetry(fn, options = {}) {
172
+ const opts = { ...defaultOptions, ...options };
173
+ let lastError;
174
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
175
+ try {
176
+ return await fn();
177
+ } catch (error) {
178
+ lastError = error;
179
+ if (attempt === opts.maxRetries) {
180
+ break;
181
+ }
182
+ const statusCode = lastError.statusCode;
183
+ if (lastError.name === "AuthenticationError" || lastError.name === "NetworkError" && (statusCode === 403 || statusCode === 404)) {
184
+ throw lastError;
185
+ }
186
+ const delay = calculateDelay(attempt, opts.baseDelay, opts.maxDelay);
187
+ opts.onRetry?.(lastError, attempt + 1, delay);
188
+ await sleep(delay);
189
+ }
190
+ }
191
+ throw lastError;
192
+ }
193
+ function sleep(ms) {
194
+ return new Promise((resolve) => setTimeout(resolve, ms));
195
+ }
196
+
197
+ // src/utils/throttle-stats.ts
198
+ var ThrottleStats = class {
199
+ latenciesMs = [];
200
+ maxSamples;
201
+ successes = 0;
202
+ rateLimits = 0;
203
+ failures = 0;
204
+ startedAt = Date.now();
205
+ lastReportAt = Date.now();
206
+ constructor(maxSamples = 200) {
207
+ this.maxSamples = maxSamples;
208
+ }
209
+ recordSuccess(latencyMs) {
210
+ this.successes++;
211
+ this.pushLatency(latencyMs);
212
+ }
213
+ recordRateLimit(latencyMs) {
214
+ this.rateLimits++;
215
+ if (latencyMs != null) this.pushLatency(latencyMs);
216
+ }
217
+ recordFailure(latencyMs) {
218
+ this.failures++;
219
+ if (latencyMs != null) this.pushLatency(latencyMs);
220
+ }
221
+ pushLatency(ms) {
222
+ this.latenciesMs.push(ms);
223
+ if (this.latenciesMs.length > this.maxSamples) {
224
+ this.latenciesMs.shift();
225
+ }
226
+ }
227
+ percentile(p) {
228
+ if (this.latenciesMs.length === 0) return 0;
229
+ const sorted = [...this.latenciesMs].sort((a, b) => a - b);
230
+ const idx = Math.min(
231
+ sorted.length - 1,
232
+ Math.max(0, Math.ceil(p / 100 * sorted.length) - 1)
233
+ );
234
+ return sorted[idx] ?? 0;
235
+ }
236
+ summary() {
237
+ const elapsedSec = Math.max(1e-3, (Date.now() - this.startedAt) / 1e3);
238
+ const done = this.successes + this.failures + this.rateLimits;
239
+ const rps = (this.successes / elapsedSec).toFixed(2);
240
+ const p50 = Math.round(this.percentile(50));
241
+ const p95 = Math.round(this.percentile(95));
242
+ return [
243
+ `ok=${this.successes}`,
244
+ `429=${this.rateLimits}`,
245
+ `fail=${this.failures}`,
246
+ `rps=${rps}`,
247
+ `p50=${p50}ms`,
248
+ `p95=${p95}ms`,
249
+ `n=${done}`
250
+ ].join(" ");
251
+ }
252
+ /** True if enough time passed to emit another verbose line. */
253
+ shouldReport(intervalMs = 15e3) {
254
+ if (Date.now() - this.lastReportAt >= intervalMs) {
255
+ this.lastReportAt = Date.now();
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ };
261
+
262
+ // src/sources/chatgpt/api/cdp-http.ts
263
+ var CdpHttpSession = class _CdpHttpSession {
264
+ port;
265
+ poolSize;
266
+ workers = [];
267
+ waiters = [];
268
+ cachedBearer = null;
269
+ connecting = null;
270
+ stats = new ThrottleStats();
271
+ /** Adaptive pause after 429 (ms); decays on success. */
272
+ cooldownMs = 0;
273
+ minGapMs;
274
+ lastStartAt = 0;
275
+ constructor(port, options = {}) {
276
+ this.port = port;
277
+ this.poolSize = Math.max(1, options.poolSize ?? 3);
278
+ this.minGapMs = Math.max(0, options.minGapMs ?? 150);
279
+ }
280
+ get debuggingPort() {
281
+ return this.port;
282
+ }
283
+ get workerCount() {
284
+ return this.workers.length;
285
+ }
286
+ static async connect(preferredPort = DEFAULT_CDP_PORT, options = {}) {
287
+ let port = preferredPort;
288
+ const { isCdpRunning: isCdpRunning2 } = await import("./cdp-PRMCSQGL.js");
289
+ if (!await isCdpRunning2(port)) {
290
+ const found = await discoverCdpPort(port);
291
+ if (found === null) {
292
+ throw new Error("No Chromium CDP endpoint found (tried 9222/9223/\u2026)");
293
+ }
294
+ port = found;
295
+ }
296
+ const session = new _CdpHttpSession(port, options);
297
+ await session.ensurePool();
298
+ return session;
299
+ }
300
+ async close() {
301
+ for (const w of this.workers) {
302
+ for (const [, waiter] of w.pending) {
303
+ waiter.reject(new Error("CDP session closed"));
304
+ }
305
+ w.pending.clear();
306
+ try {
307
+ w.ws.close();
308
+ } catch {
309
+ }
310
+ }
311
+ this.workers = [];
312
+ this.waiters = [];
313
+ this.connecting = null;
314
+ }
315
+ async ensurePool() {
316
+ if (this.workers.length >= this.poolSize) return;
317
+ if (this.connecting) return this.connecting;
318
+ this.connecting = (async () => {
319
+ const pages = await this.pickOrOpenPages(this.poolSize);
320
+ for (let i = 0; i < pages.length; i++) {
321
+ if (this.workers.some((w) => w.targetId === pages[i].id)) continue;
322
+ const worker = await this.attachWorker(i, pages[i]);
323
+ this.workers.push(worker);
324
+ }
325
+ if (this.workers.length === 0) {
326
+ throw new Error("Failed to attach any CDP page workers");
327
+ }
328
+ })();
329
+ try {
330
+ await this.connecting;
331
+ } finally {
332
+ this.connecting = null;
333
+ }
334
+ }
335
+ async pickOrOpenPages(n) {
336
+ const targets = await listCdpTargets(this.port);
337
+ const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
338
+ const chatgpt = pages.filter(
339
+ (t) => isChatGptUrl(t.url) && !t.url.includes("/api/auth/session")
340
+ );
341
+ const chosen = [...chatgpt];
342
+ while (chosen.length < n) {
343
+ const created = await openCdpTab(this.port, "https://chatgpt.com/");
344
+ chosen.push(created);
345
+ await new Promise((r) => setTimeout(r, 400));
346
+ }
347
+ return chosen.slice(0, n);
348
+ }
349
+ async attachWorker(id, page) {
350
+ await activateCdpTarget(this.port, page.id);
351
+ const refreshed = (await listCdpTargets(this.port)).find((t) => t.id === page.id) ?? page;
352
+ const wsUrl = refreshed.webSocketDebuggerUrl;
353
+ if (!wsUrl) throw new Error("CDP page has no webSocketDebuggerUrl");
354
+ const WS = globalThis.WebSocket;
355
+ if (!WS) throw new Error("WebSocket not available");
356
+ const ws = new WS(wsUrl);
357
+ const worker = {
358
+ id,
359
+ targetId: page.id,
360
+ ws,
361
+ nextId: 1,
362
+ pending: /* @__PURE__ */ new Map(),
363
+ busy: false
364
+ };
365
+ await new Promise((resolve, reject) => {
366
+ const timer = setTimeout(
367
+ () => reject(new Error("CDP WebSocket connect timeout")),
368
+ 8e3
369
+ );
370
+ ws.addEventListener("open", () => {
371
+ clearTimeout(timer);
372
+ resolve();
373
+ });
374
+ ws.addEventListener("error", () => {
375
+ clearTimeout(timer);
376
+ reject(new Error("CDP WebSocket connection failed"));
377
+ });
378
+ });
379
+ ws.addEventListener("message", (event) => {
380
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
381
+ let msg;
382
+ try {
383
+ msg = JSON.parse(raw);
384
+ } catch {
385
+ return;
386
+ }
387
+ const mid = msg.id;
388
+ if (typeof mid !== "number") return;
389
+ const waiter = worker.pending.get(mid);
390
+ if (!waiter) return;
391
+ worker.pending.delete(mid);
392
+ if (msg.error) {
393
+ const err = msg.error;
394
+ waiter.reject(
395
+ new Error(typeof err.message === "string" ? err.message : "CDP error")
396
+ );
397
+ } else {
398
+ waiter.resolve(msg.result ?? {});
399
+ }
400
+ });
401
+ await this.send(worker, "Runtime.enable");
402
+ return worker;
403
+ }
404
+ send(worker, method, params) {
405
+ if (worker.ws.readyState !== WebSocket.OPEN) {
406
+ return Promise.reject(new Error("CDP WebSocket not open"));
407
+ }
408
+ const id = worker.nextId++;
409
+ return new Promise((resolve, reject) => {
410
+ worker.pending.set(id, { resolve, reject });
411
+ worker.ws.send(JSON.stringify({ id, method, params }));
412
+ });
413
+ }
414
+ acquire() {
415
+ const free = this.workers.find((w) => !w.busy);
416
+ if (free) {
417
+ free.busy = true;
418
+ return Promise.resolve(free);
419
+ }
420
+ return new Promise((resolve) => {
421
+ this.waiters.push(resolve);
422
+ });
423
+ }
424
+ release(worker) {
425
+ const next = this.waiters.shift();
426
+ if (next) {
427
+ next(worker);
428
+ } else {
429
+ worker.busy = false;
430
+ }
431
+ }
432
+ async respectGap() {
433
+ const gap = Math.max(this.minGapMs, this.cooldownMs);
434
+ if (gap <= 0) return;
435
+ const wait = gap - (Date.now() - this.lastStartAt);
436
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
437
+ this.lastStartAt = Date.now();
438
+ }
439
+ async fetch(url, options = {}) {
440
+ await this.ensurePool();
441
+ await this.respectGap();
442
+ const worker = await this.acquire();
443
+ const t0 = Date.now();
444
+ try {
445
+ const response = await this.fetchOnWorker(worker, url, options);
446
+ const latency = Date.now() - t0;
447
+ if (response.status === 429) {
448
+ this.stats.recordRateLimit(latency);
449
+ this.cooldownMs = Math.min(15e3, Math.max(1e3, this.cooldownMs * 2 || 1e3));
450
+ } else if (response.status >= 200 && response.status < 300) {
451
+ this.stats.recordSuccess(latency);
452
+ this.cooldownMs = Math.floor(this.cooldownMs * 0.5);
453
+ } else {
454
+ this.stats.recordFailure(latency);
455
+ }
456
+ return response;
457
+ } catch (e) {
458
+ this.stats.recordFailure(Date.now() - t0);
459
+ throw e;
460
+ } finally {
461
+ this.release(worker);
462
+ }
463
+ }
464
+ async fetchOnWorker(worker, url, options) {
465
+ const method = options.method ?? "GET";
466
+ const wantRaw = options.raw ?? false;
467
+ const extraHeaders = options.headers ?? {};
468
+ const body = options.body;
469
+ const cachedBearer = this.cachedBearer;
470
+ const result = await this.send(worker, "Runtime.evaluate", {
471
+ expression: `(() => {
472
+ const url = ${JSON.stringify(url)};
473
+ const method = ${JSON.stringify(method)};
474
+ const extraHeaders = ${JSON.stringify(extraHeaders)};
475
+ const body = ${JSON.stringify(body ?? null)};
476
+ const wantRaw = ${JSON.stringify(wantRaw)};
477
+ const cachedBearer = ${JSON.stringify(cachedBearer)};
478
+
479
+ const toBase64 = async (buf) => {
480
+ const bytes = new Uint8Array(buf);
481
+ const chunk = 0x8000;
482
+ let binary = '';
483
+ for (let i = 0; i < bytes.length; i += chunk) {
484
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
485
+ }
486
+ return btoa(binary);
487
+ };
488
+
489
+ return (async () => {
490
+ const auth = {};
491
+ if (cachedBearer) {
492
+ auth.Authorization = 'Bearer ' + cachedBearer;
493
+ } else {
494
+ try {
495
+ const session = await fetch('/api/auth/session', {
496
+ credentials: 'include',
497
+ headers: { Accept: 'application/json' },
498
+ cache: 'no-store',
499
+ }).then((r) => r.json());
500
+ if (session && typeof session.accessToken === 'string') {
501
+ auth.Authorization = 'Bearer ' + session.accessToken;
502
+ }
503
+ } catch {}
504
+ }
505
+
506
+ const response = await fetch(url, {
507
+ method,
508
+ credentials: 'include',
509
+ headers: Object.assign(
510
+ { Accept: 'application/json' },
511
+ auth,
512
+ extraHeaders || {}
513
+ ),
514
+ body: body == null ? undefined : body,
515
+ });
516
+
517
+ const accessToken =
518
+ auth.Authorization && auth.Authorization.startsWith('Bearer ')
519
+ ? auth.Authorization.slice(7)
520
+ : null;
521
+
522
+ if (wantRaw) {
523
+ const buf = await response.arrayBuffer();
524
+ return {
525
+ status: response.status,
526
+ statusText: response.statusText,
527
+ bodyText: '',
528
+ bodyBase64: await toBase64(buf),
529
+ accessToken,
530
+ };
531
+ }
532
+
533
+ const bodyText = await response.text();
534
+ return {
535
+ status: response.status,
536
+ statusText: response.statusText,
537
+ bodyText,
538
+ accessToken,
539
+ };
540
+ })();
541
+ })()`,
542
+ awaitPromise: true,
543
+ returnByValue: true
544
+ });
545
+ const value = result.result?.value;
546
+ if (!value || typeof value.status !== "number") {
547
+ throw new Error("CDP fetch returned no usable response");
548
+ }
549
+ if (typeof value.accessToken === "string" && value.accessToken.startsWith("eyJ")) {
550
+ this.cachedBearer = value.accessToken;
551
+ }
552
+ return {
553
+ status: value.status,
554
+ statusText: value.statusText,
555
+ bodyText: value.bodyText,
556
+ bodyBase64: value.bodyBase64
557
+ };
558
+ }
559
+ };
560
+
561
+ // src/sources/chatgpt/api/client.ts
562
+ var BROWSER_HEADERS = {
563
+ "Content-Type": "application/json",
564
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
565
+ Accept: "application/json",
566
+ "Accept-Language": "en-US,en;q=0.9",
567
+ Referer: "https://chatgpt.com/",
568
+ Origin: "https://chatgpt.com",
569
+ "Sec-Ch-Ua": '"Chromium";v="152", "Not_A Brand";v="24"',
570
+ "Sec-Ch-Ua-Mobile": "?0",
571
+ "Sec-Ch-Ua-Platform": '"Linux"',
572
+ "Sec-Fetch-Dest": "empty",
573
+ "Sec-Fetch-Mode": "cors",
574
+ "Sec-Fetch-Site": "same-origin"
575
+ };
576
+ var ChatGPTClient = class {
577
+ accessToken;
578
+ verbose;
579
+ deviceId;
580
+ cdpPort;
581
+ cdpPoolSize;
582
+ cdpMinGapMs;
583
+ cdp;
584
+ ownsCdp = false;
585
+ constructor(accessToken = "", options = {}) {
586
+ this.accessToken = accessToken;
587
+ this.verbose = options.verbose ?? false;
588
+ this.deviceId = crypto.randomUUID();
589
+ this.cdpPort = options.cdpPort;
590
+ this.cdpPoolSize = options.cdpPoolSize ?? 3;
591
+ this.cdpMinGapMs = options.cdpMinGapMs ?? 150;
592
+ if (options.cdpSession) {
593
+ this.cdp = options.cdpSession;
594
+ this.ownsCdp = false;
595
+ }
596
+ }
597
+ get usesCdp() {
598
+ return this.cdpPort !== void 0 || this.cdp !== void 0;
599
+ }
600
+ get throttleStats() {
601
+ return this.cdp?.stats;
602
+ }
603
+ get cdpWorkers() {
604
+ return this.cdp?.workerCount ?? 0;
605
+ }
606
+ getHeaders() {
607
+ const headers = {
608
+ ...BROWSER_HEADERS,
609
+ "Oai-Device-Id": this.deviceId,
610
+ "Oai-Language": "en-US"
611
+ };
612
+ if (this.accessToken) {
613
+ headers.Authorization = `Bearer ${this.accessToken}`;
614
+ }
615
+ return headers;
616
+ }
617
+ async ensureCdp() {
618
+ if (this.cdp) return this.cdp;
619
+ if (this.cdpPort === void 0) {
620
+ throw new Error("CDP port not configured");
621
+ }
622
+ this.cdp = await CdpHttpSession.connect(this.cdpPort, {
623
+ poolSize: this.cdpPoolSize,
624
+ minGapMs: this.cdpMinGapMs
625
+ });
626
+ this.ownsCdp = true;
627
+ return this.cdp;
628
+ }
629
+ async close() {
630
+ if (this.ownsCdp && this.cdp) {
631
+ await this.cdp.close();
632
+ this.cdp = void 0;
633
+ }
634
+ }
635
+ async request(endpointOrUrl, options = {}) {
636
+ const method = options.method ?? "GET";
637
+ const absolute = options.absolute || endpointOrUrl.startsWith("http://") || endpointOrUrl.startsWith("https://");
638
+ const url = absolute ? endpointOrUrl : `${BASE_URL}${endpointOrUrl}`;
639
+ if (this.usesCdp) {
640
+ const cdp = await this.ensureCdp();
641
+ const pathOrUrl = absolute ? url : endpointOrUrl;
642
+ const response2 = await cdp.fetch(pathOrUrl, {
643
+ method,
644
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
645
+ headers: options.body !== void 0 ? { "Content-Type": "application/json" } : void 0,
646
+ raw: options.raw
647
+ });
648
+ if (options.raw) {
649
+ const b64 = response2.bodyBase64 ?? "";
650
+ const binary = Buffer.from(b64, "base64");
651
+ return {
652
+ status: response2.status,
653
+ statusText: response2.statusText,
654
+ raw: binary.buffer.slice(
655
+ binary.byteOffset,
656
+ binary.byteOffset + binary.byteLength
657
+ )
658
+ };
659
+ }
660
+ let json2;
661
+ try {
662
+ json2 = response2.bodyText ? JSON.parse(response2.bodyText) : null;
663
+ } catch {
664
+ json2 = void 0;
665
+ }
666
+ return {
667
+ status: response2.status,
668
+ statusText: response2.statusText,
669
+ text: response2.bodyText,
670
+ json: json2
671
+ };
672
+ }
673
+ const response = await fetch(url, {
674
+ method,
675
+ headers: this.getHeaders(),
676
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
677
+ });
678
+ if (options.raw) {
679
+ return {
680
+ status: response.status,
681
+ statusText: response.statusText,
682
+ raw: await response.arrayBuffer()
683
+ };
684
+ }
685
+ const text = await response.text();
686
+ let json;
687
+ try {
688
+ json = text ? JSON.parse(text) : null;
689
+ } catch {
690
+ json = void 0;
691
+ }
692
+ return {
693
+ status: response.status,
694
+ statusText: response.statusText,
695
+ text,
696
+ json
697
+ };
698
+ }
699
+ throwForStatus(status, statusText, bodyPreview) {
700
+ if (status === 401 || status === 403) {
701
+ throw new AuthenticationError(
702
+ `Access rejected (HTTP ${status})${this.usesCdp ? "" : " \u2014 prefer CDP: prompt-exporter chromium start"}.${this.verbose && bodyPreview ? ` Body: ${bodyPreview.slice(0, 200)}` : ""}`
703
+ );
704
+ }
705
+ if (status === 429) {
706
+ throw new RateLimitError("Rate limited by API");
707
+ }
708
+ throw new NetworkError(`Request failed: ${status} ${statusText}`, status);
709
+ }
710
+ async initialize() {
711
+ if (this.usesCdp) {
712
+ await this.ensureCdp();
713
+ }
714
+ const result = await this.request(
715
+ "/backend-api/conversations?offset=0&limit=1&order=updated"
716
+ );
717
+ if (result.status < 200 || result.status >= 300) {
718
+ if (this.verbose) {
719
+ console.error(`Auth check failed: HTTP ${result.status}`);
720
+ console.error(`Response body: ${(result.text ?? "").slice(0, 500)}`);
721
+ }
722
+ this.throwForStatus(result.status, result.statusText, result.text);
723
+ }
724
+ if (this.verbose) {
725
+ console.log(
726
+ this.usesCdp ? `Authenticated via Chromium CDP (port ${this.cdp?.debuggingPort ?? this.cdpPort})` : "Successfully authenticated"
727
+ );
728
+ }
729
+ }
730
+ async fetch(endpoint, options = {}) {
731
+ if (!this.usesCdp && !this.accessToken) {
732
+ throw new AuthenticationError(
733
+ "No access token and no CDP session. Run: prompt-exporter token"
734
+ );
735
+ }
736
+ const { method = "GET", body, parseResponse, retryOptions } = options;
737
+ return withRetry(
738
+ async () => {
739
+ const result = await this.request(endpoint, { method, body });
740
+ if (result.status < 200 || result.status >= 300) {
741
+ this.throwForStatus(result.status, result.statusText, result.text);
742
+ }
743
+ const data = result.json;
744
+ return parseResponse ? parseResponse(data) : data;
745
+ },
746
+ {
747
+ ...retryOptions,
748
+ onRetry: (error, attempt, delay) => {
749
+ if (this.verbose) {
750
+ console.log(
751
+ `Retry ${attempt}: ${error.message} (waiting ${Math.round(delay / 1e3)}s)`
752
+ );
753
+ }
754
+ }
755
+ }
756
+ );
757
+ }
758
+ async fetchRaw(url, retryOptions) {
759
+ const isAbsolute = url.startsWith("http://") || url.startsWith("https://");
760
+ const fullUrl = isAbsolute ? url : `${BASE_URL}${url}`;
761
+ const isSameOrigin = !isAbsolute || fullUrl.startsWith(BASE_URL);
762
+ return withRetry(
763
+ async () => {
764
+ const result = await this.request(fullUrl, {
765
+ absolute: true,
766
+ raw: true
767
+ });
768
+ if (result.status < 200 || result.status >= 300) {
769
+ if (isSameOrigin && (result.status === 401 || result.status === 403)) {
770
+ throw new AuthenticationError("Access token expired or invalid.");
771
+ }
772
+ this.throwForStatus(result.status, result.statusText);
773
+ }
774
+ return result.raw ?? new ArrayBuffer(0);
775
+ },
776
+ {
777
+ ...retryOptions,
778
+ onRetry: (error, attempt, delay) => {
779
+ if (this.verbose) {
780
+ console.log(
781
+ `Retry ${attempt}: ${error.message} (waiting ${Math.round(delay / 1e3)}s)`
782
+ );
783
+ }
784
+ }
785
+ }
786
+ );
787
+ }
788
+ };
789
+
790
+ // src/utils/credentials.ts
791
+ import fs from "fs/promises";
792
+ import path from "path";
793
+ function getTokenPath(sourceId = "chatgpt") {
794
+ return getSourceTokenPath(sourceId);
795
+ }
796
+ async function loadSavedToken(sourceId = "chatgpt") {
797
+ await ensureHomeLayout();
798
+ try {
799
+ const raw = await fs.readFile(getTokenPath(sourceId), "utf-8");
800
+ const token = raw.trim();
801
+ return token.startsWith("eyJ") ? token : null;
802
+ } catch {
803
+ return null;
804
+ }
805
+ }
806
+ async function saveToken(token, sourceId = "chatgpt") {
807
+ if (!token.startsWith("eyJ")) {
808
+ throw new Error(
809
+ "Refusing to save a value that does not look like a JWT accessToken"
810
+ );
811
+ }
812
+ await ensureHomeLayout();
813
+ const file = getTokenPath(sourceId);
814
+ await fs.mkdir(path.dirname(file), { recursive: true });
815
+ await fs.writeFile(file, `${token}
816
+ `, { encoding: "utf-8", mode: 384 });
817
+ await fs.chmod(file, 384).catch(() => void 0);
818
+ return file;
819
+ }
820
+
821
+ // src/sources/chatgpt/api/create-client.ts
822
+ async function createChatGptClient(options = {}) {
823
+ const preferCdp = options.preferCdp !== false;
824
+ let port = options.cdpPort ?? DEFAULT_CDP_PORT;
825
+ const poolSize = options.cdpPoolSize ?? 3;
826
+ const minGapMs = options.cdpMinGapMs ?? 150;
827
+ if (preferCdp) {
828
+ if (!await isCdpRunning(port)) {
829
+ const found = await discoverCdpPort(port);
830
+ if (found !== null) port = found;
831
+ }
832
+ if (await isCdpRunning(port)) {
833
+ const client = new ChatGPTClient(options.token ?? "", {
834
+ verbose: options.verbose,
835
+ cdpPort: port,
836
+ cdpPoolSize: poolSize,
837
+ cdpMinGapMs: minGapMs
838
+ });
839
+ return { client, mode: "cdp" };
840
+ }
841
+ }
842
+ const token = options.token?.trim() || process.env.PROMPT_EXPORTER_CHATGPT_TOKEN?.trim() || process.env.CHATGPT_TOKEN?.trim() || await loadSavedToken("chatgpt");
843
+ if (!token) {
844
+ throw new Error(
845
+ "No CDP browser and no access token. Run: prompt-exporter chromium start"
846
+ );
847
+ }
848
+ return {
849
+ client: new ChatGPTClient(token, { verbose: options.verbose }),
850
+ mode: "token"
851
+ };
852
+ }
853
+
854
+ // src/sources/chatgpt/api/pagination.ts
855
+ async function* fetchAllConversations(client, options = {}) {
856
+ const { limit = 100, delay = 500, onProgress } = options;
857
+ let offset = 0;
858
+ let total = Infinity;
859
+ let fetched = 0;
860
+ while (offset < total) {
861
+ const response = await client.fetch(
862
+ `${ENDPOINTS.CONVERSATIONS}?offset=${offset}&limit=${limit}&order=updated`,
863
+ {
864
+ parseResponse: (data) => ConversationsResponseSchema.parse(data)
865
+ }
866
+ );
867
+ total = response.total;
868
+ for (const item of response.items) {
869
+ yield item;
870
+ fetched++;
871
+ }
872
+ onProgress?.(fetched, total);
873
+ offset += limit;
874
+ if (offset < total && delay > 0) {
875
+ await sleep(delay);
876
+ }
877
+ }
878
+ }
879
+ async function countConversations(client) {
880
+ const response = await client.fetch(
881
+ `${ENDPOINTS.CONVERSATIONS}?offset=0&limit=1`,
882
+ {
883
+ parseResponse: (data) => ConversationsResponseSchema.parse(data)
884
+ }
885
+ );
886
+ return response.total;
887
+ }
888
+ async function fetchAllProjects(client, options = {}) {
889
+ const { delay = 500 } = options;
890
+ const allItems = [];
891
+ let cursor = null;
892
+ do {
893
+ const url = cursor ? `${ENDPOINTS.PROJECTS_SIDEBAR}?cursor=${encodeURIComponent(cursor)}` : ENDPOINTS.PROJECTS_SIDEBAR;
894
+ const response = await client.fetch(url, {
895
+ parseResponse: (data) => ProjectsSidebarResponseSchema.parse(data)
896
+ });
897
+ allItems.push(...response.items);
898
+ cursor = response.cursor;
899
+ if (cursor && delay > 0) {
900
+ await sleep(delay);
901
+ }
902
+ } while (cursor);
903
+ return allItems;
904
+ }
905
+ async function* fetchProjectConversations(client, gizmoId, options = {}) {
906
+ const { delay = 500, onProgress } = options;
907
+ let cursor = "0";
908
+ let fetched = 0;
909
+ while (cursor !== null && cursor !== void 0) {
910
+ const url = `${ENDPOINTS.PROJECT_CONVERSATIONS(gizmoId)}?cursor=${encodeURIComponent(cursor)}`;
911
+ const response = await client.fetch(url, {
912
+ parseResponse: (data) => ProjectConversationsResponseSchema.parse(data)
913
+ });
914
+ for (const item of response.items) {
915
+ yield item;
916
+ fetched++;
917
+ }
918
+ cursor = response.cursor;
919
+ onProgress?.(fetched, fetched);
920
+ if (cursor && delay > 0) {
921
+ await sleep(delay);
922
+ }
923
+ }
924
+ }
925
+
926
+ // src/services/backup-service.ts
927
+ var BackupService = class {
928
+ client;
929
+ storage;
930
+ constructor(client, storage) {
931
+ this.client = client;
932
+ this.storage = storage;
933
+ }
934
+ async listConversations(options) {
935
+ const conversations = [];
936
+ for await (const conversation of fetchAllConversations(this.client, {
937
+ delay: options.delay,
938
+ onProgress: options.onListProgress
939
+ })) {
940
+ conversations.push(conversation);
941
+ }
942
+ return conversations;
943
+ }
944
+ async getConversationCount() {
945
+ return countConversations(this.client);
946
+ }
947
+ async listProjects() {
948
+ return fetchAllProjects(this.client);
949
+ }
950
+ async listProjectConversations(gizmoId, options = {}) {
951
+ const conversations = [];
952
+ for await (const conversation of fetchProjectConversations(this.client, gizmoId, {
953
+ delay: options.delay,
954
+ onProgress: options.onListProgress
955
+ })) {
956
+ conversations.push(conversation);
957
+ }
958
+ return conversations;
959
+ }
960
+ async resolveProjectId(nameOrId) {
961
+ const projects = await this.listProjects();
962
+ const match = projects.find(
963
+ (p) => p.gizmo.id === nameOrId || p.gizmo.display.name.toLowerCase() === nameOrId.toLowerCase()
964
+ );
965
+ if (!match) {
966
+ const available = projects.map((p) => p.gizmo.display.name).join(", ");
967
+ throw new Error(
968
+ `Project "${nameOrId}" not found. Available projects: ${available || "(none)"}`
969
+ );
970
+ }
971
+ return { gizmoId: match.gizmo.id, name: match.gizmo.display.name };
972
+ }
973
+ async downloadConversation(id) {
974
+ return this.client.fetch(ENDPOINTS.CONVERSATION(id), {
975
+ parseResponse: (data) => ConversationDetailSchema.parse(data)
976
+ });
977
+ }
978
+ async backup(options) {
979
+ const {
980
+ concurrency,
981
+ delay,
982
+ incremental,
983
+ verbose,
984
+ projectGizmoId,
985
+ onListProgress,
986
+ onDownloadProgress,
987
+ onError
988
+ } = options;
989
+ await this.storage.initialize();
990
+ await this.storage.appendLog("Starting backup...");
991
+ const conversations = projectGizmoId ? await this.listProjectConversations(projectGizmoId, { delay, onListProgress }) : await this.listConversations({ delay, onListProgress });
992
+ await this.storage.saveIndex(conversations);
993
+ const errors = [];
994
+ let downloaded = 0;
995
+ let skipped = 0;
996
+ let failed = 0;
997
+ let completed = 0;
998
+ const toDownload = [];
999
+ for (const conv of conversations) {
1000
+ if (incremental) {
1001
+ const existingUpdateTime = await this.storage.getExistingConversationUpdateTime(conv.id);
1002
+ const convUpdateTime = typeof conv.update_time === "number" ? conv.update_time : conv.update_time ? new Date(conv.update_time).getTime() / 1e3 : null;
1003
+ if (existingUpdateTime !== null && convUpdateTime !== null && existingUpdateTime >= convUpdateTime) {
1004
+ skipped++;
1005
+ completed++;
1006
+ onDownloadProgress?.(completed, conversations.length, conv.title ?? conv.id);
1007
+ continue;
1008
+ }
1009
+ }
1010
+ toDownload.push(conv);
1011
+ }
1012
+ const downloadQueue = [...toDownload];
1013
+ const inProgress = /* @__PURE__ */ new Set();
1014
+ const processOne = async (conv) => {
1015
+ try {
1016
+ const detail = await this.downloadConversation(conv.id);
1017
+ await this.storage.saveConversation(conv.id, detail);
1018
+ downloaded++;
1019
+ if (verbose) {
1020
+ await this.storage.appendLog(`Downloaded: ${conv.id} - ${conv.title ?? "Untitled"}`);
1021
+ }
1022
+ } catch (error) {
1023
+ const errorMessage = error instanceof Error ? error.message : String(error);
1024
+ errors.push({ conversationId: conv.id, error: errorMessage });
1025
+ failed++;
1026
+ onError?.(conv.id, error);
1027
+ await this.storage.appendLog(`Failed: ${conv.id} - ${errorMessage}`);
1028
+ } finally {
1029
+ completed++;
1030
+ onDownloadProgress?.(completed, conversations.length, conv.title ?? conv.id);
1031
+ }
1032
+ };
1033
+ while (downloadQueue.length > 0 || inProgress.size > 0) {
1034
+ while (inProgress.size < concurrency && downloadQueue.length > 0) {
1035
+ const conv = downloadQueue.shift();
1036
+ const promise = processOne(conv).then(() => {
1037
+ inProgress.delete(promise);
1038
+ });
1039
+ inProgress.add(promise);
1040
+ if (downloadQueue.length > 0 && delay > 0) {
1041
+ await sleep(delay);
1042
+ }
1043
+ }
1044
+ if (inProgress.size > 0) {
1045
+ await Promise.race(inProgress);
1046
+ }
1047
+ }
1048
+ const metadata = {
1049
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1050
+ totalConversations: conversations.length,
1051
+ successfulDownloads: downloaded,
1052
+ failedDownloads: failed,
1053
+ errors
1054
+ };
1055
+ await this.storage.saveMetadata(metadata);
1056
+ await this.storage.appendLog(`Backup completed: ${downloaded} downloaded, ${skipped} skipped, ${failed} failed`);
1057
+ return {
1058
+ totalConversations: conversations.length,
1059
+ downloaded,
1060
+ skipped,
1061
+ failed,
1062
+ errors
1063
+ };
1064
+ }
1065
+ };
1066
+
1067
+ // src/services/file-service.ts
1068
+ import fs3 from "fs/promises";
1069
+ import path3 from "path";
1070
+
1071
+ // src/utils/conversation-files.ts
1072
+ import fs2 from "fs/promises";
1073
+ import path2 from "path";
1074
+ function conversationsRoot(outputDir, projectName) {
1075
+ if (projectName) {
1076
+ const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1077
+ return path2.join(outputDir, "projects", safeName, "conversations");
1078
+ }
1079
+ return path2.join(outputDir, "conversations");
1080
+ }
1081
+ function jsonDir(conversationsDir) {
1082
+ return path2.join(conversationsDir, "json");
1083
+ }
1084
+ function jsonPathForId(conversationsDir, id) {
1085
+ return path2.join(jsonDir(conversationsDir), `${id}.json`);
1086
+ }
1087
+ function indexPath(conversationsDir) {
1088
+ return path2.join(jsonDir(conversationsDir), "index.json");
1089
+ }
1090
+ function toDate(value) {
1091
+ if (value == null) return null;
1092
+ if (typeof value === "number") {
1093
+ const ms = value > 1e12 ? value : value * 1e3;
1094
+ const d2 = new Date(ms);
1095
+ return Number.isNaN(d2.getTime()) ? null : d2;
1096
+ }
1097
+ const asNum = Number(value);
1098
+ if (Number.isFinite(asNum) && value.trim() !== "") {
1099
+ return toDate(asNum);
1100
+ }
1101
+ const d = new Date(value);
1102
+ return Number.isNaN(d.getTime()) ? null : d;
1103
+ }
1104
+ function formatUpdateStamp(date) {
1105
+ const y = date.getFullYear();
1106
+ const m = String(date.getMonth() + 1).padStart(2, "0");
1107
+ const d = String(date.getDate()).padStart(2, "0");
1108
+ const hh = String(date.getHours()).padStart(2, "0");
1109
+ const mm = String(date.getMinutes()).padStart(2, "0");
1110
+ return `${y}-${m}-${d}-${hh}${mm}`;
1111
+ }
1112
+ function sanitizeTitleForFilename(title) {
1113
+ const base = (title?.trim() || "Untitled").replace(/[/\\]/g, "-").replace(/[:*?"<>|]/g, "-").replace(/[\0\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").trim().replace(/^\.+/, "").slice(0, 120).trim();
1114
+ return base || "Untitled";
1115
+ }
1116
+ function markdownFilename(detail) {
1117
+ const date = toDate(detail.update_time) ?? toDate(detail.create_time) ?? /* @__PURE__ */ new Date(0);
1118
+ const stamp = formatUpdateStamp(date);
1119
+ const title = sanitizeTitleForFilename(detail.title);
1120
+ return `${stamp}.${title}.md`;
1121
+ }
1122
+ function buildFrontmatter(meta) {
1123
+ const lines = ["---"];
1124
+ lines.push(`id: ${yamlScalar(meta.id)}`);
1125
+ lines.push(`title: ${yamlScalar(meta.title)}`);
1126
+ if (meta.updated) lines.push(`updated: ${yamlScalar(meta.updated)}`);
1127
+ if (meta.created) lines.push(`created: ${yamlScalar(meta.created)}`);
1128
+ lines.push(`json: ${yamlScalar(meta.json)}`);
1129
+ lines.push("---");
1130
+ return lines.join("\n");
1131
+ }
1132
+ function yamlScalar(value) {
1133
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value) && !value.includes(":")) {
1134
+ return value;
1135
+ }
1136
+ return JSON.stringify(value);
1137
+ }
1138
+ function relativeJsonFromMd(id) {
1139
+ return path2.posix.join("json", `${id}.json`);
1140
+ }
1141
+ function isoOrNull(value) {
1142
+ const d = toDate(value);
1143
+ return d ? d.toISOString() : null;
1144
+ }
1145
+ async function listJsonInDir(dir) {
1146
+ const files = [];
1147
+ try {
1148
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
1149
+ for (const entry of entries) {
1150
+ if (entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json") {
1151
+ files.push(path2.join(dir, entry.name));
1152
+ }
1153
+ }
1154
+ } catch {
1155
+ }
1156
+ return files;
1157
+ }
1158
+ async function collectAllJsonFiles(inputDir) {
1159
+ const byId = /* @__PURE__ */ new Map();
1160
+ const consider = (filePath, preferred) => {
1161
+ const id = path2.basename(filePath, ".json");
1162
+ const prev = byId.get(id);
1163
+ if (!prev || preferred && !prev.preferred) {
1164
+ byId.set(id, { path: filePath, preferred });
1165
+ }
1166
+ };
1167
+ const roots = [conversationsRoot(inputDir)];
1168
+ try {
1169
+ const projectsDir = path2.join(inputDir, "projects");
1170
+ const entries = await fs2.readdir(projectsDir, { withFileTypes: true });
1171
+ for (const entry of entries) {
1172
+ if (entry.isDirectory()) {
1173
+ roots.push(conversationsRoot(inputDir, entry.name));
1174
+ }
1175
+ }
1176
+ } catch {
1177
+ }
1178
+ for (const root of roots) {
1179
+ for (const f of await listJsonInDir(jsonDir(root))) {
1180
+ consider(f, true);
1181
+ }
1182
+ for (const f of await listJsonInDir(root)) {
1183
+ consider(f, false);
1184
+ }
1185
+ }
1186
+ return [...byId.values()].map((v) => v.path);
1187
+ }
1188
+
1189
+ // src/services/file-service.ts
1190
+ var FILE_SERVICE_PREFIX = "file-service://";
1191
+ function extractFileIdFromPointer(pointer) {
1192
+ if (!pointer.startsWith(FILE_SERVICE_PREFIX)) return null;
1193
+ return pointer.slice(FILE_SERVICE_PREFIX.length);
1194
+ }
1195
+ function extractFileReferences(detail) {
1196
+ const refs = /* @__PURE__ */ new Map();
1197
+ for (const node of Object.values(detail.mapping)) {
1198
+ const msg = node.message;
1199
+ if (!msg) continue;
1200
+ if (msg.content?.parts) {
1201
+ for (const part of msg.content.parts) {
1202
+ if (part && typeof part === "object") {
1203
+ const obj = part;
1204
+ if (obj.content_type === "image_asset_pointer") {
1205
+ const pointer = obj.asset_pointer;
1206
+ if (pointer) {
1207
+ const fileId = extractFileIdFromPointer(pointer);
1208
+ if (fileId && !refs.has(fileId)) {
1209
+ refs.set(fileId, { fileId, source: "image_asset_pointer" });
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ const metadata = msg.metadata;
1217
+ if (metadata?.attachments && Array.isArray(metadata.attachments)) {
1218
+ for (const att of metadata.attachments) {
1219
+ const a = att;
1220
+ const id = a.id;
1221
+ if (id && !refs.has(id)) {
1222
+ refs.set(id, {
1223
+ fileId: id,
1224
+ filename: a.name,
1225
+ source: "attachment"
1226
+ });
1227
+ }
1228
+ }
1229
+ }
1230
+ if (metadata?.citations && Array.isArray(metadata.citations)) {
1231
+ for (const cit of metadata.citations) {
1232
+ const c = cit;
1233
+ const metadata2 = c.metadata;
1234
+ const fileId = metadata2?.file_id ?? c.file_id;
1235
+ if (fileId && !refs.has(fileId)) {
1236
+ refs.set(fileId, {
1237
+ fileId,
1238
+ filename: metadata2?.title ?? c.title,
1239
+ source: "citation"
1240
+ });
1241
+ }
1242
+ }
1243
+ }
1244
+ }
1245
+ return Array.from(refs.values());
1246
+ }
1247
+ async function scanAllConversations(inputDir) {
1248
+ const refs = /* @__PURE__ */ new Map();
1249
+ const jsonFiles = await collectAllJsonFiles(inputDir);
1250
+ for (const jsonPath of jsonFiles) {
1251
+ try {
1252
+ const raw = await fs3.readFile(jsonPath, "utf-8");
1253
+ const detail = JSON.parse(raw);
1254
+ for (const ref of extractFileReferences(detail)) {
1255
+ if (!refs.has(ref.fileId)) {
1256
+ refs.set(ref.fileId, ref);
1257
+ }
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ }
1262
+ return Array.from(refs.values());
1263
+ }
1264
+ async function dirHasFiles(dir) {
1265
+ try {
1266
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
1267
+ for (const entry of entries) {
1268
+ if (entry.isFile()) return true;
1269
+ if (entry.isDirectory()) {
1270
+ if (await dirHasFiles(path3.join(dir, entry.name))) return true;
1271
+ }
1272
+ }
1273
+ } catch {
1274
+ }
1275
+ return false;
1276
+ }
1277
+ async function downloadFiles(client, allRefs, outputDir, options) {
1278
+ const { concurrency, delay, verbose, skipFileIds, onProgress, onError } = options;
1279
+ const filesDir = path3.join(outputDir, "files");
1280
+ const refs = skipFileIds && skipFileIds.size > 0 ? allRefs.filter((ref) => !skipFileIds.has(ref.fileId)) : allRefs;
1281
+ let downloaded = 0;
1282
+ let skipped = 0;
1283
+ let failed = 0;
1284
+ let completed = 0;
1285
+ const failedFileIds = [];
1286
+ const queue = [...refs];
1287
+ const inProgress = /* @__PURE__ */ new Set();
1288
+ const reportProgress = () => {
1289
+ completed++;
1290
+ onProgress?.(completed, refs.length, { downloaded, skipped, failed });
1291
+ };
1292
+ const processOne = async (ref) => {
1293
+ const fileDir = path3.join(filesDir, ref.fileId);
1294
+ if (await dirHasFiles(fileDir)) {
1295
+ skipped++;
1296
+ reportProgress();
1297
+ return;
1298
+ }
1299
+ try {
1300
+ const meta = await client.fetch(ENDPOINTS.FILE_DOWNLOAD(ref.fileId), {
1301
+ retryOptions: { maxRetries: 1 }
1302
+ });
1303
+ if (!meta.download_url) {
1304
+ throw new Error("File not available");
1305
+ }
1306
+ const buffer = await client.fetchRaw(meta.download_url, { maxRetries: 1 });
1307
+ const filename = meta.file_name || ref.filename || ref.fileId;
1308
+ const filePath = path3.join(fileDir, filename);
1309
+ await fs3.mkdir(path3.dirname(filePath), { recursive: true });
1310
+ await fs3.writeFile(filePath, Buffer.from(buffer));
1311
+ downloaded++;
1312
+ if (verbose) {
1313
+ console.log(` Downloaded file: ${ref.fileId} \u2192 ${filename}`);
1314
+ }
1315
+ } catch (error) {
1316
+ failed++;
1317
+ failedFileIds.push(ref.fileId);
1318
+ onError?.(ref.fileId, error);
1319
+ } finally {
1320
+ reportProgress();
1321
+ }
1322
+ };
1323
+ while (queue.length > 0 || inProgress.size > 0) {
1324
+ while (inProgress.size < concurrency && queue.length > 0) {
1325
+ const ref = queue.shift();
1326
+ const promise = processOne(ref).then(() => {
1327
+ inProgress.delete(promise);
1328
+ });
1329
+ inProgress.add(promise);
1330
+ if (queue.length > 0 && delay > 0) {
1331
+ await sleep(delay);
1332
+ }
1333
+ }
1334
+ if (inProgress.size > 0) {
1335
+ await Promise.race(inProgress);
1336
+ }
1337
+ }
1338
+ return { downloaded, skipped, failed, total: refs.length, failedFileIds };
1339
+ }
1340
+ async function findFirstFile(dir) {
1341
+ try {
1342
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
1343
+ for (const entry of entries) {
1344
+ if (entry.isFile()) return entry.name;
1345
+ if (entry.isDirectory()) {
1346
+ const nested = await findFirstFile(path3.join(dir, entry.name));
1347
+ if (nested) return path3.join(entry.name, nested);
1348
+ }
1349
+ }
1350
+ } catch {
1351
+ }
1352
+ return null;
1353
+ }
1354
+ async function buildFileMap(filesDir) {
1355
+ const fileMap = /* @__PURE__ */ new Map();
1356
+ try {
1357
+ const fileIdDirs = await fs3.readdir(filesDir, { withFileTypes: true });
1358
+ for (const entry of fileIdDirs) {
1359
+ if (!entry.isDirectory()) continue;
1360
+ const fileId = entry.name;
1361
+ const dirPath = path3.join(filesDir, fileId);
1362
+ const relFile = await findFirstFile(dirPath);
1363
+ if (relFile) {
1364
+ fileMap.set(fileId, path3.join("files", fileId, relFile));
1365
+ }
1366
+ }
1367
+ } catch {
1368
+ }
1369
+ return fileMap;
1370
+ }
1371
+
1372
+ // src/services/markdown-service.ts
1373
+ import fs4 from "fs/promises";
1374
+ import path4 from "path";
1375
+ function extractTextFromParts(parts, fileMap) {
1376
+ const pieces = [];
1377
+ for (const part of parts) {
1378
+ if (typeof part === "string") {
1379
+ pieces.push(part);
1380
+ } else if (part && typeof part === "object") {
1381
+ const obj = part;
1382
+ if (obj.content_type === "image_asset_pointer") {
1383
+ const pointer = obj.asset_pointer;
1384
+ if (pointer && fileMap) {
1385
+ const fileId = pointer.startsWith("file-service://") ? pointer.slice("file-service://".length) : null;
1386
+ if (fileId && fileMap.has(fileId)) {
1387
+ pieces.push(`![image](${fileMap.get(fileId)})`);
1388
+ continue;
1389
+ }
1390
+ }
1391
+ pieces.push(`![image](${pointer ?? "unknown"})`);
1392
+ }
1393
+ }
1394
+ }
1395
+ return pieces.join("\n");
1396
+ }
1397
+ function getLinearThread(mapping, currentNode) {
1398
+ if (currentNode && mapping[currentNode]) {
1399
+ const pathNodes = [];
1400
+ let nodeId = currentNode;
1401
+ while (nodeId && mapping[nodeId]) {
1402
+ pathNodes.unshift(mapping[nodeId]);
1403
+ nodeId = mapping[nodeId].parent;
1404
+ }
1405
+ return pathNodes;
1406
+ }
1407
+ let root;
1408
+ for (const node of Object.values(mapping)) {
1409
+ if (node.parent === null || node.parent === void 0) {
1410
+ root = node;
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (!root) return [];
1415
+ const thread = [root];
1416
+ let current = root;
1417
+ while (current.children && current.children.length > 0) {
1418
+ const nextId = current.children[0];
1419
+ const next = mapping[nextId];
1420
+ if (!next) break;
1421
+ thread.push(next);
1422
+ current = next;
1423
+ }
1424
+ return thread;
1425
+ }
1426
+ function formatRole(role) {
1427
+ switch (role) {
1428
+ case "user":
1429
+ return "User";
1430
+ case "assistant":
1431
+ return "Assistant";
1432
+ default:
1433
+ return role.charAt(0).toUpperCase() + role.slice(1);
1434
+ }
1435
+ }
1436
+ function convertConversation(detail, fileMap, options = {}) {
1437
+ const id = options.conversationId ?? detail.conversation_id ?? detail.id ?? "unknown";
1438
+ const title = detail.title ?? "Untitled";
1439
+ const frontmatter = buildFrontmatter({
1440
+ id,
1441
+ title,
1442
+ updated: isoOrNull(detail.update_time),
1443
+ created: isoOrNull(detail.create_time),
1444
+ json: relativeJsonFromMd(id)
1445
+ });
1446
+ const lines = [frontmatter, "", `# ${title}`];
1447
+ const thread = getLinearThread(
1448
+ detail.mapping,
1449
+ detail.current_node
1450
+ );
1451
+ let firstMessage = true;
1452
+ for (const node of thread) {
1453
+ const msg = node.message;
1454
+ if (!msg) continue;
1455
+ const role = msg.author.role;
1456
+ if (role === "system" || role === "tool") continue;
1457
+ if (msg.metadata?.is_visually_hidden_from_conversation) continue;
1458
+ if (msg.weight === 0) continue;
1459
+ let text = "";
1460
+ if (msg.content) {
1461
+ if (msg.content.parts && msg.content.parts.length > 0) {
1462
+ text = extractTextFromParts(msg.content.parts, fileMap);
1463
+ } else if (msg.content.text) {
1464
+ text = msg.content.text;
1465
+ }
1466
+ }
1467
+ if (!text.trim()) continue;
1468
+ if (firstMessage) {
1469
+ lines.push("");
1470
+ firstMessage = false;
1471
+ } else {
1472
+ lines.push("");
1473
+ lines.push("---");
1474
+ lines.push("");
1475
+ }
1476
+ lines.push(`**${formatRole(role)}:**`);
1477
+ lines.push("");
1478
+ lines.push(text);
1479
+ }
1480
+ lines.push("");
1481
+ return lines.join("\n");
1482
+ }
1483
+ async function removeStaleMarkdown(conversationsDir, conversationId, keepName) {
1484
+ let entries;
1485
+ try {
1486
+ entries = await fs4.readdir(conversationsDir, { withFileTypes: true });
1487
+ } catch {
1488
+ return;
1489
+ }
1490
+ const needle = `id: ${conversationId}`;
1491
+ const needleQuoted = `id: "${conversationId}"`;
1492
+ for (const entry of entries) {
1493
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1494
+ if (entry.name === keepName) continue;
1495
+ const full = path4.join(conversationsDir, entry.name);
1496
+ try {
1497
+ const head = await fs4.readFile(full, "utf-8");
1498
+ const fm = head.slice(0, 800);
1499
+ if (fm.includes(needle) || fm.includes(needleQuoted)) {
1500
+ await fs4.unlink(full);
1501
+ }
1502
+ } catch {
1503
+ }
1504
+ }
1505
+ const legacySidecar = path4.join(conversationsDir, `${conversationId}.md`);
1506
+ if (path4.basename(legacySidecar) !== keepName) {
1507
+ await fs4.unlink(legacySidecar).catch(() => void 0);
1508
+ }
1509
+ }
1510
+ var UUID_MD = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.md$/i;
1511
+ async function removeLegacyUuidMarkdown(conversationsDir) {
1512
+ let entries;
1513
+ try {
1514
+ entries = await fs4.readdir(conversationsDir, { withFileTypes: true });
1515
+ } catch {
1516
+ return;
1517
+ }
1518
+ for (const entry of entries) {
1519
+ if (entry.isFile() && UUID_MD.test(entry.name)) {
1520
+ await fs4.unlink(path4.join(conversationsDir, entry.name)).catch(() => void 0);
1521
+ }
1522
+ }
1523
+ }
1524
+ async function convertDirectory(inputDir, filesDir) {
1525
+ const jsonFiles = await collectAllJsonFiles(inputDir);
1526
+ const globalFileMap = filesDir ? await buildFileMap(filesDir) : null;
1527
+ let converted = 0;
1528
+ let errors = 0;
1529
+ const touchedDirs = /* @__PURE__ */ new Set();
1530
+ for (const jsonPath of jsonFiles) {
1531
+ try {
1532
+ const raw = await fs4.readFile(jsonPath, "utf-8");
1533
+ const detail = JSON.parse(raw);
1534
+ const id = detail.conversation_id ?? detail.id ?? path4.basename(jsonPath, ".json");
1535
+ const conversationsDir = jsonPath.includes(`${path4.sep}json${path4.sep}`) ? path4.dirname(path4.dirname(jsonPath)) : path4.dirname(jsonPath);
1536
+ touchedDirs.add(conversationsDir);
1537
+ let fileMap;
1538
+ if (globalFileMap && globalFileMap.size > 0) {
1539
+ fileMap = /* @__PURE__ */ new Map();
1540
+ for (const [fileId, filePath] of globalFileMap) {
1541
+ const absFilePath = path4.join(inputDir, filePath);
1542
+ fileMap.set(fileId, path4.relative(conversationsDir, absFilePath));
1543
+ }
1544
+ }
1545
+ const markdown = convertConversation(detail, fileMap, {
1546
+ conversationId: id
1547
+ });
1548
+ const mdName = markdownFilename(detail);
1549
+ const mdPath = path4.join(conversationsDir, mdName);
1550
+ await removeStaleMarkdown(conversationsDir, id, mdName);
1551
+ await fs4.writeFile(mdPath, markdown, "utf-8");
1552
+ converted++;
1553
+ } catch {
1554
+ errors++;
1555
+ }
1556
+ }
1557
+ for (const dir of touchedDirs) {
1558
+ await removeLegacyUuidMarkdown(dir);
1559
+ }
1560
+ return { converted, errors };
1561
+ }
1562
+
1563
+ // src/services/storage-service.ts
1564
+ import fs5 from "fs/promises";
1565
+ import path5 from "path";
1566
+ var StorageService = class {
1567
+ outputDir;
1568
+ conversationsDir;
1569
+ constructor(outputDir, projectName) {
1570
+ this.outputDir = outputDir;
1571
+ this.conversationsDir = conversationsRoot(outputDir, projectName);
1572
+ }
1573
+ getConversationsDir() {
1574
+ return this.conversationsDir;
1575
+ }
1576
+ async initialize() {
1577
+ await fs5.mkdir(jsonDir(this.conversationsDir), { recursive: true });
1578
+ await this.migrateLegacyJson();
1579
+ }
1580
+ /** Move conversations/*.json → conversations/json/*.json (one-time). */
1581
+ async migrateLegacyJson() {
1582
+ let entries;
1583
+ try {
1584
+ entries = await fs5.readdir(this.conversationsDir, { withFileTypes: true });
1585
+ } catch {
1586
+ return;
1587
+ }
1588
+ for (const entry of entries) {
1589
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
1590
+ const from = path5.join(this.conversationsDir, entry.name);
1591
+ const to = path5.join(jsonDir(this.conversationsDir), entry.name);
1592
+ try {
1593
+ await fs5.rename(from, to);
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ }
1598
+ async saveConversation(id, data) {
1599
+ const filePath = jsonPathForId(this.conversationsDir, id);
1600
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
1601
+ await fs5.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
1602
+ }
1603
+ async saveIndex(conversations) {
1604
+ const filePath = indexPath(this.conversationsDir);
1605
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
1606
+ await fs5.writeFile(filePath, JSON.stringify(conversations, null, 2), "utf-8");
1607
+ }
1608
+ async saveMetadata(metadata) {
1609
+ const filePath = path5.join(this.outputDir, "metadata.json");
1610
+ await fs5.writeFile(filePath, JSON.stringify(metadata, null, 2), "utf-8");
1611
+ }
1612
+ async appendLog(message) {
1613
+ const filePath = path5.join(this.outputDir, "backup.log");
1614
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1615
+ await fs5.appendFile(filePath, `[${timestamp}] ${message}
1616
+ `, "utf-8");
1617
+ }
1618
+ async conversationExists(id) {
1619
+ try {
1620
+ await fs5.access(jsonPathForId(this.conversationsDir, id));
1621
+ return true;
1622
+ } catch {
1623
+ return false;
1624
+ }
1625
+ }
1626
+ async getExistingConversationUpdateTime(id) {
1627
+ try {
1628
+ const content = await fs5.readFile(
1629
+ jsonPathForId(this.conversationsDir, id),
1630
+ "utf-8"
1631
+ );
1632
+ const data = JSON.parse(content);
1633
+ return data.update_time ?? null;
1634
+ } catch {
1635
+ return null;
1636
+ }
1637
+ }
1638
+ getConversationPath(id) {
1639
+ return jsonPathForId(this.conversationsDir, id);
1640
+ }
1641
+ };
1642
+
1643
+ // src/utils/progress.ts
1644
+ import cliProgress from "cli-progress";
1645
+ import chalk from "chalk";
1646
+ function createProgressBar(total, label) {
1647
+ const bar = new cliProgress.SingleBar({
1648
+ format: `${chalk.cyan(label)} |${chalk.cyan("{bar}")}| {percentage}% | {value}/{total}`,
1649
+ barCompleteChar: "\u2588",
1650
+ barIncompleteChar: "\u2591",
1651
+ hideCursor: true,
1652
+ gracefulExit: true
1653
+ });
1654
+ bar.start(total, 0);
1655
+ return bar;
1656
+ }
1657
+
1658
+ // src/cli/commands/backup.ts
1659
+ function runBackupWithProgress(service, spinner, options) {
1660
+ let listProgressBar = null;
1661
+ let downloadProgressBar = null;
1662
+ let listingDone = false;
1663
+ return service.backup({
1664
+ ...options,
1665
+ onListProgress: (fetched, totalList) => {
1666
+ if (!listingDone) {
1667
+ if (!listProgressBar) {
1668
+ spinner.stop();
1669
+ listProgressBar = createProgressBar(totalList, "Listing ");
1670
+ }
1671
+ listProgressBar.setTotal(totalList);
1672
+ listProgressBar.update(fetched);
1673
+ if (fetched >= totalList) {
1674
+ listProgressBar.stop();
1675
+ listingDone = true;
1676
+ console.log();
1677
+ }
1678
+ }
1679
+ },
1680
+ onDownloadProgress: (completed, totalDownload) => {
1681
+ if (listingDone) {
1682
+ if (!downloadProgressBar) {
1683
+ downloadProgressBar = createProgressBar(
1684
+ totalDownload,
1685
+ "Downloading"
1686
+ );
1687
+ }
1688
+ downloadProgressBar.update(completed);
1689
+ if (options.verbose && options.getThrottleSummary) {
1690
+ const summary = options.getThrottleSummary();
1691
+ if (summary && completed % 10 === 0) {
1692
+ process.stderr.write(`
1693
+ ${chalk2.dim(`[throttle] ${summary}`)}
1694
+ `);
1695
+ }
1696
+ }
1697
+ }
1698
+ },
1699
+ onError: (id, error) => {
1700
+ if (options.verbose) {
1701
+ console.error(
1702
+ chalk2.red(`
1703
+ Failed to download ${id}: ${error.message}`)
1704
+ );
1705
+ }
1706
+ }
1707
+ }).then((result) => {
1708
+ downloadProgressBar?.stop();
1709
+ if (options.verbose && options.getThrottleSummary) {
1710
+ const summary = options.getThrottleSummary();
1711
+ if (summary) {
1712
+ console.log(chalk2.dim(`
1713
+ Throttle stats: ${summary}`));
1714
+ }
1715
+ }
1716
+ return result;
1717
+ });
1718
+ }
1719
+ async function backupCommand(options) {
1720
+ const {
1721
+ token,
1722
+ output,
1723
+ concurrency: requestedConcurrency,
1724
+ delay: requestedDelay,
1725
+ incremental,
1726
+ downloadFiles: shouldDownloadFiles,
1727
+ verbose,
1728
+ project,
1729
+ cdpPort
1730
+ } = options;
1731
+ const spinner = ora("Connecting...").start();
1732
+ let client;
1733
+ let mode = "token";
1734
+ try {
1735
+ const concurrency = Math.min(Math.max(requestedConcurrency, 1), 6);
1736
+ const delay = Math.max(0, requestedDelay);
1737
+ const created = await createChatGptClient({
1738
+ token,
1739
+ verbose,
1740
+ cdpPort,
1741
+ cdpPoolSize: concurrency,
1742
+ cdpMinGapMs: Math.max(delay, 50)
1743
+ });
1744
+ client = created.client;
1745
+ mode = created.mode;
1746
+ const effectiveConcurrency = mode === "cdp" ? concurrency : requestedConcurrency;
1747
+ const effectiveDelay = delay;
1748
+ spinner.text = mode === "cdp" ? `Authenticating via Chromium CDP (${effectiveConcurrency} tabs)...` : "Authenticating via access token...";
1749
+ await client.initialize();
1750
+ spinner.succeed(
1751
+ mode === "cdp" ? `Authenticated (Chromium CDP, ${client.cdpWorkers || effectiveConcurrency} workers)` : "Authenticated (access token)"
1752
+ );
1753
+ const activeClient = client;
1754
+ const getThrottleSummary = () => activeClient.throttleStats?.summary();
1755
+ console.log(chalk2.dim(`
1756
+ Backup settings:`));
1757
+ console.log(chalk2.dim(` Output: ${output}`));
1758
+ console.log(chalk2.dim(` Concurrency: ${effectiveConcurrency}`));
1759
+ console.log(chalk2.dim(` Delay/gap: ${effectiveDelay}ms`));
1760
+ if (mode === "cdp") {
1761
+ console.log(
1762
+ chalk2.dim(
1763
+ ` Transport: CDP tab pool (tune --concurrency / --delay; -v prints throttle stats)`
1764
+ )
1765
+ );
1766
+ }
1767
+ console.log(chalk2.dim(` Incremental: ${incremental}`));
1768
+ console.log(chalk2.dim(` Download files: ${shouldDownloadFiles}`));
1769
+ if (project) {
1770
+ console.log(chalk2.dim(` Project: ${project}`));
1771
+ }
1772
+ console.log();
1773
+ if (project) {
1774
+ const baseService = new BackupService(client, new StorageService(output));
1775
+ spinner.start("Resolving project...");
1776
+ const { gizmoId, name } = await baseService.resolveProjectId(project);
1777
+ spinner.succeed(`Project: ${name}`);
1778
+ const storage = new StorageService(output, name);
1779
+ const service = new BackupService(client, storage);
1780
+ spinner.start(`Backing up project "${name}"...`);
1781
+ const result = await runBackupWithProgress(service, spinner, {
1782
+ concurrency: effectiveConcurrency,
1783
+ delay: effectiveDelay,
1784
+ incremental,
1785
+ verbose,
1786
+ projectGizmoId: gizmoId,
1787
+ getThrottleSummary
1788
+ });
1789
+ if (shouldDownloadFiles) {
1790
+ await runFileDownloads(client, output, {
1791
+ concurrency: effectiveConcurrency,
1792
+ delay: effectiveDelay,
1793
+ verbose
1794
+ });
1795
+ }
1796
+ const filesDir = shouldDownloadFiles ? path6.join(output, "files") : void 0;
1797
+ const md = await convertDirectory(output, filesDir);
1798
+ printResult(result, output);
1799
+ console.log(`Converted ${md.converted} conversations to markdown.`);
1800
+ if (md.errors > 0) {
1801
+ console.log(`Markdown conversion errors: ${chalk2.red(md.errors)}`);
1802
+ }
1803
+ } else {
1804
+ let totalDownloaded = 0;
1805
+ let totalSkipped = 0;
1806
+ let totalFailed = 0;
1807
+ let totalConversations = 0;
1808
+ console.log(chalk2.bold("Main conversations\n"));
1809
+ const mainStorage = new StorageService(output);
1810
+ const mainService = new BackupService(client, mainStorage);
1811
+ spinner.start("Fetching conversation list...");
1812
+ const mainResult = await runBackupWithProgress(mainService, spinner, {
1813
+ concurrency: effectiveConcurrency,
1814
+ delay: effectiveDelay,
1815
+ incremental,
1816
+ verbose,
1817
+ getThrottleSummary
1818
+ });
1819
+ totalDownloaded += mainResult.downloaded;
1820
+ totalSkipped += mainResult.skipped;
1821
+ totalFailed += mainResult.failed;
1822
+ totalConversations += mainResult.totalConversations;
1823
+ console.log();
1824
+ console.log(
1825
+ ` Downloaded: ${chalk2.green(mainResult.downloaded)}, Skipped: ${chalk2.yellow(mainResult.skipped)}, Failed: ${chalk2.red(mainResult.failed)}`
1826
+ );
1827
+ console.log();
1828
+ spinner.start("Fetching projects...");
1829
+ const projects = await mainService.listProjects();
1830
+ spinner.succeed(`Found ${projects.length} projects`);
1831
+ for (const proj of projects) {
1832
+ const name = proj.gizmo.display.name;
1833
+ const gizmoId = proj.gizmo.id;
1834
+ console.log();
1835
+ console.log(chalk2.bold(`Project: ${name}
1836
+ `));
1837
+ const projStorage = new StorageService(output, name);
1838
+ const projService = new BackupService(client, projStorage);
1839
+ spinner.start(`Backing up "${name}"...`);
1840
+ const projResult = await runBackupWithProgress(projService, spinner, {
1841
+ concurrency: effectiveConcurrency,
1842
+ delay: effectiveDelay,
1843
+ incremental,
1844
+ verbose,
1845
+ projectGizmoId: gizmoId,
1846
+ getThrottleSummary
1847
+ });
1848
+ totalDownloaded += projResult.downloaded;
1849
+ totalSkipped += projResult.skipped;
1850
+ totalFailed += projResult.failed;
1851
+ totalConversations += projResult.totalConversations;
1852
+ console.log();
1853
+ console.log(
1854
+ ` Downloaded: ${chalk2.green(projResult.downloaded)}, Skipped: ${chalk2.yellow(projResult.skipped)}, Failed: ${chalk2.red(projResult.failed)}`
1855
+ );
1856
+ }
1857
+ if (shouldDownloadFiles) {
1858
+ await runFileDownloads(client, output, {
1859
+ concurrency: effectiveConcurrency,
1860
+ delay: effectiveDelay,
1861
+ verbose
1862
+ });
1863
+ }
1864
+ const filesDir = shouldDownloadFiles ? path6.join(output, "files") : void 0;
1865
+ const md = await convertDirectory(output, filesDir);
1866
+ console.log();
1867
+ console.log(chalk2.green("\nBackup completed!"));
1868
+ console.log(` Total conversations: ${totalConversations}`);
1869
+ console.log(` Downloaded: ${chalk2.green(totalDownloaded)}`);
1870
+ if (totalSkipped > 0) {
1871
+ console.log(` Skipped (unchanged): ${chalk2.yellow(totalSkipped)}`);
1872
+ }
1873
+ if (totalFailed > 0) {
1874
+ console.log(` Failed: ${chalk2.red(totalFailed)}`);
1875
+ }
1876
+ console.log(` Converted ${md.converted} conversations to markdown`);
1877
+ if (md.errors > 0) {
1878
+ console.log(` Markdown errors: ${chalk2.red(md.errors)}`);
1879
+ }
1880
+ console.log(`
1881
+ Output directory: ${chalk2.cyan(output)}`);
1882
+ }
1883
+ } catch (error) {
1884
+ spinner.fail("Backup failed");
1885
+ if (error instanceof Error) {
1886
+ if (error.name === "AuthenticationError") {
1887
+ console.error(chalk2.red(`
1888
+ Authentication failed: ${error.message}`));
1889
+ console.error(
1890
+ chalk2.yellow(
1891
+ "\nKeep Chromium CDP logged in to chatgpt.com, then retry:"
1892
+ )
1893
+ );
1894
+ console.error(" prompt-exporter chromium start");
1895
+ console.error(" prompt-exporter sync");
1896
+ console.error(
1897
+ chalk2.dim(
1898
+ "\nNode Bearer requests are blocked by Cloudflare (HTTP 403)."
1899
+ )
1900
+ );
1901
+ } else {
1902
+ console.error(chalk2.red(`
1903
+ Error: ${error.message}`));
1904
+ }
1905
+ }
1906
+ process.exit(1);
1907
+ } finally {
1908
+ await client?.close();
1909
+ }
1910
+ }
1911
+ function printResult(result, output) {
1912
+ console.log();
1913
+ console.log(chalk2.green("\nBackup completed!"));
1914
+ console.log(` Total conversations: ${result.totalConversations}`);
1915
+ console.log(` Downloaded: ${chalk2.green(result.downloaded)}`);
1916
+ if (result.skipped > 0) {
1917
+ console.log(` Skipped (unchanged): ${chalk2.yellow(result.skipped)}`);
1918
+ }
1919
+ if (result.failed > 0) {
1920
+ console.log(` Failed: ${chalk2.red(result.failed)}`);
1921
+ console.log(chalk2.dim(` See ${output}/backup.log for details`));
1922
+ }
1923
+ console.log(`
1924
+ Output directory: ${chalk2.cyan(output)}`);
1925
+ }
1926
+ async function loadFailedFiles(output) {
1927
+ try {
1928
+ const raw = await fs6.readFile(path6.join(output, "metadata.json"), "utf-8");
1929
+ const metadata = JSON.parse(raw);
1930
+ if (Array.isArray(metadata.failedFiles)) {
1931
+ return new Set(metadata.failedFiles);
1932
+ }
1933
+ } catch {
1934
+ }
1935
+ return /* @__PURE__ */ new Set();
1936
+ }
1937
+ async function saveFailedFiles(output, failedFileIds) {
1938
+ const metadataPath = path6.join(output, "metadata.json");
1939
+ let metadata = {};
1940
+ try {
1941
+ const raw = await fs6.readFile(metadataPath, "utf-8");
1942
+ metadata = JSON.parse(raw);
1943
+ } catch {
1944
+ }
1945
+ metadata.failedFiles = failedFileIds;
1946
+ await fs6.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
1947
+ }
1948
+ async function runFileDownloads(client, output, options) {
1949
+ console.log();
1950
+ const spinner = ora("Scanning conversations for file references...").start();
1951
+ const refs = await scanAllConversations(output);
1952
+ const skipFileIds = await loadFailedFiles(output);
1953
+ const skippedKnown = skipFileIds.size > 0 ? refs.filter((r) => skipFileIds.has(r.fileId)).length : 0;
1954
+ const activeCount = refs.length - skippedKnown;
1955
+ spinner.succeed(
1956
+ `Found ${refs.length} file references` + (skippedKnown > 0 ? ` (${skippedKnown} previously failed, skipped)` : "") + "\n"
1957
+ );
1958
+ if (activeCount === 0) return;
1959
+ const state = { bar: null };
1960
+ const errors = [];
1961
+ const result = await downloadFiles(client, refs, output, {
1962
+ concurrency: options.concurrency,
1963
+ delay: options.delay,
1964
+ verbose: options.verbose,
1965
+ skipFileIds,
1966
+ onProgress: (completed, total) => {
1967
+ if (!state.bar) {
1968
+ state.bar = createProgressBar(total, "Files ");
1969
+ }
1970
+ state.bar.update(completed);
1971
+ },
1972
+ onError: (fileId, error) => {
1973
+ errors.push({ fileId, message: error.message });
1974
+ }
1975
+ });
1976
+ state.bar?.stop();
1977
+ console.log();
1978
+ console.log(
1979
+ ` Downloaded: ${chalk2.green(result.downloaded)}, Skipped: ${chalk2.yellow(result.skipped)}, Failed: ${chalk2.red(result.failed)}`
1980
+ );
1981
+ const allFailed = [...skipFileIds, ...result.failedFileIds];
1982
+ await saveFailedFiles(output, allFailed);
1983
+ if (errors.length > 0) {
1984
+ const grouped = /* @__PURE__ */ new Map();
1985
+ for (const e of errors) {
1986
+ const list = grouped.get(e.message) ?? [];
1987
+ list.push(e.fileId);
1988
+ grouped.set(e.message, list);
1989
+ }
1990
+ for (const [message, fileIds] of grouped) {
1991
+ console.log(chalk2.red(` ${fileIds.length} files: ${message}`));
1992
+ if (options.verbose) {
1993
+ for (const id of fileIds) {
1994
+ console.log(chalk2.dim(` ${id}`));
1995
+ }
1996
+ }
1997
+ }
1998
+ }
1999
+ }
2000
+
2001
+ // src/cli/commands/chromium.ts
2002
+ import { spawn } from "child_process";
2003
+ import fs7 from "fs/promises";
2004
+ import path7 from "path";
2005
+ import chalk3 from "chalk";
2006
+
2007
+ // src/utils/browser.ts
2008
+ import { access } from "fs/promises";
2009
+ import { constants } from "fs";
2010
+ function chromiumCandidates() {
2011
+ return [
2012
+ process.env.PROMPT_EXPORTER_BROWSER,
2013
+ process.env.CHATGPT_EXPORTER_BROWSER,
2014
+ "/usr/bin/chromium",
2015
+ "/usr/bin/chromium-browser",
2016
+ "/usr/bin/google-chrome-stable",
2017
+ "/usr/bin/google-chrome"
2018
+ ].filter((v) => Boolean(v));
2019
+ }
2020
+ async function resolveBrowser() {
2021
+ for (const candidate of chromiumCandidates()) {
2022
+ try {
2023
+ await access(candidate, constants.X_OK);
2024
+ return candidate;
2025
+ } catch {
2026
+ }
2027
+ }
2028
+ throw new Error(
2029
+ "Chromium not found. Install the chromium package (Arch: pacman -S chromium) or set PROMPT_EXPORTER_BROWSER to a Chromium-based browser path."
2030
+ );
2031
+ }
2032
+
2033
+ // src/cli/commands/chromium.ts
2034
+ function printNextSteps() {
2035
+ console.log("Then run:");
2036
+ console.log(chalk3.cyan(" prompt-exporter token"));
2037
+ console.log("to read accessToken from this browser session.");
2038
+ console.log("");
2039
+ console.log("Then sync conversations:");
2040
+ console.log(chalk3.cyan(" prompt-exporter sync"));
2041
+ console.log("");
2042
+ }
2043
+ async function isProfileLocked(userDataDir) {
2044
+ for (const name of ["SingletonLock", "lockfile", "SingletonSocket"]) {
2045
+ try {
2046
+ await fs7.access(path7.join(userDataDir, name));
2047
+ return true;
2048
+ } catch {
2049
+ }
2050
+ }
2051
+ return false;
2052
+ }
2053
+ async function reuseExistingCdp(port, url, existing) {
2054
+ const info = existing ?? await getCdpEndpointInfo(port);
2055
+ if (!info) {
2056
+ throw new Error(`No CDP endpoint on port ${port}`);
2057
+ }
2058
+ console.log(chalk3.green(`Reusing Chromium CDP on port ${port}.`));
2059
+ console.log(chalk3.dim(` CDP: http://127.0.0.1:${port}`));
2060
+ if (info.Browser) {
2061
+ console.log(chalk3.dim(` Browser: ${info.Browser}`));
2062
+ }
2063
+ if (info.webSocketDebuggerUrl) {
2064
+ console.log(chalk3.dim(` WS: ${info.webSocketDebuggerUrl}`));
2065
+ }
2066
+ console.log("");
2067
+ const targets = await listCdpTargets(port);
2068
+ const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
2069
+ const exact = pages.find((t) => t.url === url || t.url.startsWith(url + "?"));
2070
+ const chatgpt = exact ?? (isChatGptUrl(url) ? pages.find((t) => isChatGptUrl(t.url)) : void 0);
2071
+ if (chatgpt) {
2072
+ await activateCdpTarget(port, chatgpt.id);
2073
+ console.log(chalk3.green(`Focused existing tab in that browser.`));
2074
+ console.log(chalk3.dim(` ${chatgpt.title || chatgpt.id}`));
2075
+ console.log(chalk3.dim(` ${chatgpt.url}`));
2076
+ } else {
2077
+ const created = await openCdpTab(port, url);
2078
+ console.log(chalk3.green(`Opened a new tab in that browser (same session).`));
2079
+ console.log(chalk3.dim(` ${created.url || url}`));
2080
+ }
2081
+ console.log("");
2082
+ printNextSteps();
2083
+ }
2084
+ async function chromiumStartCommand(options) {
2085
+ let port = options.port || DEFAULT_CDP_PORT;
2086
+ const url = options.url ?? "https://chatgpt.com/";
2087
+ const userSetPort = options.port !== void 0 && options.port !== DEFAULT_CDP_PORT;
2088
+ let existing = await getCdpEndpointInfo(port);
2089
+ if (!existing && !userSetPort && !options.force) {
2090
+ const found = await discoverCdpPort(port);
2091
+ if (found !== null) {
2092
+ port = found;
2093
+ existing = await getCdpEndpointInfo(port);
2094
+ }
2095
+ }
2096
+ if (existing && !options.force) {
2097
+ await reuseExistingCdp(port, url, existing);
2098
+ return;
2099
+ }
2100
+ const browser = await resolveBrowser();
2101
+ const { dir: userDataDir, mode } = resolveChromiumUserDataDir({
2102
+ isolated: options.isolated,
2103
+ userDataDir: options.userDataDir,
2104
+ browserPath: browser
2105
+ });
2106
+ await fs7.mkdir(userDataDir, { recursive: true });
2107
+ if (await isProfileLocked(userDataDir)) {
2108
+ const again = await getCdpEndpointInfo(port);
2109
+ if (again && !options.force) {
2110
+ await reuseExistingCdp(port, url, again);
2111
+ return;
2112
+ }
2113
+ console.log(
2114
+ chalk3.yellow("Chromium profile is already in use (browser running).")
2115
+ );
2116
+ console.log(chalk3.dim(` Profile: ${userDataDir}`));
2117
+ console.log("");
2118
+ console.log(
2119
+ chalk3.yellow(
2120
+ `No CDP answered on port ${port}, so this running browser cannot be controlled.`
2121
+ )
2122
+ );
2123
+ console.log("");
2124
+ console.log("Either:");
2125
+ console.log(
2126
+ ` \u2022 Point at the port where CDP is listening: prompt-exporter chromium start --port <PORT>`
2127
+ );
2128
+ console.log(
2129
+ " \u2022 Or quit Chromium and start it with CDP: prompt-exporter chromium start"
2130
+ );
2131
+ console.log("");
2132
+ process.exitCode = 1;
2133
+ return;
2134
+ }
2135
+ const args = [
2136
+ `--remote-debugging-port=${port}`,
2137
+ `--user-data-dir=${userDataDir}`,
2138
+ "--no-first-run",
2139
+ "--no-default-browser-check"
2140
+ ];
2141
+ if (options.headless) {
2142
+ args.push("--headless=new");
2143
+ }
2144
+ args.push(url);
2145
+ if (options.print) {
2146
+ console.log(chalk3.dim(`Browser: ${browser}`));
2147
+ console.log(chalk3.dim(`Args: ${args.join(" ")}`));
2148
+ }
2149
+ console.log(chalk3.bold("Starting Chromium with remote debugging\u2026"));
2150
+ console.log(chalk3.dim(` CDP: http://127.0.0.1:${port}`));
2151
+ console.log(
2152
+ chalk3.dim(
2153
+ ` Profile: ${userDataDir}` + (mode === "system" ? " (system \u2014 your normal login)" : mode === "isolated" ? " (isolated)" : " (custom)")
2154
+ )
2155
+ );
2156
+ console.log("");
2157
+ printNextSteps();
2158
+ const child = spawn(browser, args, {
2159
+ detached: true,
2160
+ stdio: "ignore"
2161
+ });
2162
+ child.unref();
2163
+ console.log(chalk3.green(`Chromium started (pid ${child.pid ?? "unknown"}).`));
2164
+ }
2165
+
2166
+ // src/cli/commands/list.ts
2167
+ import chalk4 from "chalk";
2168
+ import ora2 from "ora";
2169
+ async function listCommand(options) {
2170
+ const { token, delay, verbose, json, project, cdpPort } = options;
2171
+ const spinner = ora2("Connecting...").start();
2172
+ const { client } = await createChatGptClient({ token, verbose, cdpPort });
2173
+ const storage = new StorageService(getDefaultDataDir());
2174
+ const service = new BackupService(client, storage);
2175
+ try {
2176
+ await client.initialize();
2177
+ let conversations;
2178
+ if (project) {
2179
+ spinner.text = "Resolving project...";
2180
+ const { gizmoId, name } = await service.resolveProjectId(project);
2181
+ spinner.text = `Fetching conversations from project "${name}"...`;
2182
+ let lastReported = 0;
2183
+ conversations = await service.listProjectConversations(gizmoId, {
2184
+ delay,
2185
+ onListProgress: (fetched, total) => {
2186
+ if (fetched !== lastReported) {
2187
+ spinner.text = `Fetching conversations from "${name}"... ${fetched}`;
2188
+ lastReported = fetched;
2189
+ }
2190
+ }
2191
+ });
2192
+ spinner.succeed(`Found ${conversations.length} conversations in project "${name}"`);
2193
+ } else {
2194
+ spinner.text = "Fetching conversations...";
2195
+ let lastReported = 0;
2196
+ conversations = await service.listConversations({
2197
+ delay,
2198
+ onListProgress: (fetched, total) => {
2199
+ if (fetched !== lastReported) {
2200
+ spinner.text = `Fetching conversations... ${fetched}/${total}`;
2201
+ lastReported = fetched;
2202
+ }
2203
+ }
2204
+ });
2205
+ spinner.succeed(`Found ${conversations.length} conversations`);
2206
+ }
2207
+ if (json) {
2208
+ console.log(JSON.stringify(conversations, null, 2));
2209
+ } else {
2210
+ console.log();
2211
+ for (const conv of conversations) {
2212
+ const title = conv.title ?? chalk4.dim("(untitled)");
2213
+ const date = conv.update_time ? new Date(
2214
+ typeof conv.update_time === "number" ? conv.update_time * 1e3 : conv.update_time
2215
+ ).toLocaleDateString() : "unknown date";
2216
+ console.log(`${chalk4.cyan(conv.id)} ${title} ${chalk4.dim(`[${date}]`)}`);
2217
+ }
2218
+ console.log();
2219
+ console.log(chalk4.green(`Total: ${conversations.length} conversations`));
2220
+ }
2221
+ } catch (error) {
2222
+ spinner.fail("Failed to list conversations");
2223
+ if (error instanceof Error) {
2224
+ if (error.name === "AuthenticationError") {
2225
+ console.error(chalk4.red(`
2226
+ Authentication failed: ${error.message}`));
2227
+ console.error(
2228
+ chalk4.yellow(
2229
+ "\nKeep Chromium CDP logged in to chatgpt.com, then retry:"
2230
+ )
2231
+ );
2232
+ console.error(" prompt-exporter chromium start");
2233
+ console.error(" prompt-exporter list");
2234
+ } else {
2235
+ console.error(chalk4.red(`
2236
+ Error: ${error.message}`));
2237
+ }
2238
+ }
2239
+ process.exit(1);
2240
+ } finally {
2241
+ await client.close();
2242
+ }
2243
+ }
2244
+
2245
+ // src/cli/commands/projects.ts
2246
+ import chalk5 from "chalk";
2247
+ import ora3 from "ora";
2248
+ async function projectsCommand(options) {
2249
+ const { token, verbose, json, cdpPort } = options;
2250
+ const spinner = ora3("Connecting...").start();
2251
+ const { client } = await createChatGptClient({ token, verbose, cdpPort });
2252
+ const storage = new StorageService(getDefaultDataDir());
2253
+ const service = new BackupService(client, storage);
2254
+ try {
2255
+ await client.initialize();
2256
+ spinner.text = "Fetching projects...";
2257
+ const projects = await service.listProjects();
2258
+ spinner.succeed(`Found ${projects.length} projects`);
2259
+ if (json) {
2260
+ const output = projects.map((p) => ({
2261
+ id: p.gizmo.id,
2262
+ name: p.gizmo.display.name,
2263
+ num_interactions: p.gizmo.num_interactions,
2264
+ last_interacted_at: p.gizmo.last_interacted_at,
2265
+ is_archived: p.gizmo.is_archived
2266
+ }));
2267
+ console.log(JSON.stringify(output, null, 2));
2268
+ } else {
2269
+ console.log();
2270
+ for (const project of projects) {
2271
+ const gizmo = project.gizmo;
2272
+ const name = gizmo.display.name;
2273
+ const id = gizmo.id;
2274
+ const lastInteracted = gizmo.last_interacted_at ? new Date(gizmo.last_interacted_at).toLocaleDateString() : "never";
2275
+ console.log(
2276
+ `${chalk5.cyan(id)} ${chalk5.bold(name)} ${chalk5.dim(`[last: ${lastInteracted}]`)}`
2277
+ );
2278
+ }
2279
+ console.log();
2280
+ console.log(chalk5.green(`Total: ${projects.length} projects`));
2281
+ }
2282
+ } catch (error) {
2283
+ spinner.fail("Failed to list projects");
2284
+ if (error instanceof Error) {
2285
+ console.error(chalk5.red(`
2286
+ Error: ${error.message}`));
2287
+ if (error.name === "AuthenticationError") {
2288
+ console.error(chalk5.yellow("\nKeep Chromium CDP logged in, then retry:"));
2289
+ console.error(" prompt-exporter chromium start");
2290
+ console.error(" prompt-exporter projects");
2291
+ }
2292
+ }
2293
+ process.exit(1);
2294
+ } finally {
2295
+ await client.close();
2296
+ }
2297
+ }
2298
+
2299
+ // src/cli/commands/token.ts
2300
+ import { spawn as spawn2 } from "child_process";
2301
+ import chalk6 from "chalk";
2302
+
2303
+ // src/utils/cdp-session.ts
2304
+ var SESSION_URL = "https://chatgpt.com/api/auth/session";
2305
+ var APP_URL = "https://chatgpt.com/";
2306
+ function getWebSocket() {
2307
+ const WS = globalThis.WebSocket;
2308
+ if (!WS) {
2309
+ throw new Error(
2310
+ "WebSocket is not available in this Node runtime (need Node 22+ or a WebSocket polyfill)"
2311
+ );
2312
+ }
2313
+ return WS;
2314
+ }
2315
+ async function withCdpSocket(webSocketDebuggerUrl, fn) {
2316
+ const WS = getWebSocket();
2317
+ const ws = new WS(webSocketDebuggerUrl);
2318
+ let nextId = 1;
2319
+ const pending = /* @__PURE__ */ new Map();
2320
+ const send = (method, params) => {
2321
+ const id = nextId++;
2322
+ return new Promise((resolve, reject) => {
2323
+ pending.set(id, { resolve, reject });
2324
+ ws.send(JSON.stringify({ id, method, params }));
2325
+ });
2326
+ };
2327
+ try {
2328
+ await new Promise((resolve, reject) => {
2329
+ const timer = setTimeout(
2330
+ () => reject(new Error("CDP WebSocket connect timeout")),
2331
+ 5e3
2332
+ );
2333
+ ws.addEventListener("open", () => {
2334
+ clearTimeout(timer);
2335
+ resolve();
2336
+ });
2337
+ ws.addEventListener("error", () => {
2338
+ clearTimeout(timer);
2339
+ reject(new Error("CDP WebSocket connection failed"));
2340
+ });
2341
+ });
2342
+ ws.addEventListener("message", (event) => {
2343
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
2344
+ let msg;
2345
+ try {
2346
+ msg = JSON.parse(raw);
2347
+ } catch {
2348
+ return;
2349
+ }
2350
+ const id = msg.id;
2351
+ if (typeof id !== "number") return;
2352
+ const waiter = pending.get(id);
2353
+ if (!waiter) return;
2354
+ pending.delete(id);
2355
+ if (msg.error) {
2356
+ const err = msg.error;
2357
+ waiter.reject(
2358
+ new Error(typeof err.message === "string" ? err.message : "CDP error")
2359
+ );
2360
+ } else {
2361
+ waiter.resolve(msg.result ?? {});
2362
+ }
2363
+ });
2364
+ return await fn(send);
2365
+ } finally {
2366
+ for (const [, waiter] of pending) {
2367
+ waiter.reject(new Error("CDP WebSocket closed"));
2368
+ }
2369
+ pending.clear();
2370
+ ws.close();
2371
+ }
2372
+ }
2373
+ function normalizeProbe(raw) {
2374
+ if (!raw || typeof raw !== "object") {
2375
+ return {
2376
+ accessToken: null,
2377
+ loggedIn: false,
2378
+ anonymousBannerOnly: false,
2379
+ keys: [],
2380
+ error: "empty session payload"
2381
+ };
2382
+ }
2383
+ const obj = raw;
2384
+ const keys = Object.keys(obj);
2385
+ const token = typeof obj.accessToken === "string" ? obj.accessToken : typeof obj.access_token === "string" ? obj.access_token : null;
2386
+ const validToken = token && token.startsWith("eyJ") ? token : null;
2387
+ const anonymousBannerOnly = !validToken && keys.length === 1 && keys[0] === "WARNING_BANNER";
2388
+ return {
2389
+ accessToken: validToken,
2390
+ loggedIn: Boolean(validToken),
2391
+ anonymousBannerOnly,
2392
+ keys
2393
+ };
2394
+ }
2395
+ async function probeSessionViaCdp(webSocketDebuggerUrl) {
2396
+ try {
2397
+ return await withCdpSocket(webSocketDebuggerUrl, async (send) => {
2398
+ await send("Runtime.enable");
2399
+ const result = await send("Runtime.evaluate", {
2400
+ expression: `(() => {
2401
+ const tryParse = (text) => {
2402
+ if (!text || typeof text !== 'string') return null;
2403
+ const trimmed = text.trim();
2404
+ try { return JSON.parse(trimmed); } catch {}
2405
+ // Pretty-printed / viewer noise: pull accessToken via regex
2406
+ const m = trimmed.match(/"accessToken"\\s*:\\s*"(eyJ[^"]+)"/);
2407
+ if (m) return { accessToken: m[1], __via: 'regex' };
2408
+ return null;
2409
+ };
2410
+
2411
+ const candidates = [];
2412
+ if (document.body && document.body.innerText) candidates.push(document.body.innerText);
2413
+ if (document.body && document.body.textContent) candidates.push(document.body.textContent);
2414
+ const pre = document.querySelector('pre');
2415
+ if (pre && pre.textContent) candidates.push(pre.textContent);
2416
+ // Chrome JSON viewer / plain document
2417
+ const html = document.documentElement ? document.documentElement.innerText : '';
2418
+ if (html) candidates.push(html);
2419
+
2420
+ for (const c of candidates) {
2421
+ const parsed = tryParse(c);
2422
+ if (parsed && parsed.accessToken) return Promise.resolve(parsed);
2423
+ }
2424
+
2425
+ return fetch('/api/auth/session', {
2426
+ credentials: 'include',
2427
+ headers: { Accept: 'application/json' },
2428
+ cache: 'no-store',
2429
+ })
2430
+ .then(async (r) => {
2431
+ const text = await r.text();
2432
+ const parsed = tryParse(text);
2433
+ if (parsed) return parsed;
2434
+ try { return JSON.parse(text); } catch {
2435
+ return { __error: 'unparseable session response', __preview: text.slice(0, 120) };
2436
+ }
2437
+ })
2438
+ .catch((e) => ({ __error: String(e) }));
2439
+ })()`,
2440
+ awaitPromise: true,
2441
+ returnByValue: true
2442
+ });
2443
+ const value = result.result?.value;
2444
+ if (value && typeof value === "object" && "__error" in value) {
2445
+ return {
2446
+ accessToken: null,
2447
+ loggedIn: false,
2448
+ anonymousBannerOnly: false,
2449
+ keys: [],
2450
+ error: String(value.__error)
2451
+ };
2452
+ }
2453
+ return normalizeProbe(value);
2454
+ });
2455
+ } catch (error) {
2456
+ return {
2457
+ accessToken: null,
2458
+ loggedIn: false,
2459
+ anonymousBannerOnly: false,
2460
+ keys: [],
2461
+ error: error instanceof Error ? error.message : String(error)
2462
+ };
2463
+ }
2464
+ }
2465
+ async function ensureChatGptAppTab(port) {
2466
+ const targets = await listCdpTargets(port);
2467
+ const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
2468
+ const existing = pages.find(
2469
+ (t) => t.url === APP_URL || t.url.startsWith("https://chatgpt.com/?") || t.url.startsWith("https://chatgpt.com/c/") || t.url.startsWith("https://chatgpt.com/g/")
2470
+ );
2471
+ if (existing?.webSocketDebuggerUrl) {
2472
+ await activateCdpTarget(port, existing.id);
2473
+ return existing;
2474
+ }
2475
+ if (existing) {
2476
+ await activateCdpTarget(port, existing.id);
2477
+ return existing;
2478
+ }
2479
+ return openCdpTab(port, APP_URL);
2480
+ }
2481
+ async function prepareSessionTarget(port) {
2482
+ const { target, reused } = await ensureChatGptSessionTab(port, SESSION_URL);
2483
+ const refreshed = (await listCdpTargets(port)).find((t) => t.id === target.id) ?? target;
2484
+ let wsUrl = refreshed.webSocketDebuggerUrl;
2485
+ if (!wsUrl) {
2486
+ return {
2487
+ target: refreshed,
2488
+ reused,
2489
+ probe: {
2490
+ accessToken: null,
2491
+ loggedIn: false,
2492
+ anonymousBannerOnly: false,
2493
+ keys: [],
2494
+ error: "No DevTools WebSocket for tab"
2495
+ }
2496
+ };
2497
+ }
2498
+ let probe = await probeSessionViaCdp(wsUrl);
2499
+ if (probe.loggedIn) {
2500
+ return { target: refreshed, reused, probe };
2501
+ }
2502
+ if (!refreshed.url.startsWith(SESSION_URL)) {
2503
+ await cdpNavigate(wsUrl, SESSION_URL);
2504
+ const afterNav = (await listCdpTargets(port)).find((t) => t.id === refreshed.id) ?? refreshed;
2505
+ wsUrl = afterNav.webSocketDebuggerUrl ?? wsUrl;
2506
+ probe = await probeSessionViaCdp(wsUrl);
2507
+ if (probe.loggedIn) {
2508
+ return { target: afterNav, reused, probe };
2509
+ }
2510
+ }
2511
+ await cdpNavigate(wsUrl, SESSION_URL);
2512
+ const afterReload = (await listCdpTargets(port)).find((t) => t.id === refreshed.id) ?? refreshed;
2513
+ wsUrl = afterReload.webSocketDebuggerUrl ?? wsUrl;
2514
+ await new Promise((r) => setTimeout(r, 400));
2515
+ probe = await probeSessionViaCdp(wsUrl);
2516
+ return { target: afterReload, reused, probe };
2517
+ }
2518
+ async function waitForAccessToken(port, options = {}) {
2519
+ const timeoutMs = options.timeoutMs ?? 18e4;
2520
+ const intervalMs = options.intervalMs ?? 2e3;
2521
+ const started = Date.now();
2522
+ while (Date.now() - started < timeoutMs) {
2523
+ const { probe } = await prepareSessionTarget(port);
2524
+ options.onTick?.(probe);
2525
+ if (probe.loggedIn && probe.accessToken) {
2526
+ return probe;
2527
+ }
2528
+ await new Promise((r) => setTimeout(r, intervalMs));
2529
+ }
2530
+ const last = await prepareSessionTarget(port);
2531
+ return last.probe;
2532
+ }
2533
+
2534
+ // src/cli/commands/token.ts
2535
+ async function persistAndAnnounce(token, show) {
2536
+ const file = await saveToken(token);
2537
+ console.log(chalk6.green("Saved accessToken for sync."));
2538
+ console.log(chalk6.dim(` ${file}`));
2539
+ console.log(chalk6.dim(` preview: ${token.slice(0, 16)}\u2026 (${token.length} chars)`));
2540
+ console.log("");
2541
+ if (show) {
2542
+ console.log(` export PROMPT_EXPORTER_CHATGPT_TOKEN="${token}"`);
2543
+ console.log("");
2544
+ }
2545
+ console.log("You can sync right away (no manual export needed):");
2546
+ console.log(chalk6.cyan(" prompt-exporter sync"));
2547
+ }
2548
+ function printManualSteps() {
2549
+ console.log("Steps:");
2550
+ console.log(" 1. Ensure CDP Chromium shows https://chatgpt.com/api/auth/session JSON");
2551
+ console.log(" 2. Re-run: prompt-exporter token");
2552
+ console.log(" 3. prompt-exporter sync");
2553
+ }
2554
+ async function tokenCommand(options = {}) {
2555
+ let port = options.port ?? DEFAULT_CDP_PORT;
2556
+ const wait = options.wait ?? true;
2557
+ const timeoutMs = options.timeoutMs ?? 18e4;
2558
+ const show = options.show ?? false;
2559
+ if (!await isCdpRunning(port)) {
2560
+ const found = await discoverCdpPort(port);
2561
+ if (found !== null) {
2562
+ port = found;
2563
+ }
2564
+ }
2565
+ if (!await isCdpRunning(port)) {
2566
+ console.log(
2567
+ chalk6.yellow(
2568
+ `No CDP on port ${port}. Start one with: prompt-exporter chromium start`
2569
+ )
2570
+ );
2571
+ console.log(chalk6.dim("Opening a separate Chromium window as fallback\u2026"));
2572
+ console.log("");
2573
+ const browser = await resolveBrowser();
2574
+ console.log(`Open ${SESSION_URL}, then re-run prompt-exporter token`);
2575
+ console.log("");
2576
+ printManualSteps();
2577
+ if (options.print) console.log(`Browser: ${browser}`);
2578
+ const child = spawn2(browser, ["--new-window", SESSION_URL], {
2579
+ detached: true,
2580
+ stdio: "ignore"
2581
+ });
2582
+ child.unref();
2583
+ return;
2584
+ }
2585
+ const info = await getCdpEndpointInfo(port);
2586
+ console.log(chalk6.green(`Using existing Chromium CDP on port ${port}.`));
2587
+ if (info?.Browser) {
2588
+ console.log(chalk6.dim(` Browser: ${info.Browser}`));
2589
+ }
2590
+ console.log(chalk6.dim(` Token file: ${getTokenPath()}`));
2591
+ console.log("");
2592
+ try {
2593
+ const { target, reused, probe } = await prepareSessionTarget(port);
2594
+ console.log(
2595
+ chalk6.dim(
2596
+ `${reused ? "Reused" : "Opened"} tab: ${target.title || target.id}`
2597
+ )
2598
+ );
2599
+ console.log(chalk6.dim(` ${target.url || SESSION_URL}`));
2600
+ console.log("");
2601
+ if (probe.loggedIn && probe.accessToken) {
2602
+ await persistAndAnnounce(probe.accessToken, show);
2603
+ return;
2604
+ }
2605
+ if (probe.anonymousBannerOnly || !probe.loggedIn) {
2606
+ console.log(
2607
+ chalk6.yellow(
2608
+ "No accessToken in the session tab yet (anonymous / not logged in)."
2609
+ )
2610
+ );
2611
+ if (probe.keys.length) {
2612
+ console.log(chalk6.dim(` session keys: ${probe.keys.join(", ")}`));
2613
+ }
2614
+ if (probe.error) {
2615
+ console.log(chalk6.dim(` ${probe.error}`));
2616
+ }
2617
+ console.log("");
2618
+ const appTab = await ensureChatGptAppTab(port);
2619
+ console.log(chalk6.cyan(`Focused ChatGPT for login: ${APP_URL}`));
2620
+ console.log(chalk6.dim(` Tab: ${appTab.url || APP_URL}`));
2621
+ console.log("");
2622
+ if (!wait) {
2623
+ printManualSteps();
2624
+ return;
2625
+ }
2626
+ console.log(
2627
+ chalk6.bold(
2628
+ `Waiting up to ${Math.round(timeoutMs / 1e3)}s for login, then re-reading session\u2026`
2629
+ )
2630
+ );
2631
+ console.log(chalk6.dim("Press Ctrl+C to cancel."));
2632
+ console.log("");
2633
+ const finalProbe = await waitForAccessToken(port, {
2634
+ timeoutMs,
2635
+ intervalMs: 2500,
2636
+ onTick: (p) => {
2637
+ if (p.loggedIn) return;
2638
+ process.stdout.write(
2639
+ chalk6.dim(
2640
+ ` waiting\u2026 (${p.anonymousBannerOnly ? "banner-only" : p.keys.join(",") || "no token"})\r`
2641
+ )
2642
+ );
2643
+ }
2644
+ });
2645
+ process.stdout.write("\n");
2646
+ if (finalProbe.loggedIn && finalProbe.accessToken) {
2647
+ await persistAndAnnounce(finalProbe.accessToken, show);
2648
+ return;
2649
+ }
2650
+ console.log(chalk6.red("Timed out \u2014 accessToken not found."));
2651
+ printManualSteps();
2652
+ process.exitCode = 1;
2653
+ return;
2654
+ }
2655
+ console.log(chalk6.yellow("Could not read accessToken from the session page."));
2656
+ if (probe.error) console.log(chalk6.dim(` ${probe.error}`));
2657
+ printManualSteps();
2658
+ process.exitCode = 1;
2659
+ } catch (error) {
2660
+ console.error(
2661
+ chalk6.red(
2662
+ `CDP session failed: ${error instanceof Error ? error.message : error}`
2663
+ )
2664
+ );
2665
+ printManualSteps();
2666
+ process.exitCode = 1;
2667
+ }
2668
+ }
2669
+
2670
+ // src/sources/chatgpt/index.ts
2671
+ var chatgptSource = {
2672
+ id: "chatgpt",
2673
+ label: "ChatGPT",
2674
+ description: "chatgpt.com conversation history via Chromium CDP",
2675
+ defaultDataDir() {
2676
+ return getSourceDataDir("chatgpt");
2677
+ }
2678
+ };
2679
+
2680
+ // src/sources/registry.ts
2681
+ var sources = [chatgptSource];
2682
+ function listSources() {
2683
+ return sources.map(({ id, label, description }) => ({
2684
+ id,
2685
+ label,
2686
+ description
2687
+ }));
2688
+ }
2689
+ function getSource(id) {
2690
+ const found = sources.find((s) => s.id === id);
2691
+ if (!found) {
2692
+ const known = sources.map((s) => s.id).join(", ");
2693
+ throw new Error(`Unknown source "${id}". Available: ${known}`);
2694
+ }
2695
+ return found;
2696
+ }
2697
+ function resolveSourceId(raw) {
2698
+ const id = raw?.trim() || "chatgpt";
2699
+ getSource(id);
2700
+ return id;
2701
+ }
2702
+
2703
+ // src/cli/index.ts
2704
+ function parsePort(value) {
2705
+ const port = Number.parseInt(value, 10);
2706
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
2707
+ throw new Error(`Invalid port: ${value}`);
2708
+ }
2709
+ return port;
2710
+ }
2711
+ function createCli() {
2712
+ const program2 = new Command();
2713
+ program2.name("prompt-exporter").description(
2714
+ "Export and sync AI conversation prompts from multiple sources to local JSON + Markdown"
2715
+ ).version("2.0.0").hook("preAction", async () => {
2716
+ await ensureHomeLayout();
2717
+ }).addHelpText(
2718
+ "after",
2719
+ [
2720
+ "",
2721
+ "Environment:",
2722
+ " PROMPT_EXPORTER_HOME Override ~/.prompt-exporter",
2723
+ " PROMPT_EXPORTER_DATA Parent of source dirs (default: home)",
2724
+ " PROMPT_EXPORTER_BROWSER Chromium binary path",
2725
+ " PROMPT_EXPORTER_CDP_PORT Preferred CDP port (auto-discovers 9222/9223/\u2026)",
2726
+ " PROMPT_EXPORTER_CHATGPT_TOKEN Optional Bearer fallback for chatgpt source",
2727
+ "",
2728
+ "Typical flow:",
2729
+ " prompt-exporter chromium start",
2730
+ " prompt-exporter sync --source chatgpt",
2731
+ "",
2732
+ "Data: ~/.prompt-exporter/{source}/conversations/"
2733
+ ].join("\n")
2734
+ ).action(() => {
2735
+ program2.help();
2736
+ });
2737
+ const resolveOptionalToken = async (options) => {
2738
+ if (options.token?.trim()) return options.token.trim();
2739
+ const sourceId = resolveSourceId(options.source);
2740
+ if (sourceId === "chatgpt") {
2741
+ if (process.env.PROMPT_EXPORTER_CHATGPT_TOKEN?.trim()) {
2742
+ return process.env.PROMPT_EXPORTER_CHATGPT_TOKEN.trim();
2743
+ }
2744
+ if (process.env.CHATGPT_TOKEN?.trim()) {
2745
+ return process.env.CHATGPT_TOKEN.trim();
2746
+ }
2747
+ }
2748
+ return await loadSavedToken(sourceId) ?? void 0;
2749
+ };
2750
+ const resolveOutput = (options) => {
2751
+ if (options.output?.trim()) return options.output.trim();
2752
+ return getSource(resolveSourceId(options.source)).defaultDataDir();
2753
+ };
2754
+ const sourceOption = (cmd) => cmd.option(
2755
+ "-s, --source <id>",
2756
+ "Prompt source",
2757
+ DEFAULT_SOURCE_ID
2758
+ );
2759
+ program2.command("sources").description("List available prompt sources").option("--json", "Output as JSON", false).action((options) => {
2760
+ const rows = listSources();
2761
+ if (options.json) {
2762
+ console.log(JSON.stringify(rows, null, 2));
2763
+ return;
2764
+ }
2765
+ console.log();
2766
+ for (const s of rows) {
2767
+ console.log(` ${s.id.padEnd(12)} ${s.label} \u2014 ${s.description}`);
2768
+ }
2769
+ console.log();
2770
+ });
2771
+ const chromium = program2.command("chromium").description("Manage Chromium with remote debugging (CDP)");
2772
+ chromium.command("start").description(
2773
+ `Start Chromium with remote debugging (default port ${DEFAULT_CDP_PORT})`
2774
+ ).option(
2775
+ "--port <PORT>",
2776
+ "Remote debugging (CDP) port",
2777
+ parsePort,
2778
+ DEFAULT_CDP_PORT
2779
+ ).option(
2780
+ "--isolated",
2781
+ "Use a separate empty profile under ~/.prompt-exporter/chromium",
2782
+ false
2783
+ ).option(
2784
+ "--user-data-dir <dir>",
2785
+ "Chromium user-data-dir (default: system profile, e.g. ~/.config/chromium)"
2786
+ ).option("--headless", "Run Chromium headless", false).option("--url <url>", "Initial URL", "https://chatgpt.com/").option("--force", "Start even if CDP is already running on the port", false).option("--print", "Print resolved browser path and args", false).addHelpText(
2787
+ "after",
2788
+ [
2789
+ "",
2790
+ "If Chromium is already running with CDP on this port, it is reused.",
2791
+ "Otherwise starts Chromium with your system profile.",
2792
+ "",
2793
+ "Examples:",
2794
+ " prompt-exporter chromium start",
2795
+ " prompt-exporter chromium start --port 9222",
2796
+ " prompt-exporter chromium start --isolated"
2797
+ ].join("\n")
2798
+ ).action(async (options) => {
2799
+ await chromiumStartCommand({
2800
+ port: options.port,
2801
+ headless: options.headless,
2802
+ url: options.url,
2803
+ print: options.print,
2804
+ force: options.force,
2805
+ isolated: options.isolated,
2806
+ userDataDir: options.userDataDir
2807
+ });
2808
+ });
2809
+ sourceOption(
2810
+ program2.command("token").description("Get accessToken from the CDP Chromium session (chatgpt)").option(
2811
+ "--port <PORT>",
2812
+ "CDP port of an already-running Chromium",
2813
+ parsePort,
2814
+ DEFAULT_CDP_PORT
2815
+ ).option(
2816
+ "--no-wait",
2817
+ "Do not wait for interactive login; exit immediately if not logged in"
2818
+ ).option(
2819
+ "--timeout <ms>",
2820
+ "Login wait timeout in ms",
2821
+ (v) => parseInt(v, 10),
2822
+ 18e4
2823
+ ).option("--print", "Print resolved browser path", false).option(
2824
+ "--show",
2825
+ "Print full export PROMPT_EXPORTER_CHATGPT_TOKEN=... line",
2826
+ false
2827
+ ).action(async (options) => {
2828
+ resolveSourceId(options.source);
2829
+ await tokenCommand({
2830
+ print: options.print,
2831
+ port: options.port,
2832
+ wait: options.wait !== false,
2833
+ timeoutMs: options.timeout,
2834
+ show: options.show
2835
+ });
2836
+ })
2837
+ );
2838
+ const runBackup = async (options) => {
2839
+ const sourceId = resolveSourceId(options.source);
2840
+ if (sourceId !== "chatgpt") {
2841
+ throw new Error(
2842
+ `Source "${sourceId}" is registered but sync is not implemented yet`
2843
+ );
2844
+ }
2845
+ const token = await resolveOptionalToken(options);
2846
+ await backupCommand({
2847
+ token,
2848
+ output: resolveOutput(options),
2849
+ concurrency: options.concurrency,
2850
+ delay: options.delay,
2851
+ incremental: options.incremental,
2852
+ downloadFiles: options.downloadFiles,
2853
+ verbose: options.verbose,
2854
+ project: options.project,
2855
+ cdpPort: options.port
2856
+ });
2857
+ };
2858
+ const backupOpts = (cmd) => sourceOption(cmd).option(
2859
+ "-t, --token <token>",
2860
+ "Optional Bearer fallback; not needed with CDP"
2861
+ ).option(
2862
+ "--port <PORT>",
2863
+ "Preferred Chromium CDP port (auto-discovers if idle)",
2864
+ parsePort
2865
+ ).option(
2866
+ "-o, --output <dir>",
2867
+ "Source output directory (default: ~/.prompt-exporter/{source})"
2868
+ ).option(
2869
+ "--concurrency <n>",
2870
+ "Parallel downloads / CDP tabs (default 3, max 6)",
2871
+ (v) => parseInt(v, 10),
2872
+ 3
2873
+ ).option(
2874
+ "--delay <ms>",
2875
+ "Min gap between starting requests in ms",
2876
+ (v) => parseInt(v, 10),
2877
+ 200
2878
+ ).option(
2879
+ "--no-incremental",
2880
+ "Re-download all conversations (default: skip unchanged)"
2881
+ ).option("--download-files", "Download file attachments and images", false).option(
2882
+ "--project <name-or-id>",
2883
+ "Only backup conversations from a specific project"
2884
+ ).option("-v, --verbose", "Verbose logging", false);
2885
+ backupOpts(
2886
+ program2.command("sync").description(
2887
+ `Sync conversations (default: ${getSourceDataDir(DEFAULT_SOURCE_ID)})`
2888
+ ).addHelpText(
2889
+ "after",
2890
+ [
2891
+ "",
2892
+ "Examples:",
2893
+ " prompt-exporter sync",
2894
+ " prompt-exporter sync --source chatgpt",
2895
+ " prompt-exporter sync --download-files",
2896
+ " prompt-exporter sync --no-incremental"
2897
+ ].join("\n")
2898
+ ).action(async (options) => {
2899
+ await runBackup(options);
2900
+ })
2901
+ );
2902
+ backupOpts(
2903
+ program2.command("backup").description("Alias for sync").action(async (options) => {
2904
+ await runBackup(options);
2905
+ })
2906
+ );
2907
+ sourceOption(
2908
+ program2.command("list").description("List conversations without downloading").option("-t, --token <token>", "Optional Bearer fallback").option(
2909
+ "--port <PORT>",
2910
+ "Preferred Chromium CDP port (auto-discovers if idle)",
2911
+ parsePort
2912
+ ).option(
2913
+ "--delay <ms>",
2914
+ "Delay between requests in ms",
2915
+ (v) => parseInt(v, 10),
2916
+ 500
2917
+ ).option(
2918
+ "--project <name-or-id>",
2919
+ "List conversations from a specific project"
2920
+ ).option("-v, --verbose", "Verbose logging", false).option("--json", "Output as JSON", false).action(async (options) => {
2921
+ const sourceId = resolveSourceId(options.source);
2922
+ if (sourceId !== "chatgpt") {
2923
+ throw new Error(`Source "${sourceId}" list not implemented yet`);
2924
+ }
2925
+ const token = await resolveOptionalToken(options);
2926
+ await listCommand({
2927
+ token,
2928
+ delay: options.delay,
2929
+ verbose: options.verbose,
2930
+ json: options.json,
2931
+ project: options.project,
2932
+ cdpPort: options.port
2933
+ });
2934
+ })
2935
+ );
2936
+ sourceOption(
2937
+ program2.command("projects").description("List all projects").option("-t, --token <token>", "Optional Bearer fallback").option(
2938
+ "--port <PORT>",
2939
+ "Preferred Chromium CDP port (auto-discovers if idle)",
2940
+ parsePort
2941
+ ).option("-v, --verbose", "Verbose logging", false).option("--json", "Output as JSON", false).action(async (options) => {
2942
+ const sourceId = resolveSourceId(options.source);
2943
+ if (sourceId !== "chatgpt") {
2944
+ throw new Error(`Source "${sourceId}" projects not implemented yet`);
2945
+ }
2946
+ const token = await resolveOptionalToken(options);
2947
+ await projectsCommand({
2948
+ token,
2949
+ verbose: options.verbose,
2950
+ json: options.json,
2951
+ cdpPort: options.port
2952
+ });
2953
+ })
2954
+ );
2955
+ return program2;
2956
+ }
2957
+
2958
+ // src/index.ts
2959
+ process.on("SIGINT", () => process.exit(130));
2960
+ var program = createCli();
2961
+ program.parse();