@jcyamacho/agent-memory 0.0.12 → 0.0.15

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 +58 -67
  2. package/dist/index.js +825 -510
  3. package/package.json +3 -2
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,15 +12464,16 @@ class StdioServerTransport {
12464
12464
  }
12465
12465
  }
12466
12466
  // package.json
12467
- var version2 = "0.0.12";
12467
+ var version2 = "0.0.15";
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
- var resolveConfig = (environment = process.env, argv = process.argv.slice(2)) => {
12476
+ function resolveConfig(environment = process.env, argv = process.argv.slice(2)) {
12476
12477
  const { values } = parseArgs({
12477
12478
  args: argv,
12478
12479
  options: {
@@ -12482,12 +12483,146 @@ var 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
+ }
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";
12490
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,39 +20030,8 @@ 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
- // src/tools/shared.ts
19930
- var toMcpError = (error2) => {
20033
+ // src/mcp/tools/shared.ts
20034
+ function toMcpError(error2) {
19931
20035
  if (error2 instanceof McpError) {
19932
20036
  return error2;
19933
20037
  }
@@ -19939,9 +20043,9 @@ var toMcpError = (error2) => {
19939
20043
  }
19940
20044
  const message = error2 instanceof Error ? error2.message : "Unknown server error.";
19941
20045
  return new McpError(ErrorCode.InternalError, message);
19942
- };
20046
+ }
19943
20047
  var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19944
- var parseOptionalDate = (value, fieldName) => {
20048
+ function parseOptionalDate(value, fieldName) {
19945
20049
  if (!value) {
19946
20050
  return;
19947
20051
  }
@@ -19950,19 +20054,19 @@ var parseOptionalDate = (value, fieldName) => {
19950
20054
  throw new MemoryError("VALIDATION_ERROR", `${fieldName} must be a valid ISO 8601 datetime.`);
19951
20055
  }
19952
20056
  return date4;
19953
- };
20057
+ }
19954
20058
 
19955
- // src/tools/forget.ts
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
- var registerForgetTool = (server, memoryService) => {
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 {
19965
- await memoryService.forget({ id });
20069
+ await memory.delete({ id });
19966
20070
  return {
19967
20071
  content: [{ type: "text", text: `<memory id="${id.trim()}" deleted="true" />` }]
19968
20072
  };
@@ -19970,56 +20074,127 @@ var registerForgetTool = (server, memoryService) => {
19970
20074
  throw toMcpError(error2);
19971
20075
  }
19972
20076
  });
19973
- };
19974
-
19975
- // src/memory-service.ts
19976
- import { randomUUID } from "node:crypto";
20077
+ }
19977
20078
 
19978
20079
  // src/memory.ts
19979
20080
  var toNormalizedScore = (value) => value;
19980
20081
 
19981
- // src/memory-service.ts
19982
- var DEFAULT_LIMIT = 15;
19983
- var MAX_LIMIT = 50;
19984
- var RECALL_CANDIDATE_LIMIT_MULTIPLIER = 3;
20082
+ // src/ranking.ts
19985
20083
  var RETRIEVAL_SCORE_WEIGHT = 8;
20084
+ var EMBEDDING_SIMILARITY_WEIGHT = 5;
19986
20085
  var WORKSPACE_MATCH_WEIGHT = 4;
19987
- var RECENCY_WEIGHT = 1;
19988
- 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;
19989
20088
  var GLOBAL_WORKSPACE_SCORE = 0.5;
19990
20089
  var SIBLING_WORKSPACE_SCORE = 0.25;
20090
+ function rerankSearchResults(results, workspace, queryEmbedding) {
20091
+ if (results.length <= 1) {
20092
+ return results;
20093
+ }
20094
+ const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
20095
+ const updatedAtTimes = results.map((result) => result.updatedAt.getTime());
20096
+ const minUpdatedAt = Math.min(...updatedAtTimes);
20097
+ const maxUpdatedAt = Math.max(...updatedAtTimes);
20098
+ return results.map((result) => {
20099
+ const embeddingSimilarityScore = computeEmbeddingSimilarityScore(result, queryEmbedding);
20100
+ const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
20101
+ const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
20102
+ const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + embeddingSimilarityScore * EMBEDDING_SIMILARITY_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
20103
+ return {
20104
+ ...result,
20105
+ score: toNormalizedScore(combinedScore)
20106
+ };
20107
+ }).sort((a, b) => b.score - a.score);
20108
+ }
20109
+ function computeEmbeddingSimilarityScore(result, queryEmbedding) {
20110
+ return normalizeCosineSimilarity(compareVectors(result.embedding, queryEmbedding));
20111
+ }
20112
+ function normalizeWorkspacePath(value) {
20113
+ return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
20114
+ }
20115
+ function normalizeCosineSimilarity(value) {
20116
+ return (value + 1) / 2;
20117
+ }
20118
+ function computeWorkspaceScore(memoryWs, queryWs) {
20119
+ if (!queryWs) {
20120
+ return 0;
20121
+ }
20122
+ if (!memoryWs) {
20123
+ return GLOBAL_WORKSPACE_SCORE;
20124
+ }
20125
+ const normalizedMemoryWs = normalizeWorkspacePath(memoryWs);
20126
+ if (normalizedMemoryWs === queryWs) {
20127
+ return 1;
20128
+ }
20129
+ const queryLastSlashIndex = queryWs.lastIndexOf("/");
20130
+ const memoryLastSlashIndex = normalizedMemoryWs.lastIndexOf("/");
20131
+ if (queryLastSlashIndex <= 0 || memoryLastSlashIndex <= 0) {
20132
+ return 0;
20133
+ }
20134
+ const queryParent = queryWs.slice(0, queryLastSlashIndex);
20135
+ const memoryParent = normalizedMemoryWs.slice(0, memoryLastSlashIndex);
20136
+ return memoryParent === queryParent ? SIBLING_WORKSPACE_SCORE : 0;
20137
+ }
20138
+
20139
+ // src/memory-service.ts
20140
+ var DEFAULT_RECALL_LIMIT = 15;
20141
+ var MAX_RECALL_LIMIT = 50;
20142
+ var RECALL_CANDIDATE_LIMIT_MULTIPLIER = 3;
20143
+ var DEFAULT_LIST_LIMIT = 15;
20144
+ var MAX_LIST_LIMIT = 100;
19991
20145
 
19992
20146
  class MemoryService {
19993
20147
  repository;
19994
- constructor(repository) {
20148
+ embeddingService;
20149
+ constructor(repository, embeddingService) {
19995
20150
  this.repository = repository;
20151
+ this.embeddingService = embeddingService;
19996
20152
  }
19997
- async save(input) {
20153
+ async create(input) {
19998
20154
  const content = input.content.trim();
19999
20155
  if (!content) {
20000
20156
  throw new ValidationError("Memory content is required.");
20001
20157
  }
20002
- const now = new Date;
20003
- const memory = {
20004
- id: randomUUID(),
20158
+ const memory = await this.repository.create({
20005
20159
  content,
20006
- workspace: normalizeOptionalString(input.workspace),
20007
- createdAt: now,
20008
- updatedAt: now
20009
- };
20010
- return this.repository.save(memory);
20160
+ embedding: await this.embeddingService.createVector(content),
20161
+ workspace: normalizeOptionalString(input.workspace)
20162
+ });
20163
+ return toPublicMemoryRecord(memory);
20011
20164
  }
20012
- async revise(input) {
20165
+ async update(input) {
20013
20166
  const content = input.content.trim();
20014
20167
  if (!content)
20015
20168
  throw new ValidationError("Memory content is required.");
20016
- return this.repository.update(input.id, content);
20169
+ const memory = await this.repository.update({
20170
+ id: input.id,
20171
+ content,
20172
+ embedding: await this.embeddingService.createVector(content)
20173
+ });
20174
+ return toPublicMemoryRecord(memory);
20017
20175
  }
20018
- async forget(input) {
20176
+ async delete(input) {
20019
20177
  const id = input.id.trim();
20020
20178
  if (!id)
20021
20179
  throw new ValidationError("Memory id is required.");
20022
- return this.repository.delete(id);
20180
+ return this.repository.delete({ id });
20181
+ }
20182
+ async get(id) {
20183
+ const memory = await this.repository.get(id);
20184
+ return memory ? toPublicMemoryRecord(memory) : undefined;
20185
+ }
20186
+ async list(input) {
20187
+ const workspace = normalizeOptionalString(input.workspace);
20188
+ const page = await this.repository.list({
20189
+ workspace,
20190
+ workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
20191
+ offset: normalizeOffset(input.offset),
20192
+ limit: normalizeListLimit(input.limit)
20193
+ });
20194
+ return toPublicMemoryPage(page);
20195
+ }
20196
+ async listWorkspaces() {
20197
+ return this.repository.listWorkspaces();
20023
20198
  }
20024
20199
  async search(input) {
20025
20200
  const terms = normalizeTerms(input.terms);
@@ -20034,20 +20209,56 @@ class MemoryService {
20034
20209
  updatedAfter: input.updatedAfter,
20035
20210
  updatedBefore: input.updatedBefore
20036
20211
  };
20037
- const results = await this.repository.search(normalizedQuery);
20038
- const reranked = rerankSearchResults(results, workspace);
20039
- return reranked.sort((a, b) => b.score - a.score).slice(0, requestedLimit);
20212
+ const [results, queryEmbedding] = await Promise.all([
20213
+ this.repository.search(normalizedQuery),
20214
+ this.embeddingService.createVector(terms.join(" "))
20215
+ ]);
20216
+ return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map(toPublicSearchResult);
20040
20217
  }
20041
20218
  }
20219
+ function toPublicMemoryRecord(memory) {
20220
+ return {
20221
+ id: memory.id,
20222
+ content: memory.content,
20223
+ workspace: memory.workspace,
20224
+ createdAt: memory.createdAt,
20225
+ updatedAt: memory.updatedAt
20226
+ };
20227
+ }
20228
+ function toPublicSearchResult(result) {
20229
+ return {
20230
+ id: result.id,
20231
+ content: result.content,
20232
+ score: result.score,
20233
+ workspace: result.workspace,
20234
+ createdAt: result.createdAt,
20235
+ updatedAt: result.updatedAt
20236
+ };
20237
+ }
20238
+ function toPublicMemoryPage(page) {
20239
+ return {
20240
+ items: page.items.map(toPublicMemoryRecord),
20241
+ hasMore: page.hasMore
20242
+ };
20243
+ }
20042
20244
  function normalizeLimit(value) {
20043
20245
  if (value === undefined) {
20044
- return DEFAULT_LIMIT;
20246
+ return DEFAULT_RECALL_LIMIT;
20045
20247
  }
20046
- if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) {
20047
- throw new ValidationError(`Limit must be an integer between 1 and ${MAX_LIMIT}.`);
20248
+ if (!Number.isInteger(value) || value < 1 || value > MAX_RECALL_LIMIT) {
20249
+ throw new ValidationError(`Limit must be an integer between 1 and ${MAX_RECALL_LIMIT}.`);
20048
20250
  }
20049
20251
  return value;
20050
20252
  }
20253
+ function normalizeOffset(value) {
20254
+ return Number.isInteger(value) && value && value > 0 ? value : 0;
20255
+ }
20256
+ function normalizeListLimit(value) {
20257
+ if (!Number.isInteger(value) || value === undefined) {
20258
+ return DEFAULT_LIST_LIMIT;
20259
+ }
20260
+ return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
20261
+ }
20051
20262
  function normalizeOptionalString(value) {
20052
20263
  const trimmed = value?.trim();
20053
20264
  return trimmed ? trimmed : undefined;
@@ -20056,77 +20267,37 @@ function normalizeTerms(terms) {
20056
20267
  const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
20057
20268
  return [...new Set(normalizedTerms)];
20058
20269
  }
20059
- function normalizeWorkspacePath(value) {
20060
- return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
20061
- }
20062
- function computeWorkspaceScore(memoryWs, queryWs) {
20063
- if (!queryWs) {
20064
- return 0;
20065
- }
20066
- if (!memoryWs) {
20067
- return GLOBAL_WORKSPACE_SCORE;
20068
- }
20069
- const normalizedMemoryWs = normalizeWorkspacePath(memoryWs);
20070
- if (normalizedMemoryWs === queryWs) {
20071
- return 1;
20072
- }
20073
- const queryLastSlashIndex = queryWs.lastIndexOf("/");
20074
- const memoryLastSlashIndex = normalizedMemoryWs.lastIndexOf("/");
20075
- if (queryLastSlashIndex <= 0 || memoryLastSlashIndex <= 0) {
20076
- return 0;
20077
- }
20078
- const queryParent = queryWs.slice(0, queryLastSlashIndex);
20079
- const memoryParent = normalizedMemoryWs.slice(0, memoryLastSlashIndex);
20080
- return memoryParent === queryParent ? SIBLING_WORKSPACE_SCORE : 0;
20081
- }
20082
- function rerankSearchResults(results, workspace) {
20083
- if (results.length <= 1) {
20084
- return results;
20085
- }
20086
- const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
20087
- const updatedAtTimes = results.map((result) => result.updatedAt.getTime());
20088
- const minUpdatedAt = Math.min(...updatedAtTimes);
20089
- const maxUpdatedAt = Math.max(...updatedAtTimes);
20090
- return results.map((result) => {
20091
- const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
20092
- const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
20093
- const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
20094
- return {
20095
- ...result,
20096
- score: toNormalizedScore(combinedScore)
20097
- };
20098
- });
20099
- }
20100
20270
 
20101
- // src/tools/recall.ts
20271
+ // src/mcp/tools/recall.ts
20102
20272
  var recallInputSchema = {
20103
- 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."),
20104
- limit: number2().int().min(1).max(MAX_LIMIT).optional().describe("Maximum number of matches to return. Keep this small when you only need the strongest hits."),
20105
- 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."),
20106
- 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."),
20107
- 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.")
20273
+ 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."),
20274
+ 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."),
20275
+ workspace: string2().optional().describe("Pass the current working directory. This strongly boosts memories from the active project while still allowing global and cross-workspace matches."),
20276
+ updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp."),
20277
+ updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp.")
20108
20278
  };
20109
- var toMemoryXml = (r) => {
20279
+ function toMemoryXml(r) {
20110
20280
  const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
20111
20281
  const content = escapeXml(r.content);
20112
- return `<memory id="${r.id}" score="${r.score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
20282
+ const score = Number(r.score.toFixed(3)).toString();
20283
+ return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
20113
20284
  ${content}
20114
20285
  </memory>`;
20115
- };
20116
- var registerRecallTool = (server, memoryService) => {
20286
+ }
20287
+ function registerRecallTool(server, memory) {
20117
20288
  server.registerTool("recall", {
20118
- 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.",
20289
+ 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. Returns `<memories>...</memories>` or a no-match hint.",
20119
20290
  inputSchema: recallInputSchema
20120
20291
  }, async ({ terms, limit, workspace, updated_after, updated_before }) => {
20121
20292
  try {
20122
- const results = await memoryService.search({
20293
+ const results = await memory.search({
20123
20294
  terms,
20124
20295
  limit,
20125
20296
  workspace,
20126
20297
  updatedAfter: parseOptionalDate(updated_after, "updated_after"),
20127
20298
  updatedBefore: parseOptionalDate(updated_before, "updated_before")
20128
20299
  });
20129
- const text = results.length === 0 ? "No matching memories found." : `<memories>
20300
+ 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>
20130
20301
  ${results.map(toMemoryXml).join(`
20131
20302
  `)}
20132
20303
  </memories>`;
@@ -20137,49 +20308,49 @@ ${results.map(toMemoryXml).join(`
20137
20308
  throw toMcpError(error2);
20138
20309
  }
20139
20310
  });
20140
- };
20311
+ }
20141
20312
 
20142
- // src/tools/remember.ts
20313
+ // src/mcp/tools/remember.ts
20143
20314
  var rememberInputSchema = {
20144
- content: string2().describe("The fact, preference, decision, or context to remember. Use a single self-contained sentence or short note. One fact per memory."),
20145
- 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).")
20315
+ 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."),
20316
+ workspace: string2().optional().describe("Pass the current working directory for project-specific memories. Omit only for truly global memories.")
20146
20317
  };
20147
- var registerRememberTool = (server, memoryService) => {
20318
+ function registerRememberTool(server, memory) {
20148
20319
  server.registerTool("remember", {
20149
- 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.",
20320
+ 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="..." />`.',
20150
20321
  inputSchema: rememberInputSchema
20151
20322
  }, async ({ content, workspace }) => {
20152
20323
  try {
20153
- const memory = await memoryService.save({
20324
+ const savedMemory = await memory.create({
20154
20325
  content,
20155
20326
  workspace
20156
20327
  });
20157
20328
  return {
20158
- content: [{ type: "text", text: `<memory id="${memory.id}" />` }]
20329
+ content: [{ type: "text", text: `<memory id="${savedMemory.id}" />` }]
20159
20330
  };
20160
20331
  } catch (error2) {
20161
20332
  throw toMcpError(error2);
20162
20333
  }
20163
20334
  });
20164
- };
20335
+ }
20165
20336
 
20166
- // src/tools/revise.ts
20337
+ // src/mcp/tools/revise.ts
20167
20338
  var reviseInputSchema = {
20168
- id: string2().describe("The id of the memory to update. Use the id returned by a previous recall result."),
20169
- content: string2().describe("The replacement content for the memory. Use a single self-contained sentence or short note. One fact per memory.")
20339
+ id: string2().describe("The memory id to update. Use an id returned by `recall`."),
20340
+ content: string2().describe("The corrected replacement for that same fact. Keep it to one durable fact.")
20170
20341
  };
20171
- var registerReviseTool = (server, memoryService) => {
20342
+ function registerReviseTool(server, memory) {
20172
20343
  server.registerTool("revise", {
20173
- 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.",
20344
+ 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="..." />`.',
20174
20345
  inputSchema: reviseInputSchema
20175
20346
  }, async ({ id, content }) => {
20176
20347
  try {
20177
- const memory = await memoryService.revise({ id, content });
20348
+ const revisedMemory = await memory.update({ id, content });
20178
20349
  return {
20179
20350
  content: [
20180
20351
  {
20181
20352
  type: "text",
20182
- text: `<memory id="${memory.id}" updated_at="${memory.updatedAt.toISOString()}" />`
20353
+ text: `<memory id="${revisedMemory.id}" updated_at="${revisedMemory.updatedAt.toISOString()}" />`
20183
20354
  }
20184
20355
  ]
20185
20356
  };
@@ -20187,103 +20358,278 @@ var registerReviseTool = (server, memoryService) => {
20187
20358
  throw toMcpError(error2);
20188
20359
  }
20189
20360
  });
20190
- };
20361
+ }
20191
20362
 
20192
- // src/mcp-server.ts
20363
+ // src/mcp/server.ts
20193
20364
  var SERVER_INSTRUCTIONS = [
20194
- "Stores decisions, corrections, and context that cannot be derived from code or git history.",
20195
- "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.",
20196
- "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.",
20197
- "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.",
20198
- "Use `revise` when a previously saved memory is outdated or inaccurate and needs correction rather than deletion.",
20199
- "Use `forget` to remove memories that are wrong, obsolete, or no longer relevant.",
20200
- "Always pass workspace (the current working directory) to scope results to the active project.",
20201
- "Omit workspace only when saving a memory that applies across all projects."
20365
+ "Use this server for durable memory: user preferences, corrections, decisions, and project context not obvious from code or git history.",
20366
+ "Use `recall` at conversation start and before design choices, conventions, or edge cases.",
20367
+ "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.",
20368
+ "`recall` is lexical-first; semantic reranking only reorders lexical matches.",
20369
+ "If `recall` misses, retry once with overlapping alternate terms.",
20370
+ "Use `remember` for one durable fact when the user states a preference, corrects you, or a reusable project decision becomes clear.",
20371
+ "Call `recall` before `remember`; if the fact already exists, use `revise` instead of creating a duplicate.",
20372
+ "Use `revise` to correct an existing memory and `forget` to remove a wrong or obsolete one.",
20373
+ "Pass workspace for project-scoped calls. Omit it only for truly global memories."
20202
20374
  ].join(" ");
20203
- var createMcpServer = (memoryService, version3) => {
20375
+ function createMcpServer(memory, version3) {
20204
20376
  const server = new McpServer({
20205
20377
  name: "agent-memory",
20206
20378
  version: version3
20207
20379
  }, {
20208
20380
  instructions: SERVER_INSTRUCTIONS
20209
20381
  });
20210
- registerRememberTool(server, memoryService);
20211
- registerRecallTool(server, memoryService);
20212
- registerReviseTool(server, memoryService);
20213
- registerForgetTool(server, memoryService);
20382
+ registerRememberTool(server, memory);
20383
+ registerRecallTool(server, memory);
20384
+ registerReviseTool(server, memory);
20385
+ registerForgetTool(server, memory);
20214
20386
  return server;
20215
- };
20387
+ }
20216
20388
 
20217
- // src/sqlite-db.ts
20218
- import { mkdirSync } from "node:fs";
20389
+ // src/sqlite/db.ts
20390
+ import { mkdirSync as mkdirSync2 } from "node:fs";
20219
20391
  import { dirname } from "node:path";
20220
20392
  import Database from "better-sqlite3";
20221
- var MEMORY_SCHEMA = `
20222
- CREATE TABLE IF NOT EXISTS memories (
20223
- id TEXT PRIMARY KEY,
20224
- content TEXT NOT NULL,
20225
- workspace TEXT,
20226
- created_at INTEGER NOT NULL,
20227
- updated_at INTEGER NOT NULL
20228
- );
20229
-
20230
- CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
20231
- CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
20232
-
20233
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
20234
- content,
20235
- content = 'memories',
20236
- content_rowid = 'rowid',
20237
- tokenize = 'porter unicode61'
20238
- );
20239
-
20240
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
20241
- INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20242
- END;
20243
-
20244
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
20245
- INSERT INTO memories_fts(memories_fts, rowid, content)
20246
- VALUES ('delete', old.rowid, old.content);
20247
- END;
20248
-
20249
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
20250
- INSERT INTO memories_fts(memories_fts, rowid, content)
20251
- VALUES ('delete', old.rowid, old.content);
20252
- INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20253
- END;
20254
- `;
20255
- var openMemoryDatabase = (databasePath) => {
20393
+
20394
+ // src/sqlite/memory-schema.ts
20395
+ function createMemoriesTable(database, options) {
20396
+ const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
20397
+ database.exec(`
20398
+ CREATE TABLE IF NOT EXISTS memories (
20399
+ id TEXT PRIMARY KEY,
20400
+ content TEXT NOT NULL,
20401
+ workspace TEXT,
20402
+ ${embeddingColumn}
20403
+ created_at INTEGER NOT NULL,
20404
+ updated_at INTEGER NOT NULL
20405
+ );
20406
+ `);
20407
+ }
20408
+ function createMemoryIndexes(database) {
20409
+ database.exec(`
20410
+ CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
20411
+ CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
20412
+ `);
20413
+ }
20414
+ function createMemorySearchArtifacts(database, rebuild = false) {
20415
+ database.exec(`
20416
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
20417
+ content,
20418
+ content = 'memories',
20419
+ content_rowid = 'rowid',
20420
+ tokenize = 'porter unicode61'
20421
+ );
20422
+
20423
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
20424
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20425
+ END;
20426
+
20427
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
20428
+ INSERT INTO memories_fts(memories_fts, rowid, content)
20429
+ VALUES ('delete', old.rowid, old.content);
20430
+ END;
20431
+
20432
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
20433
+ INSERT INTO memories_fts(memories_fts, rowid, content)
20434
+ VALUES ('delete', old.rowid, old.content);
20435
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
20436
+ END;
20437
+ `);
20438
+ if (rebuild) {
20439
+ database.exec("INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')");
20440
+ }
20441
+ }
20442
+ function dropMemorySearchArtifacts(database) {
20443
+ database.exec(`
20444
+ DROP TRIGGER IF EXISTS memories_ai;
20445
+ DROP TRIGGER IF EXISTS memories_ad;
20446
+ DROP TRIGGER IF EXISTS memories_au;
20447
+ DROP TABLE IF EXISTS memories_fts;
20448
+ `);
20449
+ }
20450
+ function getEmbeddingColumnSql(mode) {
20451
+ switch (mode) {
20452
+ case "omit":
20453
+ return "";
20454
+ case "nullable":
20455
+ return `embedding BLOB,
20456
+ `;
20457
+ case "required":
20458
+ return `embedding BLOB NOT NULL,
20459
+ `;
20460
+ }
20461
+ }
20462
+
20463
+ // src/sqlite/migrations/001-create-memory-schema.ts
20464
+ var createMemorySchemaMigration = {
20465
+ version: 1,
20466
+ async up(database) {
20467
+ createMemoriesTable(database, { embeddingColumn: "omit" });
20468
+ createMemoryIndexes(database);
20469
+ createMemorySearchArtifacts(database);
20470
+ }
20471
+ };
20472
+
20473
+ // src/sqlite/embedding-codec.ts
20474
+ var FLOAT32_BYTE_WIDTH = 4;
20475
+ function encodeEmbedding(vector) {
20476
+ const typedArray = Float32Array.from(vector);
20477
+ return new Uint8Array(typedArray.buffer.slice(0));
20478
+ }
20479
+ function decodeEmbedding(value) {
20480
+ const bytes = toUint8Array(value);
20481
+ if (bytes.byteLength === 0) {
20482
+ throw new Error("Embedding blob is empty.");
20483
+ }
20484
+ if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
20485
+ throw new Error("Embedding blob length is not a multiple of 4.");
20486
+ }
20487
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
20488
+ const vector = [];
20489
+ for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
20490
+ vector.push(view.getFloat32(offset, true));
20491
+ }
20492
+ return vector;
20493
+ }
20494
+ function toUint8Array(value) {
20495
+ if (value instanceof Uint8Array) {
20496
+ return value;
20497
+ }
20498
+ if (value instanceof ArrayBuffer) {
20499
+ return new Uint8Array(value);
20500
+ }
20501
+ throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
20502
+ }
20503
+
20504
+ // src/sqlite/migrations/002-add-memory-embedding.ts
20505
+ function createAddMemoryEmbeddingMigration(embeddingService = new EmbeddingService) {
20506
+ return {
20507
+ version: 2,
20508
+ async up(database) {
20509
+ database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
20510
+ const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
20511
+ const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
20512
+ for (const row of rows) {
20513
+ const embedding = await embeddingService.createVector(row.content);
20514
+ updateStatement.run(encodeEmbedding(embedding), row.id);
20515
+ }
20516
+ const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
20517
+ if ((nullRows[0]?.count ?? 0) > 0) {
20518
+ throw new Error("Failed to backfill embeddings for all memories.");
20519
+ }
20520
+ dropMemorySearchArtifacts(database);
20521
+ database.exec("ALTER TABLE memories RENAME TO memories_old");
20522
+ createMemoriesTable(database, { embeddingColumn: "required" });
20523
+ database.exec(`
20524
+ INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
20525
+ SELECT id, content, workspace, embedding, created_at, updated_at
20526
+ FROM memories_old
20527
+ `);
20528
+ database.exec("DROP TABLE memories_old");
20529
+ createMemoryIndexes(database);
20530
+ createMemorySearchArtifacts(database, true);
20531
+ }
20532
+ };
20533
+ }
20534
+ var addMemoryEmbeddingMigration = createAddMemoryEmbeddingMigration();
20535
+
20536
+ // src/sqlite/migrations/index.ts
20537
+ function createMemoryMigrations(embeddingService = new EmbeddingService) {
20538
+ return [createMemorySchemaMigration, createAddMemoryEmbeddingMigration(embeddingService)];
20539
+ }
20540
+ var MEMORY_MIGRATIONS = createMemoryMigrations();
20541
+
20542
+ // src/sqlite/db.ts
20543
+ var PRAGMA_STATEMENTS = [
20544
+ "journal_mode = WAL",
20545
+ "synchronous = NORMAL",
20546
+ "foreign_keys = ON",
20547
+ "busy_timeout = 5000"
20548
+ ];
20549
+ async function openMemoryDatabase(databasePath, options = {}) {
20550
+ let database;
20256
20551
  try {
20257
- mkdirSync(dirname(databasePath), { recursive: true });
20258
- const database = new Database(databasePath);
20259
- initializeMemoryDatabase(database);
20552
+ mkdirSync2(dirname(databasePath), { recursive: true });
20553
+ database = new Database(databasePath);
20554
+ await initializeMemoryDatabase(database, createMemoryMigrations(options.embeddingService));
20260
20555
  return database;
20261
20556
  } catch (error2) {
20557
+ database?.close();
20558
+ if (error2 instanceof PersistenceError) {
20559
+ throw error2;
20560
+ }
20262
20561
  throw new PersistenceError("Failed to initialize the SQLite database.", {
20263
20562
  cause: error2
20264
20563
  });
20265
20564
  }
20266
- };
20267
- var initializeMemoryDatabase = (database) => {
20565
+ }
20566
+ async function initializeMemoryDatabase(database, migrations = MEMORY_MIGRATIONS) {
20567
+ try {
20568
+ applyPragmas(database);
20569
+ await runSqliteMigrations(database, migrations);
20570
+ } catch (error2) {
20571
+ throw new PersistenceError("Failed to initialize the SQLite database.", {
20572
+ cause: error2
20573
+ });
20574
+ }
20575
+ }
20576
+ async function runSqliteMigrations(database, migrations) {
20577
+ validateMigrations(migrations);
20578
+ let currentVersion = getUserVersion(database);
20579
+ for (const migration of migrations) {
20580
+ if (migration.version <= currentVersion) {
20581
+ continue;
20582
+ }
20583
+ database.exec("BEGIN");
20584
+ try {
20585
+ await migration.up(database);
20586
+ setUserVersion(database, migration.version);
20587
+ database.exec("COMMIT");
20588
+ currentVersion = migration.version;
20589
+ } catch (error2) {
20590
+ try {
20591
+ database.exec("ROLLBACK");
20592
+ } catch {}
20593
+ throw error2;
20594
+ }
20595
+ }
20596
+ }
20597
+ function applyPragmas(database) {
20268
20598
  if (database.pragma) {
20269
- database.pragma("journal_mode = WAL");
20270
- database.pragma("synchronous = NORMAL");
20271
- database.pragma("foreign_keys = ON");
20272
- database.pragma("busy_timeout = 5000");
20273
- } else {
20274
- database.exec("PRAGMA journal_mode = WAL");
20275
- database.exec("PRAGMA synchronous = NORMAL");
20276
- database.exec("PRAGMA foreign_keys = ON");
20277
- database.exec("PRAGMA busy_timeout = 5000");
20599
+ for (const statement of PRAGMA_STATEMENTS) {
20600
+ database.pragma(statement);
20601
+ }
20602
+ return;
20278
20603
  }
20279
- database.exec(MEMORY_SCHEMA);
20280
- };
20604
+ for (const statement of PRAGMA_STATEMENTS) {
20605
+ database.exec(`PRAGMA ${statement}`);
20606
+ }
20607
+ }
20608
+ function getUserVersion(database) {
20609
+ const rows = database.prepare("PRAGMA user_version").all();
20610
+ return rows[0]?.user_version ?? 0;
20611
+ }
20612
+ function setUserVersion(database, version3) {
20613
+ database.exec(`PRAGMA user_version = ${version3}`);
20614
+ }
20615
+ function validateMigrations(migrations) {
20616
+ let previousVersion = 0;
20617
+ for (const migration of migrations) {
20618
+ if (!Number.isInteger(migration.version) || migration.version <= previousVersion) {
20619
+ throw new Error("SQLite migrations must use strictly increasing versions.");
20620
+ }
20621
+ previousVersion = migration.version;
20622
+ }
20623
+ }
20624
+ // src/sqlite/repository.ts
20625
+ import { randomUUID } from "node:crypto";
20626
+ var DEFAULT_SEARCH_LIMIT = 15;
20627
+ var DEFAULT_LIST_LIMIT2 = 15;
20281
20628
 
20282
- // src/sqlite-repository.ts
20283
20629
  class SqliteMemoryRepository {
20284
20630
  database;
20285
20631
  insertStatement;
20286
- findByIdStatement;
20632
+ getStatement;
20287
20633
  updateStatement;
20288
20634
  deleteStatement;
20289
20635
  listWorkspacesStatement;
@@ -20294,6 +20640,7 @@ class SqliteMemoryRepository {
20294
20640
  id,
20295
20641
  content,
20296
20642
  workspace,
20643
+ embedding,
20297
20644
  created_at,
20298
20645
  updated_at
20299
20646
  ) VALUES (
@@ -20301,17 +20648,27 @@ class SqliteMemoryRepository {
20301
20648
  ?,
20302
20649
  ?,
20303
20650
  ?,
20651
+ ?,
20304
20652
  ?
20305
20653
  )
20306
20654
  `);
20307
- this.findByIdStatement = database.prepare("SELECT id, content, workspace, created_at, updated_at FROM memories WHERE id = ?");
20308
- this.updateStatement = database.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?");
20655
+ this.getStatement = database.prepare("SELECT id, content, workspace, embedding, created_at, updated_at FROM memories WHERE id = ?");
20656
+ this.updateStatement = database.prepare("UPDATE memories SET content = ?, embedding = ?, updated_at = ? WHERE id = ?");
20309
20657
  this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
20310
20658
  this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
20311
20659
  }
20312
- async save(memory) {
20660
+ async create(input) {
20313
20661
  try {
20314
- this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
20662
+ const now = new Date;
20663
+ const memory = {
20664
+ id: randomUUID(),
20665
+ content: input.content,
20666
+ embedding: input.embedding,
20667
+ workspace: input.workspace,
20668
+ createdAt: now,
20669
+ updatedAt: now
20670
+ };
20671
+ this.insertStatement.run(memory.id, memory.content, memory.workspace, encodeEmbedding(memory.embedding), memory.createdAt.getTime(), memory.updatedAt.getTime());
20315
20672
  return memory;
20316
20673
  } catch (error2) {
20317
20674
  throw new PersistenceError("Failed to save memory.", { cause: error2 });
@@ -20320,6 +20677,7 @@ class SqliteMemoryRepository {
20320
20677
  async search(query) {
20321
20678
  try {
20322
20679
  const whereParams = [toFtsQuery(query.terms)];
20680
+ const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
20323
20681
  const whereClauses = ["memories_fts MATCH ?"];
20324
20682
  if (query.updatedAfter) {
20325
20683
  whereClauses.push("m.updated_at >= ?");
@@ -20329,12 +20687,13 @@ class SqliteMemoryRepository {
20329
20687
  whereClauses.push("m.updated_at <= ?");
20330
20688
  whereParams.push(query.updatedBefore.getTime());
20331
20689
  }
20332
- const params = [...whereParams, query.limit];
20690
+ const params = [...whereParams, limit];
20333
20691
  const statement = this.database.prepare(`
20334
20692
  SELECT
20335
20693
  m.id,
20336
20694
  m.content,
20337
20695
  m.workspace,
20696
+ m.embedding,
20338
20697
  m.created_at,
20339
20698
  m.updated_at,
20340
20699
  MAX(0, -bm25(memories_fts)) AS score
@@ -20347,7 +20706,7 @@ class SqliteMemoryRepository {
20347
20706
  const rows = statement.all(...params);
20348
20707
  const maxScore = Math.max(...rows.map((row) => row.score), 0);
20349
20708
  return rows.map((row) => ({
20350
- ...toMemoryRecord(row),
20709
+ ...toMemoryEntity(row),
20351
20710
  score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
20352
20711
  }));
20353
20712
  } catch (error2) {
@@ -20356,19 +20715,21 @@ class SqliteMemoryRepository {
20356
20715
  });
20357
20716
  }
20358
20717
  }
20359
- async findById(id) {
20718
+ async get(id) {
20360
20719
  try {
20361
- const rows = this.findByIdStatement.all(id);
20720
+ const rows = this.getStatement.all(id);
20362
20721
  const row = rows[0];
20363
- return row ? toMemoryRecord(row) : undefined;
20722
+ return row ? toMemoryEntity(row) : undefined;
20364
20723
  } catch (error2) {
20365
20724
  throw new PersistenceError("Failed to find memory.", { cause: error2 });
20366
20725
  }
20367
20726
  }
20368
- async findAll(options) {
20727
+ async list(options) {
20369
20728
  try {
20370
20729
  const whereClauses = [];
20371
20730
  const params = [];
20731
+ const offset = options.offset ?? 0;
20732
+ const limit = options.limit ?? DEFAULT_LIST_LIMIT2;
20372
20733
  if (options.workspace) {
20373
20734
  whereClauses.push("workspace = ?");
20374
20735
  params.push(options.workspace);
@@ -20376,49 +20737,49 @@ class SqliteMemoryRepository {
20376
20737
  whereClauses.push("workspace IS NULL");
20377
20738
  }
20378
20739
  const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
20379
- const limit = options.limit + 1;
20380
- params.push(limit, options.offset);
20740
+ const queryLimit = limit + 1;
20741
+ params.push(queryLimit, offset);
20381
20742
  const statement = this.database.prepare(`
20382
- SELECT id, content, workspace, created_at, updated_at
20743
+ SELECT id, content, workspace, embedding, created_at, updated_at
20383
20744
  FROM memories
20384
20745
  ${whereClause}
20385
20746
  ORDER BY created_at DESC
20386
20747
  LIMIT ? OFFSET ?
20387
20748
  `);
20388
20749
  const rows = statement.all(...params);
20389
- const hasMore = rows.length > options.limit;
20390
- const items = (hasMore ? rows.slice(0, options.limit) : rows).map(toMemoryRecord);
20750
+ const hasMore = rows.length > limit;
20751
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryEntity);
20391
20752
  return { items, hasMore };
20392
20753
  } catch (error2) {
20393
20754
  throw new PersistenceError("Failed to list memories.", { cause: error2 });
20394
20755
  }
20395
20756
  }
20396
- async update(id, content) {
20757
+ async update(input) {
20397
20758
  let result;
20398
20759
  try {
20399
20760
  const now = Date.now();
20400
- result = this.updateStatement.run(content, now, id);
20761
+ result = this.updateStatement.run(input.content, encodeEmbedding(input.embedding), now, input.id);
20401
20762
  } catch (error2) {
20402
20763
  throw new PersistenceError("Failed to update memory.", { cause: error2 });
20403
20764
  }
20404
20765
  if (result.changes === 0) {
20405
- throw new NotFoundError(`Memory not found: ${id}`);
20766
+ throw new NotFoundError(`Memory not found: ${input.id}`);
20406
20767
  }
20407
- const memory = await this.findById(id);
20768
+ const memory = await this.get(input.id);
20408
20769
  if (!memory) {
20409
- throw new NotFoundError(`Memory not found after update: ${id}`);
20770
+ throw new NotFoundError(`Memory not found after update: ${input.id}`);
20410
20771
  }
20411
20772
  return memory;
20412
20773
  }
20413
- async delete(id) {
20774
+ async delete(input) {
20414
20775
  let result;
20415
20776
  try {
20416
- result = this.deleteStatement.run(id);
20777
+ result = this.deleteStatement.run(input.id);
20417
20778
  } catch (error2) {
20418
20779
  throw new PersistenceError("Failed to delete memory.", { cause: error2 });
20419
20780
  }
20420
20781
  if (result.changes === 0) {
20421
- throw new NotFoundError(`Memory not found: ${id}`);
20782
+ throw new NotFoundError(`Memory not found: ${input.id}`);
20422
20783
  }
20423
20784
  }
20424
20785
  async listWorkspaces() {
@@ -20430,25 +20791,22 @@ class SqliteMemoryRepository {
20430
20791
  }
20431
20792
  }
20432
20793
  }
20433
- var toMemoryRecord = (row) => ({
20794
+ var toMemoryEntity = (row) => ({
20434
20795
  id: row.id,
20435
20796
  content: row.content,
20797
+ embedding: decodeEmbedding(row.embedding),
20436
20798
  workspace: row.workspace ?? undefined,
20437
20799
  createdAt: new Date(row.created_at),
20438
20800
  updatedAt: new Date(row.updated_at)
20439
20801
  });
20440
20802
  var toFtsQuery = (terms) => terms.map(toFtsTerm).join(" OR ");
20441
- var toFtsTerm = (term) => {
20803
+ function toFtsTerm(term) {
20442
20804
  const escaped = term.replaceAll('"', '""');
20443
20805
  if (term.includes(" ")) {
20444
20806
  return `"${escaped}"`;
20445
20807
  }
20446
20808
  return `"${escaped}"*`;
20447
- };
20448
-
20449
- // src/ui/server.tsx
20450
- import { randomUUID as randomUUID2 } from "node:crypto";
20451
-
20809
+ }
20452
20810
  // node_modules/@hono/node-server/dist/index.mjs
20453
20811
  import { createServer as createServerHTTP } from "http";
20454
20812
  import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
@@ -23441,182 +23799,188 @@ function jsxDEV(tag, props, key) {
23441
23799
  }
23442
23800
 
23443
23801
  // src/ui/components/create-form.tsx
23444
- var CreateForm = ({ workspace }) => /* @__PURE__ */ jsxDEV("div", {
23445
- class: "form-card",
23446
- children: /* @__PURE__ */ jsxDEV("form", {
23447
- method: "post",
23448
- action: "/memories",
23449
- children: [
23450
- /* @__PURE__ */ jsxDEV("div", {
23451
- class: "field",
23452
- children: [
23453
- /* @__PURE__ */ jsxDEV("label", {
23454
- for: "new-content",
23455
- children: "Content"
23456
- }, undefined, false, undefined, this),
23457
- /* @__PURE__ */ jsxDEV("textarea", {
23458
- id: "new-content",
23459
- name: "content",
23460
- placeholder: "Fact, preference, decision...",
23461
- required: true
23462
- }, undefined, false, undefined, this)
23463
- ]
23464
- }, undefined, true, undefined, this),
23465
- /* @__PURE__ */ jsxDEV("div", {
23466
- class: "field",
23467
- children: [
23468
- /* @__PURE__ */ jsxDEV("label", {
23469
- for: "new-workspace",
23470
- children: "Workspace (optional)"
23471
- }, undefined, false, undefined, this),
23472
- /* @__PURE__ */ jsxDEV("input", {
23473
- id: "new-workspace",
23474
- name: "workspace",
23475
- type: "text",
23476
- placeholder: "/path/to/project",
23477
- value: workspace && workspace !== NO_WORKSPACE_FILTER ? workspace : ""
23478
- }, undefined, false, undefined, this)
23479
- ]
23480
- }, undefined, true, undefined, this),
23481
- /* @__PURE__ */ jsxDEV("div", {
23482
- class: "form-actions",
23483
- children: /* @__PURE__ */ jsxDEV("button", {
23484
- type: "submit",
23485
- class: "primary",
23486
- children: "Save"
23487
- }, undefined, false, undefined, this)
23488
- }, undefined, false, undefined, this)
23489
- ]
23490
- }, undefined, true, undefined, this)
23491
- }, undefined, false, undefined, this);
23492
-
23493
- // src/ui/components/memory-card.tsx
23494
- var MemoryCard = ({ memory, editing, showWorkspace, returnUrl }) => /* @__PURE__ */ jsxDEV("div", {
23495
- class: "card",
23496
- children: [
23497
- /* @__PURE__ */ jsxDEV("div", {
23498
- class: "card-header",
23499
- children: /* @__PURE__ */ jsxDEV("div", {
23500
- class: "card-meta",
23501
- children: [
23502
- showWorkspace && memory.workspace && /* @__PURE__ */ jsxDEV(Fragment, {
23503
- children: [
23504
- /* @__PURE__ */ jsxDEV("span", {
23505
- class: "badge",
23506
- children: memory.workspace
23507
- }, undefined, false, undefined, this),
23508
- " "
23509
- ]
23510
- }, undefined, true, undefined, this),
23511
- memory.updatedAt.toLocaleString()
23512
- ]
23513
- }, undefined, true, undefined, this)
23514
- }, undefined, false, undefined, this),
23515
- editing ? /* @__PURE__ */ jsxDEV("form", {
23802
+ function CreateForm({ workspace }) {
23803
+ return /* @__PURE__ */ jsxDEV("div", {
23804
+ class: "form-card",
23805
+ children: /* @__PURE__ */ jsxDEV("form", {
23516
23806
  method: "post",
23517
- action: `/memories/${encodeURIComponent(memory.id)}/update`,
23807
+ action: "/memories",
23518
23808
  children: [
23519
- /* @__PURE__ */ jsxDEV("input", {
23520
- type: "hidden",
23521
- name: "returnUrl",
23522
- value: returnUrl
23523
- }, undefined, false, undefined, this),
23524
- /* @__PURE__ */ jsxDEV("textarea", {
23525
- name: "content",
23526
- required: true,
23527
- children: memory.content
23528
- }, undefined, false, undefined, this),
23529
23809
  /* @__PURE__ */ jsxDEV("div", {
23530
- class: "card-actions",
23810
+ class: "field",
23531
23811
  children: [
23532
- /* @__PURE__ */ jsxDEV("button", {
23533
- type: "submit",
23534
- class: "primary",
23535
- children: "Save"
23812
+ /* @__PURE__ */ jsxDEV("label", {
23813
+ for: "new-content",
23814
+ children: "Content"
23536
23815
  }, undefined, false, undefined, this),
23537
- /* @__PURE__ */ jsxDEV("a", {
23538
- href: returnUrl,
23539
- class: "btn",
23540
- children: "Cancel"
23816
+ /* @__PURE__ */ jsxDEV("textarea", {
23817
+ id: "new-content",
23818
+ name: "content",
23819
+ placeholder: "Fact, preference, decision...",
23820
+ required: true
23541
23821
  }, undefined, false, undefined, this)
23542
23822
  ]
23543
- }, undefined, true, undefined, this)
23544
- ]
23545
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
23546
- children: [
23823
+ }, undefined, true, undefined, this),
23547
23824
  /* @__PURE__ */ jsxDEV("div", {
23548
- class: "card-content",
23549
- children: memory.content
23550
- }, undefined, false, undefined, this),
23551
- /* @__PURE__ */ jsxDEV("div", {
23552
- class: "card-actions",
23825
+ class: "field",
23553
23826
  children: [
23554
- /* @__PURE__ */ jsxDEV("a", {
23555
- href: `${returnUrl}${returnUrl.includes("?") ? "&" : "?"}edit=${encodeURIComponent(memory.id)}`,
23556
- class: "btn",
23557
- children: "Edit"
23827
+ /* @__PURE__ */ jsxDEV("label", {
23828
+ for: "new-workspace",
23829
+ children: "Workspace (optional)"
23558
23830
  }, undefined, false, undefined, this),
23559
- /* @__PURE__ */ jsxDEV("form", {
23560
- method: "post",
23561
- action: `/memories/${encodeURIComponent(memory.id)}/delete`,
23562
- onsubmit: "return confirm('Delete this memory?')",
23831
+ /* @__PURE__ */ jsxDEV("input", {
23832
+ id: "new-workspace",
23833
+ name: "workspace",
23834
+ type: "text",
23835
+ placeholder: "/path/to/project",
23836
+ value: workspace && workspace !== NO_WORKSPACE_FILTER ? workspace : ""
23837
+ }, undefined, false, undefined, this)
23838
+ ]
23839
+ }, undefined, true, undefined, this),
23840
+ /* @__PURE__ */ jsxDEV("div", {
23841
+ class: "form-actions",
23842
+ children: /* @__PURE__ */ jsxDEV("button", {
23843
+ type: "submit",
23844
+ class: "primary",
23845
+ children: "Save"
23846
+ }, undefined, false, undefined, this)
23847
+ }, undefined, false, undefined, this)
23848
+ ]
23849
+ }, undefined, true, undefined, this)
23850
+ }, undefined, false, undefined, this);
23851
+ }
23852
+
23853
+ // src/ui/components/memory-card.tsx
23854
+ function MemoryCard({ memory, editing, showWorkspace, returnUrl }) {
23855
+ return /* @__PURE__ */ jsxDEV("div", {
23856
+ class: "card",
23857
+ children: [
23858
+ /* @__PURE__ */ jsxDEV("div", {
23859
+ class: "card-header",
23860
+ children: /* @__PURE__ */ jsxDEV("div", {
23861
+ class: "card-meta",
23862
+ children: [
23863
+ showWorkspace && memory.workspace && /* @__PURE__ */ jsxDEV(Fragment, {
23563
23864
  children: [
23564
- /* @__PURE__ */ jsxDEV("input", {
23565
- type: "hidden",
23566
- name: "returnUrl",
23567
- value: returnUrl
23865
+ /* @__PURE__ */ jsxDEV("span", {
23866
+ class: "badge",
23867
+ children: memory.workspace
23568
23868
  }, undefined, false, undefined, this),
23569
- /* @__PURE__ */ jsxDEV("button", {
23570
- type: "submit",
23571
- class: "danger",
23572
- children: "Delete"
23573
- }, undefined, false, undefined, this)
23869
+ " "
23574
23870
  ]
23575
- }, undefined, true, undefined, this)
23871
+ }, undefined, true, undefined, this),
23872
+ memory.updatedAt.toLocaleString()
23576
23873
  ]
23577
23874
  }, undefined, true, undefined, this)
23578
- ]
23579
- }, undefined, true, undefined, this)
23580
- ]
23581
- }, undefined, true, undefined, this);
23875
+ }, undefined, false, undefined, this),
23876
+ editing ? /* @__PURE__ */ jsxDEV("form", {
23877
+ method: "post",
23878
+ action: `/memories/${encodeURIComponent(memory.id)}/update`,
23879
+ children: [
23880
+ /* @__PURE__ */ jsxDEV("input", {
23881
+ type: "hidden",
23882
+ name: "returnUrl",
23883
+ value: returnUrl
23884
+ }, undefined, false, undefined, this),
23885
+ /* @__PURE__ */ jsxDEV("textarea", {
23886
+ name: "content",
23887
+ required: true,
23888
+ children: memory.content
23889
+ }, undefined, false, undefined, this),
23890
+ /* @__PURE__ */ jsxDEV("div", {
23891
+ class: "card-actions",
23892
+ children: [
23893
+ /* @__PURE__ */ jsxDEV("button", {
23894
+ type: "submit",
23895
+ class: "primary",
23896
+ children: "Save"
23897
+ }, undefined, false, undefined, this),
23898
+ /* @__PURE__ */ jsxDEV("a", {
23899
+ href: returnUrl,
23900
+ class: "btn",
23901
+ children: "Cancel"
23902
+ }, undefined, false, undefined, this)
23903
+ ]
23904
+ }, undefined, true, undefined, this)
23905
+ ]
23906
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
23907
+ children: [
23908
+ /* @__PURE__ */ jsxDEV("div", {
23909
+ class: "card-content",
23910
+ children: memory.content
23911
+ }, undefined, false, undefined, this),
23912
+ /* @__PURE__ */ jsxDEV("div", {
23913
+ class: "card-actions",
23914
+ children: [
23915
+ /* @__PURE__ */ jsxDEV("a", {
23916
+ href: `${returnUrl}${returnUrl.includes("?") ? "&" : "?"}edit=${encodeURIComponent(memory.id)}`,
23917
+ class: "btn",
23918
+ children: "Edit"
23919
+ }, undefined, false, undefined, this),
23920
+ /* @__PURE__ */ jsxDEV("form", {
23921
+ method: "post",
23922
+ action: `/memories/${encodeURIComponent(memory.id)}/delete`,
23923
+ onsubmit: "return confirm('Delete this memory?')",
23924
+ children: [
23925
+ /* @__PURE__ */ jsxDEV("input", {
23926
+ type: "hidden",
23927
+ name: "returnUrl",
23928
+ value: returnUrl
23929
+ }, undefined, false, undefined, this),
23930
+ /* @__PURE__ */ jsxDEV("button", {
23931
+ type: "submit",
23932
+ class: "danger",
23933
+ children: "Delete"
23934
+ }, undefined, false, undefined, this)
23935
+ ]
23936
+ }, undefined, true, undefined, this)
23937
+ ]
23938
+ }, undefined, true, undefined, this)
23939
+ ]
23940
+ }, undefined, true, undefined, this)
23941
+ ]
23942
+ }, undefined, true, undefined, this);
23943
+ }
23582
23944
 
23583
23945
  // src/ui/components/sidebar.tsx
23584
- var Sidebar = ({ workspaces, selected }) => /* @__PURE__ */ jsxDEV("nav", {
23585
- class: "sidebar",
23586
- children: [
23587
- /* @__PURE__ */ jsxDEV("h2", {
23588
- children: "Workspaces"
23589
- }, undefined, false, undefined, this),
23590
- /* @__PURE__ */ jsxDEV("a", {
23591
- href: "/",
23592
- class: `sidebar-item all-item${selected === null ? " active" : ""}`,
23593
- children: "All"
23594
- }, undefined, false, undefined, this),
23595
- /* @__PURE__ */ jsxDEV("a", {
23596
- href: `/?workspace=${NO_WORKSPACE_FILTER}`,
23597
- class: `sidebar-item no-ws-item${selected === NO_WORKSPACE_FILTER ? " active" : ""}`,
23598
- children: "No workspace"
23599
- }, undefined, false, undefined, this),
23600
- workspaces.map((ws) => /* @__PURE__ */ jsxDEV("a", {
23601
- href: `/?workspace=${encodeURIComponent(ws)}`,
23602
- class: `sidebar-item ws-item${selected === ws ? " active" : ""}`,
23603
- title: ws,
23604
- children: /* @__PURE__ */ jsxDEV("span", {
23605
- children: ws.replace(/\/$/, "")
23606
- }, undefined, false, undefined, this)
23607
- }, undefined, false, undefined, this))
23608
- ]
23609
- }, undefined, true, undefined, this);
23946
+ function Sidebar({ workspaces, selected }) {
23947
+ return /* @__PURE__ */ jsxDEV("nav", {
23948
+ class: "sidebar",
23949
+ children: [
23950
+ /* @__PURE__ */ jsxDEV("h2", {
23951
+ children: "Workspaces"
23952
+ }, undefined, false, undefined, this),
23953
+ /* @__PURE__ */ jsxDEV("a", {
23954
+ href: "/",
23955
+ class: `sidebar-item all-item${selected === null ? " active" : ""}`,
23956
+ children: "All"
23957
+ }, undefined, false, undefined, this),
23958
+ /* @__PURE__ */ jsxDEV("a", {
23959
+ href: `/?workspace=${NO_WORKSPACE_FILTER}`,
23960
+ class: `sidebar-item no-ws-item${selected === NO_WORKSPACE_FILTER ? " active" : ""}`,
23961
+ children: "No workspace"
23962
+ }, undefined, false, undefined, this),
23963
+ workspaces.map((ws) => /* @__PURE__ */ jsxDEV("a", {
23964
+ href: `/?workspace=${encodeURIComponent(ws)}`,
23965
+ class: `sidebar-item ws-item${selected === ws ? " active" : ""}`,
23966
+ title: ws,
23967
+ children: /* @__PURE__ */ jsxDEV("span", {
23968
+ children: ws.replace(/\/$/, "")
23969
+ }, undefined, false, undefined, this)
23970
+ }, undefined, false, undefined, this))
23971
+ ]
23972
+ }, undefined, true, undefined, this);
23973
+ }
23610
23974
 
23611
23975
  // src/ui/components/page.tsx
23612
- var buildUrl = (base, overrides) => {
23976
+ function buildUrl(base, overrides) {
23613
23977
  const url = new URL(base, "http://localhost");
23614
23978
  for (const [key, value] of Object.entries(overrides)) {
23615
23979
  url.searchParams.set(key, value);
23616
23980
  }
23617
23981
  return `${url.pathname}${url.search}`;
23618
- };
23619
- var Page = ({
23982
+ }
23983
+ function Page({
23620
23984
  memories,
23621
23985
  workspaces,
23622
23986
  selectedWorkspace,
@@ -23624,7 +23988,7 @@ var Page = ({
23624
23988
  currentPage,
23625
23989
  hasMore,
23626
23990
  showCreate
23627
- }) => {
23991
+ }) {
23628
23992
  const params = new URLSearchParams;
23629
23993
  if (selectedWorkspace)
23630
23994
  params.set("workspace", selectedWorkspace);
@@ -23743,27 +24107,26 @@ var Page = ({
23743
24107
  }, undefined, false, undefined, this)
23744
24108
  ]
23745
24109
  }, undefined, true, undefined, this);
23746
- };
24110
+ }
23747
24111
 
23748
- // src/ui/server.tsx
23749
- var DEFAULT_LIST_LIMIT = 15;
23750
- var MAX_LIST_LIMIT = 100;
23751
- var startWebServer = (repository, options) => {
24112
+ // src/ui/routes/page-routes.tsx
24113
+ var DEFAULT_LIST_LIMIT3 = 15;
24114
+ function createPageRoutes(memory) {
23752
24115
  const app = new Hono2;
23753
- app.get("/", async (c) => {
24116
+ async function renderPage(c) {
23754
24117
  const workspace = c.req.query("workspace") ?? null;
23755
24118
  const pageNum = Math.max(Number(c.req.query("page")) || 1, 1);
23756
24119
  const editingId = c.req.query("edit") ?? null;
23757
24120
  const showCreate = c.req.query("create") === "1";
23758
24121
  const isNoWorkspace = workspace === NO_WORKSPACE_FILTER;
23759
24122
  const wsFilter = workspace && !isNoWorkspace ? workspace : undefined;
23760
- const page = await repository.findAll({
24123
+ const page = await memory.list({
23761
24124
  workspace: wsFilter,
23762
24125
  workspaceIsNull: isNoWorkspace,
23763
- offset: (pageNum - 1) * DEFAULT_LIST_LIMIT,
23764
- limit: DEFAULT_LIST_LIMIT
24126
+ offset: (pageNum - 1) * DEFAULT_LIST_LIMIT3,
24127
+ limit: DEFAULT_LIST_LIMIT3
23765
24128
  });
23766
- const workspaces = await repository.listWorkspaces();
24129
+ const workspaces = await memory.listWorkspaces();
23767
24130
  return c.html(/* @__PURE__ */ jsxDEV(Page, {
23768
24131
  memories: page.items,
23769
24132
  workspaces,
@@ -23773,132 +24136,84 @@ var startWebServer = (repository, options) => {
23773
24136
  hasMore: page.hasMore,
23774
24137
  showCreate
23775
24138
  }, undefined, false, undefined, this));
23776
- });
23777
- app.post("/memories", async (c) => {
24139
+ }
24140
+ async function createMemory(c) {
23778
24141
  const form2 = await c.req.parseBody();
23779
- const content = typeof form2.content === "string" ? form2.content.trim() : "";
23780
- const workspace = typeof form2.workspace === "string" ? form2.workspace.trim() || undefined : undefined;
23781
- if (content) {
23782
- const now = new Date;
23783
- await repository.save({ id: randomUUID2(), content, workspace, createdAt: now, updatedAt: now });
24142
+ const content = typeof form2.content === "string" ? form2.content : "";
24143
+ const workspace = typeof form2.workspace === "string" ? form2.workspace : undefined;
24144
+ try {
24145
+ await memory.create({ content, workspace });
24146
+ } catch (error2) {
24147
+ if (!(error2 instanceof ValidationError)) {
24148
+ throw error2;
24149
+ }
23784
24150
  }
23785
- const wsParam = workspace ? `/?workspace=${encodeURIComponent(workspace)}` : "/";
24151
+ const wsParam = workspace?.trim() ? `/?workspace=${encodeURIComponent(workspace.trim())}` : "/";
23786
24152
  return c.redirect(wsParam);
23787
- });
23788
- app.post("/memories/:id/update", async (c) => {
24153
+ }
24154
+ async function updateMemory(c) {
23789
24155
  const form2 = await c.req.parseBody();
23790
- const content = typeof form2.content === "string" ? form2.content.trim() : "";
24156
+ const content = typeof form2.content === "string" ? form2.content : "";
23791
24157
  const returnUrl = safeReturnUrl(form2.returnUrl);
23792
- if (content) {
23793
- try {
23794
- await repository.update(c.req.param("id"), content);
23795
- } catch (error2) {
23796
- if (!(error2 instanceof NotFoundError))
23797
- throw error2;
23798
- }
24158
+ const id = c.req.param("id") ?? "";
24159
+ try {
24160
+ await memory.update({ id, content });
24161
+ } catch (error2) {
24162
+ if (!(error2 instanceof NotFoundError) && !(error2 instanceof ValidationError))
24163
+ throw error2;
23799
24164
  }
23800
24165
  return c.redirect(returnUrl);
23801
- });
23802
- app.post("/memories/:id/delete", async (c) => {
24166
+ }
24167
+ async function deleteMemory(c) {
23803
24168
  const form2 = await c.req.parseBody();
23804
24169
  const returnUrl = safeReturnUrl(form2.returnUrl);
24170
+ const id = c.req.param("id") ?? "";
23805
24171
  try {
23806
- await repository.delete(c.req.param("id"));
24172
+ await memory.delete({ id });
23807
24173
  } catch (error2) {
23808
24174
  if (!(error2 instanceof NotFoundError))
23809
24175
  throw error2;
23810
24176
  }
23811
24177
  return c.redirect(returnUrl);
23812
- });
23813
- app.get("/api/workspaces", async (c) => {
23814
- const workspaces = await repository.listWorkspaces();
23815
- return c.json({ workspaces });
23816
- });
23817
- app.get("/api/memories", async (c) => {
23818
- const workspace = c.req.query("workspace");
23819
- const limitParam = c.req.query("limit");
23820
- const limit = Math.min(Math.max(Number(limitParam) || DEFAULT_LIST_LIMIT, 1), MAX_LIST_LIMIT);
23821
- const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
23822
- const page = await repository.findAll({ workspace, offset, limit });
23823
- return c.json({ items: page.items.map(toMemoryJson), hasMore: page.hasMore });
23824
- });
23825
- app.get("/api/memories/:id", async (c) => {
23826
- const memory = await repository.findById(c.req.param("id"));
23827
- if (!memory)
23828
- return c.json({ error: "Memory not found." }, 404);
23829
- return c.json(toMemoryJson(memory));
23830
- });
23831
- app.post("/api/memories", async (c) => {
23832
- const body = await c.req.json().catch(() => null);
23833
- if (!body)
23834
- return c.json({ error: "Invalid JSON." }, 400);
23835
- const content = typeof body.content === "string" ? body.content.trim() : "";
23836
- if (!content)
23837
- return c.json({ error: "Content is required." }, 400);
23838
- const workspace = typeof body.workspace === "string" ? body.workspace.trim() || undefined : undefined;
23839
- const now = new Date;
23840
- const memory = await repository.save({ id: randomUUID2(), content, workspace, createdAt: now, updatedAt: now });
23841
- return c.json(toMemoryJson(memory), 201);
23842
- });
23843
- app.patch("/api/memories/:id", async (c) => {
23844
- const body = await c.req.json().catch(() => null);
23845
- if (!body)
23846
- return c.json({ error: "Invalid JSON." }, 400);
23847
- const content = typeof body.content === "string" ? body.content.trim() : "";
23848
- if (!content)
23849
- return c.json({ error: "Content is required." }, 400);
23850
- try {
23851
- const updated = await repository.update(c.req.param("id"), content);
23852
- return c.json(toMemoryJson(updated));
23853
- } catch (error2) {
23854
- if (error2 instanceof NotFoundError)
23855
- return c.json({ error: "Memory not found." }, 404);
23856
- throw error2;
23857
- }
23858
- });
23859
- app.delete("/api/memories/:id", async (c) => {
23860
- try {
23861
- await repository.delete(c.req.param("id"));
23862
- return c.body(null, 204);
23863
- } catch (error2) {
23864
- if (error2 instanceof NotFoundError)
23865
- return c.json({ error: "Memory not found." }, 404);
23866
- throw error2;
23867
- }
23868
- });
23869
- return serve({ fetch: app.fetch, port: options.port });
23870
- };
23871
- var safeReturnUrl = (value) => {
24178
+ }
24179
+ app.get("/", renderPage);
24180
+ app.post("/memories", createMemory);
24181
+ app.post("/memories/:id/update", updateMemory);
24182
+ app.post("/memories/:id/delete", deleteMemory);
24183
+ return app;
24184
+ }
24185
+ function safeReturnUrl(value) {
23872
24186
  if (typeof value === "string" && value.startsWith("/"))
23873
24187
  return value;
23874
24188
  return "/";
23875
- };
23876
- var toMemoryJson = (memory) => ({
23877
- id: memory.id,
23878
- content: memory.content,
23879
- workspace: memory.workspace ?? null,
23880
- created_at: memory.createdAt.toISOString(),
23881
- updated_at: memory.updatedAt.toISOString()
23882
- });
24189
+ }
24190
+
24191
+ // src/ui/server.tsx
24192
+ function startWebServer(memory, options) {
24193
+ const app = new Hono2;
24194
+ app.route("/", createPageRoutes(memory));
24195
+ return serve({ fetch: app.fetch, port: options.port });
24196
+ }
23883
24197
 
23884
24198
  // src/index.ts
23885
24199
  var config2 = resolveConfig();
23886
- var database = openMemoryDatabase(config2.databasePath);
23887
- var repository = new SqliteMemoryRepository(database);
24200
+ var embeddingService = new EmbeddingService({ modelsCachePath: config2.modelsCachePath });
24201
+ var database = await openMemoryDatabase(config2.databasePath, { embeddingService });
24202
+ var repository2 = new SqliteMemoryRepository(database);
24203
+ var memoryService = new MemoryService(repository2, embeddingService);
23888
24204
  if (config2.uiMode) {
23889
- const server = startWebServer(repository, { port: config2.uiPort });
23890
- const addr = server.address();
23891
- const port = typeof addr === "object" && addr ? addr.port : config2.uiPort;
23892
- console.log(`agent-memory UI running at http://localhost:${port}`);
23893
- const shutdown = () => {
24205
+ let shutdown = function() {
23894
24206
  server.close();
23895
24207
  database.close();
23896
24208
  process.exit(0);
23897
24209
  };
24210
+ const server = startWebServer(memoryService, { port: config2.uiPort });
24211
+ const addr = server.address();
24212
+ const port = typeof addr === "object" && addr ? addr.port : config2.uiPort;
24213
+ console.log(`agent-memory UI running at http://localhost:${port}`);
23898
24214
  process.on("SIGINT", shutdown);
23899
24215
  process.on("SIGTERM", shutdown);
23900
24216
  } else {
23901
- const memoryService = new MemoryService(repository);
23902
24217
  const server = createMcpServer(memoryService, version2);
23903
24218
  const transport = new StdioServerTransport;
23904
24219
  try {