@basicmemory/openclaw-basic-memory 0.2.3 → 0.23.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/README.md +49 -19
- package/bm-client.ts +55 -21
- package/commands/cli.ts +1 -2
- package/commands/skills.ts +71 -10
- package/commands/slash.ts +4 -2
- package/config.ts +0 -16
- package/context-engine/basic-memory-context-engine.ts +22 -90
- package/dist/bm-client.d.ts +179 -0
- package/dist/bm-client.js +582 -0
- package/dist/commands/cli.d.ts +4 -0
- package/dist/commands/cli.js +116 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.js +86 -0
- package/dist/commands/slash.d.ts +3 -0
- package/dist/commands/slash.js +92 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +99 -0
- package/dist/context-engine/basic-memory-context-engine.d.ts +34 -0
- package/dist/context-engine/basic-memory-context-engine.js +176 -0
- package/dist/hooks/capture.d.ts +24 -0
- package/dist/hooks/capture.js +88 -0
- package/dist/hooks/recall.d.ts +24 -0
- package/dist/hooks/recall.js +56 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +135 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +34 -0
- package/dist/schema/conversation-schema.d.ts +5 -0
- package/dist/schema/conversation-schema.js +33 -0
- package/dist/schema/task-schema.d.ts +5 -0
- package/dist/schema/task-schema.js +34 -0
- package/dist/tools/build-context.d.ts +3 -0
- package/dist/tools/build-context.js +100 -0
- package/dist/tools/delete-note.d.ts +3 -0
- package/dist/tools/delete-note.js +52 -0
- package/dist/tools/edit-note.d.ts +3 -0
- package/dist/tools/edit-note.js +81 -0
- package/dist/tools/list-memory-projects.d.ts +3 -0
- package/dist/tools/list-memory-projects.js +86 -0
- package/dist/tools/list-workspaces.d.ts +3 -0
- package/dist/tools/list-workspaces.js +64 -0
- package/dist/tools/memory-provider.d.ts +15 -0
- package/dist/tools/memory-provider.js +274 -0
- package/dist/tools/move-note.d.ts +3 -0
- package/dist/tools/move-note.js +56 -0
- package/dist/tools/read-note.d.ts +3 -0
- package/dist/tools/read-note.js +57 -0
- package/dist/tools/schema-diff.d.ts +3 -0
- package/dist/tools/schema-diff.js +81 -0
- package/dist/tools/schema-infer.d.ts +3 -0
- package/dist/tools/schema-infer.js +66 -0
- package/dist/tools/schema-validate.d.ts +3 -0
- package/dist/tools/schema-validate.js +73 -0
- package/dist/tools/search-notes.d.ts +3 -0
- package/dist/tools/search-notes.js +97 -0
- package/dist/tools/write-note.d.ts +3 -0
- package/dist/tools/write-note.js +80 -0
- package/index.ts +27 -17
- package/openclaw.plugin.json +27 -20
- package/package.json +27 -8
- package/scripts/fetch-skills.ts +62 -67
- package/scripts/setup-bm.sh +27 -10
- package/skills/manifest.json +25 -0
- package/skills/memory-capture/SKILL.md +275 -0
- package/skills/memory-ci-capture/SKILL.md +56 -0
- package/skills/memory-continue/SKILL.md +148 -0
- package/skills/memory-curate/SKILL.md +243 -0
- package/skills/memory-literary-analysis/SKILL.md +15 -12
- package/skills/memory-notes/SKILL.md +22 -6
- package/skills/memory-onboarding/SKILL.md +142 -0
- package/skills/memory-onboarding/evals/evals.json +29 -0
- package/skills/memory-onboarding/references/assistant-setup.md +64 -0
- package/skills/memory-onboarding/references/conventions.md +118 -0
- package/skills/memory-onboarding/references/domain-playbooks.md +159 -0
- package/skills/memory-onboarding/references/schema-guide.md +136 -0
- package/skills/memory-schema/SKILL.md +7 -5
- package/skills/memory-tasks/SKILL.md +1 -1
- package/tools/build-context.ts +1 -1
- package/tools/delete-note.ts +1 -1
- package/tools/edit-note.ts +1 -1
- package/tools/list-memory-projects.ts +3 -2
- package/tools/list-workspaces.ts +1 -1
- package/tools/memory-provider.ts +1 -1
- package/tools/move-note.ts +1 -1
- package/tools/read-note.ts +1 -1
- package/tools/schema-diff.ts +1 -1
- package/tools/schema-infer.ts +1 -1
- package/tools/schema-validate.ts +1 -1
- package/tools/search-notes.ts +1 -1
- package/tools/write-note.ts +1 -1
- package/context-engine/basic-memory-context-engine.test.ts +0 -398
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000];
|
|
6
|
+
export class NoteAlreadyExistsError extends Error {
|
|
7
|
+
permalink;
|
|
8
|
+
constructor(title, permalink) {
|
|
9
|
+
super(`Note already exists: "${title}" (${permalink})`);
|
|
10
|
+
this.name = "NoteAlreadyExistsError";
|
|
11
|
+
this.permalink = permalink;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const REQUIRED_TOOLS = [
|
|
15
|
+
"search_notes",
|
|
16
|
+
"read_note",
|
|
17
|
+
"write_note",
|
|
18
|
+
"edit_note",
|
|
19
|
+
"build_context",
|
|
20
|
+
"recent_activity",
|
|
21
|
+
"list_memory_projects",
|
|
22
|
+
"list_workspaces",
|
|
23
|
+
"create_memory_project",
|
|
24
|
+
"delete_note",
|
|
25
|
+
"move_note",
|
|
26
|
+
"schema_validate",
|
|
27
|
+
"schema_infer",
|
|
28
|
+
"schema_diff",
|
|
29
|
+
];
|
|
30
|
+
function getErrorMessage(err) {
|
|
31
|
+
return err instanceof Error ? err.message : String(err);
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function extractTextFromContent(content) {
|
|
37
|
+
if (!Array.isArray(content))
|
|
38
|
+
return "";
|
|
39
|
+
const textBlocks = content
|
|
40
|
+
.filter((block) => isRecord(block) &&
|
|
41
|
+
block.type === "text" &&
|
|
42
|
+
typeof block.text === "string")
|
|
43
|
+
.map((block) => block.text);
|
|
44
|
+
return textBlocks.join("\n").trim();
|
|
45
|
+
}
|
|
46
|
+
function isRecoverableConnectionError(err) {
|
|
47
|
+
const msg = getErrorMessage(err).toLowerCase();
|
|
48
|
+
return (msg.includes("connection closed") ||
|
|
49
|
+
msg.includes("not connected") ||
|
|
50
|
+
msg.includes("transport") ||
|
|
51
|
+
msg.includes("broken pipe") ||
|
|
52
|
+
msg.includes("econnreset") ||
|
|
53
|
+
msg.includes("epipe") ||
|
|
54
|
+
msg.includes("failed to start bm mcp stdio") ||
|
|
55
|
+
msg.includes("client is closed"));
|
|
56
|
+
}
|
|
57
|
+
function isNoteNotFoundError(err) {
|
|
58
|
+
const msg = getErrorMessage(err).toLowerCase();
|
|
59
|
+
return (msg.includes("entity not found") ||
|
|
60
|
+
msg.includes("note not found") ||
|
|
61
|
+
msg.includes("resource not found") ||
|
|
62
|
+
msg.includes("could not find note matching") ||
|
|
63
|
+
msg.includes("404"));
|
|
64
|
+
}
|
|
65
|
+
function asString(value) {
|
|
66
|
+
return typeof value === "string" ? value : null;
|
|
67
|
+
}
|
|
68
|
+
function projectMatchesWorkspace(project, workspace) {
|
|
69
|
+
const requested = workspace.trim().toLowerCase();
|
|
70
|
+
if (!requested)
|
|
71
|
+
return true;
|
|
72
|
+
return [
|
|
73
|
+
project.workspace_name,
|
|
74
|
+
project.workspace_slug,
|
|
75
|
+
project.workspace_tenant_id,
|
|
76
|
+
].some((value) => typeof value === "string" && value.toLowerCase() === requested);
|
|
77
|
+
}
|
|
78
|
+
export class BmClient {
|
|
79
|
+
bmPath;
|
|
80
|
+
project;
|
|
81
|
+
cwd;
|
|
82
|
+
env;
|
|
83
|
+
shouldRun = false;
|
|
84
|
+
client = null;
|
|
85
|
+
transport = null;
|
|
86
|
+
connectPromise = null;
|
|
87
|
+
retryDelaysMs = [...DEFAULT_RETRY_DELAYS_MS];
|
|
88
|
+
constructor(bmPath, project) {
|
|
89
|
+
this.bmPath = bmPath;
|
|
90
|
+
this.project = project;
|
|
91
|
+
}
|
|
92
|
+
async start(options) {
|
|
93
|
+
this.shouldRun = true;
|
|
94
|
+
if (options?.cwd) {
|
|
95
|
+
this.cwd = options.cwd;
|
|
96
|
+
}
|
|
97
|
+
if (options?.env) {
|
|
98
|
+
this.env = options.env;
|
|
99
|
+
}
|
|
100
|
+
await this.connectWithRetries();
|
|
101
|
+
}
|
|
102
|
+
async stop() {
|
|
103
|
+
this.shouldRun = false;
|
|
104
|
+
await this.disconnectCurrent(this.client, this.transport);
|
|
105
|
+
this.client = null;
|
|
106
|
+
this.transport = null;
|
|
107
|
+
}
|
|
108
|
+
async connectWithRetries() {
|
|
109
|
+
let lastErr;
|
|
110
|
+
for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
await this.ensureConnected();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
lastErr = err;
|
|
117
|
+
await this.disconnectCurrent(this.client, this.transport);
|
|
118
|
+
this.client = null;
|
|
119
|
+
this.transport = null;
|
|
120
|
+
if (attempt === this.retryDelaysMs.length) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const waitMs = this.retryDelaysMs[attempt];
|
|
124
|
+
log.warn(`BM MCP connect failed (attempt ${attempt + 1}/${this.retryDelaysMs.length + 1}): ${getErrorMessage(err)}; retrying in ${waitMs}ms`);
|
|
125
|
+
await delay(waitMs);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`BM MCP unavailable: ${getErrorMessage(lastErr)}`);
|
|
129
|
+
}
|
|
130
|
+
async ensureConnected() {
|
|
131
|
+
if (!this.shouldRun) {
|
|
132
|
+
this.shouldRun = true;
|
|
133
|
+
}
|
|
134
|
+
if (this.client && this.transport) {
|
|
135
|
+
return this.client;
|
|
136
|
+
}
|
|
137
|
+
if (!this.connectPromise) {
|
|
138
|
+
this.connectPromise = this.connectFresh();
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await this.connectPromise;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.connectPromise = null;
|
|
145
|
+
}
|
|
146
|
+
if (!this.client) {
|
|
147
|
+
throw new Error("BM MCP client was not initialized");
|
|
148
|
+
}
|
|
149
|
+
return this.client;
|
|
150
|
+
}
|
|
151
|
+
async connectFresh() {
|
|
152
|
+
const transport = new StdioClientTransport({
|
|
153
|
+
command: this.bmPath,
|
|
154
|
+
args: ["mcp", "--transport", "stdio"],
|
|
155
|
+
cwd: this.cwd,
|
|
156
|
+
env: this.env,
|
|
157
|
+
stderr: "pipe",
|
|
158
|
+
});
|
|
159
|
+
const client = new Client({
|
|
160
|
+
name: "openclaw-basic-memory",
|
|
161
|
+
version: "0.1.0",
|
|
162
|
+
}, { capabilities: {} });
|
|
163
|
+
const stderr = transport.stderr;
|
|
164
|
+
if (stderr) {
|
|
165
|
+
stderr.on("data", (data) => {
|
|
166
|
+
const msg = data.toString().trim();
|
|
167
|
+
if (msg.length > 0) {
|
|
168
|
+
log.debug(`[bm mcp] ${msg}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
transport.onclose = () => {
|
|
173
|
+
if (this.transport !== transport)
|
|
174
|
+
return;
|
|
175
|
+
log.warn("BM MCP stdio session closed");
|
|
176
|
+
this.client = null;
|
|
177
|
+
this.transport = null;
|
|
178
|
+
};
|
|
179
|
+
transport.onerror = (err) => {
|
|
180
|
+
if (this.transport !== transport)
|
|
181
|
+
return;
|
|
182
|
+
log.warn(`BM MCP transport error: ${getErrorMessage(err)}`);
|
|
183
|
+
};
|
|
184
|
+
this.client = client;
|
|
185
|
+
this.transport = transport;
|
|
186
|
+
try {
|
|
187
|
+
await client.connect(transport);
|
|
188
|
+
const tools = await client.listTools();
|
|
189
|
+
this.assertRequiredTools(tools.tools.map((tool) => tool.name));
|
|
190
|
+
log.info(`connected to BM MCP stdio (project=${this.project}, pid=${transport.pid ?? "unknown"})`);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
await this.disconnectCurrent(client, transport);
|
|
194
|
+
if (this.client === client) {
|
|
195
|
+
this.client = null;
|
|
196
|
+
}
|
|
197
|
+
if (this.transport === transport) {
|
|
198
|
+
this.transport = null;
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`failed to start BM MCP stdio: ${getErrorMessage(err)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
assertRequiredTools(toolNames) {
|
|
204
|
+
const available = new Set(toolNames);
|
|
205
|
+
const missing = REQUIRED_TOOLS.filter((name) => !available.has(name));
|
|
206
|
+
if (missing.length > 0) {
|
|
207
|
+
throw new Error(`BM MCP server missing required tools: ${missing.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async disconnectCurrent(client, transport) {
|
|
211
|
+
if (client) {
|
|
212
|
+
try {
|
|
213
|
+
await client.close();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// ignore shutdown errors
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (transport) {
|
|
220
|
+
try {
|
|
221
|
+
await transport.close();
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// ignore shutdown errors
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async callToolRaw(name, args) {
|
|
229
|
+
let lastErr;
|
|
230
|
+
for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt++) {
|
|
231
|
+
try {
|
|
232
|
+
const client = await this.ensureConnected();
|
|
233
|
+
const result = await client.callTool({
|
|
234
|
+
name,
|
|
235
|
+
arguments: args,
|
|
236
|
+
});
|
|
237
|
+
if (isRecord(result) && result.isError === true) {
|
|
238
|
+
const message = extractTextFromContent(result.content);
|
|
239
|
+
throw new Error(`BM MCP tool ${name} failed${message ? `: ${message}` : ""}`);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
if (!isRecoverableConnectionError(err)) {
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
lastErr = err;
|
|
248
|
+
await this.disconnectCurrent(this.client, this.transport);
|
|
249
|
+
this.client = null;
|
|
250
|
+
this.transport = null;
|
|
251
|
+
if (attempt === this.retryDelaysMs.length) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
const waitMs = this.retryDelaysMs[attempt];
|
|
255
|
+
log.warn(`BM MCP call ${name} failed (attempt ${attempt + 1}/${this.retryDelaysMs.length + 1}): ${getErrorMessage(err)}; retrying in ${waitMs}ms`);
|
|
256
|
+
await delay(waitMs);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`BM MCP unavailable: ${getErrorMessage(lastErr)}`);
|
|
260
|
+
}
|
|
261
|
+
async callTool(name, args) {
|
|
262
|
+
const result = await this.callToolRaw(name, args);
|
|
263
|
+
if (!isRecord(result) || result.structuredContent === undefined) {
|
|
264
|
+
throw new Error(`BM MCP tool ${name} returned no structured payload`);
|
|
265
|
+
}
|
|
266
|
+
const structuredPayload = result.structuredContent;
|
|
267
|
+
if (isRecord(structuredPayload) && structuredPayload.result !== undefined) {
|
|
268
|
+
return structuredPayload.result;
|
|
269
|
+
}
|
|
270
|
+
return structuredPayload;
|
|
271
|
+
}
|
|
272
|
+
routedProject(project) {
|
|
273
|
+
return project ?? this.project;
|
|
274
|
+
}
|
|
275
|
+
async ensureProject(projectPath) {
|
|
276
|
+
const payload = await this.callTool("create_memory_project", {
|
|
277
|
+
project_name: this.project,
|
|
278
|
+
project_path: projectPath,
|
|
279
|
+
set_default: true,
|
|
280
|
+
output_format: "json",
|
|
281
|
+
});
|
|
282
|
+
if (!isRecord(payload)) {
|
|
283
|
+
throw new Error("invalid create_memory_project response");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async listWorkspaces() {
|
|
287
|
+
const payload = await this.callTool("list_workspaces", {
|
|
288
|
+
output_format: "json",
|
|
289
|
+
});
|
|
290
|
+
if (isRecord(payload) && Array.isArray(payload.workspaces)) {
|
|
291
|
+
return payload.workspaces;
|
|
292
|
+
}
|
|
293
|
+
throw new Error("invalid list_workspaces response");
|
|
294
|
+
}
|
|
295
|
+
async listProjects(workspace) {
|
|
296
|
+
const payload = await this.callTool("list_memory_projects", {
|
|
297
|
+
output_format: "json",
|
|
298
|
+
});
|
|
299
|
+
if (isRecord(payload) && Array.isArray(payload.projects)) {
|
|
300
|
+
const projects = payload.projects;
|
|
301
|
+
if (workspace) {
|
|
302
|
+
return projects.filter((project) => projectMatchesWorkspace(project, workspace));
|
|
303
|
+
}
|
|
304
|
+
return projects;
|
|
305
|
+
}
|
|
306
|
+
throw new Error("invalid list_memory_projects response");
|
|
307
|
+
}
|
|
308
|
+
async search(query, limit = 10, project, metadata) {
|
|
309
|
+
const args = {
|
|
310
|
+
page: 1,
|
|
311
|
+
page_size: limit,
|
|
312
|
+
output_format: "json",
|
|
313
|
+
project: this.routedProject(project),
|
|
314
|
+
};
|
|
315
|
+
if (query)
|
|
316
|
+
args.query = query;
|
|
317
|
+
if (metadata?.filters)
|
|
318
|
+
args.metadata_filters = metadata.filters;
|
|
319
|
+
if (metadata?.tags)
|
|
320
|
+
args.tags = metadata.tags;
|
|
321
|
+
if (metadata?.status)
|
|
322
|
+
args.status = metadata.status;
|
|
323
|
+
if (metadata?.note_types)
|
|
324
|
+
args.note_types = metadata.note_types;
|
|
325
|
+
if (metadata?.entity_types)
|
|
326
|
+
args.entity_types = metadata.entity_types;
|
|
327
|
+
const payload = await this.callTool("search_notes", args);
|
|
328
|
+
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
329
|
+
throw new Error("invalid search_notes response");
|
|
330
|
+
}
|
|
331
|
+
return payload.results;
|
|
332
|
+
}
|
|
333
|
+
async readNote(identifier, options = {}, project) {
|
|
334
|
+
const args = {
|
|
335
|
+
identifier,
|
|
336
|
+
include_frontmatter: options.includeFrontmatter === true,
|
|
337
|
+
output_format: "json",
|
|
338
|
+
project: this.routedProject(project),
|
|
339
|
+
};
|
|
340
|
+
const payload = await this.callTool("read_note", args);
|
|
341
|
+
if (!isRecord(payload)) {
|
|
342
|
+
throw new Error("invalid read_note response");
|
|
343
|
+
}
|
|
344
|
+
const title = asString(payload.title);
|
|
345
|
+
const permalink = asString(payload.permalink);
|
|
346
|
+
const content = asString(payload.content);
|
|
347
|
+
const filePath = asString(payload.file_path);
|
|
348
|
+
if (!title || !permalink || content === null || !filePath) {
|
|
349
|
+
throw new Error("invalid read_note payload");
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
title,
|
|
353
|
+
permalink,
|
|
354
|
+
content,
|
|
355
|
+
file_path: filePath,
|
|
356
|
+
frontmatter: isRecord(payload.frontmatter) ? payload.frontmatter : null,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async writeNote(title, content, folder, project, overwrite) {
|
|
360
|
+
const args = {
|
|
361
|
+
title,
|
|
362
|
+
content,
|
|
363
|
+
directory: folder,
|
|
364
|
+
output_format: "json",
|
|
365
|
+
project: this.routedProject(project),
|
|
366
|
+
};
|
|
367
|
+
if (overwrite !== undefined)
|
|
368
|
+
args.overwrite = overwrite;
|
|
369
|
+
const payload = await this.callTool("write_note", args);
|
|
370
|
+
if (!isRecord(payload)) {
|
|
371
|
+
throw new Error("invalid write_note response");
|
|
372
|
+
}
|
|
373
|
+
if (payload.error === "NOTE_ALREADY_EXISTS") {
|
|
374
|
+
throw new NoteAlreadyExistsError(asString(payload.title) ?? title, asString(payload.permalink) ?? "");
|
|
375
|
+
}
|
|
376
|
+
const resultTitle = asString(payload.title);
|
|
377
|
+
const permalink = asString(payload.permalink);
|
|
378
|
+
const filePath = asString(payload.file_path);
|
|
379
|
+
if (!resultTitle || !permalink || !filePath) {
|
|
380
|
+
throw new Error("invalid write_note payload");
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
title: resultTitle,
|
|
384
|
+
permalink,
|
|
385
|
+
content,
|
|
386
|
+
file_path: filePath,
|
|
387
|
+
checksum: asString(payload.checksum),
|
|
388
|
+
action: payload.action === "created" || payload.action === "updated"
|
|
389
|
+
? payload.action
|
|
390
|
+
: undefined,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async buildContext(url, depth = 1, project) {
|
|
394
|
+
const args = {
|
|
395
|
+
url,
|
|
396
|
+
depth,
|
|
397
|
+
output_format: "json",
|
|
398
|
+
project: this.routedProject(project),
|
|
399
|
+
};
|
|
400
|
+
const payload = await this.callTool("build_context", args);
|
|
401
|
+
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
402
|
+
throw new Error("invalid build_context response");
|
|
403
|
+
}
|
|
404
|
+
return payload;
|
|
405
|
+
}
|
|
406
|
+
async recentActivity(timeframe = "24h", project) {
|
|
407
|
+
const args = {
|
|
408
|
+
timeframe,
|
|
409
|
+
output_format: "json",
|
|
410
|
+
project: this.routedProject(project),
|
|
411
|
+
};
|
|
412
|
+
const payload = await this.callTool("recent_activity", args);
|
|
413
|
+
if (Array.isArray(payload)) {
|
|
414
|
+
return payload;
|
|
415
|
+
}
|
|
416
|
+
throw new Error("invalid recent_activity response");
|
|
417
|
+
}
|
|
418
|
+
async editNote(identifier, operation, content, options = {}, project) {
|
|
419
|
+
const args = {
|
|
420
|
+
identifier,
|
|
421
|
+
operation,
|
|
422
|
+
content,
|
|
423
|
+
output_format: "json",
|
|
424
|
+
project: this.routedProject(project),
|
|
425
|
+
};
|
|
426
|
+
if (options.find_text)
|
|
427
|
+
args.find_text = options.find_text;
|
|
428
|
+
if (options.section)
|
|
429
|
+
args.section = options.section;
|
|
430
|
+
if (options.expected_replacements != null)
|
|
431
|
+
args.expected_replacements = options.expected_replacements;
|
|
432
|
+
const payload = await this.callTool("edit_note", args);
|
|
433
|
+
if (!isRecord(payload)) {
|
|
434
|
+
throw new Error("invalid edit_note response");
|
|
435
|
+
}
|
|
436
|
+
const title = asString(payload.title);
|
|
437
|
+
const permalink = asString(payload.permalink);
|
|
438
|
+
const filePath = asString(payload.file_path);
|
|
439
|
+
if (!title || !permalink || !filePath) {
|
|
440
|
+
throw new Error("invalid edit_note payload");
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
title,
|
|
444
|
+
permalink,
|
|
445
|
+
file_path: filePath,
|
|
446
|
+
operation,
|
|
447
|
+
checksum: asString(payload.checksum),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async deleteNote(identifier, project) {
|
|
451
|
+
const args = {
|
|
452
|
+
identifier,
|
|
453
|
+
output_format: "json",
|
|
454
|
+
project: this.routedProject(project),
|
|
455
|
+
};
|
|
456
|
+
const payload = await this.callTool("delete_note", args);
|
|
457
|
+
if (!isRecord(payload)) {
|
|
458
|
+
throw new Error("invalid delete_note response");
|
|
459
|
+
}
|
|
460
|
+
if (payload.deleted !== true) {
|
|
461
|
+
throw new Error(`delete_note did not delete "${identifier}"`);
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
title: asString(payload.title) ?? identifier,
|
|
465
|
+
permalink: asString(payload.permalink) ?? identifier,
|
|
466
|
+
file_path: asString(payload.file_path) ?? identifier,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async moveNote(identifier, newFolder, project) {
|
|
470
|
+
const args = {
|
|
471
|
+
identifier,
|
|
472
|
+
destination_folder: newFolder,
|
|
473
|
+
output_format: "json",
|
|
474
|
+
project: this.routedProject(project),
|
|
475
|
+
};
|
|
476
|
+
const payload = await this.callTool("move_note", args);
|
|
477
|
+
if (!isRecord(payload)) {
|
|
478
|
+
throw new Error("invalid move_note response");
|
|
479
|
+
}
|
|
480
|
+
if (payload.moved !== true) {
|
|
481
|
+
throw new Error(asString(payload.error) ??
|
|
482
|
+
`move_note did not move "${identifier}" to "${newFolder}"`);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
title: asString(payload.title) ?? identifier,
|
|
486
|
+
permalink: asString(payload.permalink) ?? identifier,
|
|
487
|
+
content: "",
|
|
488
|
+
file_path: asString(payload.file_path) ?? "",
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async schemaValidate(noteType, identifier, project) {
|
|
492
|
+
const args = {
|
|
493
|
+
output_format: "json",
|
|
494
|
+
project: this.routedProject(project),
|
|
495
|
+
};
|
|
496
|
+
if (noteType)
|
|
497
|
+
args.note_type = noteType;
|
|
498
|
+
if (identifier)
|
|
499
|
+
args.identifier = identifier;
|
|
500
|
+
const payload = await this.callTool("schema_validate", args);
|
|
501
|
+
if (!isRecord(payload)) {
|
|
502
|
+
throw new Error("invalid schema_validate response");
|
|
503
|
+
}
|
|
504
|
+
return payload;
|
|
505
|
+
}
|
|
506
|
+
async schemaInfer(noteType, threshold = 0.25, project) {
|
|
507
|
+
const args = {
|
|
508
|
+
note_type: noteType,
|
|
509
|
+
threshold,
|
|
510
|
+
output_format: "json",
|
|
511
|
+
project: this.routedProject(project),
|
|
512
|
+
};
|
|
513
|
+
const payload = await this.callTool("schema_infer", args);
|
|
514
|
+
if (!isRecord(payload)) {
|
|
515
|
+
throw new Error("invalid schema_infer response");
|
|
516
|
+
}
|
|
517
|
+
return payload;
|
|
518
|
+
}
|
|
519
|
+
async schemaDiff(noteType, project) {
|
|
520
|
+
const args = {
|
|
521
|
+
note_type: noteType,
|
|
522
|
+
output_format: "json",
|
|
523
|
+
project: this.routedProject(project),
|
|
524
|
+
};
|
|
525
|
+
const payload = await this.callTool("schema_diff", args);
|
|
526
|
+
if (!isRecord(payload)) {
|
|
527
|
+
throw new Error("invalid schema_diff response");
|
|
528
|
+
}
|
|
529
|
+
return payload;
|
|
530
|
+
}
|
|
531
|
+
async indexConversation(userMessage, assistantResponse) {
|
|
532
|
+
const now = new Date();
|
|
533
|
+
const dateStr = now.toISOString().split("T")[0];
|
|
534
|
+
const timeStr = now.toTimeString().slice(0, 5);
|
|
535
|
+
const title = `conversations-${dateStr}`;
|
|
536
|
+
const entry = [
|
|
537
|
+
`### ${timeStr}`,
|
|
538
|
+
"",
|
|
539
|
+
"**User:**",
|
|
540
|
+
userMessage,
|
|
541
|
+
"",
|
|
542
|
+
"**Assistant:**",
|
|
543
|
+
assistantResponse,
|
|
544
|
+
"",
|
|
545
|
+
"---",
|
|
546
|
+
].join("\n");
|
|
547
|
+
try {
|
|
548
|
+
await this.editNote(title, "append", entry);
|
|
549
|
+
log.debug(`appended conversation to: ${title}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
if (!isNoteNotFoundError(err)) {
|
|
554
|
+
log.error(`conversation append failed: ${getErrorMessage(err)}`, err);
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
log.debug(`conversation note missing, will create: ${getErrorMessage(err)}`);
|
|
558
|
+
}
|
|
559
|
+
// Create the note with frontmatter and first entry
|
|
560
|
+
const content = [
|
|
561
|
+
"---",
|
|
562
|
+
`title: Conversations ${dateStr}`,
|
|
563
|
+
"type: Conversation",
|
|
564
|
+
`date: "${dateStr}"`,
|
|
565
|
+
"---",
|
|
566
|
+
"",
|
|
567
|
+
`# Conversations ${dateStr}`,
|
|
568
|
+
"",
|
|
569
|
+
entry,
|
|
570
|
+
].join("\n");
|
|
571
|
+
try {
|
|
572
|
+
await this.writeNote(title, content, "conversations");
|
|
573
|
+
log.debug(`created conversation note: ${title}`);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
log.error("conversation index failed", err);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
getProject() {
|
|
580
|
+
return this.project;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { BmClient } from "../bm-client.ts";
|
|
3
|
+
import type { BasicMemoryConfig } from "../config.ts";
|
|
4
|
+
export declare function registerCli(api: OpenClawPluginApi, client: BmClient, cfg: BasicMemoryConfig): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { log } from "../logger.js";
|
|
2
|
+
export function registerCli(api, client, cfg) {
|
|
3
|
+
api.registerCli(
|
|
4
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
5
|
+
({ program }) => {
|
|
6
|
+
const cmd = program
|
|
7
|
+
.command("basic-memory")
|
|
8
|
+
.description("Basic Memory knowledge graph commands");
|
|
9
|
+
cmd
|
|
10
|
+
.command("search")
|
|
11
|
+
.argument("<query>", "Search query")
|
|
12
|
+
.option("--limit <n>", "Max results", "10")
|
|
13
|
+
.action(async (query, opts) => {
|
|
14
|
+
const limit = Number.parseInt(opts.limit, 10) || 10;
|
|
15
|
+
log.debug(`cli search: query="${query}" limit=${limit}`);
|
|
16
|
+
const results = await client.search(query, limit);
|
|
17
|
+
if (results.length === 0) {
|
|
18
|
+
console.log("No notes found.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const r of results) {
|
|
22
|
+
const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : "";
|
|
23
|
+
console.log(`- ${r.title}${score}`);
|
|
24
|
+
if (r.content) {
|
|
25
|
+
const preview = r.content.length > 100
|
|
26
|
+
? `${r.content.slice(0, 100)}...`
|
|
27
|
+
: r.content;
|
|
28
|
+
console.log(` ${preview}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
cmd
|
|
33
|
+
.command("read")
|
|
34
|
+
.argument("<identifier>", "Note title, permalink, or memory:// URL")
|
|
35
|
+
.option("--raw", "Return raw markdown including frontmatter", false)
|
|
36
|
+
.action(async (identifier, opts) => {
|
|
37
|
+
log.debug(`cli read: identifier="${identifier}"`);
|
|
38
|
+
const note = await client.readNote(identifier, {
|
|
39
|
+
includeFrontmatter: opts.raw === true,
|
|
40
|
+
});
|
|
41
|
+
console.log(`# ${note.title}`);
|
|
42
|
+
console.log(`permalink: ${note.permalink}`);
|
|
43
|
+
console.log(`file: ${note.file_path}`);
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log(note.content);
|
|
46
|
+
});
|
|
47
|
+
cmd
|
|
48
|
+
.command("edit")
|
|
49
|
+
.argument("<identifier>", "Note title, permalink, or memory:// URL")
|
|
50
|
+
.requiredOption("--operation <operation>", "Edit operation: append|prepend|find_replace|replace_section")
|
|
51
|
+
.requiredOption("--content <content>", "Edit content")
|
|
52
|
+
.option("--find-text <text>", "Text to find for find_replace")
|
|
53
|
+
.option("--section <heading>", "Section heading for replace_section")
|
|
54
|
+
.option("--expected-replacements <n>", "Expected replacement count for find_replace", "1")
|
|
55
|
+
.action(async (identifier, opts) => {
|
|
56
|
+
const expectedReplacements = Number.parseInt(opts.expectedReplacements, 10) || 1;
|
|
57
|
+
log.debug(`cli edit: identifier="${identifier}" op=${opts.operation} expected_replacements=${expectedReplacements}`);
|
|
58
|
+
const result = await client.editNote(identifier, opts.operation, opts.content, {
|
|
59
|
+
find_text: opts.findText,
|
|
60
|
+
section: opts.section,
|
|
61
|
+
expected_replacements: expectedReplacements,
|
|
62
|
+
});
|
|
63
|
+
console.log(`Edited: ${result.title}`);
|
|
64
|
+
console.log(`permalink: ${result.permalink}`);
|
|
65
|
+
console.log(`file: ${result.file_path}`);
|
|
66
|
+
console.log(`operation: ${result.operation}`);
|
|
67
|
+
if (result.checksum) {
|
|
68
|
+
console.log(`checksum: ${result.checksum}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
cmd
|
|
72
|
+
.command("context")
|
|
73
|
+
.argument("<url>", "Memory URL to navigate")
|
|
74
|
+
.option("--depth <n>", "Relation hops to follow", "1")
|
|
75
|
+
.action(async (url, opts) => {
|
|
76
|
+
const depth = Number.parseInt(opts.depth, 10) || 1;
|
|
77
|
+
log.debug(`cli context: url="${url}" depth=${depth}`);
|
|
78
|
+
const ctx = await client.buildContext(url, depth);
|
|
79
|
+
if (!ctx.results || ctx.results.length === 0) {
|
|
80
|
+
console.log(`No context found for "${url}".`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const result of ctx.results) {
|
|
84
|
+
console.log(`## ${result.primary_result.title}`);
|
|
85
|
+
console.log(result.primary_result.content);
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
cmd
|
|
90
|
+
.command("recent")
|
|
91
|
+
.option("--timeframe <t>", "Timeframe (e.g. 24h, 7d)", "24h")
|
|
92
|
+
.action(async (opts) => {
|
|
93
|
+
log.debug(`cli recent: timeframe="${opts.timeframe}"`);
|
|
94
|
+
const results = await client.recentActivity(opts.timeframe);
|
|
95
|
+
if (results.length === 0) {
|
|
96
|
+
console.log("No recent activity.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const r of results) {
|
|
100
|
+
console.log(`- ${r.title} (${r.permalink})`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
cmd
|
|
104
|
+
.command("status")
|
|
105
|
+
.description("Show plugin status")
|
|
106
|
+
.action(() => {
|
|
107
|
+
console.log(`Project: ${cfg.project}`);
|
|
108
|
+
console.log(`Project path: ${cfg.projectPath}`);
|
|
109
|
+
console.log(`BM CLI: ${cfg.bmPath}`);
|
|
110
|
+
console.log(`Memory dir: ${cfg.memoryDir}`);
|
|
111
|
+
console.log(`Memory file: ${cfg.memoryFile}`);
|
|
112
|
+
console.log(`Auto-capture: ${cfg.autoCapture}`);
|
|
113
|
+
console.log(`Debug: ${cfg.debug}`);
|
|
114
|
+
});
|
|
115
|
+
}, { commands: ["basic-memory"] });
|
|
116
|
+
}
|