@memtensor/memos-local-openclaw-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.env.example +11 -0
  2. package/README.md +251 -0
  3. package/SKILL.md +43 -0
  4. package/dist/capture/index.d.ts +16 -0
  5. package/dist/capture/index.d.ts.map +1 -0
  6. package/dist/capture/index.js +80 -0
  7. package/dist/capture/index.js.map +1 -0
  8. package/dist/config.d.ts +4 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +96 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/embedding/index.d.ts +12 -0
  13. package/dist/embedding/index.d.ts.map +1 -0
  14. package/dist/embedding/index.js +75 -0
  15. package/dist/embedding/index.js.map +1 -0
  16. package/dist/embedding/local.d.ts +3 -0
  17. package/dist/embedding/local.d.ts.map +1 -0
  18. package/dist/embedding/local.js +65 -0
  19. package/dist/embedding/local.js.map +1 -0
  20. package/dist/embedding/providers/cohere.d.ts +4 -0
  21. package/dist/embedding/providers/cohere.d.ts.map +1 -0
  22. package/dist/embedding/providers/cohere.js +57 -0
  23. package/dist/embedding/providers/cohere.js.map +1 -0
  24. package/dist/embedding/providers/gemini.d.ts +3 -0
  25. package/dist/embedding/providers/gemini.d.ts.map +1 -0
  26. package/dist/embedding/providers/gemini.js +31 -0
  27. package/dist/embedding/providers/gemini.js.map +1 -0
  28. package/dist/embedding/providers/mistral.d.ts +3 -0
  29. package/dist/embedding/providers/mistral.d.ts.map +1 -0
  30. package/dist/embedding/providers/mistral.js +25 -0
  31. package/dist/embedding/providers/mistral.js.map +1 -0
  32. package/dist/embedding/providers/openai.d.ts +3 -0
  33. package/dist/embedding/providers/openai.d.ts.map +1 -0
  34. package/dist/embedding/providers/openai.js +35 -0
  35. package/dist/embedding/providers/openai.js.map +1 -0
  36. package/dist/embedding/providers/voyage.d.ts +3 -0
  37. package/dist/embedding/providers/voyage.d.ts.map +1 -0
  38. package/dist/embedding/providers/voyage.js +25 -0
  39. package/dist/embedding/providers/voyage.js.map +1 -0
  40. package/dist/index.d.ts +44 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +75 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/ingest/chunker.d.ts +15 -0
  45. package/dist/ingest/chunker.d.ts.map +1 -0
  46. package/dist/ingest/chunker.js +193 -0
  47. package/dist/ingest/chunker.js.map +1 -0
  48. package/dist/ingest/dedup.d.ts +11 -0
  49. package/dist/ingest/dedup.d.ts.map +1 -0
  50. package/dist/ingest/dedup.js +29 -0
  51. package/dist/ingest/dedup.js.map +1 -0
  52. package/dist/ingest/providers/anthropic.d.ts +3 -0
  53. package/dist/ingest/providers/anthropic.d.ts.map +1 -0
  54. package/dist/ingest/providers/anthropic.js +33 -0
  55. package/dist/ingest/providers/anthropic.js.map +1 -0
  56. package/dist/ingest/providers/bedrock.d.ts +8 -0
  57. package/dist/ingest/providers/bedrock.d.ts.map +1 -0
  58. package/dist/ingest/providers/bedrock.js +41 -0
  59. package/dist/ingest/providers/bedrock.js.map +1 -0
  60. package/dist/ingest/providers/gemini.d.ts +3 -0
  61. package/dist/ingest/providers/gemini.d.ts.map +1 -0
  62. package/dist/ingest/providers/gemini.js +31 -0
  63. package/dist/ingest/providers/gemini.js.map +1 -0
  64. package/dist/ingest/providers/index.d.ts +9 -0
  65. package/dist/ingest/providers/index.d.ts.map +1 -0
  66. package/dist/ingest/providers/index.js +68 -0
  67. package/dist/ingest/providers/index.js.map +1 -0
  68. package/dist/ingest/providers/openai.d.ts +3 -0
  69. package/dist/ingest/providers/openai.d.ts.map +1 -0
  70. package/dist/ingest/providers/openai.js +41 -0
  71. package/dist/ingest/providers/openai.js.map +1 -0
  72. package/dist/ingest/worker.d.ts +21 -0
  73. package/dist/ingest/worker.d.ts.map +1 -0
  74. package/dist/ingest/worker.js +111 -0
  75. package/dist/ingest/worker.js.map +1 -0
  76. package/dist/recall/engine.d.ts +23 -0
  77. package/dist/recall/engine.d.ts.map +1 -0
  78. package/dist/recall/engine.js +153 -0
  79. package/dist/recall/engine.js.map +1 -0
  80. package/dist/recall/mmr.d.ts +17 -0
  81. package/dist/recall/mmr.d.ts.map +1 -0
  82. package/dist/recall/mmr.js +51 -0
  83. package/dist/recall/mmr.js.map +1 -0
  84. package/dist/recall/recency.d.ts +20 -0
  85. package/dist/recall/recency.d.ts.map +1 -0
  86. package/dist/recall/recency.js +26 -0
  87. package/dist/recall/recency.js.map +1 -0
  88. package/dist/recall/rrf.d.ts +16 -0
  89. package/dist/recall/rrf.d.ts.map +1 -0
  90. package/dist/recall/rrf.js +15 -0
  91. package/dist/recall/rrf.js.map +1 -0
  92. package/dist/storage/sqlite.d.ts +34 -0
  93. package/dist/storage/sqlite.d.ts.map +1 -0
  94. package/dist/storage/sqlite.js +274 -0
  95. package/dist/storage/sqlite.js.map +1 -0
  96. package/dist/storage/vector.d.ts +13 -0
  97. package/dist/storage/vector.d.ts.map +1 -0
  98. package/dist/storage/vector.js +33 -0
  99. package/dist/storage/vector.js.map +1 -0
  100. package/dist/tools/index.d.ts +4 -0
  101. package/dist/tools/index.d.ts.map +1 -0
  102. package/dist/tools/index.js +10 -0
  103. package/dist/tools/index.js.map +1 -0
  104. package/dist/tools/memory-get.d.ts +4 -0
  105. package/dist/tools/memory-get.d.ts.map +1 -0
  106. package/dist/tools/memory-get.js +59 -0
  107. package/dist/tools/memory-get.js.map +1 -0
  108. package/dist/tools/memory-search.d.ts +4 -0
  109. package/dist/tools/memory-search.d.ts.map +1 -0
  110. package/dist/tools/memory-search.js +36 -0
  111. package/dist/tools/memory-search.js.map +1 -0
  112. package/dist/tools/memory-timeline.d.ts +4 -0
  113. package/dist/tools/memory-timeline.d.ts.map +1 -0
  114. package/dist/tools/memory-timeline.js +64 -0
  115. package/dist/tools/memory-timeline.js.map +1 -0
  116. package/dist/types.d.ts +158 -0
  117. package/dist/types.d.ts.map +1 -0
  118. package/dist/types.js +25 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/viewer/html.d.ts +2 -0
  121. package/dist/viewer/html.d.ts.map +1 -0
  122. package/dist/viewer/html.js +686 -0
  123. package/dist/viewer/html.js.map +1 -0
  124. package/dist/viewer/server.d.ts +48 -0
  125. package/dist/viewer/server.d.ts.map +1 -0
  126. package/dist/viewer/server.js +470 -0
  127. package/dist/viewer/server.js.map +1 -0
  128. package/index.ts +357 -0
  129. package/openclaw.plugin.json +57 -0
  130. package/package.json +57 -0
  131. package/src/capture/index.ts +92 -0
  132. package/src/config.ts +67 -0
  133. package/src/embedding/index.ts +76 -0
  134. package/src/embedding/local.ts +35 -0
  135. package/src/embedding/providers/cohere.ts +69 -0
  136. package/src/embedding/providers/gemini.ts +41 -0
  137. package/src/embedding/providers/mistral.ts +32 -0
  138. package/src/embedding/providers/openai.ts +42 -0
  139. package/src/embedding/providers/voyage.ts +32 -0
  140. package/src/index.ts +106 -0
  141. package/src/ingest/chunker.ts +217 -0
  142. package/src/ingest/dedup.ts +37 -0
  143. package/src/ingest/providers/anthropic.ts +41 -0
  144. package/src/ingest/providers/bedrock.ts +50 -0
  145. package/src/ingest/providers/gemini.ts +41 -0
  146. package/src/ingest/providers/index.ts +67 -0
  147. package/src/ingest/providers/openai.ts +48 -0
  148. package/src/ingest/worker.ts +130 -0
  149. package/src/recall/engine.ts +182 -0
  150. package/src/recall/mmr.ts +60 -0
  151. package/src/recall/recency.ts +27 -0
  152. package/src/recall/rrf.ts +31 -0
  153. package/src/storage/sqlite.ts +305 -0
  154. package/src/storage/vector.ts +39 -0
  155. package/src/tools/index.ts +3 -0
  156. package/src/tools/memory-get.ts +68 -0
  157. package/src/tools/memory-search.ts +36 -0
  158. package/src/tools/memory-timeline.ts +73 -0
  159. package/src/types.ts +214 -0
  160. package/src/viewer/html.ts +682 -0
  161. package/src/viewer/server.ts +464 -0
  162. package/www/index.html +606 -0
@@ -0,0 +1,305 @@
1
+ import Database from "better-sqlite3";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import type { Chunk, ChunkRef, Logger } from "../types";
5
+
6
+ export class SqliteStore {
7
+ private db: Database.Database;
8
+
9
+ constructor(dbPath: string, private log: Logger) {
10
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
11
+ this.db = new Database(dbPath);
12
+ this.db.pragma("journal_mode = WAL");
13
+ this.db.pragma("foreign_keys = ON");
14
+ this.migrate();
15
+ }
16
+
17
+ // ─── Schema ───
18
+
19
+ private migrate(): void {
20
+ this.db.exec(`
21
+ CREATE TABLE IF NOT EXISTS chunks (
22
+ id TEXT PRIMARY KEY,
23
+ session_key TEXT NOT NULL,
24
+ turn_id TEXT NOT NULL,
25
+ seq INTEGER NOT NULL,
26
+ role TEXT NOT NULL,
27
+ content TEXT NOT NULL,
28
+ kind TEXT NOT NULL DEFAULT 'paragraph',
29
+ summary TEXT NOT NULL DEFAULT '',
30
+ created_at INTEGER NOT NULL,
31
+ updated_at INTEGER NOT NULL
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS idx_chunks_session
35
+ ON chunks(session_key);
36
+ CREATE INDEX IF NOT EXISTS idx_chunks_turn
37
+ ON chunks(session_key, turn_id, seq);
38
+ CREATE INDEX IF NOT EXISTS idx_chunks_created
39
+ ON chunks(created_at);
40
+
41
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
42
+ summary,
43
+ content,
44
+ content='chunks',
45
+ content_rowid='rowid',
46
+ tokenize='porter unicode61'
47
+ );
48
+
49
+ CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
50
+ INSERT INTO chunks_fts(rowid, summary, content)
51
+ VALUES (new.rowid, new.summary, new.content);
52
+ END;
53
+
54
+ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
55
+ INSERT INTO chunks_fts(chunks_fts, rowid, summary, content)
56
+ VALUES ('delete', old.rowid, old.summary, old.content);
57
+ END;
58
+
59
+ CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
60
+ INSERT INTO chunks_fts(chunks_fts, rowid, summary, content)
61
+ VALUES ('delete', old.rowid, old.summary, old.content);
62
+ INSERT INTO chunks_fts(rowid, summary, content)
63
+ VALUES (new.rowid, new.summary, new.content);
64
+ END;
65
+
66
+ CREATE TABLE IF NOT EXISTS embeddings (
67
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
68
+ vector BLOB NOT NULL,
69
+ dimensions INTEGER NOT NULL,
70
+ updated_at INTEGER NOT NULL
71
+ );
72
+ `);
73
+ this.log.debug("Database schema initialized");
74
+ }
75
+
76
+ // ─── Write ───
77
+
78
+ insertChunk(chunk: Chunk): void {
79
+ const stmt = this.db.prepare(`
80
+ INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, created_at, updated_at)
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
82
+ `);
83
+ stmt.run(
84
+ chunk.id,
85
+ chunk.sessionKey,
86
+ chunk.turnId,
87
+ chunk.seq,
88
+ chunk.role,
89
+ chunk.content,
90
+ chunk.kind,
91
+ chunk.summary,
92
+ chunk.createdAt,
93
+ chunk.updatedAt,
94
+ );
95
+ }
96
+
97
+ updateSummary(chunkId: string, summary: string): void {
98
+ this.db.prepare("UPDATE chunks SET summary = ?, updated_at = ? WHERE id = ?").run(
99
+ summary,
100
+ Date.now(),
101
+ chunkId,
102
+ );
103
+ }
104
+
105
+ upsertEmbedding(chunkId: string, vector: number[]): void {
106
+ const buf = Buffer.from(new Float32Array(vector).buffer);
107
+ this.db.prepare(`
108
+ INSERT OR REPLACE INTO embeddings (chunk_id, vector, dimensions, updated_at)
109
+ VALUES (?, ?, ?, ?)
110
+ `).run(chunkId, buf, vector.length, Date.now());
111
+ }
112
+
113
+ // ─── Read ───
114
+
115
+ getChunk(chunkId: string): Chunk | null {
116
+ const row = this.db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as ChunkRow | undefined;
117
+ return row ? rowToChunk(row) : null;
118
+ }
119
+
120
+ getChunksByRef(ref: ChunkRef): Chunk | null {
121
+ return this.getChunk(ref.chunkId);
122
+ }
123
+
124
+ getNeighborChunks(sessionKey: string, turnId: string, seq: number, window: number): Chunk[] {
125
+ const allRows = this.db.prepare(`
126
+ SELECT * FROM chunks
127
+ WHERE session_key = ?
128
+ ORDER BY created_at, seq
129
+ `).all(sessionKey) as ChunkRow[];
130
+
131
+ const targetIdx = allRows.findIndex(
132
+ (r) => r.turn_id === turnId && r.seq === seq,
133
+ );
134
+ if (targetIdx === -1) return [];
135
+
136
+ const radius = window * 3;
137
+ const start = Math.max(0, targetIdx - radius);
138
+ const end = Math.min(allRows.length, targetIdx + radius + 1);
139
+ return allRows.slice(start, end).map(rowToChunk);
140
+ }
141
+
142
+ // ─── FTS Search ───
143
+
144
+ ftsSearch(query: string, limit: number): Array<{ chunkId: string; score: number }> {
145
+ const sanitized = sanitizeFtsQuery(query);
146
+ if (!sanitized) return [];
147
+
148
+ try {
149
+ const rows = this.db.prepare(`
150
+ SELECT c.id as chunk_id, rank
151
+ FROM chunks_fts f
152
+ JOIN chunks c ON c.rowid = f.rowid
153
+ WHERE chunks_fts MATCH ?
154
+ ORDER BY rank
155
+ LIMIT ?
156
+ `).all(sanitized, limit) as Array<{ chunk_id: string; rank: number }>;
157
+
158
+ if (rows.length === 0) return [];
159
+ const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
160
+ return rows.map((r) => ({
161
+ chunkId: r.chunk_id,
162
+ score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
163
+ }));
164
+ } catch {
165
+ this.log.warn(`FTS query failed for: "${sanitized}", returning empty`);
166
+ return [];
167
+ }
168
+ }
169
+
170
+ // ─── Vector Search ───
171
+
172
+ getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
173
+ const rows = this.db.prepare(
174
+ "SELECT chunk_id, vector, dimensions FROM embeddings",
175
+ ).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
176
+
177
+ return rows.map((r) => ({
178
+ chunkId: r.chunk_id,
179
+ vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
180
+ }));
181
+ }
182
+
183
+ getEmbedding(chunkId: string): number[] | null {
184
+ const row = this.db.prepare(
185
+ "SELECT vector, dimensions FROM embeddings WHERE chunk_id = ?",
186
+ ).get(chunkId) as { vector: Buffer; dimensions: number } | undefined;
187
+ if (!row) return null;
188
+ return Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions));
189
+ }
190
+
191
+ // ─── Update ───
192
+
193
+ updateChunk(chunkId: string, fields: { summary?: string; content?: string; role?: string; kind?: string }): boolean {
194
+ const sets: string[] = [];
195
+ const params: unknown[] = [];
196
+
197
+ if (fields.summary !== undefined) {
198
+ sets.push("summary = ?");
199
+ params.push(fields.summary);
200
+ }
201
+ if (fields.content !== undefined) {
202
+ sets.push("content = ?");
203
+ params.push(fields.content);
204
+ }
205
+ if (fields.role !== undefined) {
206
+ sets.push("role = ?");
207
+ params.push(fields.role);
208
+ }
209
+ if (fields.kind !== undefined) {
210
+ sets.push("kind = ?");
211
+ params.push(fields.kind);
212
+ }
213
+ if (sets.length === 0) return false;
214
+
215
+ sets.push("updated_at = ?");
216
+ params.push(Date.now());
217
+ params.push(chunkId);
218
+
219
+ const result = this.db.prepare(
220
+ `UPDATE chunks SET ${sets.join(", ")} WHERE id = ?`,
221
+ ).run(...params);
222
+ return result.changes > 0;
223
+ }
224
+
225
+ // ─── Delete ───
226
+
227
+ deleteChunk(chunkId: string): boolean {
228
+ const result = this.db.prepare("DELETE FROM chunks WHERE id = ?").run(chunkId);
229
+ return result.changes > 0;
230
+ }
231
+
232
+ deleteSession(sessionKey: string): number {
233
+ const result = this.db.prepare("DELETE FROM chunks WHERE session_key = ?").run(sessionKey);
234
+ return result.changes;
235
+ }
236
+
237
+ deleteAll(): number {
238
+ const result = this.db.prepare("DELETE FROM chunks").run();
239
+ return result.changes;
240
+ }
241
+
242
+ // ─── Util ───
243
+
244
+ getRecentChunkIds(limit: number): string[] {
245
+ const rows = this.db.prepare(
246
+ "SELECT id FROM chunks ORDER BY created_at DESC LIMIT ?",
247
+ ).all(limit) as Array<{ id: string }>;
248
+ return rows.map((r) => r.id);
249
+ }
250
+
251
+ close(): void {
252
+ this.db.close();
253
+ }
254
+ }
255
+
256
+ // ─── FTS helpers ───
257
+
258
+ /**
259
+ * Sanitize user input for FTS5 MATCH queries.
260
+ * Strip FTS operators and special characters, then join tokens
261
+ * with implicit AND (space-separated) for safe querying.
262
+ */
263
+ function sanitizeFtsQuery(raw: string): string {
264
+ const tokens = raw
265
+ .replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`]/g, " ")
266
+ .split(/\s+/)
267
+ .map((t) => t.trim().replace(/^-+|-+$/g, ""))
268
+ .filter((t) => t.length > 1)
269
+ .filter((t) => !FTS_RESERVED.has(t.toUpperCase()));
270
+
271
+ return tokens.join(" ");
272
+ }
273
+
274
+ const FTS_RESERVED = new Set(["AND", "OR", "NOT", "NEAR"]);
275
+
276
+ // ─── Internal helpers ───
277
+
278
+ interface ChunkRow {
279
+ id: string;
280
+ session_key: string;
281
+ turn_id: string;
282
+ seq: number;
283
+ role: string;
284
+ content: string;
285
+ kind: string;
286
+ summary: string;
287
+ created_at: number;
288
+ updated_at: number;
289
+ }
290
+
291
+ function rowToChunk(row: ChunkRow): Chunk {
292
+ return {
293
+ id: row.id,
294
+ sessionKey: row.session_key,
295
+ turnId: row.turn_id,
296
+ seq: row.seq,
297
+ role: row.role as Chunk["role"],
298
+ content: row.content,
299
+ kind: row.kind as Chunk["kind"],
300
+ summary: row.summary,
301
+ embedding: null,
302
+ createdAt: row.created_at,
303
+ updatedAt: row.updated_at,
304
+ };
305
+ }
@@ -0,0 +1,39 @@
1
+ import type { SqliteStore } from "./sqlite";
2
+
3
+ export function cosineSimilarity(a: number[], b: number[]): number {
4
+ if (a.length !== b.length) return 0;
5
+ let dot = 0;
6
+ let normA = 0;
7
+ let normB = 0;
8
+ for (let i = 0; i < a.length; i++) {
9
+ dot += a[i] * b[i];
10
+ normA += a[i] * a[i];
11
+ normB += b[i] * b[i];
12
+ }
13
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
14
+ return denom === 0 ? 0 : dot / denom;
15
+ }
16
+
17
+ export interface VectorHit {
18
+ chunkId: string;
19
+ score: number;
20
+ }
21
+
22
+ /**
23
+ * Brute-force vector search over all stored embeddings.
24
+ * For local single-user usage the dataset is small enough that
25
+ * a full scan with SIMD-friendly Float32 math is sufficient.
26
+ */
27
+ export function vectorSearch(
28
+ store: SqliteStore,
29
+ queryVec: number[],
30
+ topK: number,
31
+ ): VectorHit[] {
32
+ const all = store.getAllEmbeddings();
33
+ const scored: VectorHit[] = all.map((row) => ({
34
+ chunkId: row.chunkId,
35
+ score: cosineSimilarity(queryVec, row.vector),
36
+ }));
37
+ scored.sort((a, b) => b.score - a.score);
38
+ return scored.slice(0, topK);
39
+ }
@@ -0,0 +1,3 @@
1
+ export { createMemorySearchTool } from "./memory-search";
2
+ export { createMemoryTimelineTool } from "./memory-timeline";
3
+ export { createMemoryGetTool } from "./memory-get";
@@ -0,0 +1,68 @@
1
+ import type { SqliteStore } from "../storage/sqlite";
2
+ import type { ToolDefinition, GetResult, ChunkRef } from "../types";
3
+ import { DEFAULTS } from "../types";
4
+
5
+ export function createMemoryGetTool(store: SqliteStore): ToolDefinition {
6
+ return {
7
+ name: "memory_get",
8
+ description:
9
+ "Retrieve the full original text of a specific memory chunk. Use after memory_search or memory_timeline " +
10
+ "when you need to see the complete content (not just the excerpt). Useful for verifying exact details.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ ref: {
15
+ type: "object",
16
+ description: "Reference object from a memory_search hit or memory_timeline entry.",
17
+ properties: {
18
+ sessionKey: { type: "string" },
19
+ chunkId: { type: "string" },
20
+ turnId: { type: "string" },
21
+ seq: { type: "number" },
22
+ },
23
+ required: ["sessionKey", "chunkId", "turnId", "seq"],
24
+ },
25
+ maxChars: {
26
+ type: "number",
27
+ description: `Maximum characters to return (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax}).`,
28
+ },
29
+ },
30
+ required: ["ref"],
31
+ },
32
+ handler: async (input) => {
33
+ const ref = input.ref as ChunkRef;
34
+ const maxChars = Math.min(
35
+ (input.maxChars as number) ?? DEFAULTS.getMaxCharsDefault,
36
+ DEFAULTS.getMaxCharsMax,
37
+ );
38
+
39
+ const chunk = store.getChunksByRef(ref);
40
+
41
+ if (!chunk) {
42
+ return { error: `Chunk not found: ${ref.chunkId}` };
43
+ }
44
+
45
+ const content =
46
+ chunk.content.length > maxChars
47
+ ? chunk.content.slice(0, maxChars) + "…"
48
+ : chunk.content;
49
+
50
+ const result: GetResult = {
51
+ content,
52
+ ref: {
53
+ sessionKey: chunk.sessionKey,
54
+ chunkId: chunk.id,
55
+ turnId: chunk.turnId,
56
+ seq: chunk.seq,
57
+ },
58
+ source: {
59
+ ts: chunk.createdAt,
60
+ role: chunk.role,
61
+ sessionKey: chunk.sessionKey,
62
+ },
63
+ };
64
+
65
+ return result;
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,36 @@
1
+ import type { RecallEngine } from "../recall/engine";
2
+ import type { ToolDefinition } from "../types";
3
+
4
+ export function createMemorySearchTool(engine: RecallEngine): ToolDefinition {
5
+ return {
6
+ name: "memory_search",
7
+ description:
8
+ "Search stored conversation memories. Returns matching entries with summary, original_excerpt (evidence), score, and ref for follow-up with memory_timeline or memory_get. " +
9
+ "Default: top 6 results, minScore 0.45. Increase maxResults to 12/20 or lower minScore to 0.35 if initial results are insufficient.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ query: {
14
+ type: "string",
15
+ description: "Natural language search query. Include specific entities, commands, or error messages for better recall.",
16
+ },
17
+ maxResults: {
18
+ type: "number",
19
+ description: "Maximum number of results (default 6, max 20).",
20
+ },
21
+ minScore: {
22
+ type: "number",
23
+ description: "Minimum relevance score threshold 0-1 (default 0.45, floor 0.35).",
24
+ },
25
+ },
26
+ },
27
+ handler: async (input) => {
28
+ const result = await engine.search({
29
+ query: (input.query as string) ?? "",
30
+ maxResults: input.maxResults as number | undefined,
31
+ minScore: input.minScore as number | undefined,
32
+ });
33
+ return result;
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,73 @@
1
+ import type { SqliteStore } from "../storage/sqlite";
2
+ import type { ToolDefinition, TimelineResult, TimelineEntry, ChunkRef } from "../types";
3
+ import { DEFAULTS } from "../types";
4
+
5
+ export function createMemoryTimelineTool(store: SqliteStore): ToolDefinition {
6
+ return {
7
+ name: "memory_timeline",
8
+ description:
9
+ "Retrieve neighboring context around a memory reference. Use after memory_search to expand context " +
10
+ "around a specific hit. Provides adjacent conversation chunks marked as before/current/after.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ ref: {
15
+ type: "object",
16
+ description: "Reference object from a memory_search hit (must contain sessionKey, chunkId, turnId, seq).",
17
+ properties: {
18
+ sessionKey: { type: "string" },
19
+ chunkId: { type: "string" },
20
+ turnId: { type: "string" },
21
+ seq: { type: "number" },
22
+ },
23
+ required: ["sessionKey", "chunkId", "turnId", "seq"],
24
+ },
25
+ window: {
26
+ type: "number",
27
+ description: "Number of turns/chunks to include before and after (default ±2).",
28
+ },
29
+ },
30
+ required: ["ref"],
31
+ },
32
+ handler: async (input) => {
33
+ const ref = input.ref as ChunkRef;
34
+ const window = (input.window as number) ?? DEFAULTS.timelineWindowDefault;
35
+
36
+ const neighbors = store.getNeighborChunks(
37
+ ref.sessionKey,
38
+ ref.turnId,
39
+ ref.seq,
40
+ window,
41
+ );
42
+
43
+ const entries: TimelineEntry[] = neighbors.map((chunk) => {
44
+ let relation: TimelineEntry["relation"] = "before";
45
+ if (chunk.id === ref.chunkId) {
46
+ relation = "current";
47
+ } else if (chunk.createdAt > (store.getChunk(ref.chunkId)?.createdAt ?? 0)) {
48
+ relation = "after";
49
+ }
50
+
51
+ return {
52
+ excerpt: chunk.content.slice(0, DEFAULTS.excerptMaxChars),
53
+ ref: {
54
+ sessionKey: chunk.sessionKey,
55
+ chunkId: chunk.id,
56
+ turnId: chunk.turnId,
57
+ seq: chunk.seq,
58
+ },
59
+ role: chunk.role,
60
+ ts: chunk.createdAt,
61
+ relation,
62
+ };
63
+ });
64
+
65
+ const result: TimelineResult = {
66
+ entries,
67
+ anchorRef: ref,
68
+ };
69
+
70
+ return result;
71
+ },
72
+ };
73
+ }