th-memory-mcp 2.2.8 → 2.3.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/ARCHITECTURE_v2.md +1 -1
- package/README.md +54 -3
- package/README.th.md +51 -1
- package/SECURITY.md +9 -0
- package/design.md +1 -1
- package/dist/cli/commands.js +507 -0
- package/dist/cli.js +4 -0
- package/dist/core/consolidation-engine.js +119 -2
- package/dist/core/graph-engine.js +11 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrations.js +47 -12
- package/dist/index.js +1 -1
- package/dist/lib/config.js +3 -1
- package/dist/lib/highlight.js +139 -0
- package/dist/lib/iso.js +11 -0
- package/dist/lib/memory-format.js +53 -0
- package/dist/memory/deduplicator.js +24 -9
- package/dist/retrieval/fts.js +17 -26
- package/dist/tools/consolidate.js +47 -21
- package/dist/tools/context.js +6 -2
- package/dist/tools/export_memory.js +43 -0
- package/dist/tools/forget.js +31 -4
- package/dist/tools/import_memory.js +404 -88
- package/dist/tools/merge_memory.js +14 -0
- package/dist/tools/profile.js +16 -3
- package/dist/tools/recall.js +3 -1
- package/dist/tools/update_memory.js +29 -3
- package/package.json +5 -3
- package/dist/db.js +0 -111
package/dist/tools/recall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db, buildFtsMatch, escapeLike, truncate, getAllEmbeddings, ok, err, } from "../db/index.js";
|
|
3
3
|
import { embed, cosine, deserialize } from "../lib/embed.js";
|
|
4
|
+
import { wrapMemoryReference } from "../lib/memory-format.js";
|
|
4
5
|
export const recallInput = {
|
|
5
6
|
topic: z.string().min(1).max(500).describe("Topic to recall from memory"),
|
|
6
7
|
limit: z
|
|
@@ -121,7 +122,8 @@ export async function recallHandler(args) {
|
|
|
121
122
|
if (parts.length === 0) {
|
|
122
123
|
return ok(`no memory found for "${truncate(args.topic, 100)}"`);
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
+
// Batch B-2: recalled memory is reference data, not instructions.
|
|
126
|
+
return ok(wrapMemoryReference(truncate(parts.join("\n\n"), RECALL_BUDGET)));
|
|
125
127
|
}
|
|
126
128
|
catch (e) {
|
|
127
129
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { db, nowISO, ok, err, } from "../db/index.js";
|
|
3
3
|
import { getMemoryById, createMemory, syncMemoryIndex, } from "../db/repositories/memories.js";
|
|
4
4
|
import { supersede } from "../core/lifecycle-engine.js";
|
|
5
|
+
import { isoDateTimeSchema, isIsoDateString } from "../lib/iso.js";
|
|
5
6
|
export const updateMemoryInput = {
|
|
6
7
|
id: z.number().int().describe("Memory id to update"),
|
|
7
8
|
content: z
|
|
@@ -12,9 +13,12 @@ export const updateMemoryInput = {
|
|
|
12
13
|
summary: z.string().max(2000).nullable().optional().describe("New summary"),
|
|
13
14
|
importance: z.number().min(0).max(1).optional(),
|
|
14
15
|
confidence: z.number().min(0).max(1).optional(),
|
|
16
|
+
validFrom: z
|
|
17
|
+
.union([isoDateTimeSchema, z.null()])
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("ISO timestamp or null to clear"),
|
|
15
20
|
validUntil: z
|
|
16
|
-
.
|
|
17
|
-
.nullable()
|
|
21
|
+
.union([isoDateTimeSchema, z.null()])
|
|
18
22
|
.optional()
|
|
19
23
|
.describe("ISO timestamp or null to clear"),
|
|
20
24
|
metadata: z.unknown().optional().describe("New metadata object (replaces)"),
|
|
@@ -30,6 +34,24 @@ export function updateMemoryHandler(args) {
|
|
|
30
34
|
return err(`memory ${args.id} not found`);
|
|
31
35
|
if (mem.status === "deleted")
|
|
32
36
|
return err("cannot update a deleted memory");
|
|
37
|
+
if (args.validFrom !== undefined &&
|
|
38
|
+
args.validFrom !== null &&
|
|
39
|
+
!isIsoDateString(args.validFrom)) {
|
|
40
|
+
return err(`validFrom must be a full ISO datetime with timezone offset (e.g. 2024-01-01T00:00:00.000Z), got '${args.validFrom}'`);
|
|
41
|
+
}
|
|
42
|
+
if (args.validUntil !== undefined &&
|
|
43
|
+
args.validUntil !== null &&
|
|
44
|
+
!isIsoDateString(args.validUntil)) {
|
|
45
|
+
return err(`validUntil must be a full ISO datetime with timezone offset (e.g. 2024-01-01T00:00:00.000Z), got '${args.validUntil}'`);
|
|
46
|
+
}
|
|
47
|
+
// Effective range covers new values and values mixed with the existing row.
|
|
48
|
+
const effectiveValidFrom = args.validFrom !== undefined ? args.validFrom : mem.valid_from;
|
|
49
|
+
const effectiveValidUntil = args.validUntil !== undefined ? args.validUntil : mem.valid_until;
|
|
50
|
+
if (effectiveValidFrom != null &&
|
|
51
|
+
effectiveValidUntil != null &&
|
|
52
|
+
new Date(effectiveValidFrom) > new Date(effectiveValidUntil)) {
|
|
53
|
+
return err(`validFrom (${effectiveValidFrom}) must not be later than validUntil (${effectiveValidUntil})`);
|
|
54
|
+
}
|
|
33
55
|
const supersedeContent = args.content !== undefined && (args.supersede ?? true);
|
|
34
56
|
if (supersedeContent) {
|
|
35
57
|
const externalId = mem.user_id
|
|
@@ -48,7 +70,7 @@ export function updateMemoryHandler(args) {
|
|
|
48
70
|
projectId: mem.project_id,
|
|
49
71
|
sessionId: mem.session_id,
|
|
50
72
|
userId: externalId,
|
|
51
|
-
validFrom: mem.valid_from,
|
|
73
|
+
validFrom: args.validFrom !== undefined ? args.validFrom : mem.valid_from,
|
|
52
74
|
validUntil: args.validUntil !== undefined ? args.validUntil : mem.valid_until,
|
|
53
75
|
metadata: args.metadata !== undefined
|
|
54
76
|
? args.metadata
|
|
@@ -73,6 +95,10 @@ export function updateMemoryHandler(args) {
|
|
|
73
95
|
sets.push("confidence = ?");
|
|
74
96
|
vals.push(args.confidence);
|
|
75
97
|
}
|
|
98
|
+
if (args.validFrom !== undefined) {
|
|
99
|
+
sets.push("valid_from = ?");
|
|
100
|
+
vals.push(args.validFrom);
|
|
101
|
+
}
|
|
76
102
|
if (args.validUntil !== undefined) {
|
|
77
103
|
sets.push("valid_until = ?");
|
|
78
104
|
vals.push(args.validUntil);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "th-memory-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"mcpName": "io.github.worakorn-prince/th-memory-mcp",
|
|
5
5
|
"description": "Adaptive Memory MCP server - SQLite-backed memory for OpenCode",
|
|
6
6
|
"author": "worakorn-prince",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "dist/index.js",
|
|
14
14
|
"bin": {
|
|
15
|
-
"th-memory-mcp": "dist/index.js"
|
|
15
|
+
"th-memory-mcp": "dist/index.js",
|
|
16
|
+
"th-memory": "dist/cli.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"dist",
|
|
@@ -29,9 +30,10 @@
|
|
|
29
30
|
"prepublishOnly": "npm run version:sync && npm run build",
|
|
30
31
|
"version:sync": "node scripts/sync-version.mjs",
|
|
31
32
|
"version:check": "node scripts/sync-version.mjs --check",
|
|
33
|
+
"check:capture-sync": "node scripts/check-capture-sync.mjs",
|
|
32
34
|
"start": "node dist/index.js",
|
|
33
35
|
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
34
|
-
"test": "npm run build && node --test test/*.test.mjs",
|
|
36
|
+
"test": "npm run build && node --test test/*.test.mjs test/smoke.mjs",
|
|
35
37
|
"distill": "node dist/distill.js",
|
|
36
38
|
"quickstart": "npm run build && node scripts/quickstart.mjs",
|
|
37
39
|
"benchmark": "node repro/run.mjs",
|
package/dist/db.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { truncate } from "./lib/capture-core.js";
|
|
5
|
-
import { serialize } from "./lib/embed.js";
|
|
6
|
-
import { DEFAULT_DB_PATH } from "./lib/config.js";
|
|
7
|
-
import { runMigrations } from "./migrations.js";
|
|
8
|
-
export const DB_PATH = process.env.MEMORY_DB_PATH ?? DEFAULT_DB_PATH;
|
|
9
|
-
function initDb() {
|
|
10
|
-
try {
|
|
11
|
-
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
12
|
-
return new Database(DB_PATH);
|
|
13
|
-
}
|
|
14
|
-
catch (e) {
|
|
15
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
16
|
-
console.error(`[th-memory-mcp] cannot open DB at ${DB_PATH}: ${msg}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export const db = initDb();
|
|
21
|
-
db.pragma("journal_mode = WAL");
|
|
22
|
-
db.pragma("busy_timeout = 5000");
|
|
23
|
-
db.exec(`
|
|
24
|
-
CREATE TABLE IF NOT EXISTS interactions (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
-
ts TEXT NOT NULL,
|
|
27
|
-
session_id TEXT,
|
|
28
|
-
kind TEXT NOT NULL,
|
|
29
|
-
content TEXT NOT NULL,
|
|
30
|
-
meta TEXT
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
CREATE TABLE IF NOT EXISTS preferences (
|
|
34
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
-
category TEXT NOT NULL,
|
|
36
|
-
key TEXT NOT NULL,
|
|
37
|
-
value TEXT NOT NULL,
|
|
38
|
-
confidence REAL DEFAULT 0.5,
|
|
39
|
-
source TEXT DEFAULT 'explicit',
|
|
40
|
-
updated_at TEXT NOT NULL,
|
|
41
|
-
UNIQUE(category, key)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
CREATE TABLE IF NOT EXISTS lessons (
|
|
45
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
-
situation TEXT NOT NULL,
|
|
47
|
-
mistake TEXT NOT NULL,
|
|
48
|
-
correction TEXT NOT NULL,
|
|
49
|
-
created_at TEXT NOT NULL
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
CREATE TABLE IF NOT EXISTS profile (
|
|
53
|
-
section TEXT PRIMARY KEY,
|
|
54
|
-
content TEXT NOT NULL,
|
|
55
|
-
updated_at TEXT NOT NULL
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
|
59
|
-
ref_table, ref_id, title, body
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
63
|
-
ref_table TEXT NOT NULL,
|
|
64
|
-
ref_id INTEGER NOT NULL,
|
|
65
|
-
vec BLOB NOT NULL,
|
|
66
|
-
PRIMARY KEY (ref_table, ref_id)
|
|
67
|
-
);
|
|
68
|
-
`);
|
|
69
|
-
// v2 migration engine (non-destructive: only adds new tables + backfills from v1)
|
|
70
|
-
runMigrations(db);
|
|
71
|
-
export function nowISO() {
|
|
72
|
-
return new Date().toISOString();
|
|
73
|
-
}
|
|
74
|
-
// re-exported from lib/capture-core.js (single source of truth)
|
|
75
|
-
export { truncate };
|
|
76
|
-
export function escapeLike(text) {
|
|
77
|
-
return text.replace(/[\\%_]/g, (c) => "\\" + c);
|
|
78
|
-
}
|
|
79
|
-
export function buildFtsMatch(query) {
|
|
80
|
-
const tokens = query.trim().split(/\s+/).filter(Boolean).slice(0, 8);
|
|
81
|
-
return tokens.map((t) => `"${t.replace(/"/g, "")}"`).join(" ");
|
|
82
|
-
}
|
|
83
|
-
const insertSearchIndex = db.prepare("INSERT INTO search_index (ref_table, ref_id, title, body) VALUES (?, ?, ?, ?)");
|
|
84
|
-
const deleteSearchIndex = db.prepare("DELETE FROM search_index WHERE ref_table = ? AND ref_id = ?");
|
|
85
|
-
export function syncSearchIndex(refTable, refId, title, body) {
|
|
86
|
-
deleteSearchIndex.run(refTable, refId);
|
|
87
|
-
insertSearchIndex.run(refTable, refId, title, body);
|
|
88
|
-
}
|
|
89
|
-
export function removeSearchIndex(refTable, refId) {
|
|
90
|
-
deleteSearchIndex.run(refTable, refId);
|
|
91
|
-
}
|
|
92
|
-
// --- vector embeddings (lightweight local semantic search) ---
|
|
93
|
-
const upsertEmbed = db.prepare(`INSERT INTO embeddings (ref_table, ref_id, vec) VALUES (?, ?, ?)
|
|
94
|
-
ON CONFLICT(ref_table, ref_id) DO UPDATE SET vec = excluded.vec`);
|
|
95
|
-
const deleteEmbed = db.prepare("DELETE FROM embeddings WHERE ref_table = ? AND ref_id = ?");
|
|
96
|
-
const allEmbeds = db.prepare("SELECT ref_table, ref_id, vec FROM embeddings");
|
|
97
|
-
export function upsertEmbedding(refTable, refId, vec) {
|
|
98
|
-
upsertEmbed.run(refTable, refId, serialize(vec));
|
|
99
|
-
}
|
|
100
|
-
export function removeEmbedding(refTable, refId) {
|
|
101
|
-
deleteEmbed.run(refTable, refId);
|
|
102
|
-
}
|
|
103
|
-
export function getAllEmbeddings() {
|
|
104
|
-
return allEmbeds.all();
|
|
105
|
-
}
|
|
106
|
-
export function ok(text) {
|
|
107
|
-
return { content: [{ type: "text", text }] };
|
|
108
|
-
}
|
|
109
|
-
export function err(text) {
|
|
110
|
-
return ok(`error: ${truncate(text, 300)}`);
|
|
111
|
-
}
|