@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.
- package/README.md +17 -58
- package/dist/index.js +228 -550
- 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
|
-
- `
|
|
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 `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
`
|
|
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. `
|
|
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
|
|
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 ${
|
|
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
|
|
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 =
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 `
|
|
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 `
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
-
"
|
|
20394
|
-
"
|
|
20395
|
-
"
|
|
20396
|
-
"
|
|
20397
|
-
"Pass workspace for project-scoped memory. Omit it only for facts that apply across projects.",
|
|
20398
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
20701
|
-
this.updateStatement = database.prepare("UPDATE memories SET content = ?,
|
|
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,
|
|
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 ?
|
|
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.
|
|
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,
|
|
20476
|
+
SELECT id, content, workspace, created_at, updated_at
|
|
20789
20477
|
FROM memories
|
|
20790
20478
|
${whereClause}
|
|
20791
|
-
ORDER BY
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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, {
|
|
23971
|
+
var database = await openMemoryDatabase(config2.databasePath, { workspaceResolver });
|
|
24294
23972
|
var repository2 = new SqliteMemoryRepository(database);
|
|
24295
|
-
var memoryService = new MemoryService(repository2,
|
|
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
|
|
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 --
|
|
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",
|