@jungjaehoon/mama-core 1.0.1

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,174 @@
1
+ /**
2
+ * MAMA (Memory-Augmented MCP Architecture) - Memory Injection Hook
3
+ *
4
+ * UserPromptSubmit hook that injects decision history into Claude's context
5
+ * Tasks: 1.1-1.9 (Hook setup, timeout handling, context injection)
6
+ * AC #1: Query intent → Graph query → Format → Inject (5s timeout for LLM latency)
7
+ * AC #2: No history → null (graceful fallback)
8
+ * AC #3: Timeout → graceful fallback
9
+ *
10
+ * @module memory-inject
11
+ * @version 1.0
12
+ * @date 2025-11-14
13
+ */
14
+
15
+ // Error handling policy:
16
+ // - Timeout errors: thrown (caller handles retry/fallback)
17
+ // - Vector search unavailable: returns empty array (recoverable, not critical)
18
+ const { info, error: logError } = require('./debug-logger');
19
+ const { vectorSearch } = require('./memory-store');
20
+ const { formatContext } = require('./decision-formatter');
21
+
22
+ // Configuration
23
+ const TIMEOUT_MS = 5000; // LLM-based intent detection, user accepts longer thinking
24
+ const TOKEN_BUDGET = 500; // AC #1: Max 500 tokens per injection
25
+
26
+ /**
27
+ * UserPromptSubmit Hook Handler
28
+ *
29
+ * Task 1.1-1.9: Main entry point for memory injection
30
+ * AC #1, #2, #3: Intent analysis → Query → Format → Inject
31
+ *
32
+ * @param {string} userMessage - User's message from prompt
33
+ * @returns {Promise<string|null>} Injected context or null
34
+ */
35
+ async function injectDecisionContext(userMessage) {
36
+ const startTime = Date.now();
37
+ let timeoutId = null;
38
+
39
+ try {
40
+ // Task 1.3: Implement timeout wrapper (Promise.race with timeout)
41
+ const timeoutPromise = new Promise((_, reject) => {
42
+ timeoutId = setTimeout(() => {
43
+ reject(new Error(`Memory injection timeout (${TIMEOUT_MS}ms)`));
44
+ }, TIMEOUT_MS);
45
+ });
46
+
47
+ const context = await Promise.race([
48
+ performMemoryInjection(userMessage, startTime),
49
+ timeoutPromise,
50
+ ]);
51
+
52
+ // Clear timeout on success to prevent timer leak
53
+ if (timeoutId) {
54
+ clearTimeout(timeoutId);
55
+ }
56
+
57
+ return context;
58
+ } catch (error) {
59
+ // Clear timeout on error to prevent timer leak
60
+ if (timeoutId) {
61
+ clearTimeout(timeoutId);
62
+ }
63
+ // CLAUDE.md Rule #1: NO FALLBACK
64
+ // Errors must be thrown for debugging (including timeout)
65
+ logError(`[MAMA] Memory injection FAILED: ${error.message}`);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Perform memory injection with all steps
72
+ *
73
+ * Simplified: Direct vector search without LLM intent analysis
74
+ * Faster, more reliable, works with all query types
75
+ *
76
+ * @param {string} userMessage - User's message
77
+ * @param {number} startTime - Start timestamp for latency tracking
78
+ * @returns {Promise<string|null>} Formatted context or null
79
+ */
80
+ async function performMemoryInjection(userMessage, startTime) {
81
+ // 1. Generate query embedding
82
+ const { generateEmbedding } = require('./embeddings');
83
+ const queryEmbedding = await generateEmbedding(userMessage);
84
+
85
+ const embeddingLatency = Date.now() - startTime;
86
+ info(`[MAMA] Embedding generation: ${embeddingLatency}ms`);
87
+
88
+ // 2. Adaptive threshold (shorter queries need higher confidence)
89
+ const wordCount = userMessage.split(/\s+/).length;
90
+ const adaptiveThreshold = wordCount < 3 ? 0.7 : 0.6;
91
+
92
+ // 3. Vector search (returns [] on error for graceful degradation)
93
+ let results;
94
+ try {
95
+ results = await vectorSearch(queryEmbedding, 10, 0.5); // Get more candidates
96
+ } catch (error) {
97
+ // Vector search unavailability should not block the main conversation flow
98
+ logError(`[MAMA] Vector search failed: ${error.message}`);
99
+ return null;
100
+ }
101
+
102
+ // 4. Filter by adaptive threshold
103
+ results = results.filter((r) => r.similarity >= adaptiveThreshold);
104
+
105
+ const searchLatency = Date.now() - startTime;
106
+ info(
107
+ `[MAMA] Vector search: ${searchLatency - embeddingLatency}ms (${results.length} results, threshold: ${adaptiveThreshold})`
108
+ );
109
+
110
+ // 5. Check if we have any decisions
111
+ if (results.length === 0) {
112
+ // Redact user content for privacy - only log length, not content
113
+ info(`[MAMA] No relevant decisions found (query length: ${userMessage.length} chars)`);
114
+ return null;
115
+ }
116
+
117
+ // 6. Format context summary
118
+ const formattedContext = formatContext(results, {
119
+ maxTokens: TOKEN_BUDGET,
120
+ });
121
+
122
+ const formatLatency = Date.now() - startTime;
123
+ info(`[MAMA] Format context: ${formatLatency - searchLatency}ms (total: ${formatLatency}ms)`);
124
+
125
+ // 7. Return formatted context (Claude Code will inject it)
126
+ return formattedContext;
127
+ }
128
+
129
+ // Export API
130
+ module.exports = {
131
+ injectDecisionContext,
132
+ };
133
+
134
+ // CLI execution for testing
135
+ if (require.main === module) {
136
+ info('🧠 MAMA Memory Injection - Test\n');
137
+
138
+ // Test memory injection flow
139
+ (async () => {
140
+ const testMessages = [
141
+ 'Why did we choose COMPLEX mesh structure?',
142
+ 'Read the file please',
143
+ 'We chose JWT authentication, why?',
144
+ ];
145
+
146
+ for (const message of testMessages) {
147
+ info(`═══════════════════════════`);
148
+ info(`📋 Testing: "${message}"\n`);
149
+
150
+ try {
151
+ const startTime = Date.now();
152
+ const context = await injectDecisionContext(message);
153
+ const latency = Date.now() - startTime;
154
+
155
+ if (context) {
156
+ info('\n✅ Context injected:');
157
+ info(context);
158
+ info(`\n⏱️ Latency: ${latency}ms (target: <${TIMEOUT_MS}ms)`);
159
+ } else {
160
+ info('\n⚠️ No context injected (null returned)');
161
+ info(`⏱️ Latency: ${latency}ms`);
162
+ }
163
+ } catch (error) {
164
+ logError(`\n❌ Error: ${error.message}`);
165
+ }
166
+
167
+ info('');
168
+ }
169
+
170
+ info('═══════════════════════════');
171
+ info('✅ Memory injection tests complete');
172
+ info('═══════════════════════════');
173
+ })();
174
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * MAMA Memory Store - Compatibility Wrapper
3
+ *
4
+ * This file now serves as a compatibility layer that re-exports db-manager functions.
5
+ * The actual database logic has been moved to db-manager.js which supports both
6
+ * SQLite (local development) and PostgreSQL (Railway production).
7
+ *
8
+ * Migration Note:
9
+ * - Old: memory-store.js directly used better-sqlite3 + sqlite-vss
10
+ * - New: memory-store.js → db-manager.js → db-adapter (SQLite or PostgreSQL)
11
+ *
12
+ * All MAMA modules can continue to require('memory-store') without changes.
13
+ *
14
+ * @module memory-store
15
+ * @version 2.0
16
+ * @date 2025-11-17
17
+ */
18
+
19
+ const dbManager = require('./db-manager');
20
+
21
+ // Re-export all db-manager functions for backward compatibility
22
+ // These maintain the same interface as the original memory-store.js
23
+ module.exports = {
24
+ // Core database functions
25
+ initDB: dbManager.initDB, // Now async, returns Promise<connection>
26
+ getDB: dbManager.getDB, // Sync, throws if not initialized
27
+ getAdapter: dbManager.getAdapter, // Get database adapter (PostgreSQL or SQLite)
28
+ closeDB: dbManager.closeDB, // Async
29
+
30
+ // Vector search functions
31
+ insertEmbedding: dbManager.insertEmbedding, // Async
32
+ vectorSearch: dbManager.vectorSearch, // Async (returns null if unavailable)
33
+ queryVectorSearch: dbManager.queryVectorSearch, // Async - Story 014.14
34
+
35
+ // Decision functions
36
+ insertDecisionWithEmbedding: dbManager.insertDecisionWithEmbedding, // Async
37
+ queryDecisionGraph: dbManager.queryDecisionGraph, // Async
38
+ querySemanticEdges: dbManager.querySemanticEdges, // Async - Graph traversal
39
+ updateDecisionOutcome: dbManager.updateDecisionOutcome, // Async
40
+
41
+ // Compatibility functions
42
+ getPreparedStmt: dbManager.getPreparedStmt, // Deprecated
43
+ getDbPath: dbManager.getDbPath, // Returns adapter name
44
+
45
+ // Legacy exports (for backward compatibility with old code)
46
+ traverseDecisionChain: dbManager.queryDecisionGraph, // Alias
47
+ getSessionDecisions: async (sessionId) => {
48
+ // Fallback implementation
49
+ const adapter = dbManager.getAdapter();
50
+ const stmt = adapter.prepare(`
51
+ SELECT * FROM decisions
52
+ WHERE session_id = ?
53
+ ORDER BY created_at DESC
54
+ `);
55
+ return await stmt.all(sessionId);
56
+ },
57
+ incrementUsageSuccess: async (decisionId, timeSaved = 0) => {
58
+ const adapter = dbManager.getAdapter();
59
+ const stmt = adapter.prepare(`
60
+ UPDATE decisions
61
+ SET usage_success = usage_success + 1,
62
+ time_saved = time_saved + ?,
63
+ updated_at = ?
64
+ WHERE id = ?
65
+ `);
66
+ await stmt.run(timeSaved, Date.now(), decisionId);
67
+ },
68
+ incrementUsageFailure: async (decisionId) => {
69
+ const adapter = dbManager.getAdapter();
70
+ const stmt = adapter.prepare(`
71
+ UPDATE decisions
72
+ SET usage_failure = usage_failure + 1,
73
+ updated_at = ?
74
+ WHERE id = ?
75
+ `);
76
+ await stmt.run(Date.now(), decisionId);
77
+ },
78
+ getDecisionById: async (decisionId) => {
79
+ const adapter = dbManager.getAdapter();
80
+ const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ?');
81
+ return await stmt.get(decisionId);
82
+ },
83
+
84
+ // Path exports (for compatibility)
85
+ DB_PATH: process.env.MAMA_DATABASE_URL ? 'PostgreSQL' : 'SQLite',
86
+ DB_DIR: process.env.MAMA_DATABASE_URL ? 'PostgreSQL' : '~/.mama',
87
+ LEGACY_DB_PATH: '~/.spinelift/memories.db',
88
+ DEFAULT_DB_PATH: '~/.mama/memories.db',
89
+ };
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ notifyInsight: () => null,
3
+ };
@@ -0,0 +1,391 @@
1
+ /**
2
+ * MAMA (Memory-Augmented MCP Architecture) - Ollama Client Wrapper
3
+ *
4
+ * Simple wrapper for Ollama API with EXAONE 3.5 support
5
+ * Tasks: 9.2-9.5 (HTTP client, EXAONE wrapper, Error handling, Testing)
6
+ * AC #1: LLM integration ready
7
+ *
8
+ * @module ollama-client
9
+ * @version 1.0
10
+ * @date 2025-11-14
11
+ */
12
+
13
+ const { info, error: logError } = require('./debug-logger');
14
+ const http = require('http');
15
+
16
+ // Ollama configuration
17
+ const OLLAMA_HOST = process.env.OLLAMA_HOST || 'localhost';
18
+ const OLLAMA_PORT = process.env.OLLAMA_PORT || 11434;
19
+ const DEFAULT_MODEL = 'exaone3.5:2.4b';
20
+ const FALLBACK_MODEL = 'gemma:2b';
21
+
22
+ /**
23
+ * Call Ollama API
24
+ *
25
+ * Task 9.2: Implement HTTP client for Ollama API
26
+ * AC #1: LLM API callable
27
+ *
28
+ * @param {string} endpoint - API endpoint (e.g., '/api/generate')
29
+ * @param {Object} payload - Request payload
30
+ * @param {number} timeout - Timeout in milliseconds (default: 30000)
31
+ * @returns {Promise<Object>} API response
32
+ * @throws {Error} If request fails
33
+ */
34
+ function callOllamaAPI(endpoint, payload, timeout = 30000) {
35
+ return new Promise((resolve, reject) => {
36
+ const postData = JSON.stringify(payload);
37
+
38
+ const options = {
39
+ hostname: OLLAMA_HOST,
40
+ port: OLLAMA_PORT,
41
+ path: endpoint,
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ 'Content-Length': Buffer.byteLength(postData),
46
+ },
47
+ };
48
+
49
+ const req = http.request(options, (res) => {
50
+ let data = '';
51
+
52
+ res.on('data', (chunk) => {
53
+ data += chunk;
54
+ });
55
+
56
+ res.on('end', () => {
57
+ if (res.statusCode >= 200 && res.statusCode < 300) {
58
+ try {
59
+ // Ollama returns NDJSON (newline-delimited JSON)
60
+ // For non-streaming, we only get one line
61
+ const lines = data.trim().split('\n');
62
+ const response = JSON.parse(lines[lines.length - 1]);
63
+ resolve(response);
64
+ } catch (error) {
65
+ reject(new Error(`Failed to parse Ollama response: ${error.message}`));
66
+ }
67
+ } else {
68
+ reject(new Error(`Ollama API error: ${res.statusCode} - ${data}`));
69
+ }
70
+ });
71
+ });
72
+
73
+ req.on('error', (error) => {
74
+ reject(new Error(`Ollama connection failed: ${error.message}`));
75
+ });
76
+
77
+ req.setTimeout(timeout, () => {
78
+ req.destroy();
79
+ reject(new Error(`Ollama request timeout (${timeout}ms)`));
80
+ });
81
+
82
+ req.write(postData);
83
+ req.end();
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Generate text with EXAONE 3.5
89
+ *
90
+ * Task 9.3: Implement EXAONE wrapper (with fallback to Gemma)
91
+ * AC #1: Decision detection LLM ready
92
+ *
93
+ * @param {string} prompt - Input prompt
94
+ * @param {Object} options - Generation options
95
+ * @param {string} options.model - Model name (default: EXAONE 3.5)
96
+ * @param {string} options.format - Response format ('json' or null)
97
+ * @param {number} options.temperature - Temperature (default: 0.7)
98
+ * @param {number} options.max_tokens - Max tokens (default: 500)
99
+ * @returns {Promise<string|Object>} Generated text or JSON object
100
+ * @throws {Error} If generation fails
101
+ */
102
+ async function generate(prompt, options = {}) {
103
+ const { model = DEFAULT_MODEL, format = null, temperature = 0.7, max_tokens = 500 } = options;
104
+
105
+ const payload = {
106
+ model,
107
+ prompt,
108
+ stream: false,
109
+ options: {
110
+ temperature,
111
+ num_predict: max_tokens,
112
+ },
113
+ };
114
+
115
+ if (format === 'json') {
116
+ payload.format = 'json';
117
+ }
118
+
119
+ try {
120
+ const response = await callOllamaAPI('/api/generate', payload);
121
+
122
+ // Extract response text
123
+ const responseText = response.response;
124
+
125
+ // Parse JSON if requested
126
+ if (format === 'json') {
127
+ try {
128
+ return JSON.parse(responseText);
129
+ } catch (error) {
130
+ throw new Error(`Failed to parse JSON response: ${responseText}`);
131
+ }
132
+ }
133
+
134
+ return responseText;
135
+ } catch (error) {
136
+ // Task 9.4: Try fallback model if EXAONE fails
137
+ // Match Ollama's specific "model 'xxx' not found" error pattern
138
+ const isModelNotFound =
139
+ /model ['"].*['"] not found/i.test(error.message) ||
140
+ (error.message.includes('404') && error.message.toLowerCase().includes('not found'));
141
+ if (model === DEFAULT_MODEL && isModelNotFound) {
142
+ console.warn(`[MAMA] EXAONE not found, trying fallback (${FALLBACK_MODEL})...`);
143
+
144
+ return generate(prompt, {
145
+ ...options,
146
+ model: FALLBACK_MODEL,
147
+ });
148
+ }
149
+
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Analyze decision from tool execution
156
+ *
157
+ * Wrapper for decision detection (used in Story 014.7.3)
158
+ *
159
+ * @param {Object} toolExecution - Tool execution data
160
+ * @param {Object} sessionContext - Session context
161
+ * @returns {Promise<Object>} Decision analysis result
162
+ */
163
+ async function analyzeDecision(toolExecution, sessionContext) {
164
+ const prompt = `
165
+ Analyze if this represents a DECISION (not just an action):
166
+
167
+ Session Context:
168
+ - Latest User Message: ${sessionContext.latest_user_message || 'N/A'}
169
+ - Recent Exchange: ${sessionContext.recent_exchange || 'N/A'}
170
+
171
+ Tool Execution:
172
+ - Tool: ${toolExecution.tool_name}
173
+ - Input: ${JSON.stringify(toolExecution.tool_input)}
174
+ - Result: ${toolExecution.exit_code === 0 ? 'SUCCESS' : 'FAILED'}
175
+
176
+ Decision Indicators:
177
+ 1. User explicitly chose between alternatives?
178
+ Example: "Let's use JWT" (not "Use JWT" - that's just action)
179
+
180
+ 2. User changed previous approach?
181
+ Example: "Complex → Simple approach"
182
+
183
+ 3. User expressed preference?
184
+ Example: "Let's do it this way from now", "This approach is better"
185
+
186
+ 4. Significant architectural choice?
187
+ Example: "Mesh structure: COMPLEX", "Authentication: JWT"
188
+
189
+ Is this a DECISION? Return JSON with "topic" as a short snake_case identifier:
190
+ {
191
+ "is_decision": boolean,
192
+ "topic": string or null (extract main technical topic in snake_case, e.g., "mesh_structure", "database_choice", "auth_strategy"),
193
+ "decision": string or null (the actual choice made, e.g., "COMPLEX", "PostgreSQL", "JWT"),
194
+ "reasoning": "Why this is/isn't a decision",
195
+ "confidence": 0.0-1.0
196
+ }
197
+
198
+ IMPORTANT: Generate "topic" freely based on context. Do NOT limit to predefined values.
199
+ `;
200
+
201
+ try {
202
+ const response = await generate(prompt, {
203
+ format: 'json',
204
+ temperature: 0.3, // Lower temperature for structured output
205
+ max_tokens: 300,
206
+ });
207
+
208
+ return response;
209
+ } catch (error) {
210
+ // CLAUDE.md Rule #1: NO FALLBACK
211
+ // Errors must be thrown for debugging
212
+ logError(`[MAMA] Decision analysis FAILED: ${error.message}`);
213
+ throw new Error(`Decision analysis failed: ${error.message}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Analyze query intent
219
+ *
220
+ * Wrapper for query intent detection (used in Story 014.7.2)
221
+ *
222
+ * @param {string} userMessage - User's message
223
+ * @returns {Promise<Object>} Query intent analysis
224
+ */
225
+ async function analyzeQueryIntent(userMessage) {
226
+ const prompt = `
227
+ Analyze this user message to determine if it involves past decisions:
228
+
229
+ User Message: "${userMessage}"
230
+
231
+ Questions to answer:
232
+ 1. Does this query reference past decisions or choices?
233
+ 2. Is the user asking about previous approaches?
234
+ 3. What topic is being discussed? (e.g., "mesh_structure", "authentication", "testing")
235
+
236
+ Return JSON:
237
+ {
238
+ "involves_decision": boolean,
239
+ "topic": "topic_name" | null,
240
+ "query_type": "recall" | "evolution" | "none",
241
+ "reasoning": "Why this involves/doesn't involve decisions"
242
+ }
243
+ `;
244
+
245
+ try {
246
+ const response = await generate(prompt, {
247
+ format: 'json',
248
+ temperature: 0.3,
249
+ max_tokens: 200,
250
+ });
251
+
252
+ return response;
253
+ } catch (error) {
254
+ // CLAUDE.md Rule #1: NO FALLBACK
255
+ // Errors must be thrown for debugging
256
+ logError(`[MAMA] Query intent analysis FAILED: ${error.message}`);
257
+ throw new Error(`Query intent analysis failed: ${error.message}`);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Check if Ollama is available
263
+ *
264
+ * Utility for health checks
265
+ *
266
+ * @returns {Promise<boolean>} True if Ollama is accessible
267
+ */
268
+ async function isAvailable() {
269
+ return new Promise((resolve) => {
270
+ const options = {
271
+ hostname: OLLAMA_HOST,
272
+ port: OLLAMA_PORT,
273
+ path: '/api/tags',
274
+ method: 'GET', // Use GET for health check
275
+ timeout: 2000,
276
+ };
277
+
278
+ const req = http.request(options, (res) => {
279
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
280
+ });
281
+
282
+ req.on('error', () => resolve(false));
283
+ req.on('timeout', () => {
284
+ req.destroy();
285
+ resolve(false);
286
+ });
287
+
288
+ req.end();
289
+ });
290
+ }
291
+
292
+ /**
293
+ * List available models
294
+ *
295
+ * Utility for setup script
296
+ *
297
+ * @returns {Promise<Array<string>>} Array of model names
298
+ */
299
+ async function listModels() {
300
+ return new Promise((resolve, reject) => {
301
+ const options = {
302
+ hostname: OLLAMA_HOST,
303
+ port: OLLAMA_PORT,
304
+ path: '/api/tags',
305
+ method: 'GET', // Use GET for listing models
306
+ timeout: 5000,
307
+ };
308
+
309
+ const req = http.request(options, (res) => {
310
+ let data = '';
311
+
312
+ res.on('data', (chunk) => {
313
+ data += chunk;
314
+ });
315
+
316
+ res.on('end', () => {
317
+ try {
318
+ const response = JSON.parse(data);
319
+ resolve(response.models?.map((m) => m.name) || []);
320
+ } catch (error) {
321
+ reject(new Error(`Failed to parse models response: ${error.message}`));
322
+ }
323
+ });
324
+ });
325
+
326
+ req.on('error', (error) => {
327
+ reject(new Error(`Failed to list models: ${error.message}`));
328
+ });
329
+
330
+ req.on('timeout', () => {
331
+ req.destroy();
332
+ reject(new Error('List models timeout'));
333
+ });
334
+
335
+ req.end();
336
+ });
337
+ }
338
+
339
+ // Export API
340
+ module.exports = {
341
+ generate,
342
+ analyzeDecision,
343
+ analyzeQueryIntent,
344
+ isAvailable,
345
+ listModels,
346
+ DEFAULT_MODEL,
347
+ FALLBACK_MODEL,
348
+ };
349
+
350
+ // CLI execution for testing
351
+ if (require.main === module) {
352
+ info('🧠 MAMA Ollama Client - Test\n');
353
+
354
+ // Task 9.5: Test Ollama connection and generation
355
+ (async () => {
356
+ try {
357
+ info('📋 Test 1: Check Ollama availability...');
358
+ const available = await isAvailable();
359
+ if (!available) {
360
+ throw new Error('Ollama is not available');
361
+ }
362
+ info('✅ Ollama is available\n');
363
+
364
+ info('📋 Test 2: List available models...');
365
+ const models = await listModels();
366
+ info(`✅ Found ${models.length} models:`, models.join(', '), '\n');
367
+
368
+ info('📋 Test 3: Generate text...');
369
+ const text = await generate('What is 2+2?', {
370
+ temperature: 0.1,
371
+ max_tokens: 50,
372
+ });
373
+ info(`✅ Generated: ${text.trim()}\n`);
374
+
375
+ info('📋 Test 4: Generate JSON...');
376
+ const json = await generate('Return {"test": true, "value": 42}', {
377
+ format: 'json',
378
+ temperature: 0.1,
379
+ max_tokens: 50,
380
+ });
381
+ info('✅ Generated JSON:', json, '\n');
382
+
383
+ info('═══════════════════════════');
384
+ info('✅ All tests passed!');
385
+ info('═══════════════════════════');
386
+ } catch (error) {
387
+ logError(`❌ Test failed: ${error.message}`);
388
+ process.exit(1);
389
+ }
390
+ })();
391
+ }