@jcyamacho/agent-memory 0.0.19 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +17 -58
  2. package/dist/index.js +228 -550
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -9,7 +9,7 @@ decisions across sessions.
9
9
  It exposes four tools:
10
10
 
11
11
  - `remember` -> save facts, decisions, preferences, and project context
12
- - `recall` -> retrieve the most relevant memories later
12
+ - `review` -> load workspace and global memories sorted by most recently updated
13
13
  - `revise` -> update an existing memory when it becomes outdated
14
14
  - `forget` -> delete a memory that is no longer relevant
15
15
 
@@ -43,22 +43,20 @@ OpenCode:
43
43
 
44
44
  ## Optional LLM Instructions
45
45
 
46
- Optional LLM instructions to reinforce the MCP's built-in guidance:
46
+ Optional LLM instructions to reinforce the MCP's built-in guidance. The server
47
+ instructions and tool descriptions already cover most behavior -- this prompt
48
+ targets the habits models most commonly miss:
47
49
 
48
50
  ```md
49
51
  ## Agent Memory
50
52
 
51
- - Use `memory_recall` at conversation start and before design choices,
52
- conventions, edge cases, or saving memory.
53
- - Query `memory_recall` with 2-5 short anchor-heavy terms or exact phrases,
54
- not full questions or sentences.
55
- - Pass `workspace` for project-scoped memory. Omit it only for facts that
56
- apply across projects.
57
- - Use `memory_remember` to save one durable fact when the user states a stable
58
- preference, correction, or reusable project decision.
59
- - If the fact already exists, use `memory_revise` instead of creating a duplicate.
60
- - Use `memory_forget` to remove a wrong or obsolete memory.
61
- - Do not store secrets or temporary task state in memory.
53
+ - Use `memory_review` at conversation start to load workspace memories into
54
+ context. During the session, use `memory_remember`, `memory_revise`, and
55
+ `memory_forget` to keep memories accurate.
56
+ - Pass `workspace` on `memory_remember` for project-scoped memory. Omit it
57
+ only for facts that apply across projects.
58
+ - Do not store secrets, temporary task state, or facts obvious from current
59
+ code or git history.
62
60
  ```
63
61
 
64
62
  ## Web UI
@@ -77,34 +75,15 @@ npx -y @jcyamacho/agent-memory@latest --ui --port 9090
77
75
 
78
76
  The web UI uses the same database as the MCP server.
79
77
 
80
- ## How Recall Finds Memories
81
-
82
- `recall` uses a multi-signal ranking system to surface the most relevant
83
- memories:
84
-
85
- 1. **Text relevance** is the primary signal -- memories whose content best
86
- matches your search terms rank highest.
87
- 2. **Workspace match** is the next strongest signal. When you pass
88
- `workspace`, exact matches rank highest and all other scoped workspaces rank
89
- below exact matches.
90
- 3. **Embedding similarity** is a secondary signal. Recall builds an embedding
91
- from your normalized search terms and boosts memories whose stored
92
- embeddings are most semantically similar.
93
- 4. **Global memories** (saved without a workspace) are treated as relevant
94
- everywhere. When you pass `workspace`, they rank below exact workspace
95
- matches and above memories from other workspaces.
96
- 5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
97
- ones when other signals are equal.
98
-
99
- If you omit `workspace`, recall still uses text relevance, embedding similarity,
100
- and recency. For best results, pass `workspace` whenever you have one. Save
101
- memories without a workspace only when they apply across all projects.
78
+ ## How Review Works
79
+
80
+ `review` requires a `workspace` and returns memories saved in that workspace
81
+ plus global memories (saved without a workspace), sorted by most recently
82
+ updated. Results are paginated -- pass `page` to load older memories.
102
83
 
103
84
  When you save a memory from a git worktree, `agent-memory` stores the main repo
104
- root as the workspace. `recall` applies the same normalization to incoming
85
+ root as the workspace. `review` applies the same normalization to incoming
105
86
  workspace queries so linked worktrees still match repo-scoped memories exactly.
106
- When that happens, recall returns the queried workspace value so callers can
107
- treat the match as belonging to their current worktree context.
108
87
 
109
88
  ## Configuration
110
89
 
@@ -128,26 +107,6 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
128
107
  - share a memory DB across multiple clients
129
108
  - store the DB somewhere easier to back up or inspect
130
109
 
131
- ### Model Cache Location
132
-
133
- By default, downloaded embedding model files are cached at:
134
-
135
- ```text
136
- ~/.config/agent-memory/models
137
- ```
138
-
139
- Override it with:
140
-
141
- ```bash
142
- AGENT_MEMORY_MODELS_CACHE_PATH=/absolute/path/to/models
143
- ```
144
-
145
- Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
146
-
147
- - keep model artifacts out of `node_modules`
148
- - share the model cache across reinstalls or multiple clients
149
- - store model downloads somewhere easier to inspect or manage
150
-
151
110
  Schema changes are migrated automatically, including workspace normalization for
152
111
  existing git worktree memories when the original path can still be resolved.
153
112
 
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: ValidationError2, opts } = it;
2434
+ const { gen, schemaEnv, validateName, ValidationError, 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 ${ValidationError2}(${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 ${ValidationError}(${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 ValidationError2 extends Error {
2786
+ class ValidationError 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 = ValidationError2;
2793
+ exports.default = ValidationError;
2794
2794
  });
2795
2795
 
2796
2796
  // node_modules/ajv/dist/compile/ref_error.js
@@ -12464,14 +12464,13 @@ class StdioServerTransport {
12464
12464
  }
12465
12465
  }
12466
12466
  // package.json
12467
- var version2 = "0.0.19";
12467
+ var version2 = "0.1.0";
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";
12475
12474
  var DEFAULT_UI_PORT = 6580;
12476
12475
  function resolveConfig(environment = process.env, argv = process.argv.slice(2)) {
12477
12476
  const { values } = parseArgs({
@@ -12484,7 +12483,6 @@ function resolveConfig(environment = process.env, argv = process.argv.slice(2))
12484
12483
  });
12485
12484
  return {
12486
12485
  databasePath: resolveDatabasePath(environment),
12487
- modelsCachePath: resolveModelsCachePath(environment),
12488
12486
  uiMode: Boolean(values.ui),
12489
12487
  uiPort: Number(values.port) || DEFAULT_UI_PORT
12490
12488
  };
@@ -12492,137 +12490,7 @@ function resolveConfig(environment = process.env, argv = process.argv.slice(2))
12492
12490
  function resolveDatabasePath(environment = process.env) {
12493
12491
  return environment[AGENT_MEMORY_DB_PATH_ENV] || join(homedir(), ".config", "agent-memory", "memory.db");
12494
12492
  }
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
12493
 
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
- }
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
- }
12626
12494
  // node_modules/zod/v3/helpers/util.js
12627
12495
  var util;
12628
12496
  (function(util2) {
@@ -20030,6 +19898,37 @@ var EMPTY_COMPLETION_RESULT = {
20030
19898
  }
20031
19899
  };
20032
19900
 
19901
+ // src/errors.ts
19902
+ class MemoryError extends Error {
19903
+ code;
19904
+ constructor(code, message, options) {
19905
+ super(message, options);
19906
+ this.name = "MemoryError";
19907
+ this.code = code;
19908
+ }
19909
+ }
19910
+
19911
+ class ValidationError extends MemoryError {
19912
+ constructor(message) {
19913
+ super("VALIDATION_ERROR", message);
19914
+ this.name = "ValidationError";
19915
+ }
19916
+ }
19917
+
19918
+ class NotFoundError extends MemoryError {
19919
+ constructor(message) {
19920
+ super("NOT_FOUND", message);
19921
+ this.name = "NotFoundError";
19922
+ }
19923
+ }
19924
+
19925
+ class PersistenceError extends MemoryError {
19926
+ constructor(message, options) {
19927
+ super("PERSISTENCE_ERROR", message, options);
19928
+ this.name = "PersistenceError";
19929
+ }
19930
+ }
19931
+
20033
19932
  // src/mcp/tools/shared.ts
20034
19933
  function toMcpError(error2) {
20035
19934
  if (error2 instanceof McpError) {
@@ -20045,20 +19944,10 @@ function toMcpError(error2) {
20045
19944
  return new McpError(ErrorCode.InternalError, message);
20046
19945
  }
20047
19946
  var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20048
- function parseOptionalDate(value, fieldName) {
20049
- if (!value) {
20050
- return;
20051
- }
20052
- const date4 = new Date(value);
20053
- if (Number.isNaN(date4.getTime())) {
20054
- throw new MemoryError("VALIDATION_ERROR", `${fieldName} must be a valid ISO 8601 datetime.`);
20055
- }
20056
- return date4;
20057
- }
20058
19947
 
20059
19948
  // src/mcp/tools/forget.ts
20060
19949
  var forgetInputSchema = {
20061
- id: string2().describe("Memory id to delete. Use an id returned by `recall`.")
19950
+ id: string2().describe("Memory id to delete. Use an id returned by `review`.")
20062
19951
  };
20063
19952
  function registerForgetTool(server, memory) {
20064
19953
  server.registerTool("forget", {
@@ -20068,7 +19957,7 @@ function registerForgetTool(server, memory) {
20068
19957
  idempotentHint: true,
20069
19958
  openWorldHint: false
20070
19959
  },
20071
- description: 'Delete a memory that is wrong or obsolete. Use after `recall` when you have the memory id. Use `revise` instead if the fact should remain with corrected wording. Returns `<memory id="..." deleted="true" />`.',
19960
+ description: 'Delete a memory that is wrong or obsolete. Use after `review` when you have the memory id. Use `revise` instead if the fact should remain with corrected wording. Returns `<memory id="..." deleted="true" />`.',
20072
19961
  inputSchema: forgetInputSchema
20073
19962
  }, async ({ id }) => {
20074
19963
  try {
@@ -20082,250 +19971,6 @@ function registerForgetTool(server, memory) {
20082
19971
  });
20083
19972
  }
20084
19973
 
20085
- // src/memory.ts
20086
- var toNormalizedScore = (value) => value;
20087
-
20088
- // src/ranking.ts
20089
- var RETRIEVAL_SCORE_WEIGHT = 9;
20090
- var EMBEDDING_SIMILARITY_WEIGHT = 4;
20091
- var WORKSPACE_MATCH_WEIGHT = 5;
20092
- var RECENCY_WEIGHT = 1;
20093
- var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + EMBEDDING_SIMILARITY_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
20094
- var GLOBAL_WORKSPACE_SCORE = 0.5;
20095
- function rerankSearchResults(results, workspace, queryEmbedding) {
20096
- if (results.length === 0) {
20097
- return results;
20098
- }
20099
- const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
20100
- const updatedAtTimes = results.map((result) => result.updatedAt.getTime());
20101
- const minUpdatedAt = Math.min(...updatedAtTimes);
20102
- const maxUpdatedAt = Math.max(...updatedAtTimes);
20103
- return results.map((result) => {
20104
- const embeddingSimilarityScore = computeEmbeddingSimilarityScore(result, queryEmbedding);
20105
- const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
20106
- const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
20107
- const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + embeddingSimilarityScore * EMBEDDING_SIMILARITY_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
20108
- return {
20109
- ...result,
20110
- score: toNormalizedScore(combinedScore)
20111
- };
20112
- }).sort((a, b) => b.score - a.score);
20113
- }
20114
- function computeEmbeddingSimilarityScore(result, queryEmbedding) {
20115
- return normalizeCosineSimilarity(compareVectors(result.embedding, queryEmbedding));
20116
- }
20117
- function normalizeWorkspacePath(value) {
20118
- return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
20119
- }
20120
- function normalizeCosineSimilarity(value) {
20121
- return (value + 1) / 2;
20122
- }
20123
- function computeWorkspaceScore(memoryWs, queryWs) {
20124
- if (!queryWs) {
20125
- return 0;
20126
- }
20127
- if (!memoryWs) {
20128
- return GLOBAL_WORKSPACE_SCORE;
20129
- }
20130
- const normalizedMemoryWs = normalizeWorkspacePath(memoryWs);
20131
- if (normalizedMemoryWs === queryWs) {
20132
- return 1;
20133
- }
20134
- return 0;
20135
- }
20136
-
20137
- // src/memory-service.ts
20138
- var DEFAULT_RECALL_LIMIT = 15;
20139
- var MAX_RECALL_LIMIT = 50;
20140
- var RECALL_CANDIDATE_LIMIT_MULTIPLIER = 3;
20141
- var DEFAULT_LIST_LIMIT = 15;
20142
- var MAX_LIST_LIMIT = 100;
20143
-
20144
- class MemoryService {
20145
- repository;
20146
- embeddingService;
20147
- workspaceResolver;
20148
- constructor(repository, embeddingService, workspaceResolver) {
20149
- this.repository = repository;
20150
- this.embeddingService = embeddingService;
20151
- this.workspaceResolver = workspaceResolver;
20152
- }
20153
- async create(input) {
20154
- const content = input.content.trim();
20155
- if (!content) {
20156
- throw new ValidationError("Memory content is required.");
20157
- }
20158
- const workspace = await this.workspaceResolver.resolve(input.workspace);
20159
- const memory = await this.repository.create({
20160
- content,
20161
- embedding: await this.embeddingService.createVector(content),
20162
- workspace
20163
- });
20164
- return toPublicMemoryRecord(memory);
20165
- }
20166
- async update(input) {
20167
- const content = input.content.trim();
20168
- if (!content)
20169
- throw new ValidationError("Memory content is required.");
20170
- const memory = await this.repository.update({
20171
- id: input.id,
20172
- content,
20173
- embedding: await this.embeddingService.createVector(content)
20174
- });
20175
- return toPublicMemoryRecord(memory);
20176
- }
20177
- async delete(input) {
20178
- const id = input.id.trim();
20179
- if (!id)
20180
- throw new ValidationError("Memory id is required.");
20181
- return this.repository.delete({ id });
20182
- }
20183
- async get(id) {
20184
- const memory = await this.repository.get(id);
20185
- return memory ? toPublicMemoryRecord(memory) : undefined;
20186
- }
20187
- async list(input) {
20188
- const workspace = await this.workspaceResolver.resolve(input.workspace);
20189
- const page = await this.repository.list({
20190
- workspace,
20191
- workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
20192
- offset: normalizeOffset(input.offset),
20193
- limit: normalizeListLimit(input.limit)
20194
- });
20195
- return toPublicMemoryPage(page);
20196
- }
20197
- async listWorkspaces() {
20198
- return this.repository.listWorkspaces();
20199
- }
20200
- async search(input) {
20201
- const terms = normalizeTerms(input.terms);
20202
- if (terms.length === 0) {
20203
- throw new ValidationError("At least one search term is required.");
20204
- }
20205
- const requestedLimit = normalizeLimit(input.limit);
20206
- const queryWorkspace = normalizeOptionalString(input.workspace);
20207
- const workspace = await this.workspaceResolver.resolve(input.workspace);
20208
- const normalizedQuery = {
20209
- terms,
20210
- limit: requestedLimit * RECALL_CANDIDATE_LIMIT_MULTIPLIER,
20211
- updatedAfter: input.updatedAfter,
20212
- updatedBefore: input.updatedBefore
20213
- };
20214
- const [results, queryEmbedding] = await Promise.all([
20215
- this.repository.search(normalizedQuery),
20216
- this.embeddingService.createVector(terms.join(" "))
20217
- ]);
20218
- return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map((result) => toPublicSearchResult(remapSearchResultWorkspace(result, workspace, queryWorkspace)));
20219
- }
20220
- }
20221
- function toPublicMemoryRecord(memory) {
20222
- return {
20223
- id: memory.id,
20224
- content: memory.content,
20225
- workspace: memory.workspace,
20226
- createdAt: memory.createdAt,
20227
- updatedAt: memory.updatedAt
20228
- };
20229
- }
20230
- function toPublicSearchResult(result) {
20231
- return {
20232
- id: result.id,
20233
- content: result.content,
20234
- score: result.score,
20235
- workspace: result.workspace,
20236
- createdAt: result.createdAt,
20237
- updatedAt: result.updatedAt
20238
- };
20239
- }
20240
- function toPublicMemoryPage(page) {
20241
- return {
20242
- items: page.items.map(toPublicMemoryRecord),
20243
- hasMore: page.hasMore
20244
- };
20245
- }
20246
- function normalizeLimit(value) {
20247
- if (value === undefined) {
20248
- return DEFAULT_RECALL_LIMIT;
20249
- }
20250
- if (!Number.isInteger(value) || value < 1 || value > MAX_RECALL_LIMIT) {
20251
- throw new ValidationError(`Limit must be an integer between 1 and ${MAX_RECALL_LIMIT}.`);
20252
- }
20253
- return value;
20254
- }
20255
- function normalizeOffset(value) {
20256
- return Number.isInteger(value) && value && value > 0 ? value : 0;
20257
- }
20258
- function normalizeListLimit(value) {
20259
- if (!Number.isInteger(value) || value === undefined) {
20260
- return DEFAULT_LIST_LIMIT;
20261
- }
20262
- return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
20263
- }
20264
- function normalizeTerms(terms) {
20265
- const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
20266
- return [...new Set(normalizedTerms)];
20267
- }
20268
- function normalizeOptionalString(value) {
20269
- const trimmed = value?.trim();
20270
- return trimmed ? trimmed : undefined;
20271
- }
20272
- function remapSearchResultWorkspace(result, canonicalWorkspace, queryWorkspace) {
20273
- if (!queryWorkspace || !canonicalWorkspace || result.workspace !== canonicalWorkspace) {
20274
- return result;
20275
- }
20276
- return {
20277
- ...result,
20278
- workspace: queryWorkspace
20279
- };
20280
- }
20281
-
20282
- // src/mcp/tools/recall.ts
20283
- var recallInputSchema = {
20284
- terms: array(string2()).min(1).describe("2-5 short anchor-heavy terms or exact phrases. Prefer identifiers, commands, file paths, and exact wording likely to appear in the memory."),
20285
- 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."),
20286
- workspace: string2().optional().describe("Current working directory for project-scoped recall. Omit for cross-project recall."),
20287
- updated_after: string2().optional().describe("Only return memories updated on or after this ISO 8601 timestamp."),
20288
- updated_before: string2().optional().describe("Only return memories updated on or before this ISO 8601 timestamp.")
20289
- };
20290
- function toMemoryXml(r) {
20291
- const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
20292
- const content = escapeXml(r.content);
20293
- const score = Number(r.score.toFixed(3)).toString();
20294
- return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
20295
- ${content}
20296
- </memory>`;
20297
- }
20298
- function registerRecallTool(server, memory) {
20299
- server.registerTool("recall", {
20300
- annotations: {
20301
- title: "Recall",
20302
- readOnlyHint: true,
20303
- openWorldHint: false
20304
- },
20305
- description: "Retrieve memories relevant to the current task or check whether a fact already exists before saving. Use at conversation start and before design choices. Pass short anchor-heavy `terms` and `workspace` when available. Results reflect the queried workspace context when applicable. Returns `<memories>...</memories>` or a no-match hint.",
20306
- inputSchema: recallInputSchema
20307
- }, async ({ terms, limit, workspace, updated_after, updated_before }) => {
20308
- try {
20309
- const results = await memory.search({
20310
- terms,
20311
- limit,
20312
- workspace,
20313
- updatedAfter: parseOptionalDate(updated_after, "updated_after"),
20314
- updatedBefore: parseOptionalDate(updated_before, "updated_before")
20315
- });
20316
- const text = results.length === 0 ? "No matching memories found. Retry once with 1-3 overlapping alternate terms or an exact identifier, command, file path, or phrase likely to appear in the memory." : `<memories>
20317
- ${results.map(toMemoryXml).join(`
20318
- `)}
20319
- </memories>`;
20320
- return {
20321
- content: [{ type: "text", text }]
20322
- };
20323
- } catch (error2) {
20324
- throw toMcpError(error2);
20325
- }
20326
- });
20327
- }
20328
-
20329
19974
  // src/mcp/tools/remember.ts
20330
19975
  var rememberInputSchema = {
20331
19976
  content: string2().describe("One new durable fact to save. Use a self-contained sentence or short note."),
@@ -20339,7 +19984,7 @@ function registerRememberTool(server, memory) {
20339
19984
  idempotentHint: false,
20340
19985
  openWorldHint: false
20341
19986
  },
20342
- description: 'Save one new durable fact for later recall. Use for stable preferences, reusable decisions, and project context not obvious from code or git history. If the fact already exists, use `revise` instead. Returns `<memory id="..." />`.',
19987
+ description: 'Save one new durable fact. Use for stable preferences, reusable decisions, and project context not obvious from code or git history. If the fact already exists, use `revise` instead. Returns `<memory id="..." />`.',
20343
19988
  inputSchema: rememberInputSchema
20344
19989
  }, async ({ content, workspace }) => {
20345
19990
  try {
@@ -20356,9 +20001,60 @@ function registerRememberTool(server, memory) {
20356
20001
  });
20357
20002
  }
20358
20003
 
20004
+ // src/mcp/tools/review.ts
20005
+ var REVIEW_PAGE_SIZE = 50;
20006
+ var reviewInputSchema = {
20007
+ workspace: string2().describe("Current working directory for project-scoped listing."),
20008
+ page: number2().int().min(0).optional().describe("Zero-based page number. Defaults to 0.")
20009
+ };
20010
+ function toMemoryXml(record3, workspace) {
20011
+ const global2 = record3.workspace !== workspace ? ' global="true"' : "";
20012
+ const content = escapeXml(record3.content);
20013
+ return `<memory id="${record3.id}"${global2} updated_at="${record3.updatedAt.toISOString()}">
20014
+ ${content}
20015
+ </memory>`;
20016
+ }
20017
+ function registerReviewTool(server, memory) {
20018
+ server.registerTool("review", {
20019
+ annotations: {
20020
+ title: "Review",
20021
+ readOnlyHint: true,
20022
+ openWorldHint: false
20023
+ },
20024
+ description: 'Load workspace and global memories sorted by most recently updated. Use at the start of a task and before saving or revising memory. Returns `<memories workspace="..." has_more="true|false">...</memories>` with pagination support. Global memories are marked with `global="true"`.',
20025
+ inputSchema: reviewInputSchema
20026
+ }, async ({ workspace, page }) => {
20027
+ try {
20028
+ const pageIndex = page ?? 0;
20029
+ const result = await memory.list({
20030
+ workspace,
20031
+ global: true,
20032
+ offset: pageIndex * REVIEW_PAGE_SIZE,
20033
+ limit: REVIEW_PAGE_SIZE
20034
+ });
20035
+ if (result.items.length === 0) {
20036
+ return {
20037
+ content: [{ type: "text", text: "No memories found for this workspace." }]
20038
+ };
20039
+ }
20040
+ const escapedWorkspace = escapeXml(workspace);
20041
+ const memories = result.items.map((item) => toMemoryXml(item, workspace)).join(`
20042
+ `);
20043
+ const text = `<memories workspace="${escapedWorkspace}" has_more="${result.hasMore}">
20044
+ ${memories}
20045
+ </memories>`;
20046
+ return {
20047
+ content: [{ type: "text", text }]
20048
+ };
20049
+ } catch (error2) {
20050
+ throw toMcpError(error2);
20051
+ }
20052
+ });
20053
+ }
20054
+
20359
20055
  // src/mcp/tools/revise.ts
20360
20056
  var reviseInputSchema = {
20361
- id: string2().describe("Memory id to update. Use an id returned by `recall`."),
20057
+ id: string2().describe("Memory id to update. Use an id returned by `review`."),
20362
20058
  content: string2().describe("Corrected replacement text for that memory.")
20363
20059
  };
20364
20060
  function registerReviseTool(server, memory) {
@@ -20369,7 +20065,7 @@ function registerReviseTool(server, memory) {
20369
20065
  idempotentHint: false,
20370
20066
  openWorldHint: false
20371
20067
  },
20372
- description: 'Update one existing memory when the same fact still applies but its wording or details changed. Use after `recall` when you already have the memory id. Returns `<memory id="..." updated_at="..." />`.',
20068
+ description: 'Update one existing memory when the same fact still applies but its wording or details changed. Use after `review` when you already have the memory id. Returns `<memory id="..." updated_at="..." />`.',
20373
20069
  inputSchema: reviseInputSchema
20374
20070
  }, async ({ id, content }) => {
20375
20071
  try {
@@ -20390,12 +20086,12 @@ function registerReviseTool(server, memory) {
20390
20086
 
20391
20087
  // src/mcp/server.ts
20392
20088
  var SERVER_INSTRUCTIONS = [
20393
- "Use this server only for durable memory that should survive across turns: stable preferences, corrections, reusable decisions, and project context not obvious from code or git history.",
20394
- "Use `recall` at conversation start, before design choices, and before saving or revising memory.",
20395
- "Use `remember` for one new durable fact. Use `revise` when the fact already exists but needs correction.",
20396
- "Use `forget` only when a memory is wrong or obsolete.",
20397
- "Pass workspace for project-scoped memory. Omit it only for facts that apply across projects.",
20398
- "Do not store secrets, temporary task state, or facts obvious from current code or git history."
20089
+ "Durable memory for stable preferences, corrections, reusable decisions, and project context not obvious from code or git history.",
20090
+ "Workflow: (1) Call `review` with the current workspace at conversation start -- this loads workspace and global memories into context.",
20091
+ "(2) During the session, call `remember` to save a new fact, `revise` to correct an existing one, or `forget` to remove one that is wrong or obsolete.",
20092
+ "Always check loaded memories before calling `remember` to avoid duplicates -- use `revise` instead when the fact already exists.",
20093
+ "Pass workspace on `remember` for project-scoped memory. Omit it only for facts that apply across all projects.",
20094
+ "Never store secrets, temporary task state, or facts obvious from current code or git history."
20399
20095
  ].join(" ");
20400
20096
  function createMcpServer(memory, version3) {
20401
20097
  const server = new McpServer({
@@ -20405,26 +20101,96 @@ function createMcpServer(memory, version3) {
20405
20101
  instructions: SERVER_INSTRUCTIONS
20406
20102
  });
20407
20103
  registerRememberTool(server, memory);
20408
- registerRecallTool(server, memory);
20409
20104
  registerReviseTool(server, memory);
20410
20105
  registerForgetTool(server, memory);
20106
+ registerReviewTool(server, memory);
20411
20107
  return server;
20412
20108
  }
20413
20109
 
20110
+ // src/memory-service.ts
20111
+ var DEFAULT_LIST_LIMIT = 15;
20112
+ var MAX_LIST_LIMIT = 100;
20113
+
20114
+ class MemoryService {
20115
+ repository;
20116
+ workspaceResolver;
20117
+ constructor(repository, workspaceResolver) {
20118
+ this.repository = repository;
20119
+ this.workspaceResolver = workspaceResolver;
20120
+ }
20121
+ async create(input) {
20122
+ const content = input.content.trim();
20123
+ if (!content) {
20124
+ throw new ValidationError("Memory content is required.");
20125
+ }
20126
+ const workspace = await this.workspaceResolver.resolve(input.workspace);
20127
+ return this.repository.create({ content, workspace });
20128
+ }
20129
+ async update(input) {
20130
+ const content = input.content.trim();
20131
+ if (!content)
20132
+ throw new ValidationError("Memory content is required.");
20133
+ return this.repository.update({ id: input.id, content });
20134
+ }
20135
+ async delete(input) {
20136
+ const id = input.id.trim();
20137
+ if (!id)
20138
+ throw new ValidationError("Memory id is required.");
20139
+ return this.repository.delete({ id });
20140
+ }
20141
+ async get(id) {
20142
+ return this.repository.get(id);
20143
+ }
20144
+ async list(input) {
20145
+ const queryWorkspace = normalizeOptionalString(input.workspace);
20146
+ const workspace = await this.workspaceResolver.resolve(input.workspace);
20147
+ const page = await this.repository.list({
20148
+ workspace,
20149
+ global: input.global,
20150
+ offset: normalizeOffset(input.offset),
20151
+ limit: normalizeListLimit(input.limit)
20152
+ });
20153
+ return {
20154
+ items: page.items.map((item) => remapWorkspace(item, workspace, queryWorkspace)),
20155
+ hasMore: page.hasMore
20156
+ };
20157
+ }
20158
+ async listWorkspaces() {
20159
+ return this.repository.listWorkspaces();
20160
+ }
20161
+ }
20162
+ function normalizeOffset(value) {
20163
+ return Number.isInteger(value) && value && value > 0 ? value : 0;
20164
+ }
20165
+ function normalizeListLimit(value) {
20166
+ if (!Number.isInteger(value) || value === undefined) {
20167
+ return DEFAULT_LIST_LIMIT;
20168
+ }
20169
+ return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
20170
+ }
20171
+ function normalizeOptionalString(value) {
20172
+ const trimmed = value?.trim();
20173
+ return trimmed ? trimmed : undefined;
20174
+ }
20175
+ function remapWorkspace(record3, canonicalWorkspace, queryWorkspace) {
20176
+ if (record3.workspace !== canonicalWorkspace) {
20177
+ return record3;
20178
+ }
20179
+ return { ...record3, workspace: queryWorkspace };
20180
+ }
20181
+
20414
20182
  // src/sqlite/db.ts
20415
- import { mkdirSync as mkdirSync2 } from "node:fs";
20183
+ import { mkdirSync } from "node:fs";
20416
20184
  import { dirname } from "node:path";
20417
20185
  import Database from "better-sqlite3";
20418
20186
 
20419
20187
  // src/sqlite/memory-schema.ts
20420
- function createMemoriesTable(database, options) {
20421
- const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
20188
+ function createMemoriesTable(database) {
20422
20189
  database.exec(`
20423
20190
  CREATE TABLE IF NOT EXISTS memories (
20424
20191
  id TEXT PRIMARY KEY,
20425
20192
  content TEXT NOT NULL,
20426
20193
  workspace TEXT,
20427
- ${embeddingColumn}
20428
20194
  created_at INTEGER NOT NULL,
20429
20195
  updated_at INTEGER NOT NULL
20430
20196
  );
@@ -20432,7 +20198,7 @@ function createMemoriesTable(database, options) {
20432
20198
  }
20433
20199
  function createMemoryIndexes(database) {
20434
20200
  database.exec(`
20435
- CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
20201
+ CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);
20436
20202
  CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
20437
20203
  `);
20438
20204
  }
@@ -20472,82 +20238,38 @@ function dropMemorySearchArtifacts(database) {
20472
20238
  DROP TABLE IF EXISTS memories_fts;
20473
20239
  `);
20474
20240
  }
20475
- function getEmbeddingColumnSql(mode) {
20476
- switch (mode) {
20477
- case "omit":
20478
- return "";
20479
- case "nullable":
20480
- return `embedding BLOB,
20481
- `;
20482
- case "required":
20483
- return `embedding BLOB NOT NULL,
20484
- `;
20485
- }
20486
- }
20487
20241
 
20488
20242
  // src/sqlite/migrations/001-create-memory-schema.ts
20489
20243
  var createMemorySchemaMigration = {
20490
20244
  version: 1,
20491
20245
  async up(database) {
20492
- createMemoriesTable(database, { embeddingColumn: "omit" });
20246
+ createMemoriesTable(database);
20493
20247
  createMemoryIndexes(database);
20494
20248
  createMemorySearchArtifacts(database);
20495
20249
  }
20496
20250
  };
20497
20251
 
20498
- // src/sqlite/embedding-codec.ts
20499
- var FLOAT32_BYTE_WIDTH = 4;
20500
- function encodeEmbedding(vector) {
20501
- const typedArray = Float32Array.from(vector);
20502
- return new Uint8Array(typedArray.buffer.slice(0));
20503
- }
20504
- function decodeEmbedding(value) {
20505
- const bytes = toUint8Array(value);
20506
- if (bytes.byteLength === 0) {
20507
- throw new Error("Embedding blob is empty.");
20508
- }
20509
- if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
20510
- throw new Error("Embedding blob length is not a multiple of 4.");
20511
- }
20512
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
20513
- const vector = [];
20514
- for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
20515
- vector.push(view.getFloat32(offset, true));
20516
- }
20517
- return vector;
20518
- }
20519
- function toUint8Array(value) {
20520
- if (value instanceof Uint8Array) {
20521
- return value;
20522
- }
20523
- if (value instanceof ArrayBuffer) {
20524
- return new Uint8Array(value);
20525
- }
20526
- throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
20527
- }
20528
-
20529
20252
  // src/sqlite/migrations/002-add-memory-embedding.ts
20530
- function createAddMemoryEmbeddingMigration(embeddingService) {
20253
+ function createAddMemoryEmbeddingMigration() {
20531
20254
  return {
20532
20255
  version: 2,
20533
20256
  async up(database) {
20534
20257
  database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
20535
- const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
20536
- const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
20537
- for (const row of rows) {
20538
- const embedding = await embeddingService.createVector(row.content);
20539
- updateStatement.run(encodeEmbedding(embedding), row.id);
20540
- }
20541
- const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
20542
- if ((nullRows[0]?.count ?? 0) > 0) {
20543
- throw new Error("Failed to backfill embeddings for all memories.");
20544
- }
20545
20258
  dropMemorySearchArtifacts(database);
20546
20259
  database.exec("ALTER TABLE memories RENAME TO memories_old");
20547
- createMemoriesTable(database, { embeddingColumn: "required" });
20260
+ database.exec(`
20261
+ CREATE TABLE IF NOT EXISTS memories (
20262
+ id TEXT PRIMARY KEY,
20263
+ content TEXT NOT NULL,
20264
+ workspace TEXT,
20265
+ embedding BLOB NOT NULL,
20266
+ created_at INTEGER NOT NULL,
20267
+ updated_at INTEGER NOT NULL
20268
+ );
20269
+ `);
20548
20270
  database.exec(`
20549
20271
  INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
20550
- SELECT id, content, workspace, embedding, created_at, updated_at
20272
+ SELECT id, content, workspace, COALESCE(embedding, X'00000000'), created_at, updated_at
20551
20273
  FROM memories_old
20552
20274
  `);
20553
20275
  database.exec("DROP TABLE memories_old");
@@ -20575,12 +20297,31 @@ function createNormalizeWorkspaceMigration(workspaceResolver) {
20575
20297
  };
20576
20298
  }
20577
20299
 
20300
+ // src/sqlite/migrations/004-remove-memory-embedding.ts
20301
+ var removeMemoryEmbeddingMigration = {
20302
+ version: 4,
20303
+ async up(database) {
20304
+ dropMemorySearchArtifacts(database);
20305
+ database.exec("ALTER TABLE memories RENAME TO memories_old");
20306
+ createMemoriesTable(database);
20307
+ database.exec(`
20308
+ INSERT INTO memories (id, content, workspace, created_at, updated_at)
20309
+ SELECT id, content, workspace, created_at, updated_at
20310
+ FROM memories_old
20311
+ `);
20312
+ database.exec("DROP TABLE memories_old");
20313
+ createMemoryIndexes(database);
20314
+ createMemorySearchArtifacts(database, true);
20315
+ }
20316
+ };
20317
+
20578
20318
  // src/sqlite/migrations/index.ts
20579
20319
  function createMemoryMigrations(options) {
20580
20320
  return [
20581
20321
  createMemorySchemaMigration,
20582
- createAddMemoryEmbeddingMigration(options.embeddingService),
20583
- createNormalizeWorkspaceMigration(options.workspaceResolver)
20322
+ createAddMemoryEmbeddingMigration(),
20323
+ createNormalizeWorkspaceMigration(options.workspaceResolver),
20324
+ removeMemoryEmbeddingMigration
20584
20325
  ];
20585
20326
  }
20586
20327
 
@@ -20594,7 +20335,7 @@ var PRAGMA_STATEMENTS = [
20594
20335
  async function openMemoryDatabase(databasePath, options) {
20595
20336
  let database;
20596
20337
  try {
20597
- mkdirSync2(dirname(databasePath), { recursive: true });
20338
+ mkdirSync(dirname(databasePath), { recursive: true });
20598
20339
  database = new Database(databasePath);
20599
20340
  await initializeMemoryDatabase(database, createMemoryMigrations(options));
20600
20341
  return database;
@@ -20668,7 +20409,6 @@ function validateMigrations(migrations) {
20668
20409
  }
20669
20410
  // src/sqlite/repository.ts
20670
20411
  import { randomUUID } from "node:crypto";
20671
- var DEFAULT_SEARCH_LIMIT = 15;
20672
20412
  var DEFAULT_LIST_LIMIT2 = 15;
20673
20413
 
20674
20414
  class SqliteMemoryRepository {
@@ -20681,24 +20421,11 @@ class SqliteMemoryRepository {
20681
20421
  constructor(database) {
20682
20422
  this.database = database;
20683
20423
  this.insertStatement = database.prepare(`
20684
- INSERT INTO memories (
20685
- id,
20686
- content,
20687
- workspace,
20688
- embedding,
20689
- created_at,
20690
- updated_at
20691
- ) VALUES (
20692
- ?,
20693
- ?,
20694
- ?,
20695
- ?,
20696
- ?,
20697
- ?
20698
- )
20424
+ INSERT INTO memories (id, content, workspace, created_at, updated_at)
20425
+ VALUES (?, ?, ?, ?, ?)
20699
20426
  `);
20700
- this.getStatement = database.prepare("SELECT id, content, workspace, embedding, created_at, updated_at FROM memories WHERE id = ?");
20701
- this.updateStatement = database.prepare("UPDATE memories SET content = ?, embedding = ?, updated_at = ? WHERE id = ?");
20427
+ this.getStatement = database.prepare("SELECT id, content, workspace, created_at, updated_at FROM memories WHERE id = ?");
20428
+ this.updateStatement = database.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?");
20702
20429
  this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
20703
20430
  this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
20704
20431
  }
@@ -20708,63 +20435,21 @@ class SqliteMemoryRepository {
20708
20435
  const memory = {
20709
20436
  id: randomUUID(),
20710
20437
  content: input.content,
20711
- embedding: input.embedding,
20712
20438
  workspace: input.workspace,
20713
20439
  createdAt: now,
20714
20440
  updatedAt: now
20715
20441
  };
20716
- this.insertStatement.run(memory.id, memory.content, memory.workspace, encodeEmbedding(memory.embedding), memory.createdAt.getTime(), memory.updatedAt.getTime());
20442
+ this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
20717
20443
  return memory;
20718
20444
  } catch (error2) {
20719
20445
  throw new PersistenceError("Failed to save memory.", { cause: error2 });
20720
20446
  }
20721
20447
  }
20722
- async search(query) {
20723
- try {
20724
- const whereParams = [toFtsQuery(query.terms)];
20725
- const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
20726
- const whereClauses = ["memories_fts MATCH ?"];
20727
- if (query.updatedAfter) {
20728
- whereClauses.push("m.updated_at >= ?");
20729
- whereParams.push(query.updatedAfter.getTime());
20730
- }
20731
- if (query.updatedBefore) {
20732
- whereClauses.push("m.updated_at <= ?");
20733
- whereParams.push(query.updatedBefore.getTime());
20734
- }
20735
- const params = [...whereParams, limit];
20736
- const statement = this.database.prepare(`
20737
- SELECT
20738
- m.id,
20739
- m.content,
20740
- m.workspace,
20741
- m.embedding,
20742
- m.created_at,
20743
- m.updated_at,
20744
- MAX(0, -bm25(memories_fts)) AS score
20745
- FROM memories_fts
20746
- INNER JOIN memories AS m ON m.rowid = memories_fts.rowid
20747
- WHERE ${whereClauses.join(" AND ")}
20748
- ORDER BY score DESC
20749
- LIMIT ?
20750
- `);
20751
- const rows = statement.all(...params);
20752
- const maxScore = Math.max(...rows.map((row) => row.score), 0);
20753
- return rows.map((row) => ({
20754
- ...toMemoryEntity(row),
20755
- score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
20756
- }));
20757
- } catch (error2) {
20758
- throw new PersistenceError("Failed to search memories.", {
20759
- cause: error2
20760
- });
20761
- }
20762
- }
20763
20448
  async get(id) {
20764
20449
  try {
20765
20450
  const rows = this.getStatement.all(id);
20766
20451
  const row = rows[0];
20767
- return row ? toMemoryEntity(row) : undefined;
20452
+ return row ? toMemoryRecord(row) : undefined;
20768
20453
  } catch (error2) {
20769
20454
  throw new PersistenceError("Failed to find memory.", { cause: error2 });
20770
20455
  }
@@ -20775,25 +20460,28 @@ class SqliteMemoryRepository {
20775
20460
  const params = [];
20776
20461
  const offset = options.offset ?? 0;
20777
20462
  const limit = options.limit ?? DEFAULT_LIST_LIMIT2;
20778
- if (options.workspace) {
20463
+ if (options.workspace && options.global) {
20464
+ whereClauses.push("(workspace = ? OR workspace IS NULL)");
20465
+ params.push(options.workspace);
20466
+ } else if (options.workspace) {
20779
20467
  whereClauses.push("workspace = ?");
20780
20468
  params.push(options.workspace);
20781
- } else if (options.workspaceIsNull) {
20469
+ } else if (options.global) {
20782
20470
  whereClauses.push("workspace IS NULL");
20783
20471
  }
20784
20472
  const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
20785
20473
  const queryLimit = limit + 1;
20786
20474
  params.push(queryLimit, offset);
20787
20475
  const statement = this.database.prepare(`
20788
- SELECT id, content, workspace, embedding, created_at, updated_at
20476
+ SELECT id, content, workspace, created_at, updated_at
20789
20477
  FROM memories
20790
20478
  ${whereClause}
20791
- ORDER BY created_at DESC
20479
+ ORDER BY updated_at DESC
20792
20480
  LIMIT ? OFFSET ?
20793
20481
  `);
20794
20482
  const rows = statement.all(...params);
20795
20483
  const hasMore = rows.length > limit;
20796
- const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryEntity);
20484
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryRecord);
20797
20485
  return { items, hasMore };
20798
20486
  } catch (error2) {
20799
20487
  throw new PersistenceError("Failed to list memories.", { cause: error2 });
@@ -20803,7 +20491,7 @@ class SqliteMemoryRepository {
20803
20491
  let result;
20804
20492
  try {
20805
20493
  const now = Date.now();
20806
- result = this.updateStatement.run(input.content, encodeEmbedding(input.embedding), now, input.id);
20494
+ result = this.updateStatement.run(input.content, now, input.id);
20807
20495
  } catch (error2) {
20808
20496
  throw new PersistenceError("Failed to update memory.", { cause: error2 });
20809
20497
  }
@@ -20836,22 +20524,13 @@ class SqliteMemoryRepository {
20836
20524
  }
20837
20525
  }
20838
20526
  }
20839
- var toMemoryEntity = (row) => ({
20527
+ var toMemoryRecord = (row) => ({
20840
20528
  id: row.id,
20841
20529
  content: row.content,
20842
- embedding: decodeEmbedding(row.embedding),
20843
20530
  workspace: row.workspace ?? undefined,
20844
20531
  createdAt: new Date(row.created_at),
20845
20532
  updatedAt: new Date(row.updated_at)
20846
20533
  });
20847
- var toFtsQuery = (terms) => terms.map(toFtsTerm).join(" OR ");
20848
- function toFtsTerm(term) {
20849
- const escaped = term.replaceAll('"', '""');
20850
- if (term.includes(" ")) {
20851
- return `"${escaped}"`;
20852
- }
20853
- return `"${escaped}"*`;
20854
- }
20855
20534
  // node_modules/@hono/node-server/dist/index.mjs
20856
20535
  import { createServer as createServerHTTP } from "http";
20857
20536
  import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
@@ -24167,7 +23846,7 @@ function createPageRoutes(memory) {
24167
23846
  const wsFilter = workspace && !isNoWorkspace ? workspace : undefined;
24168
23847
  const page = await memory.list({
24169
23848
  workspace: wsFilter,
24170
- workspaceIsNull: isNoWorkspace,
23849
+ global: isNoWorkspace,
24171
23850
  offset: (pageNum - 1) * DEFAULT_LIST_LIMIT3,
24172
23851
  limit: DEFAULT_LIST_LIMIT3
24173
23852
  });
@@ -24288,11 +23967,10 @@ function normalizeOptionalString2(value) {
24288
23967
 
24289
23968
  // src/index.ts
24290
23969
  var config2 = resolveConfig();
24291
- var embeddingService = new EmbeddingService({ modelsCachePath: config2.modelsCachePath });
24292
23970
  var workspaceResolver = createGitWorkspaceResolver();
24293
- var database = await openMemoryDatabase(config2.databasePath, { embeddingService, workspaceResolver });
23971
+ var database = await openMemoryDatabase(config2.databasePath, { workspaceResolver });
24294
23972
  var repository2 = new SqliteMemoryRepository(database);
24295
- var memoryService = new MemoryService(repository2, embeddingService, workspaceResolver);
23973
+ var memoryService = new MemoryService(repository2, workspaceResolver);
24296
23974
  if (config2.uiMode) {
24297
23975
  let shutdown = function() {
24298
23976
  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.19",
4
+ "version": "0.1.0",
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 --external @huggingface/transformers --outfile dist/index.js --banner \"#!/usr/bin/env node\"",
23
+ "build": "bun build src/index.ts --target=node --external better-sqlite3 --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,7 +38,6 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@hono/node-server": "^1.19.11",
41
- "@huggingface/transformers": "^3.8.1",
42
41
  "@modelcontextprotocol/sdk": "^1.27.1",
43
42
  "better-sqlite3": "^12.6.2",
44
43
  "hono": "^4.12.8",