@launchapp-dev/ao-memory-mcp 2.0.0 → 2.0.1

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.
@@ -0,0 +1,142 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const documentTools: ({
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: "object";
7
+ properties: {
8
+ title: {
9
+ type: string;
10
+ description: string;
11
+ };
12
+ content: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ source: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ namespace: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ mime_type: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ chunk_size: {
29
+ type: string;
30
+ description: string;
31
+ };
32
+ chunk_overlap: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ metadata: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ query?: undefined;
41
+ limit?: undefined;
42
+ alpha?: undefined;
43
+ id?: undefined;
44
+ };
45
+ required: string[];
46
+ };
47
+ } | {
48
+ name: string;
49
+ description: string;
50
+ inputSchema: {
51
+ type: "object";
52
+ properties: {
53
+ query: {
54
+ type: string;
55
+ description: string;
56
+ };
57
+ namespace: {
58
+ type: string;
59
+ description: string;
60
+ };
61
+ limit: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ alpha: {
66
+ type: string;
67
+ description: string;
68
+ };
69
+ title?: undefined;
70
+ content?: undefined;
71
+ source?: undefined;
72
+ mime_type?: undefined;
73
+ chunk_size?: undefined;
74
+ chunk_overlap?: undefined;
75
+ metadata?: undefined;
76
+ id?: undefined;
77
+ };
78
+ required: string[];
79
+ };
80
+ } | {
81
+ name: string;
82
+ description: string;
83
+ inputSchema: {
84
+ type: "object";
85
+ properties: {
86
+ namespace: {
87
+ type: string;
88
+ description: string;
89
+ };
90
+ limit: {
91
+ type: string;
92
+ description: string;
93
+ };
94
+ title?: undefined;
95
+ content?: undefined;
96
+ source?: undefined;
97
+ mime_type?: undefined;
98
+ chunk_size?: undefined;
99
+ chunk_overlap?: undefined;
100
+ metadata?: undefined;
101
+ query?: undefined;
102
+ alpha?: undefined;
103
+ id?: undefined;
104
+ };
105
+ required?: undefined;
106
+ };
107
+ } | {
108
+ name: string;
109
+ description: string;
110
+ inputSchema: {
111
+ type: "object";
112
+ properties: {
113
+ id: {
114
+ type: string;
115
+ description: string;
116
+ };
117
+ title?: undefined;
118
+ content?: undefined;
119
+ source?: undefined;
120
+ namespace?: undefined;
121
+ mime_type?: undefined;
122
+ chunk_size?: undefined;
123
+ chunk_overlap?: undefined;
124
+ metadata?: undefined;
125
+ query?: undefined;
126
+ limit?: undefined;
127
+ alpha?: undefined;
128
+ };
129
+ required: string[];
130
+ };
131
+ })[];
132
+ export declare function handleDocuments(db: Database.Database, name: string, args: any): {
133
+ content: {
134
+ type: "text";
135
+ text: string;
136
+ }[];
137
+ } | Promise<{
138
+ content: {
139
+ type: "text";
140
+ text: string;
141
+ }[];
142
+ }>;
@@ -0,0 +1,201 @@
1
+ import { now, jsonResult, errorResult, chunkText } from "../db.js";
2
+ import { embed, storeVector, deleteVector, hybridSearch } from "../embeddings.js";
3
+ export const documentTools = [
4
+ {
5
+ name: "memory.doc.ingest",
6
+ description: "Ingest a document into memory. Automatically chunks and embeds for semantic retrieval. Great for architecture docs, specs, READMEs, code files, or any reference material.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ title: { type: "string", description: "Document title" },
11
+ content: { type: "string", description: "Full document content" },
12
+ source: { type: "string", description: "File path, URL, or identifier" },
13
+ namespace: { type: "string", description: "Project/scope for this document" },
14
+ mime_type: { type: "string", description: "Content type (default: text/plain)" },
15
+ chunk_size: { type: "number", description: "Max chars per chunk (default: 1000)" },
16
+ chunk_overlap: { type: "number", description: "Overlap chars between chunks (default: 100)" },
17
+ metadata: { type: "object", description: "Custom metadata" },
18
+ },
19
+ required: ["title", "content"],
20
+ },
21
+ },
22
+ {
23
+ name: "memory.doc.search",
24
+ description: "Search across ingested documents using hybrid semantic + keyword search. Returns relevant chunks with document context.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ query: { type: "string", description: "Search query" },
29
+ namespace: { type: "string", description: "Restrict to namespace" },
30
+ limit: { type: "number", description: "Max chunks to return (default 5)" },
31
+ alpha: { type: "number", description: "Semantic vs keyword weight (default 0.6)" },
32
+ },
33
+ required: ["query"],
34
+ },
35
+ },
36
+ {
37
+ name: "memory.doc.list",
38
+ description: "List ingested documents.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ namespace: { type: "string", description: "Filter by namespace" },
43
+ limit: { type: "number", description: "Max results (default 50)" },
44
+ },
45
+ },
46
+ },
47
+ {
48
+ name: "memory.doc.get",
49
+ description: "Get a full document by ID, including all its chunks.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ id: { type: "number", description: "Document ID" },
54
+ },
55
+ required: ["id"],
56
+ },
57
+ },
58
+ {
59
+ name: "memory.doc.delete",
60
+ description: "Delete a document and all its chunks.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ id: { type: "number", description: "Document ID" },
65
+ },
66
+ required: ["id"],
67
+ },
68
+ },
69
+ ];
70
+ export function handleDocuments(db, name, args) {
71
+ if (name === "memory.doc.ingest")
72
+ return docIngest(db, args);
73
+ if (name === "memory.doc.search")
74
+ return docSearch(db, args);
75
+ if (name === "memory.doc.list")
76
+ return docList(db, args);
77
+ if (name === "memory.doc.get")
78
+ return docGet(db, args);
79
+ if (name === "memory.doc.delete")
80
+ return docDelete(db, args);
81
+ return null;
82
+ }
83
+ async function docIngest(db, args) {
84
+ const ts = now();
85
+ const chunkSize = args.chunk_size || 1000;
86
+ const chunkOverlap = args.chunk_overlap || 100;
87
+ // Check for existing doc with same source
88
+ if (args.source) {
89
+ const existing = db.prepare("SELECT id FROM documents WHERE source = ? AND namespace IS ?").get(args.source, args.namespace || null);
90
+ if (existing) {
91
+ // Re-ingest: delete old chunks
92
+ const oldChunks = db.prepare("SELECT id FROM chunks WHERE document_id = ?").all(existing.id);
93
+ for (const c of oldChunks)
94
+ deleteVector(db, "vec_chunks", c.id);
95
+ db.prepare("DELETE FROM chunks WHERE document_id = ?").run(existing.id);
96
+ db.prepare("DELETE FROM documents WHERE id = ?").run(existing.id);
97
+ }
98
+ }
99
+ const docResult = db.prepare(`
100
+ INSERT INTO documents (namespace, title, source, mime_type, content, metadata, created_at, updated_at)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
102
+ `).run(args.namespace || null, args.title, args.source || null, args.mime_type || "text/plain", args.content, JSON.stringify(args.metadata || {}), ts, ts);
103
+ const docId = Number(docResult.lastInsertRowid);
104
+ const textChunks = chunkText(args.content, chunkSize, chunkOverlap);
105
+ const insertChunk = db.prepare(`
106
+ INSERT INTO chunks (document_id, chunk_index, content, char_offset, char_length, metadata, created_at)
107
+ VALUES (?, ?, ?, ?, ?, '{}', ?)
108
+ `);
109
+ const chunkIds = [];
110
+ for (let i = 0; i < textChunks.length; i++) {
111
+ const c = textChunks[i];
112
+ const result = insertChunk.run(docId, i, c.content, c.offset, c.content.length, ts);
113
+ chunkIds.push(Number(result.lastInsertRowid));
114
+ }
115
+ // Embed all chunks
116
+ let embedded = 0;
117
+ for (let i = 0; i < textChunks.length; i++) {
118
+ try {
119
+ const embedding = await embed(textChunks[i].content);
120
+ storeVector(db, "vec_chunks", chunkIds[i], embedding);
121
+ embedded++;
122
+ }
123
+ catch (e) {
124
+ console.error(`[ao-memory] Chunk embed failed:`, e);
125
+ }
126
+ }
127
+ return jsonResult({ document_id: docId, chunks: textChunks.length, embedded });
128
+ }
129
+ async function docSearch(db, args) {
130
+ const limit = args.limit || 5;
131
+ const alpha = args.alpha ?? 0.6;
132
+ let queryEmbedding;
133
+ try {
134
+ queryEmbedding = await embed(args.query, true);
135
+ }
136
+ catch {
137
+ // FTS-only fallback
138
+ const rows = db.prepare(`
139
+ SELECT c.*, d.title as doc_title, d.source as doc_source
140
+ FROM chunks_fts f
141
+ JOIN chunks c ON c.id = f.rowid
142
+ JOIN documents d ON d.id = c.document_id
143
+ ${args.namespace ? "WHERE d.namespace = ?" : ""}
144
+ ORDER BY rank LIMIT ?
145
+ `).all(...(args.namespace ? [args.namespace, limit] : [limit]));
146
+ return jsonResult({ chunks: rows, count: rows.length, mode: "keyword_only" });
147
+ }
148
+ const results = hybridSearch(db, "chunks_fts", "vec_chunks", args.query, queryEmbedding, limit * 2, alpha);
149
+ if (results.length === 0)
150
+ return jsonResult({ chunks: [], count: 0 });
151
+ const ids = results.map(r => r.rowid);
152
+ const scoreMap = new Map(results.map(r => [r.rowid, r.score]));
153
+ const conditions = [`c.id IN (${ids.map(() => "?").join(",")})`];
154
+ const vals = [...ids];
155
+ if (args.namespace) {
156
+ conditions.push("d.namespace = ?");
157
+ vals.push(args.namespace);
158
+ }
159
+ const rows = db.prepare(`
160
+ SELECT c.*, d.title as doc_title, d.source as doc_source, d.namespace as doc_namespace
161
+ FROM chunks c
162
+ JOIN documents d ON d.id = c.document_id
163
+ WHERE ${conditions.join(" AND ")}
164
+ `).all(...vals);
165
+ const scored = rows
166
+ .map(r => ({ ...r, _score: scoreMap.get(r.id) || 0 }))
167
+ .sort((a, b) => b._score - a._score)
168
+ .slice(0, limit);
169
+ return jsonResult({ chunks: scored, count: scored.length });
170
+ }
171
+ function docList(db, args) {
172
+ const conditions = [];
173
+ const vals = [];
174
+ if (args.namespace) {
175
+ conditions.push("namespace = ?");
176
+ vals.push(args.namespace);
177
+ }
178
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
179
+ const limit = args.limit || 50;
180
+ const rows = db.prepare(`
181
+ SELECT d.*, (SELECT COUNT(*) FROM chunks c WHERE c.document_id = d.id) as chunk_count
182
+ FROM documents d ${where}
183
+ ORDER BY d.created_at DESC LIMIT ?
184
+ `).all(...vals, limit);
185
+ return jsonResult({ documents: rows, count: rows.length });
186
+ }
187
+ function docGet(db, args) {
188
+ const doc = db.prepare("SELECT * FROM documents WHERE id = ?").get(args.id);
189
+ if (!doc)
190
+ return errorResult(`Document ${args.id} not found`);
191
+ const chunks = db.prepare("SELECT * FROM chunks WHERE document_id = ? ORDER BY chunk_index").all(args.id);
192
+ return jsonResult({ ...doc, chunks });
193
+ }
194
+ function docDelete(db, args) {
195
+ const chunks = db.prepare("SELECT id FROM chunks WHERE document_id = ?").all(args.id);
196
+ for (const c of chunks)
197
+ deleteVector(db, "vec_chunks", c.id);
198
+ db.prepare("DELETE FROM chunks WHERE document_id = ?").run(args.id);
199
+ db.prepare("DELETE FROM documents WHERE id = ?").run(args.id);
200
+ return jsonResult({ deleted: true, chunks_removed: chunks.length });
201
+ }
@@ -0,0 +1,112 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const episodeTools: ({
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: "object";
7
+ properties: {
8
+ session_id: {
9
+ type: string;
10
+ description: string;
11
+ };
12
+ namespace: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ agent_role: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ role: {
21
+ type: string;
22
+ enum: string[];
23
+ description: string;
24
+ };
25
+ content: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ summary: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ metadata: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ limit?: undefined;
38
+ order?: undefined;
39
+ };
40
+ required: string[];
41
+ };
42
+ } | {
43
+ name: string;
44
+ description: string;
45
+ inputSchema: {
46
+ type: "object";
47
+ properties: {
48
+ session_id: {
49
+ type: string;
50
+ description: string;
51
+ };
52
+ namespace: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ agent_role: {
57
+ type: string;
58
+ description: string;
59
+ };
60
+ limit: {
61
+ type: string;
62
+ description: string;
63
+ };
64
+ order: {
65
+ type: string;
66
+ enum: string[];
67
+ description: string;
68
+ };
69
+ role?: undefined;
70
+ content?: undefined;
71
+ summary?: undefined;
72
+ metadata?: undefined;
73
+ };
74
+ required?: undefined;
75
+ };
76
+ } | {
77
+ name: string;
78
+ description: string;
79
+ inputSchema: {
80
+ type: "object";
81
+ properties: {
82
+ session_id: {
83
+ type: string;
84
+ description: string;
85
+ };
86
+ namespace: {
87
+ type: string;
88
+ description: string;
89
+ };
90
+ agent_role: {
91
+ type: string;
92
+ description: string;
93
+ };
94
+ summary: {
95
+ type: string;
96
+ description: string;
97
+ };
98
+ role?: undefined;
99
+ content?: undefined;
100
+ metadata?: undefined;
101
+ limit?: undefined;
102
+ order?: undefined;
103
+ };
104
+ required: string[];
105
+ };
106
+ })[];
107
+ export declare function handleEpisodes(db: Database.Database, name: string, args: any): {
108
+ content: {
109
+ type: "text";
110
+ text: string;
111
+ }[];
112
+ };
@@ -0,0 +1,98 @@
1
+ import { createHash } from "node:crypto";
2
+ import { now, jsonResult } from "../db.js";
3
+ export const episodeTools = [
4
+ {
5
+ name: "memory.episode.log",
6
+ description: "Log a conversation turn or run event to episodic memory. Use for tracking what happened during agent runs.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ session_id: { type: "string", description: "Session/run identifier" },
11
+ namespace: { type: "string", description: "Project or scope" },
12
+ agent_role: { type: "string", description: "Agent role" },
13
+ role: { type: "string", enum: ["user", "assistant", "system"], description: "Message role" },
14
+ content: { type: "string", description: "Message or event content" },
15
+ summary: { type: "string", description: "Optional short summary" },
16
+ metadata: { type: "object", description: "Custom metadata (e.g. tool calls, tokens used)" },
17
+ },
18
+ required: ["session_id", "role", "content"],
19
+ },
20
+ },
21
+ {
22
+ name: "memory.episode.list",
23
+ description: "List episodes for a session or namespace.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ session_id: { type: "string", description: "Filter by session" },
28
+ namespace: { type: "string", description: "Filter by namespace" },
29
+ agent_role: { type: "string", description: "Filter by agent role" },
30
+ limit: { type: "number", description: "Max results (default 50)" },
31
+ order: { type: "string", enum: ["newest", "oldest"], description: "Default: oldest" },
32
+ },
33
+ },
34
+ },
35
+ {
36
+ name: "memory.episode.summarize",
37
+ description: "Store a summary for a session. The calling agent provides the summary text.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ session_id: { type: "string", description: "Session to summarize" },
42
+ namespace: { type: "string", description: "Scope" },
43
+ agent_role: { type: "string", description: "Agent role" },
44
+ summary: { type: "string", description: "Summary text" },
45
+ },
46
+ required: ["session_id", "summary"],
47
+ },
48
+ },
49
+ ];
50
+ export function handleEpisodes(db, name, args) {
51
+ if (name === "memory.episode.log")
52
+ return episodeLog(db, args);
53
+ if (name === "memory.episode.list")
54
+ return episodeList(db, args);
55
+ if (name === "memory.episode.summarize")
56
+ return episodeSummarize(db, args);
57
+ return null;
58
+ }
59
+ function episodeLog(db, args) {
60
+ const ts = now();
61
+ const result = db.prepare(`
62
+ INSERT INTO episodes (session_id, namespace, agent_role, role, content, summary, metadata, created_at)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
64
+ `).run(args.session_id, args.namespace || null, args.agent_role || null, args.role, args.content, args.summary || null, JSON.stringify(args.metadata || {}), ts);
65
+ return jsonResult({ id: Number(result.lastInsertRowid), created: true });
66
+ }
67
+ function episodeList(db, args) {
68
+ const conditions = [];
69
+ const vals = [];
70
+ if (args.session_id) {
71
+ conditions.push("session_id = ?");
72
+ vals.push(args.session_id);
73
+ }
74
+ if (args.namespace) {
75
+ conditions.push("namespace = ?");
76
+ vals.push(args.namespace);
77
+ }
78
+ if (args.agent_role) {
79
+ conditions.push("agent_role = ?");
80
+ vals.push(args.agent_role);
81
+ }
82
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
83
+ const order = args.order === "newest" ? "DESC" : "ASC";
84
+ const limit = args.limit || 50;
85
+ const rows = db.prepare(`SELECT * FROM episodes ${where} ORDER BY created_at ${order} LIMIT ?`).all(...vals, limit);
86
+ return jsonResult({ episodes: rows, count: rows.length });
87
+ }
88
+ function episodeSummarize(db, args) {
89
+ // Update all episodes in the session with the summary
90
+ const result = db.prepare(`UPDATE episodes SET summary = ? WHERE session_id = ? AND summary IS NULL`).run(args.summary, args.session_id);
91
+ // Also store as a memory for cross-session recall
92
+ const ts = now();
93
+ db.prepare(`
94
+ INSERT INTO memories (memory_type, scope, namespace, agent_role, title, content, status, confidence, tags, metadata, created_at, occurred_at, updated_at, content_hash)
95
+ VALUES ('episodic', 'session', ?, ?, ?, ?, 'active', 1.0, '["session_summary"]', ?, ?, ?, ?, ?)
96
+ `).run(args.namespace || null, args.agent_role || null, `Session ${args.session_id} summary`, args.summary, JSON.stringify({ session_id: args.session_id }), ts, ts, ts, createHash("sha256").update(`episode\0${args.session_id}\0${args.summary}`).digest("hex"));
97
+ return jsonResult({ episodes_updated: result.changes, session_id: args.session_id });
98
+ }