@launchapp-dev/ao-memory-mcp 1.0.0 → 2.0.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.
@@ -0,0 +1,248 @@
1
+ import type Database from "better-sqlite3";
2
+ import { now, jsonResult, errorResult } from "../db.ts";
3
+
4
+ export const knowledgeTools = [
5
+ {
6
+ name: "memory.entity.add",
7
+ description:
8
+ "Add an entity to the knowledge graph. Entities represent projects, people, technologies, concepts, files, or any named thing.",
9
+ inputSchema: {
10
+ type: "object" as const,
11
+ properties: {
12
+ name: { type: "string", description: "Entity name (e.g. 'ao-cli', 'React', 'Sami')" },
13
+ entity_type: { type: "string", description: "Type (e.g. project, person, technology, concept, file, service)" },
14
+ namespace: { type: "string", description: "Scope" },
15
+ description: { type: "string", description: "Brief description" },
16
+ metadata: { type: "object", description: "Custom metadata" },
17
+ },
18
+ required: ["name", "entity_type"],
19
+ },
20
+ },
21
+ {
22
+ name: "memory.entity.link",
23
+ description:
24
+ "Create a relationship between two entities. E.g. 'ao-cli uses Rust', 'invoicer depends_on Drizzle'.",
25
+ inputSchema: {
26
+ type: "object" as const,
27
+ properties: {
28
+ source: { type: "string", description: "Source entity name" },
29
+ source_type: { type: "string", description: "Source entity type (for disambiguation)" },
30
+ relation: { type: "string", description: "Relation type (e.g. uses, depends_on, created_by, part_of, related_to)" },
31
+ target: { type: "string", description: "Target entity name" },
32
+ target_type: { type: "string", description: "Target entity type" },
33
+ weight: { type: "number", description: "Relation strength 0.0-1.0 (default 1.0)" },
34
+ memory_id: { type: "number", description: "Link to a memory entry as evidence" },
35
+ namespace: { type: "string", description: "Scope for auto-creating entities" },
36
+ metadata: { type: "object", description: "Custom metadata" },
37
+ },
38
+ required: ["source", "source_type", "relation", "target", "target_type"],
39
+ },
40
+ },
41
+ {
42
+ name: "memory.entity.query",
43
+ description:
44
+ "Query the knowledge graph. Find entities and traverse relationships. Supports multi-hop traversal.",
45
+ inputSchema: {
46
+ type: "object" as const,
47
+ properties: {
48
+ name: { type: "string", description: "Entity name to start from" },
49
+ entity_type: { type: "string", description: "Filter by entity type" },
50
+ relation: { type: "string", description: "Filter by relation type" },
51
+ direction: { type: "string", enum: ["outgoing", "incoming", "both"], description: "Traversal direction (default: both)" },
52
+ depth: { type: "number", description: "Max traversal depth (default 1, max 3)" },
53
+ namespace: { type: "string", description: "Filter by namespace" },
54
+ limit: { type: "number", description: "Max results (default 50)" },
55
+ },
56
+ },
57
+ },
58
+ {
59
+ name: "memory.entity.list",
60
+ description: "List entities in the knowledge graph.",
61
+ inputSchema: {
62
+ type: "object" as const,
63
+ properties: {
64
+ entity_type: { type: "string", description: "Filter by type" },
65
+ namespace: { type: "string", description: "Filter by namespace" },
66
+ limit: { type: "number", description: "Max results (default 50)" },
67
+ },
68
+ },
69
+ },
70
+ ];
71
+
72
+ export function handleKnowledge(db: Database.Database, name: string, args: any) {
73
+ if (name === "memory.entity.add") return entityAdd(db, args);
74
+ if (name === "memory.entity.link") return entityLink(db, args);
75
+ if (name === "memory.entity.query") return entityQuery(db, args);
76
+ if (name === "memory.entity.list") return entityList(db, args);
77
+ return null;
78
+ }
79
+
80
+ function getOrCreateEntity(db: Database.Database, name: string, entityType: string, namespace?: string): number {
81
+ const existing = db.prepare(
82
+ "SELECT id FROM entities WHERE name = ? AND entity_type = ? AND namespace IS ?"
83
+ ).get(name, entityType, namespace || null) as any;
84
+
85
+ if (existing) return existing.id;
86
+
87
+ const ts = now();
88
+ const result = db.prepare(`
89
+ INSERT INTO entities (name, entity_type, namespace, metadata, created_at, updated_at)
90
+ VALUES (?, ?, ?, '{}', ?, ?)
91
+ `).run(name, entityType, namespace || null, ts, ts);
92
+
93
+ return Number(result.lastInsertRowid);
94
+ }
95
+
96
+ function entityAdd(db: Database.Database, args: any) {
97
+ const ts = now();
98
+ const existing = db.prepare(
99
+ "SELECT id FROM entities WHERE name = ? AND entity_type = ? AND namespace IS ?"
100
+ ).get(args.name, args.entity_type, args.namespace || null) as any;
101
+
102
+ if (existing) {
103
+ // Update existing
104
+ const sets: string[] = ["updated_at = ?"];
105
+ const vals: any[] = [ts];
106
+ if (args.description) { sets.push("description = ?"); vals.push(args.description); }
107
+ if (args.metadata) { sets.push("metadata = ?"); vals.push(JSON.stringify(args.metadata)); }
108
+ vals.push(existing.id);
109
+ db.prepare(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
110
+ return jsonResult({ id: existing.id, updated: true });
111
+ }
112
+
113
+ const result = db.prepare(`
114
+ INSERT INTO entities (name, entity_type, namespace, description, metadata, created_at, updated_at)
115
+ VALUES (?, ?, ?, ?, ?, ?, ?)
116
+ `).run(
117
+ args.name, args.entity_type, args.namespace || null,
118
+ args.description || null, JSON.stringify(args.metadata || {}), ts, ts
119
+ );
120
+
121
+ return jsonResult({ id: Number(result.lastInsertRowid), created: true });
122
+ }
123
+
124
+ function entityLink(db: Database.Database, args: any) {
125
+ const sourceId = getOrCreateEntity(db, args.source, args.source_type, args.namespace);
126
+ const targetId = getOrCreateEntity(db, args.target, args.target_type, args.namespace);
127
+
128
+ const existing = db.prepare(
129
+ "SELECT id FROM relations WHERE source_entity_id = ? AND relation_type = ? AND target_entity_id = ?"
130
+ ).get(sourceId, args.relation, targetId) as any;
131
+
132
+ if (existing) {
133
+ // Update weight/metadata
134
+ const sets: string[] = [];
135
+ const vals: any[] = [];
136
+ if (args.weight !== undefined) { sets.push("weight = ?"); vals.push(args.weight); }
137
+ if (args.memory_id) { sets.push("memory_id = ?"); vals.push(args.memory_id); }
138
+ if (args.metadata) { sets.push("metadata = ?"); vals.push(JSON.stringify(args.metadata)); }
139
+ if (sets.length > 0) {
140
+ vals.push(existing.id);
141
+ db.prepare(`UPDATE relations SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
142
+ }
143
+ return jsonResult({ id: existing.id, updated: true });
144
+ }
145
+
146
+ const ts = now();
147
+ const result = db.prepare(`
148
+ INSERT INTO relations (source_entity_id, relation_type, target_entity_id, weight, memory_id, metadata, created_at)
149
+ VALUES (?, ?, ?, ?, ?, ?, ?)
150
+ `).run(sourceId, args.relation, targetId, args.weight ?? 1.0, args.memory_id || null, JSON.stringify(args.metadata || {}), ts);
151
+
152
+ return jsonResult({ id: Number(result.lastInsertRowid), created: true, source_id: sourceId, target_id: targetId });
153
+ }
154
+
155
+ function entityQuery(db: Database.Database, args: any) {
156
+ const depth = Math.min(args.depth || 1, 3);
157
+ const limit = args.limit || 50;
158
+ const direction = args.direction || "both";
159
+
160
+ // Find starting entities
161
+ const startConditions: string[] = [];
162
+ const startVals: any[] = [];
163
+ if (args.name) { startConditions.push("name = ?"); startVals.push(args.name); }
164
+ if (args.entity_type) { startConditions.push("entity_type = ?"); startVals.push(args.entity_type); }
165
+ if (args.namespace) { startConditions.push("namespace = ?"); startVals.push(args.namespace); }
166
+
167
+ if (startConditions.length === 0) {
168
+ return errorResult("At least one of: name, entity_type, or namespace required");
169
+ }
170
+
171
+ const startEntities = db.prepare(
172
+ `SELECT * FROM entities WHERE ${startConditions.join(" AND ")} LIMIT ?`
173
+ ).all(...startVals, limit) as any[];
174
+
175
+ if (startEntities.length === 0) return jsonResult({ entities: [], relations: [] });
176
+
177
+ // Traverse relations
178
+ const visited = new Set<number>();
179
+ const allEntities: any[] = [...startEntities];
180
+ const allRelations: any[] = [];
181
+ let currentIds = startEntities.map(e => e.id);
182
+ startEntities.forEach(e => visited.add(e.id));
183
+
184
+ for (let d = 0; d < depth; d++) {
185
+ if (currentIds.length === 0) break;
186
+ const placeholders = currentIds.map(() => "?").join(",");
187
+
188
+ const relConditions: string[] = [];
189
+ if (direction === "outgoing" || direction === "both") {
190
+ relConditions.push(`source_entity_id IN (${placeholders})`);
191
+ }
192
+ if (direction === "incoming" || direction === "both") {
193
+ relConditions.push(`target_entity_id IN (${placeholders})`);
194
+ }
195
+
196
+ const relFilter = args.relation ? ` AND relation_type = ?` : "";
197
+ const relVals = args.relation
198
+ ? [...currentIds, ...(direction === "both" ? currentIds : []), args.relation]
199
+ : [...currentIds, ...(direction === "both" ? currentIds : [])];
200
+
201
+ const rels = db.prepare(`
202
+ SELECT r.*,
203
+ se.name as source_name, se.entity_type as source_type,
204
+ te.name as target_name, te.entity_type as target_type
205
+ FROM relations r
206
+ JOIN entities se ON se.id = r.source_entity_id
207
+ JOIN entities te ON te.id = r.target_entity_id
208
+ WHERE (${relConditions.join(" OR ")})${relFilter}
209
+ LIMIT ?
210
+ `).all(...relVals, limit) as any[];
211
+
212
+ allRelations.push(...rels);
213
+
214
+ const nextIds: number[] = [];
215
+ for (const rel of rels) {
216
+ for (const id of [rel.source_entity_id, rel.target_entity_id]) {
217
+ if (!visited.has(id)) {
218
+ visited.add(id);
219
+ nextIds.push(id);
220
+ }
221
+ }
222
+ }
223
+
224
+ if (nextIds.length > 0) {
225
+ const ents = db.prepare(
226
+ `SELECT * FROM entities WHERE id IN (${nextIds.map(() => "?").join(",")})`
227
+ ).all(...nextIds) as any[];
228
+ allEntities.push(...ents);
229
+ }
230
+
231
+ currentIds = nextIds;
232
+ }
233
+
234
+ return jsonResult({ entities: allEntities, relations: allRelations });
235
+ }
236
+
237
+ function entityList(db: Database.Database, args: any) {
238
+ const conditions: string[] = [];
239
+ const vals: any[] = [];
240
+ if (args.entity_type) { conditions.push("entity_type = ?"); vals.push(args.entity_type); }
241
+ if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
242
+
243
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
244
+ const limit = args.limit || 50;
245
+
246
+ const rows = db.prepare(`SELECT * FROM entities ${where} ORDER BY name LIMIT ?`).all(...vals, limit);
247
+ return jsonResult({ entities: rows, count: rows.length });
248
+ }
@@ -1,52 +1,55 @@
1
1
  import type Database from "better-sqlite3";
2
- import { jsonResult, errorResult } from "../db.ts";
2
+ import { jsonResult, errorResult, touchAccess } from "../db.ts";
3
+ import { embed, hybridSearch, searchVectors } from "../embeddings.ts";
3
4
 
4
5
  export const recallTools = [
5
6
  {
6
7
  name: "memory.recall",
7
8
  description:
8
- "Query memory entries with structured filters. Returns entries sorted by date. Default: 50 most recent active entries.",
9
+ "Recall memories with structured filters. Supports filtering by type, scope, namespace, agent role, tags, date range. Returns entries sorted by date.",
9
10
  inputSchema: {
10
11
  type: "object" as const,
11
12
  properties: {
13
+ memory_type: { type: "string", enum: ["semantic", "episodic", "procedural"], description: "Filter by type" },
14
+ scope: { type: "string", description: "Filter by scope" },
15
+ namespace: { type: "string", description: "Filter by namespace (project name, etc.)" },
12
16
  agent_role: { type: "string", description: "Filter by agent role" },
13
- project: { type: "string", description: "Filter by project" },
14
- entry_type: { type: "string", description: "Filter by entry type" },
15
17
  task_id: { type: "string", description: "Filter by task ID" },
16
- status: { type: "string", enum: ["active", "summarized", "archived"], description: "Filter by status (default: active)" },
17
- date_from: { type: "string", description: "Entries from this ISO date" },
18
- date_to: { type: "string", description: "Entries up to this ISO date" },
19
- tags: { type: "array", items: { type: "string" }, description: "Filter by tags (must have ALL specified)" },
18
+ status: { type: "string", enum: ["active", "summarized", "archived"], description: "Default: active" },
19
+ date_from: { type: "string", description: "From ISO date" },
20
+ date_to: { type: "string", description: "To ISO date" },
21
+ tags: { type: "array", items: { type: "string" }, description: "Must have ALL specified tags" },
20
22
  limit: { type: "number", description: "Max results (default 50)" },
21
- offset: { type: "number", description: "Pagination offset (default 0)" },
22
- order: { type: "string", enum: ["newest", "oldest"], description: "Sort order (default: newest)" },
23
+ offset: { type: "number", description: "Pagination offset" },
24
+ order: { type: "string", enum: ["newest", "oldest", "most_accessed", "highest_confidence"], description: "Sort order" },
23
25
  },
24
26
  },
25
27
  },
26
28
  {
27
29
  name: "memory.search",
28
30
  description:
29
- "Full-text search across all memory entries. Uses FTS5 for fast matching.",
31
+ "Hybrid semantic + keyword search across all memories. Uses vector similarity and FTS5 together for best results.",
30
32
  inputSchema: {
31
33
  type: "object" as const,
32
34
  properties: {
33
- query: { type: "string", description: "Search query (supports FTS5 syntax)" },
35
+ query: { type: "string", description: "Natural language search query" },
36
+ memory_type: { type: "string", description: "Restrict to type" },
37
+ namespace: { type: "string", description: "Restrict to namespace" },
34
38
  agent_role: { type: "string", description: "Restrict to agent role" },
35
- project: { type: "string", description: "Restrict to project" },
36
- entry_type: { type: "string", description: "Restrict to entry type" },
37
39
  status: { type: "string", description: "Restrict to status" },
38
- limit: { type: "number", description: "Max results (default 20)" },
40
+ limit: { type: "number", description: "Max results (default 10)" },
41
+ alpha: { type: "number", description: "Semantic vs keyword weight: 0=all keyword, 1=all semantic (default 0.5)" },
39
42
  },
40
43
  required: ["query"],
41
44
  },
42
45
  },
43
46
  {
44
47
  name: "memory.get",
45
- description: "Get a single memory entry by ID.",
48
+ description: "Get a single memory by ID. Updates access tracking.",
46
49
  inputSchema: {
47
50
  type: "object" as const,
48
51
  properties: {
49
- id: { type: "number", description: "Memory entry ID" },
52
+ id: { type: "number", description: "Memory ID" },
50
53
  },
51
54
  required: ["id"],
52
55
  },
@@ -63,16 +66,15 @@ export function handleRecall(db: Database.Database, name: string, args: any) {
63
66
  function memoryRecall(db: Database.Database, args: any) {
64
67
  const conditions: string[] = [];
65
68
  const vals: any[] = [];
66
- const status = args.status || "active";
67
69
 
68
- conditions.push("status = ?"); vals.push(status);
70
+ conditions.push("status = ?"); vals.push(args.status || "active");
71
+ if (args.memory_type) { conditions.push("memory_type = ?"); vals.push(args.memory_type); }
72
+ if (args.scope) { conditions.push("scope = ?"); vals.push(args.scope); }
73
+ if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
69
74
  if (args.agent_role) { conditions.push("agent_role = ?"); vals.push(args.agent_role); }
70
- if (args.project) { conditions.push("project = ?"); vals.push(args.project); }
71
- if (args.entry_type) { conditions.push("entry_type = ?"); vals.push(args.entry_type); }
72
75
  if (args.task_id) { conditions.push("task_id = ?"); vals.push(args.task_id); }
73
76
  if (args.date_from) { conditions.push("occurred_at >= ?"); vals.push(args.date_from); }
74
77
  if (args.date_to) { conditions.push("occurred_at <= ?"); vals.push(args.date_to); }
75
-
76
78
  if (args.tags?.length) {
77
79
  for (const tag of args.tags) {
78
80
  conditions.push("EXISTS (SELECT 1 FROM json_each(tags) WHERE json_each.value = ?)");
@@ -80,45 +82,86 @@ function memoryRecall(db: Database.Database, args: any) {
80
82
  }
81
83
  }
82
84
 
83
- const order = args.order === "oldest" ? "ASC" : "DESC";
85
+ const orderMap: Record<string, string> = {
86
+ newest: "occurred_at DESC",
87
+ oldest: "occurred_at ASC",
88
+ most_accessed: "access_count DESC",
89
+ highest_confidence: "confidence DESC",
90
+ };
91
+ const order = orderMap[args.order || "newest"] || "occurred_at DESC";
84
92
  const limit = args.limit || 50;
85
93
  const offset = args.offset || 0;
86
94
 
87
95
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
88
96
  const rows = db.prepare(
89
- `SELECT * FROM memory_entries ${where} ORDER BY occurred_at ${order} LIMIT ? OFFSET ?`
97
+ `SELECT * FROM memories ${where} ORDER BY ${order} LIMIT ? OFFSET ?`
90
98
  ).all(...vals, limit, offset);
91
99
 
92
- return jsonResult({ entries: rows, count: rows.length });
100
+ return jsonResult({ memories: rows, count: rows.length });
93
101
  }
94
102
 
95
- function memorySearch(db: Database.Database, args: any) {
96
- const conditions: string[] = [];
97
- const vals: any[] = [];
103
+ async function memorySearch(db: Database.Database, args: any) {
104
+ const limit = args.limit || 10;
105
+ const alpha = args.alpha ?? 0.5;
106
+
107
+ // Get embedding for query
108
+ let queryEmbedding: Float32Array;
109
+ try {
110
+ queryEmbedding = await embed(args.query, true);
111
+ } catch {
112
+ // Fallback to FTS-only
113
+ return ftsOnlySearch(db, args);
114
+ }
115
+
116
+ const results = hybridSearch(db, "memories_fts", "vec_memories", args.query, queryEmbedding, limit * 3, alpha);
117
+
118
+ if (results.length === 0) return jsonResult({ memories: [], count: 0 });
119
+
120
+ // Fetch full rows and apply filters
121
+ const ids = results.map(r => r.rowid);
122
+ const scoreMap = new Map(results.map(r => [r.rowid, r.score]));
98
123
 
99
- if (args.agent_role) { conditions.push("e.agent_role = ?"); vals.push(args.agent_role); }
100
- if (args.project) { conditions.push("e.project = ?"); vals.push(args.project); }
101
- if (args.entry_type) { conditions.push("e.entry_type = ?"); vals.push(args.entry_type); }
102
- if (args.status) { conditions.push("e.status = ?"); vals.push(args.status); }
124
+ const conditions: string[] = [`id IN (${ids.map(() => "?").join(",")})`];
125
+ const vals: any[] = [...ids];
103
126
 
104
- const extraWhere = conditions.length ? `AND ${conditions.join(" AND ")}` : "";
105
- const limit = args.limit || 20;
127
+ if (args.memory_type) { conditions.push("memory_type = ?"); vals.push(args.memory_type); }
128
+ if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
129
+ if (args.agent_role) { conditions.push("agent_role = ?"); vals.push(args.agent_role); }
130
+ if (args.status) { conditions.push("status = ?"); vals.push(args.status); }
131
+
132
+ const rows = db.prepare(
133
+ `SELECT * FROM memories WHERE ${conditions.join(" AND ")}`
134
+ ).all(...vals) as any[];
135
+
136
+ // Sort by hybrid score and limit
137
+ const scored = rows
138
+ .map(r => ({ ...r, _score: scoreMap.get(r.id) || 0 }))
139
+ .sort((a, b) => b._score - a._score)
140
+ .slice(0, limit);
106
141
 
142
+ // Touch access for returned results
143
+ for (const row of scored) touchAccess(db, row.id);
144
+
145
+ return jsonResult({ memories: scored, count: scored.length });
146
+ }
147
+
148
+ function ftsOnlySearch(db: Database.Database, args: any) {
149
+ const limit = args.limit || 10;
107
150
  const rows = db.prepare(`
108
- SELECT e.*, snippet(memory_fts, 0, '<mark>', '</mark>', '...', 32) as title_snippet,
109
- snippet(memory_fts, 1, '<mark>', '</mark>', '...', 64) as body_snippet
110
- FROM memory_fts f
111
- JOIN memory_entries e ON e.id = f.rowid
112
- WHERE memory_fts MATCH ? ${extraWhere}
113
- ORDER BY rank
114
- LIMIT ?
115
- `).all(args.query, ...vals, limit);
116
-
117
- return jsonResult({ entries: rows, count: rows.length });
151
+ SELECT m.*, snippet(memories_fts, 0, '<mark>', '</mark>', '...', 32) as title_snippet,
152
+ snippet(memories_fts, 1, '<mark>', '</mark>', '...', 64) as content_snippet
153
+ FROM memories_fts f
154
+ JOIN memories m ON m.id = f.rowid
155
+ WHERE memories_fts MATCH ?
156
+ ORDER BY rank LIMIT ?
157
+ `).all(args.query, limit);
158
+
159
+ return jsonResult({ memories: rows, count: rows.length, mode: "keyword_only" });
118
160
  }
119
161
 
120
162
  function memoryGet(db: Database.Database, args: any) {
121
- const entry = db.prepare("SELECT * FROM memory_entries WHERE id = ?").get(args.id);
122
- if (!entry) return errorResult(`Entry ${args.id} not found`);
163
+ const entry = db.prepare("SELECT * FROM memories WHERE id = ?").get(args.id) as any;
164
+ if (!entry) return errorResult(`Memory ${args.id} not found`);
165
+ touchAccess(db, args.id);
123
166
  return jsonResult(entry);
124
167
  }
@@ -4,12 +4,11 @@ import { jsonResult } from "../db.ts";
4
4
  export const statsTools = [
5
5
  {
6
6
  name: "memory.stats",
7
- description:
8
- "Get aggregate statistics about memory entries. Optionally filter by project or agent role.",
7
+ description: "Get aggregate statistics across all memory types.",
9
8
  inputSchema: {
10
9
  type: "object" as const,
11
10
  properties: {
12
- project: { type: "string", description: "Filter by project" },
11
+ namespace: { type: "string", description: "Filter by namespace" },
13
12
  agent_role: { type: "string", description: "Filter by agent role" },
14
13
  },
15
14
  },
@@ -24,51 +23,29 @@ export function handleStats(db: Database.Database, name: string, args: any) {
24
23
  function memoryStats(db: Database.Database, args: any) {
25
24
  const conditions: string[] = [];
26
25
  const vals: any[] = [];
27
-
28
- if (args.project) { conditions.push("project = ?"); vals.push(args.project); }
26
+ if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
29
27
  if (args.agent_role) { conditions.push("agent_role = ?"); vals.push(args.agent_role); }
30
-
31
28
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
32
29
 
33
- const total = (db.prepare(`SELECT COUNT(*) as count FROM memory_entries ${where}`).get(...vals) as any).count;
34
-
35
- const byType = db.prepare(
36
- `SELECT entry_type, COUNT(*) as count FROM memory_entries ${where} GROUP BY entry_type ORDER BY count DESC`
37
- ).all(...vals);
38
-
39
- const byStatus = db.prepare(
40
- `SELECT status, COUNT(*) as count FROM memory_entries ${where} GROUP BY status ORDER BY count DESC`
41
- ).all(...vals);
42
-
43
- const byRole = db.prepare(
44
- `SELECT agent_role, COUNT(*) as count FROM memory_entries ${where} GROUP BY agent_role ORDER BY count DESC`
45
- ).all(...vals);
46
-
47
- const byProject = db.prepare(
48
- `SELECT project, COUNT(*) as count FROM memory_entries ${where} GROUP BY project ORDER BY count DESC`
49
- ).all(...vals);
50
-
51
- const dateRange = db.prepare(
52
- `SELECT MIN(occurred_at) as oldest, MAX(occurred_at) as newest FROM memory_entries ${where}`
53
- ).get(...vals) as any;
30
+ const totalMemories = (db.prepare(`SELECT COUNT(*) as c FROM memories ${where}`).get(...vals) as any).c;
31
+ const byType = db.prepare(`SELECT memory_type, COUNT(*) as count FROM memories ${where} GROUP BY memory_type`).all(...vals);
32
+ const byStatus = db.prepare(`SELECT status, COUNT(*) as count FROM memories ${where} GROUP BY status`).all(...vals);
33
+ const byScope = db.prepare(`SELECT scope, COUNT(*) as count FROM memories ${where} GROUP BY scope`).all(...vals);
34
+ const byRole = db.prepare(`SELECT agent_role, COUNT(*) as count FROM memories ${where} GROUP BY agent_role`).all(...vals);
35
+ const byNamespace = db.prepare(`SELECT namespace, COUNT(*) as count FROM memories ${where} GROUP BY namespace ORDER BY count DESC LIMIT 20`).all(...vals);
54
36
 
55
- const summaryCount = (db.prepare(
56
- `SELECT COUNT(*) as count FROM memory_summaries ${where.replace("entry_type", "entry_type")}`
57
- ).get(...vals) as any).count;
37
+ const dateRange = db.prepare(`SELECT MIN(occurred_at) as oldest, MAX(occurred_at) as newest FROM memories ${where}`).get(...vals) as any;
58
38
 
59
- const patternCount = (db.prepare(
60
- `SELECT COUNT(*) as count FROM memory_patterns WHERE status = 'active'`
61
- ).get() as any).count;
39
+ const totalDocs = (db.prepare("SELECT COUNT(*) as c FROM documents").get() as any).c;
40
+ const totalChunks = (db.prepare("SELECT COUNT(*) as c FROM chunks").get() as any).c;
41
+ const totalEntities = (db.prepare("SELECT COUNT(*) as c FROM entities").get() as any).c;
42
+ const totalRelations = (db.prepare("SELECT COUNT(*) as c FROM relations").get() as any).c;
43
+ const totalEpisodes = (db.prepare("SELECT COUNT(*) as c FROM episodes").get() as any).c;
62
44
 
63
45
  return jsonResult({
64
- total_entries: total,
65
- total_summaries: summaryCount,
66
- active_patterns: patternCount,
67
- oldest_entry: dateRange?.oldest,
68
- newest_entry: dateRange?.newest,
69
- by_type: byType,
70
- by_status: byStatus,
71
- by_role: byRole,
72
- by_project: byProject,
46
+ memories: { total: totalMemories, by_type: byType, by_status: byStatus, by_scope: byScope, by_role: byRole, by_namespace: byNamespace, oldest: dateRange?.oldest, newest: dateRange?.newest },
47
+ documents: { total: totalDocs, total_chunks: totalChunks },
48
+ knowledge_graph: { entities: totalEntities, relations: totalRelations },
49
+ episodes: { total: totalEpisodes },
73
50
  });
74
51
  }