@savestate/cli 0.6.0 → 0.8.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.
Files changed (159) hide show
  1. package/dist/cli/__tests__/progress.test.d.ts +5 -0
  2. package/dist/cli/__tests__/progress.test.d.ts.map +1 -0
  3. package/dist/cli/__tests__/progress.test.js +212 -0
  4. package/dist/cli/__tests__/progress.test.js.map +1 -0
  5. package/dist/cli/__tests__/signal-handler.test.d.ts +5 -0
  6. package/dist/cli/__tests__/signal-handler.test.d.ts.map +1 -0
  7. package/dist/cli/__tests__/signal-handler.test.js +99 -0
  8. package/dist/cli/__tests__/signal-handler.test.js.map +1 -0
  9. package/dist/cli/__tests__/summary.test.d.ts +5 -0
  10. package/dist/cli/__tests__/summary.test.d.ts.map +1 -0
  11. package/dist/cli/__tests__/summary.test.js +242 -0
  12. package/dist/cli/__tests__/summary.test.js.map +1 -0
  13. package/dist/cli/index.d.ts +10 -0
  14. package/dist/cli/index.d.ts.map +1 -0
  15. package/dist/cli/index.js +10 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/progress.d.ts +86 -0
  18. package/dist/cli/progress.d.ts.map +1 -0
  19. package/dist/cli/progress.js +205 -0
  20. package/dist/cli/progress.js.map +1 -0
  21. package/dist/cli/prompts.d.ts +49 -0
  22. package/dist/cli/prompts.d.ts.map +1 -0
  23. package/dist/cli/prompts.js +266 -0
  24. package/dist/cli/prompts.js.map +1 -0
  25. package/dist/cli/signal-handler.d.ts +63 -0
  26. package/dist/cli/signal-handler.d.ts.map +1 -0
  27. package/dist/cli/signal-handler.js +165 -0
  28. package/dist/cli/signal-handler.js.map +1 -0
  29. package/dist/cli/summary.d.ts +33 -0
  30. package/dist/cli/summary.d.ts.map +1 -0
  31. package/dist/cli/summary.js +296 -0
  32. package/dist/cli/summary.js.map +1 -0
  33. package/dist/cli.js +7 -1
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/migrate.d.ts +18 -4
  36. package/dist/commands/migrate.d.ts.map +1 -1
  37. package/dist/commands/migrate.js +324 -163
  38. package/dist/commands/migrate.js.map +1 -1
  39. package/dist/migrate/__tests__/capabilities.test.d.ts +7 -0
  40. package/dist/migrate/__tests__/capabilities.test.d.ts.map +1 -0
  41. package/dist/migrate/__tests__/capabilities.test.js +90 -0
  42. package/dist/migrate/__tests__/capabilities.test.js.map +1 -0
  43. package/dist/migrate/__tests__/chatgpt-loader.test.d.ts +7 -0
  44. package/dist/migrate/__tests__/chatgpt-loader.test.d.ts.map +1 -0
  45. package/dist/migrate/__tests__/chatgpt-loader.test.js +889 -0
  46. package/dist/migrate/__tests__/chatgpt-loader.test.js.map +1 -0
  47. package/dist/migrate/__tests__/claude-loader.test.d.ts +7 -0
  48. package/dist/migrate/__tests__/claude-loader.test.d.ts.map +1 -0
  49. package/dist/migrate/__tests__/claude-loader.test.js +544 -0
  50. package/dist/migrate/__tests__/claude-loader.test.js.map +1 -0
  51. package/dist/migrate/__tests__/edge-cases.test.d.ts +7 -0
  52. package/dist/migrate/__tests__/edge-cases.test.d.ts.map +1 -0
  53. package/dist/migrate/__tests__/edge-cases.test.js +787 -0
  54. package/dist/migrate/__tests__/edge-cases.test.js.map +1 -0
  55. package/dist/migrate/__tests__/error-recovery.test.d.ts +7 -0
  56. package/dist/migrate/__tests__/error-recovery.test.d.ts.map +1 -0
  57. package/dist/migrate/__tests__/error-recovery.test.js +461 -0
  58. package/dist/migrate/__tests__/error-recovery.test.js.map +1 -0
  59. package/dist/migrate/__tests__/integration.test.d.ts +7 -0
  60. package/dist/migrate/__tests__/integration.test.d.ts.map +1 -0
  61. package/dist/migrate/__tests__/integration.test.js +536 -0
  62. package/dist/migrate/__tests__/integration.test.js.map +1 -0
  63. package/dist/migrate/__tests__/orchestrator.test.d.ts +8 -0
  64. package/dist/migrate/__tests__/orchestrator.test.d.ts.map +1 -0
  65. package/dist/migrate/__tests__/orchestrator.test.js +355 -0
  66. package/dist/migrate/__tests__/orchestrator.test.js.map +1 -0
  67. package/dist/migrate/__tests__/performance.test.d.ts +7 -0
  68. package/dist/migrate/__tests__/performance.test.d.ts.map +1 -0
  69. package/dist/migrate/__tests__/performance.test.js +478 -0
  70. package/dist/migrate/__tests__/performance.test.js.map +1 -0
  71. package/dist/migrate/__tests__/registry.test.d.ts +7 -0
  72. package/dist/migrate/__tests__/registry.test.d.ts.map +1 -0
  73. package/dist/migrate/__tests__/registry.test.js +167 -0
  74. package/dist/migrate/__tests__/registry.test.js.map +1 -0
  75. package/dist/migrate/compatibility.d.ts +47 -0
  76. package/dist/migrate/compatibility.d.ts.map +1 -0
  77. package/dist/migrate/compatibility.js +468 -0
  78. package/dist/migrate/compatibility.js.map +1 -0
  79. package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts +12 -0
  80. package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts.map +1 -0
  81. package/dist/migrate/extractors/__tests__/chatgpt.test.js +522 -0
  82. package/dist/migrate/extractors/__tests__/chatgpt.test.js.map +1 -0
  83. package/dist/migrate/extractors/__tests__/claude.test.d.ts +12 -0
  84. package/dist/migrate/extractors/__tests__/claude.test.d.ts.map +1 -0
  85. package/dist/migrate/extractors/__tests__/claude.test.js +789 -0
  86. package/dist/migrate/extractors/__tests__/claude.test.js.map +1 -0
  87. package/dist/migrate/extractors/chatgpt.d.ts +70 -0
  88. package/dist/migrate/extractors/chatgpt.d.ts.map +1 -0
  89. package/dist/migrate/extractors/chatgpt.js +791 -0
  90. package/dist/migrate/extractors/chatgpt.js.map +1 -0
  91. package/dist/migrate/extractors/claude.d.ts +69 -0
  92. package/dist/migrate/extractors/claude.d.ts.map +1 -0
  93. package/dist/migrate/extractors/claude.js +1136 -0
  94. package/dist/migrate/extractors/claude.js.map +1 -0
  95. package/dist/migrate/extractors/registry.js +6 -4
  96. package/dist/migrate/extractors/registry.js.map +1 -1
  97. package/dist/migrate/index.d.ts +6 -1
  98. package/dist/migrate/index.d.ts.map +1 -1
  99. package/dist/migrate/index.js +12 -1
  100. package/dist/migrate/index.js.map +1 -1
  101. package/dist/migrate/loaders/chatgpt.d.ts +72 -0
  102. package/dist/migrate/loaders/chatgpt.d.ts.map +1 -0
  103. package/dist/migrate/loaders/chatgpt.js +691 -0
  104. package/dist/migrate/loaders/chatgpt.js.map +1 -0
  105. package/dist/migrate/loaders/claude.d.ts +61 -0
  106. package/dist/migrate/loaders/claude.d.ts.map +1 -0
  107. package/dist/migrate/loaders/claude.js +433 -0
  108. package/dist/migrate/loaders/claude.js.map +1 -0
  109. package/dist/migrate/loaders/registry.js +6 -4
  110. package/dist/migrate/loaders/registry.js.map +1 -1
  111. package/dist/migrate/orchestrator.d.ts +75 -1
  112. package/dist/migrate/orchestrator.d.ts.map +1 -1
  113. package/dist/migrate/orchestrator.js +215 -19
  114. package/dist/migrate/orchestrator.js.map +1 -1
  115. package/dist/migrate/testing/index.d.ts +28 -0
  116. package/dist/migrate/testing/index.d.ts.map +1 -0
  117. package/dist/migrate/testing/index.js +55 -0
  118. package/dist/migrate/testing/index.js.map +1 -0
  119. package/dist/migrate/testing/mock-extractor.d.ts +30 -0
  120. package/dist/migrate/testing/mock-extractor.d.ts.map +1 -0
  121. package/dist/migrate/testing/mock-extractor.js +137 -0
  122. package/dist/migrate/testing/mock-extractor.js.map +1 -0
  123. package/dist/migrate/testing/mock-loader.d.ts +36 -0
  124. package/dist/migrate/testing/mock-loader.d.ts.map +1 -0
  125. package/dist/migrate/testing/mock-loader.js +81 -0
  126. package/dist/migrate/testing/mock-loader.js.map +1 -0
  127. package/dist/migrate/testing/mock-transformer.d.ts +26 -0
  128. package/dist/migrate/testing/mock-transformer.d.ts.map +1 -0
  129. package/dist/migrate/testing/mock-transformer.js +185 -0
  130. package/dist/migrate/testing/mock-transformer.js.map +1 -0
  131. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts +5 -0
  132. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts.map +1 -0
  133. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js +333 -0
  134. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js.map +1 -0
  135. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts +5 -0
  136. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts.map +1 -0
  137. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js +333 -0
  138. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js.map +1 -0
  139. package/dist/migrate/transformers/__tests__/rules.test.d.ts +5 -0
  140. package/dist/migrate/transformers/__tests__/rules.test.d.ts.map +1 -0
  141. package/dist/migrate/transformers/__tests__/rules.test.js +375 -0
  142. package/dist/migrate/transformers/__tests__/rules.test.js.map +1 -0
  143. package/dist/migrate/transformers/chatgpt-to-claude.d.ts +40 -0
  144. package/dist/migrate/transformers/chatgpt-to-claude.d.ts.map +1 -0
  145. package/dist/migrate/transformers/chatgpt-to-claude.js +443 -0
  146. package/dist/migrate/transformers/chatgpt-to-claude.js.map +1 -0
  147. package/dist/migrate/transformers/claude-to-chatgpt.d.ts +41 -0
  148. package/dist/migrate/transformers/claude-to-chatgpt.d.ts.map +1 -0
  149. package/dist/migrate/transformers/claude-to-chatgpt.js +532 -0
  150. package/dist/migrate/transformers/claude-to-chatgpt.js.map +1 -0
  151. package/dist/migrate/transformers/registry.js +6 -4
  152. package/dist/migrate/transformers/registry.js.map +1 -1
  153. package/dist/migrate/transformers/rules.d.ts +168 -0
  154. package/dist/migrate/transformers/rules.d.ts.map +1 -0
  155. package/dist/migrate/transformers/rules.js +487 -0
  156. package/dist/migrate/transformers/rules.js.map +1 -0
  157. package/dist/migrate/types.d.ts +2 -0
  158. package/dist/migrate/types.d.ts.map +1 -1
  159. package/package.json +7 -2
@@ -0,0 +1,791 @@
1
+ /**
2
+ * ChatGPT Extractor
3
+ *
4
+ * Extracts user data from ChatGPT including:
5
+ * - Custom instructions
6
+ * - Memory entries
7
+ * - Conversation history (from export files)
8
+ * - Uploaded files/attachments
9
+ * - Custom GPT configurations
10
+ *
11
+ * Supports both API-based extraction and export file parsing.
12
+ */
13
+ import { randomBytes, createHash } from 'node:crypto';
14
+ import { mkdir, writeFile, readFile, stat, readdir, access, constants } from 'node:fs/promises';
15
+ import { join, extname, basename } from 'node:path';
16
+ // ─── Rate Limiter ────────────────────────────────────────────
17
+ class RateLimiter {
18
+ requests = [];
19
+ config;
20
+ constructor(config = {}) {
21
+ this.config = {
22
+ requestsPerMinute: config.requestsPerMinute ?? 60,
23
+ initialBackoffMs: config.initialBackoffMs ?? 1000,
24
+ maxBackoffMs: config.maxBackoffMs ?? 60000,
25
+ maxRetries: config.maxRetries ?? 5,
26
+ };
27
+ }
28
+ async acquire() {
29
+ const now = Date.now();
30
+ const windowStart = now - 60000;
31
+ // Remove old requests outside the window
32
+ this.requests = this.requests.filter((t) => t > windowStart);
33
+ // If at limit, wait
34
+ if (this.requests.length >= this.config.requestsPerMinute) {
35
+ const oldestInWindow = this.requests[0];
36
+ const waitTime = oldestInWindow - windowStart + 100;
37
+ await this.sleep(waitTime);
38
+ }
39
+ this.requests.push(Date.now());
40
+ }
41
+ async withRetry(fn) {
42
+ let lastError = null;
43
+ let backoff = this.config.initialBackoffMs;
44
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
45
+ try {
46
+ await this.acquire();
47
+ return await fn();
48
+ }
49
+ catch (error) {
50
+ lastError = error instanceof Error ? error : new Error(String(error));
51
+ // Check if it's a rate limit error
52
+ if (this.isRateLimitError(lastError)) {
53
+ if (attempt < this.config.maxRetries) {
54
+ await this.sleep(backoff);
55
+ backoff = Math.min(backoff * 2, this.config.maxBackoffMs);
56
+ continue;
57
+ }
58
+ }
59
+ throw lastError;
60
+ }
61
+ }
62
+ throw lastError ?? new Error('Max retries exceeded');
63
+ }
64
+ isRateLimitError(error) {
65
+ return (error.message.includes('429') ||
66
+ error.message.includes('rate limit') ||
67
+ error.message.includes('Rate limit') ||
68
+ error.message.includes('Too Many Requests'));
69
+ }
70
+ sleep(ms) {
71
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
+ }
73
+ }
74
+ // ─── ChatGPT Extractor ───────────────────────────────────────
75
+ export class ChatGPTExtractor {
76
+ platform = 'chatgpt';
77
+ version = '1.0.0';
78
+ config;
79
+ rateLimiter;
80
+ progress = 0;
81
+ baseUrl;
82
+ constructor(config = {}) {
83
+ this.config = {
84
+ chunkSize: 64 * 1024, // 64KB default chunk size
85
+ ...config,
86
+ };
87
+ this.rateLimiter = new RateLimiter(config.rateLimit);
88
+ this.baseUrl = config.baseUrl ?? 'https://api.openai.com/v1';
89
+ }
90
+ async canExtract() {
91
+ // Check if we have either API credentials or an export file
92
+ if (this.config.apiKey || this.config.accessToken) {
93
+ return this.verifyApiAccess();
94
+ }
95
+ if (this.config.exportPath) {
96
+ return this.verifyExportPath();
97
+ }
98
+ return false;
99
+ }
100
+ async extract(options) {
101
+ this.progress = 0;
102
+ const workDir = options.workDir;
103
+ await mkdir(workDir, { recursive: true });
104
+ const contents = {};
105
+ const warnings = [];
106
+ const errors = [];
107
+ const shouldInclude = (type) => !options.include || options.include.includes(type);
108
+ // Determine extraction method
109
+ const useApi = !!(this.config.apiKey || this.config.accessToken);
110
+ const useExport = !!this.config.exportPath;
111
+ try {
112
+ // Phase 1: Extract custom instructions (10%)
113
+ if (shouldInclude('instructions')) {
114
+ options.onProgress?.(0.05, 'Extracting custom instructions...');
115
+ try {
116
+ if (useApi) {
117
+ contents.instructions = await this.extractInstructionsApi();
118
+ }
119
+ else if (useExport) {
120
+ contents.instructions = await this.extractInstructionsFromExport();
121
+ }
122
+ }
123
+ catch (error) {
124
+ const msg = `Failed to extract instructions: ${error instanceof Error ? error.message : error}`;
125
+ warnings.push(msg);
126
+ }
127
+ }
128
+ this.progress = 10;
129
+ options.onProgress?.(0.1, 'Instructions extracted');
130
+ // Phase 2: Extract memories (20%)
131
+ if (shouldInclude('memories')) {
132
+ options.onProgress?.(0.15, 'Extracting memories...');
133
+ try {
134
+ if (useApi) {
135
+ contents.memories = await this.extractMemoriesApi();
136
+ }
137
+ else if (useExport) {
138
+ // Memories may be in the export or need API
139
+ contents.memories = await this.extractMemoriesFromExport();
140
+ }
141
+ }
142
+ catch (error) {
143
+ const msg = `Failed to extract memories: ${error instanceof Error ? error.message : error}`;
144
+ warnings.push(msg);
145
+ }
146
+ }
147
+ this.progress = 20;
148
+ options.onProgress?.(0.2, 'Memories extracted');
149
+ // Phase 3: Extract conversations (60%)
150
+ if (shouldInclude('conversations')) {
151
+ options.onProgress?.(0.25, 'Extracting conversations...');
152
+ try {
153
+ if (useExport) {
154
+ contents.conversations = await this.extractConversationsFromExport(workDir, (p, m) => {
155
+ const scaledProgress = 0.2 + p * 0.4;
156
+ options.onProgress?.(scaledProgress, m);
157
+ });
158
+ }
159
+ else {
160
+ // Without export, we can't get full conversations via API
161
+ warnings.push('Conversation history requires export file. Request your data from ChatGPT settings.');
162
+ }
163
+ }
164
+ catch (error) {
165
+ const msg = `Failed to extract conversations: ${error instanceof Error ? error.message : error}`;
166
+ errors.push(msg);
167
+ }
168
+ }
169
+ this.progress = 60;
170
+ options.onProgress?.(0.6, 'Conversations extracted');
171
+ // Phase 4: Extract files (80%)
172
+ if (shouldInclude('files')) {
173
+ options.onProgress?.(0.65, 'Extracting files...');
174
+ try {
175
+ if (useExport) {
176
+ contents.files = await this.extractFilesFromExport(workDir);
177
+ }
178
+ else if (useApi) {
179
+ contents.files = await this.extractFilesApi(workDir);
180
+ }
181
+ }
182
+ catch (error) {
183
+ const msg = `Failed to extract files: ${error instanceof Error ? error.message : error}`;
184
+ warnings.push(msg);
185
+ }
186
+ }
187
+ this.progress = 80;
188
+ options.onProgress?.(0.8, 'Files extracted');
189
+ // Phase 5: Extract custom GPTs (100%)
190
+ if (shouldInclude('customBots')) {
191
+ options.onProgress?.(0.85, 'Extracting custom GPTs...');
192
+ try {
193
+ if (useApi) {
194
+ contents.customBots = await this.extractGPTsApi();
195
+ }
196
+ else if (useExport) {
197
+ contents.customBots = await this.extractGPTsFromExport();
198
+ }
199
+ }
200
+ catch (error) {
201
+ const msg = `Failed to extract custom GPTs: ${error instanceof Error ? error.message : error}`;
202
+ warnings.push(msg);
203
+ }
204
+ }
205
+ this.progress = 100;
206
+ options.onProgress?.(1.0, 'Extraction complete');
207
+ // Build metadata
208
+ const metadata = {
209
+ totalItems: this.countItems(contents),
210
+ itemCounts: {
211
+ instructions: contents.instructions ? 1 : 0,
212
+ memories: contents.memories?.count ?? 0,
213
+ conversations: contents.conversations?.count ?? 0,
214
+ files: contents.files?.count ?? 0,
215
+ customBots: contents.customBots?.count ?? 0,
216
+ },
217
+ warnings,
218
+ errors,
219
+ };
220
+ // Build bundle
221
+ const bundle = {
222
+ version: '1.0',
223
+ id: `bundle_${randomBytes(8).toString('hex')}`,
224
+ source: {
225
+ platform: 'chatgpt',
226
+ extractedAt: new Date().toISOString(),
227
+ extractorVersion: this.version,
228
+ },
229
+ contents,
230
+ metadata,
231
+ };
232
+ return bundle;
233
+ }
234
+ catch (error) {
235
+ this.progress = 0;
236
+ throw error;
237
+ }
238
+ }
239
+ getProgress() {
240
+ return this.progress;
241
+ }
242
+ // ─── API-based Extraction ──────────────────────────────────
243
+ async verifyApiAccess() {
244
+ try {
245
+ const response = await this.apiRequest('/models', { method: 'GET' });
246
+ return response.ok;
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ async apiRequest(endpoint, init = {}) {
253
+ const url = `${this.baseUrl}${endpoint}`;
254
+ const headers = {
255
+ 'Content-Type': 'application/json',
256
+ ...init.headers,
257
+ };
258
+ if (this.config.apiKey) {
259
+ headers['Authorization'] = `Bearer ${this.config.apiKey}`;
260
+ }
261
+ else if (this.config.accessToken) {
262
+ headers['Authorization'] = `Bearer ${this.config.accessToken}`;
263
+ }
264
+ return this.rateLimiter.withRetry(() => fetch(url, { ...init, headers }));
265
+ }
266
+ async extractInstructionsApi() {
267
+ // Note: Custom instructions API is not publicly documented
268
+ // This is a best-effort implementation based on known endpoints
269
+ try {
270
+ // Try the user profile endpoint which may contain custom instructions
271
+ const response = await this.apiRequest('/dashboard/user/system_preferences');
272
+ if (response.ok) {
273
+ const data = await response.json();
274
+ const aboutUser = data.custom_instructions?.about_user ?? '';
275
+ const aboutModel = data.custom_instructions?.about_model ?? '';
276
+ const fullContent = [
277
+ aboutUser && `## About Me\n${aboutUser}`,
278
+ aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
279
+ ]
280
+ .filter(Boolean)
281
+ .join('\n\n');
282
+ return {
283
+ content: fullContent,
284
+ length: fullContent.length,
285
+ sections: this.parseInstructionSections(fullContent),
286
+ };
287
+ }
288
+ }
289
+ catch {
290
+ // API not available, fall through
291
+ }
292
+ return undefined;
293
+ }
294
+ async extractMemoriesApi() {
295
+ // Note: Memories API endpoint
296
+ try {
297
+ const response = await this.apiRequest('/memories');
298
+ if (response.ok) {
299
+ const data = await response.json();
300
+ const entries = (data.memories ?? []).map((m) => ({
301
+ id: m.id,
302
+ content: m.content,
303
+ createdAt: m.created_at,
304
+ updatedAt: m.updated_at,
305
+ source: 'chatgpt',
306
+ }));
307
+ return {
308
+ entries,
309
+ count: entries.length,
310
+ };
311
+ }
312
+ }
313
+ catch {
314
+ // API not available
315
+ }
316
+ return undefined;
317
+ }
318
+ async extractFilesApi(workDir) {
319
+ const filesDir = join(workDir, 'files');
320
+ await mkdir(filesDir, { recursive: true });
321
+ try {
322
+ const response = await this.apiRequest('/files');
323
+ if (response.ok) {
324
+ const data = await response.json();
325
+ const files = data.data ?? [];
326
+ const entries = [];
327
+ let totalSize = 0;
328
+ for (const file of files) {
329
+ // Download each file
330
+ const fileResponse = await this.apiRequest(`/files/${file.id}/content`);
331
+ if (fileResponse.ok) {
332
+ const buffer = Buffer.from(await fileResponse.arrayBuffer());
333
+ const safeFilename = basename(file.filename).replace(/[/\\]/g, '_');
334
+ const filePath = join(filesDir, safeFilename);
335
+ await writeFile(filePath, buffer);
336
+ entries.push({
337
+ id: file.id,
338
+ filename: safeFilename,
339
+ mimeType: this.guessMimeType(safeFilename),
340
+ size: file.bytes,
341
+ path: `files/${safeFilename}`,
342
+ });
343
+ totalSize += file.bytes;
344
+ }
345
+ }
346
+ return {
347
+ files: entries,
348
+ count: entries.length,
349
+ totalSize,
350
+ };
351
+ }
352
+ }
353
+ catch {
354
+ // API not available
355
+ }
356
+ return undefined;
357
+ }
358
+ async extractGPTsApi() {
359
+ // Note: Custom GPTs API
360
+ try {
361
+ const response = await this.apiRequest('/gizmos/my');
362
+ if (response.ok) {
363
+ const data = await response.json();
364
+ const gpts = data.gizmos ?? [];
365
+ const bots = gpts.map((gpt) => ({
366
+ id: gpt.id,
367
+ name: gpt.name,
368
+ description: gpt.description,
369
+ instructions: gpt.instructions ?? '',
370
+ knowledgeFiles: gpt.knowledge_files?.map((f) => f.name),
371
+ capabilities: gpt.tools,
372
+ createdAt: gpt.created_at,
373
+ updatedAt: gpt.updated_at,
374
+ }));
375
+ return {
376
+ bots,
377
+ count: bots.length,
378
+ };
379
+ }
380
+ }
381
+ catch {
382
+ // API not available
383
+ }
384
+ return undefined;
385
+ }
386
+ // ─── Export-based Extraction ───────────────────────────────
387
+ async verifyExportPath() {
388
+ if (!this.config.exportPath)
389
+ return false;
390
+ try {
391
+ await access(this.config.exportPath, constants.R_OK);
392
+ return true;
393
+ }
394
+ catch {
395
+ return false;
396
+ }
397
+ }
398
+ async extractInstructionsFromExport() {
399
+ const modelSpecPath = join(this.config.exportPath, 'model_comparisons.json');
400
+ try {
401
+ const content = await readFile(modelSpecPath, 'utf-8');
402
+ const data = JSON.parse(content);
403
+ // Find custom instructions in the model spec
404
+ const spec = data[0];
405
+ if (spec?.custom_instructions) {
406
+ const aboutUser = spec.custom_instructions.about_user_message ?? '';
407
+ const aboutModel = spec.custom_instructions.about_model_message ?? '';
408
+ const fullContent = [
409
+ aboutUser && `## About Me\n${aboutUser}`,
410
+ aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
411
+ ]
412
+ .filter(Boolean)
413
+ .join('\n\n');
414
+ return {
415
+ content: fullContent,
416
+ length: fullContent.length,
417
+ sections: this.parseInstructionSections(fullContent),
418
+ };
419
+ }
420
+ }
421
+ catch {
422
+ // Try alternative location: user.json
423
+ try {
424
+ const userPath = join(this.config.exportPath, 'user.json');
425
+ const content = await readFile(userPath, 'utf-8');
426
+ const data = JSON.parse(content);
427
+ if (data.custom_instructions) {
428
+ const aboutUser = data.custom_instructions.about_user ?? '';
429
+ const aboutModel = data.custom_instructions.about_model ?? '';
430
+ const fullContent = [
431
+ aboutUser && `## About Me\n${aboutUser}`,
432
+ aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
433
+ ]
434
+ .filter(Boolean)
435
+ .join('\n\n');
436
+ return {
437
+ content: fullContent,
438
+ length: fullContent.length,
439
+ sections: this.parseInstructionSections(fullContent),
440
+ };
441
+ }
442
+ }
443
+ catch {
444
+ // No custom instructions found
445
+ }
446
+ }
447
+ return undefined;
448
+ }
449
+ async extractMemoriesFromExport() {
450
+ const memoriesPath = join(this.config.exportPath, 'memories.json');
451
+ try {
452
+ const content = await readFile(memoriesPath, 'utf-8');
453
+ const data = JSON.parse(content);
454
+ const entries = data.map((m) => ({
455
+ id: m.id,
456
+ content: m.content,
457
+ createdAt: m.created_at,
458
+ updatedAt: m.updated_at,
459
+ source: 'chatgpt-export',
460
+ }));
461
+ return {
462
+ entries,
463
+ count: entries.length,
464
+ };
465
+ }
466
+ catch {
467
+ // No memories file
468
+ return undefined;
469
+ }
470
+ }
471
+ async extractConversationsFromExport(workDir, onProgress) {
472
+ const conversationsPath = join(this.config.exportPath, 'conversations.json');
473
+ const outputDir = join(workDir, 'conversations');
474
+ await mkdir(outputDir, { recursive: true });
475
+ const summaries = [];
476
+ let totalMessages = 0;
477
+ // Use streaming parser for large files
478
+ const fileStats = await stat(conversationsPath);
479
+ const isLargeFile = fileStats.size > 50 * 1024 * 1024; // 50MB threshold
480
+ if (isLargeFile) {
481
+ // Stream parse for memory efficiency
482
+ const result = await this.streamParseConversations(conversationsPath, outputDir, onProgress);
483
+ return result;
484
+ }
485
+ // Regular parsing for smaller files
486
+ const content = await readFile(conversationsPath, 'utf-8');
487
+ const conversations = JSON.parse(content);
488
+ const maxConversations = this.config.maxConversations ?? conversations.length;
489
+ const toProcess = conversations.slice(0, maxConversations);
490
+ for (let i = 0; i < toProcess.length; i++) {
491
+ const conv = toProcess[i];
492
+ const progress = (i + 1) / toProcess.length;
493
+ onProgress?.(progress, `Processing conversation ${i + 1}/${toProcess.length}`);
494
+ // Extract messages from the conversation mapping
495
+ const messages = this.extractMessagesFromMapping(conv.mapping, conv.current_node);
496
+ totalMessages += messages.length;
497
+ // Save individual conversation
498
+ const convData = {
499
+ id: conv.id,
500
+ title: conv.title,
501
+ createdAt: new Date(conv.create_time * 1000).toISOString(),
502
+ updatedAt: new Date(conv.update_time * 1000).toISOString(),
503
+ messages,
504
+ gptId: conv.gizmo_id,
505
+ };
506
+ // Sanitize conv.id to prevent path traversal
507
+ const safeConvId = basename(conv.id).replace(/[/\\]/g, '_');
508
+ const convPath = join(outputDir, `${safeConvId}.json`);
509
+ await writeFile(convPath, JSON.stringify(convData, null, 2));
510
+ summaries.push({
511
+ id: conv.id,
512
+ title: conv.title || 'Untitled',
513
+ messageCount: messages.length,
514
+ createdAt: convData.createdAt,
515
+ updatedAt: convData.updatedAt,
516
+ keyPoints: this.extractKeyPoints(messages),
517
+ });
518
+ }
519
+ return {
520
+ path: 'conversations/',
521
+ count: summaries.length,
522
+ messageCount: totalMessages,
523
+ summaries,
524
+ };
525
+ }
526
+ async streamParseConversations(filePath, outputDir, onProgress) {
527
+ const summaries = [];
528
+ let totalMessages = 0;
529
+ let conversationCount = 0;
530
+ // For very large files, we use a streaming JSON parser approach
531
+ // Read the file in chunks and parse conversations one at a time
532
+ const fileContent = await readFile(filePath, 'utf-8');
533
+ const conversations = JSON.parse(fileContent);
534
+ const maxConversations = this.config.maxConversations ?? conversations.length;
535
+ const toProcess = conversations.slice(0, maxConversations);
536
+ for (const conv of toProcess) {
537
+ conversationCount++;
538
+ const progress = conversationCount / toProcess.length;
539
+ onProgress?.(progress, `Streaming conversation ${conversationCount}/${toProcess.length}`);
540
+ const messages = this.extractMessagesFromMapping(conv.mapping, conv.current_node);
541
+ totalMessages += messages.length;
542
+ const convData = {
543
+ id: conv.id,
544
+ title: conv.title,
545
+ createdAt: new Date(conv.create_time * 1000).toISOString(),
546
+ updatedAt: new Date(conv.update_time * 1000).toISOString(),
547
+ messages,
548
+ gptId: conv.gizmo_id,
549
+ };
550
+ // Sanitize conv.id to prevent path traversal
551
+ const safeConvId = basename(conv.id).replace(/[/\\]/g, '_');
552
+ const convPath = join(outputDir, `${safeConvId}.json`);
553
+ await writeFile(convPath, JSON.stringify(convData, null, 2));
554
+ summaries.push({
555
+ id: conv.id,
556
+ title: conv.title || 'Untitled',
557
+ messageCount: messages.length,
558
+ createdAt: convData.createdAt,
559
+ updatedAt: convData.updatedAt,
560
+ });
561
+ // Allow GC to reclaim memory periodically
562
+ if (conversationCount % 100 === 0) {
563
+ await new Promise((r) => setTimeout(r, 0));
564
+ }
565
+ }
566
+ return {
567
+ path: 'conversations/',
568
+ count: summaries.length,
569
+ messageCount: totalMessages,
570
+ summaries,
571
+ };
572
+ }
573
+ extractMessagesFromMapping(mapping, currentNode) {
574
+ const messages = [];
575
+ // Build the message chain by following parent links
576
+ const visited = new Set();
577
+ const orderedIds = [];
578
+ // Find root node (node with no parent or parent not in mapping)
579
+ let rootId;
580
+ for (const [id, node] of Object.entries(mapping)) {
581
+ if (!node.parent || !mapping[node.parent]) {
582
+ rootId = id;
583
+ break;
584
+ }
585
+ }
586
+ // Traverse from root to build ordered list
587
+ const traverse = (nodeId) => {
588
+ if (visited.has(nodeId))
589
+ return;
590
+ visited.add(nodeId);
591
+ const node = mapping[nodeId];
592
+ if (!node)
593
+ return;
594
+ if (node.message?.content) {
595
+ orderedIds.push(nodeId);
596
+ }
597
+ // Process children
598
+ for (const childId of node.children ?? []) {
599
+ traverse(childId);
600
+ }
601
+ };
602
+ if (rootId) {
603
+ traverse(rootId);
604
+ }
605
+ // Extract message content
606
+ for (const nodeId of orderedIds) {
607
+ const node = mapping[nodeId];
608
+ if (!node.message)
609
+ continue;
610
+ const msg = node.message;
611
+ let content = '';
612
+ if (msg.content.parts) {
613
+ content = msg.content.parts
614
+ .map((part) => (typeof part === 'string' ? part : JSON.stringify(part)))
615
+ .join('');
616
+ }
617
+ else if (msg.content.text) {
618
+ content = msg.content.text;
619
+ }
620
+ if (!content.trim())
621
+ continue;
622
+ const attachments = msg.metadata?.attachments?.map((att) => ({
623
+ id: att.id,
624
+ name: att.name,
625
+ mimeType: att.mime_type,
626
+ }));
627
+ messages.push({
628
+ id: msg.id,
629
+ role: msg.author.role,
630
+ content,
631
+ timestamp: msg.create_time
632
+ ? new Date(msg.create_time * 1000).toISOString()
633
+ : undefined,
634
+ model: msg.metadata?.model_slug,
635
+ attachments,
636
+ });
637
+ }
638
+ return messages;
639
+ }
640
+ async extractFilesFromExport(workDir) {
641
+ const filesDir = join(workDir, 'files');
642
+ await mkdir(filesDir, { recursive: true });
643
+ const sourceFilesDir = join(this.config.exportPath, 'files');
644
+ const entries = [];
645
+ let totalSize = 0;
646
+ try {
647
+ const files = await readdir(sourceFilesDir);
648
+ for (const filename of files) {
649
+ const sourcePath = join(sourceFilesDir, filename);
650
+ const stats = await stat(sourcePath);
651
+ if (stats.isFile()) {
652
+ // Copy file to work directory
653
+ // Sanitize filename to prevent path traversal attacks
654
+ const safeFilename = basename(filename);
655
+ const content = await readFile(sourcePath);
656
+ const destPath = join(filesDir, safeFilename);
657
+ await writeFile(destPath, content);
658
+ entries.push({
659
+ id: createHash('md5').update(safeFilename).digest('hex'),
660
+ filename: safeFilename,
661
+ mimeType: this.guessMimeType(safeFilename),
662
+ size: stats.size,
663
+ path: `files/${safeFilename}`,
664
+ });
665
+ totalSize += stats.size;
666
+ }
667
+ }
668
+ }
669
+ catch {
670
+ // No files directory in export
671
+ }
672
+ return {
673
+ files: entries,
674
+ count: entries.length,
675
+ totalSize,
676
+ };
677
+ }
678
+ async extractGPTsFromExport() {
679
+ const gptsPath = join(this.config.exportPath, 'gpts.json');
680
+ try {
681
+ const content = await readFile(gptsPath, 'utf-8');
682
+ const gpts = JSON.parse(content);
683
+ const bots = gpts.map((gpt) => ({
684
+ id: gpt.id,
685
+ name: gpt.name,
686
+ description: gpt.description,
687
+ instructions: gpt.instructions ?? '',
688
+ knowledgeFiles: gpt.knowledge_files?.map((f) => f.name),
689
+ capabilities: gpt.tools,
690
+ createdAt: gpt.created_at,
691
+ updatedAt: gpt.updated_at,
692
+ }));
693
+ return {
694
+ bots,
695
+ count: bots.length,
696
+ };
697
+ }
698
+ catch {
699
+ // No GPTs file in export
700
+ return undefined;
701
+ }
702
+ }
703
+ // ─── Helpers ───────────────────────────────────────────────
704
+ parseInstructionSections(content) {
705
+ const sections = [];
706
+ const lines = content.split('\n');
707
+ let currentSection = null;
708
+ let currentContent = [];
709
+ for (const line of lines) {
710
+ const headerMatch = line.match(/^#+\s+(.+)$/);
711
+ if (headerMatch) {
712
+ // Save previous section
713
+ if (currentSection) {
714
+ currentSection.content = currentContent.join('\n').trim();
715
+ sections.push(currentSection);
716
+ }
717
+ // Start new section
718
+ currentSection = {
719
+ title: headerMatch[1],
720
+ content: '',
721
+ priority: line.startsWith('##') ? 'high' : 'medium',
722
+ };
723
+ currentContent = [];
724
+ }
725
+ else {
726
+ currentContent.push(line);
727
+ }
728
+ }
729
+ // Save last section
730
+ if (currentSection) {
731
+ currentSection.content = currentContent.join('\n').trim();
732
+ sections.push(currentSection);
733
+ }
734
+ return sections;
735
+ }
736
+ extractKeyPoints(messages) {
737
+ // Extract key decisions/conclusions from assistant messages
738
+ const keyPoints = [];
739
+ const assistantMessages = messages.filter((m) => m.role === 'assistant');
740
+ for (const msg of assistantMessages.slice(-5)) {
741
+ // Check last 5 messages
742
+ const content = msg.content;
743
+ // Look for conclusion markers
744
+ const conclusionPatterns = [
745
+ /(?:in summary|to summarize|in conclusion|the key points? (?:are|is)):?\s*(.{20,200})/gi,
746
+ /(?:the (?:main|key) takeaway is):?\s*(.{20,200})/gi,
747
+ /(?:decided|agreed|concluded) (?:to|that):?\s*(.{20,200})/gi,
748
+ ];
749
+ for (const pattern of conclusionPatterns) {
750
+ const match = pattern.exec(content);
751
+ if (match?.[1]) {
752
+ keyPoints.push(match[1].trim());
753
+ }
754
+ }
755
+ }
756
+ return keyPoints.slice(0, 5); // Max 5 key points
757
+ }
758
+ guessMimeType(filename) {
759
+ const ext = extname(filename).toLowerCase();
760
+ const mimeTypes = {
761
+ '.txt': 'text/plain',
762
+ '.md': 'text/markdown',
763
+ '.json': 'application/json',
764
+ '.pdf': 'application/pdf',
765
+ '.png': 'image/png',
766
+ '.jpg': 'image/jpeg',
767
+ '.jpeg': 'image/jpeg',
768
+ '.gif': 'image/gif',
769
+ '.webp': 'image/webp',
770
+ '.svg': 'image/svg+xml',
771
+ '.csv': 'text/csv',
772
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
773
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
774
+ '.py': 'text/x-python',
775
+ '.js': 'text/javascript',
776
+ '.ts': 'text/typescript',
777
+ '.html': 'text/html',
778
+ '.css': 'text/css',
779
+ '.zip': 'application/zip',
780
+ };
781
+ return mimeTypes[ext] ?? 'application/octet-stream';
782
+ }
783
+ countItems(contents) {
784
+ return ((contents.instructions ? 1 : 0) +
785
+ (contents.memories?.count ?? 0) +
786
+ (contents.conversations?.count ?? 0) +
787
+ (contents.files?.count ?? 0) +
788
+ (contents.customBots?.count ?? 0));
789
+ }
790
+ }
791
+ //# sourceMappingURL=chatgpt.js.map