opencode-mempalace-persistence 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 +214 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +273 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 geco
|
|
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,214 @@
|
|
|
1
|
+
# opencode-mempalace-persistence
|
|
2
|
+
|
|
3
|
+
An OpenCode plugin that automatically saves every conversation to MemPalace and uses stored memory to provide better, context-aware responses. Real-time, zero cron, zero external scripts.
|
|
4
|
+
|
|
5
|
+
> **It just works** — install, use OpenCode, the plugin handles the rest.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How it works in 3 seconds
|
|
12
|
+
|
|
13
|
+
| Without plugin | With plugin |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Every session starts from scratch | The model knows who you are and what you've done |
|
|
16
|
+
| You repeat context each time | Memory is automatic |
|
|
17
|
+
| No Knowledge Graph | Decisions, milestones, problems tracked |
|
|
18
|
+
|
|
19
|
+
The model searches MemPalace on every question (via AGENTS.md + MCP), and the plugin saves every response. A perfect feedback loop.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### 1. Plugin (saves conversations)
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"plugin": ["opencode-mempalace-persistence"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add this line to your `~/.config/opencode/opencode.json` and restart OpenCode.
|
|
34
|
+
|
|
35
|
+
### 2. Instructions for the model (uses memory)
|
|
36
|
+
|
|
37
|
+
Create `~/.config/opencode/AGENTS.md`:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
# Memory & Knowledge instructions
|
|
41
|
+
|
|
42
|
+
Before answering the user, always search your MemPalace memory for relevant context using the MCP tools.
|
|
43
|
+
|
|
44
|
+
1. **Search MemPalace**: Call `mempalace_search` with the user's question or key topics as query. Get the top 5-10 most relevant memory drawers.
|
|
45
|
+
2. **Query Knowledge Graph**: Call `mempalace_kg_query` for entity "user". Then filter the returned facts — keep only those whose text contains keywords from the user's question.
|
|
46
|
+
3. **Use the context**: Incorporate relevant memory and facts into your response, referencing the source naturally when useful.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Identity (who you are)
|
|
50
|
+
|
|
51
|
+
Create `~/.mempalace/identity.txt`:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
I am [name], a [role]. I work with [technologies]. My main projects are [projects].
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This is loaded automatically at session start via `instructions` in opencode.json.
|
|
58
|
+
|
|
59
|
+
### 4. Complete configuration
|
|
60
|
+
|
|
61
|
+
`~/.config/opencode/opencode.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"model": "opencode/deepseek-v4-flash-free",
|
|
66
|
+
"instructions": ["AGENTS.md", "~/.mempalace/identity.txt"],
|
|
67
|
+
"plugin": ["opencode-mempalace-persistence"],
|
|
68
|
+
"mcp": {
|
|
69
|
+
"mempalace": {
|
|
70
|
+
"type": "local",
|
|
71
|
+
"command": ["mempalace-mcp"],
|
|
72
|
+
"enabled": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. MemPalace (if not already installed)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Install
|
|
82
|
+
pipx install mempalace
|
|
83
|
+
|
|
84
|
+
# Create palace
|
|
85
|
+
mempalace init ~/opencode-memory
|
|
86
|
+
|
|
87
|
+
# Configure MCP
|
|
88
|
+
mempalace mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `mempalace mcp` command gives you the exact MCP setup string for your configuration.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## What happens after installation
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
You ask a question
|
|
99
|
+
→ AGENTS.md tells the model: "search MemPalace first"
|
|
100
|
+
→ The model calls mempalace_search("question") via MCP
|
|
101
|
+
→ Finds relevant memories → gives a better answer
|
|
102
|
+
|
|
103
|
+
The model responds
|
|
104
|
+
→ The opencode-mempalace-persistence plugin detects the response is complete
|
|
105
|
+
→ Saves the conversation to MemPalace
|
|
106
|
+
→ Extracts Knowledge Graph facts
|
|
107
|
+
|
|
108
|
+
Next time you ask
|
|
109
|
+
→ The model finds the previous memory → coherent responses
|
|
110
|
+
→ The cycle continues, memory grows
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## What gets saved
|
|
116
|
+
|
|
117
|
+
Every turn (question + answer):
|
|
118
|
+
|
|
119
|
+
- **Text** categorized by wing (developer, creative, emotions, family, consciousness)
|
|
120
|
+
- **Knowledge Graph**: automatically extracted facts
|
|
121
|
+
- `decision` → "decided to use TypeScript"
|
|
122
|
+
- `milestone` → "backend deploy completed"
|
|
123
|
+
- `problem` → "chromadb ModuleNotFoundError"
|
|
124
|
+
- `preference` → "prefer Svelte over React"
|
|
125
|
+
- `emotional` → "frustrated with Docker compose"
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
┌──────────────────────────┐
|
|
133
|
+
│ OpenCode │
|
|
134
|
+
│ │
|
|
135
|
+
User msg ─────►│ chat.message hook │
|
|
136
|
+
│ ↓ │
|
|
137
|
+
│ Query OpenCode DB │
|
|
138
|
+
│ (messages since lastSync)│
|
|
139
|
+
│ ↓ │
|
|
140
|
+
│ Categorize by wing │
|
|
141
|
+
│ ↓ │
|
|
142
|
+
│ Export delta → tmp │
|
|
143
|
+
│ ↓ │
|
|
144
|
+
│ Save state immediately │
|
|
145
|
+
│ ↓ │
|
|
146
|
+
│ mempalace mine (async) │ ← non-blocking
|
|
147
|
+
│ ↓ │
|
|
148
|
+
│ Extract KG facts │
|
|
149
|
+
│ ↓ │
|
|
150
|
+
Session idle ─►│ session.idle hook │
|
|
151
|
+
│ (saves last turn) │
|
|
152
|
+
└──────────────────────────┘
|
|
153
|
+
│
|
|
154
|
+
▼
|
|
155
|
+
┌──────────────────────────┐
|
|
156
|
+
│ MemPalace │
|
|
157
|
+
│ ~/opencode-memory/ │
|
|
158
|
+
│ Vector DB + KG SQLite │
|
|
159
|
+
└──────────────────────────┘
|
|
160
|
+
▲
|
|
161
|
+
│
|
|
162
|
+
┌──────────────────────────┐
|
|
163
|
+
│ AGENTS.md + MCP │
|
|
164
|
+
│ The model searches │
|
|
165
|
+
│ MemPalace on every │
|
|
166
|
+
│ question │
|
|
167
|
+
└──────────────────────────┘
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Relevant files
|
|
173
|
+
|
|
174
|
+
| File | Purpose |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `~/.config/opencode/opencode.json` | OpenCode config with plugin + MCP + instructions |
|
|
177
|
+
| `~/.config/opencode/AGENTS.md` | Tells the model to search MemPalace |
|
|
178
|
+
| `~/.mempalace/identity.txt` | Your identity (loaded every session) |
|
|
179
|
+
| `~/.mempalace/config.json` | MemPalace config (palace path, wings, keywords) |
|
|
180
|
+
| `~/.mempalace/knowledge_graph.sqlite3` | Knowledge Graph (structured facts) |
|
|
181
|
+
| `~/opencode-memory/` | MemPalace vector DB (all drawers) |
|
|
182
|
+
| `~/.mempalace/sync_state.json` | Last sync state (plugin + Python script) |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Install from npm
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"plugin": ["opencode-mempalace-persistence"]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Local development
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"plugin": ["/path/to/opencode-mempalace-persistence/dist/index.js"]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Debug logging
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
export OPENCODE_MEMPALACE_DEBUG=1
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
When set, the plugin writes a debug log to `/tmp/opencode-mempalace.log`.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const _default: () => Promise<{
|
|
2
|
+
"chat.message": (_input: {
|
|
3
|
+
sessionID: string;
|
|
4
|
+
agent?: string;
|
|
5
|
+
model?: {
|
|
6
|
+
providerID: string;
|
|
7
|
+
modelID: string;
|
|
8
|
+
};
|
|
9
|
+
messageID?: string;
|
|
10
|
+
variant?: string;
|
|
11
|
+
}, output: {
|
|
12
|
+
message: import("@opencode-ai/sdk").UserMessage;
|
|
13
|
+
parts: import("@opencode-ai/sdk").Part[];
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
event: ({ event }: any) => Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { execSync, exec } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, rmdirSync, unlinkSync, appendFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
const HOME = homedir();
|
|
7
|
+
const VENV_PYTHON = join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3");
|
|
8
|
+
const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
|
|
9
|
+
const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
|
|
10
|
+
const STATE_FILE = join(HOME, ".mempalace/sync_state.json");
|
|
11
|
+
const KG_DB = join(HOME, ".mempalace/knowledge_graph.sqlite3");
|
|
12
|
+
const OUT_DIR = "/tmp/oc-sessions";
|
|
13
|
+
const TMP_SCRIPT = "/tmp/oc-plugin-query.py";
|
|
14
|
+
const DEBUG = !!process.env.OPENCODE_MEMPALACE_DEBUG;
|
|
15
|
+
const LOG_FILE = "/tmp/opencode-mempalace.log";
|
|
16
|
+
function log(msg) {
|
|
17
|
+
if (!DEBUG)
|
|
18
|
+
return;
|
|
19
|
+
const ts = new Date().toISOString();
|
|
20
|
+
try {
|
|
21
|
+
appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
let pendingSync = false;
|
|
26
|
+
let lastSyncTs = 0;
|
|
27
|
+
function runPython(code) {
|
|
28
|
+
writeFileSync(TMP_SCRIPT, code);
|
|
29
|
+
try {
|
|
30
|
+
return execSync(`${VENV_PYTHON} ${TMP_SCRIPT}`, { encoding: "utf-8", timeout: 30000 }).trim();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
unlinkSync(TMP_SCRIPT);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const WING_KEYWORDS = {
|
|
37
|
+
creative: ["eurovision", "music", "art", "film", "game", "festival", "song", "concert"],
|
|
38
|
+
family: ["family", "kid", "son", "daughter", "wife", "husband", "parent", "mother", "father"],
|
|
39
|
+
emotions: ["feel", "happy", "sad", "love", "hate", "angry", "fear", "anxiety", "grateful"],
|
|
40
|
+
consciousness: ["think", "mind", "conscious", "self", "identity", "exist", "meaning"],
|
|
41
|
+
};
|
|
42
|
+
const KG_PATTERNS = {
|
|
43
|
+
decision: ["ho deciso", "ho scelto", "implementato", "added ", "ho aggiunto", "decided", "chosen", "created", "creato", "modified"],
|
|
44
|
+
milestone: ["completato", "finito", "rilasciato", "deployato", "ho completato", "successo", "funziona", "completed", "done", "deployed"],
|
|
45
|
+
problem: ["problema", "bug", "errore", "issue", "fix", "fixato", "risolto", "solved", "fixed", "error"],
|
|
46
|
+
preference: ["preferisco", "mi piace", "non mi piace", "prefer", "like to", "better"],
|
|
47
|
+
emotional: ["sentimento", "emozione", "frustrato", "contento", "happy", "sad", "feel", "emotion"],
|
|
48
|
+
};
|
|
49
|
+
function categorize(text) {
|
|
50
|
+
if (!text)
|
|
51
|
+
return "developer";
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
for (const [wing, keywords] of Object.entries(WING_KEYWORDS)) {
|
|
54
|
+
if (keywords.some((kw) => lower.includes(kw)))
|
|
55
|
+
return wing;
|
|
56
|
+
}
|
|
57
|
+
return "developer";
|
|
58
|
+
}
|
|
59
|
+
function hasText(parts) {
|
|
60
|
+
return parts
|
|
61
|
+
.filter((p) => p?.type === "text" && p?.text?.trim())
|
|
62
|
+
.map((p) => p.text.trim())
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
65
|
+
function getLastSync() {
|
|
66
|
+
if (!existsSync(STATE_FILE))
|
|
67
|
+
return 0;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8")).last_sync_ms || 0;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function dbSync() {
|
|
76
|
+
if (pendingSync)
|
|
77
|
+
return;
|
|
78
|
+
pendingSync = true;
|
|
79
|
+
try {
|
|
80
|
+
doDbSync();
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
log("sync err: " + String(e));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function doDbSync() {
|
|
87
|
+
if (lastSyncTs && Date.now() - lastSyncTs < 5000)
|
|
88
|
+
return;
|
|
89
|
+
const lastSync = getLastSync();
|
|
90
|
+
const sessions = runPython(`
|
|
91
|
+
import sqlite3, json
|
|
92
|
+
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
93
|
+
rows = db.execute("""
|
|
94
|
+
SELECT DISTINCT s.id, s.title, p.worktree, s.directory, s.slug, s.time_created
|
|
95
|
+
FROM session s
|
|
96
|
+
LEFT JOIN project p ON s.project_id = p.id
|
|
97
|
+
INNER JOIN message m ON m.session_id = s.id
|
|
98
|
+
WHERE m.time_created > ${lastSync}
|
|
99
|
+
ORDER BY s.time_created
|
|
100
|
+
""").fetchall()
|
|
101
|
+
db.close()
|
|
102
|
+
print(json.dumps(rows))
|
|
103
|
+
`);
|
|
104
|
+
let sessionsArr;
|
|
105
|
+
try {
|
|
106
|
+
sessionsArr = JSON.parse(sessions);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
pendingSync = false;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!sessionsArr || sessionsArr.length === 0) {
|
|
113
|
+
pendingSync = false;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const exported = [];
|
|
118
|
+
for (const sess of sessionsArr) {
|
|
119
|
+
const [sessId, title, worktree, directory] = sess;
|
|
120
|
+
const label = (title || "").replace(/[^a-zA-Z0-9 _-]/g, "_") || (sessId || "").slice(0, 12);
|
|
121
|
+
const prefix = `${new Date().toISOString().slice(0, 10)}_${label.slice(0, 30)}_${(sessId || "").slice(0, 8)}`;
|
|
122
|
+
const msgs = runPython(`
|
|
123
|
+
import sqlite3, json
|
|
124
|
+
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
125
|
+
rows = db.execute("""
|
|
126
|
+
SELECT m.id, m.time_created, m.data FROM message m
|
|
127
|
+
WHERE m.session_id = ${JSON.stringify(sessId)} AND m.time_created > ${lastSync}
|
|
128
|
+
ORDER BY m.time_created
|
|
129
|
+
""").fetchall()
|
|
130
|
+
texts = []
|
|
131
|
+
for (mid, mts, mdata_raw) in rows:
|
|
132
|
+
try: mdata = json.loads(mdata_raw)
|
|
133
|
+
except: mdata = {}
|
|
134
|
+
role = mdata.get("role", "unknown")
|
|
135
|
+
for (pdata_raw,) in db.execute("SELECT data FROM part WHERE message_id = ? ORDER BY time_created", (mid,)).fetchall():
|
|
136
|
+
try:
|
|
137
|
+
pdata = json.loads(pdata_raw)
|
|
138
|
+
if pdata.get("type") == "text" and pdata.get("text","").strip():
|
|
139
|
+
texts.append({"role": role, "text": pdata.get("text").strip(), "ts": mts})
|
|
140
|
+
except: pass
|
|
141
|
+
db.close()
|
|
142
|
+
print(json.dumps(texts))
|
|
143
|
+
`);
|
|
144
|
+
let msgList;
|
|
145
|
+
try {
|
|
146
|
+
msgList = JSON.parse(msgs);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (msgList.length < 2)
|
|
152
|
+
continue;
|
|
153
|
+
const lines = [
|
|
154
|
+
`# ${title || label}`,
|
|
155
|
+
`Date: ${new Date().toISOString().slice(0, 10)}`,
|
|
156
|
+
`Session: ${sessId}`,
|
|
157
|
+
"",
|
|
158
|
+
];
|
|
159
|
+
for (const m of msgList) {
|
|
160
|
+
const ts = m.ts ? new Date(m.ts).toISOString().slice(11, 19) : "";
|
|
161
|
+
lines.push(`## ${m.role.toUpperCase()} — ${ts}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(m.text);
|
|
164
|
+
lines.push("");
|
|
165
|
+
}
|
|
166
|
+
const content = lines.join("\n").trim();
|
|
167
|
+
if (!content)
|
|
168
|
+
continue;
|
|
169
|
+
const wing = categorize(content);
|
|
170
|
+
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
171
|
+
const fname = `sync_${prefix}_${contentHash}.txt`;
|
|
172
|
+
const wingDir = join(OUT_DIR, wing);
|
|
173
|
+
mkdirSync(wingDir, { recursive: true });
|
|
174
|
+
writeFileSync(join(wingDir, fname), content + "\n");
|
|
175
|
+
exported.push({ wing, fname, dir: wingDir });
|
|
176
|
+
}
|
|
177
|
+
if (exported.length === 0) {
|
|
178
|
+
pendingSync = false;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Save state immediately so next sync doesn't re-process
|
|
182
|
+
writeFileSync(STATE_FILE, JSON.stringify({ last_sync_ms: now }));
|
|
183
|
+
lastSyncTs = Date.now();
|
|
184
|
+
pendingSync = false;
|
|
185
|
+
log(`queued ${exported.length} files for mining`);
|
|
186
|
+
// Mining async (fire-and-forget, non-blocking)
|
|
187
|
+
for (const { wing, fname, dir: wingDir } of exported) {
|
|
188
|
+
const filePath = join(wingDir, fname);
|
|
189
|
+
exec(`${MEMPALACE_BIN} mine ${wingDir} --mode convos --extract general --wing ${wing}`, {
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
timeout: 120000,
|
|
192
|
+
}, (err) => {
|
|
193
|
+
if (err) {
|
|
194
|
+
log(`mine err ${wing}: ${err.message}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
rmSync(filePath);
|
|
198
|
+
try {
|
|
199
|
+
rmdirSync(wingDir);
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
log(`mined ${wing}`);
|
|
203
|
+
// KG extraction after mining
|
|
204
|
+
try {
|
|
205
|
+
const raw = runPython(`
|
|
206
|
+
from mempalace.config import MempalaceConfig
|
|
207
|
+
import chromadb, json, re
|
|
208
|
+
c = MempalaceConfig()
|
|
209
|
+
col = chromadb.PersistentClient(path=c.palace_path).get_collection(c.collection_name)
|
|
210
|
+
d = col.get(limit=5000, include=["metadatas","documents"])
|
|
211
|
+
p = ${JSON.stringify(KG_PATTERNS)}
|
|
212
|
+
r = []
|
|
213
|
+
for i,m,doc in zip(d.get("ids",[]), d.get("metadatas",[]), d.get("documents",[])):
|
|
214
|
+
if not m or not doc: continue
|
|
215
|
+
rm = m.get("room","")
|
|
216
|
+
if rm not in ("decision","milestone","problem","preference","emotional"): continue
|
|
217
|
+
da = doc.split("Date: ")[1][:10] if "Date: " in doc else ""
|
|
218
|
+
if not da: continue
|
|
219
|
+
if not any(pat in doc.lower() for pat in p.get(rm,[])): continue
|
|
220
|
+
for ln in doc.strip().split("\\n")[:5]:
|
|
221
|
+
l = ln.strip()
|
|
222
|
+
if l and not l.startswith("#") and len(l) > 20 and len(re.findall(r"[^a-zA-Z0-9\\s]", l))/len(l) <= 0.3:
|
|
223
|
+
r.append([da, rm, l[:120]]); break
|
|
224
|
+
print(json.dumps(r))
|
|
225
|
+
`);
|
|
226
|
+
const nf = JSON.parse(raw) || [];
|
|
227
|
+
if (nf.length > 0) {
|
|
228
|
+
const ex = runPython(`import sqlite3,json;d=sqlite3.connect(${JSON.stringify(KG_DB)});r=d.execute("SELECT predicate,object FROM triples WHERE subject='user'").fetchall();d.close();print(json.dumps(r))`);
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
try {
|
|
231
|
+
for (const [p, o] of JSON.parse(ex))
|
|
232
|
+
seen.add(p + "::" + o);
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
const ins = [];
|
|
236
|
+
for (const [d, t, x] of nf) {
|
|
237
|
+
const k = t + "::" + x;
|
|
238
|
+
if (seen.has(k))
|
|
239
|
+
continue;
|
|
240
|
+
seen.add(k);
|
|
241
|
+
ins.push(`INSERT OR IGNORE INTO triples(id,subject,predicate,object,valid_from,confidence,extracted_at) VALUES('t_user_${t}_${createHash("sha256").update(x).digest("hex").slice(0, 12)}','user','${t}','${x.replace(/'/g, "''")}','${d}',1.0,datetime('now'))`);
|
|
242
|
+
}
|
|
243
|
+
if (ins.length > 0) {
|
|
244
|
+
runPython(`import sqlite3;db=sqlite3.connect(${JSON.stringify(KG_DB)});[db.execute(s) for s in ${JSON.stringify(ins)}];db.commit();db.close()`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export default (async () => {
|
|
253
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
254
|
+
log("loaded");
|
|
255
|
+
return {
|
|
256
|
+
"chat.message": async (_input, output) => {
|
|
257
|
+
const role = output.message.role;
|
|
258
|
+
if (role !== "user")
|
|
259
|
+
return;
|
|
260
|
+
const text = hasText(output.parts || []);
|
|
261
|
+
if (!text)
|
|
262
|
+
return;
|
|
263
|
+
log("user msg - queue sync");
|
|
264
|
+
setTimeout(() => dbSync(), 500);
|
|
265
|
+
},
|
|
266
|
+
event: async ({ event }) => {
|
|
267
|
+
if (event?.type !== "session.idle")
|
|
268
|
+
return;
|
|
269
|
+
log("idle - queue sync");
|
|
270
|
+
setTimeout(() => dbSync(), 3000);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-mempalace-persistence",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugin — auto-sync conversations to MemPalace memory in real-time with auto-categorization and Knowledge Graph",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["opencode", "mempalace", "memory", "opencode-plugin", "typescript"],
|
|
13
|
+
"author": "geco",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/geco/opencode-mempalace.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/geco/opencode-mempalace/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/geco/opencode-mempalace#readme",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@opencode-ai/plugin": "^1.15.0",
|
|
26
|
+
"@types/node": "^25.8.0",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|