@jcyamacho/agent-memory 0.0.20 → 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 +18 -62
- package/dist/index.js +187 -555
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -6,11 +6,10 @@ Persistent memory for MCP-powered coding agents.
|
|
|
6
6
|
by SQLite. It helps your agent remember preferences, project context, and prior
|
|
7
7
|
decisions across sessions.
|
|
8
8
|
|
|
9
|
-
It exposes
|
|
9
|
+
It exposes four tools:
|
|
10
10
|
|
|
11
11
|
- `remember` -> save facts, decisions, preferences, and project context
|
|
12
|
-
- `
|
|
13
|
-
- `review` -> browse all memories for a workspace
|
|
12
|
+
- `review` -> load workspace and global memories sorted by most recently updated
|
|
14
13
|
- `revise` -> update an existing memory when it becomes outdated
|
|
15
14
|
- `forget` -> delete a memory that is no longer relevant
|
|
16
15
|
|
|
@@ -44,24 +43,20 @@ OpenCode:
|
|
|
44
43
|
|
|
45
44
|
## Optional LLM Instructions
|
|
46
45
|
|
|
47
|
-
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:
|
|
48
49
|
|
|
49
50
|
```md
|
|
50
51
|
## Agent Memory
|
|
51
52
|
|
|
52
|
-
- Use `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
apply across projects.
|
|
60
|
-
- Use `memory_remember` to save one durable fact when the user states a stable
|
|
61
|
-
preference, correction, or reusable project decision.
|
|
62
|
-
- If the fact already exists, use `memory_revise` instead of creating a duplicate.
|
|
63
|
-
- Use `memory_forget` to remove a wrong or obsolete memory.
|
|
64
|
-
- 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.
|
|
65
60
|
```
|
|
66
61
|
|
|
67
62
|
## Web UI
|
|
@@ -80,34 +75,15 @@ npx -y @jcyamacho/agent-memory@latest --ui --port 9090
|
|
|
80
75
|
|
|
81
76
|
The web UI uses the same database as the MCP server.
|
|
82
77
|
|
|
83
|
-
## How
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
memories
|
|
87
|
-
|
|
88
|
-
1. **Text relevance** is the primary signal -- memories whose content best
|
|
89
|
-
matches your search terms rank highest.
|
|
90
|
-
2. **Workspace match** is the next strongest signal. When you pass
|
|
91
|
-
`workspace`, exact matches rank highest and all other scoped workspaces rank
|
|
92
|
-
below exact matches.
|
|
93
|
-
3. **Embedding similarity** is a secondary signal. Recall builds an embedding
|
|
94
|
-
from your normalized search terms and boosts memories whose stored
|
|
95
|
-
embeddings are most semantically similar.
|
|
96
|
-
4. **Global memories** (saved without a workspace) are treated as relevant
|
|
97
|
-
everywhere. When you pass `workspace`, they rank below exact workspace
|
|
98
|
-
matches and above memories from other workspaces.
|
|
99
|
-
5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
|
|
100
|
-
ones when other signals are equal.
|
|
101
|
-
|
|
102
|
-
If you omit `workspace`, recall still uses text relevance, embedding similarity,
|
|
103
|
-
and recency. For best results, pass `workspace` whenever you have one. Save
|
|
104
|
-
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.
|
|
105
83
|
|
|
106
84
|
When you save a memory from a git worktree, `agent-memory` stores the main repo
|
|
107
|
-
root as the workspace. `
|
|
85
|
+
root as the workspace. `review` applies the same normalization to incoming
|
|
108
86
|
workspace queries so linked worktrees still match repo-scoped memories exactly.
|
|
109
|
-
When that happens, recall returns the queried workspace value so callers can
|
|
110
|
-
treat the match as belonging to their current worktree context.
|
|
111
87
|
|
|
112
88
|
## Configuration
|
|
113
89
|
|
|
@@ -131,26 +107,6 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
|
|
|
131
107
|
- share a memory DB across multiple clients
|
|
132
108
|
- store the DB somewhere easier to back up or inspect
|
|
133
109
|
|
|
134
|
-
### Model Cache Location
|
|
135
|
-
|
|
136
|
-
By default, downloaded embedding model files are cached at:
|
|
137
|
-
|
|
138
|
-
```text
|
|
139
|
-
~/.config/agent-memory/models
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Override it with:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
AGENT_MEMORY_MODELS_CACHE_PATH=/absolute/path/to/models
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
|
|
149
|
-
|
|
150
|
-
- keep model artifacts out of `node_modules`
|
|
151
|
-
- share the model cache across reinstalls or multiple clients
|
|
152
|
-
- store model downloads somewhere easier to inspect or manage
|
|
153
|
-
|
|
154
110
|
Schema changes are migrated automatically, including workspace normalization for
|
|
155
111
|
existing git worktree memories when the original path can still be resolved.
|
|
156
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
12493
|
|
|
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";
|
|
12536
|
-
function configureModelsCache(modelsCachePath) {
|
|
12537
|
-
mkdirSync(modelsCachePath, { recursive: true });
|
|
12538
|
-
transformersEnv.useFSCache = true;
|
|
12539
|
-
transformersEnv.cacheDir = modelsCachePath;
|
|
12540
|
-
}
|
|
12541
|
-
|
|
12542
|
-
class EmbeddingService {
|
|
12543
|
-
options;
|
|
12544
|
-
extractorPromise;
|
|
12545
|
-
constructor(options = {}) {
|
|
12546
|
-
this.options = options;
|
|
12547
|
-
}
|
|
12548
|
-
async warmup() {
|
|
12549
|
-
await this.getExtractor();
|
|
12550
|
-
}
|
|
12551
|
-
async createVector(text) {
|
|
12552
|
-
const normalizedText = text.trim();
|
|
12553
|
-
if (!normalizedText) {
|
|
12554
|
-
throw new ValidationError("Text is required.");
|
|
12555
|
-
}
|
|
12556
|
-
const extractor = await this.getExtractor();
|
|
12557
|
-
const embedding = await extractor(normalizedText);
|
|
12558
|
-
return normalizeVector(embedding.tolist());
|
|
12559
|
-
}
|
|
12560
|
-
getExtractor() {
|
|
12561
|
-
if (!this.extractorPromise) {
|
|
12562
|
-
this.extractorPromise = (this.options.createExtractor ?? createDefaultExtractor)();
|
|
12563
|
-
}
|
|
12564
|
-
return this.extractorPromise;
|
|
12565
|
-
}
|
|
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,245 +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 queryWorkspace = normalizeOptionalString(input.workspace);
|
|
20189
|
-
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20190
|
-
const page = await this.repository.list({
|
|
20191
|
-
workspace,
|
|
20192
|
-
workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
|
|
20193
|
-
offset: normalizeOffset(input.offset),
|
|
20194
|
-
limit: normalizeListLimit(input.limit)
|
|
20195
|
-
});
|
|
20196
|
-
return {
|
|
20197
|
-
items: page.items.map((item) => toPublicMemoryRecord(remapWorkspace(item, workspace, queryWorkspace))),
|
|
20198
|
-
hasMore: page.hasMore
|
|
20199
|
-
};
|
|
20200
|
-
}
|
|
20201
|
-
async listWorkspaces() {
|
|
20202
|
-
return this.repository.listWorkspaces();
|
|
20203
|
-
}
|
|
20204
|
-
async search(input) {
|
|
20205
|
-
const terms = normalizeTerms(input.terms);
|
|
20206
|
-
if (terms.length === 0) {
|
|
20207
|
-
throw new ValidationError("At least one search term is required.");
|
|
20208
|
-
}
|
|
20209
|
-
const requestedLimit = normalizeLimit(input.limit);
|
|
20210
|
-
const queryWorkspace = normalizeOptionalString(input.workspace);
|
|
20211
|
-
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20212
|
-
const normalizedQuery = {
|
|
20213
|
-
terms,
|
|
20214
|
-
limit: requestedLimit * RECALL_CANDIDATE_LIMIT_MULTIPLIER,
|
|
20215
|
-
updatedAfter: input.updatedAfter,
|
|
20216
|
-
updatedBefore: input.updatedBefore
|
|
20217
|
-
};
|
|
20218
|
-
const [results, queryEmbedding] = await Promise.all([
|
|
20219
|
-
this.repository.search(normalizedQuery),
|
|
20220
|
-
this.embeddingService.createVector(terms.join(" "))
|
|
20221
|
-
]);
|
|
20222
|
-
return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map((result) => toPublicSearchResult(remapWorkspace(result, workspace, queryWorkspace)));
|
|
20223
|
-
}
|
|
20224
|
-
}
|
|
20225
|
-
function toPublicMemoryRecord(memory) {
|
|
20226
|
-
return {
|
|
20227
|
-
id: memory.id,
|
|
20228
|
-
content: memory.content,
|
|
20229
|
-
workspace: memory.workspace,
|
|
20230
|
-
createdAt: memory.createdAt,
|
|
20231
|
-
updatedAt: memory.updatedAt
|
|
20232
|
-
};
|
|
20233
|
-
}
|
|
20234
|
-
function toPublicSearchResult(result) {
|
|
20235
|
-
return {
|
|
20236
|
-
id: result.id,
|
|
20237
|
-
content: result.content,
|
|
20238
|
-
score: result.score,
|
|
20239
|
-
workspace: result.workspace,
|
|
20240
|
-
createdAt: result.createdAt,
|
|
20241
|
-
updatedAt: result.updatedAt
|
|
20242
|
-
};
|
|
20243
|
-
}
|
|
20244
|
-
function normalizeLimit(value) {
|
|
20245
|
-
if (value === undefined) {
|
|
20246
|
-
return DEFAULT_RECALL_LIMIT;
|
|
20247
|
-
}
|
|
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}.`);
|
|
20250
|
-
}
|
|
20251
|
-
return value;
|
|
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
|
-
}
|
|
20262
|
-
function normalizeTerms(terms) {
|
|
20263
|
-
const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
|
|
20264
|
-
return [...new Set(normalizedTerms)];
|
|
20265
|
-
}
|
|
20266
|
-
function normalizeOptionalString(value) {
|
|
20267
|
-
const trimmed = value?.trim();
|
|
20268
|
-
return trimmed ? trimmed : undefined;
|
|
20269
|
-
}
|
|
20270
|
-
function remapWorkspace(record3, canonicalWorkspace, queryWorkspace) {
|
|
20271
|
-
if (record3.workspace !== canonicalWorkspace) {
|
|
20272
|
-
return record3;
|
|
20273
|
-
}
|
|
20274
|
-
return { ...record3, workspace: queryWorkspace };
|
|
20275
|
-
}
|
|
20276
|
-
|
|
20277
|
-
// src/mcp/tools/recall.ts
|
|
20278
|
-
var recallInputSchema = {
|
|
20279
|
-
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."),
|
|
20280
|
-
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."),
|
|
20281
|
-
workspace: string2().optional().describe("Current working directory for project-scoped recall. Omit for cross-project recall."),
|
|
20282
|
-
updated_after: string2().optional().describe("Only return memories updated on or after this ISO 8601 timestamp."),
|
|
20283
|
-
updated_before: string2().optional().describe("Only return memories updated on or before this ISO 8601 timestamp.")
|
|
20284
|
-
};
|
|
20285
|
-
function toMemoryXml(r) {
|
|
20286
|
-
const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
|
|
20287
|
-
const content = escapeXml(r.content);
|
|
20288
|
-
const score = Number(r.score.toFixed(3)).toString();
|
|
20289
|
-
return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
|
|
20290
|
-
${content}
|
|
20291
|
-
</memory>`;
|
|
20292
|
-
}
|
|
20293
|
-
function registerRecallTool(server, memory) {
|
|
20294
|
-
server.registerTool("recall", {
|
|
20295
|
-
annotations: {
|
|
20296
|
-
title: "Recall",
|
|
20297
|
-
readOnlyHint: true,
|
|
20298
|
-
openWorldHint: false
|
|
20299
|
-
},
|
|
20300
|
-
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.",
|
|
20301
|
-
inputSchema: recallInputSchema
|
|
20302
|
-
}, async ({ terms, limit, workspace, updated_after, updated_before }) => {
|
|
20303
|
-
try {
|
|
20304
|
-
const results = await memory.search({
|
|
20305
|
-
terms,
|
|
20306
|
-
limit,
|
|
20307
|
-
workspace,
|
|
20308
|
-
updatedAfter: parseOptionalDate(updated_after, "updated_after"),
|
|
20309
|
-
updatedBefore: parseOptionalDate(updated_before, "updated_before")
|
|
20310
|
-
});
|
|
20311
|
-
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>
|
|
20312
|
-
${results.map(toMemoryXml).join(`
|
|
20313
|
-
`)}
|
|
20314
|
-
</memories>`;
|
|
20315
|
-
return {
|
|
20316
|
-
content: [{ type: "text", text }]
|
|
20317
|
-
};
|
|
20318
|
-
} catch (error2) {
|
|
20319
|
-
throw toMcpError(error2);
|
|
20320
|
-
}
|
|
20321
|
-
});
|
|
20322
|
-
}
|
|
20323
|
-
|
|
20324
19974
|
// src/mcp/tools/remember.ts
|
|
20325
19975
|
var rememberInputSchema = {
|
|
20326
19976
|
content: string2().describe("One new durable fact to save. Use a self-contained sentence or short note."),
|
|
@@ -20334,7 +19984,7 @@ function registerRememberTool(server, memory) {
|
|
|
20334
19984
|
idempotentHint: false,
|
|
20335
19985
|
openWorldHint: false
|
|
20336
19986
|
},
|
|
20337
|
-
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="..." />`.',
|
|
20338
19988
|
inputSchema: rememberInputSchema
|
|
20339
19989
|
}, async ({ content, workspace }) => {
|
|
20340
19990
|
try {
|
|
@@ -20352,14 +20002,15 @@ function registerRememberTool(server, memory) {
|
|
|
20352
20002
|
}
|
|
20353
20003
|
|
|
20354
20004
|
// src/mcp/tools/review.ts
|
|
20355
|
-
var REVIEW_PAGE_SIZE =
|
|
20005
|
+
var REVIEW_PAGE_SIZE = 50;
|
|
20356
20006
|
var reviewInputSchema = {
|
|
20357
20007
|
workspace: string2().describe("Current working directory for project-scoped listing."),
|
|
20358
20008
|
page: number2().int().min(0).optional().describe("Zero-based page number. Defaults to 0.")
|
|
20359
20009
|
};
|
|
20360
|
-
function
|
|
20010
|
+
function toMemoryXml(record3, workspace) {
|
|
20011
|
+
const global2 = record3.workspace !== workspace ? ' global="true"' : "";
|
|
20361
20012
|
const content = escapeXml(record3.content);
|
|
20362
|
-
return `<memory id="${record3.id}" updated_at="${record3.updatedAt.toISOString()}">
|
|
20013
|
+
return `<memory id="${record3.id}"${global2} updated_at="${record3.updatedAt.toISOString()}">
|
|
20363
20014
|
${content}
|
|
20364
20015
|
</memory>`;
|
|
20365
20016
|
}
|
|
@@ -20370,13 +20021,14 @@ function registerReviewTool(server, memory) {
|
|
|
20370
20021
|
readOnlyHint: true,
|
|
20371
20022
|
openWorldHint: false
|
|
20372
20023
|
},
|
|
20373
|
-
description: '
|
|
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"`.',
|
|
20374
20025
|
inputSchema: reviewInputSchema
|
|
20375
20026
|
}, async ({ workspace, page }) => {
|
|
20376
20027
|
try {
|
|
20377
20028
|
const pageIndex = page ?? 0;
|
|
20378
20029
|
const result = await memory.list({
|
|
20379
20030
|
workspace,
|
|
20031
|
+
global: true,
|
|
20380
20032
|
offset: pageIndex * REVIEW_PAGE_SIZE,
|
|
20381
20033
|
limit: REVIEW_PAGE_SIZE
|
|
20382
20034
|
});
|
|
@@ -20385,9 +20037,11 @@ function registerReviewTool(server, memory) {
|
|
|
20385
20037
|
content: [{ type: "text", text: "No memories found for this workspace." }]
|
|
20386
20038
|
};
|
|
20387
20039
|
}
|
|
20388
|
-
const
|
|
20389
|
-
|
|
20390
|
-
`)
|
|
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}
|
|
20391
20045
|
</memories>`;
|
|
20392
20046
|
return {
|
|
20393
20047
|
content: [{ type: "text", text }]
|
|
@@ -20400,7 +20054,7 @@ ${result.items.map(toMemoryXml2).join(`
|
|
|
20400
20054
|
|
|
20401
20055
|
// src/mcp/tools/revise.ts
|
|
20402
20056
|
var reviseInputSchema = {
|
|
20403
|
-
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`."),
|
|
20404
20058
|
content: string2().describe("Corrected replacement text for that memory.")
|
|
20405
20059
|
};
|
|
20406
20060
|
function registerReviseTool(server, memory) {
|
|
@@ -20411,7 +20065,7 @@ function registerReviseTool(server, memory) {
|
|
|
20411
20065
|
idempotentHint: false,
|
|
20412
20066
|
openWorldHint: false
|
|
20413
20067
|
},
|
|
20414
|
-
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="..." />`.',
|
|
20415
20069
|
inputSchema: reviseInputSchema
|
|
20416
20070
|
}, async ({ id, content }) => {
|
|
20417
20071
|
try {
|
|
@@ -20432,13 +20086,12 @@ function registerReviseTool(server, memory) {
|
|
|
20432
20086
|
|
|
20433
20087
|
// src/mcp/server.ts
|
|
20434
20088
|
var SERVER_INSTRUCTIONS = [
|
|
20435
|
-
"
|
|
20436
|
-
"
|
|
20437
|
-
"
|
|
20438
|
-
"
|
|
20439
|
-
"
|
|
20440
|
-
"
|
|
20441
|
-
"Do not store secrets, temporary task state, or facts obvious from current code or git history."
|
|
20089
|
+
"Durable memory for stable preferences, corrections, reusable decisions, and project context not obvious from code or git history.",
|
|
20090
|
+
"Workflow: (1) Call `review` with the current workspace at conversation start -- this loads workspace and global memories into context.",
|
|
20091
|
+
"(2) During the session, call `remember` to save a new fact, `revise` to correct an existing one, or `forget` to remove one that is wrong or obsolete.",
|
|
20092
|
+
"Always check loaded memories before calling `remember` to avoid duplicates -- use `revise` instead when the fact already exists.",
|
|
20093
|
+
"Pass workspace on `remember` for project-scoped memory. Omit it only for facts that apply across all projects.",
|
|
20094
|
+
"Never store secrets, temporary task state, or facts obvious from current code or git history."
|
|
20442
20095
|
].join(" ");
|
|
20443
20096
|
function createMcpServer(memory, version3) {
|
|
20444
20097
|
const server = new McpServer({
|
|
@@ -20448,27 +20101,96 @@ function createMcpServer(memory, version3) {
|
|
|
20448
20101
|
instructions: SERVER_INSTRUCTIONS
|
|
20449
20102
|
});
|
|
20450
20103
|
registerRememberTool(server, memory);
|
|
20451
|
-
registerRecallTool(server, memory);
|
|
20452
20104
|
registerReviseTool(server, memory);
|
|
20453
20105
|
registerForgetTool(server, memory);
|
|
20454
20106
|
registerReviewTool(server, memory);
|
|
20455
20107
|
return server;
|
|
20456
20108
|
}
|
|
20457
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
|
+
|
|
20458
20182
|
// src/sqlite/db.ts
|
|
20459
|
-
import { mkdirSync
|
|
20183
|
+
import { mkdirSync } from "node:fs";
|
|
20460
20184
|
import { dirname } from "node:path";
|
|
20461
20185
|
import Database from "better-sqlite3";
|
|
20462
20186
|
|
|
20463
20187
|
// src/sqlite/memory-schema.ts
|
|
20464
|
-
function createMemoriesTable(database
|
|
20465
|
-
const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
|
|
20188
|
+
function createMemoriesTable(database) {
|
|
20466
20189
|
database.exec(`
|
|
20467
20190
|
CREATE TABLE IF NOT EXISTS memories (
|
|
20468
20191
|
id TEXT PRIMARY KEY,
|
|
20469
20192
|
content TEXT NOT NULL,
|
|
20470
20193
|
workspace TEXT,
|
|
20471
|
-
${embeddingColumn}
|
|
20472
20194
|
created_at INTEGER NOT NULL,
|
|
20473
20195
|
updated_at INTEGER NOT NULL
|
|
20474
20196
|
);
|
|
@@ -20476,7 +20198,7 @@ function createMemoriesTable(database, options) {
|
|
|
20476
20198
|
}
|
|
20477
20199
|
function createMemoryIndexes(database) {
|
|
20478
20200
|
database.exec(`
|
|
20479
|
-
CREATE INDEX IF NOT EXISTS
|
|
20201
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);
|
|
20480
20202
|
CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
|
|
20481
20203
|
`);
|
|
20482
20204
|
}
|
|
@@ -20516,82 +20238,38 @@ function dropMemorySearchArtifacts(database) {
|
|
|
20516
20238
|
DROP TABLE IF EXISTS memories_fts;
|
|
20517
20239
|
`);
|
|
20518
20240
|
}
|
|
20519
|
-
function getEmbeddingColumnSql(mode) {
|
|
20520
|
-
switch (mode) {
|
|
20521
|
-
case "omit":
|
|
20522
|
-
return "";
|
|
20523
|
-
case "nullable":
|
|
20524
|
-
return `embedding BLOB,
|
|
20525
|
-
`;
|
|
20526
|
-
case "required":
|
|
20527
|
-
return `embedding BLOB NOT NULL,
|
|
20528
|
-
`;
|
|
20529
|
-
}
|
|
20530
|
-
}
|
|
20531
20241
|
|
|
20532
20242
|
// src/sqlite/migrations/001-create-memory-schema.ts
|
|
20533
20243
|
var createMemorySchemaMigration = {
|
|
20534
20244
|
version: 1,
|
|
20535
20245
|
async up(database) {
|
|
20536
|
-
createMemoriesTable(database
|
|
20246
|
+
createMemoriesTable(database);
|
|
20537
20247
|
createMemoryIndexes(database);
|
|
20538
20248
|
createMemorySearchArtifacts(database);
|
|
20539
20249
|
}
|
|
20540
20250
|
};
|
|
20541
20251
|
|
|
20542
|
-
// src/sqlite/embedding-codec.ts
|
|
20543
|
-
var FLOAT32_BYTE_WIDTH = 4;
|
|
20544
|
-
function encodeEmbedding(vector) {
|
|
20545
|
-
const typedArray = Float32Array.from(vector);
|
|
20546
|
-
return new Uint8Array(typedArray.buffer.slice(0));
|
|
20547
|
-
}
|
|
20548
|
-
function decodeEmbedding(value) {
|
|
20549
|
-
const bytes = toUint8Array(value);
|
|
20550
|
-
if (bytes.byteLength === 0) {
|
|
20551
|
-
throw new Error("Embedding blob is empty.");
|
|
20552
|
-
}
|
|
20553
|
-
if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
|
|
20554
|
-
throw new Error("Embedding blob length is not a multiple of 4.");
|
|
20555
|
-
}
|
|
20556
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20557
|
-
const vector = [];
|
|
20558
|
-
for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
|
|
20559
|
-
vector.push(view.getFloat32(offset, true));
|
|
20560
|
-
}
|
|
20561
|
-
return vector;
|
|
20562
|
-
}
|
|
20563
|
-
function toUint8Array(value) {
|
|
20564
|
-
if (value instanceof Uint8Array) {
|
|
20565
|
-
return value;
|
|
20566
|
-
}
|
|
20567
|
-
if (value instanceof ArrayBuffer) {
|
|
20568
|
-
return new Uint8Array(value);
|
|
20569
|
-
}
|
|
20570
|
-
throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
|
|
20571
|
-
}
|
|
20572
|
-
|
|
20573
20252
|
// src/sqlite/migrations/002-add-memory-embedding.ts
|
|
20574
|
-
function createAddMemoryEmbeddingMigration(
|
|
20253
|
+
function createAddMemoryEmbeddingMigration() {
|
|
20575
20254
|
return {
|
|
20576
20255
|
version: 2,
|
|
20577
20256
|
async up(database) {
|
|
20578
20257
|
database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
|
|
20579
|
-
const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
|
|
20580
|
-
const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
|
|
20581
|
-
for (const row of rows) {
|
|
20582
|
-
const embedding = await embeddingService.createVector(row.content);
|
|
20583
|
-
updateStatement.run(encodeEmbedding(embedding), row.id);
|
|
20584
|
-
}
|
|
20585
|
-
const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
|
|
20586
|
-
if ((nullRows[0]?.count ?? 0) > 0) {
|
|
20587
|
-
throw new Error("Failed to backfill embeddings for all memories.");
|
|
20588
|
-
}
|
|
20589
20258
|
dropMemorySearchArtifacts(database);
|
|
20590
20259
|
database.exec("ALTER TABLE memories RENAME TO memories_old");
|
|
20591
|
-
|
|
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
|
+
`);
|
|
20592
20270
|
database.exec(`
|
|
20593
20271
|
INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
|
|
20594
|
-
SELECT id, content, workspace, embedding, created_at, updated_at
|
|
20272
|
+
SELECT id, content, workspace, COALESCE(embedding, X'00000000'), created_at, updated_at
|
|
20595
20273
|
FROM memories_old
|
|
20596
20274
|
`);
|
|
20597
20275
|
database.exec("DROP TABLE memories_old");
|
|
@@ -20619,12 +20297,31 @@ function createNormalizeWorkspaceMigration(workspaceResolver) {
|
|
|
20619
20297
|
};
|
|
20620
20298
|
}
|
|
20621
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
|
+
|
|
20622
20318
|
// src/sqlite/migrations/index.ts
|
|
20623
20319
|
function createMemoryMigrations(options) {
|
|
20624
20320
|
return [
|
|
20625
20321
|
createMemorySchemaMigration,
|
|
20626
|
-
createAddMemoryEmbeddingMigration(
|
|
20627
|
-
createNormalizeWorkspaceMigration(options.workspaceResolver)
|
|
20322
|
+
createAddMemoryEmbeddingMigration(),
|
|
20323
|
+
createNormalizeWorkspaceMigration(options.workspaceResolver),
|
|
20324
|
+
removeMemoryEmbeddingMigration
|
|
20628
20325
|
];
|
|
20629
20326
|
}
|
|
20630
20327
|
|
|
@@ -20638,7 +20335,7 @@ var PRAGMA_STATEMENTS = [
|
|
|
20638
20335
|
async function openMemoryDatabase(databasePath, options) {
|
|
20639
20336
|
let database;
|
|
20640
20337
|
try {
|
|
20641
|
-
|
|
20338
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
20642
20339
|
database = new Database(databasePath);
|
|
20643
20340
|
await initializeMemoryDatabase(database, createMemoryMigrations(options));
|
|
20644
20341
|
return database;
|
|
@@ -20712,7 +20409,6 @@ function validateMigrations(migrations) {
|
|
|
20712
20409
|
}
|
|
20713
20410
|
// src/sqlite/repository.ts
|
|
20714
20411
|
import { randomUUID } from "node:crypto";
|
|
20715
|
-
var DEFAULT_SEARCH_LIMIT = 15;
|
|
20716
20412
|
var DEFAULT_LIST_LIMIT2 = 15;
|
|
20717
20413
|
|
|
20718
20414
|
class SqliteMemoryRepository {
|
|
@@ -20725,24 +20421,11 @@ class SqliteMemoryRepository {
|
|
|
20725
20421
|
constructor(database) {
|
|
20726
20422
|
this.database = database;
|
|
20727
20423
|
this.insertStatement = database.prepare(`
|
|
20728
|
-
INSERT INTO memories (
|
|
20729
|
-
|
|
20730
|
-
content,
|
|
20731
|
-
workspace,
|
|
20732
|
-
embedding,
|
|
20733
|
-
created_at,
|
|
20734
|
-
updated_at
|
|
20735
|
-
) VALUES (
|
|
20736
|
-
?,
|
|
20737
|
-
?,
|
|
20738
|
-
?,
|
|
20739
|
-
?,
|
|
20740
|
-
?,
|
|
20741
|
-
?
|
|
20742
|
-
)
|
|
20424
|
+
INSERT INTO memories (id, content, workspace, created_at, updated_at)
|
|
20425
|
+
VALUES (?, ?, ?, ?, ?)
|
|
20743
20426
|
`);
|
|
20744
|
-
this.getStatement = database.prepare("SELECT id, content, workspace,
|
|
20745
|
-
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 = ?");
|
|
20746
20429
|
this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
|
|
20747
20430
|
this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
|
|
20748
20431
|
}
|
|
@@ -20752,63 +20435,21 @@ class SqliteMemoryRepository {
|
|
|
20752
20435
|
const memory = {
|
|
20753
20436
|
id: randomUUID(),
|
|
20754
20437
|
content: input.content,
|
|
20755
|
-
embedding: input.embedding,
|
|
20756
20438
|
workspace: input.workspace,
|
|
20757
20439
|
createdAt: now,
|
|
20758
20440
|
updatedAt: now
|
|
20759
20441
|
};
|
|
20760
|
-
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());
|
|
20761
20443
|
return memory;
|
|
20762
20444
|
} catch (error2) {
|
|
20763
20445
|
throw new PersistenceError("Failed to save memory.", { cause: error2 });
|
|
20764
20446
|
}
|
|
20765
20447
|
}
|
|
20766
|
-
async search(query) {
|
|
20767
|
-
try {
|
|
20768
|
-
const whereParams = [toFtsQuery(query.terms)];
|
|
20769
|
-
const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
20770
|
-
const whereClauses = ["memories_fts MATCH ?"];
|
|
20771
|
-
if (query.updatedAfter) {
|
|
20772
|
-
whereClauses.push("m.updated_at >= ?");
|
|
20773
|
-
whereParams.push(query.updatedAfter.getTime());
|
|
20774
|
-
}
|
|
20775
|
-
if (query.updatedBefore) {
|
|
20776
|
-
whereClauses.push("m.updated_at <= ?");
|
|
20777
|
-
whereParams.push(query.updatedBefore.getTime());
|
|
20778
|
-
}
|
|
20779
|
-
const params = [...whereParams, limit];
|
|
20780
|
-
const statement = this.database.prepare(`
|
|
20781
|
-
SELECT
|
|
20782
|
-
m.id,
|
|
20783
|
-
m.content,
|
|
20784
|
-
m.workspace,
|
|
20785
|
-
m.embedding,
|
|
20786
|
-
m.created_at,
|
|
20787
|
-
m.updated_at,
|
|
20788
|
-
MAX(0, -bm25(memories_fts)) AS score
|
|
20789
|
-
FROM memories_fts
|
|
20790
|
-
INNER JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
20791
|
-
WHERE ${whereClauses.join(" AND ")}
|
|
20792
|
-
ORDER BY score DESC
|
|
20793
|
-
LIMIT ?
|
|
20794
|
-
`);
|
|
20795
|
-
const rows = statement.all(...params);
|
|
20796
|
-
const maxScore = Math.max(...rows.map((row) => row.score), 0);
|
|
20797
|
-
return rows.map((row) => ({
|
|
20798
|
-
...toMemoryEntity(row),
|
|
20799
|
-
score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
|
|
20800
|
-
}));
|
|
20801
|
-
} catch (error2) {
|
|
20802
|
-
throw new PersistenceError("Failed to search memories.", {
|
|
20803
|
-
cause: error2
|
|
20804
|
-
});
|
|
20805
|
-
}
|
|
20806
|
-
}
|
|
20807
20448
|
async get(id) {
|
|
20808
20449
|
try {
|
|
20809
20450
|
const rows = this.getStatement.all(id);
|
|
20810
20451
|
const row = rows[0];
|
|
20811
|
-
return row ?
|
|
20452
|
+
return row ? toMemoryRecord(row) : undefined;
|
|
20812
20453
|
} catch (error2) {
|
|
20813
20454
|
throw new PersistenceError("Failed to find memory.", { cause: error2 });
|
|
20814
20455
|
}
|
|
@@ -20819,25 +20460,28 @@ class SqliteMemoryRepository {
|
|
|
20819
20460
|
const params = [];
|
|
20820
20461
|
const offset = options.offset ?? 0;
|
|
20821
20462
|
const limit = options.limit ?? DEFAULT_LIST_LIMIT2;
|
|
20822
|
-
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) {
|
|
20823
20467
|
whereClauses.push("workspace = ?");
|
|
20824
20468
|
params.push(options.workspace);
|
|
20825
|
-
} else if (options.
|
|
20469
|
+
} else if (options.global) {
|
|
20826
20470
|
whereClauses.push("workspace IS NULL");
|
|
20827
20471
|
}
|
|
20828
20472
|
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
20829
20473
|
const queryLimit = limit + 1;
|
|
20830
20474
|
params.push(queryLimit, offset);
|
|
20831
20475
|
const statement = this.database.prepare(`
|
|
20832
|
-
SELECT id, content, workspace,
|
|
20476
|
+
SELECT id, content, workspace, created_at, updated_at
|
|
20833
20477
|
FROM memories
|
|
20834
20478
|
${whereClause}
|
|
20835
|
-
ORDER BY
|
|
20479
|
+
ORDER BY updated_at DESC
|
|
20836
20480
|
LIMIT ? OFFSET ?
|
|
20837
20481
|
`);
|
|
20838
20482
|
const rows = statement.all(...params);
|
|
20839
20483
|
const hasMore = rows.length > limit;
|
|
20840
|
-
const items = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
20484
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryRecord);
|
|
20841
20485
|
return { items, hasMore };
|
|
20842
20486
|
} catch (error2) {
|
|
20843
20487
|
throw new PersistenceError("Failed to list memories.", { cause: error2 });
|
|
@@ -20847,7 +20491,7 @@ class SqliteMemoryRepository {
|
|
|
20847
20491
|
let result;
|
|
20848
20492
|
try {
|
|
20849
20493
|
const now = Date.now();
|
|
20850
|
-
result = this.updateStatement.run(input.content,
|
|
20494
|
+
result = this.updateStatement.run(input.content, now, input.id);
|
|
20851
20495
|
} catch (error2) {
|
|
20852
20496
|
throw new PersistenceError("Failed to update memory.", { cause: error2 });
|
|
20853
20497
|
}
|
|
@@ -20880,22 +20524,13 @@ class SqliteMemoryRepository {
|
|
|
20880
20524
|
}
|
|
20881
20525
|
}
|
|
20882
20526
|
}
|
|
20883
|
-
var
|
|
20527
|
+
var toMemoryRecord = (row) => ({
|
|
20884
20528
|
id: row.id,
|
|
20885
20529
|
content: row.content,
|
|
20886
|
-
embedding: decodeEmbedding(row.embedding),
|
|
20887
20530
|
workspace: row.workspace ?? undefined,
|
|
20888
20531
|
createdAt: new Date(row.created_at),
|
|
20889
20532
|
updatedAt: new Date(row.updated_at)
|
|
20890
20533
|
});
|
|
20891
|
-
var toFtsQuery = (terms) => terms.map(toFtsTerm).join(" OR ");
|
|
20892
|
-
function toFtsTerm(term) {
|
|
20893
|
-
const escaped = term.replaceAll('"', '""');
|
|
20894
|
-
if (term.includes(" ")) {
|
|
20895
|
-
return `"${escaped}"`;
|
|
20896
|
-
}
|
|
20897
|
-
return `"${escaped}"*`;
|
|
20898
|
-
}
|
|
20899
20534
|
// node_modules/@hono/node-server/dist/index.mjs
|
|
20900
20535
|
import { createServer as createServerHTTP } from "http";
|
|
20901
20536
|
import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
|
|
@@ -24211,7 +23846,7 @@ function createPageRoutes(memory) {
|
|
|
24211
23846
|
const wsFilter = workspace && !isNoWorkspace ? workspace : undefined;
|
|
24212
23847
|
const page = await memory.list({
|
|
24213
23848
|
workspace: wsFilter,
|
|
24214
|
-
|
|
23849
|
+
global: isNoWorkspace,
|
|
24215
23850
|
offset: (pageNum - 1) * DEFAULT_LIST_LIMIT3,
|
|
24216
23851
|
limit: DEFAULT_LIST_LIMIT3
|
|
24217
23852
|
});
|
|
@@ -24332,13 +23967,10 @@ function normalizeOptionalString2(value) {
|
|
|
24332
23967
|
|
|
24333
23968
|
// src/index.ts
|
|
24334
23969
|
var config2 = resolveConfig();
|
|
24335
|
-
configureModelsCache(config2.modelsCachePath);
|
|
24336
|
-
var embeddingService = new EmbeddingService;
|
|
24337
23970
|
var workspaceResolver = createGitWorkspaceResolver();
|
|
24338
|
-
var database = await openMemoryDatabase(config2.databasePath, {
|
|
23971
|
+
var database = await openMemoryDatabase(config2.databasePath, { workspaceResolver });
|
|
24339
23972
|
var repository2 = new SqliteMemoryRepository(database);
|
|
24340
|
-
var memoryService = new MemoryService(repository2,
|
|
24341
|
-
embeddingService.warmup();
|
|
23973
|
+
var memoryService = new MemoryService(repository2, workspaceResolver);
|
|
24342
23974
|
if (config2.uiMode) {
|
|
24343
23975
|
let shutdown = function() {
|
|
24344
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",
|