@jcyamacho/agent-memory 0.0.11 → 0.0.13
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 +33 -4
- package/dist/index.js +444 -394
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
Persistent memory for MCP-powered coding agents.
|
|
4
4
|
|
|
5
5
|
`agent-memory` is a stdio MCP server that gives your LLM durable memory backed
|
|
6
|
-
by SQLite. It exposes
|
|
6
|
+
by SQLite. It exposes four tools:
|
|
7
7
|
|
|
8
8
|
- `remember` -> save facts, decisions, preferences, and project context
|
|
9
9
|
- `recall` -> retrieve the most relevant memories later
|
|
10
|
+
- `revise` -> update an existing memory when it becomes outdated
|
|
11
|
+
- `forget` -> delete a memory that is no longer relevant
|
|
10
12
|
|
|
11
13
|
Use it when your agent should remember preferences, project facts, and prior
|
|
12
14
|
decisions across sessions.
|
|
@@ -66,7 +68,10 @@ Optional LLM instructions to reinforce the MCP's built-in guidance:
|
|
|
66
68
|
Use `recall` at the start of every conversation and again mid-task before
|
|
67
69
|
making design choices or picking conventions. Use `remember` when the user
|
|
68
70
|
corrects your approach, a key decision is established, or you learn project
|
|
69
|
-
context not obvious from the code.
|
|
71
|
+
context not obvious from the code. Before saving, recall to check whether a
|
|
72
|
+
memory about the same fact already exists -- if so, use `revise` to update
|
|
73
|
+
it instead of creating a duplicate. Use `forget` to remove memories that
|
|
74
|
+
are wrong or no longer relevant. Always pass workspace.
|
|
70
75
|
```
|
|
71
76
|
|
|
72
77
|
## What It Stores
|
|
@@ -92,8 +97,7 @@ Opens at `http://localhost:6580`. Use `--port` to change:
|
|
|
92
97
|
npx -y @jcyamacho/agent-memory --ui --port 9090
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
The web UI uses the same database as the MCP server.
|
|
96
|
-
append-only; the web UI is the only way to edit or delete memories.
|
|
100
|
+
The web UI uses the same database as the MCP server.
|
|
97
101
|
|
|
98
102
|
## Tools
|
|
99
103
|
|
|
@@ -127,6 +131,31 @@ Output:
|
|
|
127
131
|
|
|
128
132
|
- `results[]` with `id`, `content`, `score`, `workspace`, and `updated_at`
|
|
129
133
|
|
|
134
|
+
### `revise`
|
|
135
|
+
|
|
136
|
+
Update the content of an existing memory.
|
|
137
|
+
|
|
138
|
+
Inputs:
|
|
139
|
+
|
|
140
|
+
- `id` -> the memory id from a previous recall result
|
|
141
|
+
- `content` -> replacement content for the memory
|
|
142
|
+
|
|
143
|
+
Output:
|
|
144
|
+
|
|
145
|
+
- `id`, `updated_at`
|
|
146
|
+
|
|
147
|
+
### `forget`
|
|
148
|
+
|
|
149
|
+
Permanently delete a memory.
|
|
150
|
+
|
|
151
|
+
Inputs:
|
|
152
|
+
|
|
153
|
+
- `id` -> the memory id from a previous recall result
|
|
154
|
+
|
|
155
|
+
Output:
|
|
156
|
+
|
|
157
|
+
- `id`, `deleted`
|
|
158
|
+
|
|
130
159
|
## How Ranking Works
|
|
131
160
|
|
|
132
161
|
`recall` uses a multi-signal ranking system to surface the most relevant
|
package/dist/index.js
CHANGED
|
@@ -12464,7 +12464,7 @@ class StdioServerTransport {
|
|
|
12464
12464
|
}
|
|
12465
12465
|
}
|
|
12466
12466
|
// package.json
|
|
12467
|
-
var version2 = "0.0.
|
|
12467
|
+
var version2 = "0.0.13";
|
|
12468
12468
|
|
|
12469
12469
|
// src/config.ts
|
|
12470
12470
|
import { homedir } from "node:os";
|
|
@@ -12472,7 +12472,7 @@ 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
12474
|
var DEFAULT_UI_PORT = 6580;
|
|
12475
|
-
|
|
12475
|
+
function resolveConfig(environment = process.env, argv = process.argv.slice(2)) {
|
|
12476
12476
|
const { values } = parseArgs({
|
|
12477
12477
|
args: argv,
|
|
12478
12478
|
options: {
|
|
@@ -12486,7 +12486,7 @@ var resolveConfig = (environment = process.env, argv = process.argv.slice(2)) =>
|
|
|
12486
12486
|
uiMode: Boolean(values.ui),
|
|
12487
12487
|
uiPort: Number(values.port) || DEFAULT_UI_PORT
|
|
12488
12488
|
};
|
|
12489
|
-
}
|
|
12489
|
+
}
|
|
12490
12490
|
|
|
12491
12491
|
// node_modules/zod/v3/helpers/util.js
|
|
12492
12492
|
var util;
|
|
@@ -19895,9 +19895,6 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
19895
19895
|
}
|
|
19896
19896
|
};
|
|
19897
19897
|
|
|
19898
|
-
// src/memory-service.ts
|
|
19899
|
-
import { randomUUID } from "node:crypto";
|
|
19900
|
-
|
|
19901
19898
|
// src/errors.ts
|
|
19902
19899
|
class MemoryError extends Error {
|
|
19903
19900
|
code;
|
|
@@ -19929,39 +19926,152 @@ class PersistenceError extends MemoryError {
|
|
|
19929
19926
|
}
|
|
19930
19927
|
}
|
|
19931
19928
|
|
|
19929
|
+
// src/mcp/tools/shared.ts
|
|
19930
|
+
function toMcpError(error2) {
|
|
19931
|
+
if (error2 instanceof McpError) {
|
|
19932
|
+
return error2;
|
|
19933
|
+
}
|
|
19934
|
+
if (error2 instanceof MemoryError) {
|
|
19935
|
+
if (error2.code === "VALIDATION_ERROR" || error2.code === "NOT_FOUND") {
|
|
19936
|
+
return new McpError(ErrorCode.InvalidParams, error2.message);
|
|
19937
|
+
}
|
|
19938
|
+
return new McpError(ErrorCode.InternalError, error2.message);
|
|
19939
|
+
}
|
|
19940
|
+
const message = error2 instanceof Error ? error2.message : "Unknown server error.";
|
|
19941
|
+
return new McpError(ErrorCode.InternalError, message);
|
|
19942
|
+
}
|
|
19943
|
+
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19944
|
+
function parseOptionalDate(value, fieldName) {
|
|
19945
|
+
if (!value) {
|
|
19946
|
+
return;
|
|
19947
|
+
}
|
|
19948
|
+
const date4 = new Date(value);
|
|
19949
|
+
if (Number.isNaN(date4.getTime())) {
|
|
19950
|
+
throw new MemoryError("VALIDATION_ERROR", `${fieldName} must be a valid ISO 8601 datetime.`);
|
|
19951
|
+
}
|
|
19952
|
+
return date4;
|
|
19953
|
+
}
|
|
19954
|
+
|
|
19955
|
+
// src/mcp/tools/forget.ts
|
|
19956
|
+
var forgetInputSchema = {
|
|
19957
|
+
id: string2().describe("The id of the memory to delete. Use the id returned by a previous recall result.")
|
|
19958
|
+
};
|
|
19959
|
+
function registerForgetTool(server, memory) {
|
|
19960
|
+
server.registerTool("forget", {
|
|
19961
|
+
description: "Permanently delete a memory that is wrong, obsolete, or no longer relevant. Pass the memory id from a previous recall result.",
|
|
19962
|
+
inputSchema: forgetInputSchema
|
|
19963
|
+
}, async ({ id }) => {
|
|
19964
|
+
try {
|
|
19965
|
+
await memory.delete({ id });
|
|
19966
|
+
return {
|
|
19967
|
+
content: [{ type: "text", text: `<memory id="${id.trim()}" deleted="true" />` }]
|
|
19968
|
+
};
|
|
19969
|
+
} catch (error2) {
|
|
19970
|
+
throw toMcpError(error2);
|
|
19971
|
+
}
|
|
19972
|
+
});
|
|
19973
|
+
}
|
|
19974
|
+
|
|
19932
19975
|
// src/memory.ts
|
|
19933
19976
|
var toNormalizedScore = (value) => value;
|
|
19934
19977
|
|
|
19935
|
-
// src/
|
|
19936
|
-
var DEFAULT_LIMIT = 15;
|
|
19937
|
-
var MAX_LIMIT = 50;
|
|
19938
|
-
var RECALL_CANDIDATE_LIMIT_MULTIPLIER = 3;
|
|
19978
|
+
// src/ranking.ts
|
|
19939
19979
|
var RETRIEVAL_SCORE_WEIGHT = 8;
|
|
19940
19980
|
var WORKSPACE_MATCH_WEIGHT = 4;
|
|
19941
19981
|
var RECENCY_WEIGHT = 1;
|
|
19942
19982
|
var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
|
|
19943
19983
|
var GLOBAL_WORKSPACE_SCORE = 0.5;
|
|
19944
19984
|
var SIBLING_WORKSPACE_SCORE = 0.25;
|
|
19985
|
+
function rerankSearchResults(results, workspace) {
|
|
19986
|
+
if (results.length <= 1) {
|
|
19987
|
+
return results;
|
|
19988
|
+
}
|
|
19989
|
+
const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
|
|
19990
|
+
const updatedAtTimes = results.map((result) => result.updatedAt.getTime());
|
|
19991
|
+
const minUpdatedAt = Math.min(...updatedAtTimes);
|
|
19992
|
+
const maxUpdatedAt = Math.max(...updatedAtTimes);
|
|
19993
|
+
return results.map((result) => {
|
|
19994
|
+
const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
|
|
19995
|
+
const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
|
|
19996
|
+
const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
|
|
19997
|
+
return {
|
|
19998
|
+
...result,
|
|
19999
|
+
score: toNormalizedScore(combinedScore)
|
|
20000
|
+
};
|
|
20001
|
+
}).sort((a, b) => b.score - a.score);
|
|
20002
|
+
}
|
|
20003
|
+
function normalizeWorkspacePath(value) {
|
|
20004
|
+
return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
|
|
20005
|
+
}
|
|
20006
|
+
function computeWorkspaceScore(memoryWs, queryWs) {
|
|
20007
|
+
if (!queryWs) {
|
|
20008
|
+
return 0;
|
|
20009
|
+
}
|
|
20010
|
+
if (!memoryWs) {
|
|
20011
|
+
return GLOBAL_WORKSPACE_SCORE;
|
|
20012
|
+
}
|
|
20013
|
+
const normalizedMemoryWs = normalizeWorkspacePath(memoryWs);
|
|
20014
|
+
if (normalizedMemoryWs === queryWs) {
|
|
20015
|
+
return 1;
|
|
20016
|
+
}
|
|
20017
|
+
const queryLastSlashIndex = queryWs.lastIndexOf("/");
|
|
20018
|
+
const memoryLastSlashIndex = normalizedMemoryWs.lastIndexOf("/");
|
|
20019
|
+
if (queryLastSlashIndex <= 0 || memoryLastSlashIndex <= 0) {
|
|
20020
|
+
return 0;
|
|
20021
|
+
}
|
|
20022
|
+
const queryParent = queryWs.slice(0, queryLastSlashIndex);
|
|
20023
|
+
const memoryParent = normalizedMemoryWs.slice(0, memoryLastSlashIndex);
|
|
20024
|
+
return memoryParent === queryParent ? SIBLING_WORKSPACE_SCORE : 0;
|
|
20025
|
+
}
|
|
20026
|
+
|
|
20027
|
+
// src/memory-service.ts
|
|
20028
|
+
var DEFAULT_RECALL_LIMIT = 15;
|
|
20029
|
+
var MAX_RECALL_LIMIT = 50;
|
|
20030
|
+
var RECALL_CANDIDATE_LIMIT_MULTIPLIER = 3;
|
|
20031
|
+
var DEFAULT_LIST_LIMIT = 15;
|
|
20032
|
+
var MAX_LIST_LIMIT = 100;
|
|
19945
20033
|
|
|
19946
20034
|
class MemoryService {
|
|
19947
20035
|
repository;
|
|
19948
20036
|
constructor(repository) {
|
|
19949
20037
|
this.repository = repository;
|
|
19950
20038
|
}
|
|
19951
|
-
async
|
|
20039
|
+
async create(input) {
|
|
19952
20040
|
const content = input.content.trim();
|
|
19953
20041
|
if (!content) {
|
|
19954
20042
|
throw new ValidationError("Memory content is required.");
|
|
19955
20043
|
}
|
|
19956
|
-
|
|
19957
|
-
const memory = {
|
|
19958
|
-
id: randomUUID(),
|
|
20044
|
+
return this.repository.create({
|
|
19959
20045
|
content,
|
|
19960
|
-
workspace: normalizeOptionalString(input.workspace)
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
|
|
20046
|
+
workspace: normalizeOptionalString(input.workspace)
|
|
20047
|
+
});
|
|
20048
|
+
}
|
|
20049
|
+
async update(input) {
|
|
20050
|
+
const content = input.content.trim();
|
|
20051
|
+
if (!content)
|
|
20052
|
+
throw new ValidationError("Memory content is required.");
|
|
20053
|
+
return this.repository.update({ id: input.id, content });
|
|
20054
|
+
}
|
|
20055
|
+
async delete(input) {
|
|
20056
|
+
const id = input.id.trim();
|
|
20057
|
+
if (!id)
|
|
20058
|
+
throw new ValidationError("Memory id is required.");
|
|
20059
|
+
return this.repository.delete({ id });
|
|
20060
|
+
}
|
|
20061
|
+
async get(id) {
|
|
20062
|
+
return this.repository.get(id);
|
|
20063
|
+
}
|
|
20064
|
+
async list(input) {
|
|
20065
|
+
const workspace = normalizeOptionalString(input.workspace);
|
|
20066
|
+
return this.repository.list({
|
|
20067
|
+
workspace,
|
|
20068
|
+
workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
|
|
20069
|
+
offset: normalizeOffset(input.offset),
|
|
20070
|
+
limit: normalizeListLimit(input.limit)
|
|
20071
|
+
});
|
|
20072
|
+
}
|
|
20073
|
+
async listWorkspaces() {
|
|
20074
|
+
return this.repository.listWorkspaces();
|
|
19965
20075
|
}
|
|
19966
20076
|
async search(input) {
|
|
19967
20077
|
const terms = normalizeTerms(input.terms);
|
|
@@ -19977,19 +20087,27 @@ class MemoryService {
|
|
|
19977
20087
|
updatedBefore: input.updatedBefore
|
|
19978
20088
|
};
|
|
19979
20089
|
const results = await this.repository.search(normalizedQuery);
|
|
19980
|
-
|
|
19981
|
-
return reranked.sort((a, b) => b.score - a.score).slice(0, requestedLimit);
|
|
20090
|
+
return rerankSearchResults(results, workspace).slice(0, requestedLimit);
|
|
19982
20091
|
}
|
|
19983
20092
|
}
|
|
19984
20093
|
function normalizeLimit(value) {
|
|
19985
20094
|
if (value === undefined) {
|
|
19986
|
-
return
|
|
20095
|
+
return DEFAULT_RECALL_LIMIT;
|
|
19987
20096
|
}
|
|
19988
|
-
if (!Number.isInteger(value) || value < 1 || value >
|
|
19989
|
-
throw new ValidationError(`Limit must be an integer between 1 and ${
|
|
20097
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_RECALL_LIMIT) {
|
|
20098
|
+
throw new ValidationError(`Limit must be an integer between 1 and ${MAX_RECALL_LIMIT}.`);
|
|
19990
20099
|
}
|
|
19991
20100
|
return value;
|
|
19992
20101
|
}
|
|
20102
|
+
function normalizeOffset(value) {
|
|
20103
|
+
return Number.isInteger(value) && value && value > 0 ? value : 0;
|
|
20104
|
+
}
|
|
20105
|
+
function normalizeListLimit(value) {
|
|
20106
|
+
if (!Number.isInteger(value) || value === undefined) {
|
|
20107
|
+
return DEFAULT_LIST_LIMIT;
|
|
20108
|
+
}
|
|
20109
|
+
return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
|
|
20110
|
+
}
|
|
19993
20111
|
function normalizeOptionalString(value) {
|
|
19994
20112
|
const trimmed = value?.trim();
|
|
19995
20113
|
return trimmed ? trimmed : undefined;
|
|
@@ -19998,96 +20116,29 @@ function normalizeTerms(terms) {
|
|
|
19998
20116
|
const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
|
|
19999
20117
|
return [...new Set(normalizedTerms)];
|
|
20000
20118
|
}
|
|
20001
|
-
function normalizeWorkspacePath(value) {
|
|
20002
|
-
return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
|
|
20003
|
-
}
|
|
20004
|
-
function computeWorkspaceScore(memoryWs, queryWs) {
|
|
20005
|
-
if (!queryWs) {
|
|
20006
|
-
return 0;
|
|
20007
|
-
}
|
|
20008
|
-
if (!memoryWs) {
|
|
20009
|
-
return GLOBAL_WORKSPACE_SCORE;
|
|
20010
|
-
}
|
|
20011
|
-
const normalizedMemoryWs = normalizeWorkspacePath(memoryWs);
|
|
20012
|
-
if (normalizedMemoryWs === queryWs) {
|
|
20013
|
-
return 1;
|
|
20014
|
-
}
|
|
20015
|
-
const queryLastSlashIndex = queryWs.lastIndexOf("/");
|
|
20016
|
-
const memoryLastSlashIndex = normalizedMemoryWs.lastIndexOf("/");
|
|
20017
|
-
if (queryLastSlashIndex <= 0 || memoryLastSlashIndex <= 0) {
|
|
20018
|
-
return 0;
|
|
20019
|
-
}
|
|
20020
|
-
const queryParent = queryWs.slice(0, queryLastSlashIndex);
|
|
20021
|
-
const memoryParent = normalizedMemoryWs.slice(0, memoryLastSlashIndex);
|
|
20022
|
-
return memoryParent === queryParent ? SIBLING_WORKSPACE_SCORE : 0;
|
|
20023
|
-
}
|
|
20024
|
-
function rerankSearchResults(results, workspace) {
|
|
20025
|
-
if (results.length <= 1) {
|
|
20026
|
-
return results;
|
|
20027
|
-
}
|
|
20028
|
-
const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
|
|
20029
|
-
const updatedAtTimes = results.map((result) => result.updatedAt.getTime());
|
|
20030
|
-
const minUpdatedAt = Math.min(...updatedAtTimes);
|
|
20031
|
-
const maxUpdatedAt = Math.max(...updatedAtTimes);
|
|
20032
|
-
return results.map((result) => {
|
|
20033
|
-
const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
|
|
20034
|
-
const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
|
|
20035
|
-
const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
|
|
20036
|
-
return {
|
|
20037
|
-
...result,
|
|
20038
|
-
score: toNormalizedScore(combinedScore)
|
|
20039
|
-
};
|
|
20040
|
-
});
|
|
20041
|
-
}
|
|
20042
20119
|
|
|
20043
|
-
// src/tools/
|
|
20044
|
-
var toMcpError = (error2) => {
|
|
20045
|
-
if (error2 instanceof McpError) {
|
|
20046
|
-
return error2;
|
|
20047
|
-
}
|
|
20048
|
-
if (error2 instanceof MemoryError) {
|
|
20049
|
-
if (error2.code === "VALIDATION_ERROR") {
|
|
20050
|
-
return new McpError(ErrorCode.InvalidParams, error2.message);
|
|
20051
|
-
}
|
|
20052
|
-
return new McpError(ErrorCode.InternalError, error2.message);
|
|
20053
|
-
}
|
|
20054
|
-
const message = error2 instanceof Error ? error2.message : "Unknown server error.";
|
|
20055
|
-
return new McpError(ErrorCode.InternalError, message);
|
|
20056
|
-
};
|
|
20057
|
-
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20058
|
-
var parseOptionalDate = (value, fieldName) => {
|
|
20059
|
-
if (!value) {
|
|
20060
|
-
return;
|
|
20061
|
-
}
|
|
20062
|
-
const date4 = new Date(value);
|
|
20063
|
-
if (Number.isNaN(date4.getTime())) {
|
|
20064
|
-
throw new MemoryError("VALIDATION_ERROR", `${fieldName} must be a valid ISO 8601 datetime.`);
|
|
20065
|
-
}
|
|
20066
|
-
return date4;
|
|
20067
|
-
};
|
|
20068
|
-
|
|
20069
|
-
// src/tools/recall.ts
|
|
20120
|
+
// src/mcp/tools/recall.ts
|
|
20070
20121
|
var recallInputSchema = {
|
|
20071
20122
|
terms: array(string2()).min(1).describe("Search terms used to find relevant memories. Pass 2-5 short, distinctive items as separate array entries. Be specific: instead of 'preferences' or 'context', name the actual topic -- e.g. 'error handling', 'commit format', 'testing strategy'. Do not repeat the project or workspace name here -- use the workspace parameter for project scoping. Avoid full sentences."),
|
|
20072
|
-
limit: number2().int().min(1).max(
|
|
20123
|
+
limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum number of matches to return. Keep this small when you only need the strongest hits."),
|
|
20073
20124
|
workspace: string2().optional().describe("Always pass the current working directory. Biases ranking toward the active project while still allowing cross-workspace matches. Memories saved without a workspace are treated as global and rank between matching and non-matching results."),
|
|
20074
20125
|
updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp. Use it when you need to narrow recall to newer context."),
|
|
20075
20126
|
updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp. Use it when you need to narrow recall to older context.")
|
|
20076
20127
|
};
|
|
20077
|
-
|
|
20128
|
+
function toMemoryXml(r) {
|
|
20078
20129
|
const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
|
|
20079
20130
|
const content = escapeXml(r.content);
|
|
20080
20131
|
return `<memory id="${r.id}" score="${r.score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
|
|
20081
20132
|
${content}
|
|
20082
20133
|
</memory>`;
|
|
20083
|
-
}
|
|
20084
|
-
|
|
20134
|
+
}
|
|
20135
|
+
function registerRecallTool(server, memory) {
|
|
20085
20136
|
server.registerTool("recall", {
|
|
20086
20137
|
description: "Search memories for prior decisions, corrections, and context that cannot be derived from code or git history. Call at the start of every conversation and again mid-task when you are about to make a design choice, pick a convention, or handle an edge case that the user may have guided before. Always pass workspace.",
|
|
20087
20138
|
inputSchema: recallInputSchema
|
|
20088
20139
|
}, async ({ terms, limit, workspace, updated_after, updated_before }) => {
|
|
20089
20140
|
try {
|
|
20090
|
-
const results = await
|
|
20141
|
+
const results = await memory.search({
|
|
20091
20142
|
terms,
|
|
20092
20143
|
limit,
|
|
20093
20144
|
workspace,
|
|
@@ -20105,51 +20156,82 @@ ${results.map(toMemoryXml).join(`
|
|
|
20105
20156
|
throw toMcpError(error2);
|
|
20106
20157
|
}
|
|
20107
20158
|
});
|
|
20108
|
-
}
|
|
20159
|
+
}
|
|
20109
20160
|
|
|
20110
|
-
// src/tools/remember.ts
|
|
20161
|
+
// src/mcp/tools/remember.ts
|
|
20111
20162
|
var rememberInputSchema = {
|
|
20112
20163
|
content: string2().describe("The fact, preference, decision, or context to remember. Use a single self-contained sentence or short note. One fact per memory."),
|
|
20113
20164
|
workspace: string2().optional().describe("Always pass the current working directory to scope this memory to a project. Omit only when the memory applies across all projects (global preference).")
|
|
20114
20165
|
};
|
|
20115
|
-
|
|
20166
|
+
function registerRememberTool(server, memory) {
|
|
20116
20167
|
server.registerTool("remember", {
|
|
20117
20168
|
description: "Save durable context for later recall. Use this when the user corrects your approach, states a preference, a key decision or convention is established, or you learn project context not obvious from the code. Store one concise fact per memory. Do not store secrets, ephemeral task state, or information already in the codebase.",
|
|
20118
20169
|
inputSchema: rememberInputSchema
|
|
20119
20170
|
}, async ({ content, workspace }) => {
|
|
20120
20171
|
try {
|
|
20121
|
-
const
|
|
20172
|
+
const savedMemory = await memory.create({
|
|
20122
20173
|
content,
|
|
20123
20174
|
workspace
|
|
20124
20175
|
});
|
|
20125
20176
|
return {
|
|
20126
|
-
content: [{ type: "text", text: `<memory id="${
|
|
20177
|
+
content: [{ type: "text", text: `<memory id="${savedMemory.id}" />` }]
|
|
20127
20178
|
};
|
|
20128
20179
|
} catch (error2) {
|
|
20129
20180
|
throw toMcpError(error2);
|
|
20130
20181
|
}
|
|
20131
20182
|
});
|
|
20183
|
+
}
|
|
20184
|
+
|
|
20185
|
+
// src/mcp/tools/revise.ts
|
|
20186
|
+
var reviseInputSchema = {
|
|
20187
|
+
id: string2().describe("The id of the memory to update. Use the id returned by a previous recall result."),
|
|
20188
|
+
content: string2().describe("The replacement content for the memory. Use a single self-contained sentence or short note. One fact per memory.")
|
|
20132
20189
|
};
|
|
20190
|
+
function registerReviseTool(server, memory) {
|
|
20191
|
+
server.registerTool("revise", {
|
|
20192
|
+
description: "Update the content of an existing memory. Use when a previously saved memory is outdated or inaccurate and needs correction rather than deletion. Pass the memory id from a previous recall result.",
|
|
20193
|
+
inputSchema: reviseInputSchema
|
|
20194
|
+
}, async ({ id, content }) => {
|
|
20195
|
+
try {
|
|
20196
|
+
const revisedMemory = await memory.update({ id, content });
|
|
20197
|
+
return {
|
|
20198
|
+
content: [
|
|
20199
|
+
{
|
|
20200
|
+
type: "text",
|
|
20201
|
+
text: `<memory id="${revisedMemory.id}" updated_at="${revisedMemory.updatedAt.toISOString()}" />`
|
|
20202
|
+
}
|
|
20203
|
+
]
|
|
20204
|
+
};
|
|
20205
|
+
} catch (error2) {
|
|
20206
|
+
throw toMcpError(error2);
|
|
20207
|
+
}
|
|
20208
|
+
});
|
|
20209
|
+
}
|
|
20133
20210
|
|
|
20134
|
-
// src/mcp
|
|
20211
|
+
// src/mcp/server.ts
|
|
20135
20212
|
var SERVER_INSTRUCTIONS = [
|
|
20136
20213
|
"Stores decisions, corrections, and context that cannot be derived from code or git history.",
|
|
20137
20214
|
"Use `recall` at the start of every conversation and again mid-task before making design choices or picking conventions the user may have guided before.",
|
|
20138
20215
|
"Use `remember` when the user corrects your approach, states a preference, a key decision is established, or you learn project context not obvious from the code.",
|
|
20216
|
+
"Before saving a new memory, recall to check whether a memory about the same fact already exists. If so, use `revise` to update it instead of creating a duplicate.",
|
|
20217
|
+
"Use `revise` when a previously saved memory is outdated or inaccurate and needs correction rather than deletion.",
|
|
20218
|
+
"Use `forget` to remove memories that are wrong, obsolete, or no longer relevant.",
|
|
20139
20219
|
"Always pass workspace (the current working directory) to scope results to the active project.",
|
|
20140
20220
|
"Omit workspace only when saving a memory that applies across all projects."
|
|
20141
20221
|
].join(" ");
|
|
20142
|
-
|
|
20222
|
+
function createMcpServer(memory, version3) {
|
|
20143
20223
|
const server = new McpServer({
|
|
20144
20224
|
name: "agent-memory",
|
|
20145
20225
|
version: version3
|
|
20146
20226
|
}, {
|
|
20147
20227
|
instructions: SERVER_INSTRUCTIONS
|
|
20148
20228
|
});
|
|
20149
|
-
registerRememberTool(server,
|
|
20150
|
-
registerRecallTool(server,
|
|
20229
|
+
registerRememberTool(server, memory);
|
|
20230
|
+
registerRecallTool(server, memory);
|
|
20231
|
+
registerReviseTool(server, memory);
|
|
20232
|
+
registerForgetTool(server, memory);
|
|
20151
20233
|
return server;
|
|
20152
|
-
}
|
|
20234
|
+
}
|
|
20153
20235
|
|
|
20154
20236
|
// src/sqlite-db.ts
|
|
20155
20237
|
import { mkdirSync } from "node:fs";
|
|
@@ -20189,7 +20271,7 @@ var MEMORY_SCHEMA = `
|
|
|
20189
20271
|
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
20190
20272
|
END;
|
|
20191
20273
|
`;
|
|
20192
|
-
|
|
20274
|
+
function openMemoryDatabase(databasePath) {
|
|
20193
20275
|
try {
|
|
20194
20276
|
mkdirSync(dirname(databasePath), { recursive: true });
|
|
20195
20277
|
const database = new Database(databasePath);
|
|
@@ -20200,8 +20282,8 @@ var openMemoryDatabase = (databasePath) => {
|
|
|
20200
20282
|
cause: error2
|
|
20201
20283
|
});
|
|
20202
20284
|
}
|
|
20203
|
-
}
|
|
20204
|
-
|
|
20285
|
+
}
|
|
20286
|
+
function initializeMemoryDatabase(database) {
|
|
20205
20287
|
if (database.pragma) {
|
|
20206
20288
|
database.pragma("journal_mode = WAL");
|
|
20207
20289
|
database.pragma("synchronous = NORMAL");
|
|
@@ -20214,13 +20296,17 @@ var initializeMemoryDatabase = (database) => {
|
|
|
20214
20296
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
20215
20297
|
}
|
|
20216
20298
|
database.exec(MEMORY_SCHEMA);
|
|
20217
|
-
}
|
|
20299
|
+
}
|
|
20300
|
+
|
|
20301
|
+
// src/sqlite-memory-repository.ts
|
|
20302
|
+
import { randomUUID } from "node:crypto";
|
|
20303
|
+
var DEFAULT_SEARCH_LIMIT = 15;
|
|
20304
|
+
var DEFAULT_LIST_LIMIT2 = 15;
|
|
20218
20305
|
|
|
20219
|
-
// src/sqlite-repository.ts
|
|
20220
20306
|
class SqliteMemoryRepository {
|
|
20221
20307
|
database;
|
|
20222
20308
|
insertStatement;
|
|
20223
|
-
|
|
20309
|
+
getStatement;
|
|
20224
20310
|
updateStatement;
|
|
20225
20311
|
deleteStatement;
|
|
20226
20312
|
listWorkspacesStatement;
|
|
@@ -20241,13 +20327,21 @@ class SqliteMemoryRepository {
|
|
|
20241
20327
|
?
|
|
20242
20328
|
)
|
|
20243
20329
|
`);
|
|
20244
|
-
this.
|
|
20330
|
+
this.getStatement = database.prepare("SELECT id, content, workspace, created_at, updated_at FROM memories WHERE id = ?");
|
|
20245
20331
|
this.updateStatement = database.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?");
|
|
20246
20332
|
this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
|
|
20247
20333
|
this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
|
|
20248
20334
|
}
|
|
20249
|
-
async
|
|
20335
|
+
async create(input) {
|
|
20250
20336
|
try {
|
|
20337
|
+
const now = new Date;
|
|
20338
|
+
const memory = {
|
|
20339
|
+
id: randomUUID(),
|
|
20340
|
+
content: input.content,
|
|
20341
|
+
workspace: input.workspace,
|
|
20342
|
+
createdAt: now,
|
|
20343
|
+
updatedAt: now
|
|
20344
|
+
};
|
|
20251
20345
|
this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
|
|
20252
20346
|
return memory;
|
|
20253
20347
|
} catch (error2) {
|
|
@@ -20257,6 +20351,7 @@ class SqliteMemoryRepository {
|
|
|
20257
20351
|
async search(query) {
|
|
20258
20352
|
try {
|
|
20259
20353
|
const whereParams = [toFtsQuery(query.terms)];
|
|
20354
|
+
const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
20260
20355
|
const whereClauses = ["memories_fts MATCH ?"];
|
|
20261
20356
|
if (query.updatedAfter) {
|
|
20262
20357
|
whereClauses.push("m.updated_at >= ?");
|
|
@@ -20266,7 +20361,7 @@ class SqliteMemoryRepository {
|
|
|
20266
20361
|
whereClauses.push("m.updated_at <= ?");
|
|
20267
20362
|
whereParams.push(query.updatedBefore.getTime());
|
|
20268
20363
|
}
|
|
20269
|
-
const params = [...whereParams,
|
|
20364
|
+
const params = [...whereParams, limit];
|
|
20270
20365
|
const statement = this.database.prepare(`
|
|
20271
20366
|
SELECT
|
|
20272
20367
|
m.id,
|
|
@@ -20293,19 +20388,21 @@ class SqliteMemoryRepository {
|
|
|
20293
20388
|
});
|
|
20294
20389
|
}
|
|
20295
20390
|
}
|
|
20296
|
-
async
|
|
20391
|
+
async get(id) {
|
|
20297
20392
|
try {
|
|
20298
|
-
const rows = this.
|
|
20393
|
+
const rows = this.getStatement.all(id);
|
|
20299
20394
|
const row = rows[0];
|
|
20300
20395
|
return row ? toMemoryRecord(row) : undefined;
|
|
20301
20396
|
} catch (error2) {
|
|
20302
20397
|
throw new PersistenceError("Failed to find memory.", { cause: error2 });
|
|
20303
20398
|
}
|
|
20304
20399
|
}
|
|
20305
|
-
async
|
|
20400
|
+
async list(options) {
|
|
20306
20401
|
try {
|
|
20307
20402
|
const whereClauses = [];
|
|
20308
20403
|
const params = [];
|
|
20404
|
+
const offset = options.offset ?? 0;
|
|
20405
|
+
const limit = options.limit ?? DEFAULT_LIST_LIMIT2;
|
|
20309
20406
|
if (options.workspace) {
|
|
20310
20407
|
whereClauses.push("workspace = ?");
|
|
20311
20408
|
params.push(options.workspace);
|
|
@@ -20313,8 +20410,8 @@ class SqliteMemoryRepository {
|
|
|
20313
20410
|
whereClauses.push("workspace IS NULL");
|
|
20314
20411
|
}
|
|
20315
20412
|
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
20316
|
-
const
|
|
20317
|
-
params.push(
|
|
20413
|
+
const queryLimit = limit + 1;
|
|
20414
|
+
params.push(queryLimit, offset);
|
|
20318
20415
|
const statement = this.database.prepare(`
|
|
20319
20416
|
SELECT id, content, workspace, created_at, updated_at
|
|
20320
20417
|
FROM memories
|
|
@@ -20323,39 +20420,39 @@ class SqliteMemoryRepository {
|
|
|
20323
20420
|
LIMIT ? OFFSET ?
|
|
20324
20421
|
`);
|
|
20325
20422
|
const rows = statement.all(...params);
|
|
20326
|
-
const hasMore = rows.length >
|
|
20327
|
-
const items = (hasMore ? rows.slice(0,
|
|
20423
|
+
const hasMore = rows.length > limit;
|
|
20424
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryRecord);
|
|
20328
20425
|
return { items, hasMore };
|
|
20329
20426
|
} catch (error2) {
|
|
20330
20427
|
throw new PersistenceError("Failed to list memories.", { cause: error2 });
|
|
20331
20428
|
}
|
|
20332
20429
|
}
|
|
20333
|
-
async update(
|
|
20430
|
+
async update(input) {
|
|
20334
20431
|
let result;
|
|
20335
20432
|
try {
|
|
20336
20433
|
const now = Date.now();
|
|
20337
|
-
result = this.updateStatement.run(content, now, id);
|
|
20434
|
+
result = this.updateStatement.run(input.content, now, input.id);
|
|
20338
20435
|
} catch (error2) {
|
|
20339
20436
|
throw new PersistenceError("Failed to update memory.", { cause: error2 });
|
|
20340
20437
|
}
|
|
20341
20438
|
if (result.changes === 0) {
|
|
20342
|
-
throw new NotFoundError(`Memory not found: ${id}`);
|
|
20439
|
+
throw new NotFoundError(`Memory not found: ${input.id}`);
|
|
20343
20440
|
}
|
|
20344
|
-
const memory = await this.
|
|
20441
|
+
const memory = await this.get(input.id);
|
|
20345
20442
|
if (!memory) {
|
|
20346
|
-
throw new NotFoundError(`Memory not found after update: ${id}`);
|
|
20443
|
+
throw new NotFoundError(`Memory not found after update: ${input.id}`);
|
|
20347
20444
|
}
|
|
20348
20445
|
return memory;
|
|
20349
20446
|
}
|
|
20350
|
-
async delete(
|
|
20447
|
+
async delete(input) {
|
|
20351
20448
|
let result;
|
|
20352
20449
|
try {
|
|
20353
|
-
result = this.deleteStatement.run(id);
|
|
20450
|
+
result = this.deleteStatement.run(input.id);
|
|
20354
20451
|
} catch (error2) {
|
|
20355
20452
|
throw new PersistenceError("Failed to delete memory.", { cause: error2 });
|
|
20356
20453
|
}
|
|
20357
20454
|
if (result.changes === 0) {
|
|
20358
|
-
throw new NotFoundError(`Memory not found: ${id}`);
|
|
20455
|
+
throw new NotFoundError(`Memory not found: ${input.id}`);
|
|
20359
20456
|
}
|
|
20360
20457
|
}
|
|
20361
20458
|
async listWorkspaces() {
|
|
@@ -20375,16 +20472,13 @@ var toMemoryRecord = (row) => ({
|
|
|
20375
20472
|
updatedAt: new Date(row.updated_at)
|
|
20376
20473
|
});
|
|
20377
20474
|
var toFtsQuery = (terms) => terms.map(toFtsTerm).join(" OR ");
|
|
20378
|
-
|
|
20475
|
+
function toFtsTerm(term) {
|
|
20379
20476
|
const escaped = term.replaceAll('"', '""');
|
|
20380
20477
|
if (term.includes(" ")) {
|
|
20381
20478
|
return `"${escaped}"`;
|
|
20382
20479
|
}
|
|
20383
20480
|
return `"${escaped}"*`;
|
|
20384
|
-
}
|
|
20385
|
-
|
|
20386
|
-
// src/ui/server.tsx
|
|
20387
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20481
|
+
}
|
|
20388
20482
|
|
|
20389
20483
|
// node_modules/@hono/node-server/dist/index.mjs
|
|
20390
20484
|
import { createServer as createServerHTTP } from "http";
|
|
@@ -23378,182 +23472,188 @@ function jsxDEV(tag, props, key) {
|
|
|
23378
23472
|
}
|
|
23379
23473
|
|
|
23380
23474
|
// src/ui/components/create-form.tsx
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
action: "/memories",
|
|
23386
|
-
children: [
|
|
23387
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
23388
|
-
class: "field",
|
|
23389
|
-
children: [
|
|
23390
|
-
/* @__PURE__ */ jsxDEV("label", {
|
|
23391
|
-
for: "new-content",
|
|
23392
|
-
children: "Content"
|
|
23393
|
-
}, undefined, false, undefined, this),
|
|
23394
|
-
/* @__PURE__ */ jsxDEV("textarea", {
|
|
23395
|
-
id: "new-content",
|
|
23396
|
-
name: "content",
|
|
23397
|
-
placeholder: "Fact, preference, decision...",
|
|
23398
|
-
required: true
|
|
23399
|
-
}, undefined, false, undefined, this)
|
|
23400
|
-
]
|
|
23401
|
-
}, undefined, true, undefined, this),
|
|
23402
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
23403
|
-
class: "field",
|
|
23404
|
-
children: [
|
|
23405
|
-
/* @__PURE__ */ jsxDEV("label", {
|
|
23406
|
-
for: "new-workspace",
|
|
23407
|
-
children: "Workspace (optional)"
|
|
23408
|
-
}, undefined, false, undefined, this),
|
|
23409
|
-
/* @__PURE__ */ jsxDEV("input", {
|
|
23410
|
-
id: "new-workspace",
|
|
23411
|
-
name: "workspace",
|
|
23412
|
-
type: "text",
|
|
23413
|
-
placeholder: "/path/to/project",
|
|
23414
|
-
value: workspace && workspace !== NO_WORKSPACE_FILTER ? workspace : ""
|
|
23415
|
-
}, undefined, false, undefined, this)
|
|
23416
|
-
]
|
|
23417
|
-
}, undefined, true, undefined, this),
|
|
23418
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
23419
|
-
class: "form-actions",
|
|
23420
|
-
children: /* @__PURE__ */ jsxDEV("button", {
|
|
23421
|
-
type: "submit",
|
|
23422
|
-
class: "primary",
|
|
23423
|
-
children: "Save"
|
|
23424
|
-
}, undefined, false, undefined, this)
|
|
23425
|
-
}, undefined, false, undefined, this)
|
|
23426
|
-
]
|
|
23427
|
-
}, undefined, true, undefined, this)
|
|
23428
|
-
}, undefined, false, undefined, this);
|
|
23429
|
-
|
|
23430
|
-
// src/ui/components/memory-card.tsx
|
|
23431
|
-
var MemoryCard = ({ memory, editing, showWorkspace, returnUrl }) => /* @__PURE__ */ jsxDEV("div", {
|
|
23432
|
-
class: "card",
|
|
23433
|
-
children: [
|
|
23434
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
23435
|
-
class: "card-header",
|
|
23436
|
-
children: /* @__PURE__ */ jsxDEV("div", {
|
|
23437
|
-
class: "card-meta",
|
|
23438
|
-
children: [
|
|
23439
|
-
showWorkspace && memory.workspace && /* @__PURE__ */ jsxDEV(Fragment, {
|
|
23440
|
-
children: [
|
|
23441
|
-
/* @__PURE__ */ jsxDEV("span", {
|
|
23442
|
-
class: "badge",
|
|
23443
|
-
children: memory.workspace
|
|
23444
|
-
}, undefined, false, undefined, this),
|
|
23445
|
-
" "
|
|
23446
|
-
]
|
|
23447
|
-
}, undefined, true, undefined, this),
|
|
23448
|
-
memory.updatedAt.toLocaleString()
|
|
23449
|
-
]
|
|
23450
|
-
}, undefined, true, undefined, this)
|
|
23451
|
-
}, undefined, false, undefined, this),
|
|
23452
|
-
editing ? /* @__PURE__ */ jsxDEV("form", {
|
|
23475
|
+
function CreateForm({ workspace }) {
|
|
23476
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
23477
|
+
class: "form-card",
|
|
23478
|
+
children: /* @__PURE__ */ jsxDEV("form", {
|
|
23453
23479
|
method: "post",
|
|
23454
|
-
action:
|
|
23480
|
+
action: "/memories",
|
|
23455
23481
|
children: [
|
|
23456
|
-
/* @__PURE__ */ jsxDEV("input", {
|
|
23457
|
-
type: "hidden",
|
|
23458
|
-
name: "returnUrl",
|
|
23459
|
-
value: returnUrl
|
|
23460
|
-
}, undefined, false, undefined, this),
|
|
23461
|
-
/* @__PURE__ */ jsxDEV("textarea", {
|
|
23462
|
-
name: "content",
|
|
23463
|
-
required: true,
|
|
23464
|
-
children: memory.content
|
|
23465
|
-
}, undefined, false, undefined, this),
|
|
23466
23482
|
/* @__PURE__ */ jsxDEV("div", {
|
|
23467
|
-
class: "
|
|
23483
|
+
class: "field",
|
|
23468
23484
|
children: [
|
|
23469
|
-
/* @__PURE__ */ jsxDEV("
|
|
23470
|
-
|
|
23471
|
-
|
|
23472
|
-
children: "Save"
|
|
23485
|
+
/* @__PURE__ */ jsxDEV("label", {
|
|
23486
|
+
for: "new-content",
|
|
23487
|
+
children: "Content"
|
|
23473
23488
|
}, undefined, false, undefined, this),
|
|
23474
|
-
/* @__PURE__ */ jsxDEV("
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23489
|
+
/* @__PURE__ */ jsxDEV("textarea", {
|
|
23490
|
+
id: "new-content",
|
|
23491
|
+
name: "content",
|
|
23492
|
+
placeholder: "Fact, preference, decision...",
|
|
23493
|
+
required: true
|
|
23478
23494
|
}, undefined, false, undefined, this)
|
|
23479
23495
|
]
|
|
23480
|
-
}, undefined, true, undefined, this)
|
|
23481
|
-
]
|
|
23482
|
-
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
|
|
23483
|
-
children: [
|
|
23484
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
23485
|
-
class: "card-content",
|
|
23486
|
-
children: memory.content
|
|
23487
|
-
}, undefined, false, undefined, this),
|
|
23496
|
+
}, undefined, true, undefined, this),
|
|
23488
23497
|
/* @__PURE__ */ jsxDEV("div", {
|
|
23489
|
-
class: "
|
|
23498
|
+
class: "field",
|
|
23490
23499
|
children: [
|
|
23491
|
-
/* @__PURE__ */ jsxDEV("
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
children: "Edit"
|
|
23500
|
+
/* @__PURE__ */ jsxDEV("label", {
|
|
23501
|
+
for: "new-workspace",
|
|
23502
|
+
children: "Workspace (optional)"
|
|
23495
23503
|
}, undefined, false, undefined, this),
|
|
23496
|
-
/* @__PURE__ */ jsxDEV("
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
|
|
23504
|
+
/* @__PURE__ */ jsxDEV("input", {
|
|
23505
|
+
id: "new-workspace",
|
|
23506
|
+
name: "workspace",
|
|
23507
|
+
type: "text",
|
|
23508
|
+
placeholder: "/path/to/project",
|
|
23509
|
+
value: workspace && workspace !== NO_WORKSPACE_FILTER ? workspace : ""
|
|
23510
|
+
}, undefined, false, undefined, this)
|
|
23511
|
+
]
|
|
23512
|
+
}, undefined, true, undefined, this),
|
|
23513
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
23514
|
+
class: "form-actions",
|
|
23515
|
+
children: /* @__PURE__ */ jsxDEV("button", {
|
|
23516
|
+
type: "submit",
|
|
23517
|
+
class: "primary",
|
|
23518
|
+
children: "Save"
|
|
23519
|
+
}, undefined, false, undefined, this)
|
|
23520
|
+
}, undefined, false, undefined, this)
|
|
23521
|
+
]
|
|
23522
|
+
}, undefined, true, undefined, this)
|
|
23523
|
+
}, undefined, false, undefined, this);
|
|
23524
|
+
}
|
|
23525
|
+
|
|
23526
|
+
// src/ui/components/memory-card.tsx
|
|
23527
|
+
function MemoryCard({ memory, editing, showWorkspace, returnUrl }) {
|
|
23528
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
23529
|
+
class: "card",
|
|
23530
|
+
children: [
|
|
23531
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
23532
|
+
class: "card-header",
|
|
23533
|
+
children: /* @__PURE__ */ jsxDEV("div", {
|
|
23534
|
+
class: "card-meta",
|
|
23535
|
+
children: [
|
|
23536
|
+
showWorkspace && memory.workspace && /* @__PURE__ */ jsxDEV(Fragment, {
|
|
23500
23537
|
children: [
|
|
23501
|
-
/* @__PURE__ */ jsxDEV("
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
value: returnUrl
|
|
23538
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
23539
|
+
class: "badge",
|
|
23540
|
+
children: memory.workspace
|
|
23505
23541
|
}, undefined, false, undefined, this),
|
|
23506
|
-
|
|
23507
|
-
type: "submit",
|
|
23508
|
-
class: "danger",
|
|
23509
|
-
children: "Delete"
|
|
23510
|
-
}, undefined, false, undefined, this)
|
|
23542
|
+
" "
|
|
23511
23543
|
]
|
|
23512
|
-
}, undefined, true, undefined, this)
|
|
23544
|
+
}, undefined, true, undefined, this),
|
|
23545
|
+
memory.updatedAt.toLocaleString()
|
|
23513
23546
|
]
|
|
23514
23547
|
}, undefined, true, undefined, this)
|
|
23515
|
-
|
|
23516
|
-
|
|
23517
|
-
|
|
23518
|
-
|
|
23548
|
+
}, undefined, false, undefined, this),
|
|
23549
|
+
editing ? /* @__PURE__ */ jsxDEV("form", {
|
|
23550
|
+
method: "post",
|
|
23551
|
+
action: `/memories/${encodeURIComponent(memory.id)}/update`,
|
|
23552
|
+
children: [
|
|
23553
|
+
/* @__PURE__ */ jsxDEV("input", {
|
|
23554
|
+
type: "hidden",
|
|
23555
|
+
name: "returnUrl",
|
|
23556
|
+
value: returnUrl
|
|
23557
|
+
}, undefined, false, undefined, this),
|
|
23558
|
+
/* @__PURE__ */ jsxDEV("textarea", {
|
|
23559
|
+
name: "content",
|
|
23560
|
+
required: true,
|
|
23561
|
+
children: memory.content
|
|
23562
|
+
}, undefined, false, undefined, this),
|
|
23563
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
23564
|
+
class: "card-actions",
|
|
23565
|
+
children: [
|
|
23566
|
+
/* @__PURE__ */ jsxDEV("button", {
|
|
23567
|
+
type: "submit",
|
|
23568
|
+
class: "primary",
|
|
23569
|
+
children: "Save"
|
|
23570
|
+
}, undefined, false, undefined, this),
|
|
23571
|
+
/* @__PURE__ */ jsxDEV("a", {
|
|
23572
|
+
href: returnUrl,
|
|
23573
|
+
class: "btn",
|
|
23574
|
+
children: "Cancel"
|
|
23575
|
+
}, undefined, false, undefined, this)
|
|
23576
|
+
]
|
|
23577
|
+
}, undefined, true, undefined, this)
|
|
23578
|
+
]
|
|
23579
|
+
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
|
|
23580
|
+
children: [
|
|
23581
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
23582
|
+
class: "card-content",
|
|
23583
|
+
children: memory.content
|
|
23584
|
+
}, undefined, false, undefined, this),
|
|
23585
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
23586
|
+
class: "card-actions",
|
|
23587
|
+
children: [
|
|
23588
|
+
/* @__PURE__ */ jsxDEV("a", {
|
|
23589
|
+
href: `${returnUrl}${returnUrl.includes("?") ? "&" : "?"}edit=${encodeURIComponent(memory.id)}`,
|
|
23590
|
+
class: "btn",
|
|
23591
|
+
children: "Edit"
|
|
23592
|
+
}, undefined, false, undefined, this),
|
|
23593
|
+
/* @__PURE__ */ jsxDEV("form", {
|
|
23594
|
+
method: "post",
|
|
23595
|
+
action: `/memories/${encodeURIComponent(memory.id)}/delete`,
|
|
23596
|
+
onsubmit: "return confirm('Delete this memory?')",
|
|
23597
|
+
children: [
|
|
23598
|
+
/* @__PURE__ */ jsxDEV("input", {
|
|
23599
|
+
type: "hidden",
|
|
23600
|
+
name: "returnUrl",
|
|
23601
|
+
value: returnUrl
|
|
23602
|
+
}, undefined, false, undefined, this),
|
|
23603
|
+
/* @__PURE__ */ jsxDEV("button", {
|
|
23604
|
+
type: "submit",
|
|
23605
|
+
class: "danger",
|
|
23606
|
+
children: "Delete"
|
|
23607
|
+
}, undefined, false, undefined, this)
|
|
23608
|
+
]
|
|
23609
|
+
}, undefined, true, undefined, this)
|
|
23610
|
+
]
|
|
23611
|
+
}, undefined, true, undefined, this)
|
|
23612
|
+
]
|
|
23613
|
+
}, undefined, true, undefined, this)
|
|
23614
|
+
]
|
|
23615
|
+
}, undefined, true, undefined, this);
|
|
23616
|
+
}
|
|
23519
23617
|
|
|
23520
23618
|
// src/ui/components/sidebar.tsx
|
|
23521
|
-
|
|
23522
|
-
|
|
23523
|
-
|
|
23524
|
-
|
|
23525
|
-
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23535
|
-
|
|
23536
|
-
|
|
23537
|
-
|
|
23538
|
-
|
|
23539
|
-
|
|
23540
|
-
|
|
23541
|
-
|
|
23542
|
-
children:
|
|
23543
|
-
|
|
23544
|
-
|
|
23545
|
-
|
|
23546
|
-
|
|
23619
|
+
function Sidebar({ workspaces, selected }) {
|
|
23620
|
+
return /* @__PURE__ */ jsxDEV("nav", {
|
|
23621
|
+
class: "sidebar",
|
|
23622
|
+
children: [
|
|
23623
|
+
/* @__PURE__ */ jsxDEV("h2", {
|
|
23624
|
+
children: "Workspaces"
|
|
23625
|
+
}, undefined, false, undefined, this),
|
|
23626
|
+
/* @__PURE__ */ jsxDEV("a", {
|
|
23627
|
+
href: "/",
|
|
23628
|
+
class: `sidebar-item all-item${selected === null ? " active" : ""}`,
|
|
23629
|
+
children: "All"
|
|
23630
|
+
}, undefined, false, undefined, this),
|
|
23631
|
+
/* @__PURE__ */ jsxDEV("a", {
|
|
23632
|
+
href: `/?workspace=${NO_WORKSPACE_FILTER}`,
|
|
23633
|
+
class: `sidebar-item no-ws-item${selected === NO_WORKSPACE_FILTER ? " active" : ""}`,
|
|
23634
|
+
children: "No workspace"
|
|
23635
|
+
}, undefined, false, undefined, this),
|
|
23636
|
+
workspaces.map((ws) => /* @__PURE__ */ jsxDEV("a", {
|
|
23637
|
+
href: `/?workspace=${encodeURIComponent(ws)}`,
|
|
23638
|
+
class: `sidebar-item ws-item${selected === ws ? " active" : ""}`,
|
|
23639
|
+
title: ws,
|
|
23640
|
+
children: /* @__PURE__ */ jsxDEV("span", {
|
|
23641
|
+
children: ws.replace(/\/$/, "")
|
|
23642
|
+
}, undefined, false, undefined, this)
|
|
23643
|
+
}, undefined, false, undefined, this))
|
|
23644
|
+
]
|
|
23645
|
+
}, undefined, true, undefined, this);
|
|
23646
|
+
}
|
|
23547
23647
|
|
|
23548
23648
|
// src/ui/components/page.tsx
|
|
23549
|
-
|
|
23649
|
+
function buildUrl(base, overrides) {
|
|
23550
23650
|
const url = new URL(base, "http://localhost");
|
|
23551
23651
|
for (const [key, value] of Object.entries(overrides)) {
|
|
23552
23652
|
url.searchParams.set(key, value);
|
|
23553
23653
|
}
|
|
23554
23654
|
return `${url.pathname}${url.search}`;
|
|
23555
|
-
}
|
|
23556
|
-
|
|
23655
|
+
}
|
|
23656
|
+
function Page({
|
|
23557
23657
|
memories,
|
|
23558
23658
|
workspaces,
|
|
23559
23659
|
selectedWorkspace,
|
|
@@ -23561,7 +23661,7 @@ var Page = ({
|
|
|
23561
23661
|
currentPage,
|
|
23562
23662
|
hasMore,
|
|
23563
23663
|
showCreate
|
|
23564
|
-
})
|
|
23664
|
+
}) {
|
|
23565
23665
|
const params = new URLSearchParams;
|
|
23566
23666
|
if (selectedWorkspace)
|
|
23567
23667
|
params.set("workspace", selectedWorkspace);
|
|
@@ -23680,27 +23780,26 @@ var Page = ({
|
|
|
23680
23780
|
}, undefined, false, undefined, this)
|
|
23681
23781
|
]
|
|
23682
23782
|
}, undefined, true, undefined, this);
|
|
23683
|
-
}
|
|
23783
|
+
}
|
|
23684
23784
|
|
|
23685
|
-
// src/ui/
|
|
23686
|
-
var
|
|
23687
|
-
|
|
23688
|
-
var startWebServer = (repository, options) => {
|
|
23785
|
+
// src/ui/routes/page-routes.tsx
|
|
23786
|
+
var DEFAULT_LIST_LIMIT3 = 15;
|
|
23787
|
+
function createPageRoutes(memory) {
|
|
23689
23788
|
const app = new Hono2;
|
|
23690
|
-
|
|
23789
|
+
async function renderPage(c) {
|
|
23691
23790
|
const workspace = c.req.query("workspace") ?? null;
|
|
23692
23791
|
const pageNum = Math.max(Number(c.req.query("page")) || 1, 1);
|
|
23693
23792
|
const editingId = c.req.query("edit") ?? null;
|
|
23694
23793
|
const showCreate = c.req.query("create") === "1";
|
|
23695
23794
|
const isNoWorkspace = workspace === NO_WORKSPACE_FILTER;
|
|
23696
23795
|
const wsFilter = workspace && !isNoWorkspace ? workspace : undefined;
|
|
23697
|
-
const page = await
|
|
23796
|
+
const page = await memory.list({
|
|
23698
23797
|
workspace: wsFilter,
|
|
23699
23798
|
workspaceIsNull: isNoWorkspace,
|
|
23700
|
-
offset: (pageNum - 1) *
|
|
23701
|
-
limit:
|
|
23799
|
+
offset: (pageNum - 1) * DEFAULT_LIST_LIMIT3,
|
|
23800
|
+
limit: DEFAULT_LIST_LIMIT3
|
|
23702
23801
|
});
|
|
23703
|
-
const workspaces = await
|
|
23802
|
+
const workspaces = await memory.listWorkspaces();
|
|
23704
23803
|
return c.html(/* @__PURE__ */ jsxDEV(Page, {
|
|
23705
23804
|
memories: page.items,
|
|
23706
23805
|
workspaces,
|
|
@@ -23710,132 +23809,83 @@ var startWebServer = (repository, options) => {
|
|
|
23710
23809
|
hasMore: page.hasMore,
|
|
23711
23810
|
showCreate
|
|
23712
23811
|
}, undefined, false, undefined, this));
|
|
23713
|
-
}
|
|
23714
|
-
|
|
23812
|
+
}
|
|
23813
|
+
async function createMemory(c) {
|
|
23715
23814
|
const form2 = await c.req.parseBody();
|
|
23716
|
-
const content = typeof form2.content === "string" ? form2.content
|
|
23717
|
-
const workspace = typeof form2.workspace === "string" ? form2.workspace
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
|
|
23815
|
+
const content = typeof form2.content === "string" ? form2.content : "";
|
|
23816
|
+
const workspace = typeof form2.workspace === "string" ? form2.workspace : undefined;
|
|
23817
|
+
try {
|
|
23818
|
+
await memory.create({ content, workspace });
|
|
23819
|
+
} catch (error2) {
|
|
23820
|
+
if (!(error2 instanceof ValidationError)) {
|
|
23821
|
+
throw error2;
|
|
23822
|
+
}
|
|
23721
23823
|
}
|
|
23722
|
-
const wsParam = workspace ? `/?workspace=${encodeURIComponent(workspace)}` : "/";
|
|
23824
|
+
const wsParam = workspace?.trim() ? `/?workspace=${encodeURIComponent(workspace.trim())}` : "/";
|
|
23723
23825
|
return c.redirect(wsParam);
|
|
23724
|
-
}
|
|
23725
|
-
|
|
23826
|
+
}
|
|
23827
|
+
async function updateMemory(c) {
|
|
23726
23828
|
const form2 = await c.req.parseBody();
|
|
23727
|
-
const content = typeof form2.content === "string" ? form2.content
|
|
23829
|
+
const content = typeof form2.content === "string" ? form2.content : "";
|
|
23728
23830
|
const returnUrl = safeReturnUrl(form2.returnUrl);
|
|
23729
|
-
|
|
23730
|
-
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
}
|
|
23831
|
+
const id = c.req.param("id") ?? "";
|
|
23832
|
+
try {
|
|
23833
|
+
await memory.update({ id, content });
|
|
23834
|
+
} catch (error2) {
|
|
23835
|
+
if (!(error2 instanceof NotFoundError) && !(error2 instanceof ValidationError))
|
|
23836
|
+
throw error2;
|
|
23736
23837
|
}
|
|
23737
23838
|
return c.redirect(returnUrl);
|
|
23738
|
-
}
|
|
23739
|
-
|
|
23839
|
+
}
|
|
23840
|
+
async function deleteMemory(c) {
|
|
23740
23841
|
const form2 = await c.req.parseBody();
|
|
23741
23842
|
const returnUrl = safeReturnUrl(form2.returnUrl);
|
|
23843
|
+
const id = c.req.param("id") ?? "";
|
|
23742
23844
|
try {
|
|
23743
|
-
await
|
|
23845
|
+
await memory.delete({ id });
|
|
23744
23846
|
} catch (error2) {
|
|
23745
23847
|
if (!(error2 instanceof NotFoundError))
|
|
23746
23848
|
throw error2;
|
|
23747
23849
|
}
|
|
23748
23850
|
return c.redirect(returnUrl);
|
|
23749
|
-
}
|
|
23750
|
-
app.get("/
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
app
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
const limit = Math.min(Math.max(Number(limitParam) || DEFAULT_LIST_LIMIT, 1), MAX_LIST_LIMIT);
|
|
23758
|
-
const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
|
|
23759
|
-
const page = await repository.findAll({ workspace, offset, limit });
|
|
23760
|
-
return c.json({ items: page.items.map(toMemoryJson), hasMore: page.hasMore });
|
|
23761
|
-
});
|
|
23762
|
-
app.get("/api/memories/:id", async (c) => {
|
|
23763
|
-
const memory = await repository.findById(c.req.param("id"));
|
|
23764
|
-
if (!memory)
|
|
23765
|
-
return c.json({ error: "Memory not found." }, 404);
|
|
23766
|
-
return c.json(toMemoryJson(memory));
|
|
23767
|
-
});
|
|
23768
|
-
app.post("/api/memories", async (c) => {
|
|
23769
|
-
const body = await c.req.json().catch(() => null);
|
|
23770
|
-
if (!body)
|
|
23771
|
-
return c.json({ error: "Invalid JSON." }, 400);
|
|
23772
|
-
const content = typeof body.content === "string" ? body.content.trim() : "";
|
|
23773
|
-
if (!content)
|
|
23774
|
-
return c.json({ error: "Content is required." }, 400);
|
|
23775
|
-
const workspace = typeof body.workspace === "string" ? body.workspace.trim() || undefined : undefined;
|
|
23776
|
-
const now = new Date;
|
|
23777
|
-
const memory = await repository.save({ id: randomUUID2(), content, workspace, createdAt: now, updatedAt: now });
|
|
23778
|
-
return c.json(toMemoryJson(memory), 201);
|
|
23779
|
-
});
|
|
23780
|
-
app.patch("/api/memories/:id", async (c) => {
|
|
23781
|
-
const body = await c.req.json().catch(() => null);
|
|
23782
|
-
if (!body)
|
|
23783
|
-
return c.json({ error: "Invalid JSON." }, 400);
|
|
23784
|
-
const content = typeof body.content === "string" ? body.content.trim() : "";
|
|
23785
|
-
if (!content)
|
|
23786
|
-
return c.json({ error: "Content is required." }, 400);
|
|
23787
|
-
try {
|
|
23788
|
-
const updated = await repository.update(c.req.param("id"), content);
|
|
23789
|
-
return c.json(toMemoryJson(updated));
|
|
23790
|
-
} catch (error2) {
|
|
23791
|
-
if (error2 instanceof NotFoundError)
|
|
23792
|
-
return c.json({ error: "Memory not found." }, 404);
|
|
23793
|
-
throw error2;
|
|
23794
|
-
}
|
|
23795
|
-
});
|
|
23796
|
-
app.delete("/api/memories/:id", async (c) => {
|
|
23797
|
-
try {
|
|
23798
|
-
await repository.delete(c.req.param("id"));
|
|
23799
|
-
return c.body(null, 204);
|
|
23800
|
-
} catch (error2) {
|
|
23801
|
-
if (error2 instanceof NotFoundError)
|
|
23802
|
-
return c.json({ error: "Memory not found." }, 404);
|
|
23803
|
-
throw error2;
|
|
23804
|
-
}
|
|
23805
|
-
});
|
|
23806
|
-
return serve({ fetch: app.fetch, port: options.port });
|
|
23807
|
-
};
|
|
23808
|
-
var safeReturnUrl = (value) => {
|
|
23851
|
+
}
|
|
23852
|
+
app.get("/", renderPage);
|
|
23853
|
+
app.post("/memories", createMemory);
|
|
23854
|
+
app.post("/memories/:id/update", updateMemory);
|
|
23855
|
+
app.post("/memories/:id/delete", deleteMemory);
|
|
23856
|
+
return app;
|
|
23857
|
+
}
|
|
23858
|
+
function safeReturnUrl(value) {
|
|
23809
23859
|
if (typeof value === "string" && value.startsWith("/"))
|
|
23810
23860
|
return value;
|
|
23811
23861
|
return "/";
|
|
23812
|
-
}
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
}
|
|
23862
|
+
}
|
|
23863
|
+
|
|
23864
|
+
// src/ui/server.tsx
|
|
23865
|
+
function startWebServer(memory, options) {
|
|
23866
|
+
const app = new Hono2;
|
|
23867
|
+
app.route("/", createPageRoutes(memory));
|
|
23868
|
+
return serve({ fetch: app.fetch, port: options.port });
|
|
23869
|
+
}
|
|
23820
23870
|
|
|
23821
23871
|
// src/index.ts
|
|
23822
23872
|
var config2 = resolveConfig();
|
|
23823
23873
|
var database = openMemoryDatabase(config2.databasePath);
|
|
23824
23874
|
var repository = new SqliteMemoryRepository(database);
|
|
23875
|
+
var memoryService = new MemoryService(repository);
|
|
23825
23876
|
if (config2.uiMode) {
|
|
23826
|
-
|
|
23827
|
-
const addr = server.address();
|
|
23828
|
-
const port = typeof addr === "object" && addr ? addr.port : config2.uiPort;
|
|
23829
|
-
console.log(`agent-memory UI running at http://localhost:${port}`);
|
|
23830
|
-
const shutdown = () => {
|
|
23877
|
+
let shutdown = function() {
|
|
23831
23878
|
server.close();
|
|
23832
23879
|
database.close();
|
|
23833
23880
|
process.exit(0);
|
|
23834
23881
|
};
|
|
23882
|
+
const server = startWebServer(memoryService, { port: config2.uiPort });
|
|
23883
|
+
const addr = server.address();
|
|
23884
|
+
const port = typeof addr === "object" && addr ? addr.port : config2.uiPort;
|
|
23885
|
+
console.log(`agent-memory UI running at http://localhost:${port}`);
|
|
23835
23886
|
process.on("SIGINT", shutdown);
|
|
23836
23887
|
process.on("SIGTERM", shutdown);
|
|
23837
23888
|
} else {
|
|
23838
|
-
const memoryService = new MemoryService(repository);
|
|
23839
23889
|
const server = createMcpServer(memoryService, version2);
|
|
23840
23890
|
const transport = new StdioServerTransport;
|
|
23841
23891
|
try {
|