@jcyamacho/agent-memory 0.0.13 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +47 -104
  2. package/dist/index.js +539 -145
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -27,51 +27,17 @@ Codex CLI:
27
27
  codex mcp add memory -- npx -y @jcyamacho/agent-memory
28
28
  ```
29
29
 
30
- Example MCP server config:
31
-
32
- ```json
33
- {
34
- "mcpServers": {
35
- "memory": {
36
- "command": "npx",
37
- "args": [
38
- "-y",
39
- "@jcyamacho/agent-memory"
40
- ]
41
- }
42
- }
43
- }
44
- ```
45
-
46
- With a custom database path:
47
-
48
- ```json
49
- {
50
- "mcpServers": {
51
- "memory": {
52
- "command": "npx",
53
- "args": [
54
- "-y",
55
- "@jcyamacho/agent-memory"
56
- ],
57
- "env": {
58
- "AGENT_MEMORY_DB_PATH": "/absolute/path/to/memory.db"
59
- }
60
- }
61
- }
62
- }
63
- ```
64
-
65
30
  Optional LLM instructions to reinforce the MCP's built-in guidance:
66
31
 
67
32
  ```text
68
- Use `recall` at the start of every conversation and again mid-task before
69
- making design choices or picking conventions. Use `remember` when the user
70
- corrects your approach, a key decision is established, or you learn project
71
- context not obvious from the code. Before saving, recall to check whether a
72
- memory about the same fact already exists -- if so, use `revise` to update
73
- it instead of creating a duplicate. Use `forget` to remove memories that
74
- are wrong or no longer relevant. Always pass workspace.
33
+ Use `memory_recall` at conversation start and before design choices,
34
+ conventions, or edge cases. Query with 2-5 short anchor-heavy terms or exact
35
+ phrases, not
36
+ questions or sentences. `memory_recall` is lexical-first; if it misses, retry once
37
+ with overlapping alternate terms. Use `memory_remember` for one durable fact, then
38
+ use `memory_revise` instead of duplicates and `memory_forget` for wrong or obsolete
39
+ memories. Always pass workspace unless the memory is truly global. Git worktree
40
+ paths are canonicalized to the main repo root on save and recall.
75
41
  ```
76
42
 
77
43
  ## What It Stores
@@ -101,60 +67,10 @@ The web UI uses the same database as the MCP server.
101
67
 
102
68
  ## Tools
103
69
 
104
- ### `remember`
105
-
106
- Save durable context for later recall.
107
-
108
- Inputs:
109
-
110
- - `content` -> fact, preference, decision, or context to store
111
- - `workspace` -> repository or workspace path
112
-
113
- Output:
114
-
115
- - `id`
116
-
117
- ### `recall`
118
-
119
- Retrieve relevant memories for the current task.
120
-
121
- Inputs:
122
-
123
- - `terms` -> 2-5 distinctive terms or short phrases that should appear in the
124
- memory content; avoid full natural-language questions
125
- - `limit` -> maximum results to return
126
- - `workspace` -> workspace or repo path; biases ranking toward this workspace
127
- - `updated_after` -> ISO 8601 lower bound
128
- - `updated_before` -> ISO 8601 upper bound
129
-
130
- Output:
131
-
132
- - `results[]` with `id`, `content`, `score`, `workspace`, and `updated_at`
133
-
134
- ### `revise`
135
-
136
- Update the content of an existing memory.
137
-
138
- Inputs:
139
-
140
- - `id` -> the memory id from a previous recall result
141
- - `content` -> replacement content for the memory
142
-
143
- Output:
144
-
145
- - `id`, `updated_at`
146
-
147
- ### `forget`
148
-
149
- Permanently delete a memory.
150
-
151
- Inputs:
152
-
153
- - `id` -> the memory id from a previous recall result
154
-
155
- Output:
156
-
157
- - `id`, `deleted`
70
+ - `remember` saves durable facts, preferences, decisions, and project context.
71
+ - `recall` retrieves the most relevant saved memories.
72
+ - `revise` updates an existing memory when it becomes outdated.
73
+ - `forget` deletes a memory that is no longer relevant.
158
74
 
159
75
  ## How Ranking Works
160
76
 
@@ -163,18 +79,25 @@ memories:
163
79
 
164
80
  1. **Text relevance** is the primary signal -- memories whose content best
165
81
  matches your search terms rank highest.
166
- 2. **Workspace match** is a strong secondary signal. When you pass
82
+ 2. **Embedding similarity** is the next strongest signal. Recall builds an
83
+ embedding from your normalized search terms and boosts memories whose stored
84
+ embeddings are most semantically similar.
85
+ 3. **Workspace match** is a strong secondary signal. When you pass
167
86
  `workspace`, exact matches rank highest, sibling repositories get a small
168
87
  boost, and unrelated workspaces rank lowest.
169
- 3. **Global memories** (saved without a workspace) are treated as relevant
88
+ 4. **Global memories** (saved without a workspace) are treated as relevant
170
89
  everywhere. When you pass `workspace`, they rank below exact workspace
171
90
  matches and above sibling or unrelated repositories.
172
- 4. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
91
+ 5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
173
92
  ones when other signals are equal.
174
93
 
175
- If you omit `workspace`, recall falls back to text relevance and recency only.
176
- For best results, pass `workspace` whenever you have one. Save memories without
177
- a workspace only when they apply across all projects.
94
+ If you omit `workspace`, recall still uses text relevance, embedding similarity,
95
+ and recency. For best results, pass `workspace` whenever you have one. Save
96
+ memories without a workspace only when they apply across all projects.
97
+
98
+ When you save a memory from a git worktree, `agent-memory` stores the main repo
99
+ root as the workspace. `recall` applies the same normalization to incoming
100
+ workspace queries so linked worktrees still match repo-scoped memories exactly.
178
101
 
179
102
  ## Database location
180
103
 
@@ -196,8 +119,28 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
196
119
  - share a memory DB across multiple clients
197
120
  - store the DB somewhere easier to back up or inspect
198
121
 
199
- Beta note: schema changes are not migrated. If you are upgrading from an older
200
- beta, delete the existing memory DB and let the server create a new one.
122
+ ## Model cache location
123
+
124
+ By default, downloaded embedding model files are cached at:
125
+
126
+ ```text
127
+ ~/.config/agent-memory/models
128
+ ```
129
+
130
+ Override it with:
131
+
132
+ ```bash
133
+ AGENT_MEMORY_MODELS_CACHE_PATH=/absolute/path/to/models
134
+ ```
135
+
136
+ Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
137
+
138
+ - keep model artifacts out of `node_modules`
139
+ - share the model cache across reinstalls or multiple clients
140
+ - store model downloads somewhere easier to inspect or manage
141
+
142
+ Schema changes are migrated automatically, including workspace normalization for
143
+ existing git worktree memories when the original path can still be resolved.
201
144
 
202
145
  ## Development
203
146
 
package/dist/index.js CHANGED
@@ -2431,9 +2431,9 @@ var require_validate = __commonJS((exports) => {
2431
2431
  }
2432
2432
  }
2433
2433
  function returnResults(it) {
2434
- const { gen, schemaEnv, validateName, ValidationError, opts } = it;
2434
+ const { gen, schemaEnv, validateName, ValidationError: ValidationError2, opts } = it;
2435
2435
  if (schemaEnv.$async) {
2436
- gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${ValidationError}(${names_1.default.vErrors})`));
2436
+ gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${ValidationError2}(${names_1.default.vErrors})`));
2437
2437
  } else {
2438
2438
  gen.assign((0, codegen_1._)`${validateName}.errors`, names_1.default.vErrors);
2439
2439
  if (opts.unevaluated)
@@ -2783,14 +2783,14 @@ var require_validate = __commonJS((exports) => {
2783
2783
  var require_validation_error = __commonJS((exports) => {
2784
2784
  Object.defineProperty(exports, "__esModule", { value: true });
2785
2785
 
2786
- class ValidationError extends Error {
2786
+ class ValidationError2 extends Error {
2787
2787
  constructor(errors3) {
2788
2788
  super("validation failed");
2789
2789
  this.errors = errors3;
2790
2790
  this.ajv = this.validation = true;
2791
2791
  }
2792
2792
  }
2793
- exports.default = ValidationError;
2793
+ exports.default = ValidationError2;
2794
2794
  });
2795
2795
 
2796
2796
  // node_modules/ajv/dist/compile/ref_error.js
@@ -12464,13 +12464,14 @@ class StdioServerTransport {
12464
12464
  }
12465
12465
  }
12466
12466
  // package.json
12467
- var version2 = "0.0.13";
12467
+ var version2 = "0.0.17";
12468
12468
 
12469
12469
  // src/config.ts
12470
12470
  import { homedir } from "node:os";
12471
12471
  import { join } from "node:path";
12472
12472
  import { parseArgs } from "node:util";
12473
12473
  var AGENT_MEMORY_DB_PATH_ENV = "AGENT_MEMORY_DB_PATH";
12474
+ var AGENT_MEMORY_MODELS_CACHE_PATH_ENV = "AGENT_MEMORY_MODELS_CACHE_PATH";
12474
12475
  var DEFAULT_UI_PORT = 6580;
12475
12476
  function resolveConfig(environment = process.env, argv = process.argv.slice(2)) {
12476
12477
  const { values } = parseArgs({
@@ -12482,12 +12483,146 @@ function resolveConfig(environment = process.env, argv = process.argv.slice(2))
12482
12483
  strict: false
12483
12484
  });
12484
12485
  return {
12485
- databasePath: environment[AGENT_MEMORY_DB_PATH_ENV] || join(homedir(), ".config", "agent-memory", "memory.db"),
12486
+ databasePath: resolveDatabasePath(environment),
12487
+ modelsCachePath: resolveModelsCachePath(environment),
12486
12488
  uiMode: Boolean(values.ui),
12487
12489
  uiPort: Number(values.port) || DEFAULT_UI_PORT
12488
12490
  };
12489
12491
  }
12492
+ function resolveDatabasePath(environment = process.env) {
12493
+ return environment[AGENT_MEMORY_DB_PATH_ENV] || join(homedir(), ".config", "agent-memory", "memory.db");
12494
+ }
12495
+ function resolveModelsCachePath(environment = process.env) {
12496
+ return environment[AGENT_MEMORY_MODELS_CACHE_PATH_ENV] || join(homedir(), ".config", "agent-memory", "models");
12497
+ }
12498
+
12499
+ // src/embedding/service.ts
12500
+ import { mkdirSync } from "node:fs";
12501
+ import { pipeline, env as transformersEnv } from "@huggingface/transformers";
12502
+
12503
+ // src/errors.ts
12504
+ class MemoryError extends Error {
12505
+ code;
12506
+ constructor(code, message, options) {
12507
+ super(message, options);
12508
+ this.name = "MemoryError";
12509
+ this.code = code;
12510
+ }
12511
+ }
12512
+
12513
+ class ValidationError extends MemoryError {
12514
+ constructor(message) {
12515
+ super("VALIDATION_ERROR", message);
12516
+ this.name = "ValidationError";
12517
+ }
12518
+ }
12519
+
12520
+ class NotFoundError extends MemoryError {
12521
+ constructor(message) {
12522
+ super("NOT_FOUND", message);
12523
+ this.name = "NotFoundError";
12524
+ }
12525
+ }
12490
12526
 
12527
+ class PersistenceError extends MemoryError {
12528
+ constructor(message, options) {
12529
+ super("PERSISTENCE_ERROR", message, options);
12530
+ this.name = "PersistenceError";
12531
+ }
12532
+ }
12533
+
12534
+ // src/embedding/service.ts
12535
+ var DEFAULT_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
12536
+
12537
+ class EmbeddingService {
12538
+ options;
12539
+ extractorPromise;
12540
+ modelsCachePath;
12541
+ constructor(options = {}) {
12542
+ this.options = options;
12543
+ this.modelsCachePath = options.modelsCachePath ?? resolveModelsCachePath();
12544
+ }
12545
+ async createVector(text) {
12546
+ const normalizedText = text.trim();
12547
+ if (!normalizedText) {
12548
+ throw new ValidationError("Text is required.");
12549
+ }
12550
+ const extractor = await this.getExtractor();
12551
+ const embedding = await extractor(normalizedText);
12552
+ return normalizeVector(embedding.tolist());
12553
+ }
12554
+ getExtractor() {
12555
+ if (!this.extractorPromise) {
12556
+ configureModelsCache(this.modelsCachePath);
12557
+ this.extractorPromise = (this.options.createExtractor ?? createDefaultExtractor)();
12558
+ }
12559
+ return this.extractorPromise;
12560
+ }
12561
+ }
12562
+ function configureModelsCache(modelsCachePath) {
12563
+ mkdirSync(modelsCachePath, { recursive: true });
12564
+ transformersEnv.useFSCache = true;
12565
+ transformersEnv.cacheDir = modelsCachePath;
12566
+ }
12567
+ async function createDefaultExtractor() {
12568
+ const extractor = await pipeline("feature-extraction", DEFAULT_EMBEDDING_MODEL);
12569
+ return (text) => extractor(text, {
12570
+ pooling: "mean",
12571
+ normalize: true
12572
+ });
12573
+ }
12574
+ function normalizeVector(value) {
12575
+ if (value.length === 0) {
12576
+ throw new ValidationError("Embedding model returned an empty vector.");
12577
+ }
12578
+ const [firstItem] = value;
12579
+ if (typeof firstItem === "number") {
12580
+ return value.map((item) => {
12581
+ if (typeof item !== "number" || !Number.isFinite(item)) {
12582
+ throw new ValidationError("Embedding model returned a non-numeric vector.");
12583
+ }
12584
+ return item;
12585
+ });
12586
+ }
12587
+ if (Array.isArray(firstItem)) {
12588
+ return normalizeVector(firstItem);
12589
+ }
12590
+ throw new ValidationError("Embedding model returned an unexpected vector shape.");
12591
+ }
12592
+ // src/embedding/similarity.ts
12593
+ function compareVectors(left, right) {
12594
+ validateVector(left, "Left vector");
12595
+ validateVector(right, "Right vector");
12596
+ if (left.length !== right.length) {
12597
+ throw new ValidationError("Vectors must have the same length.");
12598
+ }
12599
+ let dotProduct = 0;
12600
+ let leftMagnitude = 0;
12601
+ let rightMagnitude = 0;
12602
+ for (const [index, leftValue] of left.entries()) {
12603
+ const rightValue = right[index];
12604
+ if (rightValue === undefined) {
12605
+ throw new ValidationError("Vectors must have the same length.");
12606
+ }
12607
+ dotProduct += leftValue * rightValue;
12608
+ leftMagnitude += leftValue * leftValue;
12609
+ rightMagnitude += rightValue * rightValue;
12610
+ }
12611
+ if (leftMagnitude === 0 || rightMagnitude === 0) {
12612
+ throw new ValidationError("Vectors must not have zero magnitude.");
12613
+ }
12614
+ return dotProduct / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
12615
+ }
12616
+ function validateVector(vector, label) {
12617
+ if (vector.length === 0) {
12618
+ throw new ValidationError(`${label} must not be empty.`);
12619
+ }
12620
+ for (const value of vector) {
12621
+ if (!Number.isFinite(value)) {
12622
+ throw new ValidationError(`${label} must contain only finite numbers.`);
12623
+ }
12624
+ }
12625
+ }
12491
12626
  // node_modules/zod/v3/helpers/util.js
12492
12627
  var util;
12493
12628
  (function(util2) {
@@ -19895,37 +20030,6 @@ var EMPTY_COMPLETION_RESULT = {
19895
20030
  }
19896
20031
  };
19897
20032
 
19898
- // src/errors.ts
19899
- class MemoryError extends Error {
19900
- code;
19901
- constructor(code, message, options) {
19902
- super(message, options);
19903
- this.name = "MemoryError";
19904
- this.code = code;
19905
- }
19906
- }
19907
-
19908
- class ValidationError extends MemoryError {
19909
- constructor(message) {
19910
- super("VALIDATION_ERROR", message);
19911
- this.name = "ValidationError";
19912
- }
19913
- }
19914
-
19915
- class NotFoundError extends MemoryError {
19916
- constructor(message) {
19917
- super("NOT_FOUND", message);
19918
- this.name = "NotFoundError";
19919
- }
19920
- }
19921
-
19922
- class PersistenceError extends MemoryError {
19923
- constructor(message, options) {
19924
- super("PERSISTENCE_ERROR", message, options);
19925
- this.name = "PersistenceError";
19926
- }
19927
- }
19928
-
19929
20033
  // src/mcp/tools/shared.ts
19930
20034
  function toMcpError(error2) {
19931
20035
  if (error2 instanceof McpError) {
@@ -19954,11 +20058,11 @@ function parseOptionalDate(value, fieldName) {
19954
20058
 
19955
20059
  // src/mcp/tools/forget.ts
19956
20060
  var forgetInputSchema = {
19957
- id: string2().describe("The id of the memory to delete. Use the id returned by a previous recall result.")
20061
+ id: string2().describe("The memory id to delete. Use an id returned by `recall`.")
19958
20062
  };
19959
20063
  function registerForgetTool(server, memory) {
19960
20064
  server.registerTool("forget", {
19961
- description: "Permanently delete a memory that is wrong, obsolete, or no longer relevant. Pass the memory id from a previous recall result.",
20065
+ description: 'Permanently delete a wrong or obsolete memory. Use `revise` instead when the fact still exists and only needs correction. Returns `<memory id="..." deleted="true" />`.',
19962
20066
  inputSchema: forgetInputSchema
19963
20067
  }, async ({ id }) => {
19964
20068
  try {
@@ -19977,12 +20081,13 @@ var toNormalizedScore = (value) => value;
19977
20081
 
19978
20082
  // src/ranking.ts
19979
20083
  var RETRIEVAL_SCORE_WEIGHT = 8;
20084
+ var EMBEDDING_SIMILARITY_WEIGHT = 5;
19980
20085
  var WORKSPACE_MATCH_WEIGHT = 4;
19981
- var RECENCY_WEIGHT = 1;
19982
- var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
20086
+ var RECENCY_WEIGHT = 2;
20087
+ var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + EMBEDDING_SIMILARITY_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
19983
20088
  var GLOBAL_WORKSPACE_SCORE = 0.5;
19984
20089
  var SIBLING_WORKSPACE_SCORE = 0.25;
19985
- function rerankSearchResults(results, workspace) {
20090
+ function rerankSearchResults(results, workspace, queryEmbedding) {
19986
20091
  if (results.length <= 1) {
19987
20092
  return results;
19988
20093
  }
@@ -19991,18 +20096,25 @@ function rerankSearchResults(results, workspace) {
19991
20096
  const minUpdatedAt = Math.min(...updatedAtTimes);
19992
20097
  const maxUpdatedAt = Math.max(...updatedAtTimes);
19993
20098
  return results.map((result) => {
20099
+ const embeddingSimilarityScore = computeEmbeddingSimilarityScore(result, queryEmbedding);
19994
20100
  const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
19995
20101
  const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
19996
- const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
20102
+ const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + embeddingSimilarityScore * EMBEDDING_SIMILARITY_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
19997
20103
  return {
19998
20104
  ...result,
19999
20105
  score: toNormalizedScore(combinedScore)
20000
20106
  };
20001
20107
  }).sort((a, b) => b.score - a.score);
20002
20108
  }
20109
+ function computeEmbeddingSimilarityScore(result, queryEmbedding) {
20110
+ return normalizeCosineSimilarity(compareVectors(result.embedding, queryEmbedding));
20111
+ }
20003
20112
  function normalizeWorkspacePath(value) {
20004
20113
  return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
20005
20114
  }
20115
+ function normalizeCosineSimilarity(value) {
20116
+ return (value + 1) / 2;
20117
+ }
20006
20118
  function computeWorkspaceScore(memoryWs, queryWs) {
20007
20119
  if (!queryWs) {
20008
20120
  return 0;
@@ -20033,24 +20145,36 @@ var MAX_LIST_LIMIT = 100;
20033
20145
 
20034
20146
  class MemoryService {
20035
20147
  repository;
20036
- constructor(repository) {
20148
+ embeddingService;
20149
+ workspaceResolver;
20150
+ constructor(repository, embeddingService, workspaceResolver) {
20037
20151
  this.repository = repository;
20152
+ this.embeddingService = embeddingService;
20153
+ this.workspaceResolver = workspaceResolver;
20038
20154
  }
20039
20155
  async create(input) {
20040
20156
  const content = input.content.trim();
20041
20157
  if (!content) {
20042
20158
  throw new ValidationError("Memory content is required.");
20043
20159
  }
20044
- return this.repository.create({
20160
+ const workspace = await this.workspaceResolver.resolve(input.workspace);
20161
+ const memory = await this.repository.create({
20045
20162
  content,
20046
- workspace: normalizeOptionalString(input.workspace)
20163
+ embedding: await this.embeddingService.createVector(content),
20164
+ workspace
20047
20165
  });
20166
+ return toPublicMemoryRecord(memory);
20048
20167
  }
20049
20168
  async update(input) {
20050
20169
  const content = input.content.trim();
20051
20170
  if (!content)
20052
20171
  throw new ValidationError("Memory content is required.");
20053
- return this.repository.update({ id: input.id, content });
20172
+ const memory = await this.repository.update({
20173
+ id: input.id,
20174
+ content,
20175
+ embedding: await this.embeddingService.createVector(content)
20176
+ });
20177
+ return toPublicMemoryRecord(memory);
20054
20178
  }
20055
20179
  async delete(input) {
20056
20180
  const id = input.id.trim();
@@ -20059,16 +20183,18 @@ class MemoryService {
20059
20183
  return this.repository.delete({ id });
20060
20184
  }
20061
20185
  async get(id) {
20062
- return this.repository.get(id);
20186
+ const memory = await this.repository.get(id);
20187
+ return memory ? toPublicMemoryRecord(memory) : undefined;
20063
20188
  }
20064
20189
  async list(input) {
20065
- const workspace = normalizeOptionalString(input.workspace);
20066
- return this.repository.list({
20190
+ const workspace = await this.workspaceResolver.resolve(input.workspace);
20191
+ const page = await this.repository.list({
20067
20192
  workspace,
20068
20193
  workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
20069
20194
  offset: normalizeOffset(input.offset),
20070
20195
  limit: normalizeListLimit(input.limit)
20071
20196
  });
20197
+ return toPublicMemoryPage(page);
20072
20198
  }
20073
20199
  async listWorkspaces() {
20074
20200
  return this.repository.listWorkspaces();
@@ -20079,17 +20205,45 @@ class MemoryService {
20079
20205
  throw new ValidationError("At least one search term is required.");
20080
20206
  }
20081
20207
  const requestedLimit = normalizeLimit(input.limit);
20082
- const workspace = normalizeOptionalString(input.workspace);
20208
+ const workspace = await this.workspaceResolver.resolve(input.workspace);
20083
20209
  const normalizedQuery = {
20084
20210
  terms,
20085
20211
  limit: requestedLimit * RECALL_CANDIDATE_LIMIT_MULTIPLIER,
20086
20212
  updatedAfter: input.updatedAfter,
20087
20213
  updatedBefore: input.updatedBefore
20088
20214
  };
20089
- const results = await this.repository.search(normalizedQuery);
20090
- return rerankSearchResults(results, workspace).slice(0, requestedLimit);
20215
+ const [results, queryEmbedding] = await Promise.all([
20216
+ this.repository.search(normalizedQuery),
20217
+ this.embeddingService.createVector(terms.join(" "))
20218
+ ]);
20219
+ return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map(toPublicSearchResult);
20091
20220
  }
20092
20221
  }
20222
+ function toPublicMemoryRecord(memory) {
20223
+ return {
20224
+ id: memory.id,
20225
+ content: memory.content,
20226
+ workspace: memory.workspace,
20227
+ createdAt: memory.createdAt,
20228
+ updatedAt: memory.updatedAt
20229
+ };
20230
+ }
20231
+ function toPublicSearchResult(result) {
20232
+ return {
20233
+ id: result.id,
20234
+ content: result.content,
20235
+ score: result.score,
20236
+ workspace: result.workspace,
20237
+ createdAt: result.createdAt,
20238
+ updatedAt: result.updatedAt
20239
+ };
20240
+ }
20241
+ function toPublicMemoryPage(page) {
20242
+ return {
20243
+ items: page.items.map(toPublicMemoryRecord),
20244
+ hasMore: page.hasMore
20245
+ };
20246
+ }
20093
20247
  function normalizeLimit(value) {
20094
20248
  if (value === undefined) {
20095
20249
  return DEFAULT_RECALL_LIMIT;
@@ -20108,10 +20262,6 @@ function normalizeListLimit(value) {
20108
20262
  }
20109
20263
  return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
20110
20264
  }
20111
- function normalizeOptionalString(value) {
20112
- const trimmed = value?.trim();
20113
- return trimmed ? trimmed : undefined;
20114
- }
20115
20265
  function normalizeTerms(terms) {
20116
20266
  const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
20117
20267
  return [...new Set(normalizedTerms)];
@@ -20119,22 +20269,23 @@ function normalizeTerms(terms) {
20119
20269
 
20120
20270
  // src/mcp/tools/recall.ts
20121
20271
  var recallInputSchema = {
20122
- terms: array(string2()).min(1).describe("Search terms used to find relevant memories. Pass 2-5 short, distinctive items as separate array entries. Be specific: instead of 'preferences' or 'context', name the actual topic -- e.g. 'error handling', 'commit format', 'testing strategy'. Do not repeat the project or workspace name here -- use the workspace parameter for project scoping. Avoid full sentences."),
20123
- limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum number of matches to return. Keep this small when you only need the strongest hits."),
20124
- workspace: string2().optional().describe("Always pass the current working directory. Biases ranking toward the active project while still allowing cross-workspace matches. Memories saved without a workspace are treated as global and rank between matching and non-matching results."),
20125
- updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp. Use it when you need to narrow recall to newer context."),
20126
- updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp. Use it when you need to narrow recall to older context.")
20272
+ terms: array(string2()).min(1).describe("Search terms for lexical memory lookup. Pass 2-5 short anchor-heavy terms or exact phrases as separate entries. Prefer identifiers, commands, file paths, package names, and conventions likely to appear verbatim in the memory. Avoid vague words, full sentences, and repeating the workspace name. If recall misses, retry once with overlapping alternate terms."),
20273
+ limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum matches to return. Keep this small when you only need the strongest hits."),
20274
+ workspace: string2().optional().describe("Pass the current working directory. Git worktree paths are normalized to the main repo root for matching. This strongly boosts memories from the active project while still allowing global and cross-workspace matches."),
20275
+ updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp."),
20276
+ updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp.")
20127
20277
  };
20128
20278
  function toMemoryXml(r) {
20129
20279
  const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
20130
20280
  const content = escapeXml(r.content);
20131
- return `<memory id="${r.id}" score="${r.score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
20281
+ const score = Number(r.score.toFixed(3)).toString();
20282
+ return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
20132
20283
  ${content}
20133
20284
  </memory>`;
20134
20285
  }
20135
20286
  function registerRecallTool(server, memory) {
20136
20287
  server.registerTool("recall", {
20137
- description: "Search memories for prior decisions, corrections, and context that cannot be derived from code or git history. Call at the start of every conversation and again mid-task when you are about to make a design choice, pick a convention, or handle an edge case that the user may have guided before. Always pass workspace.",
20288
+ description: "Retrieve relevant memories for the current task. Use at conversation start and before design choices, conventions, or edge cases. Query with 2-5 short anchor-heavy terms or exact phrases, not questions or full sentences. `recall` is lexical-first; semantic reranking only reorders lexical matches. If it misses, retry once with overlapping alternate terms. Pass workspace; git worktree paths are normalized to the main repo root for matching. Returns `<memories>...</memories>` or a no-match hint.",
20138
20289
  inputSchema: recallInputSchema
20139
20290
  }, async ({ terms, limit, workspace, updated_after, updated_before }) => {
20140
20291
  try {
@@ -20145,7 +20296,7 @@ function registerRecallTool(server, memory) {
20145
20296
  updatedAfter: parseOptionalDate(updated_after, "updated_after"),
20146
20297
  updatedBefore: parseOptionalDate(updated_before, "updated_before")
20147
20298
  });
20148
- const text = results.length === 0 ? "No matching memories found." : `<memories>
20299
+ const text = results.length === 0 ? "No matching memories found. Retry once with 1-3 alternate overlapping terms or an exact phrase likely to appear in the memory text. Recall is lexical-first, so semantic reranking cannot rescue a query with no wording overlap." : `<memories>
20149
20300
  ${results.map(toMemoryXml).join(`
20150
20301
  `)}
20151
20302
  </memories>`;
@@ -20160,12 +20311,12 @@ ${results.map(toMemoryXml).join(`
20160
20311
 
20161
20312
  // src/mcp/tools/remember.ts
20162
20313
  var rememberInputSchema = {
20163
- content: string2().describe("The fact, preference, decision, or context to remember. Use a single self-contained sentence or short note. One fact per memory."),
20164
- workspace: string2().optional().describe("Always pass the current working directory to scope this memory to a project. Omit only when the memory applies across all projects (global preference).")
20314
+ content: string2().describe("One durable fact to save. Use a single self-contained sentence or short note with concrete nouns, identifiers, commands, file paths, or exact phrases the agent is likely to reuse."),
20315
+ workspace: string2().optional().describe("Pass the current working directory for project-specific memories. Git worktree paths are saved as the main repo root automatically. Omit only for truly global memories.")
20165
20316
  };
20166
20317
  function registerRememberTool(server, memory) {
20167
20318
  server.registerTool("remember", {
20168
- description: "Save durable context for later recall. Use this when the user corrects your approach, states a preference, a key decision or convention is established, or you learn project context not obvious from the code. Store one concise fact per memory. Do not store secrets, ephemeral task state, or information already in the codebase.",
20319
+ description: 'Save one durable memory for later recall. Use when the user states a stable preference, corrects you, or establishes reusable project context not obvious from code or git history. Save one fact per memory. Call `recall` first; use `revise` instead of creating duplicates. Do not store secrets, temporary task state, or codebase facts. Returns `<memory id="..." />`.',
20169
20320
  inputSchema: rememberInputSchema
20170
20321
  }, async ({ content, workspace }) => {
20171
20322
  try {
@@ -20184,12 +20335,12 @@ function registerRememberTool(server, memory) {
20184
20335
 
20185
20336
  // src/mcp/tools/revise.ts
20186
20337
  var reviseInputSchema = {
20187
- id: string2().describe("The id of the memory to update. Use the id returned by a previous recall result."),
20188
- content: string2().describe("The replacement content for the memory. Use a single self-contained sentence or short note. One fact per memory.")
20338
+ id: string2().describe("The memory id to update. Use an id returned by `recall`."),
20339
+ content: string2().describe("The corrected replacement for that same fact. Keep it to one durable fact.")
20189
20340
  };
20190
20341
  function registerReviseTool(server, memory) {
20191
20342
  server.registerTool("revise", {
20192
- description: "Update the content of an existing memory. Use when a previously saved memory is outdated or inaccurate and needs correction rather than deletion. Pass the memory id from a previous recall result.",
20343
+ description: 'Replace one existing memory with corrected wording. Use after `recall` when the same fact still applies but details changed. Do not append unrelated facts or merge memories. Returns `<memory id="..." updated_at="..." />`.',
20193
20344
  inputSchema: reviseInputSchema
20194
20345
  }, async ({ id, content }) => {
20195
20346
  try {
@@ -20210,14 +20361,15 @@ function registerReviseTool(server, memory) {
20210
20361
 
20211
20362
  // src/mcp/server.ts
20212
20363
  var SERVER_INSTRUCTIONS = [
20213
- "Stores decisions, corrections, and context that cannot be derived from code or git history.",
20214
- "Use `recall` at the start of every conversation and again mid-task before making design choices or picking conventions the user may have guided before.",
20215
- "Use `remember` when the user corrects your approach, states a preference, a key decision is established, or you learn project context not obvious from the code.",
20216
- "Before saving a new memory, recall to check whether a memory about the same fact already exists. If so, use `revise` to update it instead of creating a duplicate.",
20217
- "Use `revise` when a previously saved memory is outdated or inaccurate and needs correction rather than deletion.",
20218
- "Use `forget` to remove memories that are wrong, obsolete, or no longer relevant.",
20219
- "Always pass workspace (the current working directory) to scope results to the active project.",
20220
- "Omit workspace only when saving a memory that applies across all projects."
20364
+ "Use this server for durable memory: user preferences, corrections, decisions, and project context not obvious from code or git history.",
20365
+ "Use `recall` at conversation start and before design choices, conventions, or edge cases.",
20366
+ "Query `recall` with 2-5 short anchor-heavy terms or exact phrases likely to appear verbatim in memory text: identifiers, commands, file paths, and conventions.",
20367
+ "`recall` is lexical-first; semantic reranking only reorders lexical matches.",
20368
+ "If `recall` misses, retry once with overlapping alternate terms.",
20369
+ "Use `remember` for one durable fact when the user states a preference, corrects you, or a reusable project decision becomes clear.",
20370
+ "Call `recall` before `remember`; if the fact already exists, use `revise` instead of creating a duplicate.",
20371
+ "Use `revise` to correct an existing memory and `forget` to remove a wrong or obsolete one.",
20372
+ "Pass workspace for project-scoped calls. Git worktree paths are canonicalized to the main repo root on save and recall. Omit workspace only for truly global memories."
20221
20373
  ].join(" ");
20222
20374
  function createMcpServer(memory, version3) {
20223
20375
  const server = new McpServer({
@@ -20233,72 +20385,262 @@ function createMcpServer(memory, version3) {
20233
20385
  return server;
20234
20386
  }
20235
20387
 
20236
- // src/sqlite-db.ts
20237
- import { mkdirSync } from "node:fs";
20388
+ // src/sqlite/db.ts
20389
+ import { mkdirSync as mkdirSync2 } from "node:fs";
20238
20390
  import { dirname } from "node:path";
20239
20391
  import Database from "better-sqlite3";
20240
- var MEMORY_SCHEMA = `
20241
- CREATE TABLE IF NOT EXISTS memories (
20242
- id TEXT PRIMARY KEY,
20243
- content TEXT NOT NULL,
20244
- workspace TEXT,
20245
- created_at INTEGER NOT NULL,
20246
- updated_at INTEGER NOT NULL
20247
- );
20248
-
20249
- CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
20250
- CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
20251
-
20252
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
20253
- content,
20254
- content = 'memories',
20255
- content_rowid = 'rowid',
20256
- tokenize = 'porter unicode61'
20257
- );
20258
-
20259
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
20260
- INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20261
- END;
20262
-
20263
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
20264
- INSERT INTO memories_fts(memories_fts, rowid, content)
20265
- VALUES ('delete', old.rowid, old.content);
20266
- END;
20267
-
20268
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
20269
- INSERT INTO memories_fts(memories_fts, rowid, content)
20270
- VALUES ('delete', old.rowid, old.content);
20271
- INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20272
- END;
20273
- `;
20274
- function openMemoryDatabase(databasePath) {
20392
+
20393
+ // src/sqlite/memory-schema.ts
20394
+ function createMemoriesTable(database, options) {
20395
+ const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
20396
+ database.exec(`
20397
+ CREATE TABLE IF NOT EXISTS memories (
20398
+ id TEXT PRIMARY KEY,
20399
+ content TEXT NOT NULL,
20400
+ workspace TEXT,
20401
+ ${embeddingColumn}
20402
+ created_at INTEGER NOT NULL,
20403
+ updated_at INTEGER NOT NULL
20404
+ );
20405
+ `);
20406
+ }
20407
+ function createMemoryIndexes(database) {
20408
+ database.exec(`
20409
+ CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
20410
+ CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
20411
+ `);
20412
+ }
20413
+ function createMemorySearchArtifacts(database, rebuild = false) {
20414
+ database.exec(`
20415
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
20416
+ content,
20417
+ content = 'memories',
20418
+ content_rowid = 'rowid',
20419
+ tokenize = 'porter unicode61'
20420
+ );
20421
+
20422
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
20423
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20424
+ END;
20425
+
20426
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
20427
+ INSERT INTO memories_fts(memories_fts, rowid, content)
20428
+ VALUES ('delete', old.rowid, old.content);
20429
+ END;
20430
+
20431
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
20432
+ INSERT INTO memories_fts(memories_fts, rowid, content)
20433
+ VALUES ('delete', old.rowid, old.content);
20434
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20435
+ END;
20436
+ `);
20437
+ if (rebuild) {
20438
+ database.exec("INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')");
20439
+ }
20440
+ }
20441
+ function dropMemorySearchArtifacts(database) {
20442
+ database.exec(`
20443
+ DROP TRIGGER IF EXISTS memories_ai;
20444
+ DROP TRIGGER IF EXISTS memories_ad;
20445
+ DROP TRIGGER IF EXISTS memories_au;
20446
+ DROP TABLE IF EXISTS memories_fts;
20447
+ `);
20448
+ }
20449
+ function getEmbeddingColumnSql(mode) {
20450
+ switch (mode) {
20451
+ case "omit":
20452
+ return "";
20453
+ case "nullable":
20454
+ return `embedding BLOB,
20455
+ `;
20456
+ case "required":
20457
+ return `embedding BLOB NOT NULL,
20458
+ `;
20459
+ }
20460
+ }
20461
+
20462
+ // src/sqlite/migrations/001-create-memory-schema.ts
20463
+ var createMemorySchemaMigration = {
20464
+ version: 1,
20465
+ async up(database) {
20466
+ createMemoriesTable(database, { embeddingColumn: "omit" });
20467
+ createMemoryIndexes(database);
20468
+ createMemorySearchArtifacts(database);
20469
+ }
20470
+ };
20471
+
20472
+ // src/sqlite/embedding-codec.ts
20473
+ var FLOAT32_BYTE_WIDTH = 4;
20474
+ function encodeEmbedding(vector) {
20475
+ const typedArray = Float32Array.from(vector);
20476
+ return new Uint8Array(typedArray.buffer.slice(0));
20477
+ }
20478
+ function decodeEmbedding(value) {
20479
+ const bytes = toUint8Array(value);
20480
+ if (bytes.byteLength === 0) {
20481
+ throw new Error("Embedding blob is empty.");
20482
+ }
20483
+ if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
20484
+ throw new Error("Embedding blob length is not a multiple of 4.");
20485
+ }
20486
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
20487
+ const vector = [];
20488
+ for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
20489
+ vector.push(view.getFloat32(offset, true));
20490
+ }
20491
+ return vector;
20492
+ }
20493
+ function toUint8Array(value) {
20494
+ if (value instanceof Uint8Array) {
20495
+ return value;
20496
+ }
20497
+ if (value instanceof ArrayBuffer) {
20498
+ return new Uint8Array(value);
20499
+ }
20500
+ throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
20501
+ }
20502
+
20503
+ // src/sqlite/migrations/002-add-memory-embedding.ts
20504
+ function createAddMemoryEmbeddingMigration(embeddingService) {
20505
+ return {
20506
+ version: 2,
20507
+ async up(database) {
20508
+ database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
20509
+ const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
20510
+ const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
20511
+ for (const row of rows) {
20512
+ const embedding = await embeddingService.createVector(row.content);
20513
+ updateStatement.run(encodeEmbedding(embedding), row.id);
20514
+ }
20515
+ const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
20516
+ if ((nullRows[0]?.count ?? 0) > 0) {
20517
+ throw new Error("Failed to backfill embeddings for all memories.");
20518
+ }
20519
+ dropMemorySearchArtifacts(database);
20520
+ database.exec("ALTER TABLE memories RENAME TO memories_old");
20521
+ createMemoriesTable(database, { embeddingColumn: "required" });
20522
+ database.exec(`
20523
+ INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
20524
+ SELECT id, content, workspace, embedding, created_at, updated_at
20525
+ FROM memories_old
20526
+ `);
20527
+ database.exec("DROP TABLE memories_old");
20528
+ createMemoryIndexes(database);
20529
+ createMemorySearchArtifacts(database, true);
20530
+ }
20531
+ };
20532
+ }
20533
+
20534
+ // src/sqlite/migrations/003-normalize-workspaces.ts
20535
+ function createNormalizeWorkspaceMigration(workspaceResolver) {
20536
+ return {
20537
+ version: 3,
20538
+ async up(database) {
20539
+ const rows = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace").all();
20540
+ const updateStatement = database.prepare("UPDATE memories SET workspace = ? WHERE workspace = ?");
20541
+ for (const row of rows) {
20542
+ const normalizedWorkspace = await workspaceResolver.resolve(row.workspace);
20543
+ if (!normalizedWorkspace || normalizedWorkspace === row.workspace) {
20544
+ continue;
20545
+ }
20546
+ updateStatement.run(normalizedWorkspace, row.workspace);
20547
+ }
20548
+ }
20549
+ };
20550
+ }
20551
+
20552
+ // src/sqlite/migrations/index.ts
20553
+ function createMemoryMigrations(options) {
20554
+ return [
20555
+ createMemorySchemaMigration,
20556
+ createAddMemoryEmbeddingMigration(options.embeddingService),
20557
+ createNormalizeWorkspaceMigration(options.workspaceResolver)
20558
+ ];
20559
+ }
20560
+
20561
+ // src/sqlite/db.ts
20562
+ var PRAGMA_STATEMENTS = [
20563
+ "journal_mode = WAL",
20564
+ "synchronous = NORMAL",
20565
+ "foreign_keys = ON",
20566
+ "busy_timeout = 5000"
20567
+ ];
20568
+ async function openMemoryDatabase(databasePath, options) {
20569
+ let database;
20275
20570
  try {
20276
- mkdirSync(dirname(databasePath), { recursive: true });
20277
- const database = new Database(databasePath);
20278
- initializeMemoryDatabase(database);
20571
+ mkdirSync2(dirname(databasePath), { recursive: true });
20572
+ database = new Database(databasePath);
20573
+ await initializeMemoryDatabase(database, createMemoryMigrations(options));
20279
20574
  return database;
20280
20575
  } catch (error2) {
20576
+ database?.close();
20577
+ if (error2 instanceof PersistenceError) {
20578
+ throw error2;
20579
+ }
20281
20580
  throw new PersistenceError("Failed to initialize the SQLite database.", {
20282
20581
  cause: error2
20283
20582
  });
20284
20583
  }
20285
20584
  }
20286
- function initializeMemoryDatabase(database) {
20585
+ async function initializeMemoryDatabase(database, migrations) {
20586
+ try {
20587
+ applyPragmas(database);
20588
+ await runSqliteMigrations(database, migrations);
20589
+ } catch (error2) {
20590
+ throw new PersistenceError("Failed to initialize the SQLite database.", {
20591
+ cause: error2
20592
+ });
20593
+ }
20594
+ }
20595
+ async function runSqliteMigrations(database, migrations) {
20596
+ validateMigrations(migrations);
20597
+ let currentVersion = getUserVersion(database);
20598
+ for (const migration of migrations) {
20599
+ if (migration.version <= currentVersion) {
20600
+ continue;
20601
+ }
20602
+ database.exec("BEGIN");
20603
+ try {
20604
+ await migration.up(database);
20605
+ setUserVersion(database, migration.version);
20606
+ database.exec("COMMIT");
20607
+ currentVersion = migration.version;
20608
+ } catch (error2) {
20609
+ try {
20610
+ database.exec("ROLLBACK");
20611
+ } catch {}
20612
+ throw error2;
20613
+ }
20614
+ }
20615
+ }
20616
+ function applyPragmas(database) {
20287
20617
  if (database.pragma) {
20288
- database.pragma("journal_mode = WAL");
20289
- database.pragma("synchronous = NORMAL");
20290
- database.pragma("foreign_keys = ON");
20291
- database.pragma("busy_timeout = 5000");
20292
- } else {
20293
- database.exec("PRAGMA journal_mode = WAL");
20294
- database.exec("PRAGMA synchronous = NORMAL");
20295
- database.exec("PRAGMA foreign_keys = ON");
20296
- database.exec("PRAGMA busy_timeout = 5000");
20618
+ for (const statement of PRAGMA_STATEMENTS) {
20619
+ database.pragma(statement);
20620
+ }
20621
+ return;
20622
+ }
20623
+ for (const statement of PRAGMA_STATEMENTS) {
20624
+ database.exec(`PRAGMA ${statement}`);
20297
20625
  }
20298
- database.exec(MEMORY_SCHEMA);
20299
20626
  }
20300
-
20301
- // src/sqlite-memory-repository.ts
20627
+ function getUserVersion(database) {
20628
+ const rows = database.prepare("PRAGMA user_version").all();
20629
+ return rows[0]?.user_version ?? 0;
20630
+ }
20631
+ function setUserVersion(database, version3) {
20632
+ database.exec(`PRAGMA user_version = ${version3}`);
20633
+ }
20634
+ function validateMigrations(migrations) {
20635
+ let previousVersion = 0;
20636
+ for (const migration of migrations) {
20637
+ if (!Number.isInteger(migration.version) || migration.version <= previousVersion) {
20638
+ throw new Error("SQLite migrations must use strictly increasing versions.");
20639
+ }
20640
+ previousVersion = migration.version;
20641
+ }
20642
+ }
20643
+ // src/sqlite/repository.ts
20302
20644
  import { randomUUID } from "node:crypto";
20303
20645
  var DEFAULT_SEARCH_LIMIT = 15;
20304
20646
  var DEFAULT_LIST_LIMIT2 = 15;
@@ -20317,6 +20659,7 @@ class SqliteMemoryRepository {
20317
20659
  id,
20318
20660
  content,
20319
20661
  workspace,
20662
+ embedding,
20320
20663
  created_at,
20321
20664
  updated_at
20322
20665
  ) VALUES (
@@ -20324,11 +20667,12 @@ class SqliteMemoryRepository {
20324
20667
  ?,
20325
20668
  ?,
20326
20669
  ?,
20670
+ ?,
20327
20671
  ?
20328
20672
  )
20329
20673
  `);
20330
- this.getStatement = database.prepare("SELECT id, content, workspace, created_at, updated_at FROM memories WHERE id = ?");
20331
- this.updateStatement = database.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?");
20674
+ this.getStatement = database.prepare("SELECT id, content, workspace, embedding, created_at, updated_at FROM memories WHERE id = ?");
20675
+ this.updateStatement = database.prepare("UPDATE memories SET content = ?, embedding = ?, updated_at = ? WHERE id = ?");
20332
20676
  this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
20333
20677
  this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
20334
20678
  }
@@ -20338,11 +20682,12 @@ class SqliteMemoryRepository {
20338
20682
  const memory = {
20339
20683
  id: randomUUID(),
20340
20684
  content: input.content,
20685
+ embedding: input.embedding,
20341
20686
  workspace: input.workspace,
20342
20687
  createdAt: now,
20343
20688
  updatedAt: now
20344
20689
  };
20345
- this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
20690
+ this.insertStatement.run(memory.id, memory.content, memory.workspace, encodeEmbedding(memory.embedding), memory.createdAt.getTime(), memory.updatedAt.getTime());
20346
20691
  return memory;
20347
20692
  } catch (error2) {
20348
20693
  throw new PersistenceError("Failed to save memory.", { cause: error2 });
@@ -20367,6 +20712,7 @@ class SqliteMemoryRepository {
20367
20712
  m.id,
20368
20713
  m.content,
20369
20714
  m.workspace,
20715
+ m.embedding,
20370
20716
  m.created_at,
20371
20717
  m.updated_at,
20372
20718
  MAX(0, -bm25(memories_fts)) AS score
@@ -20379,7 +20725,7 @@ class SqliteMemoryRepository {
20379
20725
  const rows = statement.all(...params);
20380
20726
  const maxScore = Math.max(...rows.map((row) => row.score), 0);
20381
20727
  return rows.map((row) => ({
20382
- ...toMemoryRecord(row),
20728
+ ...toMemoryEntity(row),
20383
20729
  score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
20384
20730
  }));
20385
20731
  } catch (error2) {
@@ -20392,7 +20738,7 @@ class SqliteMemoryRepository {
20392
20738
  try {
20393
20739
  const rows = this.getStatement.all(id);
20394
20740
  const row = rows[0];
20395
- return row ? toMemoryRecord(row) : undefined;
20741
+ return row ? toMemoryEntity(row) : undefined;
20396
20742
  } catch (error2) {
20397
20743
  throw new PersistenceError("Failed to find memory.", { cause: error2 });
20398
20744
  }
@@ -20413,7 +20759,7 @@ class SqliteMemoryRepository {
20413
20759
  const queryLimit = limit + 1;
20414
20760
  params.push(queryLimit, offset);
20415
20761
  const statement = this.database.prepare(`
20416
- SELECT id, content, workspace, created_at, updated_at
20762
+ SELECT id, content, workspace, embedding, created_at, updated_at
20417
20763
  FROM memories
20418
20764
  ${whereClause}
20419
20765
  ORDER BY created_at DESC
@@ -20421,7 +20767,7 @@ class SqliteMemoryRepository {
20421
20767
  `);
20422
20768
  const rows = statement.all(...params);
20423
20769
  const hasMore = rows.length > limit;
20424
- const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryRecord);
20770
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryEntity);
20425
20771
  return { items, hasMore };
20426
20772
  } catch (error2) {
20427
20773
  throw new PersistenceError("Failed to list memories.", { cause: error2 });
@@ -20431,7 +20777,7 @@ class SqliteMemoryRepository {
20431
20777
  let result;
20432
20778
  try {
20433
20779
  const now = Date.now();
20434
- result = this.updateStatement.run(input.content, now, input.id);
20780
+ result = this.updateStatement.run(input.content, encodeEmbedding(input.embedding), now, input.id);
20435
20781
  } catch (error2) {
20436
20782
  throw new PersistenceError("Failed to update memory.", { cause: error2 });
20437
20783
  }
@@ -20464,9 +20810,10 @@ class SqliteMemoryRepository {
20464
20810
  }
20465
20811
  }
20466
20812
  }
20467
- var toMemoryRecord = (row) => ({
20813
+ var toMemoryEntity = (row) => ({
20468
20814
  id: row.id,
20469
20815
  content: row.content,
20816
+ embedding: decodeEmbedding(row.embedding),
20470
20817
  workspace: row.workspace ?? undefined,
20471
20818
  createdAt: new Date(row.created_at),
20472
20819
  updatedAt: new Date(row.updated_at)
@@ -20479,7 +20826,6 @@ function toFtsTerm(term) {
20479
20826
  }
20480
20827
  return `"${escaped}"*`;
20481
20828
  }
20482
-
20483
20829
  // node_modules/@hono/node-server/dist/index.mjs
20484
20830
  import { createServer as createServerHTTP } from "http";
20485
20831
  import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
@@ -23868,11 +24214,59 @@ function startWebServer(memory, options) {
23868
24214
  return serve({ fetch: app.fetch, port: options.port });
23869
24215
  }
23870
24216
 
24217
+ // src/workspace-resolver.ts
24218
+ import { execFile } from "node:child_process";
24219
+ import { basename, dirname as dirname2 } from "node:path";
24220
+ import { promisify } from "node:util";
24221
+ var execFileAsync = promisify(execFile);
24222
+ function createGitWorkspaceResolver(options = {}) {
24223
+ const getGitCommonDir = options.getGitCommonDir ?? defaultGetGitCommonDir;
24224
+ const cache = new Map;
24225
+ return {
24226
+ async resolve(workspace) {
24227
+ const trimmed = normalizeOptionalString(workspace);
24228
+ if (!trimmed) {
24229
+ return;
24230
+ }
24231
+ const cached2 = cache.get(trimmed);
24232
+ if (cached2) {
24233
+ return cached2;
24234
+ }
24235
+ const pending = resolveWorkspace(trimmed, getGitCommonDir);
24236
+ cache.set(trimmed, pending);
24237
+ return pending;
24238
+ }
24239
+ };
24240
+ }
24241
+ async function resolveWorkspace(workspace, getGitCommonDir) {
24242
+ try {
24243
+ const gitCommonDir = (await getGitCommonDir(workspace)).trim();
24244
+ if (basename(gitCommonDir) !== ".git") {
24245
+ return workspace;
24246
+ }
24247
+ return dirname2(gitCommonDir);
24248
+ } catch {
24249
+ return workspace;
24250
+ }
24251
+ }
24252
+ async function defaultGetGitCommonDir(cwd) {
24253
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
24254
+ cwd
24255
+ });
24256
+ return stdout.trim();
24257
+ }
24258
+ function normalizeOptionalString(value) {
24259
+ const trimmed = value?.trim();
24260
+ return trimmed ? trimmed : undefined;
24261
+ }
24262
+
23871
24263
  // src/index.ts
23872
24264
  var config2 = resolveConfig();
23873
- var database = openMemoryDatabase(config2.databasePath);
23874
- var repository = new SqliteMemoryRepository(database);
23875
- var memoryService = new MemoryService(repository);
24265
+ var embeddingService = new EmbeddingService({ modelsCachePath: config2.modelsCachePath });
24266
+ var workspaceResolver = createGitWorkspaceResolver();
24267
+ var database = await openMemoryDatabase(config2.databasePath, { embeddingService, workspaceResolver });
24268
+ var repository2 = new SqliteMemoryRepository(database);
24269
+ var memoryService = new MemoryService(repository2, embeddingService, workspaceResolver);
23876
24270
  if (config2.uiMode) {
23877
24271
  let shutdown = function() {
23878
24272
  server.close();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jcyamacho/agent-memory",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.13",
4
+ "version": "0.0.17",
5
5
  "bin": {
6
6
  "agent-memory": "dist/index.js"
7
7
  },
@@ -20,7 +20,7 @@
20
20
  "url": "git+https://github.com/jcyamacho/agent-memory.git"
21
21
  },
22
22
  "scripts": {
23
- "build": "bun build src/index.ts --target=node --external better-sqlite3 --outfile dist/index.js --banner \"#!/usr/bin/env node\"",
23
+ "build": "bun build src/index.ts --target=node --external better-sqlite3 --external @huggingface/transformers --outfile dist/index.js --banner \"#!/usr/bin/env node\"",
24
24
  "start": "node dist/index.js",
25
25
  "test": "bun test",
26
26
  "lint": "biome check --write && tsc --noEmit",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@hono/node-server": "^1.19.11",
41
+ "@huggingface/transformers": "^3.8.1",
41
42
  "@modelcontextprotocol/sdk": "^1.27.1",
42
43
  "better-sqlite3": "^12.6.2",
43
44
  "hono": "^4.12.8",