chatgpt-exporter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # ChatGPT Exporter
2
+
3
+ A CLI tool to back up your ChatGPT conversations as JSON files and convert them to readable Markdown.
4
+
5
+ ## Prerequisites
6
+
7
+ You need **Node.js 20+** installed on your machine.
8
+
9
+ - **macOS:** `brew install node` (requires [Homebrew](https://brew.sh/))
10
+ - **Windows/Linux:** download from [nodejs.org](https://nodejs.org/) instead
11
+ - Verify with: `node -v`
12
+
13
+ ## Quick start
14
+
15
+ No installation needed — just run it with `npx`:
16
+
17
+ ```bash
18
+ npx chatgpt-exporter backup --token "eyJhbG..."
19
+ ```
20
+
21
+ ## Getting your ChatGPT access token
22
+
23
+ This tool needs a ChatGPT access token from your browser session. Here's how to get it:
24
+
25
+ 1. Open [chatgpt.com](https://chatgpt.com) and make sure you're logged in
26
+ 2. In the same browser, go to: **[chatgpt.com/api/auth/session](https://chatgpt.com/api/auth/session)**
27
+ 3. You'll see a JSON response — copy the value of `accessToken` (the long string starting with `eyJhbG...`)
28
+
29
+ > **Tip:** The token expires after a while. If you get an authentication error, just grab a fresh one.
30
+
31
+ You can pass the token directly or set it as an environment variable so you don't have to paste it every time:
32
+
33
+ ```bash
34
+ export CHATGPT_TOKEN="eyJhbG..."
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### 1. Back up conversations
40
+
41
+ Downloads all your conversations (including projects) as JSON files:
42
+
43
+ ```bash
44
+ npx chatgpt-exporter backup --token "eyJhbG..."
45
+ ```
46
+
47
+ This creates a `chatgpt-export/` folder with JSON files and their Markdown equivalents:
48
+
49
+ ```
50
+ chatgpt-export/
51
+ conversations/
52
+ index.json
53
+ <conversation-id>.json
54
+ <conversation-id>.md
55
+ ...
56
+ projects/
57
+ <Project_Name>/
58
+ conversations/
59
+ index.json
60
+ <conversation-id>.json
61
+ <conversation-id>.md
62
+ ...
63
+ metadata.json
64
+ ```
65
+
66
+ **Options:**
67
+
68
+ | Flag | Description | Default |
69
+ | --------------------- | ------------------------------------------ | ------------------ |
70
+ | `-t, --token <token>` | Access token (or use `CHATGPT_TOKEN` env) | — |
71
+ | `-o, --output <dir>` | Output directory | `./chatgpt-export` |
72
+ | `--incremental` | Only download new or updated conversations | `false` |
73
+ | `--project <name>` | Only backup a specific project | all |
74
+ | `--concurrency <n>` | Parallel downloads | `3` |
75
+ | `--delay <ms>` | Delay between API requests | `500` |
76
+ | `-v, --verbose` | Show detailed error messages | `false` |
77
+
78
+ **Incremental backups** (recommended after the first full export):
79
+
80
+ ```bash
81
+ npx chatgpt-exporter backup --token "eyJhbG..." --incremental
82
+ ```
83
+
84
+ ### 2. List conversations
85
+
86
+ Preview your conversations without downloading them:
87
+
88
+ ```bash
89
+ npx chatgpt-exporter list --token "eyJhbG..."
90
+ ```
91
+
92
+ Add `--json` to get machine-readable output, or `--project <name>` to filter by project.
93
+
94
+ ### 3. List projects
95
+
96
+ See all your ChatGPT projects:
97
+
98
+ ```bash
99
+ npx chatgpt-exporter projects --token "eyJhbG..."
100
+ ```
101
+
102
+ ## Typical workflow
103
+
104
+ ```bash
105
+ # First time: full export (includes markdown conversion)
106
+ npx chatgpt-exporter backup --token "eyJhbG..."
107
+
108
+ # Later: only fetch what changed
109
+ npx chatgpt-exporter backup --token "eyJhbG..." --incremental
110
+ ```
111
+
112
+ ## Troubleshooting
113
+
114
+ **"Authentication failed"** — Your token has expired. Grab a fresh one from [chatgpt.com/api/auth/session](https://chatgpt.com/api/auth/session).
115
+
116
+ **"Rate limit"** — ChatGPT is throttling requests. The tool retries automatically, but you can increase the delay: `--delay 1000`.
117
+
118
+ **Empty or short Markdown files** — This is normal for short conversations. The tool skips system messages and internal tool calls (like PDF parsing), so a conversation where you uploaded a file and got one response will produce a small `.md` with just your question and the answer.
119
+
120
+ ## Development
121
+
122
+ To work on the tool locally:
123
+
124
+ ```bash
125
+ git clone <repo-url>
126
+ cd chatgpt-exporter
127
+ npm install
128
+ npm run build
129
+ ```
130
+
131
+ Run from source during development:
132
+
133
+ ```bash
134
+ npm run dev -- backup --token "eyJhbG..."
135
+ ```
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,1088 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/backup.ts
7
+ import chalk2 from "chalk";
8
+ import ora from "ora";
9
+
10
+ // src/api/client.ts
11
+ import crypto from "crypto";
12
+
13
+ // src/api/endpoints.ts
14
+ var BASE_URL = "https://chatgpt.com";
15
+ var ENDPOINTS = {
16
+ SESSION: "/api/auth/session",
17
+ CONVERSATIONS: "/backend-api/conversations",
18
+ CONVERSATION: (id) => `/backend-api/conversation/${id}`,
19
+ PROJECTS_SIDEBAR: "/backend-api/gizmos/snorlax/sidebar",
20
+ PROJECT_CONVERSATIONS: (gizmoId) => `/backend-api/gizmos/${gizmoId}/conversations`
21
+ };
22
+
23
+ // src/api/types.ts
24
+ import { z } from "zod";
25
+ var SessionSchema = z.looseObject({
26
+ accessToken: z.string(),
27
+ user: z.looseObject({
28
+ id: z.string(),
29
+ name: z.string().optional(),
30
+ email: z.string().optional(),
31
+ image: z.string().optional()
32
+ }).optional(),
33
+ expires: z.string().optional()
34
+ });
35
+ var ConversationItemSchema = z.looseObject({
36
+ id: z.string(),
37
+ title: z.string().nullable(),
38
+ create_time: z.string().or(z.number()).nullable(),
39
+ update_time: z.string().or(z.number()).nullable(),
40
+ mapping: z.record(z.string(), z.unknown()).nullable().optional(),
41
+ current_node: z.string().nullable().optional(),
42
+ conversation_template_id: z.string().nullable().optional(),
43
+ gizmo_id: z.string().nullable().optional(),
44
+ is_archived: z.boolean().optional(),
45
+ workspace_id: z.string().nullable().optional()
46
+ });
47
+ var ConversationsResponseSchema = z.looseObject({
48
+ items: z.array(ConversationItemSchema),
49
+ total: z.number(),
50
+ limit: z.number(),
51
+ offset: z.number(),
52
+ has_missing_conversations: z.boolean().optional()
53
+ });
54
+ var MessageContentSchema = z.looseObject({
55
+ content_type: z.string(),
56
+ parts: z.array(z.unknown()).optional(),
57
+ text: z.string().optional()
58
+ });
59
+ var MessageSchema = z.looseObject({
60
+ id: z.string(),
61
+ author: z.looseObject({
62
+ role: z.string(),
63
+ name: z.string().nullable().optional(),
64
+ metadata: z.record(z.string(), z.unknown()).optional()
65
+ }),
66
+ create_time: z.number().nullable().optional(),
67
+ update_time: z.number().nullable().optional(),
68
+ content: MessageContentSchema.optional(),
69
+ status: z.string().optional(),
70
+ end_turn: z.boolean().nullable().optional(),
71
+ weight: z.number().optional(),
72
+ metadata: z.record(z.string(), z.unknown()).optional(),
73
+ recipient: z.string().optional()
74
+ });
75
+ var MappingNodeSchema = z.looseObject({
76
+ id: z.string(),
77
+ message: MessageSchema.nullable().optional(),
78
+ parent: z.string().nullable().optional(),
79
+ children: z.array(z.string()).optional()
80
+ });
81
+ var ConversationDetailSchema = z.looseObject({
82
+ id: z.string().optional(),
83
+ title: z.string().nullable(),
84
+ create_time: z.number().nullable(),
85
+ update_time: z.number().nullable(),
86
+ mapping: z.record(z.string(), MappingNodeSchema),
87
+ moderation_results: z.array(z.unknown()).optional(),
88
+ current_node: z.string().nullable().optional(),
89
+ conversation_id: z.string().optional(),
90
+ is_archived: z.boolean().optional()
91
+ });
92
+ var ProjectGizmoSchema = z.looseObject({
93
+ id: z.string(),
94
+ display: z.looseObject({
95
+ name: z.string(),
96
+ theme: z.unknown().optional()
97
+ }),
98
+ created_at: z.string().optional(),
99
+ updated_at: z.string().optional(),
100
+ last_interacted_at: z.string().nullable().optional(),
101
+ num_interactions: z.number().optional(),
102
+ is_archived: z.boolean().optional(),
103
+ gizmo_type: z.string().optional()
104
+ });
105
+ var SidebarItemSchema = z.looseObject({
106
+ gizmo: ProjectGizmoSchema
107
+ });
108
+ var ProjectsSidebarResponseSchema = z.looseObject({
109
+ items: z.array(SidebarItemSchema),
110
+ cursor: z.string().nullable().optional()
111
+ });
112
+ var ProjectConversationsResponseSchema = z.looseObject({
113
+ items: z.array(ConversationItemSchema),
114
+ cursor: z.string().nullable().optional()
115
+ });
116
+ var AuthenticationError = class extends Error {
117
+ constructor(message) {
118
+ super(message);
119
+ this.name = "AuthenticationError";
120
+ }
121
+ };
122
+ var RateLimitError = class extends Error {
123
+ retryAfter;
124
+ constructor(message, retryAfter) {
125
+ super(message);
126
+ this.name = "RateLimitError";
127
+ this.retryAfter = retryAfter;
128
+ }
129
+ };
130
+ var NetworkError = class extends Error {
131
+ statusCode;
132
+ constructor(message, statusCode) {
133
+ super(message);
134
+ this.name = "NetworkError";
135
+ this.statusCode = statusCode;
136
+ }
137
+ };
138
+
139
+ // src/utils/retry.ts
140
+ var defaultOptions = {
141
+ maxRetries: 5,
142
+ baseDelay: 1e3,
143
+ maxDelay: 6e4
144
+ };
145
+ function calculateDelay(attempt, baseDelay, maxDelay) {
146
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
147
+ const jitter = Math.random() * 0.3 * exponentialDelay;
148
+ return Math.min(exponentialDelay + jitter, maxDelay);
149
+ }
150
+ async function withRetry(fn, options = {}) {
151
+ const opts = { ...defaultOptions, ...options };
152
+ let lastError;
153
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
154
+ try {
155
+ return await fn();
156
+ } catch (error) {
157
+ lastError = error;
158
+ if (attempt === opts.maxRetries) {
159
+ break;
160
+ }
161
+ if (lastError.name === "AuthenticationError" || lastError.name === "NetworkError" && lastError.statusCode === 404) {
162
+ throw lastError;
163
+ }
164
+ const delay = calculateDelay(attempt, opts.baseDelay, opts.maxDelay);
165
+ opts.onRetry?.(lastError, attempt + 1, delay);
166
+ await sleep(delay);
167
+ }
168
+ }
169
+ throw lastError;
170
+ }
171
+ function sleep(ms) {
172
+ return new Promise((resolve) => setTimeout(resolve, ms));
173
+ }
174
+
175
+ // src/api/client.ts
176
+ var BROWSER_HEADERS = {
177
+ "Content-Type": "application/json",
178
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
179
+ "Accept": "application/json",
180
+ "Accept-Language": "en-US,en;q=0.9",
181
+ "Referer": "https://chatgpt.com/",
182
+ "Origin": "https://chatgpt.com",
183
+ "Sec-Ch-Ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
184
+ "Sec-Ch-Ua-Mobile": "?0",
185
+ "Sec-Ch-Ua-Platform": '"macOS"',
186
+ "Sec-Fetch-Dest": "empty",
187
+ "Sec-Fetch-Mode": "cors",
188
+ "Sec-Fetch-Site": "same-origin"
189
+ };
190
+ var ChatGPTClient = class {
191
+ accessToken;
192
+ verbose;
193
+ deviceId;
194
+ constructor(accessToken, options = {}) {
195
+ this.accessToken = accessToken;
196
+ this.verbose = options.verbose ?? false;
197
+ this.deviceId = crypto.randomUUID();
198
+ }
199
+ getHeaders() {
200
+ return {
201
+ ...BROWSER_HEADERS,
202
+ Authorization: `Bearer ${this.accessToken}`,
203
+ "Oai-Device-Id": this.deviceId,
204
+ "Oai-Language": "en-US"
205
+ };
206
+ }
207
+ async initialize() {
208
+ const response = await fetch(`${BASE_URL}/backend-api/conversations?offset=0&limit=1`, {
209
+ headers: this.getHeaders()
210
+ });
211
+ if (!response.ok) {
212
+ const body = await response.text().catch(() => "(no body)");
213
+ if (this.verbose) {
214
+ console.error(`Auth check failed: HTTP ${response.status}`);
215
+ console.error(`Response body: ${body.slice(0, 500)}`);
216
+ }
217
+ if (response.status === 401 || response.status === 403) {
218
+ throw new AuthenticationError(
219
+ `Access token rejected (HTTP ${response.status}). ${this.verbose ? "" : "Re-run with --verbose to see the full response."}`
220
+ );
221
+ }
222
+ throw new NetworkError(`Failed to verify token: ${response.status}`, response.status);
223
+ }
224
+ if (this.verbose) {
225
+ console.log("Successfully authenticated");
226
+ }
227
+ }
228
+ async fetch(endpoint, options = {}) {
229
+ if (!this.accessToken) {
230
+ throw new AuthenticationError("Client not initialized. Call initialize() first.");
231
+ }
232
+ const { method = "GET", body, parseResponse } = options;
233
+ return withRetry(
234
+ async () => {
235
+ const response = await fetch(`${BASE_URL}${endpoint}`, {
236
+ method,
237
+ headers: this.getHeaders(),
238
+ body: body ? JSON.stringify(body) : void 0
239
+ });
240
+ if (!response.ok) {
241
+ if (response.status === 401 || response.status === 403) {
242
+ throw new AuthenticationError("Access token expired or invalid.");
243
+ }
244
+ if (response.status === 429) {
245
+ const retryAfter = response.headers.get("retry-after");
246
+ throw new RateLimitError(
247
+ "Rate limited by API",
248
+ retryAfter ? parseInt(retryAfter, 10) * 1e3 : void 0
249
+ );
250
+ }
251
+ throw new NetworkError(
252
+ `Request failed: ${response.status} ${response.statusText}`,
253
+ response.status
254
+ );
255
+ }
256
+ const data = await response.json();
257
+ return parseResponse ? parseResponse(data) : data;
258
+ },
259
+ {
260
+ onRetry: (error, attempt, delay) => {
261
+ if (this.verbose) {
262
+ console.log(`Retry ${attempt}: ${error.message} (waiting ${Math.round(delay / 1e3)}s)`);
263
+ }
264
+ }
265
+ }
266
+ );
267
+ }
268
+ };
269
+
270
+ // src/api/pagination.ts
271
+ async function* fetchAllConversations(client, options = {}) {
272
+ const { limit = 28, delay = 500, onProgress } = options;
273
+ let offset = 0;
274
+ let total = Infinity;
275
+ let fetched = 0;
276
+ while (offset < total) {
277
+ const response = await client.fetch(
278
+ `${ENDPOINTS.CONVERSATIONS}?offset=${offset}&limit=${limit}&order=updated`,
279
+ {
280
+ parseResponse: (data) => ConversationsResponseSchema.parse(data)
281
+ }
282
+ );
283
+ total = response.total;
284
+ for (const item of response.items) {
285
+ yield item;
286
+ fetched++;
287
+ }
288
+ onProgress?.(fetched, total);
289
+ offset += limit;
290
+ if (offset < total && delay > 0) {
291
+ await sleep(delay);
292
+ }
293
+ }
294
+ }
295
+ async function countConversations(client) {
296
+ const response = await client.fetch(
297
+ `${ENDPOINTS.CONVERSATIONS}?offset=0&limit=1`,
298
+ {
299
+ parseResponse: (data) => ConversationsResponseSchema.parse(data)
300
+ }
301
+ );
302
+ return response.total;
303
+ }
304
+ async function fetchAllProjects(client, options = {}) {
305
+ const { delay = 500 } = options;
306
+ const allItems = [];
307
+ let cursor = null;
308
+ do {
309
+ const url = cursor ? `${ENDPOINTS.PROJECTS_SIDEBAR}?cursor=${encodeURIComponent(cursor)}` : ENDPOINTS.PROJECTS_SIDEBAR;
310
+ const response = await client.fetch(url, {
311
+ parseResponse: (data) => ProjectsSidebarResponseSchema.parse(data)
312
+ });
313
+ allItems.push(...response.items);
314
+ cursor = response.cursor;
315
+ if (cursor && delay > 0) {
316
+ await sleep(delay);
317
+ }
318
+ } while (cursor);
319
+ return allItems;
320
+ }
321
+ async function* fetchProjectConversations(client, gizmoId, options = {}) {
322
+ const { delay = 500, onProgress } = options;
323
+ let cursor = "0";
324
+ let fetched = 0;
325
+ while (cursor !== null && cursor !== void 0) {
326
+ const url = `${ENDPOINTS.PROJECT_CONVERSATIONS(gizmoId)}?cursor=${encodeURIComponent(cursor)}`;
327
+ const response = await client.fetch(url, {
328
+ parseResponse: (data) => ProjectConversationsResponseSchema.parse(data)
329
+ });
330
+ for (const item of response.items) {
331
+ yield item;
332
+ fetched++;
333
+ }
334
+ cursor = response.cursor;
335
+ onProgress?.(fetched, fetched);
336
+ if (cursor && delay > 0) {
337
+ await sleep(delay);
338
+ }
339
+ }
340
+ }
341
+
342
+ // src/services/backup-service.ts
343
+ var BackupService = class {
344
+ client;
345
+ storage;
346
+ constructor(client, storage) {
347
+ this.client = client;
348
+ this.storage = storage;
349
+ }
350
+ async listConversations(options) {
351
+ const conversations = [];
352
+ for await (const conversation of fetchAllConversations(this.client, {
353
+ delay: options.delay,
354
+ onProgress: options.onListProgress
355
+ })) {
356
+ conversations.push(conversation);
357
+ }
358
+ return conversations;
359
+ }
360
+ async getConversationCount() {
361
+ return countConversations(this.client);
362
+ }
363
+ async listProjects() {
364
+ return fetchAllProjects(this.client);
365
+ }
366
+ async listProjectConversations(gizmoId, options = {}) {
367
+ const conversations = [];
368
+ for await (const conversation of fetchProjectConversations(this.client, gizmoId, {
369
+ delay: options.delay,
370
+ onProgress: options.onListProgress
371
+ })) {
372
+ conversations.push(conversation);
373
+ }
374
+ return conversations;
375
+ }
376
+ async resolveProjectId(nameOrId) {
377
+ const projects = await this.listProjects();
378
+ const match = projects.find(
379
+ (p) => p.gizmo.id === nameOrId || p.gizmo.display.name.toLowerCase() === nameOrId.toLowerCase()
380
+ );
381
+ if (!match) {
382
+ const available = projects.map((p) => p.gizmo.display.name).join(", ");
383
+ throw new Error(
384
+ `Project "${nameOrId}" not found. Available projects: ${available || "(none)"}`
385
+ );
386
+ }
387
+ return { gizmoId: match.gizmo.id, name: match.gizmo.display.name };
388
+ }
389
+ async downloadConversation(id) {
390
+ return this.client.fetch(ENDPOINTS.CONVERSATION(id), {
391
+ parseResponse: (data) => ConversationDetailSchema.parse(data)
392
+ });
393
+ }
394
+ async backup(options) {
395
+ const {
396
+ concurrency,
397
+ delay,
398
+ incremental,
399
+ verbose,
400
+ projectGizmoId,
401
+ onListProgress,
402
+ onDownloadProgress,
403
+ onError
404
+ } = options;
405
+ await this.storage.initialize();
406
+ await this.storage.appendLog("Starting backup...");
407
+ const conversations = projectGizmoId ? await this.listProjectConversations(projectGizmoId, { delay, onListProgress }) : await this.listConversations({ delay, onListProgress });
408
+ await this.storage.saveIndex(conversations);
409
+ const errors = [];
410
+ let downloaded = 0;
411
+ let skipped = 0;
412
+ let failed = 0;
413
+ let completed = 0;
414
+ const toDownload = [];
415
+ for (const conv of conversations) {
416
+ if (incremental) {
417
+ const existingUpdateTime = await this.storage.getExistingConversationUpdateTime(conv.id);
418
+ const convUpdateTime = typeof conv.update_time === "number" ? conv.update_time : conv.update_time ? new Date(conv.update_time).getTime() / 1e3 : null;
419
+ if (existingUpdateTime !== null && convUpdateTime !== null && existingUpdateTime >= convUpdateTime) {
420
+ skipped++;
421
+ completed++;
422
+ onDownloadProgress?.(completed, conversations.length, conv.title ?? conv.id);
423
+ continue;
424
+ }
425
+ }
426
+ toDownload.push(conv);
427
+ }
428
+ const downloadQueue = [...toDownload];
429
+ const inProgress = /* @__PURE__ */ new Set();
430
+ const processOne = async (conv) => {
431
+ try {
432
+ const detail = await this.downloadConversation(conv.id);
433
+ await this.storage.saveConversation(conv.id, detail);
434
+ downloaded++;
435
+ if (verbose) {
436
+ await this.storage.appendLog(`Downloaded: ${conv.id} - ${conv.title ?? "Untitled"}`);
437
+ }
438
+ } catch (error) {
439
+ const errorMessage = error instanceof Error ? error.message : String(error);
440
+ errors.push({ conversationId: conv.id, error: errorMessage });
441
+ failed++;
442
+ onError?.(conv.id, error);
443
+ await this.storage.appendLog(`Failed: ${conv.id} - ${errorMessage}`);
444
+ } finally {
445
+ completed++;
446
+ onDownloadProgress?.(completed, conversations.length, conv.title ?? conv.id);
447
+ }
448
+ };
449
+ while (downloadQueue.length > 0 || inProgress.size > 0) {
450
+ while (inProgress.size < concurrency && downloadQueue.length > 0) {
451
+ const conv = downloadQueue.shift();
452
+ const promise = processOne(conv).then(() => {
453
+ inProgress.delete(promise);
454
+ });
455
+ inProgress.add(promise);
456
+ if (downloadQueue.length > 0 && delay > 0) {
457
+ await sleep(delay);
458
+ }
459
+ }
460
+ if (inProgress.size > 0) {
461
+ await Promise.race(inProgress);
462
+ }
463
+ }
464
+ const metadata = {
465
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
466
+ totalConversations: conversations.length,
467
+ successfulDownloads: downloaded,
468
+ failedDownloads: failed,
469
+ errors
470
+ };
471
+ await this.storage.saveMetadata(metadata);
472
+ await this.storage.appendLog(`Backup completed: ${downloaded} downloaded, ${skipped} skipped, ${failed} failed`);
473
+ return {
474
+ totalConversations: conversations.length,
475
+ downloaded,
476
+ skipped,
477
+ failed,
478
+ errors
479
+ };
480
+ }
481
+ };
482
+
483
+ // src/services/markdown-service.ts
484
+ import fs from "fs/promises";
485
+ import path from "path";
486
+ function extractTextFromParts(parts) {
487
+ const pieces = [];
488
+ for (const part of parts) {
489
+ if (typeof part === "string") {
490
+ pieces.push(part);
491
+ } else if (part && typeof part === "object") {
492
+ const obj = part;
493
+ if (obj.content_type === "image_asset_pointer") {
494
+ const pointer = obj.asset_pointer;
495
+ pieces.push(`![image](${pointer ?? "unknown"})`);
496
+ }
497
+ }
498
+ }
499
+ return pieces.join("\n");
500
+ }
501
+ function getLinearThread(mapping, currentNode) {
502
+ if (currentNode && mapping[currentNode]) {
503
+ const path3 = [];
504
+ let nodeId = currentNode;
505
+ while (nodeId && mapping[nodeId]) {
506
+ path3.unshift(mapping[nodeId]);
507
+ nodeId = mapping[nodeId].parent;
508
+ }
509
+ return path3;
510
+ }
511
+ let root;
512
+ for (const node of Object.values(mapping)) {
513
+ if (node.parent === null || node.parent === void 0) {
514
+ root = node;
515
+ break;
516
+ }
517
+ }
518
+ if (!root) return [];
519
+ const thread = [root];
520
+ let current = root;
521
+ while (current.children && current.children.length > 0) {
522
+ const nextId = current.children[0];
523
+ const next = mapping[nextId];
524
+ if (!next) break;
525
+ thread.push(next);
526
+ current = next;
527
+ }
528
+ return thread;
529
+ }
530
+ function formatDate(timestamp) {
531
+ const date = new Date(timestamp * 1e3);
532
+ return date.toISOString().split("T")[0];
533
+ }
534
+ function formatRole(role) {
535
+ switch (role) {
536
+ case "user":
537
+ return "User";
538
+ case "assistant":
539
+ return "Assistant";
540
+ default:
541
+ return role.charAt(0).toUpperCase() + role.slice(1);
542
+ }
543
+ }
544
+ function convertConversation(detail) {
545
+ const lines = [];
546
+ const title = detail.title ?? "Untitled";
547
+ lines.push(`# ${title}`);
548
+ if (detail.create_time) {
549
+ lines.push(`*${formatDate(detail.create_time)}*`);
550
+ }
551
+ const thread = getLinearThread(
552
+ detail.mapping,
553
+ detail.current_node
554
+ );
555
+ let firstMessage = true;
556
+ for (const node of thread) {
557
+ const msg = node.message;
558
+ if (!msg) continue;
559
+ const role = msg.author.role;
560
+ if (role === "system" || role === "tool") continue;
561
+ if (msg.metadata?.is_visually_hidden_from_conversation) continue;
562
+ if (msg.weight === 0) continue;
563
+ let text = "";
564
+ if (msg.content) {
565
+ if (msg.content.parts && msg.content.parts.length > 0) {
566
+ text = extractTextFromParts(msg.content.parts);
567
+ } else if (msg.content.text) {
568
+ text = msg.content.text;
569
+ }
570
+ }
571
+ if (!text.trim()) continue;
572
+ if (firstMessage) {
573
+ lines.push("");
574
+ firstMessage = false;
575
+ } else {
576
+ lines.push("");
577
+ lines.push("---");
578
+ lines.push("");
579
+ }
580
+ lines.push(`**${formatRole(role)}:**`);
581
+ lines.push("");
582
+ lines.push(text);
583
+ }
584
+ lines.push("");
585
+ return lines.join("\n");
586
+ }
587
+ async function findJsonFiles(dir) {
588
+ const files = [];
589
+ try {
590
+ const entries = await fs.readdir(dir, { withFileTypes: true });
591
+ for (const entry of entries) {
592
+ const fullPath = path.join(dir, entry.name);
593
+ if (entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json") {
594
+ files.push(fullPath);
595
+ }
596
+ }
597
+ } catch {
598
+ }
599
+ return files;
600
+ }
601
+ async function collectAllJsonFiles(inputDir) {
602
+ const files = [];
603
+ const mainDir = path.join(inputDir, "conversations");
604
+ files.push(...await findJsonFiles(mainDir));
605
+ const projectsDir = path.join(inputDir, "projects");
606
+ try {
607
+ const projectEntries = await fs.readdir(projectsDir, { withFileTypes: true });
608
+ for (const entry of projectEntries) {
609
+ if (entry.isDirectory()) {
610
+ const projectConvDir = path.join(projectsDir, entry.name, "conversations");
611
+ files.push(...await findJsonFiles(projectConvDir));
612
+ }
613
+ }
614
+ } catch {
615
+ }
616
+ return files;
617
+ }
618
+ async function convertDirectory(inputDir) {
619
+ const jsonFiles = await collectAllJsonFiles(inputDir);
620
+ let converted = 0;
621
+ let errors = 0;
622
+ for (const jsonPath of jsonFiles) {
623
+ try {
624
+ const raw = await fs.readFile(jsonPath, "utf-8");
625
+ const detail = JSON.parse(raw);
626
+ const markdown = convertConversation(detail);
627
+ const mdPath = jsonPath.replace(/\.json$/, ".md");
628
+ await fs.writeFile(mdPath, markdown, "utf-8");
629
+ converted++;
630
+ } catch {
631
+ errors++;
632
+ }
633
+ }
634
+ return { converted, errors };
635
+ }
636
+
637
+ // src/services/storage-service.ts
638
+ import fs2 from "fs/promises";
639
+ import path2 from "path";
640
+ var StorageService = class {
641
+ outputDir;
642
+ conversationsDir;
643
+ constructor(outputDir, projectName) {
644
+ this.outputDir = outputDir;
645
+ if (projectName) {
646
+ const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
647
+ this.conversationsDir = path2.join(outputDir, "projects", safeName, "conversations");
648
+ } else {
649
+ this.conversationsDir = path2.join(outputDir, "conversations");
650
+ }
651
+ }
652
+ async initialize() {
653
+ await fs2.mkdir(this.conversationsDir, { recursive: true });
654
+ }
655
+ async saveConversation(id, data) {
656
+ const filePath = path2.join(this.conversationsDir, `${id}.json`);
657
+ await fs2.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
658
+ }
659
+ async saveIndex(conversations) {
660
+ const filePath = path2.join(this.conversationsDir, "index.json");
661
+ await fs2.writeFile(filePath, JSON.stringify(conversations, null, 2), "utf-8");
662
+ }
663
+ async saveMetadata(metadata) {
664
+ const filePath = path2.join(this.outputDir, "metadata.json");
665
+ await fs2.writeFile(filePath, JSON.stringify(metadata, null, 2), "utf-8");
666
+ }
667
+ async appendLog(message) {
668
+ const filePath = path2.join(this.outputDir, "backup.log");
669
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
670
+ await fs2.appendFile(filePath, `[${timestamp}] ${message}
671
+ `, "utf-8");
672
+ }
673
+ async conversationExists(id) {
674
+ const filePath = path2.join(this.conversationsDir, `${id}.json`);
675
+ try {
676
+ await fs2.access(filePath);
677
+ return true;
678
+ } catch {
679
+ return false;
680
+ }
681
+ }
682
+ async getExistingConversationUpdateTime(id) {
683
+ const filePath = path2.join(this.conversationsDir, `${id}.json`);
684
+ try {
685
+ const content = await fs2.readFile(filePath, "utf-8");
686
+ const data = JSON.parse(content);
687
+ return data.update_time ?? null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+ getConversationPath(id) {
693
+ return path2.join(this.conversationsDir, `${id}.json`);
694
+ }
695
+ };
696
+
697
+ // src/utils/progress.ts
698
+ import cliProgress from "cli-progress";
699
+ import chalk from "chalk";
700
+ function createProgressBar(total, label) {
701
+ const bar = new cliProgress.SingleBar({
702
+ format: `${chalk.cyan(label)} |${chalk.cyan("{bar}")}| {percentage}% | {value}/{total}`,
703
+ barCompleteChar: "\u2588",
704
+ barIncompleteChar: "\u2591",
705
+ hideCursor: true,
706
+ gracefulExit: true
707
+ });
708
+ bar.start(total, 0);
709
+ return bar;
710
+ }
711
+
712
+ // src/cli/commands/backup.ts
713
+ function runBackupWithProgress(service, spinner, options) {
714
+ let listProgressBar = null;
715
+ let downloadProgressBar = null;
716
+ let listingDone = false;
717
+ return service.backup({
718
+ ...options,
719
+ onListProgress: (fetched, totalList) => {
720
+ if (!listingDone) {
721
+ if (!listProgressBar) {
722
+ spinner.stop();
723
+ listProgressBar = createProgressBar(totalList, "Listing ");
724
+ }
725
+ listProgressBar.setTotal(totalList);
726
+ listProgressBar.update(fetched);
727
+ if (fetched >= totalList) {
728
+ listProgressBar.stop();
729
+ listingDone = true;
730
+ console.log();
731
+ }
732
+ }
733
+ },
734
+ onDownloadProgress: (completed, totalDownload) => {
735
+ if (listingDone) {
736
+ if (!downloadProgressBar) {
737
+ downloadProgressBar = createProgressBar(totalDownload, "Downloading");
738
+ }
739
+ downloadProgressBar.update(completed);
740
+ }
741
+ },
742
+ onError: (id, error) => {
743
+ if (options.verbose) {
744
+ console.error(chalk2.red(`
745
+ Failed to download ${id}: ${error.message}`));
746
+ }
747
+ }
748
+ }).then((result) => {
749
+ downloadProgressBar?.stop();
750
+ return result;
751
+ });
752
+ }
753
+ async function backupCommand(options) {
754
+ const { token, output, concurrency, delay, incremental, verbose, project } = options;
755
+ const client = new ChatGPTClient(token, { verbose });
756
+ const spinner = ora("Authenticating...").start();
757
+ try {
758
+ await client.initialize();
759
+ spinner.succeed("Authenticated");
760
+ console.log(chalk2.dim(`
761
+ Backup settings:`));
762
+ console.log(chalk2.dim(` Output: ${output}`));
763
+ console.log(chalk2.dim(` Concurrency: ${concurrency}`));
764
+ console.log(chalk2.dim(` Delay: ${delay}ms`));
765
+ console.log(chalk2.dim(` Incremental: ${incremental}`));
766
+ if (project) {
767
+ console.log(chalk2.dim(` Project: ${project}`));
768
+ }
769
+ console.log();
770
+ if (project) {
771
+ const baseService = new BackupService(client, new StorageService(output));
772
+ spinner.start("Resolving project...");
773
+ const { gizmoId, name } = await baseService.resolveProjectId(project);
774
+ spinner.succeed(`Project: ${name}`);
775
+ const storage = new StorageService(output, name);
776
+ const service = new BackupService(client, storage);
777
+ spinner.start(`Backing up project "${name}"...`);
778
+ const result = await runBackupWithProgress(service, spinner, {
779
+ concurrency,
780
+ delay,
781
+ incremental,
782
+ verbose,
783
+ projectGizmoId: gizmoId
784
+ });
785
+ printResult(result, output);
786
+ const md = await convertDirectory(output);
787
+ console.log(`Converted ${md.converted} conversations to markdown.`);
788
+ if (md.errors > 0) {
789
+ console.log(`Markdown conversion errors: ${chalk2.red(md.errors)}`);
790
+ }
791
+ } else {
792
+ let totalDownloaded = 0;
793
+ let totalSkipped = 0;
794
+ let totalFailed = 0;
795
+ let totalConversations = 0;
796
+ console.log(chalk2.bold("Main conversations\n"));
797
+ const mainStorage = new StorageService(output);
798
+ const mainService = new BackupService(client, mainStorage);
799
+ spinner.start("Fetching conversation list...");
800
+ const mainResult = await runBackupWithProgress(mainService, spinner, {
801
+ concurrency,
802
+ delay,
803
+ incremental,
804
+ verbose
805
+ });
806
+ totalDownloaded += mainResult.downloaded;
807
+ totalSkipped += mainResult.skipped;
808
+ totalFailed += mainResult.failed;
809
+ totalConversations += mainResult.totalConversations;
810
+ console.log();
811
+ console.log(` Downloaded: ${chalk2.green(mainResult.downloaded)}, Skipped: ${chalk2.yellow(mainResult.skipped)}, Failed: ${chalk2.red(mainResult.failed)}`);
812
+ console.log();
813
+ spinner.start("Fetching projects...");
814
+ const projects = await mainService.listProjects();
815
+ spinner.succeed(`Found ${projects.length} projects`);
816
+ for (const proj of projects) {
817
+ const name = proj.gizmo.display.name;
818
+ const gizmoId = proj.gizmo.id;
819
+ console.log();
820
+ console.log(chalk2.bold(`Project: ${name}
821
+ `));
822
+ const projStorage = new StorageService(output, name);
823
+ const projService = new BackupService(client, projStorage);
824
+ spinner.start(`Backing up "${name}"...`);
825
+ const projResult = await runBackupWithProgress(projService, spinner, {
826
+ concurrency,
827
+ delay,
828
+ incremental,
829
+ verbose,
830
+ projectGizmoId: gizmoId
831
+ });
832
+ totalDownloaded += projResult.downloaded;
833
+ totalSkipped += projResult.skipped;
834
+ totalFailed += projResult.failed;
835
+ totalConversations += projResult.totalConversations;
836
+ console.log();
837
+ console.log(` Downloaded: ${chalk2.green(projResult.downloaded)}, Skipped: ${chalk2.yellow(projResult.skipped)}, Failed: ${chalk2.red(projResult.failed)}`);
838
+ }
839
+ console.log();
840
+ console.log(chalk2.green("\nBackup completed!"));
841
+ console.log(` Total conversations: ${totalConversations}`);
842
+ console.log(` Downloaded: ${chalk2.green(totalDownloaded)}`);
843
+ if (totalSkipped > 0) {
844
+ console.log(` Skipped (unchanged): ${chalk2.yellow(totalSkipped)}`);
845
+ }
846
+ if (totalFailed > 0) {
847
+ console.log(` Failed: ${chalk2.red(totalFailed)}`);
848
+ }
849
+ console.log(`
850
+ Output directory: ${chalk2.cyan(output)}`);
851
+ const md = await convertDirectory(output);
852
+ console.log(`
853
+ Converted ${md.converted} conversations to markdown.`);
854
+ if (md.errors > 0) {
855
+ console.log(`Markdown conversion errors: ${chalk2.red(md.errors)}`);
856
+ }
857
+ }
858
+ } catch (error) {
859
+ spinner.fail("Backup failed");
860
+ if (error instanceof Error) {
861
+ if (error.name === "AuthenticationError") {
862
+ console.error(chalk2.red(`
863
+ Authentication failed: ${error.message}`));
864
+ console.error(chalk2.yellow("\nTo get a new access token:"));
865
+ console.error(" 1. Open chatgpt.com in your browser and log in");
866
+ console.error(" 2. Open DevTools (F12) \u2192 Network tab");
867
+ console.error(" 3. Refresh the page or send a message");
868
+ console.error(" 4. Find any request to /backend-api/*");
869
+ console.error(' 5. Look in Request Headers for "Authorization: Bearer <token>"');
870
+ console.error(' 6. Copy the token (starts with "eyJhbG...")');
871
+ } else {
872
+ console.error(chalk2.red(`
873
+ Error: ${error.message}`));
874
+ }
875
+ }
876
+ process.exit(1);
877
+ }
878
+ }
879
+ function printResult(result, output) {
880
+ console.log();
881
+ console.log(chalk2.green("\nBackup completed!"));
882
+ console.log(` Total conversations: ${result.totalConversations}`);
883
+ console.log(` Downloaded: ${chalk2.green(result.downloaded)}`);
884
+ if (result.skipped > 0) {
885
+ console.log(` Skipped (unchanged): ${chalk2.yellow(result.skipped)}`);
886
+ }
887
+ if (result.failed > 0) {
888
+ console.log(` Failed: ${chalk2.red(result.failed)}`);
889
+ console.log(chalk2.dim(` See ${output}/backup.log for details`));
890
+ }
891
+ console.log(`
892
+ Output directory: ${chalk2.cyan(output)}`);
893
+ }
894
+
895
+ // src/cli/commands/list.ts
896
+ import chalk3 from "chalk";
897
+ import ora2 from "ora";
898
+ async function listCommand(options) {
899
+ const { token, delay, verbose, json, project } = options;
900
+ const client = new ChatGPTClient(token, { verbose });
901
+ const storage = new StorageService("./chatgpt-export");
902
+ const service = new BackupService(client, storage);
903
+ const spinner = ora2("Authenticating...").start();
904
+ try {
905
+ await client.initialize();
906
+ let conversations;
907
+ if (project) {
908
+ spinner.text = "Resolving project...";
909
+ const { gizmoId, name } = await service.resolveProjectId(project);
910
+ spinner.text = `Fetching conversations from project "${name}"...`;
911
+ let lastReported = 0;
912
+ conversations = await service.listProjectConversations(gizmoId, {
913
+ delay,
914
+ onListProgress: (fetched, total) => {
915
+ if (fetched !== lastReported) {
916
+ spinner.text = `Fetching conversations from "${name}"... ${fetched}`;
917
+ lastReported = fetched;
918
+ }
919
+ }
920
+ });
921
+ spinner.succeed(`Found ${conversations.length} conversations in project "${name}"`);
922
+ } else {
923
+ spinner.text = "Fetching conversations...";
924
+ let lastReported = 0;
925
+ conversations = await service.listConversations({
926
+ delay,
927
+ onListProgress: (fetched, total) => {
928
+ if (fetched !== lastReported) {
929
+ spinner.text = `Fetching conversations... ${fetched}/${total}`;
930
+ lastReported = fetched;
931
+ }
932
+ }
933
+ });
934
+ spinner.succeed(`Found ${conversations.length} conversations`);
935
+ }
936
+ if (json) {
937
+ console.log(JSON.stringify(conversations, null, 2));
938
+ } else {
939
+ console.log();
940
+ for (const conv of conversations) {
941
+ const title = conv.title ?? chalk3.dim("(untitled)");
942
+ const date = conv.update_time ? new Date(
943
+ typeof conv.update_time === "number" ? conv.update_time * 1e3 : conv.update_time
944
+ ).toLocaleDateString() : "unknown date";
945
+ console.log(`${chalk3.cyan(conv.id)} ${title} ${chalk3.dim(`[${date}]`)}`);
946
+ }
947
+ console.log();
948
+ console.log(chalk3.green(`Total: ${conversations.length} conversations`));
949
+ }
950
+ } catch (error) {
951
+ spinner.fail("Failed to list conversations");
952
+ if (error instanceof Error) {
953
+ if (error.name === "AuthenticationError") {
954
+ console.error(chalk3.red(`
955
+ Authentication failed: ${error.message}`));
956
+ console.error(chalk3.yellow("\nTo get a new access token:"));
957
+ console.error(" 1. Open chatgpt.com in your browser and log in");
958
+ console.error(" 2. Open DevTools (F12) \u2192 Network tab");
959
+ console.error(" 3. Refresh the page or send a message");
960
+ console.error(" 4. Find any request to /backend-api/*");
961
+ console.error(' 5. Look in Request Headers for "Authorization: Bearer <token>"');
962
+ console.error(' 6. Copy the token (starts with "eyJhbG...")');
963
+ } else {
964
+ console.error(chalk3.red(`
965
+ Error: ${error.message}`));
966
+ }
967
+ }
968
+ process.exit(1);
969
+ }
970
+ }
971
+
972
+ // src/cli/commands/projects.ts
973
+ import chalk4 from "chalk";
974
+ import ora3 from "ora";
975
+ async function projectsCommand(options) {
976
+ const { token, verbose, json } = options;
977
+ const client = new ChatGPTClient(token, { verbose });
978
+ const storage = new StorageService("./chatgpt-export");
979
+ const service = new BackupService(client, storage);
980
+ const spinner = ora3("Authenticating...").start();
981
+ try {
982
+ await client.initialize();
983
+ spinner.text = "Fetching projects...";
984
+ const projects = await service.listProjects();
985
+ spinner.succeed(`Found ${projects.length} projects`);
986
+ if (json) {
987
+ const output = projects.map((p) => ({
988
+ id: p.gizmo.id,
989
+ name: p.gizmo.display.name,
990
+ num_interactions: p.gizmo.num_interactions,
991
+ last_interacted_at: p.gizmo.last_interacted_at,
992
+ is_archived: p.gizmo.is_archived
993
+ }));
994
+ console.log(JSON.stringify(output, null, 2));
995
+ } else {
996
+ console.log();
997
+ for (const project of projects) {
998
+ const gizmo = project.gizmo;
999
+ const name = gizmo.display.name;
1000
+ const id = gizmo.id;
1001
+ const lastInteracted = gizmo.last_interacted_at ? new Date(gizmo.last_interacted_at).toLocaleDateString() : "never";
1002
+ console.log(
1003
+ `${chalk4.cyan(id)} ${chalk4.bold(name)} ${chalk4.dim(`[last: ${lastInteracted}]`)}`
1004
+ );
1005
+ }
1006
+ console.log();
1007
+ console.log(chalk4.green(`Total: ${projects.length} projects`));
1008
+ }
1009
+ } catch (error) {
1010
+ spinner.fail("Failed to list projects");
1011
+ if (error instanceof Error) {
1012
+ if (error.name === "AuthenticationError") {
1013
+ console.error(chalk4.red(`
1014
+ Authentication failed: ${error.message}`));
1015
+ console.error(chalk4.yellow("\nTo get a new access token:"));
1016
+ console.error(" 1. Open chatgpt.com in your browser and log in");
1017
+ console.error(" 2. Open DevTools (F12) \u2192 Network tab");
1018
+ console.error(" 3. Refresh the page or send a message");
1019
+ console.error(" 4. Find any request to /backend-api/*");
1020
+ console.error(' 5. Look in Request Headers for "Authorization: Bearer <token>"');
1021
+ console.error(' 6. Copy the token (starts with "eyJhbG...")');
1022
+ } else {
1023
+ console.error(chalk4.red(`
1024
+ Error: ${error.message}`));
1025
+ }
1026
+ }
1027
+ process.exit(1);
1028
+ }
1029
+ }
1030
+
1031
+ // src/cli/index.ts
1032
+ function createCli() {
1033
+ const program2 = new Command();
1034
+ program2.name("chatgpt-exporter").description("Export your ChatGPT conversations").version("1.0.0").action(() => {
1035
+ program2.help();
1036
+ });
1037
+ const getToken = (options) => {
1038
+ const token = options.token ?? process.env.CHATGPT_TOKEN;
1039
+ if (!token) {
1040
+ console.error("Error: Access token required. Use --token or set CHATGPT_TOKEN env variable.");
1041
+ console.error("\nTo get your access token:");
1042
+ console.error(" 1. Open chatgpt.com in your browser and log in");
1043
+ console.error(" 2. Open DevTools (F12) \u2192 Network tab");
1044
+ console.error(" 3. Refresh the page or send a message");
1045
+ console.error(" 4. Find any request to /backend-api/*");
1046
+ console.error(' 5. Look in Request Headers for "Authorization: Bearer <token>"');
1047
+ console.error(' 6. Copy the token (starts with "eyJhbG...")');
1048
+ process.exit(1);
1049
+ }
1050
+ return token;
1051
+ };
1052
+ program2.command("backup").description("Download all conversations").option("-t, --token <token>", "Access token (or CHATGPT_TOKEN env)").option("-o, --output <dir>", "Output directory", "./chatgpt-export").option("--concurrency <n>", "Parallel downloads", (v) => parseInt(v, 10), 3).option("--delay <ms>", "Delay between requests in ms", (v) => parseInt(v, 10), 500).option("--incremental", "Only download new/updated conversations", false).option("--project <name-or-id>", "Only backup conversations from a specific project").option("-v, --verbose", "Verbose logging", false).action(async (options) => {
1053
+ const token = getToken(options);
1054
+ await backupCommand({
1055
+ token,
1056
+ output: options.output,
1057
+ concurrency: options.concurrency,
1058
+ delay: options.delay,
1059
+ incremental: options.incremental,
1060
+ verbose: options.verbose,
1061
+ project: options.project
1062
+ });
1063
+ });
1064
+ program2.command("list").description("List conversations without downloading").option("-t, --token <token>", "Access token (or CHATGPT_TOKEN env)").option("--delay <ms>", "Delay between requests in ms", (v) => parseInt(v, 10), 500).option("--project <name-or-id>", "List conversations from a specific project").option("-v, --verbose", "Verbose logging", false).option("--json", "Output as JSON", false).action(async (options) => {
1065
+ const token = getToken(options);
1066
+ await listCommand({
1067
+ token,
1068
+ delay: options.delay,
1069
+ verbose: options.verbose,
1070
+ json: options.json,
1071
+ project: options.project
1072
+ });
1073
+ });
1074
+ program2.command("projects").description("List all projects").option("-t, --token <token>", "Access token (or CHATGPT_TOKEN env)").option("-v, --verbose", "Verbose logging", false).option("--json", "Output as JSON", false).action(async (options) => {
1075
+ const token = getToken(options);
1076
+ await projectsCommand({
1077
+ token,
1078
+ verbose: options.verbose,
1079
+ json: options.json
1080
+ });
1081
+ });
1082
+ return program2;
1083
+ }
1084
+
1085
+ // src/index.ts
1086
+ process.on("SIGINT", () => process.exit(130));
1087
+ var program = createCli();
1088
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "chatgpt-exporter",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to export ChatGPT conversations",
5
+ "license": "MIT",
6
+ "author": "Rodrigo Fernández",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "chatgpt-exporter": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "dev": "tsx src/index.ts",
20
+ "build": "tsup src/index.ts --format esm --dts --clean",
21
+ "start": "node dist/index.js",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/FdezRomero/chatgpt-export.git"
27
+ },
28
+ "keywords": [
29
+ "chatgpt",
30
+ "backup",
31
+ "export",
32
+ "cli",
33
+ "markdown"
34
+ ],
35
+ "dependencies": {
36
+ "chalk": "^5.3.0",
37
+ "cli-progress": "^3.12.0",
38
+ "commander": "^14.0.3",
39
+ "ora": "^9.3.0",
40
+ "zod": "^4.3.6"
41
+ },
42
+ "devDependencies": {
43
+ "@types/cli-progress": "^3.11.5",
44
+ "@types/node": "^25.3.0",
45
+ "tsup": "^8.1.0",
46
+ "tsx": "^4.15.0",
47
+ "typescript": "^5.4.5"
48
+ }
49
+ }