@basicmemory/openclaw-basic-memory 0.24.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -12
- package/bm-client.ts +302 -112
- package/commands/cli.ts +5 -6
- package/commands/slash.ts +4 -4
- package/context-engine/basic-memory-context-engine.ts +74 -9
- package/context-engine/types.ts +21 -0
- package/dist/bm-client.d.ts +61 -12
- package/dist/bm-client.js +250 -101
- package/dist/commands/cli.d.ts +1 -1
- package/dist/commands/cli.js +5 -5
- package/dist/commands/slash.d.ts +1 -1
- package/dist/commands/slash.js +4 -3
- package/dist/context-engine/basic-memory-context-engine.d.ts +20 -1
- package/dist/context-engine/basic-memory-context-engine.js +50 -2
- package/dist/context-engine/types.d.ts +9 -0
- package/dist/context-engine/types.js +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +79 -69
- package/dist/memory/basic-memory-capability.d.ts +19 -0
- package/dist/memory/basic-memory-capability.js +190 -0
- package/dist/tools/build-context.d.ts +1 -1
- package/dist/tools/build-context.js +12 -4
- package/dist/tools/delete-note.d.ts +1 -1
- package/dist/tools/delete-note.js +2 -1
- package/dist/tools/edit-note.d.ts +1 -1
- package/dist/tools/edit-note.js +22 -12
- package/dist/tools/list-memory-projects.d.ts +1 -1
- package/dist/tools/list-memory-projects.js +2 -1
- package/dist/tools/list-workspaces.d.ts +1 -1
- package/dist/tools/list-workspaces.js +4 -4
- package/dist/tools/move-note.d.ts +1 -1
- package/dist/tools/move-note.js +2 -1
- package/dist/tools/read-note.d.ts +1 -1
- package/dist/tools/read-note.js +2 -1
- package/dist/tools/schema-diff.d.ts +1 -1
- package/dist/tools/schema-diff.js +2 -1
- package/dist/tools/schema-infer.d.ts +1 -1
- package/dist/tools/schema-infer.js +2 -1
- package/dist/tools/schema-validate.d.ts +1 -1
- package/dist/tools/schema-validate.js +2 -1
- package/dist/tools/search-notes.d.ts +1 -1
- package/dist/tools/search-notes.js +13 -6
- package/dist/tools/write-note.d.ts +1 -1
- package/dist/tools/write-note.js +11 -12
- package/index.ts +93 -84
- package/memory/basic-memory-capability.ts +253 -0
- package/openclaw.plugin.json +0 -2
- package/package.json +6 -5
- package/skills/manifest.json +5 -0
- package/skills/memory-agents/SKILL.md +23 -10
- package/skills/memory-capture/SKILL.md +29 -18
- package/skills/memory-comark/SKILL.md +17 -6
- package/skills/memory-continue/SKILL.md +47 -27
- package/skills/memory-curate/SKILL.md +40 -24
- package/skills/memory-defrag/SKILL.md +18 -7
- package/skills/memory-doctor/SKILL.md +93 -0
- package/skills/memory-ingest/SKILL.md +33 -17
- package/skills/memory-lifecycle/SKILL.md +35 -22
- package/skills/memory-literary-analysis/SKILL.md +112 -75
- package/skills/memory-metadata-search/SKILL.md +39 -28
- package/skills/memory-notes/SKILL.md +75 -50
- package/skills/memory-okf/SKILL.md +27 -15
- package/skills/memory-onboarding/SKILL.md +20 -9
- package/skills/memory-onboarding/references/assistant-setup.md +1 -1
- package/skills/memory-onboarding/references/conventions.md +4 -4
- package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
- package/skills/memory-onboarding/references/schema-guide.md +9 -8
- package/skills/memory-quest/SKILL.md +18 -7
- package/skills/memory-reflect/SKILL.md +25 -13
- package/skills/memory-research/SKILL.md +28 -13
- package/skills/memory-schema/SKILL.md +36 -24
- package/skills/memory-tasks/SKILL.md +34 -22
- package/tools/build-context.ts +14 -5
- package/tools/delete-note.ts +2 -2
- package/tools/edit-note.ts +27 -14
- package/tools/list-memory-projects.ts +6 -2
- package/tools/list-workspaces.ts +8 -5
- package/tools/move-note.ts +2 -2
- package/tools/read-note.ts +2 -2
- package/tools/schema-diff.ts +2 -2
- package/tools/schema-infer.ts +2 -2
- package/tools/schema-validate.ts +2 -2
- package/tools/search-notes.ts +18 -7
- package/tools/write-note.ts +12 -12
- package/dist/tools/memory-provider.d.ts +0 -15
- package/dist/tools/memory-provider.js +0 -275
- package/tools/memory-provider.ts +0 -336
package/dist/bm-client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
3
4
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -5,22 +6,23 @@ import { log } from "./logger.js";
|
|
|
5
6
|
const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000];
|
|
6
7
|
export class NoteAlreadyExistsError extends Error {
|
|
7
8
|
permalink;
|
|
8
|
-
constructor(title, permalink) {
|
|
9
|
-
super(`Note already exists: "${title}" (${permalink})`);
|
|
9
|
+
constructor(title, permalink, message) {
|
|
10
|
+
super(message ?? `Note already exists: "${title}" (${permalink})`);
|
|
10
11
|
this.name = "NoteAlreadyExistsError";
|
|
11
12
|
this.permalink = permalink;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* bm_write refused a conditional overwrite: the note is no longer the
|
|
16
17
|
* revision named by expected_checksum. Nothing was written; currentChecksum is
|
|
17
|
-
*
|
|
18
|
+
* available revision when supplied by a server. A null value means unknown;
|
|
19
|
+
* 1.0 puts revision details in the refusal message, not a checksum field.
|
|
18
20
|
*/
|
|
19
21
|
export class NoteRevisionConflictError extends Error {
|
|
20
22
|
permalink;
|
|
21
23
|
currentChecksum;
|
|
22
|
-
constructor(title, permalink, currentChecksum) {
|
|
23
|
-
super(`Note revision conflict: "${title}" changed since it was read`);
|
|
24
|
+
constructor(title, permalink, currentChecksum, message) {
|
|
25
|
+
super(message ?? `Note revision conflict: "${title}" changed since it was read`);
|
|
24
26
|
this.name = "NoteRevisionConflictError";
|
|
25
27
|
this.permalink = permalink;
|
|
26
28
|
this.currentChecksum = currentChecksum;
|
|
@@ -38,21 +40,24 @@ export class BmToolError extends Error {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
const REQUIRED_TOOLS = [
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"delete_note",
|
|
51
|
-
"move_note",
|
|
52
|
-
"schema_validate",
|
|
53
|
-
"schema_infer",
|
|
54
|
-
"schema_diff",
|
|
43
|
+
"bm_search",
|
|
44
|
+
"bm_read",
|
|
45
|
+
"bm_write",
|
|
46
|
+
"bm_edit",
|
|
47
|
+
"bm_context",
|
|
48
|
+
"bm_project",
|
|
49
|
+
"bm_delete",
|
|
50
|
+
"bm_move",
|
|
51
|
+
"bm_schema",
|
|
55
52
|
];
|
|
53
|
+
/**
|
|
54
|
+
* The text to preview for a search hit. `content` is the start of the note,
|
|
55
|
+
* so for a long note (a day of captured conversation) the matching part can
|
|
56
|
+
* sit far below any preview cut-off; the matched chunk is the part that hit.
|
|
57
|
+
*/
|
|
58
|
+
export function searchResultText(result) {
|
|
59
|
+
return result.matched_chunk?.trim() || result.content || "";
|
|
60
|
+
}
|
|
56
61
|
function getErrorMessage(err) {
|
|
57
62
|
return err instanceof Error ? err.message : String(err);
|
|
58
63
|
}
|
|
@@ -80,19 +85,24 @@ function isRecoverableConnectionError(err) {
|
|
|
80
85
|
msg.includes("failed to start bm mcp stdio") ||
|
|
81
86
|
msg.includes("client is closed"));
|
|
82
87
|
}
|
|
83
|
-
function isNoteNotFoundError(err) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
export function isNoteNotFoundError(err) {
|
|
89
|
+
return (err instanceof BmToolError &&
|
|
90
|
+
parseRefusalPayload(err.toolMessage)?.error === "NOT_FOUND");
|
|
91
|
+
}
|
|
92
|
+
function conversationDate(date) {
|
|
93
|
+
return date.toISOString().split("T")[0];
|
|
94
|
+
}
|
|
95
|
+
// Host advancement keys are opaque and may contain characters that would end
|
|
96
|
+
// an HTML comment, so the note stores a fixed-length digest instead.
|
|
97
|
+
function conversationTurnMarker(turnKey) {
|
|
98
|
+
const digest = createHash("sha256").update(turnKey).digest("hex").slice(0, 32);
|
|
99
|
+
return `<!-- openclaw-turn:${digest} -->`;
|
|
90
100
|
}
|
|
91
101
|
function asString(value) {
|
|
92
102
|
return typeof value === "string" ? value : null;
|
|
93
103
|
}
|
|
94
104
|
// In JSON mode BM reports a refused write as a tool error whose text is the
|
|
95
|
-
// refusal payload itself ({"error": "
|
|
105
|
+
// refusal payload itself ({"error": "ALREADY_EXISTS", ...}). Any other
|
|
96
106
|
// error text (plain-text failures, older servers) is not a refusal payload.
|
|
97
107
|
function parseRefusalPayload(text) {
|
|
98
108
|
let parsed;
|
|
@@ -104,6 +114,24 @@ function parseRefusalPayload(text) {
|
|
|
104
114
|
}
|
|
105
115
|
return isRecord(parsed) && typeof parsed.error === "string" ? parsed : null;
|
|
106
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The server's own explanation of a failed tool call, for showing to an agent.
|
|
119
|
+
* A refusal payload becomes "CODE: message"; other tool text passes through.
|
|
120
|
+
*/
|
|
121
|
+
export function describeToolError(err) {
|
|
122
|
+
if (!(err instanceof BmToolError))
|
|
123
|
+
return getErrorMessage(err);
|
|
124
|
+
const payload = parseRefusalPayload(err.toolMessage);
|
|
125
|
+
if (!payload)
|
|
126
|
+
return err.toolMessage || err.message;
|
|
127
|
+
const message = asString(payload.message);
|
|
128
|
+
return message ? `${payload.error}: ${message}` : String(payload.error);
|
|
129
|
+
}
|
|
130
|
+
/** describeToolError, but saying plainly when the note does not exist. */
|
|
131
|
+
export function describeNoteError(err) {
|
|
132
|
+
const reason = describeToolError(err);
|
|
133
|
+
return isNoteNotFoundError(err) ? `it does not exist. ${reason}` : reason;
|
|
134
|
+
}
|
|
107
135
|
function projectMatchesWorkspace(project, workspace) {
|
|
108
136
|
const requested = workspace.trim().toLowerCase();
|
|
109
137
|
if (!requested)
|
|
@@ -120,26 +148,93 @@ export class BmClient {
|
|
|
120
148
|
cwd;
|
|
121
149
|
env;
|
|
122
150
|
shouldRun = false;
|
|
151
|
+
// True only between start() and stop(), i.e. while the plugin service owns
|
|
152
|
+
// the BM MCP session. A lazy connection opened by a tool or memory call in a
|
|
153
|
+
// one-shot CLI process leaves this false.
|
|
154
|
+
serviceRunning = false;
|
|
155
|
+
// Connect plus project creation, started by start(). Tool calls wait for
|
|
156
|
+
// it so none can reach a project that does not exist yet. Null when the
|
|
157
|
+
// client was never started (tests, one-shot CLI readers).
|
|
158
|
+
startup = null;
|
|
159
|
+
startupRetry = null;
|
|
160
|
+
projectPath;
|
|
161
|
+
prepare;
|
|
123
162
|
client = null;
|
|
124
163
|
transport = null;
|
|
125
164
|
connectPromise = null;
|
|
126
165
|
retryDelaysMs = [...DEFAULT_RETRY_DELAYS_MS];
|
|
166
|
+
// Conversation captures run one at a time so a turn-key duplicate check
|
|
167
|
+
// and the append that follows it cannot interleave with another capture.
|
|
168
|
+
conversationWrites = Promise.resolve();
|
|
127
169
|
constructor(bmPath, project) {
|
|
128
170
|
this.bmPath = bmPath;
|
|
129
171
|
this.project = project;
|
|
130
172
|
}
|
|
131
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Connect to BM and create the plugin's project.
|
|
175
|
+
*
|
|
176
|
+
* Returns the startup promise; callers that must not block (the plugin
|
|
177
|
+
* service, which OpenClaw gives only a few seconds) can leave it running.
|
|
178
|
+
* Tool calls made meanwhile wait for it. If it fails, the next tool call
|
|
179
|
+
* runs it again, so a transient failure does not leave the plugin dead.
|
|
180
|
+
*/
|
|
181
|
+
start(options) {
|
|
132
182
|
this.shouldRun = true;
|
|
183
|
+
this.serviceRunning = true;
|
|
133
184
|
if (options?.cwd) {
|
|
134
185
|
this.cwd = options.cwd;
|
|
135
186
|
}
|
|
136
187
|
if (options?.env) {
|
|
137
188
|
this.env = options.env;
|
|
138
189
|
}
|
|
190
|
+
if (options?.projectPath) {
|
|
191
|
+
this.projectPath = options.projectPath;
|
|
192
|
+
}
|
|
193
|
+
if (options?.prepare) {
|
|
194
|
+
this.prepare = options.prepare;
|
|
195
|
+
}
|
|
196
|
+
this.startup = this.runStartup();
|
|
197
|
+
return this.startup;
|
|
198
|
+
}
|
|
199
|
+
async runStartup() {
|
|
200
|
+
await this.prepare?.();
|
|
139
201
|
await this.connectWithRetries();
|
|
202
|
+
if (this.projectPath)
|
|
203
|
+
await this.createProject(this.projectPath);
|
|
204
|
+
}
|
|
205
|
+
async waitForStartup() {
|
|
206
|
+
if (!this.startup)
|
|
207
|
+
return;
|
|
208
|
+
try {
|
|
209
|
+
await this.startup;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// Trigger: the startup this call was waiting on failed.
|
|
213
|
+
// Why: the service no longer fails on a slow or failed start, so the
|
|
214
|
+
// first tool call afterwards is the place to recover.
|
|
215
|
+
// Outcome: one fresh attempt, shared by concurrent callers; its error
|
|
216
|
+
// reaches the tool if it fails too.
|
|
217
|
+
if (!this.serviceRunning)
|
|
218
|
+
throw err;
|
|
219
|
+
if (this.startupRetry === null) {
|
|
220
|
+
this.startupRetry = this.runStartup().finally(() => {
|
|
221
|
+
this.startupRetry = null;
|
|
222
|
+
});
|
|
223
|
+
this.startup = this.startupRetry;
|
|
224
|
+
}
|
|
225
|
+
await this.startupRetry;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
isServiceRunning() {
|
|
229
|
+
return this.serviceRunning;
|
|
140
230
|
}
|
|
141
231
|
async stop() {
|
|
142
232
|
this.shouldRun = false;
|
|
233
|
+
this.serviceRunning = false;
|
|
234
|
+
// Let an in-flight startup settle first, so it cannot reconnect after
|
|
235
|
+
// the disconnect below.
|
|
236
|
+
await this.startup?.catch(() => undefined);
|
|
237
|
+
this.startup = null;
|
|
143
238
|
await this.disconnectCurrent(this.client, this.transport);
|
|
144
239
|
this.client = null;
|
|
145
240
|
this.transport = null;
|
|
@@ -297,6 +392,11 @@ export class BmClient {
|
|
|
297
392
|
throw new Error(`BM MCP unavailable: ${getErrorMessage(lastErr)}`);
|
|
298
393
|
}
|
|
299
394
|
async callTool(name, args) {
|
|
395
|
+
await this.waitForStartup();
|
|
396
|
+
return this.callStructured(name, args);
|
|
397
|
+
}
|
|
398
|
+
// Ungated: startup itself calls this to create the project.
|
|
399
|
+
async callStructured(name, args) {
|
|
300
400
|
const result = await this.callToolRaw(name, args);
|
|
301
401
|
if (!isRecord(result) || result.structuredContent === undefined) {
|
|
302
402
|
throw new Error(`BM MCP tool ${name} returned no structured payload`);
|
|
@@ -310,36 +410,45 @@ export class BmClient {
|
|
|
310
410
|
routedProject(project) {
|
|
311
411
|
return project ?? this.project;
|
|
312
412
|
}
|
|
313
|
-
//
|
|
314
|
-
// a name (or the plugin's default project) would make routing depend on
|
|
315
|
-
// server-side precedence rather than on what the caller asked for.
|
|
413
|
+
// The 1.0 project parameter accepts either a name or an exact project UUID.
|
|
316
414
|
routing(project, projectId) {
|
|
317
415
|
if (projectId !== undefined)
|
|
318
|
-
return {
|
|
416
|
+
return { project: projectId };
|
|
319
417
|
return { project: this.routedProject(project) };
|
|
320
418
|
}
|
|
321
419
|
async ensureProject(projectPath) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
420
|
+
await this.waitForStartup();
|
|
421
|
+
await this.createProject(projectPath);
|
|
422
|
+
}
|
|
423
|
+
async createProject(projectPath) {
|
|
424
|
+
const payload = await this.callStructured("bm_project", {
|
|
425
|
+
command: "create",
|
|
426
|
+
name: this.project,
|
|
427
|
+
path: projectPath,
|
|
325
428
|
set_default: true,
|
|
326
429
|
output_format: "json",
|
|
327
430
|
});
|
|
328
431
|
if (!isRecord(payload)) {
|
|
329
|
-
throw new Error("invalid
|
|
432
|
+
throw new Error("invalid bm_project response");
|
|
330
433
|
}
|
|
331
434
|
}
|
|
332
435
|
async listWorkspaces() {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
436
|
+
const projects = await this.listProjects();
|
|
437
|
+
const workspaces = new Map();
|
|
438
|
+
for (const project of projects) {
|
|
439
|
+
if (project.workspace_tenant_id && project.workspace_name) {
|
|
440
|
+
workspaces.set(project.workspace_tenant_id, {
|
|
441
|
+
tenant_id: project.workspace_tenant_id,
|
|
442
|
+
name: project.workspace_name,
|
|
443
|
+
workspace_type: project.workspace_type ?? null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
338
446
|
}
|
|
339
|
-
|
|
447
|
+
return [...workspaces.values()];
|
|
340
448
|
}
|
|
341
449
|
async listProjects(workspace) {
|
|
342
|
-
const payload = await this.callTool("
|
|
450
|
+
const payload = await this.callTool("bm_project", {
|
|
451
|
+
command: "list",
|
|
343
452
|
output_format: "json",
|
|
344
453
|
});
|
|
345
454
|
if (isRecord(payload) && Array.isArray(payload.projects)) {
|
|
@@ -349,7 +458,7 @@ export class BmClient {
|
|
|
349
458
|
}
|
|
350
459
|
return projects;
|
|
351
460
|
}
|
|
352
|
-
throw new Error("invalid
|
|
461
|
+
throw new Error("invalid bm_project response");
|
|
353
462
|
}
|
|
354
463
|
async search(query, limit = 10, project, metadata) {
|
|
355
464
|
const args = {
|
|
@@ -360,19 +469,21 @@ export class BmClient {
|
|
|
360
469
|
};
|
|
361
470
|
if (query)
|
|
362
471
|
args.query = query;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (metadata?.tags)
|
|
366
|
-
|
|
367
|
-
if (metadata?.status)
|
|
368
|
-
|
|
472
|
+
const filters = { ...metadata?.filters };
|
|
473
|
+
// Explicit predicates take precedence over convenience shortcuts.
|
|
474
|
+
if (metadata?.tags?.length && !("tags" in filters))
|
|
475
|
+
filters.tags = { $contains: metadata.tags };
|
|
476
|
+
if (metadata?.status && !("status" in filters))
|
|
477
|
+
filters.status = metadata.status;
|
|
478
|
+
if (Object.keys(filters).length)
|
|
479
|
+
args.filters = filters;
|
|
369
480
|
if (metadata?.note_types)
|
|
370
481
|
args.note_types = metadata.note_types;
|
|
371
482
|
if (metadata?.entity_types)
|
|
372
|
-
args.
|
|
373
|
-
const payload = await this.callTool("
|
|
483
|
+
args.items = metadata.entity_types.map((item) => item === "entity" ? "note" : item);
|
|
484
|
+
const payload = await this.callTool("bm_search", args);
|
|
374
485
|
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
375
|
-
throw new Error("invalid
|
|
486
|
+
throw new Error("invalid bm_search response");
|
|
376
487
|
}
|
|
377
488
|
return payload.results;
|
|
378
489
|
}
|
|
@@ -383,16 +494,16 @@ export class BmClient {
|
|
|
383
494
|
output_format: "json",
|
|
384
495
|
...this.routing(project, options.projectId),
|
|
385
496
|
};
|
|
386
|
-
const payload = await this.callTool("
|
|
497
|
+
const payload = await this.callTool("bm_read", args);
|
|
387
498
|
if (!isRecord(payload)) {
|
|
388
|
-
throw new Error("invalid
|
|
499
|
+
throw new Error("invalid bm_read response");
|
|
389
500
|
}
|
|
390
501
|
const title = asString(payload.title);
|
|
391
502
|
const permalink = asString(payload.permalink);
|
|
392
503
|
const content = asString(payload.content);
|
|
393
504
|
const filePath = asString(payload.file_path);
|
|
394
505
|
if (!title || !permalink || content === null || !filePath) {
|
|
395
|
-
throw new Error("invalid
|
|
506
|
+
throw new Error("invalid bm_read payload");
|
|
396
507
|
}
|
|
397
508
|
return {
|
|
398
509
|
title,
|
|
@@ -423,30 +534,26 @@ export class BmClient {
|
|
|
423
534
|
args.expected_checksum = options.expectedChecksum;
|
|
424
535
|
let payload;
|
|
425
536
|
try {
|
|
426
|
-
payload = await this.callTool("
|
|
537
|
+
payload = await this.callTool("bm_write", args);
|
|
427
538
|
}
|
|
428
539
|
catch (err) {
|
|
429
540
|
const refusal = err instanceof BmToolError ? parseRefusalPayload(err.toolMessage) : null;
|
|
430
|
-
if (refusal?.error === "
|
|
431
|
-
throw new NoteAlreadyExistsError(asString(refusal.title) ?? title, asString(refusal.permalink) ?? "");
|
|
541
|
+
if (refusal?.error === "ALREADY_EXISTS") {
|
|
542
|
+
throw new NoteAlreadyExistsError(asString(refusal.title) ?? title, asString(refusal.permalink) ?? "", asString(refusal.message) ?? undefined);
|
|
432
543
|
}
|
|
433
|
-
if (refusal?.error === "
|
|
434
|
-
throw new NoteRevisionConflictError(asString(refusal.title) ?? title, asString(refusal.permalink), asString(refusal.currentChecksum));
|
|
544
|
+
if (refusal?.error === "REVISION_CONFLICT") {
|
|
545
|
+
throw new NoteRevisionConflictError(asString(refusal.title) ?? title, asString(refusal.permalink), asString(refusal.currentChecksum), asString(refusal.message) ?? undefined);
|
|
435
546
|
}
|
|
436
547
|
throw err;
|
|
437
548
|
}
|
|
438
549
|
if (!isRecord(payload)) {
|
|
439
|
-
throw new Error("invalid
|
|
440
|
-
}
|
|
441
|
-
// Older servers returned the refusal as a successful payload.
|
|
442
|
-
if (payload.error === "NOTE_ALREADY_EXISTS") {
|
|
443
|
-
throw new NoteAlreadyExistsError(asString(payload.title) ?? title, asString(payload.permalink) ?? "");
|
|
550
|
+
throw new Error("invalid bm_write response");
|
|
444
551
|
}
|
|
445
552
|
const resultTitle = asString(payload.title);
|
|
446
553
|
const permalink = asString(payload.permalink);
|
|
447
554
|
const filePath = asString(payload.file_path);
|
|
448
555
|
if (!resultTitle || !permalink || !filePath) {
|
|
449
|
-
throw new Error("invalid
|
|
556
|
+
throw new Error("invalid bm_write payload");
|
|
450
557
|
}
|
|
451
558
|
return {
|
|
452
559
|
title: resultTitle,
|
|
@@ -461,33 +568,34 @@ export class BmClient {
|
|
|
461
568
|
}
|
|
462
569
|
async buildContext(url, depth = 1, project) {
|
|
463
570
|
const args = {
|
|
464
|
-
url,
|
|
571
|
+
identifier: url,
|
|
465
572
|
depth,
|
|
466
573
|
output_format: "json",
|
|
467
574
|
project: this.routedProject(project),
|
|
468
575
|
};
|
|
469
|
-
const payload = await this.callTool("
|
|
576
|
+
const payload = await this.callTool("bm_context", args);
|
|
470
577
|
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
471
|
-
throw new Error("invalid
|
|
578
|
+
throw new Error("invalid bm_context response");
|
|
472
579
|
}
|
|
473
580
|
return payload;
|
|
474
581
|
}
|
|
475
582
|
async recentActivity(timeframe = "24h", project) {
|
|
476
583
|
const args = {
|
|
477
|
-
|
|
584
|
+
command: "recent",
|
|
585
|
+
since: timeframe,
|
|
478
586
|
output_format: "json",
|
|
479
587
|
project: this.routedProject(project),
|
|
480
588
|
};
|
|
481
|
-
const payload = await this.callTool("
|
|
482
|
-
if (Array.isArray(payload)) {
|
|
483
|
-
return payload;
|
|
589
|
+
const payload = await this.callTool("bm_project", args);
|
|
590
|
+
if (isRecord(payload) && Array.isArray(payload.results)) {
|
|
591
|
+
return payload.results;
|
|
484
592
|
}
|
|
485
|
-
throw new Error("invalid
|
|
593
|
+
throw new Error("invalid bm_project response");
|
|
486
594
|
}
|
|
487
595
|
async editNote(identifier, operation, content, options = {}, project) {
|
|
488
596
|
const args = {
|
|
489
597
|
identifier,
|
|
490
|
-
operation,
|
|
598
|
+
command: operation,
|
|
491
599
|
content,
|
|
492
600
|
output_format: "json",
|
|
493
601
|
project: this.routedProject(project),
|
|
@@ -500,15 +608,15 @@ export class BmClient {
|
|
|
500
608
|
args.expected_replacements = options.expected_replacements;
|
|
501
609
|
if (options.metadata !== undefined)
|
|
502
610
|
args.metadata = options.metadata;
|
|
503
|
-
const payload = await this.callTool("
|
|
611
|
+
const payload = await this.callTool("bm_edit", args);
|
|
504
612
|
if (!isRecord(payload)) {
|
|
505
|
-
throw new Error("invalid
|
|
613
|
+
throw new Error("invalid bm_edit response");
|
|
506
614
|
}
|
|
507
615
|
const title = asString(payload.title);
|
|
508
616
|
const permalink = asString(payload.permalink);
|
|
509
617
|
const filePath = asString(payload.file_path);
|
|
510
618
|
if (!title || !permalink || !filePath) {
|
|
511
|
-
throw new Error("invalid
|
|
619
|
+
throw new Error("invalid bm_edit payload");
|
|
512
620
|
}
|
|
513
621
|
return {
|
|
514
622
|
title,
|
|
@@ -524,12 +632,12 @@ export class BmClient {
|
|
|
524
632
|
output_format: "json",
|
|
525
633
|
project: this.routedProject(project),
|
|
526
634
|
};
|
|
527
|
-
const payload = await this.callTool("
|
|
635
|
+
const payload = await this.callTool("bm_delete", args);
|
|
528
636
|
if (!isRecord(payload)) {
|
|
529
|
-
throw new Error("invalid
|
|
637
|
+
throw new Error("invalid bm_delete response");
|
|
530
638
|
}
|
|
531
|
-
if (payload.
|
|
532
|
-
throw new Error(`
|
|
639
|
+
if (payload.action !== "deleted") {
|
|
640
|
+
throw new Error(`bm_delete did not delete "${identifier}"`);
|
|
533
641
|
}
|
|
534
642
|
return {
|
|
535
643
|
title: asString(payload.title) ?? identifier,
|
|
@@ -540,17 +648,17 @@ export class BmClient {
|
|
|
540
648
|
async moveNote(identifier, newFolder, project) {
|
|
541
649
|
const args = {
|
|
542
650
|
identifier,
|
|
543
|
-
|
|
651
|
+
destination: `${newFolder.replace(/\/+$/, "")}/`,
|
|
544
652
|
output_format: "json",
|
|
545
653
|
project: this.routedProject(project),
|
|
546
654
|
};
|
|
547
|
-
const payload = await this.callTool("
|
|
655
|
+
const payload = await this.callTool("bm_move", args);
|
|
548
656
|
if (!isRecord(payload)) {
|
|
549
|
-
throw new Error("invalid
|
|
657
|
+
throw new Error("invalid bm_move response");
|
|
550
658
|
}
|
|
551
|
-
if (payload.
|
|
659
|
+
if (payload.action !== "moved") {
|
|
552
660
|
throw new Error(asString(payload.error) ??
|
|
553
|
-
`
|
|
661
|
+
`bm_move did not move "${identifier}" to "${newFolder}"`);
|
|
554
662
|
}
|
|
555
663
|
return {
|
|
556
664
|
title: asString(payload.title) ?? identifier,
|
|
@@ -561,6 +669,7 @@ export class BmClient {
|
|
|
561
669
|
}
|
|
562
670
|
async schemaValidate(noteType, identifier, project) {
|
|
563
671
|
const args = {
|
|
672
|
+
command: "validate",
|
|
564
673
|
output_format: "json",
|
|
565
674
|
project: this.routedProject(project),
|
|
566
675
|
};
|
|
@@ -568,44 +677,77 @@ export class BmClient {
|
|
|
568
677
|
args.note_type = noteType;
|
|
569
678
|
if (identifier)
|
|
570
679
|
args.identifier = identifier;
|
|
571
|
-
const payload = await this.callTool("
|
|
680
|
+
const payload = await this.callTool("bm_schema", args);
|
|
572
681
|
if (!isRecord(payload)) {
|
|
573
|
-
throw new Error("invalid
|
|
682
|
+
throw new Error("invalid bm_schema response");
|
|
574
683
|
}
|
|
575
684
|
return payload;
|
|
576
685
|
}
|
|
577
686
|
async schemaInfer(noteType, threshold = 0.25, project) {
|
|
578
687
|
const args = {
|
|
688
|
+
command: "infer",
|
|
579
689
|
note_type: noteType,
|
|
580
690
|
threshold,
|
|
581
691
|
output_format: "json",
|
|
582
692
|
project: this.routedProject(project),
|
|
583
693
|
};
|
|
584
|
-
const payload = await this.callTool("
|
|
694
|
+
const payload = await this.callTool("bm_schema", args);
|
|
585
695
|
if (!isRecord(payload)) {
|
|
586
|
-
throw new Error("invalid
|
|
696
|
+
throw new Error("invalid bm_schema response");
|
|
587
697
|
}
|
|
588
698
|
return payload;
|
|
589
699
|
}
|
|
590
700
|
async schemaDiff(noteType, project) {
|
|
591
701
|
const args = {
|
|
702
|
+
command: "diff",
|
|
592
703
|
note_type: noteType,
|
|
593
704
|
output_format: "json",
|
|
594
705
|
project: this.routedProject(project),
|
|
595
706
|
};
|
|
596
|
-
const payload = await this.callTool("
|
|
707
|
+
const payload = await this.callTool("bm_schema", args);
|
|
597
708
|
if (!isRecord(payload)) {
|
|
598
|
-
throw new Error("invalid
|
|
709
|
+
throw new Error("invalid bm_schema response");
|
|
599
710
|
}
|
|
600
711
|
return payload;
|
|
601
712
|
}
|
|
602
|
-
|
|
713
|
+
/**
|
|
714
|
+
* Append one user/assistant exchange to today's conversation note.
|
|
715
|
+
*
|
|
716
|
+
* With a turnKey the capture is idempotent: the entry carries a marker
|
|
717
|
+
* derived from the key, and a key already present in today's or
|
|
718
|
+
* yesterday's note is reported as a duplicate instead of written again.
|
|
719
|
+
* Yesterday is checked because a host retry can cross midnight.
|
|
720
|
+
*/
|
|
721
|
+
async indexConversation(userMessage, assistantResponse, options = {}) {
|
|
722
|
+
const write = this.conversationWrites.then(() => this.writeConversationEntry(userMessage, assistantResponse, options.turnKey));
|
|
723
|
+
this.conversationWrites = write.catch(() => undefined);
|
|
724
|
+
return write;
|
|
725
|
+
}
|
|
726
|
+
async writeConversationEntry(userMessage, assistantResponse, turnKey) {
|
|
603
727
|
const now = new Date();
|
|
604
|
-
const dateStr = now
|
|
728
|
+
const dateStr = conversationDate(now);
|
|
605
729
|
const timeStr = now.toTimeString().slice(0, 5);
|
|
606
730
|
const title = `conversations-${dateStr}`;
|
|
731
|
+
const marker = turnKey ? conversationTurnMarker(turnKey) : null;
|
|
732
|
+
// Trigger: the host supplied an advancement key for this turn.
|
|
733
|
+
// Why: hosts retry commits after a crash, and a second append would
|
|
734
|
+
// duplicate the exchange in the note.
|
|
735
|
+
// Outcome: an already-recorded key short-circuits before any write.
|
|
736
|
+
if (marker) {
|
|
737
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
738
|
+
for (const candidate of [
|
|
739
|
+
title,
|
|
740
|
+
`conversations-${conversationDate(yesterday)}`,
|
|
741
|
+
]) {
|
|
742
|
+
if (await this.noteContains(candidate, marker)) {
|
|
743
|
+
log.debug(`conversation turn already recorded in: ${candidate}`);
|
|
744
|
+
return "duplicate";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
607
748
|
const entry = [
|
|
608
749
|
`### ${timeStr}`,
|
|
750
|
+
...(marker ? [marker] : []),
|
|
609
751
|
"",
|
|
610
752
|
"**User:**",
|
|
611
753
|
userMessage,
|
|
@@ -618,7 +760,7 @@ export class BmClient {
|
|
|
618
760
|
try {
|
|
619
761
|
await this.editNote(title, "append", entry);
|
|
620
762
|
log.debug(`appended conversation to: ${title}`);
|
|
621
|
-
return;
|
|
763
|
+
return "committed";
|
|
622
764
|
}
|
|
623
765
|
catch (err) {
|
|
624
766
|
if (!isNoteNotFoundError(err)) {
|
|
@@ -639,12 +781,19 @@ export class BmClient {
|
|
|
639
781
|
"",
|
|
640
782
|
entry,
|
|
641
783
|
].join("\n");
|
|
784
|
+
await this.writeNote(title, content, "conversations");
|
|
785
|
+
log.debug(`created conversation note: ${title}`);
|
|
786
|
+
return "committed";
|
|
787
|
+
}
|
|
788
|
+
async noteContains(identifier, text) {
|
|
642
789
|
try {
|
|
643
|
-
await this.
|
|
644
|
-
|
|
790
|
+
const note = await this.readNote(identifier);
|
|
791
|
+
return note.content.includes(text);
|
|
645
792
|
}
|
|
646
793
|
catch (err) {
|
|
647
|
-
|
|
794
|
+
if (isNoteNotFoundError(err))
|
|
795
|
+
return false;
|
|
796
|
+
throw err;
|
|
648
797
|
}
|
|
649
798
|
}
|
|
650
799
|
getProject() {
|
package/dist/commands/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
-
import type
|
|
2
|
+
import { type BmClient } from "../bm-client.ts";
|
|
3
3
|
import type { BasicMemoryConfig } from "../config.ts";
|
|
4
4
|
export declare function registerCli(api: OpenClawPluginApi, client: BmClient, cfg: BasicMemoryConfig): void;
|
package/dist/commands/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { searchResultText } from "../bm-client.js";
|
|
1
2
|
import { log } from "../logger.js";
|
|
2
3
|
export function registerCli(api, client, cfg) {
|
|
3
4
|
api.registerCli(
|
|
@@ -19,12 +20,11 @@ export function registerCli(api, client, cfg) {
|
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
for (const r of results) {
|
|
22
|
-
const score = r.score ? ` (${
|
|
23
|
+
const score = r.score ? ` (score ${r.score.toFixed(2)})` : "";
|
|
23
24
|
console.log(`- ${r.title}${score}`);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
: r.content;
|
|
25
|
+
const text = searchResultText(r);
|
|
26
|
+
if (text) {
|
|
27
|
+
const preview = text.length > 100 ? `${text.slice(0, 100)}...` : text;
|
|
28
28
|
console.log(` ${preview}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
package/dist/commands/slash.d.ts
CHANGED