audrey 0.16.1 → 0.17.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.
@@ -1,10 +1,8 @@
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
2
  import { z } from 'zod';
5
3
  import { homedir } from 'node:os';
6
4
  import { join, resolve } from 'node:path';
7
- import { existsSync, readFileSync } from 'node:fs';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
6
  import { execFileSync } from 'node:child_process';
9
7
  import { fileURLToPath } from 'node:url';
10
8
  import { Audrey } from '../src/index.js';
@@ -12,10 +10,11 @@ import { readStoredDimensions } from '../src/db.js';
12
10
  import {
13
11
  VERSION,
14
12
  SERVER_NAME,
15
- DEFAULT_DATA_DIR,
16
13
  buildAudreyConfig,
17
14
  buildInstallArgs,
15
+ resolveDataDir,
18
16
  resolveEmbeddingProvider,
17
+ resolveLLMProvider,
19
18
  } from './config.js';
20
19
 
21
20
  const VALID_SOURCES = ['direct-observation', 'told-by-user', 'tool-result', 'inference', 'model-generated'];
@@ -50,6 +49,13 @@ export async function initializeEmbeddingProvider(provider) {
50
49
  }
51
50
  }
52
51
 
52
+ async function closeAudreyGracefully(audrey) {
53
+ if (audrey && typeof audrey.waitForIdle === 'function') {
54
+ await audrey.waitForIdle();
55
+ }
56
+ audrey?.close();
57
+ }
58
+
53
59
  export const memoryEncodeToolSchema = {
54
60
  content: z.string()
55
61
  .max(MAX_MEMORY_CONTENT_LENGTH)
@@ -105,7 +111,7 @@ export const memoryForgetToolSchema = {
105
111
  };
106
112
 
107
113
  async function reembed() {
108
- const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
114
+ const dataDir = resolveDataDir(process.env);
109
115
  const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
110
116
  const embedding = resolveEmbeddingProvider(process.env, explicit);
111
117
  const storedDims = readStoredDimensions(dataDir);
@@ -123,33 +129,12 @@ async function reembed() {
123
129
  const counts = await reembedAll(audrey.db, audrey.embeddingProvider, { dropAndRecreate: dimensionsChanged });
124
130
  console.log(`Done. Re-embedded: ${counts.episodes} episodes, ${counts.semantics} semantics, ${counts.procedures} procedures`);
125
131
  } 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 };
132
+ await closeAudreyGracefully(audrey);
134
133
  }
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
134
  }
150
135
 
151
136
  async function dream() {
152
- const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
137
+ const dataDir = resolveDataDir(process.env);
153
138
  const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
154
139
  const embedding = resolveEmbeddingProvider(process.env, explicit);
155
140
  const storedDims = readStoredDimensions(dataDir);
@@ -160,7 +145,7 @@ async function dream() {
160
145
  embedding,
161
146
  };
162
147
 
163
- const llm = resolveLLMConfig();
148
+ const llm = resolveLLMProvider(process.env, process.env.AUDREY_LLM_PROVIDER);
164
149
  if (llm) config.llm = llm;
165
150
 
166
151
  const audrey = new Audrey(config);
@@ -192,34 +177,52 @@ async function dream() {
192
177
  );
193
178
  console.log('[audrey] Dream complete.');
194
179
  } finally {
195
- audrey.close();
180
+ await closeAudreyGracefully(audrey);
196
181
  }
197
182
  }
198
183
 
199
184
  async function greeting() {
200
- const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
185
+ const dataDir = resolveDataDir(process.env);
186
+ const contextArg = process.argv[3] || undefined;
201
187
 
202
188
  if (!existsSync(dataDir)) {
203
- console.log('[audrey] No data yet fresh start.');
189
+ console.log('[audrey] No data yet - fresh start.');
204
190
  return;
205
191
  }
206
192
 
207
- const dimensions = readStoredDimensions(dataDir) || 8;
193
+ const storedDimensions = readStoredDimensions(dataDir);
194
+ const resolvedEmbedding = resolveEmbeddingProvider(process.env, process.env.AUDREY_EMBEDDING_PROVIDER);
195
+ const canUseResolvedEmbedding = Boolean(contextArg)
196
+ && storedDimensions !== null
197
+ && storedDimensions === resolvedEmbedding.dimensions;
198
+ const dimensions = storedDimensions || resolvedEmbedding.dimensions || 8;
208
199
  const audrey = new Audrey({
209
200
  dataDir,
210
201
  agent: 'greeting',
211
- embedding: { provider: 'mock', dimensions },
202
+ embedding: canUseResolvedEmbedding
203
+ ? resolvedEmbedding
204
+ : { provider: 'mock', dimensions },
212
205
  });
213
206
 
214
207
  try {
215
- const contextArg = process.argv[3] || undefined;
216
- const result = await audrey.greeting({ context: contextArg });
208
+ if (canUseResolvedEmbedding) {
209
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
210
+ }
211
+ const result = await audrey.greeting({ context: canUseResolvedEmbedding ? contextArg : undefined });
217
212
  const health = audrey.memoryStatus();
218
213
 
219
214
  const lines = [];
220
215
  lines.push(`[Audrey v${VERSION}] Memory briefing`);
221
216
  lines.push('');
222
217
 
218
+ if (contextArg && !canUseResolvedEmbedding) {
219
+ lines.push(
220
+ `Context recall skipped: stored index is ${storedDimensions ?? 'unknown'}d `
221
+ + `but current embedding config resolves to ${resolvedEmbedding.dimensions}d.`
222
+ );
223
+ lines.push('');
224
+ }
225
+
223
226
  // Mood
224
227
  if (result.mood && result.mood.samples > 0) {
225
228
  const v = result.mood.valence;
@@ -280,7 +283,7 @@ async function greeting() {
280
283
 
281
284
  console.log(lines.join('\n'));
282
285
  } finally {
283
- audrey.close();
286
+ await closeAudreyGracefully(audrey);
284
287
  }
285
288
  }
286
289
 
@@ -295,7 +298,7 @@ function timeSince(isoDate) {
295
298
  }
296
299
 
297
300
  async function reflect() {
298
- const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
301
+ const dataDir = resolveDataDir(process.env);
299
302
  const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
300
303
  const embedding = resolveEmbeddingProvider(process.env, explicit);
301
304
 
@@ -305,7 +308,7 @@ async function reflect() {
305
308
  embedding,
306
309
  };
307
310
 
308
- const llm = resolveLLMConfig();
311
+ const llm = resolveLLMProvider(process.env, process.env.AUDREY_LLM_PROVIDER);
309
312
  if (llm) config.llm = llm;
310
313
 
311
314
  const audrey = new Audrey(config);
@@ -356,7 +359,340 @@ async function reflect() {
356
359
  );
357
360
  console.log('[audrey] Dream complete.');
358
361
  } finally {
359
- audrey.close();
362
+ await closeAudreyGracefully(audrey);
363
+ }
364
+ }
365
+
366
+ async function recall() {
367
+ const dataDir = resolveDataDir(process.env);
368
+
369
+ if (!existsSync(dataDir)) {
370
+ // No data yet — nothing to recall
371
+ process.exit(0);
372
+ }
373
+
374
+ // Read hook JSON from stdin
375
+ let hookInput = null;
376
+ if (!process.stdin.isTTY) {
377
+ const chunks = [];
378
+ for await (const chunk of process.stdin) {
379
+ chunks.push(chunk);
380
+ }
381
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
382
+ if (raw) {
383
+ try {
384
+ hookInput = JSON.parse(raw);
385
+ } catch {
386
+ console.error('[audrey] Could not parse stdin as JSON');
387
+ process.exit(0);
388
+ }
389
+ }
390
+ }
391
+
392
+ // Extract query from hook input or CLI arg
393
+ const query = hookInput?.prompt // UserPromptSubmit hook
394
+ || hookInput?.query // direct query field
395
+ || process.argv[3]; // CLI argument
396
+
397
+ if (!query || typeof query !== 'string' || !query.trim()) {
398
+ process.exit(0);
399
+ }
400
+
401
+ const storedDimensions = readStoredDimensions(dataDir);
402
+ const resolvedEmbedding = resolveEmbeddingProvider(process.env, process.env.AUDREY_EMBEDDING_PROVIDER);
403
+ const canEmbed = storedDimensions !== null && storedDimensions === resolvedEmbedding.dimensions;
404
+
405
+ if (!canEmbed) {
406
+ // Dimension mismatch — skip recall silently
407
+ process.exit(0);
408
+ }
409
+
410
+ const audrey = new Audrey({
411
+ dataDir,
412
+ agent: 'recall-hook',
413
+ embedding: resolvedEmbedding,
414
+ });
415
+
416
+ try {
417
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
418
+
419
+ const limit = parseInt(process.argv[4], 10) || 5;
420
+ const results = await audrey.recall(query.trim(), {
421
+ limit,
422
+ includePrivate: false,
423
+ });
424
+
425
+ if (!results || results.length === 0) {
426
+ process.exit(0);
427
+ }
428
+
429
+ const lines = results.map(r => {
430
+ const type = r.type === 'semantic' ? 'principle' : r.type === 'procedural' ? 'procedure' : 'memory';
431
+ return `[${type}] ${r.content}`;
432
+ });
433
+
434
+ const output = {
435
+ additionalContext: `Relevant memories from Audrey:\n\n${lines.join('\n\n')}`,
436
+ };
437
+
438
+ console.log(JSON.stringify(output));
439
+ } finally {
440
+ await closeAudreyGracefully(audrey);
441
+ }
442
+ }
443
+
444
+ export function buildHooksConfig({ scope = 'user' } = {}) {
445
+ const audreyBin = 'npx audrey';
446
+
447
+ return {
448
+ SessionStart: [
449
+ {
450
+ matcher: 'startup|resume',
451
+ hooks: [
452
+ {
453
+ type: 'command',
454
+ command: `${audreyBin} greeting`,
455
+ timeout: 30,
456
+ },
457
+ ],
458
+ },
459
+ ],
460
+ UserPromptSubmit: [
461
+ {
462
+ matcher: '',
463
+ hooks: [
464
+ {
465
+ type: 'command',
466
+ command: `${audreyBin} recall`,
467
+ timeout: 15,
468
+ },
469
+ ],
470
+ },
471
+ ],
472
+ Stop: [
473
+ {
474
+ matcher: '',
475
+ hooks: [
476
+ {
477
+ type: 'command',
478
+ command: `${audreyBin} reflect`,
479
+ timeout: 120,
480
+ },
481
+ ],
482
+ },
483
+ ],
484
+ PostCompact: [
485
+ {
486
+ matcher: '',
487
+ hooks: [
488
+ {
489
+ type: 'command',
490
+ command: `${audreyBin} greeting`,
491
+ timeout: 30,
492
+ },
493
+ ],
494
+ },
495
+ ],
496
+ };
497
+ }
498
+
499
+ function hooksInstall() {
500
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
501
+ const settingsDir = join(homedir(), '.claude');
502
+
503
+ let settings = {};
504
+ if (existsSync(settingsPath)) {
505
+ try {
506
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
507
+ } catch {
508
+ console.error(`[audrey] Could not parse ${settingsPath}. Please fix it manually.`);
509
+ process.exit(1);
510
+ }
511
+ }
512
+
513
+ const audreyHooks = buildHooksConfig();
514
+
515
+ if (!settings.hooks) {
516
+ settings.hooks = {};
517
+ }
518
+
519
+ // Merge Audrey hooks with existing hooks, preserving user's existing hooks
520
+ for (const [event, audreyEntries] of Object.entries(audreyHooks)) {
521
+ if (!settings.hooks[event]) {
522
+ settings.hooks[event] = [];
523
+ }
524
+
525
+ // Remove any previously-installed Audrey hooks (by command match)
526
+ settings.hooks[event] = settings.hooks[event].filter(entry => {
527
+ if (!entry.hooks) return true;
528
+ return !entry.hooks.some(h => h.command && h.command.includes('npx audrey'));
529
+ });
530
+
531
+ // Add Audrey hooks
532
+ settings.hooks[event].push(...audreyEntries);
533
+ }
534
+
535
+ mkdirSync(settingsDir, { recursive: true });
536
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
537
+
538
+ console.log(`[audrey] Hooks installed in ${settingsPath}
539
+
540
+ Hooks configured:
541
+ SessionStart → npx audrey greeting (load identity, principles, mood)
542
+ UserPromptSubmit → npx audrey recall (semantic memory search per prompt)
543
+ Stop → npx audrey reflect (consolidate learnings + dream cycle)
544
+ PostCompact → npx audrey greeting (re-inject memories after compaction)
545
+
546
+ Verify: Open ${settingsPath} or run claude /hooks
547
+ `);
548
+ }
549
+
550
+ function hooksUninstall() {
551
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
552
+
553
+ if (!existsSync(settingsPath)) {
554
+ console.log('[audrey] No settings.json found. Nothing to remove.');
555
+ return;
556
+ }
557
+
558
+ let settings;
559
+ try {
560
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
561
+ } catch {
562
+ console.error(`[audrey] Could not parse ${settingsPath}.`);
563
+ process.exit(1);
564
+ }
565
+
566
+ if (!settings.hooks) {
567
+ console.log('[audrey] No hooks configured. Nothing to remove.');
568
+ return;
569
+ }
570
+
571
+ let removed = 0;
572
+ for (const event of Object.keys(settings.hooks)) {
573
+ const before = settings.hooks[event].length;
574
+ settings.hooks[event] = settings.hooks[event].filter(entry => {
575
+ if (!entry.hooks) return true;
576
+ return !entry.hooks.some(h => h.command && h.command.includes('npx audrey'));
577
+ });
578
+ removed += before - settings.hooks[event].length;
579
+
580
+ // Clean up empty arrays
581
+ if (settings.hooks[event].length === 0) {
582
+ delete settings.hooks[event];
583
+ }
584
+ }
585
+
586
+ // Clean up empty hooks object
587
+ if (Object.keys(settings.hooks).length === 0) {
588
+ delete settings.hooks;
589
+ }
590
+
591
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
592
+ console.log(`[audrey] Removed ${removed} hook(s) from ${settingsPath}`);
593
+ }
594
+
595
+ export function resolveSnapshotPath(outputArg, dataDir) {
596
+ if (outputArg) return resolve(outputArg);
597
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
598
+ return resolve(dataDir, '..', `audrey-snapshot-${timestamp}.json`);
599
+ }
600
+
601
+ async function snapshot() {
602
+ const dataDir = resolveDataDir(process.env);
603
+
604
+ if (!existsSync(dataDir)) {
605
+ console.error('[audrey] No data directory found. Nothing to snapshot.');
606
+ process.exit(1);
607
+ }
608
+
609
+ const storedDimensions = readStoredDimensions(dataDir);
610
+ const dimensions = storedDimensions || 8;
611
+ const audrey = new Audrey({
612
+ dataDir,
613
+ agent: 'snapshot',
614
+ embedding: { provider: 'mock', dimensions },
615
+ });
616
+
617
+ try {
618
+ const data = audrey.export();
619
+ const stats = audrey.introspect();
620
+
621
+ const outputPath = resolveSnapshotPath(process.argv[3], dataDir);
622
+
623
+ writeFileSync(outputPath, JSON.stringify(data, null, 2) + '\n');
624
+
625
+ console.log(`[audrey] Snapshot saved to ${outputPath}`);
626
+ console.log(` ${stats.episodic} episodes, ${stats.semantic} semantics, ${stats.procedural} procedures`);
627
+ console.log(` ${data.contradictions?.length || 0} contradictions, ${data.causalLinks?.length || 0} causal links`);
628
+ console.log(` Version: ${data.version}, exported at: ${data.exportedAt}`);
629
+ console.log('');
630
+ console.log('To restore: npx audrey restore ' + outputPath);
631
+ } finally {
632
+ await closeAudreyGracefully(audrey);
633
+ }
634
+ }
635
+
636
+ async function restore() {
637
+ const snapshotPath = process.argv[3];
638
+ if (!snapshotPath) {
639
+ console.error('Usage: npx audrey restore <snapshot-file>');
640
+ console.error(' e.g.: npx audrey restore audrey-snapshot-2026-03-24.json');
641
+ process.exit(1);
642
+ }
643
+
644
+ const resolvedPath = resolve(snapshotPath);
645
+ if (!existsSync(resolvedPath)) {
646
+ console.error(`[audrey] Snapshot file not found: ${resolvedPath}`);
647
+ process.exit(1);
648
+ }
649
+
650
+ let data;
651
+ try {
652
+ data = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
653
+ } catch {
654
+ console.error(`[audrey] Could not parse snapshot file: ${resolvedPath}`);
655
+ process.exit(1);
656
+ }
657
+
658
+ if (!data.version || !data.episodes) {
659
+ console.error('[audrey] Invalid snapshot: missing version or episodes field.');
660
+ process.exit(1);
661
+ }
662
+
663
+ const dataDir = resolveDataDir(process.env);
664
+ const explicit = process.env.AUDREY_EMBEDDING_PROVIDER;
665
+ const embedding = resolveEmbeddingProvider(process.env, explicit);
666
+
667
+ const audrey = new Audrey({ dataDir, agent: 'restore', embedding });
668
+
669
+ try {
670
+ await initializeEmbeddingProvider(audrey.embeddingProvider);
671
+
672
+ const stats = audrey.introspect();
673
+ const isEmpty = stats.episodic === 0 && stats.semantic === 0 && stats.procedural === 0;
674
+
675
+ if (!isEmpty) {
676
+ const force = process.argv.includes('--force');
677
+ if (!force) {
678
+ console.error('[audrey] Database is not empty. Use --force to purge and restore.');
679
+ console.error(` Current: ${stats.episodic} episodes, ${stats.semantic} semantics, ${stats.procedural} procedures`);
680
+ process.exit(1);
681
+ }
682
+ console.log('[audrey] --force: purging existing memories before restore...');
683
+ audrey.purge();
684
+ }
685
+
686
+ console.log(`[audrey] Restoring from snapshot v${data.version} (${data.exportedAt || 'unknown date'})...`);
687
+ console.log(`[audrey] Re-embedding with ${embedding.provider} (${embedding.dimensions}d)...`);
688
+
689
+ await audrey.import(data);
690
+
691
+ const restored = audrey.introspect();
692
+ console.log(`[audrey] Restored: ${restored.episodic} episodes, ${restored.semantic} semantics, ${restored.procedural} procedures`);
693
+ console.log('[audrey] Restore complete.');
694
+ } finally {
695
+ await closeAudreyGracefully(audrey);
360
696
  }
361
697
  }
362
698
 
@@ -368,17 +704,27 @@ function install() {
368
704
  process.exit(1);
369
705
  }
370
706
 
371
- const resolvedEmbedding = resolveEmbeddingProvider(process.env);
707
+ const dataDir = resolveDataDir(process.env);
708
+ const resolvedEmbedding = resolveEmbeddingProvider(process.env, process.env.AUDREY_EMBEDDING_PROVIDER);
709
+ const resolvedLlm = resolveLLMProvider(process.env, process.env.AUDREY_LLM_PROVIDER);
372
710
  if (resolvedEmbedding.provider === 'gemini') {
373
- console.log('Detected GOOGLE_API_KEY/GEMINI_API_KEY - using Gemini embeddings (3072d)');
711
+ console.log('Using Gemini embeddings (3072d)');
374
712
  } else if (resolvedEmbedding.provider === 'local') {
375
- console.log('No explicit embedding provider configured - using local embeddings (384d)');
713
+ console.log(`Using local embeddings (384d, device=${resolvedEmbedding.device || 'gpu'})`);
376
714
  } else if (resolvedEmbedding.provider === 'openai') {
377
- console.log('Using explicit OpenAI embeddings (1536d)');
715
+ console.log('Using OpenAI embeddings (1536d)');
716
+ } else if (resolvedEmbedding.provider === 'mock') {
717
+ console.log('Using mock embeddings');
378
718
  }
379
719
 
380
- if (process.env.ANTHROPIC_API_KEY) {
381
- console.log('Detected ANTHROPIC_API_KEY - enabling LLM-powered consolidation + contradiction detection');
720
+ if (resolvedLlm?.provider === 'anthropic') {
721
+ console.log('Using Anthropic for LLM-powered consolidation, contradiction detection, and reflection');
722
+ } else if (resolvedLlm?.provider === 'openai') {
723
+ console.log('Using OpenAI for LLM-powered consolidation, contradiction detection, and reflection');
724
+ } else if (resolvedLlm?.provider === 'mock') {
725
+ console.log('Using mock LLM provider');
726
+ } else {
727
+ console.log('No LLM provider configured - consolidation and contradiction detection will use heuristics');
382
728
  }
383
729
 
384
730
  try {
@@ -417,12 +763,28 @@ CLI subcommands:
417
763
  npx audrey install - Register MCP server with Claude Code
418
764
  npx audrey uninstall - Remove MCP server registration
419
765
  npx audrey status - Show memory store health and stats
766
+ npx audrey status --json - Emit machine-readable health output
767
+ npx audrey status --json --fail-on-unhealthy - Exit non-zero on unhealthy status
420
768
  npx audrey greeting - Output session briefing (for hooks)
769
+ npx audrey recall - Semantic recall for hook context injection
421
770
  npx audrey reflect - Reflect on conversation + dream cycle (for hooks)
422
771
  npx audrey dream - Run consolidation + decay cycle
423
772
  npx audrey reembed - Re-embed all memories with current provider
424
773
 
425
- Data stored in: ${DEFAULT_DATA_DIR}
774
+ Versioning (git-friendly memory snapshots):
775
+ npx audrey snapshot [file] - Export memories to a JSON snapshot file
776
+ npx audrey restore <file> - Restore memories from a snapshot (--force to overwrite)
777
+
778
+ Hooks integration (automatic memory in every session):
779
+ npx audrey hooks install - Add Audrey hooks to ~/.claude/settings.json
780
+ npx audrey hooks uninstall - Remove Audrey hooks from settings
781
+
782
+ REST API server (any language, any framework):
783
+ npx audrey serve [port] - Start HTTP server (default: 3487)
784
+ AUDREY_API_KEY=secret npx audrey serve - Start with Bearer token auth
785
+ npx audrey dashboard - Start server and open memory dashboard
786
+
787
+ Data stored in: ${dataDir}
426
788
  Verify: claude mcp list
427
789
  `);
428
790
  }
@@ -444,9 +806,15 @@ function uninstall() {
444
806
  }
445
807
  }
446
808
 
447
- function status() {
809
+ function cliHasFlag(flag, argv = process.argv) {
810
+ return Array.isArray(argv) && argv.includes(flag);
811
+ }
812
+
813
+ export function buildStatusReport({
814
+ dataDir = resolveDataDir(process.env),
815
+ claudeJsonPath = join(homedir(), '.claude.json'),
816
+ } = {}) {
448
817
  let registered = false;
449
- const claudeJsonPath = join(homedir(), '.claude.json');
450
818
  try {
451
819
  const claudeConfig = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
452
820
  registered = SERVER_NAME in (claudeConfig.mcpServers || {});
@@ -454,41 +822,108 @@ function status() {
454
822
  // Ignore unreadable config.
455
823
  }
456
824
 
457
- console.log(`Registration: ${registered ? 'active' : 'not registered'}`);
825
+ const report = {
826
+ generatedAt: new Date().toISOString(),
827
+ registered,
828
+ dataDir,
829
+ exists: existsSync(dataDir),
830
+ storedDimensions: null,
831
+ stats: null,
832
+ health: null,
833
+ lastConsolidation: null,
834
+ error: null,
835
+ };
458
836
 
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;
837
+ if (!report.exists) {
838
+ return report;
462
839
  }
463
840
 
464
841
  try {
465
- const dimensions = readStoredDimensions(DEFAULT_DATA_DIR) || 8;
842
+ report.storedDimensions = readStoredDimensions(dataDir);
843
+ const dimensions = report.storedDimensions || 8;
466
844
  const audrey = new Audrey({
467
- dataDir: DEFAULT_DATA_DIR,
845
+ dataDir,
468
846
  agent: 'status-check',
469
847
  embedding: { provider: 'mock', dimensions },
470
848
  });
471
- const stats = audrey.introspect();
472
- const health = audrey.memoryStatus();
473
- const lastConsolidation = audrey.db.prepare(`
849
+ report.stats = audrey.introspect();
850
+ report.health = audrey.memoryStatus();
851
+ report.lastConsolidation = audrey.db.prepare(`
474
852
  SELECT completed_at FROM consolidation_runs
475
853
  WHERE status = 'completed'
476
854
  ORDER BY completed_at DESC
477
855
  LIMIT 1
478
856
  `).get()?.completed_at || 'never';
479
857
  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
858
  } catch (err) {
491
- console.log(`Data directory: ${DEFAULT_DATA_DIR} (exists but could not read: ${err.message})`);
859
+ report.error = err.message || String(err);
860
+ }
861
+
862
+ return report;
863
+ }
864
+
865
+ export function formatStatusReport(report) {
866
+ const lines = [];
867
+ lines.push(`Registration: ${report.registered ? 'active' : 'not registered'}`);
868
+
869
+ if (!report.exists) {
870
+ lines.push(`Data directory: ${report.dataDir} (not yet created - will be created on first use)`);
871
+ return lines.join('\n');
872
+ }
873
+
874
+ if (report.error) {
875
+ lines.push(`Data directory: ${report.dataDir} (exists but could not read: ${report.error})`);
876
+ return lines.join('\n');
877
+ }
878
+
879
+ lines.push(`Data directory: ${report.dataDir}`);
880
+ lines.push(`Stored dimensions: ${report.storedDimensions ?? 'unknown'}`);
881
+ lines.push(
882
+ `Memories: ${report.stats.episodic} episodic, ${report.stats.semantic} semantic, ${report.stats.procedural} procedural`
883
+ );
884
+ lines.push(
885
+ `Index sync: ${report.health.vec_episodes}/${report.health.searchable_episodes} episodic, `
886
+ + `${report.health.vec_semantics}/${report.health.searchable_semantics} semantic, `
887
+ + `${report.health.vec_procedures}/${report.health.searchable_procedures} procedural`
888
+ );
889
+ lines.push(
890
+ `Health: ${report.health.healthy ? 'healthy' : 'unhealthy'}`
891
+ + `${report.health.reembed_recommended ? ' (re-embed recommended)' : ''}`
892
+ );
893
+ lines.push(`Dormant: ${report.stats.dormant}`);
894
+ lines.push(`Causal links: ${report.stats.causalLinks}`);
895
+ lines.push(`Contradictions: ${report.stats.contradictions.open} open, ${report.stats.contradictions.resolved} resolved`);
896
+ lines.push(`Consolidation runs: ${report.stats.totalConsolidationRuns}`);
897
+ lines.push(`Last consolidation: ${report.lastConsolidation}`);
898
+
899
+ return lines.join('\n');
900
+ }
901
+
902
+ export function runStatusCommand({
903
+ argv = process.argv,
904
+ dataDir = resolveDataDir(process.env),
905
+ claudeJsonPath = join(homedir(), '.claude.json'),
906
+ out = console.log,
907
+ } = {}) {
908
+ const report = buildStatusReport({ dataDir, claudeJsonPath });
909
+ if (cliHasFlag('--json', argv)) {
910
+ out(JSON.stringify(report, null, 2));
911
+ } else {
912
+ out(formatStatusReport(report));
913
+ }
914
+
915
+ const exitCode = report.error
916
+ || (cliHasFlag('--fail-on-unhealthy', argv) && report.exists && report.health && !report.health.healthy)
917
+ ? 1
918
+ : 0;
919
+
920
+ return { report, exitCode };
921
+ }
922
+
923
+ function status() {
924
+ const { exitCode } = runStatusCommand();
925
+ if (exitCode !== 0) {
926
+ process.exitCode = exitCode;
492
927
  }
493
928
  }
494
929
 
@@ -500,6 +935,61 @@ function toolError(err) {
500
935
  return { isError: true, content: [{ type: 'text', text: `Error: ${err.message || String(err)}` }] };
501
936
  }
502
937
 
938
+ export function registerShutdownHandlers(processRef, audrey, logger = console.error) {
939
+ let closed = false;
940
+
941
+ const shutdown = (message, exitCode = 0) => {
942
+ if (message) {
943
+ logger(message);
944
+ }
945
+ if (!closed) {
946
+ closed = true;
947
+ if (typeof audrey?.waitForIdle === 'function') {
948
+ Promise.resolve(audrey.waitForIdle())
949
+ .catch(err => {
950
+ logger(`[audrey-mcp] shutdown wait error: ${err.message || String(err)}`);
951
+ exitCode = exitCode === 0 ? 1 : exitCode;
952
+ })
953
+ .finally(() => {
954
+ try {
955
+ audrey.close();
956
+ } catch (err) {
957
+ logger(`[audrey-mcp] shutdown error: ${err.message || String(err)}`);
958
+ exitCode = exitCode === 0 ? 1 : exitCode;
959
+ }
960
+ if (typeof processRef.exit === 'function') {
961
+ processRef.exit(exitCode);
962
+ }
963
+ });
964
+ return;
965
+ }
966
+ try {
967
+ audrey.close();
968
+ } catch (err) {
969
+ logger(`[audrey-mcp] shutdown error: ${err.message || String(err)}`);
970
+ exitCode = exitCode === 0 ? 1 : exitCode;
971
+ }
972
+ }
973
+ if (typeof processRef.exit === 'function') {
974
+ processRef.exit(exitCode);
975
+ }
976
+ };
977
+
978
+ processRef.once('SIGINT', () => shutdown('[audrey-mcp] received SIGINT, shutting down'));
979
+ processRef.once('SIGTERM', () => shutdown('[audrey-mcp] received SIGTERM, shutting down'));
980
+ processRef.once('SIGHUP', () => shutdown('[audrey-mcp] received SIGHUP, shutting down'));
981
+ processRef.once('uncaughtException', err => {
982
+ logger('[audrey-mcp] uncaught exception:', err);
983
+ shutdown(null, 1);
984
+ });
985
+ processRef.once('unhandledRejection', reason => {
986
+ logger('[audrey-mcp] unhandled rejection:', reason);
987
+ shutdown(null, 1);
988
+ });
989
+
990
+ return shutdown;
991
+ }
992
+
503
993
  export function registerDreamTool(server, audrey) {
504
994
  server.tool(
505
995
  'memory_dream',
@@ -524,6 +1014,8 @@ export function registerDreamTool(server, audrey) {
524
1014
  }
525
1015
 
526
1016
  async function main() {
1017
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
1018
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
527
1019
  const config = buildAudreyConfig();
528
1020
  const audrey = new Audrey(config);
529
1021
 
@@ -683,12 +1175,7 @@ async function main() {
683
1175
  const transport = new StdioServerTransport();
684
1176
  await server.connect(transport);
685
1177
  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
- });
1178
+ registerShutdownHandlers(process, audrey);
692
1179
  }
693
1180
 
694
1181
  const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
@@ -718,6 +1205,55 @@ if (isDirectRun) {
718
1205
  console.error('[audrey] reflect failed:', err);
719
1206
  process.exit(1);
720
1207
  });
1208
+ } else if (subcommand === 'recall') {
1209
+ recall().catch(err => {
1210
+ console.error('[audrey] recall failed:', err);
1211
+ process.exit(1);
1212
+ });
1213
+ } else if (subcommand === 'hooks') {
1214
+ const hooksAction = process.argv[3];
1215
+ if (hooksAction === 'install') {
1216
+ hooksInstall();
1217
+ } else if (hooksAction === 'uninstall') {
1218
+ hooksUninstall();
1219
+ } else {
1220
+ console.error('Usage: npx audrey hooks [install|uninstall]');
1221
+ process.exit(1);
1222
+ }
1223
+ } else if (subcommand === 'snapshot') {
1224
+ snapshot().catch(err => {
1225
+ console.error('[audrey] snapshot failed:', err);
1226
+ process.exit(1);
1227
+ });
1228
+ } else if (subcommand === 'restore') {
1229
+ restore().catch(err => {
1230
+ console.error('[audrey] restore failed:', err);
1231
+ process.exit(1);
1232
+ });
1233
+ } else if (subcommand === 'serve') {
1234
+ import('./serve.js').then(({ startServer }) => {
1235
+ const port = process.argv[3] ? parseInt(process.argv[3], 10) : undefined;
1236
+ return startServer({ port });
1237
+ }).catch(err => {
1238
+ console.error('[audrey] serve failed:', err);
1239
+ process.exit(1);
1240
+ });
1241
+ } else if (subcommand === 'dashboard') {
1242
+ import('./serve.js').then(({ startServer }) => {
1243
+ const port = process.argv[3] ? parseInt(process.argv[3], 10) : undefined;
1244
+ return startServer({ port }).then(({ server }) => {
1245
+ const addr = server.address();
1246
+ const url = `http://localhost:${addr.port}/dashboard`;
1247
+ console.log(`[audrey] Opening dashboard: ${url}`);
1248
+ import('node:child_process').then(({ exec: execCmd }) => {
1249
+ const cmd = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`;
1250
+ execCmd(cmd);
1251
+ });
1252
+ });
1253
+ }).catch(err => {
1254
+ console.error('[audrey] dashboard failed:', err);
1255
+ process.exit(1);
1256
+ });
721
1257
  } else if (subcommand === 'status') {
722
1258
  status();
723
1259
  } else {