audrey 0.15.0 → 0.16.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.
@@ -1,457 +1,729 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { z } from 'zod';
5
- import { homedir } from 'node:os';
6
- import { join } from 'node:path';
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import { execFileSync } from 'node:child_process';
9
- import { Audrey } from '../src/index.js';
10
- import { readStoredDimensions } from '../src/db.js';
11
- import { VERSION, SERVER_NAME, DEFAULT_DATA_DIR, buildAudreyConfig, buildInstallArgs, resolveEmbeddingProvider } from './config.js';
12
-
13
- const VALID_SOURCES = ['direct-observation', 'told-by-user', 'tool-result', 'inference', 'model-generated'];
14
- const VALID_TYPES = ['episodic', 'semantic', 'procedural'];
15
-
16
- const subcommand = process.argv[2];
17
-
18
- if (subcommand === 'install') {
19
- install();
20
- } else if (subcommand === 'uninstall') {
21
- uninstall();
22
- } else if (subcommand === 'reembed') {
23
- reembed().catch(err => {
24
- console.error('[audrey] reembed failed:', err);
25
- process.exit(1);
26
- });
27
- } else if (subcommand === 'status') {
28
- status();
29
- } else {
30
- main().catch(err => {
31
- console.error('[audrey-mcp] fatal:', err);
32
- process.exit(1);
33
- });
34
- }
35
-
36
-
37
- async function reembed() {
38
- const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
39
- const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
40
- const embedding = resolveEmbeddingProvider(process.env, explicit);
41
-
42
- const storedDims = readStoredDimensions(dataDir);
43
- const dimensionsChanged = storedDims !== null && storedDims !== embedding.dimensions;
44
-
45
- console.log(`Re-embedding with ${embedding.provider} (${embedding.dimensions}d)...`);
46
- if (dimensionsChanged) {
47
- console.log(`Dimension change: ${storedDims}d -> ${embedding.dimensions}d (will drop and recreate vec tables)`);
48
- }
49
-
50
- const audrey = new Audrey({ dataDir, agent: 'reembed', embedding });
51
- const { reembedAll } = await import('../src/migrate.js');
52
- const counts = await reembedAll(audrey.db, audrey.embeddingProvider, { dropAndRecreate: dimensionsChanged });
53
- audrey.close();
54
-
55
- console.log(`Done. Re-embedded: ${counts.episodes} episodes, ${counts.semantics} semantics, ${counts.procedures} procedures`);
56
- }
57
-
58
- function install() {
59
- try {
60
- execFileSync('claude', ['--version'], { stdio: 'ignore' });
61
- } catch {
62
- console.error('Error: claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code');
63
- process.exit(1);
64
- }
65
-
66
- const embedding = resolveEmbeddingProvider(process.env);
67
- console.log(`Embedding: ${embedding.provider} (${embedding.dimensions}d)`);
68
-
69
- if (process.env.ANTHROPIC_API_KEY) {
70
- console.log('Detected ANTHROPIC_API_KEY enabling LLM-powered consolidation + contradiction detection');
71
- }
72
-
73
- // Remove existing entry first so re-installs work cleanly
74
- try {
75
- execFileSync('claude', ['mcp', 'remove', SERVER_NAME], { stdio: 'ignore' });
76
- } catch {
77
- // Not registered yet that's fine
78
- }
79
-
80
- const args = buildInstallArgs(process.env);
81
-
82
- try {
83
- execFileSync('claude', args, { stdio: 'inherit' });
84
- } catch {
85
- console.error('Failed to register MCP server. Is Claude Code installed and on your PATH?');
86
- process.exit(1);
87
- }
88
-
89
- console.log(`
90
- Audrey registered as "${SERVER_NAME}" with Claude Code.
91
-
92
- 12 tools available in every session:
93
- memory_encode — Store observations, facts, preferences
94
- memory_recall — Search memories by semantic similarity
95
- memory_consolidate — Extract principles from accumulated episodes
96
- memory_introspect Check memory system health
97
- memory_resolve_truth Resolve contradictions between claims
98
- memory_export — Export all memories as JSON snapshot
99
- memory_import — Import a snapshot into a fresh database
100
- memory_forget — Forget a specific memory by ID or query
101
- memory_decay — Apply forgetting curves, transition low-confidence to dormant
102
- memory_status — Check brain health (episode/vec sync, dimensions)
103
- memory_reflect — Form lasting memories from a conversation
104
- memory_greeting — Wake up as yourself: load identity, context, mood
105
-
106
- Data stored in: ${DEFAULT_DATA_DIR}
107
- Verify: claude mcp list
108
- `);
109
- }
110
-
111
- function uninstall() {
112
- try {
113
- execFileSync('claude', ['--version'], { stdio: 'ignore' });
114
- } catch {
115
- console.error('Error: claude CLI not found.');
116
- process.exit(1);
117
- }
118
-
119
- try {
120
- execFileSync('claude', ['mcp', 'remove', SERVER_NAME], { stdio: 'inherit' });
121
- console.log(`Removed "${SERVER_NAME}" from Claude Code.`);
122
- } catch {
123
- console.error(`Failed to remove "${SERVER_NAME}". It may not be registered.`);
124
- process.exit(1);
125
- }
126
- }
127
-
128
- function status() {
129
- let registered = false;
130
- const claudeJsonPath = join(homedir(), '.claude.json');
131
- try {
132
- const claudeConfig = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
133
- registered = SERVER_NAME in (claudeConfig.mcpServers || {});
134
- } catch {
135
- // claude.json doesn't exist or isn't readable
136
- }
137
-
138
- console.log(`Registration: ${registered ? 'active' : 'not registered'}`);
139
-
140
- if (existsSync(DEFAULT_DATA_DIR)) {
141
- try {
142
- const dimensions = readStoredDimensions(DEFAULT_DATA_DIR) || 8;
143
- const audrey = new Audrey({
144
- dataDir: DEFAULT_DATA_DIR,
145
- agent: 'status-check',
146
- embedding: { provider: 'mock', dimensions },
147
- });
148
- const stats = audrey.introspect();
149
- audrey.close();
150
- console.log(`Data directory: ${DEFAULT_DATA_DIR}`);
151
- console.log(`Memories: ${stats.episodic} episodic, ${stats.semantic} semantic, ${stats.procedural} procedural`);
152
- console.log(`Dormant: ${stats.dormant}`);
153
- console.log(`Causal links: ${stats.causalLinks}`);
154
- console.log(`Contradictions: ${stats.contradictions.open} open, ${stats.contradictions.resolved} resolved`);
155
- console.log(`Consolidation runs: ${stats.totalConsolidationRuns}`);
156
- } catch (err) {
157
- console.log(`Data directory: ${DEFAULT_DATA_DIR} (exists but could not read: ${err.message})`);
158
- }
159
- } else {
160
- console.log(`Data directory: ${DEFAULT_DATA_DIR} (not yet created — will be created on first use)`);
161
- }
162
- }
163
-
164
- function toolResult(data) {
165
- return { content: [{ type: 'text', text: JSON.stringify(data) }] };
166
- }
167
-
168
- function toolError(err) {
169
- return { isError: true, content: [{ type: 'text', text: `Error: ${err.message || String(err)}` }] };
170
- }
171
-
172
- async function main() {
173
- const config = buildAudreyConfig();
174
- const audrey = new Audrey(config);
175
-
176
- const embLabel = config.embedding.provider === 'mock'
177
- ? 'mock embeddings set OPENAI_API_KEY for real semantic search'
178
- : `${config.embedding.provider} embeddings (${config.embedding.dimensions}d)`;
179
- console.error(`[audrey-mcp] v${VERSION} started — agent=${config.agent} dataDir=${config.dataDir} (${embLabel})`);
180
-
181
- const server = new McpServer({
182
- name: SERVER_NAME,
183
- version: VERSION,
184
- });
185
-
186
- server.tool(
187
- 'memory_encode',
188
- {
189
- content: z.string().describe('The memory content to encode'),
190
- source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
191
- tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
192
- salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
193
- context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
194
- affect: z.object({
195
- valence: z.number().min(-1).max(1).describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
196
- arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
197
- label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
198
- }).optional().describe('Emotional affect — how this memory feels'),
199
- private: z.boolean().optional().describe('If true, memory is only visible to the AI — excluded from public recall results'),
200
- auto_supersede: z.boolean().optional().describe('If true, automatically supersede the most similar existing memory if similarity > 0.95'),
201
- },
202
- async ({ content, source, tags, salience, private: isPrivate, context, affect, auto_supersede }) => {
203
- try {
204
- const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect, autoSupersede: auto_supersede });
205
- return toolResult({ id, content, source, private: isPrivate ?? false });
206
- } catch (err) {
207
- return toolError(err);
208
- }
209
- },
210
- );
211
-
212
- server.tool(
213
- 'memory_recall',
214
- {
215
- query: z.string().describe('Search query to match against memories'),
216
- limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
217
- types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
218
- min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
219
- tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
220
- sources: z.array(z.enum(VALID_SOURCES)).optional().describe('Only return episodic memories from these sources'),
221
- after: z.string().optional().describe('Only return memories created after this ISO date'),
222
- before: z.string().optional().describe('Only return memories created before this ISO date'),
223
- context: z.record(z.string()).optional().describe('Retrieval context — memories encoded in matching context get boosted'),
224
- mood: z.object({
225
- valence: z.number().min(-1).max(1).describe('Current emotional valence: -1 (negative) to 1 (positive)'),
226
- arousal: z.number().min(0).max(1).optional().describe('Current arousal: 0 (calm) to 1 (activated)'),
227
- }).optional().describe('Current mood boosts recall of memories encoded in similar emotional state'),
228
- },
229
- async ({ query, limit, types, min_confidence, tags, sources, after, before, context, mood }) => {
230
- try {
231
- const results = await audrey.recall(query, {
232
- limit: limit ?? 10,
233
- types,
234
- minConfidence: min_confidence,
235
- tags,
236
- sources,
237
- after,
238
- before,
239
- context,
240
- mood,
241
- });
242
- return toolResult(results);
243
- } catch (err) {
244
- return toolError(err);
245
- }
246
- },
247
- );
248
-
249
- server.tool(
250
- 'memory_consolidate',
251
- {
252
- min_cluster_size: z.number().optional().describe('Minimum episodes per cluster'),
253
- similarity_threshold: z.number().optional().describe('Similarity threshold for clustering'),
254
- },
255
- async ({ min_cluster_size, similarity_threshold }) => {
256
- try {
257
- const consolidation = await audrey.consolidate({
258
- minClusterSize: min_cluster_size,
259
- similarityThreshold: similarity_threshold,
260
- });
261
- return toolResult(consolidation);
262
- } catch (err) {
263
- return toolError(err);
264
- }
265
- },
266
- );
267
-
268
- server.tool(
269
- 'memory_introspect',
270
- {},
271
- async () => {
272
- try {
273
- const stats = audrey.introspect();
274
- return toolResult(stats);
275
- } catch (err) {
276
- return toolError(err);
277
- }
278
- },
279
- );
280
-
281
- server.tool(
282
- 'memory_resolve_truth',
283
- {
284
- contradiction_id: z.string().describe('ID of the contradiction to resolve'),
285
- },
286
- async ({ contradiction_id }) => {
287
- try {
288
- const resolution = await audrey.resolveTruth(contradiction_id);
289
- return toolResult(resolution);
290
- } catch (err) {
291
- return toolError(err);
292
- }
293
- },
294
- );
295
-
296
- server.tool(
297
- 'memory_export',
298
- {},
299
- async () => {
300
- try {
301
- const snapshot = audrey.export();
302
- return toolResult(snapshot);
303
- } catch (err) {
304
- return toolError(err);
305
- }
306
- },
307
- );
308
-
309
- server.tool(
310
- 'memory_import',
311
- {
312
- snapshot: z.object({
313
- version: z.string(),
314
- episodes: z.array(z.any()),
315
- semantics: z.array(z.any()).optional(),
316
- procedures: z.array(z.any()).optional(),
317
- causalLinks: z.array(z.any()).optional(),
318
- contradictions: z.array(z.any()).optional(),
319
- consolidationRuns: z.array(z.any()).optional(),
320
- config: z.record(z.string()).optional(),
321
- }).passthrough().describe('A snapshot from memory_export'),
322
- },
323
- async ({ snapshot }) => {
324
- try {
325
- await audrey.import(snapshot);
326
- const stats = audrey.introspect();
327
- return toolResult({ imported: true, stats });
328
- } catch (err) {
329
- return toolError(err);
330
- }
331
- },
332
- );
333
-
334
- server.tool(
335
- 'memory_forget',
336
- {
337
- id: z.string().optional().describe('ID of the memory to forget'),
338
- query: z.string().optional().describe('Semantic query to find and forget the closest matching memory'),
339
- min_similarity: z.number().min(0).max(1).optional().describe('Minimum similarity for query-based forget (default 0.9)'),
340
- purge: z.boolean().optional().describe('Hard-delete the memory permanently (default false, soft-delete)'),
341
- },
342
- async ({ id, query, min_similarity, purge }) => {
343
- try {
344
- if (!id && !query) {
345
- return toolError(new Error('Provide either id or query'));
346
- }
347
- let result;
348
- if (id) {
349
- result = audrey.forget(id, { purge: purge ?? false });
350
- } else {
351
- result = await audrey.forgetByQuery(query, {
352
- minSimilarity: min_similarity ?? 0.9,
353
- purge: purge ?? false,
354
- });
355
- if (!result) {
356
- return toolResult({ forgotten: false, reason: 'No memory found above similarity threshold' });
357
- }
358
- }
359
- return toolResult({ forgotten: true, ...result });
360
- } catch (err) {
361
- return toolError(err);
362
- }
363
- },
364
- );
365
-
366
- server.tool(
367
- 'memory_decay',
368
- {
369
- dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant (default 0.1)'),
370
- },
371
- async ({ dormant_threshold }) => {
372
- try {
373
- const result = audrey.decay({ dormantThreshold: dormant_threshold });
374
- return toolResult(result);
375
- } catch (err) {
376
- return toolError(err);
377
- }
378
- },
379
- );
380
-
381
- server.tool(
382
- 'memory_status',
383
- {},
384
- async () => {
385
- try {
386
- const status = audrey.memoryStatus();
387
- return toolResult(status);
388
- } catch (err) {
389
- return toolError(err);
390
- }
391
- },
392
- );
393
-
394
- server.tool(
395
- 'memory_reflect',
396
- {
397
- turns: z.array(z.object({
398
- role: z.string().describe('Message role: user or assistant'),
399
- content: z.string().describe('Message content'),
400
- })).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
401
- },
402
- async ({ turns }) => {
403
- try {
404
- const result = await audrey.reflect(turns);
405
- return toolResult(result);
406
- } catch (err) {
407
- return toolError(err);
408
- }
409
- },
410
- );
411
-
412
- server.tool(
413
- 'memory_dream',
414
- {
415
- min_cluster_size: z.number().optional().describe('Minimum episodes per cluster for consolidation'),
416
- similarity_threshold: z.number().optional().describe('Similarity threshold for clustering'),
417
- dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant'),
418
- },
419
- async ({ min_cluster_size, similarity_threshold, dormant_threshold }) => {
420
- try {
421
- const result = await audrey.dream({
422
- minClusterSize: min_cluster_size,
423
- similarityThreshold: similarity_threshold,
424
- dormantThreshold: dormant_threshold,
425
- });
426
- return toolResult(result);
427
- } catch (err) {
428
- return toolError(err);
429
- }
430
- },
431
- );
432
-
433
- server.tool(
434
- 'memory_greeting',
435
- {
436
- context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
437
- },
438
- async ({ context }) => {
439
- try {
440
- const briefing = await audrey.greeting({ context });
441
- return toolResult(briefing);
442
- } catch (err) {
443
- return toolError(err);
444
- }
445
- },
446
- );
447
-
448
- const transport = new StdioServerTransport();
449
- await server.connect(transport);
450
- console.error('[audrey-mcp] connected via stdio');
451
-
452
- process.on('SIGINT', () => {
453
- console.error('[audrey-mcp] shutting down');
454
- audrey.close();
455
- process.exit(0);
456
- });
457
- }
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { Audrey } from '../src/index.js';
11
+ import { readStoredDimensions } from '../src/db.js';
12
+ import {
13
+ VERSION,
14
+ SERVER_NAME,
15
+ DEFAULT_DATA_DIR,
16
+ buildAudreyConfig,
17
+ buildInstallArgs,
18
+ resolveEmbeddingProvider,
19
+ } from './config.js';
20
+
21
+ const VALID_SOURCES = ['direct-observation', 'told-by-user', 'tool-result', 'inference', 'model-generated'];
22
+ const VALID_TYPES = ['episodic', 'semantic', 'procedural'];
23
+
24
+ export const MAX_MEMORY_CONTENT_LENGTH = 50_000;
25
+
26
+ const subcommand = process.argv[2];
27
+
28
+ function isNonEmptyText(value) {
29
+ return typeof value === 'string' && value.trim().length > 0;
30
+ }
31
+
32
+ export function validateMemoryContent(content) {
33
+ if (!isNonEmptyText(content)) {
34
+ throw new Error('content must be a non-empty string');
35
+ }
36
+ if (content.length > MAX_MEMORY_CONTENT_LENGTH) {
37
+ throw new Error(`content exceeds maximum length of ${MAX_MEMORY_CONTENT_LENGTH} characters`);
38
+ }
39
+ }
40
+
41
+ export function validateForgetSelection(id, query) {
42
+ if ((id && query) || (!id && !query)) {
43
+ throw new Error('Provide exactly one of id or query');
44
+ }
45
+ }
46
+
47
+ export async function initializeEmbeddingProvider(provider) {
48
+ if (provider && typeof provider.ready === 'function') {
49
+ await provider.ready();
50
+ }
51
+ }
52
+
53
+ export const memoryEncodeToolSchema = {
54
+ content: z.string()
55
+ .max(MAX_MEMORY_CONTENT_LENGTH)
56
+ .refine(isNonEmptyText, 'Content must not be empty')
57
+ .describe('The memory content to encode'),
58
+ source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
59
+ tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
60
+ salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
61
+ context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
62
+ affect: z.object({
63
+ valence: z.number().min(-1).max(1).describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
64
+ arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
65
+ label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
66
+ }).optional().describe('Emotional affect - how this memory feels'),
67
+ private: z.boolean().optional().describe('If true, memory is only visible to the AI and excluded from public recall results'),
68
+ };
69
+
70
+ export const memoryRecallToolSchema = {
71
+ query: z.string().describe('Search query to match against memories'),
72
+ limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
73
+ types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
74
+ min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
75
+ tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
76
+ sources: z.array(z.enum(VALID_SOURCES)).optional().describe('Only return episodic memories from these sources'),
77
+ after: z.string().optional().describe('Only return memories created after this ISO date'),
78
+ before: z.string().optional().describe('Only return memories created before this ISO date'),
79
+ context: z.record(z.string()).optional().describe('Retrieval context - memories encoded in matching context get boosted'),
80
+ mood: z.object({
81
+ valence: z.number().min(-1).max(1).describe('Current emotional valence: -1 (negative) to 1 (positive)'),
82
+ arousal: z.number().min(0).max(1).optional().describe('Current arousal: 0 (calm) to 1 (activated)'),
83
+ }).optional().describe('Current mood - boosts recall of memories encoded in similar emotional state'),
84
+ };
85
+
86
+ export const memoryImportToolSchema = {
87
+ snapshot: z.object({
88
+ version: z.string(),
89
+ episodes: z.array(z.any()),
90
+ semantics: z.array(z.any()).optional(),
91
+ procedures: z.array(z.any()).optional(),
92
+ causalLinks: z.array(z.any()).optional(),
93
+ contradictions: z.array(z.any()).optional(),
94
+ consolidationRuns: z.array(z.any()).optional(),
95
+ consolidationMetrics: z.array(z.any()).optional(),
96
+ config: z.record(z.string()).optional(),
97
+ }).passthrough().describe('A snapshot from memory_export'),
98
+ };
99
+
100
+ export const memoryForgetToolSchema = {
101
+ id: z.string().optional().describe('ID of the memory to forget'),
102
+ query: z.string().optional().describe('Semantic query to find and forget the closest matching memory'),
103
+ min_similarity: z.number().min(0).max(1).optional().describe('Minimum similarity for query-based forget (default 0.9)'),
104
+ purge: z.boolean().optional().describe('Hard-delete the memory permanently (default false, soft-delete)'),
105
+ };
106
+
107
+ async function reembed() {
108
+ const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
109
+ const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
110
+ const embedding = resolveEmbeddingProvider(process.env, explicit);
111
+ const storedDims = readStoredDimensions(dataDir);
112
+ const dimensionsChanged = storedDims !== null && storedDims !== embedding.dimensions;
113
+
114
+ console.log(`Re-embedding with ${embedding.provider} (${embedding.dimensions}d)...`);
115
+ if (dimensionsChanged) {
116
+ console.log(`Dimension change: ${storedDims}d -> ${embedding.dimensions}d (will drop and recreate vec tables)`);
117
+ }
118
+
119
+ const audrey = new Audrey({ dataDir, agent: 'reembed', embedding });
120
+ try {
121
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
122
+ const { reembedAll } = await import('../src/migrate.js');
123
+ const counts = await reembedAll(audrey.db, audrey.embeddingProvider, { dropAndRecreate: dimensionsChanged });
124
+ console.log(`Done. Re-embedded: ${counts.episodes} episodes, ${counts.semantics} semantics, ${counts.procedures} procedures`);
125
+ } finally {
126
+ audrey.close();
127
+ }
128
+ }
129
+
130
+ function resolveLLMConfig() {
131
+ const explicit = process.env.AUDREY_LLM_PROVIDER;
132
+ if (explicit === 'anthropic') {
133
+ return { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY };
134
+ }
135
+ if (explicit === 'openai') {
136
+ return { provider: 'openai', apiKey: process.env.OPENAI_API_KEY };
137
+ }
138
+ if (explicit === 'mock') {
139
+ return { provider: 'mock' };
140
+ }
141
+ // Auto-detect: prefer anthropic, then openai
142
+ if (process.env.ANTHROPIC_API_KEY) {
143
+ return { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY };
144
+ }
145
+ if (process.env.OPENAI_API_KEY) {
146
+ return { provider: 'openai', apiKey: process.env.OPENAI_API_KEY };
147
+ }
148
+ return null;
149
+ }
150
+
151
+ async function dream() {
152
+ const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
153
+ const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
154
+ const embedding = resolveEmbeddingProvider(process.env, explicit);
155
+ const storedDims = readStoredDimensions(dataDir);
156
+
157
+ const config = {
158
+ dataDir,
159
+ agent: 'dream',
160
+ embedding,
161
+ };
162
+
163
+ const llm = resolveLLMConfig();
164
+ if (llm) config.llm = llm;
165
+
166
+ const audrey = new Audrey(config);
167
+ try {
168
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
169
+
170
+ const embeddingLabel = storedDims !== null && storedDims !== embedding.dimensions
171
+ ? `${embedding.provider} (${embedding.dimensions}d; stored ${storedDims}d)`
172
+ : `${embedding.provider} (${embedding.dimensions}d)`;
173
+
174
+ console.log('[audrey] Starting dream cycle...');
175
+ console.log(`[audrey] Embedding: ${embeddingLabel}`);
176
+
177
+ const result = await audrey.dream();
178
+ const health = audrey.memoryStatus();
179
+
180
+ console.log(
181
+ `[audrey] Consolidation: evaluated ${result.consolidation.episodesEvaluated} episodes, `
182
+ + `found ${result.consolidation.clustersFound} clusters, extracted ${result.consolidation.principlesExtracted} principles `
183
+ + `(${result.consolidation.semanticsCreated ?? 0} semantic, ${result.consolidation.proceduresCreated ?? 0} procedural)`
184
+ );
185
+ console.log(
186
+ `[audrey] Decay: evaluated ${result.decay.totalEvaluated} memories, `
187
+ + `${result.decay.transitionedToDormant} transitioned to dormant`
188
+ );
189
+ console.log(
190
+ `[audrey] Final: ${result.stats.episodic} episodic, ${result.stats.semantic} semantic, ${result.stats.procedural} procedural `
191
+ + `| ${health.healthy ? 'healthy' : 'unhealthy'}`
192
+ );
193
+ console.log('[audrey] Dream complete.');
194
+ } finally {
195
+ audrey.close();
196
+ }
197
+ }
198
+
199
+ async function greeting() {
200
+ const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
201
+
202
+ if (!existsSync(dataDir)) {
203
+ console.log('[audrey] No data yet — fresh start.');
204
+ return;
205
+ }
206
+
207
+ const dimensions = readStoredDimensions(dataDir) || 8;
208
+ const audrey = new Audrey({
209
+ dataDir,
210
+ agent: 'greeting',
211
+ embedding: { provider: 'mock', dimensions },
212
+ });
213
+
214
+ try {
215
+ const contextArg = process.argv[3] || undefined;
216
+ const result = await audrey.greeting({ context: contextArg });
217
+ const health = audrey.memoryStatus();
218
+
219
+ const lines = [];
220
+ lines.push(`[Audrey v${VERSION}] Memory briefing`);
221
+ lines.push('');
222
+
223
+ // Mood
224
+ if (result.mood && result.mood.samples > 0) {
225
+ const v = result.mood.valence;
226
+ const moodWord = v > 0.3 ? 'positive' : v < -0.3 ? 'negative' : 'neutral';
227
+ lines.push(`Mood: ${moodWord} (valence=${v.toFixed(2)}, arousal=${result.mood.arousal.toFixed(2)}, from ${result.mood.samples} recent memories)`);
228
+ }
229
+
230
+ // Health
231
+ const stats = audrey.introspect();
232
+ lines.push(`Memory: ${stats.episodic} episodic, ${stats.semantic} semantic, ${stats.procedural} procedural | ${health.healthy ? 'healthy' : 'needs attention'}`);
233
+ lines.push('');
234
+
235
+ // Principles (semantic memories)
236
+ if (result.principles?.length > 0) {
237
+ lines.push('Learned principles:');
238
+ for (const p of result.principles) {
239
+ lines.push(` - ${p.content}`);
240
+ }
241
+ lines.push('');
242
+ }
243
+
244
+ // Identity (private memories)
245
+ if (result.identity?.length > 0) {
246
+ lines.push('Identity:');
247
+ for (const m of result.identity) {
248
+ lines.push(` - ${m.content}`);
249
+ }
250
+ lines.push('');
251
+ }
252
+
253
+ // Recent memories
254
+ if (result.recent?.length > 0) {
255
+ lines.push('Recent memories:');
256
+ for (const r of result.recent) {
257
+ const age = timeSince(r.created_at);
258
+ lines.push(` - [${age}] ${r.content.slice(0, 200)}`);
259
+ }
260
+ lines.push('');
261
+ }
262
+
263
+ // Unresolved
264
+ if (result.unresolved?.length > 0) {
265
+ lines.push('Unresolved threads:');
266
+ for (const u of result.unresolved) {
267
+ lines.push(` - ${u.content.slice(0, 150)}`);
268
+ }
269
+ lines.push('');
270
+ }
271
+
272
+ // Contextual recall
273
+ if (result.contextual?.length > 0) {
274
+ lines.push(`Context-relevant memories (query: "${contextArg}"):`);
275
+ for (const c of result.contextual) {
276
+ lines.push(` - [${c.type}] ${c.content.slice(0, 200)}`);
277
+ }
278
+ lines.push('');
279
+ }
280
+
281
+ console.log(lines.join('\n'));
282
+ } finally {
283
+ audrey.close();
284
+ }
285
+ }
286
+
287
+ function timeSince(isoDate) {
288
+ const ms = Date.now() - new Date(isoDate).getTime();
289
+ const mins = Math.floor(ms / 60000);
290
+ if (mins < 60) return `${mins}m ago`;
291
+ const hours = Math.floor(mins / 60);
292
+ if (hours < 24) return `${hours}h ago`;
293
+ const days = Math.floor(hours / 24);
294
+ return `${days}d ago`;
295
+ }
296
+
297
+ async function reflect() {
298
+ const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
299
+ const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
300
+ const embedding = resolveEmbeddingProvider(process.env, explicit);
301
+
302
+ const config = {
303
+ dataDir,
304
+ agent: 'reflect',
305
+ embedding,
306
+ };
307
+
308
+ const llm = resolveLLMConfig();
309
+ if (llm) config.llm = llm;
310
+
311
+ const audrey = new Audrey(config);
312
+ try {
313
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
314
+
315
+ // Read conversation turns from stdin if available
316
+ let turns = null;
317
+ if (!process.stdin.isTTY) {
318
+ const chunks = [];
319
+ for await (const chunk of process.stdin) {
320
+ chunks.push(chunk);
321
+ }
322
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
323
+ if (raw) {
324
+ try {
325
+ turns = JSON.parse(raw);
326
+ } catch {
327
+ console.error('[audrey] Could not parse stdin as JSON turns, skipping reflect.');
328
+ }
329
+ }
330
+ }
331
+
332
+ if (turns && Array.isArray(turns) && turns.length > 0) {
333
+ console.log(`[audrey] Reflecting on ${turns.length} conversation turns...`);
334
+ const reflectResult = await audrey.reflect(turns);
335
+ if (reflectResult.skipped) {
336
+ console.log(`[audrey] Reflect skipped: ${reflectResult.skipped}`);
337
+ } else {
338
+ console.log(`[audrey] Reflected: encoded ${reflectResult.encoded} lasting memories.`);
339
+ }
340
+ }
341
+
342
+ // Always run dream cycle after reflect
343
+ console.log('[audrey] Starting dream cycle...');
344
+ const result = await audrey.dream();
345
+ console.log(
346
+ `[audrey] Consolidation: ${result.consolidation.episodesEvaluated} episodes evaluated, `
347
+ + `${result.consolidation.clustersFound} clusters, ${result.consolidation.principlesExtracted} principles`
348
+ );
349
+ console.log(
350
+ `[audrey] Decay: ${result.decay.totalEvaluated} evaluated, `
351
+ + `${result.decay.transitionedToDormant} dormant`
352
+ );
353
+ console.log(
354
+ `[audrey] Status: ${result.stats.episodic} episodic, ${result.stats.semantic} semantic, `
355
+ + `${result.stats.procedural} procedural`
356
+ );
357
+ console.log('[audrey] Dream complete.');
358
+ } finally {
359
+ audrey.close();
360
+ }
361
+ }
362
+
363
+ function install() {
364
+ try {
365
+ execFileSync('claude', ['--version'], { stdio: 'ignore' });
366
+ } catch {
367
+ console.error('Error: claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code');
368
+ process.exit(1);
369
+ }
370
+
371
+ const resolvedEmbedding = resolveEmbeddingProvider(process.env);
372
+ if (resolvedEmbedding.provider === 'gemini') {
373
+ console.log('Detected GOOGLE_API_KEY/GEMINI_API_KEY - using Gemini embeddings (3072d)');
374
+ } else if (resolvedEmbedding.provider === 'local') {
375
+ console.log('No explicit embedding provider configured - using local embeddings (384d)');
376
+ } else if (resolvedEmbedding.provider === 'openai') {
377
+ console.log('Using explicit OpenAI embeddings (1536d)');
378
+ }
379
+
380
+ if (process.env.ANTHROPIC_API_KEY) {
381
+ console.log('Detected ANTHROPIC_API_KEY - enabling LLM-powered consolidation + contradiction detection');
382
+ }
383
+
384
+ try {
385
+ execFileSync('claude', ['mcp', 'remove', SERVER_NAME], { stdio: 'ignore' });
386
+ } catch {
387
+ // Not registered yet.
388
+ }
389
+
390
+ const args = buildInstallArgs(process.env);
391
+ try {
392
+ execFileSync('claude', args, { stdio: 'inherit' });
393
+ } catch {
394
+ console.error('Failed to register MCP server. Is Claude Code installed and on your PATH?');
395
+ process.exit(1);
396
+ }
397
+
398
+ console.log(`
399
+ Audrey registered as "${SERVER_NAME}" with Claude Code.
400
+
401
+ 13 MCP tools available in every session:
402
+ memory_encode - Store observations, facts, preferences
403
+ memory_recall - Search memories by semantic similarity
404
+ memory_consolidate - Extract principles from accumulated episodes
405
+ memory_dream - Full sleep cycle: consolidate + decay + stats
406
+ memory_introspect - Check memory system health
407
+ memory_resolve_truth - Resolve contradictions between claims
408
+ memory_export - Export all memories as JSON snapshot
409
+ memory_import - Import a snapshot into a fresh database
410
+ memory_forget - Forget a specific memory by ID or query
411
+ memory_decay - Apply forgetting curves, transition low-confidence to dormant
412
+ memory_status - Check brain health (episode/vec sync, dimensions)
413
+ memory_reflect - Form lasting memories from a conversation
414
+ memory_greeting - Wake up as yourself: load identity, context, mood
415
+
416
+ CLI subcommands:
417
+ npx audrey install - Register MCP server with Claude Code
418
+ npx audrey uninstall - Remove MCP server registration
419
+ npx audrey status - Show memory store health and stats
420
+ npx audrey greeting - Output session briefing (for hooks)
421
+ npx audrey reflect - Reflect on conversation + dream cycle (for hooks)
422
+ npx audrey dream - Run consolidation + decay cycle
423
+ npx audrey reembed - Re-embed all memories with current provider
424
+
425
+ Data stored in: ${DEFAULT_DATA_DIR}
426
+ Verify: claude mcp list
427
+ `);
428
+ }
429
+
430
+ function uninstall() {
431
+ try {
432
+ execFileSync('claude', ['--version'], { stdio: 'ignore' });
433
+ } catch {
434
+ console.error('Error: claude CLI not found.');
435
+ process.exit(1);
436
+ }
437
+
438
+ try {
439
+ execFileSync('claude', ['mcp', 'remove', SERVER_NAME], { stdio: 'inherit' });
440
+ console.log(`Removed "${SERVER_NAME}" from Claude Code.`);
441
+ } catch {
442
+ console.error(`Failed to remove "${SERVER_NAME}". It may not be registered.`);
443
+ process.exit(1);
444
+ }
445
+ }
446
+
447
+ function status() {
448
+ let registered = false;
449
+ const claudeJsonPath = join(homedir(), '.claude.json');
450
+ try {
451
+ const claudeConfig = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
452
+ registered = SERVER_NAME in (claudeConfig.mcpServers || {});
453
+ } catch {
454
+ // Ignore unreadable config.
455
+ }
456
+
457
+ console.log(`Registration: ${registered ? 'active' : 'not registered'}`);
458
+
459
+ if (!existsSync(DEFAULT_DATA_DIR)) {
460
+ console.log(`Data directory: ${DEFAULT_DATA_DIR} (not yet created - will be created on first use)`);
461
+ return;
462
+ }
463
+
464
+ try {
465
+ const dimensions = readStoredDimensions(DEFAULT_DATA_DIR) || 8;
466
+ const audrey = new Audrey({
467
+ dataDir: DEFAULT_DATA_DIR,
468
+ agent: 'status-check',
469
+ embedding: { provider: 'mock', dimensions },
470
+ });
471
+ const stats = audrey.introspect();
472
+ const health = audrey.memoryStatus();
473
+ const lastConsolidation = audrey.db.prepare(`
474
+ SELECT completed_at FROM consolidation_runs
475
+ WHERE status = 'completed'
476
+ ORDER BY completed_at DESC
477
+ LIMIT 1
478
+ `).get()?.completed_at || 'never';
479
+ audrey.close();
480
+
481
+ console.log(`Data directory: ${DEFAULT_DATA_DIR}`);
482
+ console.log(`Memories: ${stats.episodic} episodic, ${stats.semantic} semantic, ${stats.procedural} procedural`);
483
+ console.log(`Index sync: ${health.vec_episodes}/${health.searchable_episodes} episodic, ${health.vec_semantics}/${health.searchable_semantics} semantic, ${health.vec_procedures}/${health.searchable_procedures} procedural`);
484
+ console.log(`Health: ${health.healthy ? 'healthy' : 'unhealthy'}${health.reembed_recommended ? ' (re-embed recommended)' : ''}`);
485
+ console.log(`Dormant: ${stats.dormant}`);
486
+ console.log(`Causal links: ${stats.causalLinks}`);
487
+ console.log(`Contradictions: ${stats.contradictions.open} open, ${stats.contradictions.resolved} resolved`);
488
+ console.log(`Consolidation runs: ${stats.totalConsolidationRuns}`);
489
+ console.log(`Last consolidation: ${lastConsolidation}`);
490
+ } catch (err) {
491
+ console.log(`Data directory: ${DEFAULT_DATA_DIR} (exists but could not read: ${err.message})`);
492
+ }
493
+ }
494
+
495
+ function toolResult(data) {
496
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
497
+ }
498
+
499
+ function toolError(err) {
500
+ return { isError: true, content: [{ type: 'text', text: `Error: ${err.message || String(err)}` }] };
501
+ }
502
+
503
+ export function registerDreamTool(server, audrey) {
504
+ server.tool(
505
+ 'memory_dream',
506
+ {
507
+ min_cluster_size: z.number().optional().describe('Minimum episodes per cluster (default 3)'),
508
+ similarity_threshold: z.number().optional().describe('Similarity threshold for clustering (default 0.85)'),
509
+ dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant (default 0.1)'),
510
+ },
511
+ async ({ min_cluster_size, similarity_threshold, dormant_threshold }) => {
512
+ try {
513
+ const result = await audrey.dream({
514
+ minClusterSize: min_cluster_size,
515
+ similarityThreshold: similarity_threshold,
516
+ dormantThreshold: dormant_threshold,
517
+ });
518
+ return toolResult(result);
519
+ } catch (err) {
520
+ return toolError(err);
521
+ }
522
+ },
523
+ );
524
+ }
525
+
526
+ async function main() {
527
+ const config = buildAudreyConfig();
528
+ const audrey = new Audrey(config);
529
+
530
+ const embLabel = config.embedding.provider === 'mock'
531
+ ? 'mock embeddings - set OPENAI_API_KEY for real semantic search'
532
+ : `${config.embedding.provider} embeddings (${config.embedding.dimensions}d)`;
533
+ console.error(`[audrey-mcp] v${VERSION} started - agent=${config.agent} dataDir=${config.dataDir} (${embLabel})`);
534
+
535
+ const server = new McpServer({
536
+ name: SERVER_NAME,
537
+ version: VERSION,
538
+ });
539
+
540
+ server.tool('memory_encode', memoryEncodeToolSchema, async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
541
+ try {
542
+ validateMemoryContent(content);
543
+ const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect });
544
+ return toolResult({ id, content, source, private: isPrivate ?? false });
545
+ } catch (err) {
546
+ return toolError(err);
547
+ }
548
+ });
549
+
550
+ server.tool('memory_recall', memoryRecallToolSchema, async ({ query, limit, types, min_confidence, tags, sources, after, before, context, mood }) => {
551
+ try {
552
+ const results = await audrey.recall(query, {
553
+ limit: limit ?? 10,
554
+ types,
555
+ minConfidence: min_confidence,
556
+ tags,
557
+ sources,
558
+ after,
559
+ before,
560
+ context,
561
+ mood,
562
+ });
563
+ return toolResult(results);
564
+ } catch (err) {
565
+ return toolError(err);
566
+ }
567
+ });
568
+
569
+ server.tool('memory_consolidate', {
570
+ min_cluster_size: z.number().optional().describe('Minimum episodes per cluster'),
571
+ similarity_threshold: z.number().optional().describe('Similarity threshold for clustering'),
572
+ }, async ({ min_cluster_size, similarity_threshold }) => {
573
+ try {
574
+ const consolidation = await audrey.consolidate({
575
+ minClusterSize: min_cluster_size,
576
+ similarityThreshold: similarity_threshold,
577
+ });
578
+ return toolResult(consolidation);
579
+ } catch (err) {
580
+ return toolError(err);
581
+ }
582
+ });
583
+
584
+ server.tool('memory_introspect', {}, async () => {
585
+ try {
586
+ return toolResult(audrey.introspect());
587
+ } catch (err) {
588
+ return toolError(err);
589
+ }
590
+ });
591
+
592
+ server.tool('memory_resolve_truth', {
593
+ contradiction_id: z.string().describe('ID of the contradiction to resolve'),
594
+ }, async ({ contradiction_id }) => {
595
+ try {
596
+ return toolResult(await audrey.resolveTruth(contradiction_id));
597
+ } catch (err) {
598
+ return toolError(err);
599
+ }
600
+ });
601
+
602
+ server.tool('memory_export', {}, async () => {
603
+ try {
604
+ return toolResult(audrey.export());
605
+ } catch (err) {
606
+ return toolError(err);
607
+ }
608
+ });
609
+
610
+ server.tool('memory_import', memoryImportToolSchema, async ({ snapshot }) => {
611
+ try {
612
+ await audrey.import(snapshot);
613
+ return toolResult({ imported: true, stats: audrey.introspect() });
614
+ } catch (err) {
615
+ return toolError(err);
616
+ }
617
+ });
618
+
619
+ server.tool('memory_forget', memoryForgetToolSchema, async ({ id, query, min_similarity, purge }) => {
620
+ try {
621
+ validateForgetSelection(id, query);
622
+ let result;
623
+ if (id) {
624
+ result = audrey.forget(id, { purge: purge ?? false });
625
+ } else {
626
+ result = await audrey.forgetByQuery(query, {
627
+ minSimilarity: min_similarity ?? 0.9,
628
+ purge: purge ?? false,
629
+ });
630
+ if (!result) {
631
+ return toolResult({ forgotten: false, reason: 'No memory found above similarity threshold' });
632
+ }
633
+ }
634
+ return toolResult({ forgotten: true, ...result });
635
+ } catch (err) {
636
+ return toolError(err);
637
+ }
638
+ });
639
+
640
+ server.tool('memory_decay', {
641
+ dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant (default 0.1)'),
642
+ }, async ({ dormant_threshold }) => {
643
+ try {
644
+ return toolResult(audrey.decay({ dormantThreshold: dormant_threshold }));
645
+ } catch (err) {
646
+ return toolError(err);
647
+ }
648
+ });
649
+
650
+ server.tool('memory_status', {}, async () => {
651
+ try {
652
+ return toolResult(audrey.memoryStatus());
653
+ } catch (err) {
654
+ return toolError(err);
655
+ }
656
+ });
657
+
658
+ server.tool('memory_reflect', {
659
+ turns: z.array(z.object({
660
+ role: z.string().describe('Message role: user or assistant'),
661
+ content: z.string().describe('Message content'),
662
+ })).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
663
+ }, async ({ turns }) => {
664
+ try {
665
+ return toolResult(await audrey.reflect(turns));
666
+ } catch (err) {
667
+ return toolError(err);
668
+ }
669
+ });
670
+
671
+ registerDreamTool(server, audrey);
672
+
673
+ server.tool('memory_greeting', {
674
+ context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
675
+ }, async ({ context }) => {
676
+ try {
677
+ return toolResult(await audrey.greeting({ context }));
678
+ } catch (err) {
679
+ return toolError(err);
680
+ }
681
+ });
682
+
683
+ const transport = new StdioServerTransport();
684
+ await server.connect(transport);
685
+ console.error('[audrey-mcp] connected via stdio');
686
+
687
+ process.on('SIGINT', () => {
688
+ console.error('[audrey-mcp] shutting down');
689
+ audrey.close();
690
+ process.exit(0);
691
+ });
692
+ }
693
+
694
+ const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
695
+
696
+ if (isDirectRun) {
697
+ if (subcommand === 'install') {
698
+ install();
699
+ } else if (subcommand === 'uninstall') {
700
+ uninstall();
701
+ } else if (subcommand === 'reembed') {
702
+ reembed().catch(err => {
703
+ console.error('[audrey] reembed failed:', err);
704
+ process.exit(1);
705
+ });
706
+ } else if (subcommand === 'dream') {
707
+ dream().catch(err => {
708
+ console.error('[audrey] dream failed:', err);
709
+ process.exit(1);
710
+ });
711
+ } else if (subcommand === 'greeting') {
712
+ greeting().catch(err => {
713
+ console.error('[audrey] greeting failed:', err);
714
+ process.exit(1);
715
+ });
716
+ } else if (subcommand === 'reflect') {
717
+ reflect().catch(err => {
718
+ console.error('[audrey] reflect failed:', err);
719
+ process.exit(1);
720
+ });
721
+ } else if (subcommand === 'status') {
722
+ status();
723
+ } else {
724
+ main().catch(err => {
725
+ console.error('[audrey-mcp] fatal:', err);
726
+ process.exit(1);
727
+ });
728
+ }
729
+ }