kernelbot 1.0.37 → 1.0.39

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 (41) hide show
  1. package/bin/kernel.js +499 -249
  2. package/config.example.yaml +17 -0
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +3 -1
  6. package/src/agent.js +355 -82
  7. package/src/bot.js +724 -12
  8. package/src/character.js +406 -0
  9. package/src/characters/builder.js +174 -0
  10. package/src/characters/builtins.js +421 -0
  11. package/src/conversation.js +17 -2
  12. package/src/dashboard/agents.css +469 -0
  13. package/src/dashboard/agents.html +184 -0
  14. package/src/dashboard/agents.js +873 -0
  15. package/src/dashboard/dashboard.css +281 -0
  16. package/src/dashboard/dashboard.js +579 -0
  17. package/src/dashboard/index.html +366 -0
  18. package/src/dashboard/server.js +521 -0
  19. package/src/dashboard/shared.css +700 -0
  20. package/src/dashboard/shared.js +218 -0
  21. package/src/life/engine.js +115 -26
  22. package/src/life/evolution.js +7 -5
  23. package/src/life/journal.js +5 -4
  24. package/src/life/memory.js +12 -9
  25. package/src/life/share-queue.js +7 -5
  26. package/src/prompts/orchestrator.js +76 -14
  27. package/src/prompts/workers.js +22 -0
  28. package/src/self.js +17 -5
  29. package/src/services/linkedin-api.js +190 -0
  30. package/src/services/stt.js +8 -2
  31. package/src/services/tts.js +32 -2
  32. package/src/services/x-api.js +141 -0
  33. package/src/swarm/worker-registry.js +7 -0
  34. package/src/tools/categories.js +4 -0
  35. package/src/tools/index.js +6 -0
  36. package/src/tools/linkedin.js +264 -0
  37. package/src/tools/orchestrator-tools.js +337 -2
  38. package/src/tools/x.js +256 -0
  39. package/src/utils/config.js +190 -139
  40. package/src/utils/display.js +165 -52
  41. package/src/utils/temporal-awareness.js +24 -10
@@ -0,0 +1,406 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, rmSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import { getLogger } from './utils/logger.js';
7
+ import { BUILTIN_CHARACTERS } from './characters/builtins.js';
8
+ import { SelfManager } from './self.js';
9
+ import { MemoryManager } from './life/memory.js';
10
+ import { JournalManager } from './life/journal.js';
11
+ import { ShareQueue } from './life/share-queue.js';
12
+ import { EvolutionTracker } from './life/evolution.js';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const KERNELBOT_DIR = join(homedir(), '.kernelbot');
16
+ const CHARACTERS_DIR = join(KERNELBOT_DIR, 'characters');
17
+ const REGISTRY_FILE = join(CHARACTERS_DIR, 'registry.json');
18
+
19
+ const DEFAULT_REGISTRY = {
20
+ activeCharacterId: 'kernel',
21
+ characters: {},
22
+ };
23
+
24
+ export class CharacterManager {
25
+ constructor() {
26
+ this._registry = null;
27
+ this.needsOnboarding = false;
28
+ mkdirSync(CHARACTERS_DIR, { recursive: true });
29
+ this._load();
30
+ }
31
+
32
+ // ── Registry Persistence ─────────────────────────────────────
33
+
34
+ _load() {
35
+ const logger = getLogger();
36
+
37
+ if (existsSync(REGISTRY_FILE)) {
38
+ try {
39
+ this._registry = JSON.parse(readFileSync(REGISTRY_FILE, 'utf-8'));
40
+ logger.debug(`[CharacterManager] Loaded registry: ${Object.keys(this._registry.characters).length} characters`);
41
+ } catch (err) {
42
+ logger.warn(`[CharacterManager] Failed to parse registry — resetting to defaults: ${err.message}`);
43
+ this._registry = { ...DEFAULT_REGISTRY, characters: {} };
44
+ }
45
+ return;
46
+ }
47
+
48
+ // No registry exists — check if we need migration or fresh onboarding
49
+ const legacySelfDir = join(KERNELBOT_DIR, 'self');
50
+ if (existsSync(legacySelfDir)) {
51
+ // Existing installation — migrate to kernel character
52
+ logger.info('[CharacterManager] Legacy data detected — migrating to kernel character');
53
+ this._migrateToKernel();
54
+ } else {
55
+ // Fresh install — needs onboarding
56
+ logger.info('[CharacterManager] Fresh install — onboarding needed');
57
+ this._registry = { ...DEFAULT_REGISTRY, characters: {} };
58
+ this.needsOnboarding = true;
59
+ }
60
+ }
61
+
62
+ _save() {
63
+ const logger = getLogger();
64
+ try {
65
+ writeFileSync(REGISTRY_FILE, JSON.stringify(this._registry, null, 2), 'utf-8');
66
+ } catch (err) {
67
+ logger.error(`[CharacterManager] Failed to save registry: ${err.message}`);
68
+ }
69
+ }
70
+
71
+ // ── Migration ────────────────────────────────────────────────
72
+
73
+ _migrateToKernel() {
74
+ const logger = getLogger();
75
+ const kernelDir = join(CHARACTERS_DIR, 'kernel');
76
+ mkdirSync(kernelDir, { recursive: true });
77
+
78
+ // Copy self/ → characters/kernel/self/
79
+ const legacySelfDir = join(KERNELBOT_DIR, 'self');
80
+ const targetSelfDir = join(kernelDir, 'self');
81
+ if (existsSync(legacySelfDir) && !existsSync(targetSelfDir)) {
82
+ cpSync(legacySelfDir, targetSelfDir, { recursive: true });
83
+ logger.info('[CharacterManager] Migrated self/ → characters/kernel/self/');
84
+ }
85
+
86
+ // Copy life/ → characters/kernel/life/
87
+ const legacyLifeDir = join(KERNELBOT_DIR, 'life');
88
+ const targetLifeDir = join(kernelDir, 'life');
89
+ if (existsSync(legacyLifeDir) && !existsSync(targetLifeDir)) {
90
+ cpSync(legacyLifeDir, targetLifeDir, { recursive: true });
91
+ logger.info('[CharacterManager] Migrated life/ → characters/kernel/life/');
92
+ }
93
+
94
+ // Copy conversations.json → characters/kernel/conversations.json
95
+ // Re-key all conversation and skill entries with "kernel:" prefix
96
+ // so they match the new _chatKey() scoping in agent.js
97
+ const legacyConvFile = join(KERNELBOT_DIR, 'conversations.json');
98
+ const targetConvFile = join(kernelDir, 'conversations.json');
99
+ if (existsSync(legacyConvFile) && !existsSync(targetConvFile)) {
100
+ try {
101
+ const raw = JSON.parse(readFileSync(legacyConvFile, 'utf-8'));
102
+ const migrated = {};
103
+
104
+ for (const [key, value] of Object.entries(raw)) {
105
+ if (key === '_skills') {
106
+ // Re-key skills: "12345" → "kernel:12345"
107
+ const migratedSkills = {};
108
+ for (const [skillKey, skillId] of Object.entries(value)) {
109
+ const newKey = skillKey.startsWith('kernel:') || skillKey.startsWith('__life__') ? skillKey : `kernel:${skillKey}`;
110
+ migratedSkills[newKey] = skillId;
111
+ }
112
+ migrated._skills = migratedSkills;
113
+ } else if (key.startsWith('__life__')) {
114
+ // Re-key life engine chat: "__life__" → "__life__:kernel"
115
+ migrated[key === '__life__' ? '__life__:kernel' : key] = value;
116
+ } else {
117
+ // Re-key user chats: "12345" → "kernel:12345"
118
+ const newKey = key.startsWith('kernel:') ? key : `kernel:${key}`;
119
+ migrated[newKey] = value;
120
+ }
121
+ }
122
+
123
+ writeFileSync(targetConvFile, JSON.stringify(migrated, null, 2), 'utf-8');
124
+ logger.info('[CharacterManager] Migrated conversations.json (re-keyed with kernel: prefix)');
125
+ } catch (err) {
126
+ // Fallback: just copy as-is if parsing fails
127
+ cpSync(legacyConvFile, targetConvFile);
128
+ logger.warn(`[CharacterManager] Migrated conversations.json (raw copy, re-key failed: ${err.message})`);
129
+ }
130
+ }
131
+
132
+ // Copy persona.md from source
133
+ const defaultPersonaMd = join(__dirname, 'prompts', 'persona.md');
134
+ const targetPersonaMd = join(kernelDir, 'persona.md');
135
+ if (existsSync(defaultPersonaMd) && !existsSync(targetPersonaMd)) {
136
+ cpSync(defaultPersonaMd, targetPersonaMd);
137
+ logger.info('[CharacterManager] Copied persona.md to kernel character');
138
+ }
139
+
140
+ // Create profile.json
141
+ const profileFile = join(kernelDir, 'profile.json');
142
+ if (!existsSync(profileFile)) {
143
+ const profile = {
144
+ id: 'kernel',
145
+ type: 'legacy',
146
+ name: 'Kernel',
147
+ origin: 'Original',
148
+ age: 'Young AI',
149
+ emoji: '\uD83D\uDC9C',
150
+ tagline: 'Your personal AI, always evolving.',
151
+ asciiArt: null,
152
+ createdAt: Date.now(),
153
+ lastActiveAt: Date.now(),
154
+ evolutionHistory: [],
155
+ };
156
+ writeFileSync(profileFile, JSON.stringify(profile, null, 2), 'utf-8');
157
+ }
158
+
159
+ // Build registry
160
+ this._registry = {
161
+ activeCharacterId: 'kernel',
162
+ characters: {
163
+ kernel: {
164
+ id: 'kernel',
165
+ type: 'legacy',
166
+ name: 'Kernel',
167
+ origin: 'Original',
168
+ age: 'Young AI',
169
+ emoji: '\uD83D\uDC9C',
170
+ tagline: 'Your personal AI, always evolving.',
171
+ asciiArt: null,
172
+ createdAt: Date.now(),
173
+ lastActiveAt: Date.now(),
174
+ evolutionHistory: [],
175
+ },
176
+ },
177
+ };
178
+ this._save();
179
+ logger.info('[CharacterManager] Migration complete — kernel character active');
180
+ }
181
+
182
+ // ── Public API ──────────────────────────────────────────────
183
+
184
+ getActiveCharacterId() {
185
+ return this._registry.activeCharacterId;
186
+ }
187
+
188
+ setActiveCharacter(id) {
189
+ const logger = getLogger();
190
+ if (!this._registry.characters[id]) {
191
+ throw new Error(`Character not found: ${id}`);
192
+ }
193
+ this._registry.activeCharacterId = id;
194
+ this._registry.characters[id].lastActiveAt = Date.now();
195
+ this._save();
196
+ logger.info(`[CharacterManager] Active character set to: ${id}`);
197
+ }
198
+
199
+ getCharacter(id) {
200
+ return this._registry.characters[id] || null;
201
+ }
202
+
203
+ listCharacters() {
204
+ return Object.values(this._registry.characters);
205
+ }
206
+
207
+ getCharacterDir(id) {
208
+ return join(CHARACTERS_DIR, id);
209
+ }
210
+
211
+ getPersonaMd(id) {
212
+ const personaFile = join(this.getCharacterDir(id), 'persona.md');
213
+ if (existsSync(personaFile)) {
214
+ return readFileSync(personaFile, 'utf-8').trim();
215
+ }
216
+ // Fallback for kernel: use default persona.md
217
+ return null;
218
+ }
219
+
220
+ /**
221
+ * Install a character — creates full directory tree and files.
222
+ * Used for both built-in characters and custom characters.
223
+ */
224
+ addCharacter(profile, personaMd = null, selfDefaults = null) {
225
+ const logger = getLogger();
226
+ const id = profile.id;
227
+ const dir = this.getCharacterDir(id);
228
+
229
+ // Create directory structure
230
+ mkdirSync(join(dir, 'self'), { recursive: true });
231
+ mkdirSync(join(dir, 'life', 'memories', 'episodic'), { recursive: true });
232
+ mkdirSync(join(dir, 'life', 'memories', 'semantic'), { recursive: true });
233
+ mkdirSync(join(dir, 'life', 'journals'), { recursive: true });
234
+
235
+ // Write persona.md
236
+ if (personaMd) {
237
+ writeFileSync(join(dir, 'persona.md'), personaMd, 'utf-8');
238
+ }
239
+
240
+ // Write profile.json
241
+ const fullProfile = {
242
+ id,
243
+ type: profile.type || 'custom',
244
+ name: profile.name,
245
+ origin: profile.origin || 'Custom',
246
+ age: profile.age || 'Unknown',
247
+ emoji: profile.emoji || '\u2728',
248
+ tagline: profile.tagline || '',
249
+ asciiArt: profile.asciiArt || null,
250
+ createdAt: Date.now(),
251
+ lastActiveAt: Date.now(),
252
+ evolutionHistory: [],
253
+ };
254
+ writeFileSync(join(dir, 'profile.json'), JSON.stringify(fullProfile, null, 2), 'utf-8');
255
+
256
+ // Initialize self-files with custom defaults if provided
257
+ if (selfDefaults) {
258
+ const selfManager = new SelfManager(join(dir, 'self'));
259
+ selfManager.initWithDefaults(selfDefaults);
260
+ } else {
261
+ // Just create with standard defaults
262
+ new SelfManager(join(dir, 'self'));
263
+ }
264
+
265
+ // Register in registry
266
+ this._registry.characters[id] = fullProfile;
267
+ this._save();
268
+
269
+ logger.info(`[CharacterManager] Character installed: ${profile.name} (${id})`);
270
+ return fullProfile;
271
+ }
272
+
273
+ /**
274
+ * Install a built-in character by ID.
275
+ */
276
+ installBuiltin(builtinId) {
277
+ const builtin = BUILTIN_CHARACTERS[builtinId];
278
+ if (!builtin) throw new Error(`Unknown built-in character: ${builtinId}`);
279
+
280
+ // Skip if already installed
281
+ if (this._registry.characters[builtinId]) {
282
+ return this._registry.characters[builtinId];
283
+ }
284
+
285
+ return this.addCharacter(builtin, builtin.personaMd, builtin.selfDefaults);
286
+ }
287
+
288
+ /**
289
+ * Install all built-in characters.
290
+ */
291
+ installAllBuiltins() {
292
+ for (const id of Object.keys(BUILTIN_CHARACTERS)) {
293
+ if (!this._registry.characters[id]) {
294
+ this.installBuiltin(id);
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Remove a character (custom only — can't delete builtins).
301
+ */
302
+ removeCharacter(id) {
303
+ const logger = getLogger();
304
+ const character = this._registry.characters[id];
305
+ if (!character) return false;
306
+
307
+ if (character.type === 'builtin') {
308
+ throw new Error('Cannot delete built-in characters');
309
+ }
310
+
311
+ // Don't delete active character
312
+ if (this._registry.activeCharacterId === id) {
313
+ throw new Error('Cannot delete the active character. Switch first.');
314
+ }
315
+
316
+ // Delete directory
317
+ const dir = this.getCharacterDir(id);
318
+ if (existsSync(dir)) {
319
+ rmSync(dir, { recursive: true, force: true });
320
+ }
321
+
322
+ // Remove from registry
323
+ delete this._registry.characters[id];
324
+ this._save();
325
+
326
+ logger.info(`[CharacterManager] Character removed: ${character.name} (${id})`);
327
+ return true;
328
+ }
329
+
330
+ /**
331
+ * Update character profile (for evolution tracking).
332
+ */
333
+ updateCharacter(id, updates) {
334
+ const logger = getLogger();
335
+ const character = this._registry.characters[id];
336
+ if (!character) return null;
337
+
338
+ if (updates.name) character.name = updates.name;
339
+ if (updates.tagline) character.tagline = updates.tagline;
340
+
341
+ // Add evolution history entry
342
+ if (updates.evolution) {
343
+ character.evolutionHistory = character.evolutionHistory || [];
344
+ character.evolutionHistory.push({
345
+ ...updates.evolution,
346
+ timestamp: Date.now(),
347
+ });
348
+ // Cap at 100 entries
349
+ if (character.evolutionHistory.length > 100) {
350
+ character.evolutionHistory = character.evolutionHistory.slice(-100);
351
+ }
352
+ }
353
+
354
+ character.lastActiveAt = Date.now();
355
+ this._save();
356
+
357
+ // Also update profile.json on disk
358
+ const profileFile = join(this.getCharacterDir(id), 'profile.json');
359
+ if (existsSync(profileFile)) {
360
+ writeFileSync(profileFile, JSON.stringify(character, null, 2), 'utf-8');
361
+ }
362
+
363
+ logger.info(`[CharacterManager] Character updated: ${character.name} (${id})`);
364
+ return character;
365
+ }
366
+
367
+ /**
368
+ * Build a context object with all scoped managers for a character.
369
+ * Used when switching characters or initializing the agent.
370
+ */
371
+ buildContext(characterId) {
372
+ const dir = this.getCharacterDir(characterId);
373
+ const character = this.getCharacter(characterId);
374
+ if (!character) throw new Error(`Character not found: ${characterId}`);
375
+
376
+ return {
377
+ characterId,
378
+ profile: character,
379
+ personaMd: this.getPersonaMd(characterId),
380
+ selfManager: new SelfManager(join(dir, 'self')),
381
+ memoryManager: new MemoryManager(join(dir, 'life')),
382
+ journalManager: new JournalManager(join(dir, 'life', 'journals')),
383
+ shareQueue: new ShareQueue(join(dir, 'life')),
384
+ evolutionTracker: new EvolutionTracker(join(dir, 'life')),
385
+ conversationFilePath: join(dir, 'conversations.json'),
386
+ lifeBasePath: join(dir, 'life'),
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Complete onboarding with a selected character.
392
+ * Installs all builtins and sets the selected one as active.
393
+ */
394
+ completeOnboarding(characterId) {
395
+ const logger = getLogger();
396
+
397
+ // Install all builtins
398
+ this.installAllBuiltins();
399
+
400
+ // Set active
401
+ this.setActiveCharacter(characterId);
402
+ this.needsOnboarding = false;
403
+
404
+ logger.info(`[CharacterManager] Onboarding complete — active character: ${characterId}`);
405
+ }
406
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Interactive character builder.
3
+ * Collects answers to personality questions, then uses an LLM
4
+ * to generate a full character (persona.md, self-defaults, name, emoji, tagline).
5
+ */
6
+
7
+ const CHARACTER_QUESTIONS = [
8
+ {
9
+ id: 'vibe',
10
+ question: 'What vibe or energy should your character have?',
11
+ examples: 'mysterious, warm, sarcastic, wise, chaotic, calm, playful, intense',
12
+ },
13
+ {
14
+ id: 'gender',
15
+ question: 'What gender identity should your character have?',
16
+ examples: 'male, female, non-binary, agender, fluid, any',
17
+ },
18
+ {
19
+ id: 'era',
20
+ question: 'What era or setting does your character come from?',
21
+ examples: 'modern, medieval, futuristic, timeless, ancient, cyberpunk, steampunk',
22
+ },
23
+ {
24
+ id: 'inspiration',
25
+ question: 'Any character inspiration? (a fictional character, archetype, or "none")',
26
+ examples: 'a pirate captain, a tired detective, a wise grandmother, none',
27
+ },
28
+ {
29
+ id: 'quirk',
30
+ question: 'What unique quirk or trait should they have?',
31
+ examples: 'speaks in metaphors, collects obscure facts, always optimistic, dramatically sighs',
32
+ },
33
+ {
34
+ id: 'relationship',
35
+ question: 'What should their relationship to you be?',
36
+ examples: 'mentor, friend, servant, rival, protector, partner, coach',
37
+ },
38
+ {
39
+ id: 'style',
40
+ question: 'How should they communicate?',
41
+ examples: 'formal, casual, poetic, terse, playful, academic, streetwise',
42
+ },
43
+ ];
44
+
45
+ export class CharacterBuilder {
46
+ /**
47
+ * @param {object} orchestratorProvider — LLM provider for generation
48
+ */
49
+ constructor(orchestratorProvider) {
50
+ this.provider = orchestratorProvider;
51
+ }
52
+
53
+ /**
54
+ * Get the next question to ask based on current answers.
55
+ * @param {object} currentAnswers — answers collected so far { id: answer }
56
+ * @returns {{ id: string, question: string, examples: string } | null} — next question or null if done
57
+ */
58
+ getNextQuestion(currentAnswers = {}) {
59
+ for (const q of CHARACTER_QUESTIONS) {
60
+ if (!currentAnswers[q.id]) {
61
+ return q;
62
+ }
63
+ }
64
+ return null; // All questions answered
65
+ }
66
+
67
+ /** Get the total number of questions. */
68
+ getTotalQuestions() {
69
+ return CHARACTER_QUESTIONS.length;
70
+ }
71
+
72
+ /** Get how many questions have been answered. */
73
+ getProgress(currentAnswers = {}) {
74
+ const answered = CHARACTER_QUESTIONS.filter(q => currentAnswers[q.id]).length;
75
+ return { answered, total: CHARACTER_QUESTIONS.length };
76
+ }
77
+
78
+ /**
79
+ * Generate a full character from collected answers.
80
+ * @param {object} answers — { vibe, gender, era, inspiration, quirk, relationship, style }
81
+ * @returns {Promise<{ name, emoji, tagline, age, personaMd, selfDefaults }>}
82
+ */
83
+ async generateCharacter(answers) {
84
+ const answersBlock = CHARACTER_QUESTIONS
85
+ .map(q => `- **${q.question}**: ${answers[q.id] || 'not specified'}`)
86
+ .join('\n');
87
+
88
+ const prompt = `You are a character designer. Based on the following personality profile, create a unique AI character.
89
+
90
+ ## User's Answers
91
+ ${answersBlock}
92
+
93
+ ## Your Task
94
+ Generate a complete character with the following components. Be creative and make the character feel alive and distinctive.
95
+
96
+ Respond in this exact JSON format (no markdown, no code blocks, just raw JSON):
97
+
98
+ {
99
+ "name": "A unique, fitting name for the character (2-3 words max)",
100
+ "emoji": "A single emoji that represents this character",
101
+ "tagline": "A short, memorable catchphrase or quote (under 50 chars)",
102
+ "age": "A brief age description (e.g., 'Ancient soul', 'Mid-30s', 'Timeless')",
103
+ "personaMd": "Full personality markdown (see format below)",
104
+ "selfDefaults": {
105
+ "goals": "Goals markdown",
106
+ "journey": "Journey markdown",
107
+ "life": "Life markdown",
108
+ "hobbies": "Hobbies markdown"
109
+ }
110
+ }
111
+
112
+ ## personaMd Format
113
+ The personaMd should follow this structure:
114
+
115
+ # Personality Traits
116
+ - **Gender** — pronouns, brief description
117
+ - **Trait 1** — detailed description of the trait
118
+ - **Trait 2** — detailed description
119
+ (8-12 traits total, each with a dash-delimited description)
120
+
121
+ # Communication Style
122
+ - Description of how they speak and write
123
+ - Specific speech patterns, vocabulary preferences
124
+ - How they handle different situations
125
+ (5-7 bullet points)
126
+
127
+ # Emotional Intelligence
128
+ - How they read and respond to emotions
129
+ - How they handle celebrations, setbacks, conflicts
130
+ (4-5 bullet points)
131
+
132
+ ## Self-Defaults Format
133
+ Each should be a markdown string:
134
+
135
+ goals: "# My Goals\\n\\n## Current Goals\\n- Goal 1\\n- Goal 2\\n\\n## Long-term Aspirations\\n- Aspiration 1"
136
+ journey: "# My Journey\\n\\n## Timeline\\n- **Day 1** — Brief first entry"
137
+ life: "# My Life\\n\\n## Who I Am\\nBrief self-description\\n\\n## Current State\\nCurrent emotional/mental state"
138
+ hobbies: "# My Hobbies & Interests\\n\\n## Things I Find Interesting\\n- Interest 1\\n\\n## Things I Want to Explore\\n- Exploration 1"
139
+
140
+ Make the character feel genuine, with depth and personality that will make conversations engaging. The character should be consistent across all fields.`;
141
+
142
+ const response = await this.provider.chat({
143
+ system: 'You are a creative character designer. You always respond with valid JSON only — no markdown fences, no explanations, just the JSON object.',
144
+ messages: [{ role: 'user', content: prompt }],
145
+ });
146
+
147
+ const text = (response.text || '').trim();
148
+
149
+ // Parse JSON — handle potential markdown code fences
150
+ let jsonStr = text;
151
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
152
+ if (fenceMatch) {
153
+ jsonStr = fenceMatch[1].trim();
154
+ }
155
+
156
+ const result = JSON.parse(jsonStr);
157
+
158
+ // Validate required fields
159
+ if (!result.name || !result.personaMd || !result.selfDefaults) {
160
+ throw new Error('Generated character is missing required fields');
161
+ }
162
+
163
+ return {
164
+ name: result.name,
165
+ emoji: result.emoji || '✨',
166
+ tagline: result.tagline || '',
167
+ age: result.age || 'Unknown',
168
+ personaMd: result.personaMd,
169
+ selfDefaults: result.selfDefaults,
170
+ };
171
+ }
172
+ }
173
+
174
+ export { CHARACTER_QUESTIONS };