opencode-memory 1.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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/index.ts +2 -0
- package/package.json +20 -0
- package/src/index.ts +359 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Simple Memory Plugin for OpenCode
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@knikolov/opencode-plugin-simple-memory)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A persistent memory plugin for [OpenCode](https://opencode.ai) that enables the AI assistant to remember context across sessions.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
1. Add the plugin to your [OpenCode config](https://opencode.ai/docs/config/):
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"$schema": "https://opencode.ai/config.json",
|
|
15
|
+
"plugin": ["@knikolov/opencode-plugin-simple-memory"]
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Start using memory commands in your conversations.
|
|
20
|
+
|
|
21
|
+
Memories are stored in `.opencode/memory/` as daily logfmt files.
|
|
22
|
+
|
|
23
|
+
## Updating
|
|
24
|
+
|
|
25
|
+
> [!WARNING]
|
|
26
|
+
> OpenCode does NOT auto-update plugins.
|
|
27
|
+
|
|
28
|
+
To get the latest version, clear the cached plugin and let OpenCode reinstall it:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Remove the plugin from cache
|
|
32
|
+
rm -rf ~/.cache/opencode/node_modules/@knikolov/opencode-plugin-simple-memory
|
|
33
|
+
|
|
34
|
+
# Run OpenCode to trigger reinstall
|
|
35
|
+
opencode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
The plugin provides five tools:
|
|
41
|
+
|
|
42
|
+
| Tool | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `memory_remember` | Store a new memory |
|
|
45
|
+
| `memory_recall` | Retrieve memories by scope, type, or search query |
|
|
46
|
+
| `memory_update` | Update an existing memory |
|
|
47
|
+
| `memory_forget` | Delete a memory (with audit logging) |
|
|
48
|
+
| `memory_list` | List all scopes and types for discovery |
|
|
49
|
+
|
|
50
|
+
## Memory Types
|
|
51
|
+
|
|
52
|
+
- `decision` - Architectural or design decisions
|
|
53
|
+
- `learning` - Things learned during development
|
|
54
|
+
- `preference` - User or project preferences
|
|
55
|
+
- `blocker` - Current blockers or issues
|
|
56
|
+
- `context` - General context information
|
|
57
|
+
- `pattern` - Recurring patterns or conventions
|
|
58
|
+
|
|
59
|
+
## Example Usage
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
User: Remember that my name is Kris
|
|
63
|
+
|
|
64
|
+
AI: [calls memory_remember with type="context", scope="user",
|
|
65
|
+
content="User's name is Kris"]
|
|
66
|
+
|
|
67
|
+
I've stored that your name is Kris.
|
|
68
|
+
|
|
69
|
+
User: My preferred programming language is Erlang
|
|
70
|
+
|
|
71
|
+
AI: [calls memory_remember with type="preference", scope="user",
|
|
72
|
+
content="Preferred programming language is Erlang"]
|
|
73
|
+
|
|
74
|
+
Got it! I've noted that your preferred programming language is Erlang.
|
|
75
|
+
|
|
76
|
+
User: What do you know about me?
|
|
77
|
+
|
|
78
|
+
AI: [calls memory_recall with scope="user"]
|
|
79
|
+
|
|
80
|
+
Here's what I know about you:
|
|
81
|
+
- Your name is Kris
|
|
82
|
+
- Your preferred programming language is Erlang
|
|
83
|
+
|
|
84
|
+
User: Actually, I prefer Elixir now.
|
|
85
|
+
|
|
86
|
+
AI: [calls memory_update with type="preference", scope="user",
|
|
87
|
+
content="Preferred programming language is Elixir"]
|
|
88
|
+
|
|
89
|
+
Updated your language preference to Elixir.
|
|
90
|
+
|
|
91
|
+
User: Forget my language preference
|
|
92
|
+
|
|
93
|
+
AI: [calls memory_forget with type="preference", scope="user",
|
|
94
|
+
reason="User requested removal"]
|
|
95
|
+
|
|
96
|
+
Done. I've removed your language preference from memory.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Local Development
|
|
100
|
+
|
|
101
|
+
Clone the repository and install dependencies:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone https://github.com/cnicolov/opencode-plugin-simple-memory.git
|
|
105
|
+
cd opencode-plugin-simple-memory
|
|
106
|
+
bun install
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Point your OpenCode config to the local checkout via a `file://` URL:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"$schema": "https://opencode.ai/config.json",
|
|
114
|
+
"plugin": ["file:///absolute/path/to/opencode-plugin-simple-memory"]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Replace `/absolute/path/to/opencode-plugin-simple-memory` with your actual path.
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-memory",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "knikolov",
|
|
6
|
+
"repository": "https://github.com/shuans/opencode-memory",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@opencode-ai/plugin": "^1.0.153",
|
|
15
|
+
"@types/bun": "latest"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const MEMORY_DIR = "~/.config/opencode/memory"
|
|
4
|
+
|
|
5
|
+
const getMemoryFile = () => {
|
|
6
|
+
const date = new Date().toISOString().split("T")[0]
|
|
7
|
+
return Bun.file(`${MEMORY_DIR}/${date}.logfmt`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ensureDir = async () => {
|
|
11
|
+
const dir = Bun.file(MEMORY_DIR)
|
|
12
|
+
if (!(await dir.exists())) {
|
|
13
|
+
await Bun.$`mkdir -p ${MEMORY_DIR}`
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Memory {
|
|
18
|
+
ts: string
|
|
19
|
+
type: string
|
|
20
|
+
scope: string
|
|
21
|
+
content: string
|
|
22
|
+
issue?: string
|
|
23
|
+
tags?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parseLine = (line: string): Memory | null => {
|
|
27
|
+
const tsMatch = line.match(/ts=([^\s]+)/)
|
|
28
|
+
const typeMatch = line.match(/type=([^\s]+)/)
|
|
29
|
+
const scopeMatch = line.match(/scope=([^\s]+)/)
|
|
30
|
+
const contentMatch = line.match(/content="([^"]*(?:\\"[^"]*)*)"/)
|
|
31
|
+
const issueMatch = line.match(/issue=([^\s]+)/)
|
|
32
|
+
const tagsMatch = line.match(/tags=([^\s]+)/)
|
|
33
|
+
|
|
34
|
+
if (!tsMatch?.[1] || !typeMatch?.[1] || !scopeMatch?.[1]) return null
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ts: tsMatch[1],
|
|
38
|
+
type: typeMatch[1],
|
|
39
|
+
scope: scopeMatch[1],
|
|
40
|
+
content: contentMatch?.[1]?.replace(/\\"/g, '"') || "",
|
|
41
|
+
issue: issueMatch?.[1],
|
|
42
|
+
tags: tagsMatch?.[1]?.split(","),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatMemory = (m: Memory): string => {
|
|
47
|
+
const date = m.ts.split("T")[0]
|
|
48
|
+
const tags = m.tags?.length ? ` [${m.tags.join(", ")}]` : ""
|
|
49
|
+
const issue = m.issue ? ` (${m.issue})` : ""
|
|
50
|
+
return `[${date}] ${m.type}/${m.scope}: ${m.content}${issue}${tags}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scoreMatch = (memory: Memory, words: string[]): number => {
|
|
54
|
+
const searchable = `${memory.type} ${memory.scope} ${memory.content} ${memory.tags?.join(" ") || ""}`.toLowerCase()
|
|
55
|
+
let score = 0
|
|
56
|
+
for (const word of words) {
|
|
57
|
+
if (searchable.includes(word)) score++
|
|
58
|
+
if (memory.scope.toLowerCase() === word) score += 2
|
|
59
|
+
if (memory.type.toLowerCase() === word) score += 2
|
|
60
|
+
}
|
|
61
|
+
return score
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const remember = tool({
|
|
65
|
+
description: "Store a memory (decision, learning, preference, blocker, context, pattern)",
|
|
66
|
+
args: {
|
|
67
|
+
type: tool.schema
|
|
68
|
+
.enum(["decision", "learning", "preference", "blocker", "context", "pattern"])
|
|
69
|
+
.describe("Type of memory"),
|
|
70
|
+
scope: tool.schema.string().describe("Scope/area (e.g., auth, api, mobile)"),
|
|
71
|
+
content: tool.schema.string().describe("The memory content"),
|
|
72
|
+
issue: tool.schema.string().optional().describe("Related GitHub issue (e.g., #51)"),
|
|
73
|
+
tags: tool.schema.array(tool.schema.string()).optional().describe("Additional tags"),
|
|
74
|
+
},
|
|
75
|
+
async execute(args) {
|
|
76
|
+
await ensureDir()
|
|
77
|
+
|
|
78
|
+
const ts = new Date().toISOString()
|
|
79
|
+
const issue = args.issue ? ` issue=${args.issue}` : ""
|
|
80
|
+
const tags = args.tags?.length ? ` tags=${args.tags.join(",")}` : ""
|
|
81
|
+
const content = args.content.replace(/"/g, '\\"')
|
|
82
|
+
const line = `ts=${ts} type=${args.type} scope=${args.scope} content="${content}"${issue}${tags}\n`
|
|
83
|
+
|
|
84
|
+
const file = getMemoryFile()
|
|
85
|
+
const existing = (await file.exists()) ? await file.text() : ""
|
|
86
|
+
await Bun.write(file, existing + line)
|
|
87
|
+
|
|
88
|
+
return `Remembered: ${args.type} in ${args.scope}`
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const getAllMemories = async (): Promise<Memory[]> => {
|
|
93
|
+
const glob = new Bun.Glob("*.logfmt")
|
|
94
|
+
const files = await Array.fromAsync(glob.scan(MEMORY_DIR))
|
|
95
|
+
|
|
96
|
+
if (!files.length) return []
|
|
97
|
+
|
|
98
|
+
const lines: string[] = []
|
|
99
|
+
for (const filename of files) {
|
|
100
|
+
if (filename === "deletions.logfmt") continue // skip audit log
|
|
101
|
+
const file = Bun.file(`${MEMORY_DIR}/${filename}`)
|
|
102
|
+
const text = await file.text()
|
|
103
|
+
lines.push(...text.trim().split("\n").filter(Boolean))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return lines.map(parseLine).filter((m): m is Memory => m !== null)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const logDeletion = async (memory: Memory, reason: string) => {
|
|
110
|
+
await ensureDir()
|
|
111
|
+
const ts = new Date().toISOString()
|
|
112
|
+
const content = memory.content.replace(/"/g, '\\"')
|
|
113
|
+
const originalTs = memory.ts
|
|
114
|
+
const issue = memory.issue ? ` issue=${memory.issue}` : ""
|
|
115
|
+
const tags = memory.tags?.length ? ` tags=${memory.tags.join(",")}` : ""
|
|
116
|
+
const escapedReason = reason.replace(/"/g, '\\"')
|
|
117
|
+
const line = `ts=${ts} action=deleted original_ts=${originalTs} type=${memory.type} scope=${memory.scope} content="${content}" reason="${escapedReason}"${issue}${tags}\n`
|
|
118
|
+
|
|
119
|
+
const file = Bun.file(`${MEMORY_DIR}/deletions.logfmt`)
|
|
120
|
+
const existing = (await file.exists()) ? await file.text() : ""
|
|
121
|
+
await Bun.write(file, existing + line)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const recall = tool({
|
|
125
|
+
description: "Retrieve memories by scope, type, or search query",
|
|
126
|
+
args: {
|
|
127
|
+
scope: tool.schema.string().optional().describe("Filter by scope"),
|
|
128
|
+
type: tool.schema
|
|
129
|
+
.enum(["decision", "learning", "preference", "blocker", "context", "pattern"])
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Filter by type"),
|
|
132
|
+
query: tool.schema.string().optional().describe("Search term (space-separated words, matches any)"),
|
|
133
|
+
limit: tool.schema.number().optional().describe("Max results (default 20)"),
|
|
134
|
+
},
|
|
135
|
+
async execute(args) {
|
|
136
|
+
let results = await getAllMemories()
|
|
137
|
+
|
|
138
|
+
if (!results.length) return "No memories found"
|
|
139
|
+
|
|
140
|
+
const totalCount = results.length
|
|
141
|
+
|
|
142
|
+
if (args.scope) {
|
|
143
|
+
results = results.filter((m) => m.scope === args.scope || m.scope.includes(args.scope!))
|
|
144
|
+
}
|
|
145
|
+
if (args.type) {
|
|
146
|
+
results = results.filter((m) => m.type === args.type)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (args.query) {
|
|
150
|
+
const words = args.query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
151
|
+
const scored = results
|
|
152
|
+
.map((m) => ({ memory: m, score: scoreMatch(m, words) }))
|
|
153
|
+
.filter((x) => x.score > 0)
|
|
154
|
+
.sort((a, b) => b.score - a.score)
|
|
155
|
+
results = scored.map((x) => x.memory)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const filteredCount = results.length
|
|
159
|
+
const limit = args.limit || 20
|
|
160
|
+
const limited = results.slice(-limit)
|
|
161
|
+
|
|
162
|
+
if (!limited.length) return "No matching memories"
|
|
163
|
+
|
|
164
|
+
const header = filteredCount > limit
|
|
165
|
+
? `Found ${filteredCount} memories (showing last ${limit} of ${totalCount} total)\n\n`
|
|
166
|
+
: filteredCount !== totalCount
|
|
167
|
+
? `Found ${filteredCount} memories (${totalCount} total)\n\n`
|
|
168
|
+
: `Found ${filteredCount} memories\n\n`
|
|
169
|
+
|
|
170
|
+
return header + limited.map(formatMemory).join("\n")
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const update = tool({
|
|
175
|
+
description: "Update an existing memory by scope and type (finds matching memory and updates its content)",
|
|
176
|
+
args: {
|
|
177
|
+
scope: tool.schema.string().describe("Scope of memory to update"),
|
|
178
|
+
type: tool.schema
|
|
179
|
+
.enum(["decision", "learning", "preference", "blocker", "context", "pattern"])
|
|
180
|
+
.describe("Type of memory"),
|
|
181
|
+
content: tool.schema.string().describe("The new content for the memory"),
|
|
182
|
+
query: tool.schema.string().optional().describe("Search term to find specific memory if multiple exist"),
|
|
183
|
+
issue: tool.schema.string().optional().describe("Update related GitHub issue (e.g., #51)"),
|
|
184
|
+
tags: tool.schema.array(tool.schema.string()).optional().describe("Update tags"),
|
|
185
|
+
},
|
|
186
|
+
async execute(args) {
|
|
187
|
+
const glob = new Bun.Glob("*.logfmt")
|
|
188
|
+
const files = await Array.fromAsync(glob.scan(MEMORY_DIR))
|
|
189
|
+
|
|
190
|
+
if (!files.length) return "No memory files found"
|
|
191
|
+
|
|
192
|
+
// Find matching memories
|
|
193
|
+
const matches: { memory: Memory; filepath: string; lineIndex: number }[] = []
|
|
194
|
+
|
|
195
|
+
for (const filename of files) {
|
|
196
|
+
if (filename === "deletions.logfmt") continue
|
|
197
|
+
const filepath = `${MEMORY_DIR}/${filename}`
|
|
198
|
+
const file = Bun.file(filepath)
|
|
199
|
+
const text = await file.text()
|
|
200
|
+
const lines = text.split("\n")
|
|
201
|
+
|
|
202
|
+
lines.forEach((line, lineIndex) => {
|
|
203
|
+
const memory = parseLine(line)
|
|
204
|
+
if (!memory) return
|
|
205
|
+
if (memory.scope === args.scope && memory.type === args.type) {
|
|
206
|
+
matches.push({ memory, filepath, lineIndex })
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (matches.length === 0) {
|
|
212
|
+
return `No memories found for ${args.type} in ${args.scope}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If multiple matches and query provided, filter by query
|
|
216
|
+
let target: typeof matches[number] | undefined = matches[0]
|
|
217
|
+
if (matches.length > 1) {
|
|
218
|
+
if (args.query) {
|
|
219
|
+
const words = args.query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
220
|
+
const scored = matches
|
|
221
|
+
.map((m) => ({ ...m, score: scoreMatch(m.memory, words) }))
|
|
222
|
+
.filter((x) => x.score > 0)
|
|
223
|
+
.sort((a, b) => b.score - a.score)
|
|
224
|
+
|
|
225
|
+
if (scored.length === 0) {
|
|
226
|
+
return `Found ${matches.length} memories for ${args.type}/${args.scope}, but none matched query "${args.query}". Use recall to see all matches.`
|
|
227
|
+
}
|
|
228
|
+
target = scored[0]
|
|
229
|
+
} else {
|
|
230
|
+
return `Found ${matches.length} memories for ${args.type}/${args.scope}. Provide a query to select which one to update, or use recall to see all matches.`
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!target) {
|
|
235
|
+
return `No memories found for ${args.type} in ${args.scope}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Log the old version before updating
|
|
239
|
+
await logDeletion(target.memory, `Updated to: ${args.content}`)
|
|
240
|
+
|
|
241
|
+
// Update the memory
|
|
242
|
+
const file = Bun.file(target.filepath)
|
|
243
|
+
const text = await file.text()
|
|
244
|
+
const lines = text.split("\n")
|
|
245
|
+
|
|
246
|
+
const ts = new Date().toISOString()
|
|
247
|
+
const issue = args.issue !== undefined ? args.issue : target.memory.issue
|
|
248
|
+
const tags = args.tags !== undefined ? args.tags : target.memory.tags
|
|
249
|
+
const issueStr = issue ? ` issue=${issue}` : ""
|
|
250
|
+
const tagsStr = tags?.length ? ` tags=${tags.join(",")}` : ""
|
|
251
|
+
const content = args.content.replace(/"/g, '\\"')
|
|
252
|
+
const newLine = `ts=${ts} type=${args.type} scope=${args.scope} content="${content}"${issueStr}${tagsStr}`
|
|
253
|
+
|
|
254
|
+
lines[target.lineIndex] = newLine
|
|
255
|
+
await Bun.write(target.filepath, lines.join("\n"))
|
|
256
|
+
|
|
257
|
+
return `Updated ${args.type} in ${args.scope}: "${args.content}"`
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const listMemories = tool({
|
|
262
|
+
description: "List all unique scopes and types in memory for discovery",
|
|
263
|
+
args: {},
|
|
264
|
+
async execute() {
|
|
265
|
+
const memories = await getAllMemories()
|
|
266
|
+
|
|
267
|
+
if (!memories.length) return "No memories found"
|
|
268
|
+
|
|
269
|
+
const scopes = new Map<string, number>()
|
|
270
|
+
const types = new Map<string, number>()
|
|
271
|
+
const scopeTypes = new Map<string, Set<string>>()
|
|
272
|
+
|
|
273
|
+
for (const m of memories) {
|
|
274
|
+
scopes.set(m.scope, (scopes.get(m.scope) || 0) + 1)
|
|
275
|
+
types.set(m.type, (types.get(m.type) || 0) + 1)
|
|
276
|
+
if (!scopeTypes.has(m.scope)) scopeTypes.set(m.scope, new Set())
|
|
277
|
+
scopeTypes.get(m.scope)!.add(m.type)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const lines: string[] = []
|
|
281
|
+
lines.push(`Total memories: ${memories.length}`)
|
|
282
|
+
lines.push("")
|
|
283
|
+
lines.push("Scopes:")
|
|
284
|
+
for (const [scope, count] of [...scopes.entries()].sort((a, b) => b[1] - a[1])) {
|
|
285
|
+
const typeList = [...scopeTypes.get(scope)!].join(", ")
|
|
286
|
+
lines.push(` ${scope}: ${count} (${typeList})`)
|
|
287
|
+
}
|
|
288
|
+
lines.push("")
|
|
289
|
+
lines.push("Types:")
|
|
290
|
+
for (const [type, count] of [...types.entries()].sort((a, b) => b[1] - a[1])) {
|
|
291
|
+
lines.push(` ${type}: ${count}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return lines.join("\n")
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const forget = tool({
|
|
299
|
+
description: "Delete a memory by scope and type (removes matching lines from all memory files, logs deletion for audit)",
|
|
300
|
+
args: {
|
|
301
|
+
scope: tool.schema.string().describe("Scope of memory to delete"),
|
|
302
|
+
type: tool.schema
|
|
303
|
+
.enum(["decision", "learning", "preference", "blocker", "context", "pattern"])
|
|
304
|
+
.describe("Type of memory"),
|
|
305
|
+
reason: tool.schema.string().describe("Why this is being deleted (for audit purposes)"),
|
|
306
|
+
},
|
|
307
|
+
async execute(args) {
|
|
308
|
+
const glob = new Bun.Glob("*.logfmt")
|
|
309
|
+
const files = await Array.fromAsync(glob.scan(MEMORY_DIR))
|
|
310
|
+
|
|
311
|
+
if (!files.length) return "No memory files found"
|
|
312
|
+
|
|
313
|
+
let deleted = 0
|
|
314
|
+
const deletedMemories: Memory[] = []
|
|
315
|
+
|
|
316
|
+
for (const filename of files) {
|
|
317
|
+
if (filename === "deletions.logfmt") continue // skip audit log
|
|
318
|
+
const filepath = `${MEMORY_DIR}/${filename}`
|
|
319
|
+
const file = Bun.file(filepath)
|
|
320
|
+
const text = await file.text()
|
|
321
|
+
const lines = text.split("\n")
|
|
322
|
+
const filtered = lines.filter((line) => {
|
|
323
|
+
const memory = parseLine(line)
|
|
324
|
+
if (!memory) return true
|
|
325
|
+
if (memory.scope === args.scope && memory.type === args.type) {
|
|
326
|
+
deleted++
|
|
327
|
+
deletedMemories.push(memory)
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
return true
|
|
331
|
+
})
|
|
332
|
+
if (filtered.length !== lines.length) {
|
|
333
|
+
await Bun.write(filepath, filtered.join("\n"))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Log all deletions to audit file
|
|
338
|
+
for (const memory of deletedMemories) {
|
|
339
|
+
await logDeletion(memory, args.reason)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (deleted === 0) return `No memories found for ${args.type} in ${args.scope}`
|
|
343
|
+
return `Deleted ${deleted} ${args.type} memory(s) from ${args.scope}. Reason: ${args.reason}\nDeletions logged to ${MEMORY_DIR}/deletions.logfmt`
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
export const MemoryPlugin: Plugin = async (_ctx) => {
|
|
348
|
+
return {
|
|
349
|
+
tool: {
|
|
350
|
+
memory_remember: remember,
|
|
351
|
+
memory_recall: recall,
|
|
352
|
+
memory_update: update,
|
|
353
|
+
memory_forget: forget,
|
|
354
|
+
memory_list: listMemories,
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export default MemoryPlugin
|