audrey 0.9.0 → 0.14.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,26 +1,42 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const VERSION = '0.9.0';
4
+ export const VERSION = '0.14.0';
5
5
  export const SERVER_NAME = 'audrey-memory';
6
6
  export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
7
7
 
8
+ /**
9
+ * Resolves which embedding provider to use.
10
+ * Priority: explicit config -> gemini (if GOOGLE_API_KEY exists) -> local
11
+ * OpenAI is NEVER auto-selected -- must be set explicitly via AUDREY_EMBEDDING_PROVIDER=openai.
12
+ */
13
+ export function resolveEmbeddingProvider(env, explicit) {
14
+ if (explicit && explicit !== 'auto') {
15
+ const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ? 3072 : 384;
16
+ const apiKey = explicit === 'gemini'
17
+ ? (env.GOOGLE_API_KEY || env.GEMINI_API_KEY)
18
+ : explicit === 'openai'
19
+ ? env.OPENAI_API_KEY
20
+ : undefined;
21
+ const result = { provider: explicit, apiKey, dimensions: dims };
22
+ if (explicit === 'local') result.device = env.AUDREY_DEVICE || 'gpu';
23
+ return result;
24
+ }
25
+ if (env.GOOGLE_API_KEY || env.GEMINI_API_KEY) {
26
+ return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions: 3072 };
27
+ }
28
+ return { provider: 'local', dimensions: 384, device: env.AUDREY_DEVICE || 'gpu' };
29
+ }
30
+
8
31
  export function buildAudreyConfig() {
9
32
  const dataDir = process.env.AUDREY_DATA_DIR || DEFAULT_DATA_DIR;
10
33
  const agent = process.env.AUDREY_AGENT || 'claude-code';
11
- const embProvider = process.env.AUDREY_EMBEDDING_PROVIDER || 'mock';
12
- const embDimensions = parseInt(process.env.AUDREY_EMBEDDING_DIMENSIONS || '8', 10);
34
+ const explicitProvider = process.env.AUDREY_EMBEDDING_PROVIDER;
13
35
  const llmProvider = process.env.AUDREY_LLM_PROVIDER;
14
36
 
15
- const config = {
16
- dataDir,
17
- agent,
18
- embedding: { provider: embProvider, dimensions: embDimensions },
19
- };
37
+ const embedding = resolveEmbeddingProvider(process.env, explicitProvider);
20
38
 
21
- if (embProvider === 'openai') {
22
- config.embedding.apiKey = process.env.OPENAI_API_KEY;
23
- }
39
+ const config = { dataDir, agent, embedding };
24
40
 
25
41
  if (llmProvider === 'anthropic') {
26
42
  config.llm = { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY };
@@ -36,13 +52,13 @@ export function buildAudreyConfig() {
36
52
  export function buildInstallArgs(env = process.env) {
37
53
  const envPairs = [`AUDREY_DATA_DIR=${DEFAULT_DATA_DIR}`];
38
54
 
39
- if (env.OPENAI_API_KEY) {
55
+ const embedding = resolveEmbeddingProvider(env);
56
+ if (embedding.provider === 'gemini') {
57
+ envPairs.push('AUDREY_EMBEDDING_PROVIDER=gemini');
58
+ envPairs.push(`GOOGLE_API_KEY=${embedding.apiKey}`);
59
+ } else if (embedding.provider === 'openai') {
40
60
  envPairs.push('AUDREY_EMBEDDING_PROVIDER=openai');
41
- envPairs.push('AUDREY_EMBEDDING_DIMENSIONS=1536');
42
61
  envPairs.push(`OPENAI_API_KEY=${env.OPENAI_API_KEY}`);
43
- } else {
44
- envPairs.push('AUDREY_EMBEDDING_PROVIDER=mock');
45
- envPairs.push('AUDREY_EMBEDDING_DIMENSIONS=8');
46
62
  }
47
63
 
48
64
  if (env.ANTHROPIC_API_KEY) {
@@ -8,7 +8,7 @@ import { existsSync, readFileSync } from 'node:fs';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { Audrey } from '../src/index.js';
10
10
  import { readStoredDimensions } from '../src/db.js';
11
- import { VERSION, SERVER_NAME, DEFAULT_DATA_DIR, buildAudreyConfig, buildInstallArgs } from './config.js';
11
+ import { VERSION, SERVER_NAME, DEFAULT_DATA_DIR, buildAudreyConfig, buildInstallArgs, resolveEmbeddingProvider } from './config.js';
12
12
 
13
13
  const VALID_SOURCES = ['direct-observation', 'told-by-user', 'tool-result', 'inference', 'model-generated'];
14
14
  const VALID_TYPES = ['episodic', 'semantic', 'procedural'];
@@ -19,6 +19,11 @@ if (subcommand === 'install') {
19
19
  install();
20
20
  } else if (subcommand === 'uninstall') {
21
21
  uninstall();
22
+ } else if (subcommand === 'reembed') {
23
+ reembed().catch(err => {
24
+ console.error('[audrey] reembed failed:', err);
25
+ process.exit(1);
26
+ });
22
27
  } else if (subcommand === 'status') {
23
28
  status();
24
29
  } else {
@@ -28,6 +33,28 @@ if (subcommand === 'install') {
28
33
  });
29
34
  }
30
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
+
31
58
  function install() {
32
59
  try {
33
60
  execFileSync('claude', ['--version'], { stdio: 'ignore' });
@@ -65,7 +92,7 @@ function install() {
65
92
  console.log(`
66
93
  Audrey registered as "${SERVER_NAME}" with Claude Code.
67
94
 
68
- 9 tools available in every session:
95
+ 12 tools available in every session:
69
96
  memory_encode — Store observations, facts, preferences
70
97
  memory_recall — Search memories by semantic similarity
71
98
  memory_consolidate — Extract principles from accumulated episodes
@@ -75,6 +102,9 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
75
102
  memory_import — Import a snapshot into a fresh database
76
103
  memory_forget — Forget a specific memory by ID or query
77
104
  memory_decay — Apply forgetting curves, transition low-confidence to dormant
105
+ memory_status — Check brain health (episode/vec sync, dimensions)
106
+ memory_reflect — Form lasting memories from a conversation
107
+ memory_greeting — Wake up as yourself: load identity, context, mood
78
108
 
79
109
  Data stored in: ${DEFAULT_DATA_DIR}
80
110
  Verify: claude mcp list
@@ -169,11 +199,12 @@ async function main() {
169
199
  arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
170
200
  label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
171
201
  }).optional().describe('Emotional affect — how this memory feels'),
202
+ private: z.boolean().optional().describe('If true, memory is only visible to the AI � excluded from public recall results'),
172
203
  },
173
- async ({ content, source, tags, salience, context, affect }) => {
204
+ async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
174
205
  try {
175
- const id = await audrey.encode({ content, source, tags, salience, context, affect });
176
- return toolResult({ id, content, source });
206
+ const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect });
207
+ return toolResult({ id, content, source, private: isPrivate ?? false });
177
208
  } catch (err) {
178
209
  return toolError(err);
179
210
  }
@@ -349,6 +380,52 @@ async function main() {
349
380
  },
350
381
  );
351
382
 
383
+ server.tool(
384
+ 'memory_status',
385
+ {},
386
+ async () => {
387
+ try {
388
+ const status = audrey.memoryStatus();
389
+ return toolResult(status);
390
+ } catch (err) {
391
+ return toolError(err);
392
+ }
393
+ },
394
+ );
395
+
396
+ server.tool(
397
+ 'memory_reflect',
398
+ {
399
+ turns: z.array(z.object({
400
+ role: z.string().describe('Message role: user or assistant'),
401
+ content: z.string().describe('Message content'),
402
+ })).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
403
+ },
404
+ async ({ turns }) => {
405
+ try {
406
+ const result = await audrey.reflect(turns);
407
+ return toolResult(result);
408
+ } catch (err) {
409
+ return toolError(err);
410
+ }
411
+ },
412
+ );
413
+
414
+ server.tool(
415
+ 'memory_greeting',
416
+ {
417
+ context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
418
+ },
419
+ async ({ context }) => {
420
+ try {
421
+ const briefing = await audrey.greeting({ context });
422
+ return toolResult(briefing);
423
+ } catch (err) {
424
+ return toolError(err);
425
+ }
426
+ },
427
+ );
428
+
352
429
  const transport = new StdioServerTransport();
353
430
  await server.connect(transport);
354
431
  console.error('[audrey-mcp] connected via stdio');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audrey",
3
- "version": "0.9.0",
3
+ "version": "0.14.0",
4
4
  "description": "Biological memory architecture for AI agents — encode, consolidate, and recall memories with confidence decay, contradiction detection, and causal graphs",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -63,7 +63,9 @@
63
63
  },
64
64
  "license": "MIT",
65
65
  "dependencies": {
66
+ "@huggingface/transformers": "^3.8.1",
66
67
  "@modelcontextprotocol/sdk": "^1.26.0",
68
+ "audrey": "^0.11.0",
67
69
  "better-sqlite3": "^12.6.2",
68
70
  "sqlite-vec": "^0.1.7-alpha.2",
69
71
  "ulid": "^3.0.2",
package/src/affect.js CHANGED
@@ -55,7 +55,7 @@ export async function detectResonance(db, embeddingProvider, episodeId, { conten
55
55
  priorAffect,
56
56
  semanticSimilarity: match.similarity,
57
57
  emotionalSimilarity,
58
- timeDelta: Date.now() - new Date(match.created_at).getTime(),
58
+ timeDeltaDays: Math.floor((Date.now() - new Date(match.created_at).getTime()) / 86400000),
59
59
  priorCreatedAt: match.created_at,
60
60
  });
61
61
  }
package/src/audrey.js CHANGED
@@ -10,7 +10,7 @@ import { applyDecay } from './decay.js';
10
10
  import { rollbackConsolidation, getConsolidationHistory } from './rollback.js';
11
11
  import { forgetMemory, forgetByQuery as forgetByQueryFn, purgeMemories } from './forget.js';
12
12
  import { introspect as introspectFn } from './introspect.js';
13
- import { buildContextResolutionPrompt } from './prompts.js';
13
+ import { buildContextResolutionPrompt, buildReflectionPrompt } from './prompts.js';
14
14
  import { exportMemories } from './export.js';
15
15
  import { importMemories } from './import.js';
16
16
  import { suggestConsolidationParams as suggestParamsFn } from './adaptive.js';
@@ -29,6 +29,8 @@ import { detectResonance } from './affect.js';
29
29
  * @property {{ trigger?: string, consequence?: string }} [causal]
30
30
  * @property {string[]} [tags]
31
31
  * @property {string} [supersedes]
32
+ * @property {Record<string, string>} [context]
33
+ * @property {{ valence?: number, arousal?: number, label?: string }} [affect]
32
34
  *
33
35
  * @typedef {Object} RecallOptions
34
36
  * @property {number} [minConfidence]
@@ -40,6 +42,8 @@ import { detectResonance } from './affect.js';
40
42
  * @property {string[]} [sources]
41
43
  * @property {string} [after]
42
44
  * @property {string} [before]
45
+ * @property {Record<string, string>} [context]
46
+ * @property {{ valence?: number, arousal?: number }} [mood]
43
47
  *
44
48
  * @typedef {Object} RecallResult
45
49
  * @property {string} id
@@ -94,6 +98,7 @@ export class Audrey extends EventEmitter {
94
98
  interference = {},
95
99
  context = {},
96
100
  affect = {},
101
+ autoReflect = false,
97
102
  } = {}) {
98
103
  super();
99
104
 
@@ -148,6 +153,7 @@ export class Audrey extends EventEmitter {
148
153
  affectThreshold: affect.resonance?.affectThreshold ?? 0.6,
149
154
  },
150
155
  };
156
+ this.autoReflect = autoReflect;
151
157
  }
152
158
 
153
159
  async _ensureMigrated() {
@@ -212,6 +218,48 @@ export class Audrey extends EventEmitter {
212
218
  return id;
213
219
  }
214
220
 
221
+
222
+ async reflect(turns) {
223
+ if (!this.llmProvider) return { encoded: 0, memories: [], skipped: 'no llm provider' };
224
+
225
+ const prompt = buildReflectionPrompt(turns);
226
+ let raw;
227
+ try {
228
+ raw = await this.llmProvider.chat(prompt);
229
+ } catch (err) {
230
+ this.emit('error', err);
231
+ return { encoded: 0, memories: [], skipped: 'llm error' };
232
+ }
233
+
234
+ let parsed;
235
+ try {
236
+ parsed = JSON.parse(raw);
237
+ } catch {
238
+ return { encoded: 0, memories: [], skipped: 'invalid llm response' };
239
+ }
240
+
241
+ const memories = parsed.memories ?? [];
242
+ let encoded = 0;
243
+ for (const mem of memories) {
244
+ if (!mem.content || !mem.source) continue;
245
+ try {
246
+ await this.encode({
247
+ content: mem.content,
248
+ source: mem.source,
249
+ salience: mem.salience,
250
+ tags: mem.tags,
251
+ private: mem.private ?? false,
252
+ affect: mem.affect ?? undefined,
253
+ });
254
+ encoded++;
255
+ } catch (err) {
256
+ this.emit('error', err);
257
+ }
258
+ }
259
+
260
+ return { encoded, memories };
261
+ }
262
+
215
263
  /**
216
264
  * @param {EncodeParams[]} paramsList
217
265
  * @returns {Promise<string[]>}
@@ -376,6 +424,94 @@ export class Audrey extends EventEmitter {
376
424
  return introspectFn(this.db);
377
425
  }
378
426
 
427
+ memoryStatus() {
428
+ const episodes = this.db.prepare('SELECT COUNT(*) as c FROM episodes').get().c;
429
+ const semantics = this.db.prepare('SELECT COUNT(*) as c FROM semantics').get().c;
430
+ const procedures = this.db.prepare('SELECT COUNT(*) as c FROM procedures').get().c;
431
+
432
+ let vecEpisodes = 0, vecSemantics = 0, vecProcedures = 0;
433
+ try {
434
+ vecEpisodes = this.db.prepare('SELECT COUNT(*) as c FROM vec_episodes').get().c;
435
+ vecSemantics = this.db.prepare('SELECT COUNT(*) as c FROM vec_semantics').get().c;
436
+ vecProcedures = this.db.prepare('SELECT COUNT(*) as c FROM vec_procedures').get().c;
437
+ } catch {
438
+ // vec tables may not exist if no dimensions configured
439
+ }
440
+
441
+ const dimsRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
442
+ const dimensions = dimsRow ? parseInt(dimsRow.value, 10) : null;
443
+ const versionRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
444
+ const schemaVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
445
+
446
+ const device = this.embeddingProvider._actualDevice
447
+ ?? this.embeddingProvider.device
448
+ ?? null;
449
+
450
+ const healthy = episodes === vecEpisodes
451
+ && semantics === vecSemantics
452
+ && procedures === vecProcedures;
453
+
454
+ return {
455
+ episodes,
456
+ vec_episodes: vecEpisodes,
457
+ semantics,
458
+ vec_semantics: vecSemantics,
459
+ procedures,
460
+ vec_procedures: vecProcedures,
461
+ dimensions,
462
+ schema_version: schemaVersion,
463
+ device,
464
+ healthy,
465
+ };
466
+ }
467
+
468
+ async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
469
+ const recent = this.db.prepare(
470
+ 'SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?'
471
+ ).all(recentLimit);
472
+
473
+ const principles = this.db.prepare(
474
+ 'SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?'
475
+ ).all('active', principleLimit);
476
+
477
+ const identity = this.db.prepare(
478
+ 'SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?'
479
+ ).all(identityLimit);
480
+
481
+ const unresolved = this.db.prepare(
482
+ "SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10"
483
+ ).all();
484
+
485
+ const rawAffectRows = this.db.prepare(
486
+ "SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20"
487
+ ).all();
488
+
489
+ const affectParsed = rawAffectRows
490
+ .map(r => { try { return JSON.parse(r.affect); } catch { return null; } })
491
+ .filter(a => a && a.valence !== undefined);
492
+
493
+ let mood;
494
+ if (affectParsed.length === 0) {
495
+ mood = { valence: 0, arousal: 0, samples: 0 };
496
+ } else {
497
+ const sumV = affectParsed.reduce((s, a) => s + a.valence, 0);
498
+ const sumA = affectParsed.reduce((s, a) => s + (a.arousal ?? 0), 0);
499
+ mood = {
500
+ valence: sumV / affectParsed.length,
501
+ arousal: sumA / affectParsed.length,
502
+ samples: affectParsed.length,
503
+ };
504
+ }
505
+
506
+ const result = { recent, principles, mood, unresolved, identity };
507
+
508
+ if (context) {
509
+ result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
510
+ }
511
+
512
+ return result;
513
+ }
514
+
379
515
  export() {
380
516
  return exportMemories(this.db);
381
517
  }