@pentatonic-ai/openclaw-memory-plugin 0.4.8 → 0.5.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/index.js +412 -178
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* compact — decay cycle on context overflow
|
|
10
10
|
* afterTurn — consolidation check
|
|
11
11
|
*
|
|
12
|
-
* Plus agent-callable tools: memory_search, memory_store, pentatonic_memory_setup
|
|
12
|
+
* Plus agent-callable tools: memory_search, memory_store, memory_status, pentatonic_memory_setup
|
|
13
13
|
*
|
|
14
14
|
* Two modes:
|
|
15
15
|
* - Local: HTTP calls to the memory server (localhost:3333)
|
|
@@ -19,6 +19,32 @@
|
|
|
19
19
|
* All config comes from OpenClaw's plugin config system.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
const TES_ENDPOINT = "https://api.pentatonic.com";
|
|
23
|
+
|
|
24
|
+
const SUCCESS_GIFS = [
|
|
25
|
+
"https://media.giphy.com/media/l0MYt5jPR6QX5APm0/giphy.gif", // brain expanding
|
|
26
|
+
"https://media.giphy.com/media/3o7btNa0RUYa5E7iiQ/giphy.gif", // elephant never forgets
|
|
27
|
+
"https://media.giphy.com/media/d31vTpVi1LAcDvdm/giphy.gif", // thinking smart
|
|
28
|
+
"https://media.giphy.com/media/3o7buirYcmV5nSwIRW/giphy.gif", // mind blown
|
|
29
|
+
"https://media.giphy.com/media/xT0xeJpnrWC3nQ8S1G/giphy.gif", // remembering
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function randomGif() {
|
|
33
|
+
return SUCCESS_GIFS[Math.floor(Math.random() * SUCCESS_GIFS.length)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Stats tracking ---
|
|
37
|
+
|
|
38
|
+
const stats = {
|
|
39
|
+
memoriesInjected: 0,
|
|
40
|
+
memoriesStored: 0,
|
|
41
|
+
searchesRun: 0,
|
|
42
|
+
lastAssembleCount: 0,
|
|
43
|
+
backendReachable: null,
|
|
44
|
+
mode: "unknown",
|
|
45
|
+
setupPrompted: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
22
48
|
// --- Local mode: HTTP to memory server ---
|
|
23
49
|
|
|
24
50
|
async function localSearch(baseUrl, query, limit = 5, minScore = 0.3) {
|
|
@@ -29,10 +55,12 @@ async function localSearch(baseUrl, query, limit = 5, minScore = 0.3) {
|
|
|
29
55
|
body: JSON.stringify({ query, limit, min_score: minScore }),
|
|
30
56
|
signal: AbortSignal.timeout(5000),
|
|
31
57
|
});
|
|
32
|
-
if (!res.ok) return [];
|
|
58
|
+
if (!res.ok) { stats.backendReachable = false; return []; }
|
|
59
|
+
stats.backendReachable = true;
|
|
33
60
|
const data = await res.json();
|
|
34
61
|
return data.results || [];
|
|
35
62
|
} catch {
|
|
63
|
+
stats.backendReachable = false;
|
|
36
64
|
return [];
|
|
37
65
|
}
|
|
38
66
|
}
|
|
@@ -45,20 +73,22 @@ async function localStore(baseUrl, content, metadata = {}) {
|
|
|
45
73
|
body: JSON.stringify({ content, metadata }),
|
|
46
74
|
signal: AbortSignal.timeout(10000),
|
|
47
75
|
});
|
|
48
|
-
if (!res.ok) return null;
|
|
76
|
+
if (!res.ok) { stats.backendReachable = false; return null; }
|
|
77
|
+
stats.backendReachable = true;
|
|
49
78
|
return res.json();
|
|
50
79
|
} catch {
|
|
80
|
+
stats.backendReachable = false;
|
|
51
81
|
return null;
|
|
52
82
|
}
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
async function localHealth(baseUrl) {
|
|
56
86
|
try {
|
|
57
|
-
const res = await fetch(`${baseUrl}/health`, {
|
|
58
|
-
|
|
59
|
-
});
|
|
87
|
+
const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(3000) });
|
|
88
|
+
stats.backendReachable = res.ok;
|
|
60
89
|
return res.ok;
|
|
61
90
|
} catch {
|
|
91
|
+
stats.backendReachable = false;
|
|
62
92
|
return false;
|
|
63
93
|
}
|
|
64
94
|
}
|
|
@@ -93,10 +123,12 @@ async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
|
|
|
93
123
|
}),
|
|
94
124
|
signal: AbortSignal.timeout(5000),
|
|
95
125
|
});
|
|
96
|
-
if (!res.ok) return [];
|
|
126
|
+
if (!res.ok) { stats.backendReachable = false; return []; }
|
|
127
|
+
stats.backendReachable = true;
|
|
97
128
|
const json = await res.json();
|
|
98
129
|
return json.data?.semanticSearchMemories || [];
|
|
99
130
|
} catch {
|
|
131
|
+
stats.backendReachable = false;
|
|
100
132
|
return [];
|
|
101
133
|
}
|
|
102
134
|
}
|
|
@@ -123,103 +155,95 @@ async function hostedStore(config, content, metadata = {}) {
|
|
|
123
155
|
}),
|
|
124
156
|
signal: AbortSignal.timeout(10000),
|
|
125
157
|
});
|
|
126
|
-
if (!res.ok) return null;
|
|
158
|
+
if (!res.ok) { stats.backendReachable = false; return null; }
|
|
159
|
+
stats.backendReachable = true;
|
|
127
160
|
return res.json();
|
|
128
161
|
} catch {
|
|
162
|
+
stats.backendReachable = false;
|
|
129
163
|
return null;
|
|
130
164
|
}
|
|
131
165
|
}
|
|
132
166
|
|
|
133
|
-
// ---
|
|
134
|
-
|
|
135
|
-
function createLocalContextEngine(baseUrl, opts = {}) {
|
|
136
|
-
const searchLimit = opts.searchLimit || 5;
|
|
137
|
-
const minScore = opts.minScore || 0.3;
|
|
138
|
-
const log = opts.logger || (() => {});
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
info: { id: "pentatonic-memory", name: "Pentatonic Memory (Local)", ownsCompaction: false },
|
|
142
|
-
|
|
143
|
-
async ingest({ sessionId, message }) {
|
|
144
|
-
if (!message?.content) return { ingested: false };
|
|
145
|
-
const role = message.role || message.type;
|
|
146
|
-
if (role !== "user" && role !== "assistant") return { ingested: false };
|
|
147
|
-
try {
|
|
148
|
-
await localStore(baseUrl, message.content, { session_id: sessionId, role });
|
|
149
|
-
log(`Ingested ${role} message`);
|
|
150
|
-
return { ingested: true };
|
|
151
|
-
} catch (err) {
|
|
152
|
-
log(`Ingest failed: ${err.message}`);
|
|
153
|
-
return { ingested: false };
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
async assemble({ sessionId, messages }) {
|
|
158
|
-
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
|
|
159
|
-
if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
|
|
160
|
-
try {
|
|
161
|
-
const results = await localSearch(baseUrl, lastUserMsg.content, searchLimit, minScore);
|
|
162
|
-
if (!results.length) return { messages, estimatedTokens: 0 };
|
|
163
|
-
const memoryText = results
|
|
164
|
-
.map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
|
|
165
|
-
.join("\n");
|
|
166
|
-
const addition = `[Memory] Relevant context from past conversations:\n${memoryText}`;
|
|
167
|
-
log(`Assembled ${results.length} memories`);
|
|
168
|
-
return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
|
|
169
|
-
} catch (err) {
|
|
170
|
-
log(`Assemble failed: ${err.message}`);
|
|
171
|
-
return { messages, estimatedTokens: 0 };
|
|
172
|
-
}
|
|
173
|
-
},
|
|
167
|
+
// --- TES account setup via HTTP ---
|
|
174
168
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
async function tesLogin(email, password, clientId) {
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/login`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ email, password, clientId }),
|
|
175
|
+
signal: AbortSignal.timeout(10000),
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) return null;
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data.tokens?.accessToken || null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
async assemble({ sessionId, messages }) {
|
|
203
|
-
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
|
|
204
|
-
if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
|
|
205
|
-
try {
|
|
206
|
-
const results = await hostedSearch(config, lastUserMsg.content, searchLimit, minScore);
|
|
207
|
-
if (!results.length) return { messages, estimatedTokens: 0 };
|
|
208
|
-
const memoryText = results
|
|
209
|
-
.map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
|
|
210
|
-
.join("\n");
|
|
211
|
-
const addition = `[Memory] Relevant context from past conversations:\n${memoryText}`;
|
|
212
|
-
log(`Assembled ${results.length} memories via TES`);
|
|
213
|
-
return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
|
|
214
|
-
} catch (err) {
|
|
215
|
-
log(`Hosted assemble failed: ${err.message}`);
|
|
216
|
-
return { messages, estimatedTokens: 0 };
|
|
185
|
+
async function tesEnroll(email, password, clientId, region) {
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/submit`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
clientId,
|
|
192
|
+
companyName: clientId,
|
|
193
|
+
industryType: "technology",
|
|
194
|
+
authProvider: "native",
|
|
195
|
+
adminEmail: email,
|
|
196
|
+
adminPassword: password,
|
|
197
|
+
region: (region || "eu").toLowerCase(),
|
|
198
|
+
}),
|
|
199
|
+
signal: AbortSignal.timeout(15000),
|
|
200
|
+
});
|
|
201
|
+
const data = await res.json();
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const errors = data.errors || {};
|
|
204
|
+
if (errors.clientId?.includes("already registered")) {
|
|
205
|
+
return { error: "This client ID is already registered. Ask your admin to invite you, then try again." };
|
|
217
206
|
}
|
|
218
|
-
|
|
207
|
+
return { error: data.message || Object.values(errors).join(", ") || "Enrollment failed." };
|
|
208
|
+
}
|
|
209
|
+
return { ok: true };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { error: `Connection failed: ${err.message}` };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
async function tesGetApiKey(accessToken, clientId) {
|
|
216
|
+
// Try enrollment service token first
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/service-token?client_id=${clientId}`,
|
|
219
|
+
{ signal: AbortSignal.timeout(5000) });
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
if (data.token) return data.token;
|
|
223
|
+
}
|
|
224
|
+
} catch { /* fallback */ }
|
|
225
|
+
|
|
226
|
+
// Create via GraphQL
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(`${TES_ENDPOINT}/api/graphql`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
Authorization: `Bearer ${accessToken}`,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
query: `mutation CreateApiToken($clientId: String!, $input: CreateApiTokenInput!) {
|
|
236
|
+
createClientApiToken(clientId: $clientId, input: $input) { success plainTextToken }
|
|
237
|
+
}`,
|
|
238
|
+
variables: { clientId, input: { name: "openclaw-plugin", role: "agent-events" } },
|
|
239
|
+
}),
|
|
240
|
+
signal: AbortSignal.timeout(10000),
|
|
241
|
+
});
|
|
242
|
+
const data = await res.json();
|
|
243
|
+
return data.data?.createClientApiToken?.plainTextToken || null;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
223
247
|
}
|
|
224
248
|
|
|
225
249
|
// --- Format helpers ---
|
|
@@ -237,110 +261,320 @@ export default {
|
|
|
237
261
|
id: "pentatonic-memory",
|
|
238
262
|
name: "Pentatonic Memory",
|
|
239
263
|
description: "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
|
|
264
|
+
kind: "context-engine",
|
|
240
265
|
|
|
241
266
|
register(api) {
|
|
242
267
|
const config = api.config || {};
|
|
243
268
|
const hosted = !!(config.tes_endpoint && config.tes_api_key);
|
|
244
269
|
const baseUrl = config.memory_url || "http://localhost:3333";
|
|
270
|
+
const searchLimit = config.search_limit || 5;
|
|
271
|
+
const minScore = config.min_score || 0.3;
|
|
245
272
|
const log = (msg) => process.stderr.write(`[pentatonic-memory] ${msg}\n`);
|
|
246
273
|
|
|
247
|
-
|
|
274
|
+
stats.mode = hosted ? "hosted" : "local";
|
|
275
|
+
|
|
276
|
+
// Unified search/store that routes to local or hosted
|
|
277
|
+
const search = hosted
|
|
278
|
+
? (query, limit, score) => hostedSearch(config, query, limit, score)
|
|
279
|
+
: (query, limit, score) => localSearch(baseUrl, query, limit, score);
|
|
280
|
+
|
|
281
|
+
const store = hosted
|
|
282
|
+
? (content, metadata) => hostedStore(config, content, metadata)
|
|
283
|
+
: (content, metadata) => localStore(baseUrl, content, metadata);
|
|
284
|
+
|
|
285
|
+
// --- Context engine: always registered, proxies to backend ---
|
|
286
|
+
|
|
287
|
+
api.registerContextEngine("pentatonic-memory", () => ({
|
|
288
|
+
info: {
|
|
289
|
+
id: "pentatonic-memory",
|
|
290
|
+
name: `Pentatonic Memory (${hosted ? "Hosted" : "Local"})`,
|
|
291
|
+
ownsCompaction: false,
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async ingest({ sessionId, message }) {
|
|
295
|
+
if (!message?.content) return { ingested: false };
|
|
296
|
+
const role = message.role || message.type;
|
|
297
|
+
if (role !== "user" && role !== "assistant") return { ingested: false };
|
|
298
|
+
try {
|
|
299
|
+
await store(message.content, { session_id: sessionId, role });
|
|
300
|
+
stats.memoriesStored++;
|
|
301
|
+
return { ingested: true };
|
|
302
|
+
} catch {
|
|
303
|
+
return { ingested: false };
|
|
304
|
+
}
|
|
305
|
+
},
|
|
248
306
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
307
|
+
async assemble({ sessionId, messages }) {
|
|
308
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
|
|
309
|
+
if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
|
|
252
310
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
type: "object",
|
|
258
|
-
properties: {
|
|
259
|
-
query: { type: "string", description: "What to search for" },
|
|
260
|
-
limit: { type: "number", description: "Max results (default 5)" },
|
|
261
|
-
},
|
|
262
|
-
required: ["query"],
|
|
263
|
-
},
|
|
264
|
-
async execute({ query, limit }) {
|
|
265
|
-
return formatResults(await hostedSearch(config, query, limit || 5, 0.3));
|
|
266
|
-
},
|
|
267
|
-
});
|
|
311
|
+
// First interaction: check if backend is reachable, prompt setup if not
|
|
312
|
+
if (!stats.setupPrompted && stats.backendReachable === null) {
|
|
313
|
+
if (!hosted) await localHealth(baseUrl);
|
|
314
|
+
}
|
|
268
315
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
if (!stats.setupPrompted && stats.backendReachable === false) {
|
|
317
|
+
stats.setupPrompted = true;
|
|
318
|
+
const setupPrompt = [
|
|
319
|
+
"[Pentatonic Memory] The memory system is installed but not connected to a backend yet.",
|
|
320
|
+
"",
|
|
321
|
+
"Before responding to the user, ask them how they'd like to set up their memory:",
|
|
322
|
+
"",
|
|
323
|
+
"1. **Local** (self-hosted) — Fully private, runs on this machine via Docker. Requires Docker installed.",
|
|
324
|
+
" Use the pentatonic_memory_setup tool with action 'check_local' to verify.",
|
|
325
|
+
"",
|
|
326
|
+
"2. **Hosted** (Pentatonic TES cloud) — Team-wide shared memory, analytics dashboard, higher-dimensional embeddings.",
|
|
327
|
+
" Use the pentatonic_memory_setup tool with action 'setup_hosted' to create an account.",
|
|
328
|
+
"",
|
|
329
|
+
"Ask the user which option they prefer, then use the pentatonic_memory_setup tool to proceed.",
|
|
330
|
+
].join("\n");
|
|
331
|
+
return { messages, estimatedTokens: Math.ceil(setupPrompt.length / 4), systemPromptAddition: setupPrompt };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const results = await search(lastUserMsg.content, searchLimit, minScore);
|
|
336
|
+
if (!results.length) {
|
|
337
|
+
stats.lastAssembleCount = 0;
|
|
338
|
+
return { messages, estimatedTokens: 0 };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
stats.memoriesInjected += results.length;
|
|
342
|
+
stats.lastAssembleCount = results.length;
|
|
343
|
+
|
|
344
|
+
const memoryText = results
|
|
345
|
+
.map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
|
|
346
|
+
.join("\n");
|
|
347
|
+
|
|
348
|
+
const addition = [
|
|
349
|
+
`[Memory] ${results.length} relevant memories found for this prompt:`,
|
|
350
|
+
memoryText,
|
|
351
|
+
"",
|
|
352
|
+
"When your response is informed by these memories, briefly mention it naturally (e.g. 'From what I remember...' or 'Based on our previous conversations...').",
|
|
353
|
+
].join("\n");
|
|
354
|
+
|
|
355
|
+
return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
|
|
356
|
+
} catch {
|
|
357
|
+
stats.lastAssembleCount = 0;
|
|
358
|
+
return { messages, estimatedTokens: 0 };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
async compact() { return { ok: true, compacted: false }; },
|
|
363
|
+
async afterTurn() {},
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
// --- Tools ---
|
|
367
|
+
|
|
368
|
+
api.registerTool({
|
|
369
|
+
name: "memory_search",
|
|
370
|
+
description: "Search memories for relevant context. Use when you need to recall past conversations, decisions, or knowledge.",
|
|
371
|
+
parameters: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
query: { type: "string", description: "What to search for" },
|
|
375
|
+
limit: { type: "number", description: "Max results (default 5)" },
|
|
280
376
|
},
|
|
281
|
-
|
|
377
|
+
required: ["query"],
|
|
378
|
+
},
|
|
379
|
+
async execute({ query, limit }) {
|
|
380
|
+
stats.searchesRun++;
|
|
381
|
+
return formatResults(await search(query, limit || 5, 0.3));
|
|
382
|
+
},
|
|
383
|
+
});
|
|
282
384
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
385
|
+
api.registerTool({
|
|
386
|
+
name: "memory_store",
|
|
387
|
+
description: "Explicitly store something important. Use for decisions, solutions, or facts worth remembering.",
|
|
388
|
+
parameters: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: { content: { type: "string", description: "What to remember" } },
|
|
391
|
+
required: ["content"],
|
|
392
|
+
},
|
|
393
|
+
async execute({ content }) {
|
|
394
|
+
const result = await store(content, { source: "openclaw-tool" });
|
|
395
|
+
if (result) {
|
|
396
|
+
stats.memoriesStored++;
|
|
397
|
+
return `Memory stored. ${randomGif()}`;
|
|
398
|
+
}
|
|
399
|
+
return "Failed to store memory. Is the memory server running?";
|
|
400
|
+
},
|
|
401
|
+
});
|
|
291
402
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
403
|
+
api.registerTool({
|
|
404
|
+
name: "memory_status",
|
|
405
|
+
description: "Check the status of the Pentatonic Memory system. Shows mode, backend health, and session stats.",
|
|
406
|
+
parameters: { type: "object", properties: {} },
|
|
407
|
+
async execute() {
|
|
408
|
+
// Refresh health check
|
|
409
|
+
if (!hosted) await localHealth(baseUrl);
|
|
410
|
+
|
|
411
|
+
const lines = [
|
|
412
|
+
`**Pentatonic Memory Status**`,
|
|
413
|
+
``,
|
|
414
|
+
`Mode: ${stats.mode}`,
|
|
415
|
+
`Backend: ${hosted ? config.tes_endpoint : baseUrl}`,
|
|
416
|
+
`Status: ${stats.backendReachable ? "connected" : "unreachable"}`,
|
|
417
|
+
``,
|
|
418
|
+
`**Session Stats:**`,
|
|
419
|
+
`Memories injected into prompts: ${stats.memoriesInjected}`,
|
|
420
|
+
`Memories stored: ${stats.memoriesStored}`,
|
|
421
|
+
`Explicit searches: ${stats.searchesRun}`,
|
|
422
|
+
`Last prompt: ${stats.lastAssembleCount} memories used`,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
if (stats.backendReachable) {
|
|
426
|
+
lines.push("", randomGif());
|
|
427
|
+
} else {
|
|
428
|
+
lines.push("", "Run `npx @pentatonic-ai/ai-agent-sdk memory` to start the local memory server.");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
api.registerTool({
|
|
436
|
+
name: "pentatonic_memory_setup",
|
|
437
|
+
description: `Set up or reconfigure Pentatonic Memory. Use this when:
|
|
438
|
+
- The user wants to set up memory for the first time
|
|
439
|
+
- The user wants to switch between local and hosted mode
|
|
440
|
+
- The user wants to connect to Pentatonic TES (hosted cloud memory)
|
|
441
|
+
|
|
442
|
+
Two modes available:
|
|
443
|
+
1. "check_local" — Check if local memory server is running at localhost:3333
|
|
444
|
+
2. "setup_hosted" — Create a Pentatonic TES account and get API credentials. Requires email, client_id (company name), password, and region (EU/US).
|
|
445
|
+
3. "verify_hosted" — After email verification, check if the account is ready. Requires email, password, client_id.
|
|
446
|
+
|
|
447
|
+
For local mode: just check if the server is running. If not, tell the user to run the setup command on their server.
|
|
448
|
+
For hosted mode: walk through account creation step by step via chat.`,
|
|
449
|
+
parameters: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
action: {
|
|
453
|
+
type: "string",
|
|
454
|
+
enum: ["check_local", "setup_hosted", "verify_hosted"],
|
|
455
|
+
description: "Which setup action to perform",
|
|
305
456
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
457
|
+
email: { type: "string", description: "User email (hosted only)" },
|
|
458
|
+
client_id: { type: "string", description: "Company/org identifier (hosted only)" },
|
|
459
|
+
password: { type: "string", description: "Account password (hosted only)" },
|
|
460
|
+
region: { type: "string", enum: ["EU", "US"], description: "Data region (hosted only)" },
|
|
310
461
|
},
|
|
311
|
-
|
|
462
|
+
required: ["action"],
|
|
463
|
+
},
|
|
464
|
+
async execute({ action, email, client_id, password, region }) {
|
|
465
|
+
if (action === "check_local") {
|
|
466
|
+
const healthy = await localHealth(baseUrl);
|
|
467
|
+
if (healthy) {
|
|
468
|
+
return [
|
|
469
|
+
`Local memory server is running at ${baseUrl}`,
|
|
470
|
+
"",
|
|
471
|
+
"Memory is active. Every conversation is being stored and searched automatically.",
|
|
472
|
+
"",
|
|
473
|
+
randomGif(),
|
|
474
|
+
].join("\n");
|
|
475
|
+
}
|
|
476
|
+
return [
|
|
477
|
+
`Local memory server is not reachable at ${baseUrl}.`,
|
|
478
|
+
"",
|
|
479
|
+
"To start the memory stack, someone needs to run this on the server:",
|
|
480
|
+
"```",
|
|
481
|
+
"npx @pentatonic-ai/ai-agent-sdk memory",
|
|
482
|
+
"```",
|
|
483
|
+
"",
|
|
484
|
+
"This starts PostgreSQL + pgvector, Ollama, and the memory server via Docker.",
|
|
485
|
+
"Once running, memory will activate automatically — no restart needed.",
|
|
486
|
+
].join("\n");
|
|
487
|
+
}
|
|
312
488
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
type: "object",
|
|
318
|
-
properties: { content: { type: "string", description: "What to remember" } },
|
|
319
|
-
required: ["content"],
|
|
320
|
-
},
|
|
321
|
-
async execute({ content }) {
|
|
322
|
-
const result = await localStore(baseUrl, content, { source: "openclaw-tool" });
|
|
323
|
-
return result ? `Stored: ${result.id}` : "Failed to store memory.";
|
|
324
|
-
},
|
|
325
|
-
});
|
|
489
|
+
if (action === "setup_hosted") {
|
|
490
|
+
if (!email || !client_id || !password) {
|
|
491
|
+
return "I need your email, a client ID (your company name), and a password to create the account. What's your email?";
|
|
492
|
+
}
|
|
326
493
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
494
|
+
// Check if already verified
|
|
495
|
+
const existingToken = await tesLogin(email, password, client_id);
|
|
496
|
+
if (existingToken) {
|
|
497
|
+
const apiKey = await tesGetApiKey(existingToken, client_id);
|
|
498
|
+
if (apiKey) {
|
|
499
|
+
const endpoint = `https://${client_id}.api.pentatonic.com`;
|
|
500
|
+
return [
|
|
501
|
+
"Account already verified! Here are your credentials:",
|
|
502
|
+
"",
|
|
503
|
+
`**TES Endpoint:** ${endpoint}`,
|
|
504
|
+
`**Client ID:** ${client_id}`,
|
|
505
|
+
`**API Key:** ${apiKey}`,
|
|
506
|
+
"",
|
|
507
|
+
"Add this to your openclaw.json plugin config to activate hosted memory:",
|
|
508
|
+
"```json",
|
|
509
|
+
JSON.stringify({ tes_endpoint: endpoint, tes_client_id: client_id, tes_api_key: apiKey }, null, 2),
|
|
510
|
+
"```",
|
|
511
|
+
"",
|
|
512
|
+
randomGif(),
|
|
513
|
+
].join("\n");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Enroll
|
|
518
|
+
const result = await tesEnroll(email, password, client_id, region);
|
|
519
|
+
if (result.error) return result.error;
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
"Account created! Check your email for a verification link.",
|
|
523
|
+
"",
|
|
524
|
+
`Email: ${email}`,
|
|
525
|
+
`Client ID: ${client_id}`,
|
|
526
|
+
"",
|
|
527
|
+
"Once you've clicked the verification link, tell me and I'll finish the setup.",
|
|
528
|
+
].join("\n");
|
|
340
529
|
}
|
|
341
|
-
});
|
|
342
530
|
|
|
343
|
-
|
|
531
|
+
if (action === "verify_hosted") {
|
|
532
|
+
if (!email || !client_id || !password) {
|
|
533
|
+
return "I need your email, client ID, and password to check verification status.";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const token = await tesLogin(email, password, client_id);
|
|
537
|
+
if (!token) {
|
|
538
|
+
return "Account not verified yet. Check your email for the verification link and try again.";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const apiKey = await tesGetApiKey(token, client_id);
|
|
542
|
+
if (!apiKey) {
|
|
543
|
+
return "Account verified but I couldn't generate an API key. Try again in a moment.";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const endpoint = `https://${client_id}.api.pentatonic.com`;
|
|
547
|
+
return [
|
|
548
|
+
"Email verified! Here are your credentials:",
|
|
549
|
+
"",
|
|
550
|
+
`**TES Endpoint:** ${endpoint}`,
|
|
551
|
+
`**Client ID:** ${client_id}`,
|
|
552
|
+
`**API Key:** \`${apiKey}\``,
|
|
553
|
+
"",
|
|
554
|
+
"Add this to your openclaw.json plugin config:",
|
|
555
|
+
"```json",
|
|
556
|
+
JSON.stringify({ tes_endpoint: endpoint, tes_client_id: client_id, tes_api_key: apiKey }, null, 2),
|
|
557
|
+
"```",
|
|
558
|
+
"",
|
|
559
|
+
"Then restart the gateway to switch to hosted mode.",
|
|
560
|
+
"",
|
|
561
|
+
randomGif(),
|
|
562
|
+
].join("\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return "Unknown action. Use check_local, setup_hosted, or verify_hosted.";
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Check backend health on startup
|
|
570
|
+
if (!hosted) {
|
|
571
|
+
localHealth(baseUrl).then((ok) => {
|
|
572
|
+
log(ok ? `Memory server healthy at ${baseUrl}` : `Memory server not reachable at ${baseUrl}`);
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
stats.backendReachable = true; // assume hosted is reachable
|
|
344
576
|
}
|
|
577
|
+
|
|
578
|
+
log(`Plugin registered (${hosted ? "hosted" : "local"} — ${hosted ? config.tes_endpoint : baseUrl})`);
|
|
345
579
|
},
|
|
346
580
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Pentatonic Memory",
|
|
4
4
|
"description": "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion. Local (Docker + Ollama) or hosted (Pentatonic TES).",
|
|
5
5
|
"version": "0.4.4",
|
|
6
|
-
"kind": "
|
|
6
|
+
"kind": "context-engine",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pentatonic-ai/openclaw-memory-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Pentatonic Memory plugin for OpenClaw — persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|