@parallelclaw/memex-openclaw 0.1.0 → 0.1.1
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 +42 -2
- package/index.js +94 -65
- package/lib/store.js +22 -1
- package/lib/tools.js +142 -0
- package/openclaw.plugin.json +2 -3
- package/package.json +1 -1
- package/lib/corpus_supplement.js +0 -131
package/README.md
CHANGED
|
@@ -9,17 +9,23 @@ Replaces the v0.11.x `memex-sync` file-watcher daemon. Captures via OpenClaw's n
|
|
|
9
9
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
|
-
Three lifecycle hooks +
|
|
12
|
+
Three lifecycle hooks + two LLM-facing tools:
|
|
13
13
|
|
|
14
14
|
| Hook | What we do |
|
|
15
15
|
|---|---|
|
|
16
16
|
| `agent_end` | Insert the just-completed turn's user + assistant messages into memex.db, verbatim. Channel comes from `ctx.messageProvider` — no parsing. |
|
|
17
17
|
| `before_compaction` | Save messages that are about to be dropped from active context. They become searchable from memex even after OpenClaw forgets them. |
|
|
18
18
|
| `session_end` | Update conversation last_ts. Safety-net marker. |
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
| Tool | What it does |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `memex_search(query, limit?)` | FTS5 lexical search across all captured sources. Returns IDs + 100-char previews (cheap — Tier 1 of progressive disclosure). |
|
|
23
|
+
| `memex_get(ids)` | Full verbatim text by record ID. Use after `memex_search` to read the records that look relevant (Tier 2). |
|
|
20
24
|
|
|
21
25
|
Storage: `~/.memex/data/memex.db` (override via plugin config `dbPath`). Same SQLite schema as memex-mvp (npm) and memex-hermes (pip) — all three can write to the same DB concurrently.
|
|
22
26
|
|
|
27
|
+
> **v0.1.0 used `registerMemoryCorpusSupplement` to surface memex content through OpenClaw's built-in `memory_search`.** Turned out that API is not exported to npm-installed (external) plugins in OpenClaw 2026.5.x — only to bundled ones. v0.1.1 switched to standalone tools, which work everywhere.
|
|
28
|
+
|
|
23
29
|
## Install
|
|
24
30
|
|
|
25
31
|
Once published to npm:
|
|
@@ -74,6 +80,40 @@ Restart OpenClaw:
|
|
|
74
80
|
openclaw gateway restart
|
|
75
81
|
```
|
|
76
82
|
|
|
83
|
+
### If `MemexStore` fails to open (better-sqlite3 native binary missing)
|
|
84
|
+
|
|
85
|
+
`openclaw plugins install` may run npm install with `--ignore-scripts`, which skips better-sqlite3's postinstall script that downloads the prebuilt native binary. Result: at runtime the plugin can't open the DB.
|
|
86
|
+
|
|
87
|
+
Manual fix:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd ~/.openclaw/npm/node_modules/@parallelclaw/memex-openclaw
|
|
91
|
+
npm rebuild better-sqlite3
|
|
92
|
+
# On low-memory VPS where gyp rebuild OOMs, force prebuilt-only:
|
|
93
|
+
# npm rebuild better-sqlite3 --build-from-source=false
|
|
94
|
+
openclaw gateway restart
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Bug-1 diagnostic (v0.1.1)
|
|
98
|
+
|
|
99
|
+
If after install the plugin appears `loaded` in `openclaw plugins inspect memex-openclaw` but no rows are captured, check the diagnostic trace file we write at the top of every `register()` invocation:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cat /tmp/memex-openclaw-debug.log
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A correct sequence looks like:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
2026-... module loaded (top-level)
|
|
109
|
+
2026-... register() called — gateway recognised plugin
|
|
110
|
+
2026-... store opened: ~/.memex/data/memex.db, rows=N
|
|
111
|
+
2026-... tools registered: memex_search, memex_get
|
|
112
|
+
2026-... register() returned — hooks active
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If only the first line is present and nothing else fires on `openclaw gateway restart`, OpenClaw is not invoking the plugin's `register()` function for external (npm-installed) plugins. Open an issue on the repo with the full diagnostic file content.
|
|
116
|
+
|
|
77
117
|
## Conversation routing
|
|
78
118
|
|
|
79
119
|
| OpenClaw context | memex conversation_id |
|
package/index.js
CHANGED
|
@@ -2,34 +2,45 @@
|
|
|
2
2
|
* memex-openclaw — OpenClaw plugin that captures every turn verbatim
|
|
3
3
|
* into the memex unified SQLite corpus.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* •
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `ctx.messageProvider` (e.g. "telegram") and `ctx.channelId` (e.g.
|
|
15
|
-
* "97592799") directly in the hook context.
|
|
16
|
-
*
|
|
17
|
-
* Storage: ~/.memex/data/memex.db (override via plugin config db_path).
|
|
18
|
-
* Schema parity with memex-mvp (npm) and memex-hermes (pip) — all three
|
|
19
|
-
* write to the same database with the same UNIQUE-constraint dedup.
|
|
5
|
+
* v0.1.1 changes from 0.1.0:
|
|
6
|
+
* • Removed `kind: "memory"` — conflicted with memory-core's exclusive
|
|
7
|
+
* memory slot (bug 3 from 2026-05-21 VPS test report).
|
|
8
|
+
* • Replaced `registerMemoryCorpusSupplement` with `api.registerTool`
|
|
9
|
+
* — corpus supplement API is not exported to external (npm) plugins
|
|
10
|
+
* in OpenClaw 2026.5.x (bug 2).
|
|
11
|
+
* • Hardened defensive logging: writes to /tmp/memex-openclaw-debug.log
|
|
12
|
+
* at the very first line of register() so we can verify register()
|
|
13
|
+
* fires at all on gateway restart (bug 1 diagnostic instrumentation).
|
|
20
14
|
*
|
|
21
15
|
* @see openclaw.plugin.json for manifest
|
|
22
16
|
* @see package.json for npm distribution
|
|
23
17
|
*/
|
|
24
18
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
registerMemoryCorpusSupplement,
|
|
28
|
-
} from 'openclaw/plugin-sdk/core';
|
|
19
|
+
import { definePluginEntry } from 'openclaw/plugin-sdk/core';
|
|
20
|
+
import { appendFileSync } from 'node:fs';
|
|
29
21
|
|
|
30
22
|
import { MemexStore } from './lib/store.js';
|
|
31
23
|
import { deriveConvId, deriveMsgId, extractText } from './lib/conv_id.js';
|
|
32
|
-
import {
|
|
24
|
+
import { registerMemexTools } from './lib/tools.js';
|
|
25
|
+
|
|
26
|
+
// v0.1.1 BUG-1 DIAGNOSTIC: trace register() invocations to a file the
|
|
27
|
+
// user can grep. v0.1.0 had a problem where register() was called
|
|
28
|
+
// during `openclaw plugins install` but NOT on `gateway restart` for
|
|
29
|
+
// external (npm-installed) plugins. This trace will tell us if the
|
|
30
|
+
// problem persists in 0.1.1 or got fixed by changes to manifest.
|
|
31
|
+
function traceRegister(msg) {
|
|
32
|
+
try {
|
|
33
|
+
appendFileSync(
|
|
34
|
+
'/tmp/memex-openclaw-debug.log',
|
|
35
|
+
`[${new Date().toISOString()}] ${msg}\n`,
|
|
36
|
+
{ mode: 0o644 },
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
/* /tmp not writable? whatever — diag-only */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
traceRegister('module loaded (top-level)');
|
|
33
44
|
|
|
34
45
|
export default definePluginEntry({
|
|
35
46
|
id: 'memex-openclaw',
|
|
@@ -37,55 +48,66 @@ export default definePluginEntry({
|
|
|
37
48
|
description:
|
|
38
49
|
'Captures every OpenClaw turn verbatim into the memex unified SQLite corpus. ' +
|
|
39
50
|
'Pair with memex-mvp (npm) to search OpenClaw + Hermes + Claude Code + Telegram from one place.',
|
|
40
|
-
kind: 'memory',
|
|
41
51
|
|
|
42
52
|
register(api) {
|
|
53
|
+
traceRegister('register() called — gateway recognised plugin');
|
|
54
|
+
|
|
43
55
|
const logger = api.logger;
|
|
44
56
|
const cfg = api.pluginConfig || {};
|
|
45
57
|
let store;
|
|
46
58
|
|
|
47
59
|
try {
|
|
48
60
|
store = new MemexStore(cfg.dbPath);
|
|
49
|
-
|
|
61
|
+
const initialCount = store.count();
|
|
62
|
+
logger.info(
|
|
63
|
+
`memex-openclaw: opened ${store.dbPath} (current rows: ${initialCount})`,
|
|
64
|
+
);
|
|
65
|
+
traceRegister(`store opened: ${store.dbPath}, rows=${initialCount}`);
|
|
50
66
|
} catch (err) {
|
|
51
67
|
logger.error(`memex-openclaw: failed to open memex.db: ${err.message}`);
|
|
52
|
-
|
|
68
|
+
traceRegister(`store open FAILED: ${err.message}`);
|
|
69
|
+
// v0.1.1: do NOT early-return. We still register tools (even if they
|
|
70
|
+
// fail later) so the user can see the plugin is at least live and
|
|
71
|
+
// diagnose. Capture hooks will no-op cleanly if store is null.
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
//
|
|
56
|
-
// 1. Primary capture — every turn
|
|
57
|
-
//
|
|
74
|
+
// ------------------------------------------------------------
|
|
75
|
+
// 1. Primary capture — every successful turn
|
|
76
|
+
// ------------------------------------------------------------
|
|
58
77
|
api.on('agent_end', async (event, ctx) => {
|
|
59
|
-
if (!
|
|
78
|
+
if (!store) return;
|
|
79
|
+
if (!event?.success) return;
|
|
60
80
|
try {
|
|
61
81
|
const platform = ctx?.messageProvider || 'unknown';
|
|
62
82
|
const channelId = ctx?.channelId;
|
|
63
83
|
const sessionId = ctx?.sessionId;
|
|
64
84
|
const agentId = ctx?.agentId || 'main';
|
|
65
|
-
const convId = deriveConvId({
|
|
85
|
+
const convId = deriveConvId({
|
|
86
|
+
messageProvider: platform,
|
|
87
|
+
channelId,
|
|
88
|
+
sessionId,
|
|
89
|
+
});
|
|
66
90
|
|
|
67
|
-
// Capture only the LAST TURN's user + assistant messages
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// re-inserting harmless but wasteful.)
|
|
91
|
+
// Capture only the LAST TURN's user + assistant messages.
|
|
92
|
+
// OpenClaw passes full history but earlier turns were already
|
|
93
|
+
// captured by prior agent_end invocations; UNIQUE dedup makes
|
|
94
|
+
// re-insertion harmless but wasteful.
|
|
72
95
|
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
73
96
|
const lastTurn = messages.slice(-2);
|
|
74
|
-
|
|
75
97
|
const baseTs = Math.floor(Date.now() / 1000);
|
|
98
|
+
|
|
76
99
|
for (let i = 0; i < lastTurn.length; i++) {
|
|
77
100
|
const msg = lastTurn[i];
|
|
78
101
|
if (!msg || (msg.role !== 'user' && msg.role !== 'assistant')) continue;
|
|
79
102
|
const text = extractText(msg);
|
|
80
103
|
if (!text || !text.trim()) continue;
|
|
81
104
|
|
|
82
|
-
const msgId = deriveMsgId({ role: msg.role, text, convId });
|
|
83
105
|
store.insertMessage({
|
|
84
106
|
conversationId: convId,
|
|
85
|
-
msgId,
|
|
107
|
+
msgId: deriveMsgId({ role: msg.role, text, convId }),
|
|
86
108
|
role: msg.role,
|
|
87
109
|
text,
|
|
88
|
-
ts: baseTs + i,
|
|
110
|
+
ts: baseTs + i,
|
|
89
111
|
channel: platform,
|
|
90
112
|
metadata: {
|
|
91
113
|
raw_type: 'openclaw-agent-end',
|
|
@@ -100,7 +122,6 @@ export default definePluginEntry({
|
|
|
100
122
|
});
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
// Keep conversations.last_ts current.
|
|
104
125
|
store.upsertConversation({
|
|
105
126
|
conversationId: convId,
|
|
106
127
|
title: convId,
|
|
@@ -112,10 +133,11 @@ export default definePluginEntry({
|
|
|
112
133
|
}
|
|
113
134
|
});
|
|
114
135
|
|
|
115
|
-
//
|
|
136
|
+
// ------------------------------------------------------------
|
|
116
137
|
// 2. Preserve messages before they're compacted out of context
|
|
117
|
-
//
|
|
138
|
+
// ------------------------------------------------------------
|
|
118
139
|
api.on('before_compaction', async (event, ctx) => {
|
|
140
|
+
if (!store) return;
|
|
119
141
|
try {
|
|
120
142
|
const messages = Array.isArray(event?.messages) ? event.messages : [];
|
|
121
143
|
if (messages.length === 0) return;
|
|
@@ -123,9 +145,13 @@ export default definePluginEntry({
|
|
|
123
145
|
const platform = ctx?.messageProvider || 'unknown';
|
|
124
146
|
const channelId = ctx?.channelId;
|
|
125
147
|
const sessionId = ctx?.sessionId;
|
|
126
|
-
const convId = deriveConvId({
|
|
127
|
-
|
|
148
|
+
const convId = deriveConvId({
|
|
149
|
+
messageProvider: platform,
|
|
150
|
+
channelId,
|
|
151
|
+
sessionId,
|
|
152
|
+
});
|
|
128
153
|
const baseTs = Math.floor(Date.now() / 1000);
|
|
154
|
+
|
|
129
155
|
let saved = 0;
|
|
130
156
|
for (let i = 0; i < messages.length; i++) {
|
|
131
157
|
const msg = messages[i];
|
|
@@ -158,45 +184,48 @@ export default definePluginEntry({
|
|
|
158
184
|
}
|
|
159
185
|
});
|
|
160
186
|
|
|
161
|
-
//
|
|
162
|
-
// 3. Session-end safety net
|
|
163
|
-
//
|
|
187
|
+
// ------------------------------------------------------------
|
|
188
|
+
// 3. Session-end safety net
|
|
189
|
+
// ------------------------------------------------------------
|
|
164
190
|
api.on('session_end', async (event, ctx) => {
|
|
191
|
+
if (!store) return;
|
|
165
192
|
try {
|
|
166
|
-
// session_end doesn't carry messages[] in OpenClaw 2026.5 —
|
|
167
|
-
// we have sessionId + sessionKey + reason. The hook serves as
|
|
168
|
-
// a marker that this conv is "done"; we update conv last_ts
|
|
169
|
-
// and let agent_end captures already-in-DB stand.
|
|
170
193
|
const platform = ctx?.messageProvider || 'unknown';
|
|
171
194
|
const channelId = ctx?.channelId;
|
|
172
195
|
const sessionId = event?.sessionId || ctx?.sessionId;
|
|
173
|
-
const convId = deriveConvId({
|
|
196
|
+
const convId = deriveConvId({
|
|
197
|
+
messageProvider: platform,
|
|
198
|
+
channelId,
|
|
199
|
+
sessionId,
|
|
200
|
+
});
|
|
174
201
|
store.upsertConversation({
|
|
175
202
|
conversationId: convId,
|
|
176
203
|
title: convId,
|
|
177
204
|
lastTs: Math.floor(Date.now() / 1000),
|
|
178
205
|
});
|
|
179
|
-
logger.debug(`memex-openclaw: session_end conv=${convId} reason=${event?.reason}`);
|
|
180
206
|
} catch (err) {
|
|
181
207
|
logger.error(`memex-openclaw: session_end failed: ${err.message}`);
|
|
182
208
|
}
|
|
183
209
|
});
|
|
184
210
|
|
|
185
|
-
//
|
|
186
|
-
// 4.
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`memex-openclaw:
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
// ------------------------------------------------------------
|
|
212
|
+
// 4. Register tools the LLM can call directly
|
|
213
|
+
// (v0.1.1: replaces registerMemoryCorpusSupplement which is
|
|
214
|
+
// not exported to external plugins in OpenClaw 2026.5.x)
|
|
215
|
+
// ------------------------------------------------------------
|
|
216
|
+
if (store) {
|
|
217
|
+
try {
|
|
218
|
+
registerMemexTools(api, store, logger);
|
|
219
|
+
traceRegister('tools registered: memex_search, memex_get');
|
|
220
|
+
} catch (err) {
|
|
221
|
+
logger.error(`memex-openclaw: tool registration failed: ${err.message}`);
|
|
222
|
+
traceRegister(`tool registration FAILED: ${err.message}`);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
logger.warn('memex-openclaw: store unavailable, skipping tool registration');
|
|
198
226
|
}
|
|
199
227
|
|
|
200
|
-
logger.info('memex-openclaw: plugin activated');
|
|
228
|
+
logger.info('memex-openclaw: plugin activated (v0.1.1)');
|
|
229
|
+
traceRegister('register() returned — hooks active');
|
|
201
230
|
},
|
|
202
231
|
});
|
package/lib/store.js
CHANGED
|
@@ -14,7 +14,28 @@
|
|
|
14
14
|
* while we write.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
// v0.1.1: dynamic import + helpful error message for environments where
|
|
18
|
+
// better-sqlite3's native binary failed to install (small-VPS OOM during
|
|
19
|
+
// gyp rebuild, --ignore-scripts during plugin install, etc.). Bug 4 from
|
|
20
|
+
// the 2026-05-21 VPS test report.
|
|
21
|
+
let Database;
|
|
22
|
+
try {
|
|
23
|
+
Database = (await import('better-sqlite3')).default;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const helpful = new Error(
|
|
26
|
+
'memex-openclaw: failed to load better-sqlite3 native binary.\n' +
|
|
27
|
+
' Original error: ' + err.message + '\n' +
|
|
28
|
+
' This usually means the prebuilt binary download failed during\n' +
|
|
29
|
+
' `openclaw plugins install` (often blocked by --ignore-scripts).\n' +
|
|
30
|
+
' Manual fix:\n' +
|
|
31
|
+
' cd ~/.openclaw/npm/node_modules/@parallelclaw/memex-openclaw\n' +
|
|
32
|
+
' npm rebuild better-sqlite3\n' +
|
|
33
|
+
' Then `openclaw gateway restart`.\n' +
|
|
34
|
+
' On low-memory VPS where rebuild OOMs: `npm rebuild better-sqlite3 --build-from-source=false` to force prebuilt-only.',
|
|
35
|
+
);
|
|
36
|
+
helpful.cause = err;
|
|
37
|
+
throw helpful;
|
|
38
|
+
}
|
|
18
39
|
import { mkdirSync } from 'node:fs';
|
|
19
40
|
import { dirname } from 'node:path';
|
|
20
41
|
import { homedir } from 'node:os';
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memex tool definitions for OpenClaw's `api.registerTool()`.
|
|
3
|
+
*
|
|
4
|
+
* v0.1.0 attempted to expose memex contents via OpenClaw's bundled-only
|
|
5
|
+
* `registerMemoryCorpusSupplement()` — that function turned out NOT to
|
|
6
|
+
* be exported to external (npm-installed) plugins. v0.1.1 switches to
|
|
7
|
+
* the universally-available `api.registerTool()` API.
|
|
8
|
+
*
|
|
9
|
+
* Trade-off: the model now has TWO distinct tools (memex_search +
|
|
10
|
+
* memex_get) instead of the OpenClaw-merged memory_search experience.
|
|
11
|
+
* In exchange we don't depend on internal API surface.
|
|
12
|
+
*
|
|
13
|
+
* Progressive disclosure pattern preserved:
|
|
14
|
+
* memex_search(query) → IDs + 100-char previews (cheap, Tier 1)
|
|
15
|
+
* memex_get(ids) → full verbatim text (only when needed, Tier 2)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register memex_search + memex_get tools on the OpenClaw plugin API.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} api - OpenClaw plugin API (passed to register(api))
|
|
22
|
+
* @param {object} store - MemexStore instance
|
|
23
|
+
* @param {object} logger - api.logger
|
|
24
|
+
*/
|
|
25
|
+
export function registerMemexTools(api, store, logger) {
|
|
26
|
+
// Tool 1: memex_search — Tier 1 (cheap, returns IDs + previews)
|
|
27
|
+
api.registerTool('memex_search', {
|
|
28
|
+
description:
|
|
29
|
+
'Search the memex verbatim corpus across all captured sources (OpenClaw + Hermes + Claude Code + Telegram + etc.). ' +
|
|
30
|
+
'Returns abbreviated records — id, role, channel, 100-char preview. ' +
|
|
31
|
+
'Call memex_get(ids) afterwards to fetch full text. ' +
|
|
32
|
+
'Use this BEFORE memex_get to find what is relevant; rarely call memex_get directly.',
|
|
33
|
+
parameters: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
query: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description:
|
|
39
|
+
'FTS5 query. Simple keywords work; phrases use double quotes. ' +
|
|
40
|
+
'Example: install ffmpeg or "docker compose" production.',
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: 'integer',
|
|
44
|
+
description: 'Max results (default 10, max 50).',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ['query'],
|
|
48
|
+
},
|
|
49
|
+
handler: async ({ query, limit }) => {
|
|
50
|
+
try {
|
|
51
|
+
const rows = store.search(query || '', limit || 10);
|
|
52
|
+
if (!rows.length) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: JSON.stringify({
|
|
57
|
+
results: [],
|
|
58
|
+
hint: 'No matches. Try different keywords.',
|
|
59
|
+
}),
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: JSON.stringify({
|
|
67
|
+
results: rows,
|
|
68
|
+
count: rows.length,
|
|
69
|
+
hint: 'Call memex_get(ids=[...]) for full verbatim text of records you want to read.',
|
|
70
|
+
}),
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger?.error?.(`memex_search failed: ${err.message}`);
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: JSON.stringify({ error: err.message }),
|
|
79
|
+
}],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Tool 2: memex_get — Tier 2 (full text by ID)
|
|
86
|
+
api.registerTool('memex_get', {
|
|
87
|
+
description:
|
|
88
|
+
'Fetch full verbatim text of specific records by ID. ' +
|
|
89
|
+
'Call this after memex_search to read the records that look relevant. ' +
|
|
90
|
+
'Returns the original text in full, not a summary.',
|
|
91
|
+
parameters: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
ids: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: { type: 'integer' },
|
|
97
|
+
description: 'Record IDs returned by memex_search.',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ['ids'],
|
|
101
|
+
},
|
|
102
|
+
handler: async ({ ids }) => {
|
|
103
|
+
try {
|
|
104
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: JSON.stringify({ error: 'ids must be a non-empty array of integers' }),
|
|
109
|
+
}],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Cap at 20 to avoid runaway token usage.
|
|
113
|
+
const capped = ids.slice(0, 20);
|
|
114
|
+
const truncated = ids.length > capped.length;
|
|
115
|
+
const records = capped
|
|
116
|
+
.map((id) => store.getById(id))
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
const out = { records, count: records.length };
|
|
119
|
+
if (truncated) {
|
|
120
|
+
out.truncated = true;
|
|
121
|
+
out.hint = `Capped at 20 records; ${ids.length - 20} more were not fetched.`;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
content: [{
|
|
125
|
+
type: 'text',
|
|
126
|
+
text: JSON.stringify(out),
|
|
127
|
+
}],
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger?.error?.(`memex_get failed: ${err.message}`);
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: JSON.stringify({ error: err.message }),
|
|
135
|
+
}],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
logger?.info?.('memex-openclaw: registered tools memex_search, memex_get');
|
|
142
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
"id": "memex-openclaw",
|
|
3
3
|
"name": "Memex",
|
|
4
4
|
"description": "Captures every OpenClaw turn verbatim into the memex unified SQLite corpus. Pair with memex-mvp (npm) to search OpenClaw + Hermes + Claude Code + Telegram from one place.",
|
|
5
|
-
"version": "0.1.
|
|
6
|
-
"kind": "memory",
|
|
5
|
+
"version": "0.1.1",
|
|
7
6
|
"activation": {
|
|
8
7
|
"onStartup": true
|
|
9
8
|
},
|
|
10
9
|
"contracts": {
|
|
11
|
-
"
|
|
10
|
+
"tools": ["memex_search", "memex_get"]
|
|
12
11
|
},
|
|
13
12
|
"uiHints": {
|
|
14
13
|
"category": "memory",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallelclaw/memex-openclaw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenClaw plugin that captures every turn verbatim into the memex unified SQLite corpus. Pair with memex-mvp for one searchable history across OpenClaw, Hermes, Claude Code, Telegram, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/lib/corpus_supplement.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MemoryCorpusSupplement adapter — exposes memex contents to OpenClaw's
|
|
3
|
-
* built-in `memory_search` and `memory_get` tools.
|
|
4
|
-
*
|
|
5
|
-
* The OpenClaw runtime ships memory-core, which provides workspace-based
|
|
6
|
-
* memory (MEMORY.md / USER.md / memory/YYYY-MM-DD.md). Plugins can
|
|
7
|
-
* REGISTER A SUPPLEMENT — additional rows that show up in the same tool
|
|
8
|
-
* output, prefixed/labelled so the model knows which corpus a row came
|
|
9
|
-
* from.
|
|
10
|
-
*
|
|
11
|
-
* This is the strategic positioning: memex doesn't REPLACE memory-core,
|
|
12
|
-
* it ADDS to it. The model sees a single `memory_search` tool, gets
|
|
13
|
-
* results from BOTH built-in workspace memory AND memex's verbatim
|
|
14
|
-
* cross-client corpus, in one shot.
|
|
15
|
-
*
|
|
16
|
-
* Multi-plugin coexistence: Mem0, Memoria, memex can all register
|
|
17
|
-
* their own supplements. OpenClaw merges results.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const CORPUS_LABEL = 'memex';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Build the supplement object that gets handed to OpenClaw's
|
|
24
|
-
* `registerMemoryCorpusSupplement(pluginId, supplement)` call.
|
|
25
|
-
*
|
|
26
|
-
* Contract (from OpenClaw 2026.5.4 plugin-sdk/memory-state.d.ts):
|
|
27
|
-
*
|
|
28
|
-
* supplement.search({ query, maxResults, agentSessionKey })
|
|
29
|
-
* → Promise<MemoryCorpusSearchResult[]>
|
|
30
|
-
*
|
|
31
|
-
* supplement.get({ lookup, fromLine, lineCount, agentSessionKey })
|
|
32
|
-
* → Promise<MemoryCorpusGetResult | null>
|
|
33
|
-
*/
|
|
34
|
-
export function buildCorpusSupplement(store, logger) {
|
|
35
|
-
return {
|
|
36
|
-
async search({ query, maxResults = 10 } = {}) {
|
|
37
|
-
try {
|
|
38
|
-
const rows = store.search(query || '', maxResults);
|
|
39
|
-
return rows.map((r) => toSearchResult(r));
|
|
40
|
-
} catch (err) {
|
|
41
|
-
logger?.warn?.(`memex-openclaw: corpus search failed: ${err.message}`);
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
async get({ lookup }) {
|
|
47
|
-
// `lookup` is the id we returned in search results' `id` field.
|
|
48
|
-
// We use the message row id as a string for portability.
|
|
49
|
-
try {
|
|
50
|
-
const numericId = parseInt(String(lookup).replace(/^memex:/, ''), 10);
|
|
51
|
-
if (!Number.isFinite(numericId)) return null;
|
|
52
|
-
const row = store.getById(numericId);
|
|
53
|
-
return row ? toGetResult(row) : null;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
logger?.warn?.(`memex-openclaw: corpus get failed: ${err.message}`);
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Map a memex row (from store.search) to OpenClaw's
|
|
64
|
-
* MemoryCorpusSearchResult shape:
|
|
65
|
-
*
|
|
66
|
-
* { corpus, path, title?, kind?, score, snippet, id?,
|
|
67
|
-
* startLine?, endLine?, citation?, source?,
|
|
68
|
-
* provenanceLabel?, sourceType?, sourcePath?, updatedAt? }
|
|
69
|
-
*/
|
|
70
|
-
function toSearchResult(row) {
|
|
71
|
-
const date = row.ts ? new Date(row.ts * 1000).toISOString() : undefined;
|
|
72
|
-
return {
|
|
73
|
-
corpus: CORPUS_LABEL,
|
|
74
|
-
id: `memex:${row.id}`,
|
|
75
|
-
path: `memex://${row.conversation_id}/#msg-${row.id}`,
|
|
76
|
-
title: titleFromRow(row),
|
|
77
|
-
kind: row.channel || 'openclaw',
|
|
78
|
-
score: 1.0, // memex returns ranked-by-ts; bm25 inside FTS would refine
|
|
79
|
-
snippet: row.preview || '',
|
|
80
|
-
source: 'openclaw',
|
|
81
|
-
provenanceLabel: row.channel
|
|
82
|
-
? `memex • ${row.channel}`
|
|
83
|
-
: 'memex • openclaw',
|
|
84
|
-
sourceType: 'verbatim',
|
|
85
|
-
sourcePath: row.conversation_id,
|
|
86
|
-
updatedAt: date,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Map a memex row (from store.getById) to OpenClaw's
|
|
92
|
-
* MemoryCorpusGetResult shape:
|
|
93
|
-
*
|
|
94
|
-
* { corpus, path, title?, kind?, content, fromLine, lineCount,
|
|
95
|
-
* id?, provenanceLabel?, sourceType?, sourcePath?, updatedAt? }
|
|
96
|
-
*/
|
|
97
|
-
function toGetResult(row) {
|
|
98
|
-
const date = row.ts ? new Date(row.ts * 1000).toISOString() : undefined;
|
|
99
|
-
const lineCount = row.text ? row.text.split('\n').length : 1;
|
|
100
|
-
return {
|
|
101
|
-
corpus: CORPUS_LABEL,
|
|
102
|
-
id: `memex:${row.id}`,
|
|
103
|
-
path: `memex://${row.conversation_id}/#msg-${row.id}`,
|
|
104
|
-
title: titleFromRow(row),
|
|
105
|
-
kind: row.channel || 'openclaw',
|
|
106
|
-
content: row.text || '',
|
|
107
|
-
fromLine: 1,
|
|
108
|
-
lineCount,
|
|
109
|
-
provenanceLabel: row.channel
|
|
110
|
-
? `memex • ${row.channel}`
|
|
111
|
-
: 'memex • openclaw',
|
|
112
|
-
sourceType: 'verbatim',
|
|
113
|
-
sourcePath: row.conversation_id,
|
|
114
|
-
updatedAt: date,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function titleFromRow(row) {
|
|
119
|
-
const role = row.role || '?';
|
|
120
|
-
const when = row.ts
|
|
121
|
-
? new Date(row.ts * 1000).toISOString().slice(0, 10)
|
|
122
|
-
: '?';
|
|
123
|
-
// First ~40 chars of text → title-ish
|
|
124
|
-
const preview = (row.text || row.preview || '')
|
|
125
|
-
.replace(/\s+/g, ' ')
|
|
126
|
-
.trim()
|
|
127
|
-
.slice(0, 40);
|
|
128
|
-
return preview
|
|
129
|
-
? `[${when} ${role}] ${preview}${preview.length === 40 ? '…' : ''}`
|
|
130
|
-
: `${role} @ ${when}`;
|
|
131
|
-
}
|