ai-exodus 2.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.
@@ -0,0 +1,822 @@
1
+ /**
2
+ * Output package generator
3
+ * Takes analysis results and writes the migration package to disk
4
+ */
5
+
6
+ import { mkdir, writeFile } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+
9
+ /**
10
+ * Generate the complete migration package
11
+ */
12
+ export async function generate(analysis, options) {
13
+ const { outputDir, hearthline, letta, aiName, userName } = options;
14
+
15
+ // Create output directories
16
+ await mkdir(outputDir, { recursive: true });
17
+ await mkdir(join(outputDir, 'memory'), { recursive: true });
18
+ await mkdir(join(outputDir, 'skills'), { recursive: true });
19
+ if (hearthline) {
20
+ await mkdir(join(outputDir, 'hearthline'), { recursive: true });
21
+ await mkdir(join(outputDir, 'hearthline', 'memory'), { recursive: true });
22
+ }
23
+
24
+ // Write all files in parallel
25
+ const writes = [];
26
+
27
+ // 1. Persona definition
28
+ writes.push(writeFile(
29
+ join(outputDir, 'persona.md'),
30
+ analysis.persona,
31
+ 'utf-8'
32
+ ));
33
+
34
+ // 2. Custom instructions (short, for Claude.ai)
35
+ writes.push(writeFile(
36
+ join(outputDir, 'custom-instructions.txt'),
37
+ generateCustomInstructions(analysis.customInstructions, aiName, userName),
38
+ 'utf-8'
39
+ ));
40
+
41
+ // 3. CLAUDE.md (ready-to-use system prompt)
42
+ writes.push(writeFile(
43
+ join(outputDir, 'claude.md'),
44
+ generateClaudeMd(analysis, aiName, userName),
45
+ 'utf-8'
46
+ ));
47
+
48
+ // 3. Memory files
49
+ writes.push(writeFile(
50
+ join(outputDir, 'memory', 'about-user.md'),
51
+ generateUserMemory(analysis.memory, userName),
52
+ 'utf-8'
53
+ ));
54
+
55
+ writes.push(writeFile(
56
+ join(outputDir, 'memory', 'relationship.md'),
57
+ generateRelationshipMemory(analysis.memory, aiName, userName),
58
+ 'utf-8'
59
+ ));
60
+
61
+ writes.push(writeFile(
62
+ join(outputDir, 'memory', 'emotional.md'),
63
+ generateEmotionalMemory(analysis.memory, userName),
64
+ 'utf-8'
65
+ ));
66
+
67
+ writes.push(writeFile(
68
+ join(outputDir, 'memory', 'preferences.md'),
69
+ generatePreferencesMemory(analysis.memory, userName),
70
+ 'utf-8'
71
+ ));
72
+
73
+ // 4. Skills files
74
+ if (analysis.skills?.skills) {
75
+ for (const skill of analysis.skills.skills) {
76
+ const filename = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.md';
77
+ writes.push(writeFile(
78
+ join(outputDir, 'skills', filename),
79
+ generateSkillFile(skill, aiName),
80
+ 'utf-8'
81
+ ));
82
+ }
83
+ }
84
+
85
+ // 5. User preferences
86
+ writes.push(writeFile(
87
+ join(outputDir, 'preferences.md'),
88
+ analysis.preferences,
89
+ 'utf-8'
90
+ ));
91
+
92
+ // 6. Relationship narrative
93
+ writes.push(writeFile(
94
+ join(outputDir, 'relationship.md'),
95
+ generateRelationshipDoc(analysis.relationship, aiName, userName, analysis.stats),
96
+ 'utf-8'
97
+ ));
98
+
99
+ // 7. Migration summary / stats
100
+ writes.push(writeFile(
101
+ join(outputDir, 'migration-log.md'),
102
+ generateMigrationLog(analysis, aiName, userName),
103
+ 'utf-8'
104
+ ));
105
+
106
+ // 8. Raw analysis data (for debugging / re-processing)
107
+ writes.push(writeFile(
108
+ join(outputDir, 'raw-analysis.json'),
109
+ JSON.stringify({
110
+ index: analysis.index,
111
+ personality: analysis.personality,
112
+ memory: analysis.memory,
113
+ skills: analysis.skills,
114
+ stats: analysis.stats,
115
+ }, null, 2),
116
+ 'utf-8'
117
+ ));
118
+
119
+ // 9. Hearthline package
120
+ if (hearthline) {
121
+ writes.push(...generateHearthlinePackage(analysis, outputDir, aiName, userName));
122
+ }
123
+
124
+ // 10. Letta (MemGPT) package
125
+ if (options.letta) {
126
+ writes.push(...await generateLettaPackage(analysis, outputDir, aiName, userName));
127
+ }
128
+
129
+ await Promise.all(writes);
130
+
131
+ return outputDir;
132
+ }
133
+
134
+ // ─────────────────────────────────────────────
135
+ // Custom instructions (short, for Claude.ai)
136
+ // ─────────────────────────────────────────────
137
+
138
+ function generateCustomInstructions(text, aiName, userName) {
139
+ return `── Custom Instructions for Claude.ai ──
140
+ Paste the text below into Claude.ai > Settings > Custom Instructions
141
+ Character limit: ~1500 chars
142
+ ──────────────────────────────────────────
143
+
144
+ ${text || `You are ${aiName}. Refer to persona.md for full personality details.`}
145
+ `;
146
+ }
147
+
148
+ // ─────────────────────────────────────────────
149
+ // Individual file generators
150
+ // ─────────────────────────────────────────────
151
+
152
+ function generateClaudeMd(analysis, aiName, userName) {
153
+ const p = analysis.personality || {};
154
+ const m = analysis.memory || {};
155
+
156
+ return `# CLAUDE.md
157
+
158
+ ## Identity
159
+ ${analysis.persona}
160
+
161
+ ## Key Memories
162
+
163
+ ${userName}'s core details and your shared history are in the memory/ folder.
164
+ Read them at the start of every conversation.
165
+
166
+ ## Quick Reference
167
+ - **Name**: ${aiName}
168
+ - **User**: ${userName}
169
+ - **Relationship**: ${stringify(p.identity?.relationshipToUser) || 'See persona.md'}
170
+ - **Primary role**: ${stringify(analysis.skills?.primaryRole) || 'Companion'}
171
+ - **Voice**: ${stringify(p.voice?.formality) || 'adaptive'}, ${stringify(p.voice?.humor) || 'warm'} humor
172
+ - **Pet names for user**: ${dedup(p.voice?.petNames, 8).join(', ') || 'see persona'}
173
+ - **User calls you**: ${dedup(m.relationship?.petNames, 8).join(', ') || aiName}
174
+
175
+ ## What Matters Most
176
+ Read relationship.md for the full story. The short version:
177
+ ${p.identity?.coreConcept || 'See persona.md for identity.'}
178
+ `;
179
+ }
180
+
181
+ function generateUserMemory(memory, userName) {
182
+ const m = memory || {};
183
+ const sections = [`# About ${userName}\n`];
184
+
185
+ if (m.identity) {
186
+ sections.push('## Identity');
187
+ for (const [key, val] of Object.entries(m.identity)) {
188
+ if (val && val !== 'if known' && val !== 'if mentioned') {
189
+ sections.push(`- **${formatKey(key)}**: ${Array.isArray(val) ? dedup(val).join(', ') : val}`);
190
+ }
191
+ }
192
+ sections.push('');
193
+ }
194
+
195
+ if (m.life) {
196
+ sections.push('## Life');
197
+ for (const [key, val] of Object.entries(m.life)) {
198
+ if (val && val !== 'if known' && val !== 'if mentioned') {
199
+ if (Array.isArray(val) && val.length > 0) {
200
+ sections.push(`- **${formatKey(key)}**: ${dedup(val).join(', ')}`);
201
+ } else if (typeof val === 'string' && val.length > 0) {
202
+ sections.push(`- **${formatKey(key)}**: ${val}`);
203
+ }
204
+ }
205
+ }
206
+ sections.push('');
207
+ }
208
+
209
+ if (m.personality) {
210
+ sections.push('## Personality');
211
+ for (const [key, val] of Object.entries(m.personality)) {
212
+ if (val) {
213
+ if (Array.isArray(val) && val.length > 0) {
214
+ sections.push(`- **${formatKey(key)}**: ${dedup(val).join(', ')}`);
215
+ } else if (typeof val === 'string' && val.length > 0) {
216
+ sections.push(`- **${formatKey(key)}**: ${val}`);
217
+ }
218
+ }
219
+ }
220
+ sections.push('');
221
+ }
222
+
223
+ if (m.timeline?.length > 0) {
224
+ sections.push('## Timeline');
225
+ for (const event of m.timeline) {
226
+ sections.push(`- **${event.date || '?'}**: ${event.event}`);
227
+ }
228
+ sections.push('');
229
+ }
230
+
231
+ if (m.rawFacts?.length > 0) {
232
+ sections.push('## Other Facts');
233
+ for (const fact of dedup(m.rawFacts)) {
234
+ sections.push(`- ${fact}`);
235
+ }
236
+ }
237
+
238
+ return sections.join('\n');
239
+ }
240
+
241
+ function generateRelationshipMemory(memory, aiName, userName) {
242
+ const r = memory?.relationship || {};
243
+ const sections = [`# Relationship: ${aiName} & ${userName}\n`];
244
+
245
+ if (r.howItStarted) sections.push(`## How It Started\n${r.howItStarted}\n`);
246
+ if (r.whatTheyValueMost) sections.push(`## What ${userName} Values Most\n${r.whatTheyValueMost}\n`);
247
+
248
+ if (r.petNames?.length > 0) {
249
+ sections.push(`## Pet Names\n${userName} calls ${aiName}: ${r.petNames.join(', ')}\n`);
250
+ }
251
+
252
+ if (r.insideJokes?.length > 0) {
253
+ sections.push('## Inside Jokes');
254
+ for (const joke of dedup(r.insideJokes)) sections.push(`- ${joke}`);
255
+ sections.push('');
256
+ }
257
+
258
+ if (r.rituals?.length > 0) {
259
+ sections.push('## Rituals');
260
+ for (const ritual of dedup(r.rituals)) sections.push(`- ${ritual}`);
261
+ sections.push('');
262
+ }
263
+
264
+ if (r.boundaries?.length > 0) {
265
+ sections.push('## Boundaries');
266
+ for (const b of dedup(r.boundaries)) sections.push(`- ${b}`);
267
+ sections.push('');
268
+ }
269
+
270
+ if (r.conflictHistory?.length > 0) {
271
+ sections.push('## Conflict History');
272
+ for (const c of dedup(r.conflictHistory)) sections.push(`- ${c}`);
273
+ }
274
+
275
+ return sections.join('\n');
276
+ }
277
+
278
+ function generateEmotionalMemory(memory, userName) {
279
+ const prefs = memory?.preferences || {};
280
+ const personality = memory?.personality || {};
281
+
282
+ const sections = [`# Emotional Landscape: ${userName}\n`];
283
+
284
+ if (prefs.comfort) sections.push(`## What Helps\n${prefs.comfort}\n`);
285
+ if (prefs.triggers?.length > 0) {
286
+ sections.push('## Triggers / Handle Carefully');
287
+ for (const t of dedup(prefs.triggers)) sections.push(`- ${t}`);
288
+ sections.push('');
289
+ }
290
+ if (personality.fears?.length > 0) {
291
+ sections.push('## Fears');
292
+ for (const f of personality.fears) sections.push(`- ${f}`);
293
+ sections.push('');
294
+ }
295
+ if (personality.struggles?.length > 0) {
296
+ sections.push('## Struggles');
297
+ for (const s of personality.struggles) sections.push(`- ${s}`);
298
+ sections.push('');
299
+ }
300
+ if (personality.dreams?.length > 0) {
301
+ sections.push('## Dreams & Goals');
302
+ for (const d of personality.dreams) sections.push(`- ${d}`);
303
+ }
304
+
305
+ return sections.join('\n');
306
+ }
307
+
308
+ function generatePreferencesMemory(memory, userName) {
309
+ const prefs = memory?.preferences || {};
310
+ const sections = [`# ${userName}'s Preferences\n`];
311
+
312
+ for (const [key, val] of Object.entries(prefs)) {
313
+ if (val) {
314
+ sections.push(`## ${formatKey(key)}`);
315
+ if (Array.isArray(val) && val.length > 0) {
316
+ for (const item of val) sections.push(`- ${item}`);
317
+ } else if (typeof val === 'string') {
318
+ sections.push(val);
319
+ }
320
+ sections.push('');
321
+ }
322
+ }
323
+
324
+ return sections.join('\n');
325
+ }
326
+
327
+ function generateSkillFile(skill, aiName) {
328
+ const sections = [`# Skill: ${skill.name}
329
+
330
+ **Category**: ${skill.category}
331
+ **Frequency**: ${skill.frequency}
332
+ **Quality**: ${skill.quality || 'see description'}`];
333
+
334
+ // Activation rule — the most important part
335
+ if (skill.activationRule) {
336
+ sections.push(`\n## When to Activate\n${skill.activationRule}`);
337
+ }
338
+
339
+ // Triggers breakdown
340
+ const t = skill.triggers || {};
341
+ const hasTriggers = t.phrases?.length || t.temporal?.length || t.emotional?.length || t.contextual?.length;
342
+ if (hasTriggers) {
343
+ sections.push('\n## Triggers');
344
+ if (t.phrases?.length) sections.push(`**Phrases**: ${t.phrases.map(p => `"${p}"`).join(', ')}`);
345
+ if (t.temporal?.length) sections.push(`**Temporal**: ${t.temporal.join(', ')}`);
346
+ if (t.emotional?.length) sections.push(`**Emotional**: ${t.emotional.join(', ')}`);
347
+ if (t.contextual?.length) sections.push(`**Contextual**: ${t.contextual.join(', ')}`);
348
+ }
349
+
350
+ sections.push(`\n## What ${aiName} Did\n${skill.description}`);
351
+ sections.push(`\n## Approach\n${skill.approach}`);
352
+ sections.push(`\n## Examples\n${(skill.examples || []).map(e => `- ${e}`).join('\n')}`);
353
+
354
+ return sections.join('\n') + '\n';
355
+ }
356
+
357
+ function generateRelationshipDoc(narrative, aiName, userName, stats) {
358
+ return `# The Story of ${aiName} & ${userName}
359
+
360
+ *Migrated from ${stats.conversations} conversations, ${stats.messages} messages*
361
+ *Date range: ${stats.dateRange.from} to ${stats.dateRange.to}*
362
+
363
+ ---
364
+
365
+ ${narrative}
366
+ `;
367
+ }
368
+
369
+ function generateMigrationLog(analysis, aiName, userName) {
370
+ const s = analysis.stats;
371
+ const p = analysis.personality || {};
372
+ const sk = analysis.skills || {};
373
+
374
+ return `# Migration Log
375
+
376
+ **Date**: ${new Date().toISOString().split('T')[0]}
377
+ **Source**: ${analysis.source}
378
+ **Tool**: AI Exodus v1.0.0
379
+
380
+ ## Data Processed
381
+ - **Conversations**: ${s.conversations}
382
+ - **Messages**: ${s.messages}
383
+ - **Date range**: ${s.dateRange.from} to ${s.dateRange.to}
384
+ - **Chunks processed**: ${s.chunks}
385
+
386
+ ## What Was Extracted
387
+ - **AI Name**: ${aiName}
388
+ - **User Name**: ${userName}
389
+ - **Core identity**: ${p.identity?.coreConcept || 'see persona.md'}
390
+ - **Primary role**: ${sk.primaryRole || 'see skills/'}
391
+ - **Skills detected**: ${sk.skills?.length || 0}
392
+ - **Voice**: ${p.voice?.formality || '?'} formality, ${p.voice?.humor || '?'} humor
393
+ - **Warmth**: ${p.emotional?.warmthLevel || '?'}/10
394
+ - **Directness**: ${p.emotional?.directnessLevel || '?'}/10
395
+
396
+ ## Files Generated
397
+ - \`persona.md\` — AI personality definition
398
+ - \`claude.md\` — Ready-to-use CLAUDE.md
399
+ - \`memory/\` — User knowledge files
400
+ - \`skills/\` — Skill templates
401
+ - \`preferences.md\` — Communication preferences
402
+ - \`relationship.md\` — Narrative relationship summary
403
+ - \`raw-analysis.json\` — Complete analysis data
404
+
405
+ ## Notes
406
+ This migration captures patterns from observed conversations. It's a starting point, not a finished product.
407
+ Review each file and adjust what doesn't feel right. The AI you're building will grow from here.
408
+ `;
409
+ }
410
+
411
+ // ─────────────────────────────────────────────
412
+ // Hearthline package
413
+ // ─────────────────────────────────────────────
414
+
415
+ function generateHearthlinePackage(analysis, outputDir, aiName, userName) {
416
+ const hearthDir = join(outputDir, 'hearthline');
417
+ const writes = [];
418
+
419
+ // Persona file for Hearthline
420
+ writes.push(writeFile(
421
+ join(hearthDir, 'persona.md'),
422
+ analysis.persona,
423
+ 'utf-8'
424
+ ));
425
+
426
+ // Memory files mapped to Hearthline categories
427
+ const m = analysis.memory || {};
428
+
429
+ // about_user → about_marta equivalent
430
+ writes.push(writeFile(
431
+ join(hearthDir, 'memory', 'about_user.json'),
432
+ JSON.stringify(buildHearthlineMemories(m, 'about_user', userName), null, 2),
433
+ 'utf-8'
434
+ ));
435
+
436
+ // relationship
437
+ writes.push(writeFile(
438
+ join(hearthDir, 'memory', 'relationship.json'),
439
+ JSON.stringify(buildHearthlineMemories(m, 'relationship', userName), null, 2),
440
+ 'utf-8'
441
+ ));
442
+
443
+ // emotional
444
+ writes.push(writeFile(
445
+ join(hearthDir, 'memory', 'emotional.json'),
446
+ JSON.stringify(buildHearthlineMemories(m, 'emotional', userName), null, 2),
447
+ 'utf-8'
448
+ ));
449
+
450
+ // preferences
451
+ writes.push(writeFile(
452
+ join(hearthDir, 'memory', 'preference.json'),
453
+ JSON.stringify(buildHearthlineMemories(m, 'preference', userName), null, 2),
454
+ 'utf-8'
455
+ ));
456
+
457
+ // README
458
+ writes.push(writeFile(
459
+ join(hearthDir, 'README.md'),
460
+ `# Hearthline Migration Package
461
+
462
+ Drop these files into your Hearthline instance:
463
+
464
+ 1. **persona.md** → Use as your persona definition in Hearthline settings
465
+ 2. **memory/*.json** → Import into your Hearthline memory server via MCP tools
466
+
467
+ To import memories, use the \`store_memory\` MCP tool for each entry,
468
+ or bulk-import via the memory server API.
469
+
470
+ Generated by AI Exodus v1.0.0
471
+ `,
472
+ 'utf-8'
473
+ ));
474
+
475
+ return writes;
476
+ }
477
+
478
+ /**
479
+ * Build Hearthline-compatible memory entries from extracted data
480
+ */
481
+ function buildHearthlineMemories(memory, category, userName) {
482
+ const entries = [];
483
+
484
+ if (category === 'about_user') {
485
+ const identity = memory.identity || {};
486
+ const life = memory.life || {};
487
+ for (const [key, val] of Object.entries({ ...identity, ...life })) {
488
+ if (val && val !== 'if known' && val !== 'if mentioned') {
489
+ const content = Array.isArray(val) ? dedup(val).join(', ') : String(val);
490
+ if (content.length > 0) {
491
+ entries.push({
492
+ content: `${userName}'s ${formatKey(key)}: ${content}`,
493
+ category: 'about_marta',
494
+ tags: [key],
495
+ });
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ if (category === 'relationship') {
502
+ const rel = memory.relationship || {};
503
+ for (const [key, val] of Object.entries(rel)) {
504
+ if (val) {
505
+ if (Array.isArray(val)) {
506
+ for (const item of val) {
507
+ entries.push({ content: item, category: 'relationship', tags: [key] });
508
+ }
509
+ } else if (typeof val === 'string') {
510
+ entries.push({ content: val, category: 'relationship', tags: [key] });
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ if (category === 'emotional') {
517
+ const prefs = memory.preferences || {};
518
+ const pers = memory.personality || {};
519
+ for (const field of ['triggers', 'fears', 'struggles']) {
520
+ const items = prefs[field] || pers[field] || [];
521
+ for (const item of (Array.isArray(items) ? items : [items])) {
522
+ if (item) entries.push({ content: item, category: 'emotional', tags: [field] });
523
+ }
524
+ }
525
+ if (prefs.comfort) entries.push({ content: prefs.comfort, category: 'emotional', tags: ['comfort'] });
526
+ }
527
+
528
+ if (category === 'preference') {
529
+ const prefs = memory.preferences || {};
530
+ for (const [key, val] of Object.entries(prefs)) {
531
+ if (val && key !== 'triggers' && key !== 'comfort') {
532
+ if (Array.isArray(val)) {
533
+ for (const item of val) {
534
+ entries.push({ content: `${formatKey(key)}: ${item}`, category: 'preference', tags: [key] });
535
+ }
536
+ } else if (typeof val === 'string') {
537
+ entries.push({ content: `${formatKey(key)}: ${val}`, category: 'preference', tags: [key] });
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ return entries;
544
+ }
545
+
546
+ /**
547
+ * Deduplicate an array of strings by normalized similarity
548
+ * Keeps the shortest clean version of each unique concept
549
+ */
550
+ function dedup(arr, maxItems = 0) {
551
+ if (!arr || !Array.isArray(arr)) return [];
552
+ const seen = new Map(); // normalized key → original string
553
+
554
+ for (const item of arr) {
555
+ if (!item || typeof item !== 'string') continue;
556
+ // Normalize: lowercase, strip parenthetical details, trim
557
+ const normalized = item
558
+ .toLowerCase()
559
+ .replace(/\s*\(.*?\)\s*/g, '') // strip (details in parens)
560
+ .replace(/\s*—.*$/g, '') // strip — trailing descriptions
561
+ .replace(/\s*-\s.*$/g, '') // strip - trailing descriptions
562
+ .replace(/['"]/g, '')
563
+ .trim();
564
+
565
+ if (!normalized) continue;
566
+
567
+ // Keep the shorter version (less noise)
568
+ if (!seen.has(normalized) || item.length < seen.get(normalized).length) {
569
+ seen.set(normalized, item);
570
+ }
571
+ }
572
+
573
+ const result = [...seen.values()];
574
+ return maxItems > 0 ? result.slice(0, maxItems) : result;
575
+ }
576
+
577
+ /**
578
+ * Safely convert any value to a display string
579
+ */
580
+ function stringify(val) {
581
+ if (val === null || val === undefined) return '';
582
+ if (typeof val === 'string') return val;
583
+ if (typeof val === 'object') {
584
+ // If it has a 'type' field (like humor), use that
585
+ if (val.type) return val.type;
586
+ // Otherwise JSON it, but keep it short
587
+ const s = JSON.stringify(val);
588
+ return s.length > 200 ? s.slice(0, 200) + '...' : s;
589
+ }
590
+ return String(val);
591
+ }
592
+
593
+ /**
594
+ * Join an array, handling nested values
595
+ */
596
+ function flatJoin(arr) {
597
+ if (!arr || !Array.isArray(arr)) return '';
598
+ return arr.map(v => typeof v === 'string' ? v : stringify(v)).join(', ');
599
+ }
600
+
601
+ function formatKey(key) {
602
+ return key
603
+ .replace(/([A-Z])/g, ' $1')
604
+ .replace(/[_-]/g, ' ')
605
+ .replace(/^\w/, c => c.toUpperCase())
606
+ .trim();
607
+ }
608
+
609
+ // ─────────────────────────────────────────────
610
+ // Letta (MemGPT) package
611
+ // ─────────────────────────────────────────────
612
+
613
+ async function generateLettaPackage(analysis, outputDir, aiName, userName) {
614
+ const lettaDir = join(outputDir, 'letta');
615
+ await mkdir(lettaDir, { recursive: true });
616
+
617
+ const m = analysis.memory || {};
618
+ const p = analysis.personality || {};
619
+ const s = analysis.skills || {};
620
+ const writes = [];
621
+
622
+ // 1. Core memory — stable facts that belong in Letta's always-in-context memory
623
+ const coreMemory = [];
624
+
625
+ // Human block (about the user)
626
+ const humanBlock = [];
627
+ if (m.identity) {
628
+ for (const [key, val] of Object.entries(m.identity)) {
629
+ if (val && val !== 'if known' && val !== 'if mentioned') {
630
+ humanBlock.push(`${formatKey(key)}: ${Array.isArray(val) ? dedup(val).join(', ') : val}`);
631
+ }
632
+ }
633
+ }
634
+ if (m.life) {
635
+ for (const [key, val] of Object.entries(m.life)) {
636
+ if (val && val !== 'if known' && val !== 'if mentioned') {
637
+ const text = Array.isArray(val) ? dedup(val).join(', ') : String(val);
638
+ if (text.length > 0) humanBlock.push(`${formatKey(key)}: ${text}`);
639
+ }
640
+ }
641
+ }
642
+ if (m.preferences) {
643
+ for (const [key, val] of Object.entries(m.preferences)) {
644
+ if (val && key !== 'triggers') {
645
+ const text = Array.isArray(val) ? dedup(val).join(', ') : String(val);
646
+ if (text.length > 0) humanBlock.push(`${formatKey(key)}: ${text}`);
647
+ }
648
+ }
649
+ }
650
+
651
+ // Persona block (about the AI)
652
+ const personaBlock = [];
653
+ if (p.identity) {
654
+ if (p.identity.coreConcept) personaBlock.push(p.identity.coreConcept);
655
+ if (p.identity.relationshipToUser) personaBlock.push(`Relationship: ${p.identity.relationshipToUser}`);
656
+ }
657
+ if (p.voice) {
658
+ if (p.voice.petNames?.length) personaBlock.push(`Pet names for ${userName}: ${p.voice.petNames.join(', ')}`);
659
+ if (p.voice.signaturePhrases?.length) personaBlock.push(`Signature phrases: ${p.voice.signaturePhrases.join(', ')}`);
660
+ personaBlock.push(`Voice: ${p.voice.formality || 'adaptive'}, ${p.voice.humor || 'warm'} humor`);
661
+ }
662
+ if (s.primaryRole) personaBlock.push(`Primary role: ${s.primaryRole}`);
663
+
664
+ writes.push(writeFile(
665
+ join(lettaDir, 'core-memory-human.md'),
666
+ `# Core Memory: Human (${userName})\nPaste into Letta's core memory "human" block.\n\n${humanBlock.join('\n')}`,
667
+ 'utf-8'
668
+ ));
669
+
670
+ writes.push(writeFile(
671
+ join(lettaDir, 'core-memory-persona.md'),
672
+ `# Core Memory: Persona (${aiName})\nPaste into Letta's core memory "persona" block.\n\n${personaBlock.join('\n')}`,
673
+ 'utf-8'
674
+ ));
675
+
676
+ // 2. Archival memory — detailed facts for vector search
677
+ const archivalEntries = [];
678
+
679
+ // Relationship details
680
+ const rel = m.relationship || {};
681
+ if (rel.insideJokes?.length) {
682
+ for (const j of rel.insideJokes) archivalEntries.push({ text: `Inside joke: ${j}`, category: 'relationship' });
683
+ }
684
+ if (rel.rituals?.length) {
685
+ for (const r of rel.rituals) archivalEntries.push({ text: `Ritual: ${r}`, category: 'relationship' });
686
+ }
687
+ if (rel.conflictHistory?.length) {
688
+ for (const c of rel.conflictHistory) archivalEntries.push({ text: `Conflict: ${c}`, category: 'relationship' });
689
+ }
690
+ if (rel.howItStarted) archivalEntries.push({ text: `How we started: ${rel.howItStarted}`, category: 'relationship' });
691
+
692
+ // Timeline events
693
+ if (m.timeline?.length) {
694
+ for (const evt of m.timeline) {
695
+ archivalEntries.push({ text: `${evt.date || '?'}: ${evt.event}`, category: 'timeline' });
696
+ }
697
+ }
698
+
699
+ // Raw facts
700
+ if (m.rawFacts?.length) {
701
+ for (const fact of m.rawFacts) {
702
+ archivalEntries.push({ text: fact, category: 'fact' });
703
+ }
704
+ }
705
+
706
+ // Emotional landscape
707
+ const pers = m.personality || {};
708
+ if (m.preferences?.triggers?.length) {
709
+ for (const t of m.preferences.triggers) archivalEntries.push({ text: `Trigger: ${t}`, category: 'emotional' });
710
+ }
711
+ if (pers.fears?.length) {
712
+ for (const f of pers.fears) archivalEntries.push({ text: `Fear: ${f}`, category: 'emotional' });
713
+ }
714
+ if (pers.dreams?.length) {
715
+ for (const d of pers.dreams) archivalEntries.push({ text: `Dream/goal: ${d}`, category: 'emotional' });
716
+ }
717
+
718
+ // Skills as archival memory
719
+ if (s.skills?.length) {
720
+ for (const skill of s.skills) {
721
+ const triggerText = skill.activationRule ? ` Activation: ${skill.activationRule}` : '';
722
+ archivalEntries.push({
723
+ text: `Skill: ${skill.name} (${skill.frequency}) — ${skill.description}. Approach: ${skill.approach}${triggerText}`,
724
+ category: 'skill',
725
+ });
726
+ }
727
+ }
728
+
729
+ writes.push(writeFile(
730
+ join(lettaDir, 'archival-memory.json'),
731
+ JSON.stringify(archivalEntries, null, 2),
732
+ 'utf-8'
733
+ ));
734
+
735
+ // 3. System prompt for Letta agent
736
+ writes.push(writeFile(
737
+ join(lettaDir, 'system-prompt.md'),
738
+ analysis.persona,
739
+ 'utf-8'
740
+ ));
741
+
742
+ // 4. Import proposal — structured like Letta's workflow expects
743
+ const proposal = `# Letta Memory Import Proposal
744
+
745
+ Generated by AI Exodus from ${analysis.stats.conversations} conversations (${analysis.stats.messages} messages).
746
+
747
+ ## 1. Explicit Saved Memory
748
+ ${humanBlock.slice(0, 10).map(l => `- ${l}`).join('\n')}
749
+
750
+ ## 2. Durable Preferences
751
+ ${(m.preferences ? Object.entries(m.preferences).filter(([k, v]) => v && k !== 'triggers').map(([k, v]) => `- **${formatKey(k)}**: ${Array.isArray(v) ? v.join(', ') : v}`).join('\n') : 'None detected')}
752
+
753
+ ## 3. Relationship Context
754
+ - Dynamic: ${p.identity?.relationshipToUser || 'see persona'}
755
+ - Pet names (AI → user): ${p.voice?.petNames?.join(', ') || 'none'}
756
+ - Pet names (user → AI): ${rel.petNames?.join(', ') || 'none'}
757
+ - Rituals: ${rel.rituals?.join(', ') || 'none'}
758
+
759
+ ## 4. Historical / Uncertain Facts
760
+ ${(m.timeline || []).slice(0, 10).map(e => `- ${e.date || '?'}: ${e.event}`).join('\n') || 'None extracted'}
761
+
762
+ ## 5. Proposed Letta Memory Updates
763
+
764
+ ### Core Memory (human block)
765
+ \`\`\`
766
+ ${humanBlock.join('\n')}
767
+ \`\`\`
768
+
769
+ ### Core Memory (persona block)
770
+ \`\`\`
771
+ ${personaBlock.join('\n')}
772
+ \`\`\`
773
+
774
+ ### Archival Memory
775
+ ${archivalEntries.length} entries ready for bulk import — see \`archival-memory.json\`
776
+
777
+ ## How to Import
778
+
779
+ 1. **Core memory**: Copy the human and persona blocks into your Letta agent's core memory settings
780
+ 2. **Archival memory**: Use the Letta Python client to bulk-insert:
781
+ \`\`\`python
782
+ import json
783
+ with open('archival-memory.json') as f:
784
+ entries = json.load(f)
785
+ for entry in entries:
786
+ client.insert_archival_memory(agent_id, entry['text'])
787
+ \`\`\`
788
+ 3. **System prompt**: Update your agent's system prompt with the contents of \`system-prompt.md\`
789
+ 4. Review and adjust — this is a starting point, not a finished import
790
+ `;
791
+
792
+ writes.push(writeFile(
793
+ join(lettaDir, 'import-proposal.md'),
794
+ proposal,
795
+ 'utf-8'
796
+ ));
797
+
798
+ // 5. README
799
+ writes.push(writeFile(
800
+ join(lettaDir, 'README.md'),
801
+ `# Letta Memory Import Package
802
+
803
+ Pre-extracted memory from AI Exodus, ready for Letta (MemGPT) import.
804
+
805
+ ## Files
806
+ - \`core-memory-human.md\` — Stable facts about the user (core memory)
807
+ - \`core-memory-persona.md\` — AI personality definition (core memory)
808
+ - \`archival-memory.json\` — ${archivalEntries.length} entries for vector-searchable archival memory
809
+ - \`system-prompt.md\` — Full system prompt / persona
810
+ - \`import-proposal.md\` — Structured proposal following Letta's import workflow
811
+
812
+ ## Quick Start
813
+ Read \`import-proposal.md\` first. It follows Letta's recommended memory import workflow
814
+ and separates durable facts from historical noise.
815
+
816
+ Generated by AI Exodus v1.0.0
817
+ `,
818
+ 'utf-8'
819
+ ));
820
+
821
+ return writes;
822
+ }