@savestate/cli 0.1.1 → 0.2.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.
@@ -0,0 +1,668 @@
1
+ /**
2
+ * ChatGPT Adapter
3
+ *
4
+ * Export-based adapter for ChatGPT data.
5
+ * Captures conversations, memories, custom instructions, and user profile
6
+ * from a ChatGPT data export (Settings → Data Controls → Export Data).
7
+ *
8
+ * Export structure:
9
+ * conversations.json — All conversations (tree-structured mapping)
10
+ * memories.json — ChatGPT memories (newer exports)
11
+ * custom_instructions.json — About user + response preferences
12
+ * user.json — User profile metadata
13
+ * model_comparisons.json — A/B testing data
14
+ * shared_conversations.json — Publicly shared conversations
15
+ * message_feedback.json — Thumbs up/down on messages
16
+ * chat.html — HTML render of conversations
17
+ *
18
+ * Detection:
19
+ * - SAVESTATE_CHATGPT_EXPORT env var → path to export directory or zip
20
+ * - chatgpt-export/ or conversations.json in cwd
21
+ * - .savestate/imports/chatgpt/ extracted export
22
+ *
23
+ * Restore is partial — ChatGPT has no public API for importing
24
+ * conversations. Generates chatgpt-restore-guide.md with data
25
+ * for manual re-entry.
26
+ */
27
+ import { readFile, writeFile } from 'node:fs/promises';
28
+ import { existsSync } from 'node:fs';
29
+ import { join, basename } from 'node:path';
30
+ import { createHash } from 'node:crypto';
31
+ import { SAF_VERSION, generateSnapshotId } from '../format.js';
32
+ // ─── Constants ───────────────────────────────────────────────
33
+ const PROGRESS_INTERVAL = 500; // Log progress every N conversations
34
+ // ─── Adapter ─────────────────────────────────────────────────
35
+ export class ChatGPTAdapter {
36
+ id = 'chatgpt';
37
+ name = 'ChatGPT';
38
+ platform = 'chatgpt';
39
+ version = '0.1.0';
40
+ exportDir = '';
41
+ warnings = [];
42
+ async detect() {
43
+ const resolved = this.resolveExportPath();
44
+ return resolved !== null;
45
+ }
46
+ async extract() {
47
+ this.warnings = [];
48
+ const exportPath = this.resolveExportPath();
49
+ if (!exportPath) {
50
+ throw new Error('ChatGPT export not found.\n' +
51
+ 'Provide the path via:\n' +
52
+ ' • SAVESTATE_CHATGPT_EXPORT env var (path to export directory or zip)\n' +
53
+ ' • Place chatgpt-export/ or conversations.json in the current directory\n' +
54
+ ' • Extract export to .savestate/imports/chatgpt/\n\n' +
55
+ 'Export your data from ChatGPT: Settings → Data Controls → Export Data');
56
+ }
57
+ this.exportDir = exportPath;
58
+ this.log(`Using ChatGPT export from: ${this.exportDir}`);
59
+ // 1. Parse user profile
60
+ this.log('Reading user profile...');
61
+ const userInfo = await this.readUserJson();
62
+ // 2. Parse custom instructions → identity.personality
63
+ this.log('Reading custom instructions...');
64
+ const { personality, instructionCount } = await this.readCustomInstructions();
65
+ // 3. Parse memories → memory.core
66
+ this.log('Reading memories...');
67
+ const memories = await this.readMemories();
68
+ // 4. Parse conversations → conversations + conversation details
69
+ this.log('Reading conversations...');
70
+ const { conversationMetas, conversationEntries } = await this.readConversations();
71
+ // Summary
72
+ this.log(`Found ${conversationMetas.length} conversations, ` +
73
+ `${memories.length} memories, ` +
74
+ `${instructionCount} custom instructions`);
75
+ // Build snapshot
76
+ const snapshotId = generateSnapshotId();
77
+ const now = new Date().toISOString();
78
+ // Build config from user info
79
+ const config = {};
80
+ if (userInfo) {
81
+ config.user = userInfo;
82
+ }
83
+ // Store conversation data as memory entries so they survive the snapshot format
84
+ // (the conversations index only has metadata, not full messages)
85
+ const allMemory = [...memories, ...conversationEntries];
86
+ if (this.warnings.length > 0) {
87
+ for (const w of this.warnings) {
88
+ console.warn(` ⚠ ${w}`);
89
+ }
90
+ }
91
+ const snapshot = {
92
+ manifest: {
93
+ version: SAF_VERSION,
94
+ timestamp: now,
95
+ id: snapshotId,
96
+ platform: this.platform,
97
+ adapter: this.id,
98
+ checksum: '',
99
+ size: 0,
100
+ },
101
+ identity: {
102
+ personality: personality || undefined,
103
+ config: Object.keys(config).length > 0 ? config : undefined,
104
+ tools: [],
105
+ },
106
+ memory: {
107
+ core: allMemory,
108
+ knowledge: [],
109
+ },
110
+ conversations: {
111
+ total: conversationMetas.length,
112
+ conversations: conversationMetas,
113
+ },
114
+ platform: await this.identify(),
115
+ chain: {
116
+ current: snapshotId,
117
+ ancestors: [],
118
+ },
119
+ restoreHints: {
120
+ platform: this.platform,
121
+ steps: [
122
+ {
123
+ type: 'file',
124
+ description: 'Generate restore guide with memories and custom instructions',
125
+ target: 'chatgpt-restore-guide.md',
126
+ },
127
+ {
128
+ type: 'manual',
129
+ description: 'Re-enter custom instructions in ChatGPT settings',
130
+ target: 'Settings → Personalization → Custom Instructions',
131
+ },
132
+ {
133
+ type: 'manual',
134
+ description: 'Review and re-add memories via ChatGPT memory settings',
135
+ target: 'Settings → Personalization → Memory',
136
+ },
137
+ ],
138
+ manualSteps: [
139
+ 'ChatGPT does not support importing conversations via API',
140
+ 'Custom instructions must be manually re-entered in Settings → Personalization',
141
+ 'Memories can be viewed but must be re-added manually or through conversation',
142
+ 'Conversation history cannot be restored — it is preserved in the snapshot for reference',
143
+ ],
144
+ },
145
+ };
146
+ this.log(`✓ Extraction complete`);
147
+ return snapshot;
148
+ }
149
+ async restore(snapshot) {
150
+ this.warnings = [];
151
+ const outputDir = process.cwd();
152
+ const guidePath = join(outputDir, 'chatgpt-restore-guide.md');
153
+ this.log('ChatGPT restore is partial — generating restore guide...');
154
+ const guide = this.buildRestoreGuide(snapshot);
155
+ await writeFile(guidePath, guide, 'utf-8');
156
+ // Also write memories to a separate file for easy reference
157
+ const memoriesFromCore = snapshot.memory.core.filter(m => m.source === 'chatgpt-memory');
158
+ if (memoriesFromCore.length > 0) {
159
+ const memoriesPath = join(outputDir, 'chatgpt-memories.md');
160
+ const memoriesDoc = this.buildMemoriesDoc(memoriesFromCore);
161
+ await writeFile(memoriesPath, memoriesDoc, 'utf-8');
162
+ this.log(` Wrote ${memoriesFromCore.length} memories to chatgpt-memories.md`);
163
+ }
164
+ // Write custom instructions if present
165
+ if (snapshot.identity.personality) {
166
+ const instructionsPath = join(outputDir, 'chatgpt-custom-instructions.md');
167
+ const instructionsDoc = this.buildInstructionsDoc(snapshot.identity.personality);
168
+ await writeFile(instructionsPath, instructionsDoc, 'utf-8');
169
+ this.log(' Wrote custom instructions to chatgpt-custom-instructions.md');
170
+ }
171
+ console.error('');
172
+ console.error('┌─────────────────────────────────────────────────────┐');
173
+ console.error('│ ⚠ ChatGPT restore is partial │');
174
+ console.error('├─────────────────────────────────────────────────────┤');
175
+ console.error('│ See chatgpt-restore-guide.md for instructions. │');
176
+ console.error('│ Conversations are preserved but cannot be imported. │');
177
+ console.error('│ Memories and custom instructions require manual │');
178
+ console.error('│ re-entry in ChatGPT settings. │');
179
+ console.error('└─────────────────────────────────────────────────────┘');
180
+ if (this.warnings.length > 0) {
181
+ console.error('');
182
+ for (const w of this.warnings) {
183
+ console.warn(` ⚠ ${w}`);
184
+ }
185
+ }
186
+ }
187
+ async identify() {
188
+ return {
189
+ name: 'ChatGPT',
190
+ version: this.version,
191
+ exportMethod: 'data-export',
192
+ };
193
+ }
194
+ // ─── Private: Export Path Resolution ───────────────────────
195
+ /**
196
+ * Resolve the export directory path from env var, cwd, or .savestate/imports/.
197
+ * Returns null if no valid export is found.
198
+ */
199
+ resolveExportPath() {
200
+ // 1. SAVESTATE_CHATGPT_EXPORT env var
201
+ const envPath = process.env.SAVESTATE_CHATGPT_EXPORT;
202
+ if (envPath) {
203
+ // If it ends with .zip, we don't support direct zip reading (yet).
204
+ // Require pre-extracted.
205
+ if (envPath.endsWith('.zip')) {
206
+ // Check if there's an extracted directory next to the zip
207
+ const extractedDir = envPath.replace(/\.zip$/, '');
208
+ if (existsSync(extractedDir) && this.hasExportFiles(extractedDir)) {
209
+ return extractedDir;
210
+ }
211
+ // Check if the zip name suggests a directory exists
212
+ const parentDir = join(extractedDir, '..');
213
+ const possibleDirs = ['chatgpt-export', basename(extractedDir)];
214
+ for (const dir of possibleDirs) {
215
+ const candidate = join(parentDir, dir);
216
+ if (existsSync(candidate) && this.hasExportFiles(candidate)) {
217
+ return candidate;
218
+ }
219
+ }
220
+ return null; // Zip not yet extracted
221
+ }
222
+ if (existsSync(envPath) && this.hasExportFiles(envPath)) {
223
+ return envPath;
224
+ }
225
+ return null;
226
+ }
227
+ // 2. chatgpt-export/ in current directory
228
+ const chatgptExportDir = join(process.cwd(), 'chatgpt-export');
229
+ if (existsSync(chatgptExportDir) && this.hasExportFiles(chatgptExportDir)) {
230
+ return chatgptExportDir;
231
+ }
232
+ // 3. conversations.json directly in current directory
233
+ const conversationsJson = join(process.cwd(), 'conversations.json');
234
+ if (existsSync(conversationsJson)) {
235
+ return process.cwd();
236
+ }
237
+ // 4. .savestate/imports/chatgpt/
238
+ const savestateImports = join(process.cwd(), '.savestate', 'imports', 'chatgpt');
239
+ if (existsSync(savestateImports) && this.hasExportFiles(savestateImports)) {
240
+ return savestateImports;
241
+ }
242
+ return null;
243
+ }
244
+ /**
245
+ * Check if a directory contains ChatGPT export files.
246
+ */
247
+ hasExportFiles(dir) {
248
+ // Must have at least conversations.json
249
+ return existsSync(join(dir, 'conversations.json'));
250
+ }
251
+ // ─── Private: File Parsers ─────────────────────────────────
252
+ /**
253
+ * Safely read and parse a JSON file from the export directory.
254
+ */
255
+ async readJsonFile(filename) {
256
+ const filePath = join(this.exportDir, filename);
257
+ if (!existsSync(filePath))
258
+ return null;
259
+ try {
260
+ const content = await readFile(filePath, 'utf-8');
261
+ return JSON.parse(content);
262
+ }
263
+ catch (err) {
264
+ this.warn(`Failed to parse ${filename}: ${err instanceof Error ? err.message : String(err)}`);
265
+ return null;
266
+ }
267
+ }
268
+ /**
269
+ * Read user.json for profile metadata.
270
+ */
271
+ async readUserJson() {
272
+ const data = await this.readJsonFile('user.json');
273
+ if (!data)
274
+ return null;
275
+ // Normalize nested structure
276
+ const user = data.user ?? data;
277
+ return {
278
+ id: user.id ?? data.id,
279
+ email: user.email ?? data.email,
280
+ name: user.name ?? data.name,
281
+ chatgpt_plus_user: data.chatgpt_plus_user,
282
+ };
283
+ }
284
+ /**
285
+ * Read custom_instructions.json and build personality string.
286
+ */
287
+ async readCustomInstructions() {
288
+ const data = await this.readJsonFile('custom_instructions.json');
289
+ if (!data)
290
+ return { personality: '', instructionCount: 0 };
291
+ const parts = [];
292
+ let count = 0;
293
+ // Handle both old and new format keys
294
+ const aboutUser = data.about_user_message ?? data.about_user;
295
+ const responsePrefs = data.about_model_message ?? data.response_preferences;
296
+ if (aboutUser) {
297
+ parts.push(`--- About User (Custom Instructions) ---\n${aboutUser}`);
298
+ count++;
299
+ }
300
+ if (responsePrefs) {
301
+ parts.push(`--- Response Preferences (Custom Instructions) ---\n${responsePrefs}`);
302
+ count++;
303
+ }
304
+ return {
305
+ personality: parts.join('\n\n'),
306
+ instructionCount: count,
307
+ };
308
+ }
309
+ /**
310
+ * Read memories.json into MemoryEntry array.
311
+ */
312
+ async readMemories() {
313
+ const data = await this.readJsonFile('memories.json');
314
+ if (!data || !Array.isArray(data))
315
+ return [];
316
+ return data.map(mem => {
317
+ const createdAt = mem.created_at
318
+ ?? (mem.create_time ? new Date(mem.create_time * 1000).toISOString() : new Date().toISOString());
319
+ const updatedAt = mem.updated_at
320
+ ?? (mem.update_time ? new Date(mem.update_time * 1000).toISOString() : undefined);
321
+ return {
322
+ id: `chatgpt-memory:${mem.id}`,
323
+ content: mem.content,
324
+ source: 'chatgpt-memory',
325
+ createdAt,
326
+ updatedAt,
327
+ metadata: { platform: 'chatgpt' },
328
+ };
329
+ });
330
+ }
331
+ /**
332
+ * Read conversations.json, walk tree structures, and produce metadata + entries.
333
+ */
334
+ async readConversations() {
335
+ const data = await this.readJsonFile('conversations.json');
336
+ if (!data || !Array.isArray(data))
337
+ return { conversationMetas: [], conversationEntries: [] };
338
+ const conversationMetas = [];
339
+ const conversationEntries = [];
340
+ const total = data.length;
341
+ const isLarge = total >= 1000;
342
+ if (isLarge) {
343
+ this.log(` Processing ${total} conversations (this may take a moment)...`);
344
+ }
345
+ for (let i = 0; i < data.length; i++) {
346
+ const conv = data[i];
347
+ // Progress logging for large exports
348
+ if (isLarge && i > 0 && i % PROGRESS_INTERVAL === 0) {
349
+ this.log(` Progress: ${i}/${total} conversations processed...`);
350
+ }
351
+ try {
352
+ const messages = this.walkConversationTree(conv);
353
+ const convId = conv.conversation_id ?? conv.id ?? this.hashString(conv.title + conv.create_time);
354
+ const createdAt = conv.create_time
355
+ ? new Date(conv.create_time * 1000).toISOString()
356
+ : new Date().toISOString();
357
+ const updatedAt = conv.update_time
358
+ ? new Date(conv.update_time * 1000).toISOString()
359
+ : createdAt;
360
+ conversationMetas.push({
361
+ id: convId,
362
+ title: conv.title || undefined,
363
+ createdAt,
364
+ updatedAt,
365
+ messageCount: messages.length,
366
+ path: `conversations/${convId}.json`,
367
+ });
368
+ // Store conversation messages as a memory entry for snapshot persistence
369
+ const conversation = {
370
+ id: convId,
371
+ title: conv.title || undefined,
372
+ createdAt,
373
+ updatedAt,
374
+ messages,
375
+ metadata: {
376
+ default_model: conv.default_model_slug,
377
+ is_archived: conv.is_archived,
378
+ },
379
+ };
380
+ conversationEntries.push({
381
+ id: `conversation:${convId}`,
382
+ content: JSON.stringify(conversation),
383
+ source: 'chatgpt-conversation',
384
+ createdAt,
385
+ updatedAt,
386
+ metadata: {
387
+ title: conv.title,
388
+ messageCount: messages.length,
389
+ model: conv.default_model_slug,
390
+ },
391
+ });
392
+ }
393
+ catch (err) {
394
+ this.warn(`Failed to parse conversation "${conv.title ?? 'unknown'}": ` +
395
+ `${err instanceof Error ? err.message : String(err)}`);
396
+ }
397
+ }
398
+ if (isLarge) {
399
+ this.log(` Processed all ${total} conversations.`);
400
+ }
401
+ return { conversationMetas, conversationEntries };
402
+ }
403
+ // ─── Private: Tree Walking ─────────────────────────────────
404
+ /**
405
+ * Walk the conversation tree (mapping) and produce a linear list of messages.
406
+ *
407
+ * Algorithm:
408
+ * 1. Find root node (parent is null or missing)
409
+ * 2. Walk children depth-first
410
+ * 3. When a node has multiple children (branching = user edited), take the last branch
411
+ * 4. Skip null/system messages without content
412
+ * 5. Collect messages in order
413
+ */
414
+ walkConversationTree(conv) {
415
+ const mapping = conv.mapping;
416
+ if (!mapping || Object.keys(mapping).length === 0)
417
+ return [];
418
+ // Find root node(s) — node with no parent or parent not in mapping
419
+ const rootNodes = Object.values(mapping).filter(node => !node.parent || !mapping[node.parent]);
420
+ if (rootNodes.length === 0)
421
+ return [];
422
+ // Start from the first root (there should usually be exactly one)
423
+ const root = rootNodes[0];
424
+ const messages = [];
425
+ this.collectMessages(root, mapping, messages);
426
+ return messages;
427
+ }
428
+ /**
429
+ * Recursively collect messages by walking the tree depth-first.
430
+ * For branches (multiple children), take the last child (most recent edit/branch).
431
+ */
432
+ collectMessages(node, mapping, messages) {
433
+ // Extract message from this node if it exists and has content
434
+ if (node.message) {
435
+ const msg = this.extractMessage(node.message);
436
+ if (msg) {
437
+ messages.push(msg);
438
+ }
439
+ }
440
+ // Walk children — take the last child when branching
441
+ if (node.children.length > 0) {
442
+ // Last child = most recent branch (user edited and continued from latest)
443
+ const nextChildId = node.children[node.children.length - 1];
444
+ const nextChild = mapping[nextChildId];
445
+ if (nextChild) {
446
+ this.collectMessages(nextChild, mapping, messages);
447
+ }
448
+ }
449
+ }
450
+ /**
451
+ * Extract a Message from a ChatGPT message node.
452
+ * Returns null if the message should be skipped (system, empty, etc.)
453
+ */
454
+ extractMessage(msg) {
455
+ const role = msg.author.role;
456
+ // Skip system messages and empty messages
457
+ if (role === 'system')
458
+ return null;
459
+ // Extract text content
460
+ const text = this.extractContent(msg.content);
461
+ if (!text)
462
+ return null;
463
+ // Map roles: user, assistant, tool
464
+ let mappedRole;
465
+ if (role === 'user') {
466
+ mappedRole = 'user';
467
+ }
468
+ else if (role === 'assistant') {
469
+ mappedRole = 'assistant';
470
+ }
471
+ else if (role === 'tool') {
472
+ mappedRole = 'tool';
473
+ }
474
+ else {
475
+ // Unknown role — treat as system and skip
476
+ return null;
477
+ }
478
+ const timestamp = msg.create_time
479
+ ? new Date(msg.create_time * 1000).toISOString()
480
+ : new Date().toISOString();
481
+ return {
482
+ id: msg.id,
483
+ role: mappedRole,
484
+ content: text,
485
+ timestamp,
486
+ metadata: {
487
+ ...(msg.metadata?.model_slug ? { model: msg.metadata.model_slug } : {}),
488
+ ...(msg.author.name ? { author_name: msg.author.name } : {}),
489
+ ...(msg.recipient && msg.recipient !== 'all' ? { recipient: msg.recipient } : {}),
490
+ ...(msg.status ? { status: msg.status } : {}),
491
+ },
492
+ };
493
+ }
494
+ /**
495
+ * Extract text from ChatGPT content structure.
496
+ * Handles content_type: "text", "code", "tether_browsing_display", etc.
497
+ */
498
+ extractContent(content) {
499
+ if (!content)
500
+ return null;
501
+ // Direct text field
502
+ if (content.text)
503
+ return content.text;
504
+ // Result field (for tool outputs)
505
+ if (content.result)
506
+ return content.result;
507
+ // Parts array
508
+ if (content.parts && Array.isArray(content.parts)) {
509
+ const textParts = [];
510
+ for (const part of content.parts) {
511
+ if (typeof part === 'string') {
512
+ if (part.trim())
513
+ textParts.push(part);
514
+ }
515
+ else if (part && typeof part === 'object') {
516
+ // Image, file, or other structured content
517
+ if ('text' in part && typeof part.text === 'string') {
518
+ textParts.push(part.text);
519
+ }
520
+ else if ('asset_pointer' in part) {
521
+ textParts.push('[Image]');
522
+ }
523
+ else if ('content_type' in part && part.content_type === 'image_asset_pointer') {
524
+ textParts.push('[Image]');
525
+ }
526
+ }
527
+ }
528
+ const joined = textParts.join('\n');
529
+ return joined || null;
530
+ }
531
+ return null;
532
+ }
533
+ // ─── Private: Restore Guide Generation ─────────────────────
534
+ /**
535
+ * Build the chatgpt-restore-guide.md content.
536
+ */
537
+ buildRestoreGuide(snapshot) {
538
+ const lines = [];
539
+ lines.push('# ChatGPT Restore Guide');
540
+ lines.push('');
541
+ lines.push(`Generated by SaveState on ${new Date().toISOString()}`);
542
+ lines.push(`Snapshot ID: ${snapshot.manifest.id}`);
543
+ lines.push('');
544
+ lines.push('---');
545
+ lines.push('');
546
+ lines.push('## ⚠️ Important');
547
+ lines.push('');
548
+ lines.push('ChatGPT does not support importing conversations or memories via API.');
549
+ lines.push('This guide contains your data for manual re-entry.');
550
+ lines.push('');
551
+ // Custom Instructions
552
+ if (snapshot.identity.personality) {
553
+ lines.push('## Custom Instructions');
554
+ lines.push('');
555
+ lines.push('Go to **ChatGPT → Settings → Personalization → Custom Instructions**');
556
+ lines.push('and copy the following:');
557
+ lines.push('');
558
+ lines.push('```');
559
+ lines.push(snapshot.identity.personality);
560
+ lines.push('```');
561
+ lines.push('');
562
+ }
563
+ // Memories
564
+ const memories = snapshot.memory.core.filter(m => m.source === 'chatgpt-memory');
565
+ if (memories.length > 0) {
566
+ lines.push('## Memories');
567
+ lines.push('');
568
+ lines.push(`You had ${memories.length} memories. To re-create them, you can:`);
569
+ lines.push('1. Mention these facts in conversations and ChatGPT will learn them');
570
+ lines.push('2. Go to **Settings → Personalization → Memory → Manage** to review');
571
+ lines.push('');
572
+ for (const mem of memories) {
573
+ lines.push(`- ${mem.content}`);
574
+ }
575
+ lines.push('');
576
+ }
577
+ // Conversation summary
578
+ if (snapshot.conversations.total > 0) {
579
+ lines.push('## Conversations');
580
+ lines.push('');
581
+ lines.push(`Your export contained ${snapshot.conversations.total} conversations.`);
582
+ lines.push('These are preserved in the snapshot but cannot be imported into ChatGPT.');
583
+ lines.push('Use `savestate inspect` to browse them.');
584
+ lines.push('');
585
+ // Show a few recent ones
586
+ const recent = snapshot.conversations.conversations
587
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
588
+ .slice(0, 20);
589
+ if (recent.length > 0) {
590
+ lines.push('### Recent Conversations');
591
+ lines.push('');
592
+ lines.push('| Date | Title | Messages |');
593
+ lines.push('|------|-------|----------|');
594
+ for (const conv of recent) {
595
+ const date = conv.updatedAt.slice(0, 10);
596
+ const title = conv.title ?? '(untitled)';
597
+ lines.push(`| ${date} | ${title} | ${conv.messageCount} |`);
598
+ }
599
+ lines.push('');
600
+ }
601
+ }
602
+ // Platform info
603
+ const config = snapshot.identity.config;
604
+ if (config?.user) {
605
+ const user = config.user;
606
+ lines.push('## Account Info');
607
+ lines.push('');
608
+ if (user.email)
609
+ lines.push(`- Email: ${user.email}`);
610
+ if (user.name)
611
+ lines.push(`- Name: ${user.name}`);
612
+ if (user.chatgpt_plus_user)
613
+ lines.push('- Plan: ChatGPT Plus');
614
+ lines.push('');
615
+ }
616
+ return lines.join('\n');
617
+ }
618
+ /**
619
+ * Build a standalone memories document.
620
+ */
621
+ buildMemoriesDoc(memories) {
622
+ const lines = [];
623
+ lines.push('# ChatGPT Memories');
624
+ lines.push('');
625
+ lines.push(`Exported ${memories.length} memories.`);
626
+ lines.push('');
627
+ lines.push('To restore these, mention them in conversations with ChatGPT');
628
+ lines.push('or manually add them in Settings → Personalization → Memory.');
629
+ lines.push('');
630
+ lines.push('---');
631
+ lines.push('');
632
+ for (const mem of memories) {
633
+ const date = mem.createdAt.slice(0, 10);
634
+ lines.push(`- **[${date}]** ${mem.content}`);
635
+ }
636
+ lines.push('');
637
+ return lines.join('\n');
638
+ }
639
+ /**
640
+ * Build a standalone custom instructions document.
641
+ */
642
+ buildInstructionsDoc(personality) {
643
+ const lines = [];
644
+ lines.push('# ChatGPT Custom Instructions');
645
+ lines.push('');
646
+ lines.push('Copy these into ChatGPT → Settings → Personalization → Custom Instructions.');
647
+ lines.push('');
648
+ lines.push('---');
649
+ lines.push('');
650
+ lines.push(personality);
651
+ lines.push('');
652
+ return lines.join('\n');
653
+ }
654
+ // ─── Private: Utilities ────────────────────────────────────
655
+ log(msg) {
656
+ console.error(msg);
657
+ }
658
+ warn(msg) {
659
+ this.warnings.push(msg);
660
+ }
661
+ /**
662
+ * Simple string hash for generating IDs when conversation_id is missing.
663
+ */
664
+ hashString(input) {
665
+ return createHash('sha256').update(input).digest('hex').slice(0, 16);
666
+ }
667
+ }
668
+ //# sourceMappingURL=chatgpt.js.map