@knikolov/opencode-plugin-simple-memory 1.0.0 → 1.0.2
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/README.md +62 -67
- package/index.ts +2 -0
- package/package.json +10 -32
- package/src/index.ts +359 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12637
package/README.md
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Simple Memory Plugin for OpenCode
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@knikolov/opencode-plugin-simple-memory)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
5
|
|
|
3
6
|
A persistent memory plugin for [OpenCode](https://opencode.ai) that enables the AI assistant to remember context across sessions.
|
|
4
7
|
|
|
5
|
-
##
|
|
8
|
+
## Setup
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- **Memory Types**: Support for decisions, learnings, preferences, blockers, context, and patterns
|
|
9
|
-
- **Scoped Organization**: Organize memories by scope (e.g., user, project, api, auth)
|
|
10
|
-
- **Search & Filtering**: Query memories by scope, type, or free-text search
|
|
11
|
-
- **Audit Trail**: Deletions are logged for accountability
|
|
10
|
+
1. Add the plugin to your [OpenCode config](https://opencode.ai/docs/config/):
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"$schema": "https://opencode.ai/config.json",
|
|
15
|
+
"plugin": ["@knikolov/opencode-plugin-simple-memory"]
|
|
16
|
+
}
|
|
17
|
+
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
2. Start using memory commands in your conversations.
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
23
36
|
```
|
|
24
37
|
|
|
25
38
|
## Tools
|
|
@@ -43,81 +56,63 @@ The plugin provides five tools:
|
|
|
43
56
|
- `context` - General context information
|
|
44
57
|
- `pattern` - Recurring patterns or conventions
|
|
45
58
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
Memories are stored in `.opencode/memory/` as daily logfmt files:
|
|
59
|
+
## Example Usage
|
|
49
60
|
|
|
50
61
|
```
|
|
51
|
-
|
|
52
|
-
2025-12-15.logfmt
|
|
53
|
-
deletions.logfmt # audit log
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Example Chat
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
User: What do you know about me?
|
|
62
|
+
User: Remember that my name is Kris
|
|
60
63
|
|
|
61
|
-
AI:
|
|
62
|
-
|
|
64
|
+
AI: [calls memory_remember with type="context", scope="user",
|
|
65
|
+
content="User's name is Kris"]
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Total memories: 2
|
|
67
|
-
Scopes:
|
|
68
|
-
user: 2 (context)
|
|
69
|
-
Types:
|
|
70
|
-
context: 2
|
|
71
|
-
|
|
72
|
-
[calls memory_recall with scope="user"]
|
|
73
|
-
|
|
74
|
-
Found 2 memories:
|
|
75
|
-
[2025-12-15] context/user: User's name is Kris
|
|
76
|
-
[2025-12-15] context/user: Passionate about Software Engineering
|
|
77
|
-
|
|
78
|
-
Based on the stored memories, here's what I know about you:
|
|
79
|
-
1. Name: Kris
|
|
80
|
-
2. Interest: You're passionate about Software Engineering
|
|
67
|
+
I've stored that your name is Kris.
|
|
81
68
|
|
|
82
69
|
User: My preferred programming language is Erlang
|
|
83
70
|
|
|
84
71
|
AI: [calls memory_remember with type="preference", scope="user",
|
|
85
72
|
content="Preferred programming language is Erlang"]
|
|
86
73
|
|
|
87
|
-
Got it
|
|
88
|
-
A solid choice for concurrent, fault-tolerant systems!
|
|
74
|
+
Got it! I've noted that your preferred programming language is Erlang.
|
|
89
75
|
|
|
90
|
-
User: What do you know about me
|
|
76
|
+
User: What do you know about me?
|
|
91
77
|
|
|
92
78
|
AI: [calls memory_recall with scope="user"]
|
|
93
79
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
[2025-12-15] preference/user: Preferred programming language is Erlang
|
|
98
|
-
|
|
99
|
-
Here's everything I know about you:
|
|
100
|
-
1. Name: Kris
|
|
101
|
-
2. Passionate about Software Engineering
|
|
102
|
-
3. Preferred programming language: Erlang
|
|
80
|
+
Here's what I know about you:
|
|
81
|
+
- Your name is Kris
|
|
82
|
+
- Your preferred programming language is Erlang
|
|
103
83
|
|
|
104
|
-
User: Actually, I
|
|
84
|
+
User: Actually, I prefer Elixir now.
|
|
105
85
|
|
|
106
86
|
AI: [calls memory_update with type="preference", scope="user",
|
|
107
87
|
content="Preferred programming language is Elixir"]
|
|
108
88
|
|
|
109
|
-
Updated
|
|
110
|
-
get the Erlang VM benefits with a more modern syntax.
|
|
89
|
+
Updated your language preference to Elixir.
|
|
111
90
|
|
|
112
|
-
User: Forget my language preference
|
|
91
|
+
User: Forget my language preference
|
|
113
92
|
|
|
114
93
|
AI: [calls memory_forget with type="preference", scope="user",
|
|
115
|
-
reason="User requested removal
|
|
94
|
+
reason="User requested removal"]
|
|
116
95
|
|
|
117
|
-
Done. I've removed your language preference from memory.
|
|
118
|
-
been logged for audit purposes.
|
|
96
|
+
Done. I've removed your language preference from memory.
|
|
119
97
|
```
|
|
120
98
|
|
|
121
|
-
##
|
|
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
|
+
```
|
|
122
117
|
|
|
123
|
-
|
|
118
|
+
Replace `/absolute/path/to/opencode-plugin-simple-memory` with your actual path.
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,42 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knikolov/opencode-plugin-simple-memory",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"version": "1.0.2",
|
|
5
|
+
"author": "knikolov",
|
|
6
|
+
"repository": "https://github.com/cnicolov/opencode-plugin-simple-memory",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src"
|
|
9
10
|
],
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target node && bun run build:types",
|
|
12
|
-
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
13
|
-
"prepublishOnly": "bun run build"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"opencode",
|
|
17
|
-
"opencode-plugin",
|
|
18
|
-
"memory",
|
|
19
|
-
"ai",
|
|
20
|
-
"assistant",
|
|
21
|
-
"context"
|
|
22
|
-
],
|
|
23
|
-
"author": "Kris",
|
|
24
11
|
"license": "MIT",
|
|
25
|
-
"
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/knikolov/opencode-plugin-simple-memory.git"
|
|
28
|
-
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"@opencode-ai/plugin": "^1.0.153"
|
|
31
|
-
},
|
|
12
|
+
"type": "module",
|
|
32
13
|
"devDependencies": {
|
|
33
|
-
"
|
|
34
|
-
"@types/bun": "
|
|
14
|
+
"@opencode-ai/plugin": "^1.0.153",
|
|
15
|
+
"@types/bun": "latest"
|
|
35
16
|
},
|
|
36
17
|
"peerDependencies": {
|
|
37
|
-
"
|
|
38
|
-
},
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=18.0.0"
|
|
18
|
+
"typescript": "^5"
|
|
41
19
|
}
|
|
42
20
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const MEMORY_DIR = ".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
|
package/dist/index.d.ts
DELETED
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AAsVvD,eAAO,MAAM,YAAY,EAAE,MAU1B,CAAA;AAED,eAAe,YAAY,CAAA"}
|