@astramindapp/openclaw-mind 0.1.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 +137 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +1499 -0
- package/dist/index.js.map +1 -0
- package/openclaw.plugin.json +178 -0
- package/package.json +82 -0
- package/skills/memory-dream/SKILL.md +113 -0
- package/skills/memory-triage/SKILL.md +118 -0
- package/skills/mind-emotional-encoding/SKILL.md +112 -0
- package/skills/mind-graph-recall/SKILL.md +114 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
|
|
4
|
+
// src/mind-client.ts
|
|
5
|
+
var MindClientError = class extends Error {
|
|
6
|
+
constructor(message, status, path3, body) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.path = path3;
|
|
10
|
+
this.body = body;
|
|
11
|
+
this.name = "MindClientError";
|
|
12
|
+
}
|
|
13
|
+
status;
|
|
14
|
+
path;
|
|
15
|
+
body;
|
|
16
|
+
};
|
|
17
|
+
var MindClient = class {
|
|
18
|
+
apiKey;
|
|
19
|
+
baseUrl;
|
|
20
|
+
logger;
|
|
21
|
+
timeoutMs;
|
|
22
|
+
maxRetries;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
if (!opts.apiKey) {
|
|
25
|
+
throw new Error("MindClient: apiKey is required");
|
|
26
|
+
}
|
|
27
|
+
this.apiKey = opts.apiKey;
|
|
28
|
+
this.baseUrl = (opts.baseUrl ?? "https://www.m-i-n-d.ai").replace(/\/$/, "");
|
|
29
|
+
this.logger = opts.logger;
|
|
30
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
31
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
32
|
+
}
|
|
33
|
+
// ─── Query / Search ───
|
|
34
|
+
async query(req) {
|
|
35
|
+
return this.request("POST", "/developer/v1/query", req);
|
|
36
|
+
}
|
|
37
|
+
// ─── Documents ───
|
|
38
|
+
async createDocument(req) {
|
|
39
|
+
return this.request("POST", "/developer/v1/documents", req);
|
|
40
|
+
}
|
|
41
|
+
async listDocuments(opts = {}) {
|
|
42
|
+
const params = new URLSearchParams();
|
|
43
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
44
|
+
if (opts.offset !== void 0) params.set("offset", String(opts.offset));
|
|
45
|
+
const qs = params.toString();
|
|
46
|
+
return this.request(
|
|
47
|
+
"GET",
|
|
48
|
+
`/developer/v1/documents${qs ? `?${qs}` : ""}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
async getDocument(id) {
|
|
52
|
+
return this.request("GET", `/developer/v1/documents/${id}`);
|
|
53
|
+
}
|
|
54
|
+
async deleteDocument(id) {
|
|
55
|
+
return this.request("DELETE", `/developer/v1/documents/${id}`);
|
|
56
|
+
}
|
|
57
|
+
// ─── Entries (lighter-weight than documents) ───
|
|
58
|
+
async createEntry(req) {
|
|
59
|
+
return this.request("POST", "/developer/v1/entries", req);
|
|
60
|
+
}
|
|
61
|
+
async listEntries(opts = {}) {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
64
|
+
if (opts.offset !== void 0) params.set("offset", String(opts.offset));
|
|
65
|
+
const qs = params.toString();
|
|
66
|
+
return this.request(
|
|
67
|
+
"GET",
|
|
68
|
+
`/developer/v1/entries${qs ? `?${qs}` : ""}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
async searchEntries(query, limit = 10) {
|
|
72
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
73
|
+
return this.request(
|
|
74
|
+
"GET",
|
|
75
|
+
`/developer/v1/entries/search?${params}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
async deleteEntry(id) {
|
|
79
|
+
return this.request("DELETE", `/developer/v1/entries/${id}`);
|
|
80
|
+
}
|
|
81
|
+
// ─── Graph ───
|
|
82
|
+
async getGraphInfo() {
|
|
83
|
+
return this.request("GET", "/developer/v1/graph");
|
|
84
|
+
}
|
|
85
|
+
async getGraphDiagnostics() {
|
|
86
|
+
return this.request("GET", "/developer/v1/graph/diagnostics");
|
|
87
|
+
}
|
|
88
|
+
// ─── Life Management ───
|
|
89
|
+
async createLifeItem(req) {
|
|
90
|
+
return this.request("POST", "/developer/v1/life/items", req);
|
|
91
|
+
}
|
|
92
|
+
async listLifeItems(opts = {}) {
|
|
93
|
+
const params = new URLSearchParams();
|
|
94
|
+
if (opts.status) params.set("status", opts.status);
|
|
95
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
96
|
+
const qs = params.toString();
|
|
97
|
+
return this.request(
|
|
98
|
+
"GET",
|
|
99
|
+
`/developer/v1/life/items${qs ? `?${qs}` : ""}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
async completeLifeItem(id) {
|
|
103
|
+
return this.request("POST", `/developer/v1/life/items/${id}/complete`, {});
|
|
104
|
+
}
|
|
105
|
+
// ─── CRM ───
|
|
106
|
+
async createCrmContact(req) {
|
|
107
|
+
return this.request("POST", "/developer/v1/crm/contacts", req);
|
|
108
|
+
}
|
|
109
|
+
async listCrmContacts(opts = {}) {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
112
|
+
const qs = params.toString();
|
|
113
|
+
return this.request(
|
|
114
|
+
"GET",
|
|
115
|
+
`/developer/v1/crm/contacts${qs ? `?${qs}` : ""}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
async logCrmActivity(contactId, req) {
|
|
119
|
+
return this.request(
|
|
120
|
+
"POST",
|
|
121
|
+
`/developer/v1/crm/contacts/${contactId}/activities`,
|
|
122
|
+
req
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
// ─── Internal HTTP layer ───
|
|
126
|
+
async request(method, path3, body) {
|
|
127
|
+
const url = `${this.baseUrl}${path3}`;
|
|
128
|
+
const headers = {
|
|
129
|
+
"X-API-Key": this.apiKey,
|
|
130
|
+
Accept: "application/json",
|
|
131
|
+
"User-Agent": "openclaw-mind/0.1.0"
|
|
132
|
+
};
|
|
133
|
+
if (body !== void 0) {
|
|
134
|
+
headers["Content-Type"] = "application/json";
|
|
135
|
+
}
|
|
136
|
+
let lastError;
|
|
137
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
140
|
+
try {
|
|
141
|
+
const resp = await fetch(url, {
|
|
142
|
+
method,
|
|
143
|
+
headers,
|
|
144
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
145
|
+
signal: controller.signal
|
|
146
|
+
});
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
if (!resp.ok) {
|
|
149
|
+
const text = await resp.text().catch(() => "");
|
|
150
|
+
let parsed = text;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(text);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
if ([429, 500, 502, 503, 504].includes(resp.status) && attempt < this.maxRetries) {
|
|
156
|
+
const backoff = 500 * Math.pow(2, attempt);
|
|
157
|
+
this.logger?.warn?.(
|
|
158
|
+
`MIND ${method} ${path3} \u2192 ${resp.status}, retrying in ${backoff}ms (attempt ${attempt + 1}/${this.maxRetries + 1})`
|
|
159
|
+
);
|
|
160
|
+
await sleep(backoff);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
throw new MindClientError(
|
|
164
|
+
`MIND API ${resp.status} ${resp.statusText} on ${method} ${path3}`,
|
|
165
|
+
resp.status,
|
|
166
|
+
path3,
|
|
167
|
+
parsed
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const ct = resp.headers.get("content-type") ?? "";
|
|
171
|
+
if (ct.includes("application/json")) {
|
|
172
|
+
return await resp.json();
|
|
173
|
+
}
|
|
174
|
+
return await resp.text();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
clearTimeout(timeout);
|
|
177
|
+
lastError = err;
|
|
178
|
+
if (err instanceof MindClientError) throw err;
|
|
179
|
+
if (attempt < this.maxRetries) {
|
|
180
|
+
const backoff = 500 * Math.pow(2, attempt);
|
|
181
|
+
this.logger?.warn?.(
|
|
182
|
+
`MIND ${method} ${path3} network error, retrying in ${backoff}ms: ${err.message}`
|
|
183
|
+
);
|
|
184
|
+
await sleep(backoff);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw lastError ?? new Error("MindClient: exhausted retries");
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function sleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/config.ts
|
|
198
|
+
var DEFAULT_BASE_URL = "https://www.m-i-n-d.ai";
|
|
199
|
+
var DEFAULT_TOP_K = 10;
|
|
200
|
+
var DEFAULT_SEARCH_THRESHOLD = 0.5;
|
|
201
|
+
var DEFAULT_QUERY_MODE = "hybrid";
|
|
202
|
+
function resolveConfig(raw) {
|
|
203
|
+
const apiKey = raw?.apiKey ?? process.env.MIND_API_KEY ?? process.env.MINDAPP_API_KEY;
|
|
204
|
+
const baseUrl = raw?.baseUrl ?? process.env.MIND_BASE_URL ?? DEFAULT_BASE_URL;
|
|
205
|
+
return {
|
|
206
|
+
apiKey,
|
|
207
|
+
baseUrl,
|
|
208
|
+
userId: raw?.userId ?? process.env.MIND_USER_ID,
|
|
209
|
+
autoCapture: raw?.autoCapture ?? true,
|
|
210
|
+
autoRecall: raw?.autoRecall ?? true,
|
|
211
|
+
topK: raw?.topK ?? DEFAULT_TOP_K,
|
|
212
|
+
searchThreshold: raw?.searchThreshold ?? DEFAULT_SEARCH_THRESHOLD,
|
|
213
|
+
queryMode: raw?.queryMode ?? DEFAULT_QUERY_MODE,
|
|
214
|
+
enableMindsense: raw?.enableMindsense ?? true,
|
|
215
|
+
enableLifeIntegration: raw?.enableLifeIntegration ?? true,
|
|
216
|
+
enableCrmLogging: raw?.enableCrmLogging ?? true,
|
|
217
|
+
skills: raw?.skills,
|
|
218
|
+
needsSetup: !apiKey
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/tools/search.ts
|
|
223
|
+
import { Type } from "@sinclair/typebox";
|
|
224
|
+
var SearchParameters = Type.Object({
|
|
225
|
+
query: Type.String({ description: "What to search for" }),
|
|
226
|
+
mode: Type.Optional(
|
|
227
|
+
Type.Union(
|
|
228
|
+
[
|
|
229
|
+
Type.Literal("hybrid"),
|
|
230
|
+
Type.Literal("mix"),
|
|
231
|
+
Type.Literal("global"),
|
|
232
|
+
Type.Literal("local"),
|
|
233
|
+
Type.Literal("naive")
|
|
234
|
+
],
|
|
235
|
+
{
|
|
236
|
+
description: "Query mode. 'hybrid' (default) blends vector + graph. 'global' synthesizes across the full graph. 'local' is entity-focused."
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
top_k: Type.Optional(
|
|
241
|
+
Type.Number({
|
|
242
|
+
description: "Maximum number of memories to return. Default: 10",
|
|
243
|
+
minimum: 1,
|
|
244
|
+
maximum: 100
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
});
|
|
248
|
+
function createMindSearchTool(deps) {
|
|
249
|
+
return {
|
|
250
|
+
name: "mind_search",
|
|
251
|
+
label: "MIND Search",
|
|
252
|
+
description: "Search the MIND knowledge graph using hybrid semantic + graph queries. Returns an AI-synthesized answer grounded in stored memories. Use this whenever the agent needs prior context, decisions, preferences, or facts about the user. MIND's true KG beats vector-only tools because it follows entity relationships, not just text similarity.",
|
|
253
|
+
parameters: SearchParameters,
|
|
254
|
+
async execute(_toolCallId, params) {
|
|
255
|
+
try {
|
|
256
|
+
const result = await deps.client.query({
|
|
257
|
+
query: params.query,
|
|
258
|
+
mode: params.mode ?? deps.config.queryMode,
|
|
259
|
+
top_k: params.top_k ?? deps.config.topK
|
|
260
|
+
});
|
|
261
|
+
const sources = result.sources ?? [];
|
|
262
|
+
const sourceList = sources.length > 0 ? `
|
|
263
|
+
|
|
264
|
+
Sources:
|
|
265
|
+
${sources.slice(0, 5).map((s, i) => `${i + 1}. ${s.title ?? "(untitled)"}`).join("\n")}` : "";
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: `${result.answer}${sourceList}` }],
|
|
268
|
+
details: {
|
|
269
|
+
answer: result.answer,
|
|
270
|
+
sources,
|
|
271
|
+
model_used: result.model_used
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const msg = err.message;
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: "text", text: `MIND search failed: ${msg}` }],
|
|
278
|
+
details: { error: msg }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/tools/add.ts
|
|
286
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
287
|
+
var AddParameters = Type2.Object({
|
|
288
|
+
content: Type2.String({
|
|
289
|
+
description: "The fact or memory to store. Any length \u2014 short thoughts to multi-paragraph documents."
|
|
290
|
+
}),
|
|
291
|
+
title: Type2.Optional(
|
|
292
|
+
Type2.String({ description: "Optional title. Auto-generated from content if omitted." })
|
|
293
|
+
),
|
|
294
|
+
type: Type2.Optional(
|
|
295
|
+
Type2.Union(
|
|
296
|
+
[Type2.Literal("document"), Type2.Literal("entry"), Type2.Literal("thought")],
|
|
297
|
+
{
|
|
298
|
+
description: "Storage type. 'document' (long-form), 'entry' (medium), 'thought' (quick). Default: auto-decide by content length."
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
),
|
|
302
|
+
tags: Type2.Optional(
|
|
303
|
+
Type2.Array(Type2.String(), { description: "Optional tags for retrieval." })
|
|
304
|
+
)
|
|
305
|
+
});
|
|
306
|
+
function createMindAddTool(deps) {
|
|
307
|
+
return {
|
|
308
|
+
name: "mind_add",
|
|
309
|
+
label: "MIND Add Memory",
|
|
310
|
+
description: "Store a memory in the MIND knowledge graph. Auto-extracts entities (people, places, concepts, events) and the relationships between them. If MINDsense is enabled, also assigns valence/arousal emotional weights that determine encoding depth \u2014 emotionally significant content is encoded more deeply, mirroring biological memory consolidation.",
|
|
311
|
+
parameters: AddParameters,
|
|
312
|
+
async execute(_toolCallId, params) {
|
|
313
|
+
try {
|
|
314
|
+
const type = params.type ?? autoDecideType(params.content);
|
|
315
|
+
const title = params.title ?? deriveTitle(params.content);
|
|
316
|
+
if (type === "document") {
|
|
317
|
+
const doc = await deps.client.createDocument({
|
|
318
|
+
title,
|
|
319
|
+
content: params.content,
|
|
320
|
+
tags: params.tags,
|
|
321
|
+
source: "openclaw-mind"
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: "text",
|
|
327
|
+
text: `Stored as document "${doc.title}" (id: ${doc.id})`
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
details: { id: doc.id, type: "document", title: doc.title }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const entry = await deps.client.createEntry({
|
|
334
|
+
title,
|
|
335
|
+
content: params.content,
|
|
336
|
+
type: type === "thought" ? "thought" : "entry",
|
|
337
|
+
tags: params.tags,
|
|
338
|
+
source: "openclaw-mind"
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{ type: "text", text: `Stored as ${type} "${entry.title ?? title}" (id: ${entry.id})` }
|
|
343
|
+
],
|
|
344
|
+
details: { id: entry.id, type, title: entry.title ?? title }
|
|
345
|
+
};
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const msg = err.message;
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: "text", text: `mind_add failed: ${msg}` }],
|
|
350
|
+
details: { error: msg }
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function autoDecideType(content) {
|
|
357
|
+
const words = content.trim().split(/\s+/).length;
|
|
358
|
+
if (words >= 200) return "document";
|
|
359
|
+
if (words >= 30) return "entry";
|
|
360
|
+
return "thought";
|
|
361
|
+
}
|
|
362
|
+
function deriveTitle(content) {
|
|
363
|
+
const firstLine = content.trim().split("\n")[0] ?? content;
|
|
364
|
+
return firstLine.slice(0, 100).trim() || "Untitled memory";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/tools/get.ts
|
|
368
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
369
|
+
var GetParameters = Type3.Object({
|
|
370
|
+
id: Type3.String({ description: "The memory ID to retrieve" }),
|
|
371
|
+
type: Type3.Optional(
|
|
372
|
+
Type3.Union(
|
|
373
|
+
[Type3.Literal("document"), Type3.Literal("entry")],
|
|
374
|
+
{ description: "Storage type. Default: 'document'" }
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
});
|
|
378
|
+
function createMindGetTool(deps) {
|
|
379
|
+
return {
|
|
380
|
+
name: "mind_get",
|
|
381
|
+
label: "MIND Get Memory",
|
|
382
|
+
description: "Retrieve a single memory by its ID. Returns full content, metadata, tags, timestamps.",
|
|
383
|
+
parameters: GetParameters,
|
|
384
|
+
async execute(_toolCallId, params) {
|
|
385
|
+
try {
|
|
386
|
+
const type = params.type ?? "document";
|
|
387
|
+
let result;
|
|
388
|
+
if (type === "document") {
|
|
389
|
+
result = await deps.client.getDocument(params.id);
|
|
390
|
+
} else {
|
|
391
|
+
const list = await deps.client.listEntries({ limit: 100 });
|
|
392
|
+
result = list.entries.find((e) => e.id === params.id);
|
|
393
|
+
if (!result) {
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: "text", text: `Entry ${params.id} not found` }],
|
|
396
|
+
details: { error: "not_found" }
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
402
|
+
details: result
|
|
403
|
+
};
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const msg = err.message;
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text", text: `mind_get failed: ${msg}` }],
|
|
408
|
+
details: { error: msg }
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/tools/list.ts
|
|
416
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
417
|
+
var ListParameters = Type4.Object({
|
|
418
|
+
type: Type4.Optional(
|
|
419
|
+
Type4.Union(
|
|
420
|
+
[Type4.Literal("document"), Type4.Literal("entry"), Type4.Literal("all")],
|
|
421
|
+
{ description: "Filter by storage type. Default: 'all'" }
|
|
422
|
+
)
|
|
423
|
+
),
|
|
424
|
+
limit: Type4.Optional(
|
|
425
|
+
Type4.Number({ description: "Maximum number to return. Default: 20", minimum: 1, maximum: 100 })
|
|
426
|
+
),
|
|
427
|
+
offset: Type4.Optional(Type4.Number({ description: "Pagination offset. Default: 0", minimum: 0 }))
|
|
428
|
+
});
|
|
429
|
+
function createMindListTool(deps) {
|
|
430
|
+
return {
|
|
431
|
+
name: "mind_list",
|
|
432
|
+
label: "MIND List",
|
|
433
|
+
description: "List stored memories with pagination. Returns IDs, titles, types, tags, timestamps. Use for browsing or when the agent needs to enumerate before deciding what to retrieve.",
|
|
434
|
+
parameters: ListParameters,
|
|
435
|
+
async execute(_toolCallId, params) {
|
|
436
|
+
try {
|
|
437
|
+
const limit = params.limit ?? 20;
|
|
438
|
+
const offset = params.offset ?? 0;
|
|
439
|
+
const type = params.type ?? "all";
|
|
440
|
+
const result = {};
|
|
441
|
+
if (type === "document" || type === "all") {
|
|
442
|
+
const docs = await deps.client.listDocuments({ limit, offset });
|
|
443
|
+
result.documents = docs.documents;
|
|
444
|
+
}
|
|
445
|
+
if (type === "entry" || type === "all") {
|
|
446
|
+
const entries = await deps.client.listEntries({ limit, offset });
|
|
447
|
+
result.entries = entries.entries;
|
|
448
|
+
}
|
|
449
|
+
const summary = [
|
|
450
|
+
`${result.documents?.length ?? 0} documents`,
|
|
451
|
+
`${result.entries?.length ?? 0} entries`
|
|
452
|
+
].join(", ");
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text", text: `${summary}
|
|
455
|
+
|
|
456
|
+
${JSON.stringify(result, null, 2)}` }],
|
|
457
|
+
details: result
|
|
458
|
+
};
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const msg = err.message;
|
|
461
|
+
return {
|
|
462
|
+
content: [{ type: "text", text: `mind_list failed: ${msg}` }],
|
|
463
|
+
details: { error: msg }
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/tools/update.ts
|
|
471
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
472
|
+
var UpdateParameters = Type5.Object({
|
|
473
|
+
id: Type5.String({ description: "The memory ID to update" }),
|
|
474
|
+
content: Type5.String({ description: "New content. Replaces the existing content." }),
|
|
475
|
+
title: Type5.Optional(Type5.String({ description: "New title." })),
|
|
476
|
+
tags: Type5.Optional(Type5.Array(Type5.String())),
|
|
477
|
+
type: Type5.Optional(
|
|
478
|
+
Type5.Union(
|
|
479
|
+
[Type5.Literal("document"), Type5.Literal("entry")],
|
|
480
|
+
{ description: "Storage type. Default: 'document'" }
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
});
|
|
484
|
+
function createMindUpdateTool(deps) {
|
|
485
|
+
return {
|
|
486
|
+
name: "mind_update",
|
|
487
|
+
label: "MIND Update Memory",
|
|
488
|
+
description: "Update an existing memory's content, title, or tags. The KG is re-extracted from the new content automatically. Use when a stored fact has changed (e.g., user's role, deadline shift, decision update).",
|
|
489
|
+
parameters: UpdateParameters,
|
|
490
|
+
async execute(_toolCallId, params) {
|
|
491
|
+
try {
|
|
492
|
+
const type = params.type ?? "document";
|
|
493
|
+
if (type === "document") {
|
|
494
|
+
const original = await deps.client.getDocument(params.id);
|
|
495
|
+
await deps.client.deleteDocument(params.id);
|
|
496
|
+
const created = await deps.client.createDocument({
|
|
497
|
+
title: params.title ?? original.title,
|
|
498
|
+
content: params.content,
|
|
499
|
+
tags: params.tags ?? original.tags,
|
|
500
|
+
source: "openclaw-mind"
|
|
501
|
+
});
|
|
502
|
+
return {
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
type: "text",
|
|
506
|
+
text: `Updated memory. New id: ${created.id} (was: ${params.id})`
|
|
507
|
+
}
|
|
508
|
+
],
|
|
509
|
+
details: { id: created.id, previous_id: params.id, title: created.title }
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
await deps.client.deleteEntry(params.id);
|
|
513
|
+
const entry = await deps.client.createEntry({
|
|
514
|
+
title: params.title,
|
|
515
|
+
content: params.content,
|
|
516
|
+
type: "entry",
|
|
517
|
+
tags: params.tags,
|
|
518
|
+
source: "openclaw-mind"
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
content: [
|
|
522
|
+
{ type: "text", text: `Updated entry. New id: ${entry.id} (was: ${params.id})` }
|
|
523
|
+
],
|
|
524
|
+
details: { id: entry.id, previous_id: params.id, title: entry.title }
|
|
525
|
+
};
|
|
526
|
+
} catch (err) {
|
|
527
|
+
const msg = err.message;
|
|
528
|
+
return {
|
|
529
|
+
content: [{ type: "text", text: `mind_update failed: ${msg}` }],
|
|
530
|
+
details: { error: msg }
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/tools/delete.ts
|
|
538
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
539
|
+
var DeleteParameters = Type6.Object({
|
|
540
|
+
id: Type6.String({ description: "The memory ID to delete" }),
|
|
541
|
+
type: Type6.Optional(
|
|
542
|
+
Type6.Union(
|
|
543
|
+
[Type6.Literal("document"), Type6.Literal("entry")],
|
|
544
|
+
{ description: "Storage type. Default: 'document'" }
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
});
|
|
548
|
+
function createMindDeleteTool(deps) {
|
|
549
|
+
return {
|
|
550
|
+
name: "mind_delete",
|
|
551
|
+
label: "MIND Delete Memory",
|
|
552
|
+
description: "Delete a memory by its ID. KG entities and relationships sourced from this memory are removed. Use sparingly \u2014 prefer mind_update if content is just changing.",
|
|
553
|
+
parameters: DeleteParameters,
|
|
554
|
+
async execute(_toolCallId, params) {
|
|
555
|
+
try {
|
|
556
|
+
const type = params.type ?? "document";
|
|
557
|
+
if (type === "document") {
|
|
558
|
+
await deps.client.deleteDocument(params.id);
|
|
559
|
+
} else {
|
|
560
|
+
await deps.client.deleteEntry(params.id);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
content: [{ type: "text", text: `Deleted ${type} ${params.id}` }],
|
|
564
|
+
details: { id: params.id, status: "deleted" }
|
|
565
|
+
};
|
|
566
|
+
} catch (err) {
|
|
567
|
+
const msg = err.message;
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: `mind_delete failed: ${msg}` }],
|
|
570
|
+
details: { error: msg }
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/tools/query-graph.ts
|
|
578
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
579
|
+
var QueryGraphParameters = Type7.Object({
|
|
580
|
+
question: Type7.String({
|
|
581
|
+
description: "A graph-shaped question. Examples: 'Who is connected to Sarah?', 'What concepts cluster around the KGC pitch?', 'What decisions are linked to the auth migration?'"
|
|
582
|
+
}),
|
|
583
|
+
mode: Type7.Optional(
|
|
584
|
+
Type7.Union([Type7.Literal("global"), Type7.Literal("local"), Type7.Literal("mix")], {
|
|
585
|
+
description: "Graph query mode. 'global' (default) synthesizes across the full graph. 'local' is entity-focused. 'mix' balances both."
|
|
586
|
+
})
|
|
587
|
+
)
|
|
588
|
+
});
|
|
589
|
+
function createMindQueryGraphTool(deps) {
|
|
590
|
+
return {
|
|
591
|
+
name: "mind_query_graph",
|
|
592
|
+
label: "MIND Graph Query",
|
|
593
|
+
description: "Query the MIND knowledge graph by structure, not just similarity. Use for questions about RELATIONSHIPS between entities. Example: mind_search answers 'what did the user say about Sarah?'; mind_query_graph answers 'who is Sarah connected to and how?'. This is a true KG capability vector-only memory tools cannot match.",
|
|
594
|
+
parameters: QueryGraphParameters,
|
|
595
|
+
async execute(_toolCallId, params) {
|
|
596
|
+
try {
|
|
597
|
+
const result = await deps.client.query({
|
|
598
|
+
query: params.question,
|
|
599
|
+
mode: params.mode ?? "global",
|
|
600
|
+
top_k: 30
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
content: [{ type: "text", text: result.answer }],
|
|
604
|
+
details: {
|
|
605
|
+
answer: result.answer,
|
|
606
|
+
sources: result.sources ?? [],
|
|
607
|
+
mode: params.mode ?? "global"
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
} catch (err) {
|
|
611
|
+
const msg = err.message;
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: "text", text: `mind_query_graph failed: ${msg}` }],
|
|
614
|
+
details: { error: msg }
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/tools/recall-emotional.ts
|
|
622
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
623
|
+
var RecallEmotionalParameters = Type8.Object({
|
|
624
|
+
query: Type8.String({
|
|
625
|
+
description: "What to search for, biased toward emotionally salient memories."
|
|
626
|
+
}),
|
|
627
|
+
emotion_filter: Type8.Optional(
|
|
628
|
+
Type8.Union(
|
|
629
|
+
[
|
|
630
|
+
Type8.Literal("positive"),
|
|
631
|
+
Type8.Literal("negative"),
|
|
632
|
+
Type8.Literal("high_arousal"),
|
|
633
|
+
Type8.Literal("low_arousal"),
|
|
634
|
+
Type8.Literal("triumph"),
|
|
635
|
+
Type8.Literal("warning"),
|
|
636
|
+
Type8.Literal("any")
|
|
637
|
+
],
|
|
638
|
+
{
|
|
639
|
+
description: "Filter by emotional category. 'positive'=high valence. 'negative'=low valence. 'high_arousal'=intense moments. 'triumph' / 'warning' = MINDsense semantic categories."
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
),
|
|
643
|
+
min_intensity: Type8.Optional(
|
|
644
|
+
Type8.Number({
|
|
645
|
+
description: "Minimum emotional intensity (0-1). Default: 0.3",
|
|
646
|
+
minimum: 0,
|
|
647
|
+
maximum: 1
|
|
648
|
+
})
|
|
649
|
+
),
|
|
650
|
+
top_k: Type8.Optional(
|
|
651
|
+
Type8.Number({ description: "Maximum results. Default: 10", minimum: 1, maximum: 50 })
|
|
652
|
+
)
|
|
653
|
+
});
|
|
654
|
+
function createMindRecallEmotionalTool(deps) {
|
|
655
|
+
return {
|
|
656
|
+
name: "mind_recall_emotional",
|
|
657
|
+
label: "MIND Emotional Recall",
|
|
658
|
+
description: "Recall memories weighted by emotional significance (MINDsense, patented). Use when the agent needs to understand the user's strong feelings, important moments, frustrations, wins, or warnings. Example queries: 'what frustrates the user about deployment', 'what wins did the user celebrate this month'.",
|
|
659
|
+
parameters: RecallEmotionalParameters,
|
|
660
|
+
async execute(_toolCallId, params) {
|
|
661
|
+
try {
|
|
662
|
+
const emotionTag = params.emotion_filter ?? "any";
|
|
663
|
+
const intensityHint = params.min_intensity ?? 0.3;
|
|
664
|
+
const augmentedQuery = `${params.query}
|
|
665
|
+
|
|
666
|
+
[Emotional context: ${emotionTag}, min_intensity=${intensityHint}]`;
|
|
667
|
+
const result = await deps.client.query({
|
|
668
|
+
query: augmentedQuery,
|
|
669
|
+
mode: "hybrid",
|
|
670
|
+
top_k: params.top_k ?? 10
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
content: [{ type: "text", text: result.answer }],
|
|
674
|
+
details: {
|
|
675
|
+
answer: result.answer,
|
|
676
|
+
sources: result.sources ?? [],
|
|
677
|
+
emotion_filter: emotionTag,
|
|
678
|
+
min_intensity: intensityHint
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
} catch (err) {
|
|
682
|
+
const msg = err.message;
|
|
683
|
+
return {
|
|
684
|
+
content: [{ type: "text", text: `mind_recall_emotional failed: ${msg}` }],
|
|
685
|
+
details: { error: msg }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/tools/context.ts
|
|
693
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
694
|
+
var ContextParameters = Type9.Object({
|
|
695
|
+
sections: Type9.Optional(
|
|
696
|
+
Type9.Array(
|
|
697
|
+
Type9.Union([
|
|
698
|
+
Type9.Literal("soul"),
|
|
699
|
+
Type9.Literal("user"),
|
|
700
|
+
Type9.Literal("rules"),
|
|
701
|
+
Type9.Literal("priorities"),
|
|
702
|
+
Type9.Literal("recent")
|
|
703
|
+
]),
|
|
704
|
+
{
|
|
705
|
+
description: "Which context sections to load. Default: all 5. 'soul'=identity. 'user'=preferences. 'rules'=constraints. 'priorities'=current goals. 'recent'=last 7 days."
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
});
|
|
710
|
+
var SECTION_QUERIES = {
|
|
711
|
+
soul: "user identity mission personality core values",
|
|
712
|
+
user: "user preferences role responsibilities personal details",
|
|
713
|
+
rules: "operating rules constraints guardrails behavioral preferences",
|
|
714
|
+
priorities: "current priorities goals deadlines active projects this quarter",
|
|
715
|
+
recent: "recent activity last 7 days outcomes decisions"
|
|
716
|
+
};
|
|
717
|
+
function createMindContextTool(deps) {
|
|
718
|
+
return {
|
|
719
|
+
name: "mind_context",
|
|
720
|
+
label: "MIND Context",
|
|
721
|
+
description: "Load structured persistent context at session start. Returns 5 sections: soul, user, rules, priorities, recent. Call FIRST in any session to ground the agent in who the user is and what matters now.",
|
|
722
|
+
parameters: ContextParameters,
|
|
723
|
+
async execute(_toolCallId, params) {
|
|
724
|
+
const sections = params.sections ?? ["soul", "user", "rules", "priorities", "recent"];
|
|
725
|
+
const result = {};
|
|
726
|
+
await Promise.all(
|
|
727
|
+
sections.map(async (section) => {
|
|
728
|
+
const queryStr = SECTION_QUERIES[section] ?? section;
|
|
729
|
+
try {
|
|
730
|
+
const resp = await deps.client.query({
|
|
731
|
+
query: queryStr,
|
|
732
|
+
mode: "hybrid",
|
|
733
|
+
top_k: 8
|
|
734
|
+
});
|
|
735
|
+
result[section] = {
|
|
736
|
+
summary: resp.answer,
|
|
737
|
+
sources: (resp.sources ?? []).slice(0, 5)
|
|
738
|
+
};
|
|
739
|
+
} catch (err) {
|
|
740
|
+
result[section] = { error: err.message };
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
);
|
|
744
|
+
const summary = sections.map((s) => {
|
|
745
|
+
const sec = result[s];
|
|
746
|
+
if (sec?.error) return `## ${s.toUpperCase()}
|
|
747
|
+
[Error: ${sec.error}]`;
|
|
748
|
+
return `## ${s.toUpperCase()}
|
|
749
|
+
${sec?.summary ?? ""}`;
|
|
750
|
+
}).join("\n\n");
|
|
751
|
+
return {
|
|
752
|
+
content: [{ type: "text", text: summary }],
|
|
753
|
+
details: result
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/tools/life.ts
|
|
760
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
761
|
+
var LifeParameters = Type10.Object({
|
|
762
|
+
action: Type10.Union(
|
|
763
|
+
[Type10.Literal("create"), Type10.Literal("list"), Type10.Literal("complete")],
|
|
764
|
+
{ description: "What to do" }
|
|
765
|
+
),
|
|
766
|
+
title: Type10.Optional(Type10.String({ description: "Item title (required for create)" })),
|
|
767
|
+
description: Type10.Optional(Type10.String()),
|
|
768
|
+
type: Type10.Optional(
|
|
769
|
+
Type10.Union(
|
|
770
|
+
[Type10.Literal("task"), Type10.Literal("goal"), Type10.Literal("project")],
|
|
771
|
+
{ description: "Item type. Default: 'task'" }
|
|
772
|
+
)
|
|
773
|
+
),
|
|
774
|
+
priority: Type10.Optional(
|
|
775
|
+
Type10.Union(
|
|
776
|
+
[
|
|
777
|
+
Type10.Literal("low"),
|
|
778
|
+
Type10.Literal("medium"),
|
|
779
|
+
Type10.Literal("high"),
|
|
780
|
+
Type10.Literal("urgent")
|
|
781
|
+
],
|
|
782
|
+
{ description: "Priority. Default: 'medium'" }
|
|
783
|
+
)
|
|
784
|
+
),
|
|
785
|
+
due_date: Type10.Optional(Type10.String({ description: "ISO date or datetime" })),
|
|
786
|
+
status: Type10.Optional(
|
|
787
|
+
Type10.Union(
|
|
788
|
+
[
|
|
789
|
+
Type10.Literal("todo"),
|
|
790
|
+
Type10.Literal("in_progress"),
|
|
791
|
+
Type10.Literal("blocked"),
|
|
792
|
+
Type10.Literal("done"),
|
|
793
|
+
Type10.Literal("all")
|
|
794
|
+
],
|
|
795
|
+
{ description: "Filter for list. Default: 'todo'" }
|
|
796
|
+
)
|
|
797
|
+
),
|
|
798
|
+
limit: Type10.Optional(Type10.Number({ description: "Max results for list. Default: 20" })),
|
|
799
|
+
item_id: Type10.Optional(Type10.String({ description: "Item ID for complete" }))
|
|
800
|
+
});
|
|
801
|
+
function createMindLifeTool(deps) {
|
|
802
|
+
return {
|
|
803
|
+
name: "mind_life",
|
|
804
|
+
label: "MIND Life",
|
|
805
|
+
description: "Manage MIND Life items (tasks, goals, projects). Use when the user mentions something to do, a deadline, a goal, or a project. Actions: 'create', 'list', 'complete'. MIND Life syncs to web and mobile apps.",
|
|
806
|
+
parameters: LifeParameters,
|
|
807
|
+
async execute(_toolCallId, params) {
|
|
808
|
+
try {
|
|
809
|
+
switch (params.action) {
|
|
810
|
+
case "create": {
|
|
811
|
+
if (!params.title) {
|
|
812
|
+
return {
|
|
813
|
+
content: [{ type: "text", text: "'title' is required for action='create'" }],
|
|
814
|
+
details: { error: "missing_title" }
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const item = await deps.client.createLifeItem({
|
|
818
|
+
title: params.title,
|
|
819
|
+
description: params.description,
|
|
820
|
+
type: params.type ?? "task",
|
|
821
|
+
status: "todo",
|
|
822
|
+
priority: params.priority ?? "medium",
|
|
823
|
+
due_date: params.due_date
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: "text", text: `Created life item "${item.title}" (id: ${item.id})` }],
|
|
827
|
+
details: item
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
case "list": {
|
|
831
|
+
const status = params.status ?? "todo";
|
|
832
|
+
const items = await deps.client.listLifeItems({
|
|
833
|
+
status: status === "all" ? void 0 : status,
|
|
834
|
+
limit: params.limit ?? 20
|
|
835
|
+
});
|
|
836
|
+
const list = items.items.map((i) => `- [${i.status}] ${i.title} ${i.due_date ? `(due ${i.due_date})` : ""}`).join("\n");
|
|
837
|
+
return {
|
|
838
|
+
content: [{ type: "text", text: `${items.items.length} items:
|
|
839
|
+
${list}` }],
|
|
840
|
+
details: items
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
case "complete": {
|
|
844
|
+
if (!params.item_id) {
|
|
845
|
+
return {
|
|
846
|
+
content: [{ type: "text", text: "'item_id' is required for action='complete'" }],
|
|
847
|
+
details: { error: "missing_item_id" }
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
await deps.client.completeLifeItem(params.item_id);
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: `Completed life item ${params.item_id}` }],
|
|
853
|
+
details: { id: params.item_id, status: "completed" }
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
const msg = err.message;
|
|
859
|
+
return {
|
|
860
|
+
content: [{ type: "text", text: `mind_life failed: ${msg}` }],
|
|
861
|
+
details: { error: msg }
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/tools/crm-log.ts
|
|
869
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
870
|
+
var CrmLogParameters = Type11.Object({
|
|
871
|
+
action: Type11.Union(
|
|
872
|
+
[
|
|
873
|
+
Type11.Literal("create_contact"),
|
|
874
|
+
Type11.Literal("list_contacts"),
|
|
875
|
+
Type11.Literal("log_activity")
|
|
876
|
+
],
|
|
877
|
+
{ description: "What to do" }
|
|
878
|
+
),
|
|
879
|
+
name: Type11.Optional(Type11.String({ description: "Contact name (required for create_contact)" })),
|
|
880
|
+
email: Type11.Optional(Type11.String()),
|
|
881
|
+
phone: Type11.Optional(Type11.String()),
|
|
882
|
+
company: Type11.Optional(Type11.String()),
|
|
883
|
+
type: Type11.Optional(Type11.String({ description: "Relationship type: investor, partner, customer" })),
|
|
884
|
+
stage: Type11.Optional(Type11.String({ description: "Pipeline stage" })),
|
|
885
|
+
notes: Type11.Optional(Type11.String()),
|
|
886
|
+
contact_id: Type11.Optional(Type11.String({ description: "Contact ID for log_activity" })),
|
|
887
|
+
activity_type: Type11.Optional(
|
|
888
|
+
Type11.Union(
|
|
889
|
+
[
|
|
890
|
+
Type11.Literal("call"),
|
|
891
|
+
Type11.Literal("email"),
|
|
892
|
+
Type11.Literal("meeting"),
|
|
893
|
+
Type11.Literal("note"),
|
|
894
|
+
Type11.Literal("other")
|
|
895
|
+
],
|
|
896
|
+
{ description: "Activity type" }
|
|
897
|
+
)
|
|
898
|
+
),
|
|
899
|
+
summary: Type11.Optional(Type11.String({ description: "What happened" })),
|
|
900
|
+
occurred_at: Type11.Optional(Type11.String({ description: "ISO datetime" })),
|
|
901
|
+
limit: Type11.Optional(Type11.Number({ description: "Max contacts to list" }))
|
|
902
|
+
});
|
|
903
|
+
function createMindCrmLogTool(deps) {
|
|
904
|
+
return {
|
|
905
|
+
name: "mind_crm_log",
|
|
906
|
+
label: "MIND CRM",
|
|
907
|
+
description: "Manage MIND CRM contacts and log interactions. Use when the user mentions a person they're working with, a meeting, an email, or a relationship. Actions: 'create_contact', 'list_contacts', 'log_activity'.",
|
|
908
|
+
parameters: CrmLogParameters,
|
|
909
|
+
async execute(_toolCallId, params) {
|
|
910
|
+
try {
|
|
911
|
+
switch (params.action) {
|
|
912
|
+
case "create_contact": {
|
|
913
|
+
if (!params.name) {
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: "text", text: "'name' is required for create_contact" }],
|
|
916
|
+
details: { error: "missing_name" }
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const contact = await deps.client.createCrmContact({
|
|
920
|
+
name: params.name,
|
|
921
|
+
email: params.email,
|
|
922
|
+
phone: params.phone,
|
|
923
|
+
company: params.company,
|
|
924
|
+
type: params.type,
|
|
925
|
+
stage: params.stage,
|
|
926
|
+
notes: params.notes
|
|
927
|
+
});
|
|
928
|
+
return {
|
|
929
|
+
content: [{ type: "text", text: `Created contact ${contact.name} (id: ${contact.id})` }],
|
|
930
|
+
details: contact
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
case "list_contacts": {
|
|
934
|
+
const contacts = await deps.client.listCrmContacts({ limit: params.limit ?? 20 });
|
|
935
|
+
const list = contacts.contacts.map((c) => `- ${c.name} ${c.company ? `(${c.company})` : ""}`).join("\n");
|
|
936
|
+
return {
|
|
937
|
+
content: [{ type: "text", text: `${contacts.contacts.length} contacts:
|
|
938
|
+
${list}` }],
|
|
939
|
+
details: contacts
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
case "log_activity": {
|
|
943
|
+
if (!params.contact_id || !params.activity_type || !params.summary) {
|
|
944
|
+
return {
|
|
945
|
+
content: [
|
|
946
|
+
{
|
|
947
|
+
type: "text",
|
|
948
|
+
text: "'contact_id', 'activity_type', and 'summary' are required for log_activity"
|
|
949
|
+
}
|
|
950
|
+
],
|
|
951
|
+
details: { error: "missing_required" }
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
await deps.client.logCrmActivity(params.contact_id, {
|
|
955
|
+
type: params.activity_type,
|
|
956
|
+
summary: params.summary,
|
|
957
|
+
occurred_at: params.occurred_at
|
|
958
|
+
});
|
|
959
|
+
return {
|
|
960
|
+
content: [
|
|
961
|
+
{
|
|
962
|
+
type: "text",
|
|
963
|
+
text: `Logged ${params.activity_type} activity for contact ${params.contact_id}`
|
|
964
|
+
}
|
|
965
|
+
],
|
|
966
|
+
details: { contact_id: params.contact_id, activity_type: params.activity_type }
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} catch (err) {
|
|
971
|
+
const msg = err.message;
|
|
972
|
+
return {
|
|
973
|
+
content: [{ type: "text", text: `mind_crm_log failed: ${msg}` }],
|
|
974
|
+
details: { error: msg }
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/tools/index.ts
|
|
982
|
+
function registerAllTools(api, client, config) {
|
|
983
|
+
const deps = { client, config };
|
|
984
|
+
api.registerTool(createMindSearchTool(deps));
|
|
985
|
+
api.registerTool(createMindAddTool(deps));
|
|
986
|
+
api.registerTool(createMindGetTool(deps));
|
|
987
|
+
api.registerTool(createMindListTool(deps));
|
|
988
|
+
api.registerTool(createMindUpdateTool(deps));
|
|
989
|
+
api.registerTool(createMindDeleteTool(deps));
|
|
990
|
+
api.registerTool(createMindQueryGraphTool(deps));
|
|
991
|
+
if (config.enableMindsense) {
|
|
992
|
+
api.registerTool(createMindRecallEmotionalTool(deps));
|
|
993
|
+
}
|
|
994
|
+
api.registerTool(createMindContextTool(deps));
|
|
995
|
+
if (config.enableLifeIntegration) {
|
|
996
|
+
api.registerTool(createMindLifeTool(deps));
|
|
997
|
+
}
|
|
998
|
+
if (config.enableCrmLogging) {
|
|
999
|
+
api.registerTool(createMindCrmLogTool(deps));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/cli/auth-file.ts
|
|
1004
|
+
import { promises as fs } from "fs";
|
|
1005
|
+
import * as path from "path";
|
|
1006
|
+
import * as os from "os";
|
|
1007
|
+
var AUTH_DIR = path.join(os.homedir(), ".openclaw", "plugins");
|
|
1008
|
+
var AUTH_FILE = path.join(AUTH_DIR, "openclaw-mind.json");
|
|
1009
|
+
async function writeAuthFile(auth) {
|
|
1010
|
+
await fs.mkdir(AUTH_DIR, { recursive: true, mode: 448 });
|
|
1011
|
+
await fs.writeFile(AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 384 });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/cli/init.ts
|
|
1015
|
+
async function initCommand(api, opts, config) {
|
|
1016
|
+
const baseUrl = opts.baseUrl ?? config.baseUrl;
|
|
1017
|
+
if (opts.apiKey) {
|
|
1018
|
+
return setupWithApiKey(api, opts.apiKey, baseUrl);
|
|
1019
|
+
}
|
|
1020
|
+
if (opts.email) {
|
|
1021
|
+
return setupWithMagicLink(api, opts.email, baseUrl);
|
|
1022
|
+
}
|
|
1023
|
+
api.logger.info(
|
|
1024
|
+
[
|
|
1025
|
+
"To set up MIND, provide an API key or email:",
|
|
1026
|
+
"",
|
|
1027
|
+
" openclaw mind init --api-key mind_your_key_here",
|
|
1028
|
+
" openclaw mind init --email you@example.com",
|
|
1029
|
+
"",
|
|
1030
|
+
"Get an API key at https://www.m-i-n-d.ai \u2192 Settings \u2192 Developer \u2192 Create API Key"
|
|
1031
|
+
].join("\n")
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
async function setupWithApiKey(api, apiKey, baseUrl) {
|
|
1035
|
+
if (!apiKey.startsWith("mind_")) {
|
|
1036
|
+
api.logger.error("Invalid API key format. Keys must start with 'mind_'.");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const client = new MindClient({ apiKey, baseUrl, logger: api.logger });
|
|
1040
|
+
try {
|
|
1041
|
+
const info = await client.getGraphInfo();
|
|
1042
|
+
api.logger.info(
|
|
1043
|
+
[
|
|
1044
|
+
"\u2713 Connected to MIND",
|
|
1045
|
+
` Entities: ${info.entity_count ?? "?"}`,
|
|
1046
|
+
` Relationships: ${info.relationship_count ?? "?"}`,
|
|
1047
|
+
` Credits remaining: ${info.credits_remaining ?? "?"}`
|
|
1048
|
+
].join("\n")
|
|
1049
|
+
);
|
|
1050
|
+
await writeAuthFile({ apiKey, baseUrl });
|
|
1051
|
+
api.logger.info("\u2713 API key saved. MIND auto-recall and auto-capture are now active.");
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
api.logger.error(
|
|
1054
|
+
`Failed to verify MIND API key: ${err.message}
|
|
1055
|
+
Double-check the key at https://www.m-i-n-d.ai/settings/developer`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function setupWithMagicLink(api, _email, _baseUrl) {
|
|
1060
|
+
api.logger.error(
|
|
1061
|
+
[
|
|
1062
|
+
"Magic-link auth is coming in v0.2. For now, use --api-key:",
|
|
1063
|
+
"",
|
|
1064
|
+
" openclaw mind init --api-key mind_your_key_here",
|
|
1065
|
+
"",
|
|
1066
|
+
"Get a key at https://www.m-i-n-d.ai \u2192 Settings \u2192 Developer."
|
|
1067
|
+
].join("\n")
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/cli/status.ts
|
|
1072
|
+
async function statusCommand(api, client, config) {
|
|
1073
|
+
if (!client || config.needsSetup) {
|
|
1074
|
+
api.logger.info(
|
|
1075
|
+
[
|
|
1076
|
+
"MIND: NOT CONNECTED",
|
|
1077
|
+
"",
|
|
1078
|
+
"Run: openclaw mind init --api-key mind_your_key_here",
|
|
1079
|
+
"Get a key at https://www.m-i-n-d.ai \u2192 Settings \u2192 Developer"
|
|
1080
|
+
].join("\n")
|
|
1081
|
+
);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const info = await client.getGraphInfo();
|
|
1086
|
+
api.logger.info(
|
|
1087
|
+
[
|
|
1088
|
+
"MIND: CONNECTED",
|
|
1089
|
+
` Base URL: ${config.baseUrl}`,
|
|
1090
|
+
` Entities: ${info.entity_count ?? "?"}`,
|
|
1091
|
+
` Relationships: ${info.relationship_count ?? "?"}`,
|
|
1092
|
+
` Storage health: ${info.storage_health ?? "?"}`,
|
|
1093
|
+
` Credits left: ${info.credits_remaining ?? "?"}`,
|
|
1094
|
+
"",
|
|
1095
|
+
"Settings:",
|
|
1096
|
+
` Auto-recall: ${config.autoRecall}`,
|
|
1097
|
+
` Auto-capture: ${config.autoCapture}`,
|
|
1098
|
+
` Top-K recall: ${config.topK}`,
|
|
1099
|
+
` Query mode: ${config.queryMode}`,
|
|
1100
|
+
` MINDsense: ${config.enableMindsense}`,
|
|
1101
|
+
` Life integration: ${config.enableLifeIntegration}`,
|
|
1102
|
+
` CRM logging: ${config.enableCrmLogging}`
|
|
1103
|
+
].join("\n")
|
|
1104
|
+
);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
api.logger.error(`Failed to reach MIND: ${err.message}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/cli/import.ts
|
|
1111
|
+
import { promises as fs2 } from "fs";
|
|
1112
|
+
import * as path2 from "path";
|
|
1113
|
+
import * as os2 from "os";
|
|
1114
|
+
var ALWAYS_IMPORT = ["SOUL.md", "IDENTITY.md", "USER.md", "MEMORY.md"];
|
|
1115
|
+
var SKIP = /* @__PURE__ */ new Set(["AGENTS.md", "BOOTSTRAP.md", "HEARTBEAT.md", "TOOLS.md"]);
|
|
1116
|
+
var WORDS_PER_DOCUMENT_THRESHOLD = 2e3;
|
|
1117
|
+
async function importCommand(api, client, config, opts) {
|
|
1118
|
+
if (!client || config.needsSetup) {
|
|
1119
|
+
api.logger.error("MIND not configured. Run: openclaw mind init --api-key mind_...");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const workspace = opts.workspace ?? path2.join(os2.homedir(), ".openclaw", "workspace");
|
|
1123
|
+
const days = parseInt(opts.days ?? "30", 10);
|
|
1124
|
+
if (!await pathExists(workspace)) {
|
|
1125
|
+
api.logger.error(`Workspace not found: ${workspace}`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
api.logger.info(`Scanning ${workspace}...`);
|
|
1129
|
+
const items = [];
|
|
1130
|
+
for (const filename of ALWAYS_IMPORT) {
|
|
1131
|
+
const filepath = path2.join(workspace, filename);
|
|
1132
|
+
if (await pathExists(filepath)) {
|
|
1133
|
+
const content = await fs2.readFile(filepath, "utf8");
|
|
1134
|
+
items.push(...splitFile(filename, content, ["openclaw-import", "core-memory"]));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const memoryDir = path2.join(workspace, "memory");
|
|
1138
|
+
if (await pathExists(memoryDir)) {
|
|
1139
|
+
const files = await fs2.readdir(memoryDir);
|
|
1140
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
1141
|
+
const dateFiles = files.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort().reverse();
|
|
1142
|
+
for (const file of dateFiles) {
|
|
1143
|
+
const dateStr = file.replace(".md", "");
|
|
1144
|
+
const fileTime = new Date(dateStr).getTime();
|
|
1145
|
+
if (Number.isNaN(fileTime) || fileTime < cutoff) continue;
|
|
1146
|
+
const filepath = path2.join(memoryDir, file);
|
|
1147
|
+
const content = await fs2.readFile(filepath, "utf8");
|
|
1148
|
+
items.push(...splitFile(file, content, ["openclaw-import", "daily-memory", dateStr]));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (items.length === 0) {
|
|
1152
|
+
api.logger.info("Nothing to import.");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
api.logger.info(`Found ${items.length} memory chunks. Importing to MIND...`);
|
|
1156
|
+
let succeeded = 0;
|
|
1157
|
+
let failed = 0;
|
|
1158
|
+
for (const item of items) {
|
|
1159
|
+
try {
|
|
1160
|
+
await client.createDocument({
|
|
1161
|
+
title: item.title,
|
|
1162
|
+
content: item.content,
|
|
1163
|
+
tags: item.tags,
|
|
1164
|
+
source: "openclaw-mind-import"
|
|
1165
|
+
});
|
|
1166
|
+
succeeded++;
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
failed++;
|
|
1169
|
+
api.logger?.warn(`Failed to import "${item.title}": ${err.message}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
api.logger.info(
|
|
1173
|
+
`Done. Imported: ${succeeded}, failed: ${failed}. Run \`openclaw mind status\` to see updated entity count.`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
function splitFile(filename, content, tags) {
|
|
1177
|
+
if (SKIP.has(filename)) return [];
|
|
1178
|
+
const wordCount = content.trim().split(/\s+/).length;
|
|
1179
|
+
if (wordCount <= WORDS_PER_DOCUMENT_THRESHOLD) {
|
|
1180
|
+
return [
|
|
1181
|
+
{
|
|
1182
|
+
title: `${filename}`,
|
|
1183
|
+
content: content.trim(),
|
|
1184
|
+
tags
|
|
1185
|
+
}
|
|
1186
|
+
];
|
|
1187
|
+
}
|
|
1188
|
+
const sections = [];
|
|
1189
|
+
const lines = content.split("\n");
|
|
1190
|
+
let currentTitle = filename;
|
|
1191
|
+
let currentBuffer = [];
|
|
1192
|
+
const flush = () => {
|
|
1193
|
+
const text = currentBuffer.join("\n").trim();
|
|
1194
|
+
if (text.length > 0) {
|
|
1195
|
+
sections.push({ title: currentTitle, content: text, tags });
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
for (const line of lines) {
|
|
1199
|
+
if (line.startsWith("# ")) {
|
|
1200
|
+
flush();
|
|
1201
|
+
currentBuffer = [line];
|
|
1202
|
+
currentTitle = `${filename} \u2014 ${line.replace(/^#\s*/, "").trim()}`;
|
|
1203
|
+
} else {
|
|
1204
|
+
currentBuffer.push(line);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
flush();
|
|
1208
|
+
return sections;
|
|
1209
|
+
}
|
|
1210
|
+
async function pathExists(p) {
|
|
1211
|
+
try {
|
|
1212
|
+
await fs2.stat(p);
|
|
1213
|
+
return true;
|
|
1214
|
+
} catch {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/cli/search.ts
|
|
1220
|
+
async function searchCommand(api, client, opts) {
|
|
1221
|
+
if (!client) {
|
|
1222
|
+
api.logger.error("MIND not configured. Run: openclaw mind init --api-key mind_...");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (!opts.query) {
|
|
1226
|
+
api.logger.error('Usage: openclaw mind search "your query"');
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const mode = opts.mode ?? "hybrid";
|
|
1230
|
+
const topK = parseInt(opts.topK ?? "5", 10);
|
|
1231
|
+
try {
|
|
1232
|
+
const result = await client.query({
|
|
1233
|
+
query: opts.query,
|
|
1234
|
+
mode,
|
|
1235
|
+
top_k: topK
|
|
1236
|
+
});
|
|
1237
|
+
api.logger.info(`
|
|
1238
|
+
${result.answer}
|
|
1239
|
+
`);
|
|
1240
|
+
if (result.sources && result.sources.length > 0) {
|
|
1241
|
+
const sourceLines = result.sources.map((s, i) => {
|
|
1242
|
+
const score = s.score ? `(score: ${s.score.toFixed(2)})` : "";
|
|
1243
|
+
return ` ${i + 1}. ${s.title ?? "(untitled)"} ${score}`;
|
|
1244
|
+
});
|
|
1245
|
+
api.logger.info(`Sources:
|
|
1246
|
+
${sourceLines.join("\n")}`);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
api.logger.error(`Search failed: ${err.message}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/cli/index.ts
|
|
1254
|
+
function registerCliCommands(api, client, config) {
|
|
1255
|
+
api.registerCli(
|
|
1256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1257
|
+
(ctx) => {
|
|
1258
|
+
const program = ctx.program;
|
|
1259
|
+
const mind = program.command("mind").description("MIND personal knowledge graph commands");
|
|
1260
|
+
mind.command("init").description("Set up your MIND API key and verify the connection").option("--api-key <key>", "MIND API key (mind_...)").option("--email <email>", "Email for magic-link auth (alternative to api-key)").option("--base-url <url>", "Override the MIND base URL (for self-hosting)").action(async (opts) => {
|
|
1261
|
+
await initCommand(
|
|
1262
|
+
api,
|
|
1263
|
+
{ apiKey: opts.apiKey, email: opts.email, baseUrl: opts.baseUrl },
|
|
1264
|
+
config
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
mind.command("status").description("Verify the MIND connection and show your account info").action(async () => {
|
|
1268
|
+
await statusCommand(api, client, config);
|
|
1269
|
+
});
|
|
1270
|
+
mind.command("import").description("Import OpenClaw workspace memory files into MIND").option(
|
|
1271
|
+
"--workspace <path>",
|
|
1272
|
+
"Path to OpenClaw workspace dir (default: ~/.openclaw/workspace)"
|
|
1273
|
+
).option("--days <n>", "How many days of recent memory files to import (default: 30)").action(async (opts) => {
|
|
1274
|
+
await importCommand(api, client, config, {
|
|
1275
|
+
workspace: opts.workspace,
|
|
1276
|
+
days: opts.days
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
mind.command("search").description("Quick CLI search of the MIND knowledge graph").argument("<query>", "What to search for").option("--mode <mode>", "Query mode (hybrid/mix/global/local/naive)").option("--top-k <n>", "Max results (default: 5)").action(async (query, opts) => {
|
|
1280
|
+
await searchCommand(api, client, {
|
|
1281
|
+
query,
|
|
1282
|
+
mode: opts.mode,
|
|
1283
|
+
topK: opts.topK
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
},
|
|
1287
|
+
{ commands: ["mind"] }
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/lib/session.ts
|
|
1292
|
+
var NON_INTERACTIVE_TRIGGERS = /* @__PURE__ */ new Set(["cron", "scheduled", "webhook", "event", "system"]);
|
|
1293
|
+
var SUBAGENT_PATTERN = /:subagent:/;
|
|
1294
|
+
var CRON_SESSION_PATTERN = /:cron:/;
|
|
1295
|
+
function isNonInteractiveTrigger(trigger, sessionKey) {
|
|
1296
|
+
if (trigger && NON_INTERACTIVE_TRIGGERS.has(trigger.toLowerCase())) {
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
if (sessionKey && CRON_SESSION_PATTERN.test(sessionKey)) {
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
function isSubagentSession(sessionKey) {
|
|
1305
|
+
if (!sessionKey) return false;
|
|
1306
|
+
return SUBAGENT_PATTERN.test(sessionKey);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/lib/filter.ts
|
|
1310
|
+
var NOISE_PATTERNS = [
|
|
1311
|
+
/^heartbeat\b/i,
|
|
1312
|
+
/^cron:/i,
|
|
1313
|
+
/^ping$/i,
|
|
1314
|
+
/^pong$/i,
|
|
1315
|
+
/^ok$/i,
|
|
1316
|
+
/^thanks$/i,
|
|
1317
|
+
/^thank you$/i,
|
|
1318
|
+
/^got it$/i,
|
|
1319
|
+
/^sure$/i,
|
|
1320
|
+
/^yes$/i,
|
|
1321
|
+
/^no$/i
|
|
1322
|
+
];
|
|
1323
|
+
var GENERIC_ASSISTANT_PATTERNS = [
|
|
1324
|
+
/^how can i (help|assist) you/i,
|
|
1325
|
+
/^what (can|would you like)/i,
|
|
1326
|
+
/^i('m| am) (here to help|happy to help)/i,
|
|
1327
|
+
/^sure[,.!]? (i can|let me)/i,
|
|
1328
|
+
/^certainly[,.!]/i
|
|
1329
|
+
];
|
|
1330
|
+
var NOISE_FRAGMENTS = [
|
|
1331
|
+
/<routing-metadata>[\s\S]*?<\/routing-metadata>/g,
|
|
1332
|
+
/<compaction-audit>[\s\S]*?<\/compaction-audit>/g,
|
|
1333
|
+
/<openclaw:heartbeat>[\s\S]*?<\/openclaw:heartbeat>/g
|
|
1334
|
+
];
|
|
1335
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
1336
|
+
function isNoiseMessage(content) {
|
|
1337
|
+
const trimmed = content.trim();
|
|
1338
|
+
if (trimmed.length === 0) return true;
|
|
1339
|
+
if (trimmed.length < 4) return true;
|
|
1340
|
+
return NOISE_PATTERNS.some((p) => p.test(trimmed));
|
|
1341
|
+
}
|
|
1342
|
+
function isGenericAssistantMessage(content) {
|
|
1343
|
+
const trimmed = content.trim();
|
|
1344
|
+
if (trimmed.length > 200) return false;
|
|
1345
|
+
return GENERIC_ASSISTANT_PATTERNS.some((p) => p.test(trimmed));
|
|
1346
|
+
}
|
|
1347
|
+
function stripNoiseFromContent(content) {
|
|
1348
|
+
let result = content;
|
|
1349
|
+
for (const pattern of NOISE_FRAGMENTS) {
|
|
1350
|
+
result = result.replace(pattern, "");
|
|
1351
|
+
}
|
|
1352
|
+
return result.trim();
|
|
1353
|
+
}
|
|
1354
|
+
function filterMessagesForExtraction(messages) {
|
|
1355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1356
|
+
const filtered = [];
|
|
1357
|
+
for (const msg of messages) {
|
|
1358
|
+
let content = stripNoiseFromContent(msg.content);
|
|
1359
|
+
if (isNoiseMessage(content)) continue;
|
|
1360
|
+
if (msg.role === "assistant" && isGenericAssistantMessage(content)) continue;
|
|
1361
|
+
if (content.length > MAX_MESSAGE_CHARS) {
|
|
1362
|
+
content = `${content.slice(0, MAX_MESSAGE_CHARS)}\u2026[truncated]`;
|
|
1363
|
+
}
|
|
1364
|
+
const key = `${msg.role}:${content}`;
|
|
1365
|
+
if (seen.has(key)) continue;
|
|
1366
|
+
seen.add(key);
|
|
1367
|
+
filtered.push({ role: msg.role, content });
|
|
1368
|
+
}
|
|
1369
|
+
return filtered;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/hooks/index.ts
|
|
1373
|
+
function registerLifecycleHooks(api, client, config) {
|
|
1374
|
+
if (config.autoRecall) {
|
|
1375
|
+
api.on(
|
|
1376
|
+
"before_prompt_build",
|
|
1377
|
+
async (event, ctx) => {
|
|
1378
|
+
try {
|
|
1379
|
+
if (isNonInteractiveTrigger(ctx?.trigger, ctx?.sessionKey)) return;
|
|
1380
|
+
if (isSubagentSession(ctx?.sessionKey)) return;
|
|
1381
|
+
const recallQuery = pickRecallQuery(event);
|
|
1382
|
+
if (!recallQuery || recallQuery.length < 5) return;
|
|
1383
|
+
const result = await client.query({
|
|
1384
|
+
query: recallQuery,
|
|
1385
|
+
mode: config.queryMode,
|
|
1386
|
+
top_k: config.topK
|
|
1387
|
+
});
|
|
1388
|
+
if (!result.answer) return;
|
|
1389
|
+
const memoryContext = formatRecallContext(result);
|
|
1390
|
+
api.logger.debug?.(
|
|
1391
|
+
`mind auto-recall: injected ${result.sources?.length ?? 0} sources`
|
|
1392
|
+
);
|
|
1393
|
+
return { prependContext: memoryContext };
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
api.logger.warn(`mind auto-recall failed: ${err.message}`);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (config.autoCapture) {
|
|
1402
|
+
api.on(
|
|
1403
|
+
"agent_end",
|
|
1404
|
+
async (event, ctx) => {
|
|
1405
|
+
try {
|
|
1406
|
+
if (isNonInteractiveTrigger(ctx?.trigger, ctx?.sessionKey)) return;
|
|
1407
|
+
if (isSubagentSession(ctx?.sessionKey)) return;
|
|
1408
|
+
if (!event.success) return;
|
|
1409
|
+
const messages = event.messages ?? [];
|
|
1410
|
+
const normalized = messages.filter(
|
|
1411
|
+
(m) => typeof m.role === "string" && typeof m.content === "string"
|
|
1412
|
+
).map((m) => ({ role: m.role, content: m.content }));
|
|
1413
|
+
const filtered = filterMessagesForExtraction(normalized);
|
|
1414
|
+
if (filtered.length === 0) return;
|
|
1415
|
+
const conversationText = filtered.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n").slice(0, 8e3);
|
|
1416
|
+
await client.createEntry({
|
|
1417
|
+
content: conversationText,
|
|
1418
|
+
type: "entry",
|
|
1419
|
+
tags: ["openclaw", "auto-capture"],
|
|
1420
|
+
source: "openclaw-mind"
|
|
1421
|
+
});
|
|
1422
|
+
api.logger.debug?.("mind auto-capture: stored turn");
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
api.logger.warn(`mind auto-capture failed: ${err.message}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function pickRecallQuery(event) {
|
|
1431
|
+
if (event.prompt) return event.prompt;
|
|
1432
|
+
const messages = event.messages ?? [];
|
|
1433
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
1434
|
+
return typeof lastUser?.content === "string" ? lastUser.content : void 0;
|
|
1435
|
+
}
|
|
1436
|
+
function formatRecallContext(result) {
|
|
1437
|
+
const sourceList = (result.sources ?? []).slice(0, 5).map((s, i) => `${i + 1}. ${s.title ?? "(untitled)"}`).join("\n");
|
|
1438
|
+
return [
|
|
1439
|
+
"## Persistent Memory (MIND knowledge graph)",
|
|
1440
|
+
"",
|
|
1441
|
+
"Context retrieved from the user's MIND knowledge graph based on the current request. Use it to inform your response.",
|
|
1442
|
+
"",
|
|
1443
|
+
result.answer,
|
|
1444
|
+
sourceList ? `
|
|
1445
|
+
**Sources:**
|
|
1446
|
+
${sourceList}` : ""
|
|
1447
|
+
].join("\n");
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/index.ts
|
|
1451
|
+
var SETUP_MESSAGE = [
|
|
1452
|
+
"openclaw-mind: MIND_API_KEY not configured. Memory features are disabled.",
|
|
1453
|
+
" To set up:",
|
|
1454
|
+
" 1. Get a key at https://www.m-i-n-d.ai \u2192 Settings \u2192 Developer",
|
|
1455
|
+
" 2. Run: openclaw mind init --api-key mind_your_key_here",
|
|
1456
|
+
" Or set MIND_API_KEY in your environment."
|
|
1457
|
+
].join("\n");
|
|
1458
|
+
var index_default = definePluginEntry({
|
|
1459
|
+
id: "openclaw-mind",
|
|
1460
|
+
name: "MIND",
|
|
1461
|
+
description: "MIND personal knowledge graph for OpenClaw \u2014 true KG, 50+ AI models, MINDsense emotional intelligence, life management, CRM. The most complete memory plugin for AI agents.",
|
|
1462
|
+
kind: "memory",
|
|
1463
|
+
register(api) {
|
|
1464
|
+
const cfg = resolveConfig(api.pluginConfig);
|
|
1465
|
+
if (cfg.needsSetup) {
|
|
1466
|
+
api.logger.warn(SETUP_MESSAGE);
|
|
1467
|
+
registerCliCommands(api, null, cfg);
|
|
1468
|
+
api.registerService({
|
|
1469
|
+
id: "openclaw-mind",
|
|
1470
|
+
start: () => {
|
|
1471
|
+
api.logger.info("openclaw-mind: waiting for API key configuration");
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const client = new MindClient({
|
|
1477
|
+
apiKey: cfg.apiKey,
|
|
1478
|
+
baseUrl: cfg.baseUrl,
|
|
1479
|
+
logger: api.logger
|
|
1480
|
+
});
|
|
1481
|
+
registerAllTools(api, client, cfg);
|
|
1482
|
+
registerCliCommands(api, client, cfg);
|
|
1483
|
+
if (cfg.autoRecall || cfg.autoCapture) {
|
|
1484
|
+
registerLifecycleHooks(api, client, cfg);
|
|
1485
|
+
}
|
|
1486
|
+
api.registerService({
|
|
1487
|
+
id: "openclaw-mind",
|
|
1488
|
+
start: () => {
|
|
1489
|
+
api.logger.info(
|
|
1490
|
+
`openclaw-mind v0.1.0 ready \u2014 autoRecall=${cfg.autoRecall} autoCapture=${cfg.autoCapture} baseUrl=${cfg.baseUrl}`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
export {
|
|
1497
|
+
index_default as default
|
|
1498
|
+
};
|
|
1499
|
+
//# sourceMappingURL=index.js.map
|