openrecall 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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/package.json +51 -0
- package/src/agent.ts +268 -0
- package/src/client.ts +16 -0
- package/src/config.ts +79 -0
- package/src/db.ts +93 -0
- package/src/extract.ts +142 -0
- package/src/index.ts +262 -0
- package/src/maintenance.ts +134 -0
- package/src/memory.ts +604 -0
- package/src/migrations/001_initial.ts +73 -0
- package/src/migrations/002_tags.ts +20 -0
- package/src/migrations/003_decay.ts +15 -0
- package/src/migrations/004_links.ts +23 -0
- package/src/migrations/005_metadata.ts +17 -0
- package/src/migrations/index.ts +16 -0
- package/src/tools.ts +658 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import {
|
|
3
|
+
storeMemory,
|
|
4
|
+
searchMemories,
|
|
5
|
+
updateMemory,
|
|
6
|
+
deleteMemory,
|
|
7
|
+
listMemories,
|
|
8
|
+
getStats,
|
|
9
|
+
refreshMemory,
|
|
10
|
+
getMemory,
|
|
11
|
+
addTags,
|
|
12
|
+
removeTags,
|
|
13
|
+
getTagsForMemory,
|
|
14
|
+
listAllTags,
|
|
15
|
+
searchByTag,
|
|
16
|
+
addLink,
|
|
17
|
+
removeLink,
|
|
18
|
+
getLinksForMemory,
|
|
19
|
+
} from "./memory"
|
|
20
|
+
import { getConfig } from "./config"
|
|
21
|
+
import { isDbAvailable } from "./db"
|
|
22
|
+
import {
|
|
23
|
+
runMaintenance,
|
|
24
|
+
purgeOldMemories,
|
|
25
|
+
getDbSize,
|
|
26
|
+
vacuumDb,
|
|
27
|
+
} from "./maintenance"
|
|
28
|
+
|
|
29
|
+
function safeExecute<T>(fn: () => T, fallback: string): T | string {
|
|
30
|
+
if (!isDbAvailable()) {
|
|
31
|
+
return "[OpenRecall] Memory database is unavailable. Plugin may not have initialized correctly."
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return fn()
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
console.error("[OpenRecall] Tool error:", e)
|
|
37
|
+
return `${fallback}: ${e.message || e}`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createTools(projectId: string) {
|
|
42
|
+
const config = getConfig()
|
|
43
|
+
return {
|
|
44
|
+
memory_store: tool({
|
|
45
|
+
description:
|
|
46
|
+
"Store an important finding, decision, pattern, or learning in persistent cross-session memory. " +
|
|
47
|
+
"Use this to save things worth remembering: architectural decisions, debugging insights, " +
|
|
48
|
+
"user preferences, code patterns, project conventions, or important discoveries. " +
|
|
49
|
+
"These memories persist across sessions and can be searched later.",
|
|
50
|
+
args: {
|
|
51
|
+
content: tool.schema
|
|
52
|
+
.string()
|
|
53
|
+
.describe(
|
|
54
|
+
"The memory content to store. Be specific and include context. " +
|
|
55
|
+
"Good: 'The auth module uses JWT with RS256 signing, keys stored in /etc/app/keys'. " +
|
|
56
|
+
"Bad: 'auth uses JWT'.",
|
|
57
|
+
),
|
|
58
|
+
category: tool.schema
|
|
59
|
+
.enum([
|
|
60
|
+
"decision",
|
|
61
|
+
"pattern",
|
|
62
|
+
"debugging",
|
|
63
|
+
"preference",
|
|
64
|
+
"convention",
|
|
65
|
+
"discovery",
|
|
66
|
+
"general",
|
|
67
|
+
])
|
|
68
|
+
.optional()
|
|
69
|
+
.describe(
|
|
70
|
+
"Category of this memory. " +
|
|
71
|
+
"decision: architectural/design decisions. " +
|
|
72
|
+
"pattern: code patterns and idioms. " +
|
|
73
|
+
"debugging: debugging insights and solutions. " +
|
|
74
|
+
"preference: user preferences and workflow. " +
|
|
75
|
+
"convention: project conventions and standards. " +
|
|
76
|
+
"discovery: important findings. " +
|
|
77
|
+
"general: anything else.",
|
|
78
|
+
),
|
|
79
|
+
source: tool.schema
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe(
|
|
83
|
+
"Where this memory came from, e.g. a file path or context description",
|
|
84
|
+
),
|
|
85
|
+
tags: tool.schema
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe(
|
|
89
|
+
"Comma-separated tags for this memory, e.g. 'auth,jwt,security'. " +
|
|
90
|
+
"Tags help organize and filter memories across categories.",
|
|
91
|
+
),
|
|
92
|
+
global: tool.schema
|
|
93
|
+
.boolean()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(
|
|
96
|
+
"If true, this memory applies to ALL projects (not just the current one). " +
|
|
97
|
+
"Use for user preferences, workflow conventions, or cross-project knowledge.",
|
|
98
|
+
),
|
|
99
|
+
force: tool.schema
|
|
100
|
+
.boolean()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe(
|
|
103
|
+
"If true, skip deduplication check and always create a new memory. " +
|
|
104
|
+
"By default, duplicates are detected and merged.",
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
async execute(args, context) {
|
|
108
|
+
return safeExecute(() => {
|
|
109
|
+
const tags = args.tags
|
|
110
|
+
? args.tags.split(",").map((t: string) => t.trim()).filter(Boolean)
|
|
111
|
+
: undefined
|
|
112
|
+
const memory = storeMemory({
|
|
113
|
+
content: args.content,
|
|
114
|
+
category: args.category || "general",
|
|
115
|
+
sessionId: context.sessionID,
|
|
116
|
+
projectId,
|
|
117
|
+
source: args.source,
|
|
118
|
+
tags,
|
|
119
|
+
global: args.global,
|
|
120
|
+
force: args.force,
|
|
121
|
+
})
|
|
122
|
+
const scope = memory.project_id ? "project" : "global"
|
|
123
|
+
return `Stored ${scope} memory [${memory.id}] in category "${memory.category}".`
|
|
124
|
+
}, "Failed to store memory")
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
memory_search: tool({
|
|
129
|
+
description:
|
|
130
|
+
"Search persistent cross-session memory for relevant past context. " +
|
|
131
|
+
"Use this to recall previous findings, decisions, patterns, or debugging insights. " +
|
|
132
|
+
"Searches across all sessions using full-text search with BM25 ranking. " +
|
|
133
|
+
"Query with natural language or keywords.",
|
|
134
|
+
args: {
|
|
135
|
+
query: tool.schema
|
|
136
|
+
.string()
|
|
137
|
+
.describe(
|
|
138
|
+
"Search query. Use keywords or natural language. " +
|
|
139
|
+
"Examples: 'authentication JWT', 'database migration strategy', 'user preference dark mode'.",
|
|
140
|
+
),
|
|
141
|
+
category: tool.schema
|
|
142
|
+
.enum([
|
|
143
|
+
"decision",
|
|
144
|
+
"pattern",
|
|
145
|
+
"debugging",
|
|
146
|
+
"preference",
|
|
147
|
+
"convention",
|
|
148
|
+
"discovery",
|
|
149
|
+
"general",
|
|
150
|
+
])
|
|
151
|
+
.optional()
|
|
152
|
+
.describe("Filter results to a specific category"),
|
|
153
|
+
limit: tool.schema
|
|
154
|
+
.number()
|
|
155
|
+
.optional()
|
|
156
|
+
.describe("Max results to return (default: 10)"),
|
|
157
|
+
},
|
|
158
|
+
async execute(args) {
|
|
159
|
+
return safeExecute(() => {
|
|
160
|
+
const results = searchMemories({
|
|
161
|
+
query: args.query,
|
|
162
|
+
category: args.category,
|
|
163
|
+
projectId,
|
|
164
|
+
limit: args.limit || config.searchLimit,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
return "No memories found matching the query."
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const formatted = results
|
|
172
|
+
.map((r, i) => {
|
|
173
|
+
const time = new Date(r.memory.time_created * 1000).toISOString()
|
|
174
|
+
return [
|
|
175
|
+
`[${i + 1}] ${r.memory.category.toUpperCase()}`,
|
|
176
|
+
` ${r.memory.content}`,
|
|
177
|
+
r.memory.source ? ` Source: ${r.memory.source}` : "",
|
|
178
|
+
` Stored: ${time} | ID: ${r.memory.id}`,
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join("\n")
|
|
182
|
+
})
|
|
183
|
+
.join("\n\n")
|
|
184
|
+
|
|
185
|
+
return `Found ${results.length} memories:\n\n${formatted}`
|
|
186
|
+
}, "Failed to search memories")
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
memory_update: tool({
|
|
191
|
+
description:
|
|
192
|
+
"Update an existing memory's content, category, or source. " +
|
|
193
|
+
"Use this to refine or correct a previously stored memory without losing its ID and creation timestamp.",
|
|
194
|
+
args: {
|
|
195
|
+
id: tool.schema.string().describe("The memory ID to update"),
|
|
196
|
+
content: tool.schema
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("New content to replace the existing content"),
|
|
200
|
+
category: tool.schema
|
|
201
|
+
.enum([
|
|
202
|
+
"decision",
|
|
203
|
+
"pattern",
|
|
204
|
+
"debugging",
|
|
205
|
+
"preference",
|
|
206
|
+
"convention",
|
|
207
|
+
"discovery",
|
|
208
|
+
"general",
|
|
209
|
+
])
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("New category"),
|
|
212
|
+
source: tool.schema
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe("New source description"),
|
|
216
|
+
},
|
|
217
|
+
async execute(args) {
|
|
218
|
+
return safeExecute(() => {
|
|
219
|
+
const updated = updateMemory(args.id, {
|
|
220
|
+
content: args.content,
|
|
221
|
+
category: args.category,
|
|
222
|
+
source: args.source,
|
|
223
|
+
})
|
|
224
|
+
if (!updated) return `Memory ${args.id} not found.`
|
|
225
|
+
return `Updated memory ${args.id}. Category: "${updated.category}".`
|
|
226
|
+
}, "Failed to update memory")
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
|
|
230
|
+
memory_delete: tool({
|
|
231
|
+
description: "Delete a specific memory by its ID.",
|
|
232
|
+
args: {
|
|
233
|
+
id: tool.schema.string().describe("The memory ID to delete"),
|
|
234
|
+
},
|
|
235
|
+
async execute(args) {
|
|
236
|
+
return safeExecute(() => {
|
|
237
|
+
const deleted = deleteMemory(args.id)
|
|
238
|
+
return deleted
|
|
239
|
+
? `Deleted memory ${args.id}.`
|
|
240
|
+
: `Memory ${args.id} not found.`
|
|
241
|
+
}, "Failed to delete memory")
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
memory_list: tool({
|
|
246
|
+
description:
|
|
247
|
+
"List recent memories, optionally filtered by category and scope. " +
|
|
248
|
+
"Use this to browse what has been remembered without a specific search query.",
|
|
249
|
+
args: {
|
|
250
|
+
category: tool.schema
|
|
251
|
+
.enum([
|
|
252
|
+
"decision",
|
|
253
|
+
"pattern",
|
|
254
|
+
"debugging",
|
|
255
|
+
"preference",
|
|
256
|
+
"convention",
|
|
257
|
+
"discovery",
|
|
258
|
+
"general",
|
|
259
|
+
])
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Filter by category"),
|
|
262
|
+
scope: tool.schema
|
|
263
|
+
.enum(["project", "global", "all"])
|
|
264
|
+
.optional()
|
|
265
|
+
.describe(
|
|
266
|
+
"Filter by scope: 'project' (current project only), 'global' (cross-project), " +
|
|
267
|
+
"'all' (both). Default: 'all'.",
|
|
268
|
+
),
|
|
269
|
+
limit: tool.schema
|
|
270
|
+
.number()
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("Max results (default: 20)"),
|
|
273
|
+
},
|
|
274
|
+
async execute(args) {
|
|
275
|
+
return safeExecute(() => {
|
|
276
|
+
const memories = listMemories({
|
|
277
|
+
category: args.category,
|
|
278
|
+
projectId,
|
|
279
|
+
scope: args.scope || "all",
|
|
280
|
+
limit: args.limit || 20,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
if (memories.length === 0) {
|
|
284
|
+
return "No memories stored yet."
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const formatted = memories
|
|
288
|
+
.map((m, i) => {
|
|
289
|
+
const time = new Date(m.time_created * 1000).toISOString()
|
|
290
|
+
return [
|
|
291
|
+
`[${i + 1}] ${m.category.toUpperCase()}`,
|
|
292
|
+
` ${m.content}`,
|
|
293
|
+
m.source ? ` Source: ${m.source}` : "",
|
|
294
|
+
` Stored: ${time} | ID: ${m.id}`,
|
|
295
|
+
]
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.join("\n")
|
|
298
|
+
})
|
|
299
|
+
.join("\n\n")
|
|
300
|
+
|
|
301
|
+
return `${memories.length} memories:\n\n${formatted}`
|
|
302
|
+
}, "Failed to list memories")
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
|
|
306
|
+
memory_refresh: tool({
|
|
307
|
+
description:
|
|
308
|
+
"Manually boost a memory's relevance so it ranks higher in future searches. " +
|
|
309
|
+
"Use this when you encounter a memory that is still highly relevant and should not decay.",
|
|
310
|
+
args: {
|
|
311
|
+
id: tool.schema.string().describe("The memory ID to refresh"),
|
|
312
|
+
},
|
|
313
|
+
async execute(args) {
|
|
314
|
+
return safeExecute(() => {
|
|
315
|
+
const refreshed = refreshMemory(args.id)
|
|
316
|
+
if (!refreshed) return `Memory ${args.id} not found.`
|
|
317
|
+
return `Refreshed memory ${args.id}. Access count: ${refreshed.access_count}.`
|
|
318
|
+
}, "Failed to refresh memory")
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
|
|
322
|
+
memory_tag: tool({
|
|
323
|
+
description:
|
|
324
|
+
"Manage tags on a memory: add, remove, or list tags. " +
|
|
325
|
+
"Also list all known tags with counts, or find memories by tag.",
|
|
326
|
+
args: {
|
|
327
|
+
action: tool.schema
|
|
328
|
+
.enum(["add", "remove", "list", "list_all", "search"])
|
|
329
|
+
.describe(
|
|
330
|
+
"Action to perform. " +
|
|
331
|
+
"add: add tags to a memory. " +
|
|
332
|
+
"remove: remove tags from a memory. " +
|
|
333
|
+
"list: list tags for a specific memory. " +
|
|
334
|
+
"list_all: list all known tags with counts. " +
|
|
335
|
+
"search: find memories with a specific tag.",
|
|
336
|
+
),
|
|
337
|
+
id: tool.schema
|
|
338
|
+
.string()
|
|
339
|
+
.optional()
|
|
340
|
+
.describe("Memory ID (required for add/remove/list)"),
|
|
341
|
+
tags: tool.schema
|
|
342
|
+
.string()
|
|
343
|
+
.optional()
|
|
344
|
+
.describe("Comma-separated tags (required for add/remove/search)"),
|
|
345
|
+
},
|
|
346
|
+
async execute(args) {
|
|
347
|
+
return safeExecute(() => {
|
|
348
|
+
const tagList = args.tags
|
|
349
|
+
? args.tags.split(",").map((t: string) => t.trim()).filter(Boolean)
|
|
350
|
+
: []
|
|
351
|
+
|
|
352
|
+
switch (args.action) {
|
|
353
|
+
case "add": {
|
|
354
|
+
if (!args.id) return "Memory ID is required for add action."
|
|
355
|
+
if (tagList.length === 0) return "At least one tag is required."
|
|
356
|
+
addTags(args.id, tagList)
|
|
357
|
+
return `Added tags [${tagList.join(", ")}] to memory ${args.id}.`
|
|
358
|
+
}
|
|
359
|
+
case "remove": {
|
|
360
|
+
if (!args.id) return "Memory ID is required for remove action."
|
|
361
|
+
if (tagList.length === 0) return "At least one tag is required."
|
|
362
|
+
removeTags(args.id, tagList)
|
|
363
|
+
return `Removed tags [${tagList.join(", ")}] from memory ${args.id}.`
|
|
364
|
+
}
|
|
365
|
+
case "list": {
|
|
366
|
+
if (!args.id) return "Memory ID is required for list action."
|
|
367
|
+
const tags = getTagsForMemory(args.id)
|
|
368
|
+
if (tags.length === 0) return `No tags on memory ${args.id}.`
|
|
369
|
+
return `Tags for ${args.id}: ${tags.join(", ")}`
|
|
370
|
+
}
|
|
371
|
+
case "list_all": {
|
|
372
|
+
const all = listAllTags()
|
|
373
|
+
if (all.length === 0) return "No tags exist yet."
|
|
374
|
+
return all.map((t) => ` ${t.tag}: ${t.count} memories`).join("\n")
|
|
375
|
+
}
|
|
376
|
+
case "search": {
|
|
377
|
+
if (tagList.length === 0) return "A tag is required for search."
|
|
378
|
+
const memories = searchByTag(tagList[0]!, { projectId })
|
|
379
|
+
if (memories.length === 0) return `No memories tagged "${tagList[0]}".`
|
|
380
|
+
const formatted = memories
|
|
381
|
+
.map((m, i) => {
|
|
382
|
+
const time = new Date(m.time_created * 1000).toISOString()
|
|
383
|
+
return `[${i + 1}] ${m.category.toUpperCase()}\n ${m.content}\n Stored: ${time} | ID: ${m.id}`
|
|
384
|
+
})
|
|
385
|
+
.join("\n\n")
|
|
386
|
+
return `Memories tagged "${tagList[0]}":\n\n${formatted}`
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
return "Unknown action."
|
|
390
|
+
}
|
|
391
|
+
}, "Failed to manage tags")
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
|
|
395
|
+
memory_link: tool({
|
|
396
|
+
description:
|
|
397
|
+
"Manage relationships between memories: link, unlink, or view links. " +
|
|
398
|
+
"Relationships: 'related' (general connection), 'supersedes' (replaces older memory), " +
|
|
399
|
+
"'contradicts' (conflicts with another), 'extends' (builds upon another).",
|
|
400
|
+
args: {
|
|
401
|
+
action: tool.schema
|
|
402
|
+
.enum(["link", "unlink", "list"])
|
|
403
|
+
.describe(
|
|
404
|
+
"Action: link (create relationship), unlink (remove relationship), " +
|
|
405
|
+
"list (show all links for a memory).",
|
|
406
|
+
),
|
|
407
|
+
source_id: tool.schema
|
|
408
|
+
.string()
|
|
409
|
+
.describe("The source memory ID"),
|
|
410
|
+
target_id: tool.schema
|
|
411
|
+
.string()
|
|
412
|
+
.optional()
|
|
413
|
+
.describe("The target memory ID (required for link/unlink)"),
|
|
414
|
+
relationship: tool.schema
|
|
415
|
+
.enum(["related", "supersedes", "contradicts", "extends"])
|
|
416
|
+
.optional()
|
|
417
|
+
.describe("Relationship type (required for link)"),
|
|
418
|
+
},
|
|
419
|
+
async execute(args) {
|
|
420
|
+
return safeExecute(() => {
|
|
421
|
+
switch (args.action) {
|
|
422
|
+
case "link": {
|
|
423
|
+
if (!args.target_id) return "Target memory ID is required for link action."
|
|
424
|
+
if (!args.relationship) return "Relationship type is required for link action."
|
|
425
|
+
const ok = addLink(args.source_id, args.target_id, args.relationship)
|
|
426
|
+
if (!ok) return "Failed to link: one or both memory IDs not found, or same ID."
|
|
427
|
+
return `Linked ${args.source_id} → ${args.relationship} → ${args.target_id}.`
|
|
428
|
+
}
|
|
429
|
+
case "unlink": {
|
|
430
|
+
if (!args.target_id) return "Target memory ID is required for unlink action."
|
|
431
|
+
const removed = removeLink(args.source_id, args.target_id)
|
|
432
|
+
return removed
|
|
433
|
+
? `Unlinked ${args.source_id} from ${args.target_id}.`
|
|
434
|
+
: "Link not found."
|
|
435
|
+
}
|
|
436
|
+
case "list": {
|
|
437
|
+
const links = getLinksForMemory(args.source_id)
|
|
438
|
+
if (links.length === 0) return `No links for memory ${args.source_id}.`
|
|
439
|
+
const formatted = links
|
|
440
|
+
.map((l, i) => {
|
|
441
|
+
const dir = l.source_id === args.source_id ? "→" : "←"
|
|
442
|
+
return `[${i + 1}] ${dir} ${l.relationship}: ${l.linked_memory.content} (ID: ${l.linked_memory.id})`
|
|
443
|
+
})
|
|
444
|
+
.join("\n")
|
|
445
|
+
return `Links for ${args.source_id}:\n${formatted}`
|
|
446
|
+
}
|
|
447
|
+
default:
|
|
448
|
+
return "Unknown action."
|
|
449
|
+
}
|
|
450
|
+
}, "Failed to manage memory links")
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
|
|
454
|
+
memory_stats: tool({
|
|
455
|
+
description: "Show memory statistics: total count and breakdown by category.",
|
|
456
|
+
args: {},
|
|
457
|
+
async execute() {
|
|
458
|
+
return safeExecute(() => {
|
|
459
|
+
const stats = getStats()
|
|
460
|
+
|
|
461
|
+
if (stats.total === 0) {
|
|
462
|
+
return "No memories stored yet."
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const breakdown = Object.entries(stats.byCategory)
|
|
466
|
+
.map(([cat, count]) => ` ${cat}: ${count}`)
|
|
467
|
+
.join("\n")
|
|
468
|
+
|
|
469
|
+
const sizeBytes = getDbSize()
|
|
470
|
+
const sizeStr =
|
|
471
|
+
sizeBytes > 1048576
|
|
472
|
+
? `${(sizeBytes / 1048576).toFixed(1)} MB`
|
|
473
|
+
: `${(sizeBytes / 1024).toFixed(1)} KB`
|
|
474
|
+
|
|
475
|
+
return `Total memories: ${stats.total}\nDB size: ${sizeStr}\n\nBy category:\n${breakdown}`
|
|
476
|
+
}, "Failed to get memory stats")
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
|
|
480
|
+
memory_cleanup: tool({
|
|
481
|
+
description:
|
|
482
|
+
"Run database maintenance: optimize FTS index, enforce memory limits, " +
|
|
483
|
+
"optionally purge old unused memories or vacuum the database.",
|
|
484
|
+
args: {
|
|
485
|
+
purge_days: tool.schema
|
|
486
|
+
.number()
|
|
487
|
+
.optional()
|
|
488
|
+
.describe(
|
|
489
|
+
"Purge memories older than this many days that have never been accessed. " +
|
|
490
|
+
"Only affects unaccessed memories.",
|
|
491
|
+
),
|
|
492
|
+
vacuum: tool.schema
|
|
493
|
+
.boolean()
|
|
494
|
+
.optional()
|
|
495
|
+
.describe("If true, also vacuum the database to reclaim disk space."),
|
|
496
|
+
},
|
|
497
|
+
async execute(args) {
|
|
498
|
+
return safeExecute(() => {
|
|
499
|
+
const result = runMaintenance()
|
|
500
|
+
const lines = [
|
|
501
|
+
`FTS optimized: ${result.ftsOptimized ? "yes" : "no"}`,
|
|
502
|
+
`Memories trimmed (over limit): ${result.memoriesTrimmed}`,
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
if (args.purge_days) {
|
|
506
|
+
const purged = purgeOldMemories(args.purge_days)
|
|
507
|
+
lines.push(`Purged (older than ${args.purge_days} days, never accessed): ${purged}`)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (args.vacuum) {
|
|
511
|
+
vacuumDb()
|
|
512
|
+
lines.push("Database vacuumed.")
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const sizeBytes = getDbSize()
|
|
516
|
+
const sizeStr =
|
|
517
|
+
sizeBytes > 1048576
|
|
518
|
+
? `${(sizeBytes / 1048576).toFixed(1)} MB`
|
|
519
|
+
: `${(sizeBytes / 1024).toFixed(1)} KB`
|
|
520
|
+
lines.push(`DB size: ${sizeStr}`)
|
|
521
|
+
|
|
522
|
+
return `Maintenance complete:\n${lines.join("\n")}`
|
|
523
|
+
}, "Failed to run maintenance")
|
|
524
|
+
},
|
|
525
|
+
}),
|
|
526
|
+
|
|
527
|
+
memory_export: tool({
|
|
528
|
+
description:
|
|
529
|
+
"Export memories to JSON format for backup or migration. " +
|
|
530
|
+
"Includes all metadata, tags, and relationships.",
|
|
531
|
+
args: {
|
|
532
|
+
category: tool.schema
|
|
533
|
+
.enum([
|
|
534
|
+
"decision", "pattern", "debugging", "preference",
|
|
535
|
+
"convention", "discovery", "general",
|
|
536
|
+
])
|
|
537
|
+
.optional()
|
|
538
|
+
.describe("Filter by category"),
|
|
539
|
+
scope: tool.schema
|
|
540
|
+
.enum(["project", "global", "all"])
|
|
541
|
+
.optional()
|
|
542
|
+
.describe("Filter by scope (default: all)"),
|
|
543
|
+
},
|
|
544
|
+
async execute(args) {
|
|
545
|
+
return safeExecute(() => {
|
|
546
|
+
const memories = listMemories({
|
|
547
|
+
category: args.category,
|
|
548
|
+
projectId,
|
|
549
|
+
scope: args.scope || "all",
|
|
550
|
+
limit: 10000,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
const exportData = {
|
|
554
|
+
version: 1,
|
|
555
|
+
exported_at: new Date().toISOString(),
|
|
556
|
+
memories: memories.map((m) => ({
|
|
557
|
+
id: m.id,
|
|
558
|
+
content: m.content,
|
|
559
|
+
category: m.category,
|
|
560
|
+
source: m.source,
|
|
561
|
+
project_id: m.project_id,
|
|
562
|
+
time_created: m.time_created,
|
|
563
|
+
time_updated: m.time_updated,
|
|
564
|
+
access_count: m.access_count,
|
|
565
|
+
tags: getTagsForMemory(m.id),
|
|
566
|
+
links: getLinksForMemory(m.id).map((l) => ({
|
|
567
|
+
target_id: l.source_id === m.id ? l.target_id : l.source_id,
|
|
568
|
+
relationship: l.relationship,
|
|
569
|
+
})),
|
|
570
|
+
})),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return JSON.stringify(exportData, null, 2)
|
|
574
|
+
}, "Failed to export memories")
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
577
|
+
|
|
578
|
+
memory_import: tool({
|
|
579
|
+
description:
|
|
580
|
+
"Import memories from JSON format (as produced by memory_export). " +
|
|
581
|
+
"Handles ID conflicts by skipping duplicates.",
|
|
582
|
+
args: {
|
|
583
|
+
data: tool.schema
|
|
584
|
+
.string()
|
|
585
|
+
.describe("The JSON string of exported memories to import"),
|
|
586
|
+
},
|
|
587
|
+
async execute(args) {
|
|
588
|
+
return safeExecute(() => {
|
|
589
|
+
let parsed: any
|
|
590
|
+
try {
|
|
591
|
+
parsed = JSON.parse(args.data)
|
|
592
|
+
} catch {
|
|
593
|
+
return "Invalid JSON data."
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!parsed.memories || !Array.isArray(parsed.memories)) {
|
|
597
|
+
return "Invalid export format: missing 'memories' array."
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
let added = 0
|
|
601
|
+
let skipped = 0
|
|
602
|
+
let errors = 0
|
|
603
|
+
const idMap = new Map<string, string>()
|
|
604
|
+
|
|
605
|
+
for (const entry of parsed.memories) {
|
|
606
|
+
try {
|
|
607
|
+
// Skip if memory with same ID already exists
|
|
608
|
+
if (getMemory(entry.id)) {
|
|
609
|
+
idMap.set(entry.id, entry.id)
|
|
610
|
+
skipped++
|
|
611
|
+
continue
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const memory = storeMemory({
|
|
615
|
+
content: entry.content,
|
|
616
|
+
category: entry.category || "general",
|
|
617
|
+
projectId: entry.project_id || undefined,
|
|
618
|
+
source: entry.source || undefined,
|
|
619
|
+
tags: entry.tags,
|
|
620
|
+
global: !entry.project_id,
|
|
621
|
+
force: true,
|
|
622
|
+
})
|
|
623
|
+
idMap.set(entry.id, memory.id)
|
|
624
|
+
added++
|
|
625
|
+
} catch {
|
|
626
|
+
errors++
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Restore links using the ID map
|
|
631
|
+
let linksRestored = 0
|
|
632
|
+
for (const entry of parsed.memories) {
|
|
633
|
+
if (entry.links && Array.isArray(entry.links)) {
|
|
634
|
+
for (const link of entry.links) {
|
|
635
|
+
const sourceId = idMap.get(entry.id)
|
|
636
|
+
const targetId = idMap.get(link.target_id)
|
|
637
|
+
if (sourceId && targetId) {
|
|
638
|
+
try {
|
|
639
|
+
addLink(sourceId, targetId, link.relationship)
|
|
640
|
+
linksRestored++
|
|
641
|
+
} catch { /* skip invalid links */ }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return [
|
|
648
|
+
`Import complete:`,
|
|
649
|
+
` Added: ${added}`,
|
|
650
|
+
` Skipped (existing): ${skipped}`,
|
|
651
|
+
` Errors: ${errors}`,
|
|
652
|
+
` Links restored: ${linksRestored}`,
|
|
653
|
+
].join("\n")
|
|
654
|
+
}, "Failed to import memories")
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
}
|
|
658
|
+
}
|