@basicmemory/openclaw-basic-memory 0.24.0 → 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/bm-client.ts
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"
|
|
@@ -7,17 +8,18 @@ const DEFAULT_RETRY_DELAYS_MS = [500, 1000, 2000]
|
|
|
7
8
|
|
|
8
9
|
export class NoteAlreadyExistsError extends Error {
|
|
9
10
|
readonly permalink: string
|
|
10
|
-
constructor(title: string, permalink: string) {
|
|
11
|
-
super(`Note already exists: "${title}" (${permalink})`)
|
|
11
|
+
constructor(title: string, permalink: string, message?: string) {
|
|
12
|
+
super(message ?? `Note already exists: "${title}" (${permalink})`)
|
|
12
13
|
this.name = "NoteAlreadyExistsError"
|
|
13
14
|
this.permalink = permalink
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
+
* bm_write refused a conditional overwrite: the note is no longer the
|
|
19
20
|
* revision named by expected_checksum. Nothing was written; currentChecksum is
|
|
20
|
-
*
|
|
21
|
+
* available revision when supplied by a server. A null value means unknown;
|
|
22
|
+
* 1.0 puts revision details in the refusal message, not a checksum field.
|
|
21
23
|
*/
|
|
22
24
|
export class NoteRevisionConflictError extends Error {
|
|
23
25
|
readonly permalink: string | null
|
|
@@ -26,8 +28,11 @@ export class NoteRevisionConflictError extends Error {
|
|
|
26
28
|
title: string,
|
|
27
29
|
permalink: string | null,
|
|
28
30
|
currentChecksum: string | null,
|
|
31
|
+
message?: string,
|
|
29
32
|
) {
|
|
30
|
-
super(
|
|
33
|
+
super(
|
|
34
|
+
message ?? `Note revision conflict: "${title}" changed since it was read`,
|
|
35
|
+
)
|
|
31
36
|
this.name = "NoteRevisionConflictError"
|
|
32
37
|
this.permalink = permalink
|
|
33
38
|
this.currentChecksum = currentChecksum
|
|
@@ -49,20 +54,15 @@ export class BmToolError extends Error {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const REQUIRED_TOOLS = [
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"delete_note",
|
|
62
|
-
"move_note",
|
|
63
|
-
"schema_validate",
|
|
64
|
-
"schema_infer",
|
|
65
|
-
"schema_diff",
|
|
57
|
+
"bm_search",
|
|
58
|
+
"bm_read",
|
|
59
|
+
"bm_write",
|
|
60
|
+
"bm_edit",
|
|
61
|
+
"bm_context",
|
|
62
|
+
"bm_project",
|
|
63
|
+
"bm_delete",
|
|
64
|
+
"bm_move",
|
|
65
|
+
"bm_schema",
|
|
66
66
|
]
|
|
67
67
|
|
|
68
68
|
export interface SearchResult {
|
|
@@ -71,6 +71,17 @@ export interface SearchResult {
|
|
|
71
71
|
content: string
|
|
72
72
|
score?: number
|
|
73
73
|
file_path: string
|
|
74
|
+
// The chunk of the note that matched, when the server ranked by chunk.
|
|
75
|
+
matched_chunk?: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The text to preview for a search hit. `content` is the start of the note,
|
|
80
|
+
* so for a long note (a day of captured conversation) the matching part can
|
|
81
|
+
* sit far below any preview cut-off; the matched chunk is the part that hit.
|
|
82
|
+
*/
|
|
83
|
+
export function searchResultText(result: SearchResult): string {
|
|
84
|
+
return result.matched_chunk?.trim() || result.content || ""
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
export interface NoteResult {
|
|
@@ -87,7 +98,7 @@ export interface EditNoteResult {
|
|
|
87
98
|
title: string
|
|
88
99
|
permalink: string
|
|
89
100
|
file_path: string
|
|
90
|
-
operation:
|
|
101
|
+
operation: EditOperation
|
|
91
102
|
checksum?: string | null
|
|
92
103
|
}
|
|
93
104
|
|
|
@@ -107,6 +118,16 @@ interface WriteNoteOptions {
|
|
|
107
118
|
expectedChecksum?: string
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
// bm_edit commands the plugin exposes. set_metadata is left out: edit_note
|
|
122
|
+
// already merges `metadata` alongside any other operation.
|
|
123
|
+
export type EditOperation =
|
|
124
|
+
| "append"
|
|
125
|
+
| "prepend"
|
|
126
|
+
| "find_replace"
|
|
127
|
+
| "replace_section"
|
|
128
|
+
| "insert_before_section"
|
|
129
|
+
| "insert_after_section"
|
|
130
|
+
|
|
110
131
|
interface EditNoteOptions {
|
|
111
132
|
find_text?: string
|
|
112
133
|
section?: string
|
|
@@ -127,7 +148,10 @@ export interface ContextResult {
|
|
|
127
148
|
permalink: string
|
|
128
149
|
relation_type?: string
|
|
129
150
|
from_entity?: string
|
|
130
|
-
|
|
151
|
+
// Null for a forward reference whose target note does not exist yet;
|
|
152
|
+
// to_name then carries the link text.
|
|
153
|
+
to_entity?: string | null
|
|
154
|
+
to_name?: string | null
|
|
131
155
|
}>
|
|
132
156
|
}>
|
|
133
157
|
}
|
|
@@ -158,10 +182,7 @@ export interface ProjectListResult {
|
|
|
158
182
|
export interface WorkspaceResult {
|
|
159
183
|
tenant_id: string
|
|
160
184
|
name: string
|
|
161
|
-
workspace_type: string
|
|
162
|
-
role: string
|
|
163
|
-
organization_id?: string | null
|
|
164
|
-
has_active_subscription: boolean
|
|
185
|
+
workspace_type: string | null
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
export interface SchemaValidationResult {
|
|
@@ -249,23 +270,32 @@ function isRecoverableConnectionError(err: unknown): boolean {
|
|
|
249
270
|
)
|
|
250
271
|
}
|
|
251
272
|
|
|
252
|
-
function isNoteNotFoundError(err: unknown): boolean {
|
|
253
|
-
const msg = getErrorMessage(err).toLowerCase()
|
|
273
|
+
export function isNoteNotFoundError(err: unknown): boolean {
|
|
254
274
|
return (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
msg.includes("resource not found") ||
|
|
258
|
-
msg.includes("could not find note matching") ||
|
|
259
|
-
msg.includes("404")
|
|
275
|
+
err instanceof BmToolError &&
|
|
276
|
+
parseRefusalPayload(err.toolMessage)?.error === "NOT_FOUND"
|
|
260
277
|
)
|
|
261
278
|
}
|
|
262
279
|
|
|
280
|
+
export type ConversationCaptureStatus = "committed" | "duplicate"
|
|
281
|
+
|
|
282
|
+
function conversationDate(date: Date): string {
|
|
283
|
+
return date.toISOString().split("T")[0]
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Host advancement keys are opaque and may contain characters that would end
|
|
287
|
+
// an HTML comment, so the note stores a fixed-length digest instead.
|
|
288
|
+
function conversationTurnMarker(turnKey: string): string {
|
|
289
|
+
const digest = createHash("sha256").update(turnKey).digest("hex").slice(0, 32)
|
|
290
|
+
return `<!-- openclaw-turn:${digest} -->`
|
|
291
|
+
}
|
|
292
|
+
|
|
263
293
|
function asString(value: unknown): string | null {
|
|
264
294
|
return typeof value === "string" ? value : null
|
|
265
295
|
}
|
|
266
296
|
|
|
267
297
|
// In JSON mode BM reports a refused write as a tool error whose text is the
|
|
268
|
-
// refusal payload itself ({"error": "
|
|
298
|
+
// refusal payload itself ({"error": "ALREADY_EXISTS", ...}). Any other
|
|
269
299
|
// error text (plain-text failures, older servers) is not a refusal payload.
|
|
270
300
|
function parseRefusalPayload(text: string): Record<string, unknown> | null {
|
|
271
301
|
let parsed: unknown
|
|
@@ -277,6 +307,24 @@ function parseRefusalPayload(text: string): Record<string, unknown> | null {
|
|
|
277
307
|
return isRecord(parsed) && typeof parsed.error === "string" ? parsed : null
|
|
278
308
|
}
|
|
279
309
|
|
|
310
|
+
/**
|
|
311
|
+
* The server's own explanation of a failed tool call, for showing to an agent.
|
|
312
|
+
* A refusal payload becomes "CODE: message"; other tool text passes through.
|
|
313
|
+
*/
|
|
314
|
+
export function describeToolError(err: unknown): string {
|
|
315
|
+
if (!(err instanceof BmToolError)) return getErrorMessage(err)
|
|
316
|
+
const payload = parseRefusalPayload(err.toolMessage)
|
|
317
|
+
if (!payload) return err.toolMessage || err.message
|
|
318
|
+
const message = asString(payload.message)
|
|
319
|
+
return message ? `${payload.error}: ${message}` : String(payload.error)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** describeToolError, but saying plainly when the note does not exist. */
|
|
323
|
+
export function describeNoteError(err: unknown): string {
|
|
324
|
+
const reason = describeToolError(err)
|
|
325
|
+
return isNoteNotFoundError(err) ? `it does not exist. ${reason}` : reason
|
|
326
|
+
}
|
|
327
|
+
|
|
280
328
|
function projectMatchesWorkspace(
|
|
281
329
|
project: ProjectListResult,
|
|
282
330
|
workspace: string,
|
|
@@ -299,34 +347,103 @@ export class BmClient {
|
|
|
299
347
|
private cwd?: string
|
|
300
348
|
private env?: Record<string, string>
|
|
301
349
|
private shouldRun = false
|
|
350
|
+
// True only between start() and stop(), i.e. while the plugin service owns
|
|
351
|
+
// the BM MCP session. A lazy connection opened by a tool or memory call in a
|
|
352
|
+
// one-shot CLI process leaves this false.
|
|
353
|
+
private serviceRunning = false
|
|
354
|
+
// Connect plus project creation, started by start(). Tool calls wait for
|
|
355
|
+
// it so none can reach a project that does not exist yet. Null when the
|
|
356
|
+
// client was never started (tests, one-shot CLI readers).
|
|
357
|
+
private startup: Promise<void> | null = null
|
|
358
|
+
private startupRetry: Promise<void> | null = null
|
|
359
|
+
private projectPath?: string
|
|
360
|
+
private prepare?: () => Promise<void>
|
|
302
361
|
|
|
303
362
|
private client: Client | null = null
|
|
304
363
|
private transport: StdioClientTransport | null = null
|
|
305
364
|
private connectPromise: Promise<void> | null = null
|
|
306
365
|
private retryDelaysMs = [...DEFAULT_RETRY_DELAYS_MS]
|
|
366
|
+
// Conversation captures run one at a time so a turn-key duplicate check
|
|
367
|
+
// and the append that follows it cannot interleave with another capture.
|
|
368
|
+
private conversationWrites: Promise<unknown> = Promise.resolve()
|
|
307
369
|
|
|
308
370
|
constructor(bmPath: string, project: string) {
|
|
309
371
|
this.bmPath = bmPath
|
|
310
372
|
this.project = project
|
|
311
373
|
}
|
|
312
374
|
|
|
313
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Connect to BM and create the plugin's project.
|
|
377
|
+
*
|
|
378
|
+
* Returns the startup promise; callers that must not block (the plugin
|
|
379
|
+
* service, which OpenClaw gives only a few seconds) can leave it running.
|
|
380
|
+
* Tool calls made meanwhile wait for it. If it fails, the next tool call
|
|
381
|
+
* runs it again, so a transient failure does not leave the plugin dead.
|
|
382
|
+
*/
|
|
383
|
+
start(options?: {
|
|
314
384
|
cwd?: string
|
|
315
385
|
env?: Record<string, string>
|
|
386
|
+
projectPath?: string
|
|
387
|
+
// Runs before each connect attempt, e.g. installing the bm CLI.
|
|
388
|
+
prepare?: () => Promise<void>
|
|
316
389
|
}): Promise<void> {
|
|
317
390
|
this.shouldRun = true
|
|
391
|
+
this.serviceRunning = true
|
|
318
392
|
if (options?.cwd) {
|
|
319
393
|
this.cwd = options.cwd
|
|
320
394
|
}
|
|
321
395
|
if (options?.env) {
|
|
322
396
|
this.env = options.env
|
|
323
397
|
}
|
|
398
|
+
if (options?.projectPath) {
|
|
399
|
+
this.projectPath = options.projectPath
|
|
400
|
+
}
|
|
401
|
+
if (options?.prepare) {
|
|
402
|
+
this.prepare = options.prepare
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.startup = this.runStartup()
|
|
406
|
+
return this.startup
|
|
407
|
+
}
|
|
324
408
|
|
|
409
|
+
private async runStartup(): Promise<void> {
|
|
410
|
+
await this.prepare?.()
|
|
325
411
|
await this.connectWithRetries()
|
|
412
|
+
if (this.projectPath) await this.createProject(this.projectPath)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async waitForStartup(): Promise<void> {
|
|
416
|
+
if (!this.startup) return
|
|
417
|
+
try {
|
|
418
|
+
await this.startup
|
|
419
|
+
} catch (err) {
|
|
420
|
+
// Trigger: the startup this call was waiting on failed.
|
|
421
|
+
// Why: the service no longer fails on a slow or failed start, so the
|
|
422
|
+
// first tool call afterwards is the place to recover.
|
|
423
|
+
// Outcome: one fresh attempt, shared by concurrent callers; its error
|
|
424
|
+
// reaches the tool if it fails too.
|
|
425
|
+
if (!this.serviceRunning) throw err
|
|
426
|
+
if (this.startupRetry === null) {
|
|
427
|
+
this.startupRetry = this.runStartup().finally(() => {
|
|
428
|
+
this.startupRetry = null
|
|
429
|
+
})
|
|
430
|
+
this.startup = this.startupRetry
|
|
431
|
+
}
|
|
432
|
+
await this.startupRetry
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
isServiceRunning(): boolean {
|
|
437
|
+
return this.serviceRunning
|
|
326
438
|
}
|
|
327
439
|
|
|
328
440
|
async stop(): Promise<void> {
|
|
329
441
|
this.shouldRun = false
|
|
442
|
+
this.serviceRunning = false
|
|
443
|
+
// Let an in-flight startup settle first, so it cannot reconnect after
|
|
444
|
+
// the disconnect below.
|
|
445
|
+
await this.startup?.catch(() => undefined)
|
|
446
|
+
this.startup = null
|
|
330
447
|
await this.disconnectCurrent(this.client, this.transport)
|
|
331
448
|
this.client = null
|
|
332
449
|
this.transport = null
|
|
@@ -527,6 +644,15 @@ export class BmClient {
|
|
|
527
644
|
private async callTool(
|
|
528
645
|
name: string,
|
|
529
646
|
args: Record<string, unknown>,
|
|
647
|
+
): Promise<unknown> {
|
|
648
|
+
await this.waitForStartup()
|
|
649
|
+
return this.callStructured(name, args)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Ungated: startup itself calls this to create the project.
|
|
653
|
+
private async callStructured(
|
|
654
|
+
name: string,
|
|
655
|
+
args: Record<string, unknown>,
|
|
530
656
|
): Promise<unknown> {
|
|
531
657
|
const result = await this.callToolRaw(name, args)
|
|
532
658
|
|
|
@@ -546,44 +672,49 @@ export class BmClient {
|
|
|
546
672
|
return project ?? this.project
|
|
547
673
|
}
|
|
548
674
|
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
private routing(
|
|
553
|
-
project?: string,
|
|
554
|
-
projectId?: string,
|
|
555
|
-
): { project: string } | { project_id: string } {
|
|
556
|
-
if (projectId !== undefined) return { project_id: projectId }
|
|
675
|
+
// The 1.0 project parameter accepts either a name or an exact project UUID.
|
|
676
|
+
private routing(project?: string, projectId?: string): { project: string } {
|
|
677
|
+
if (projectId !== undefined) return { project: projectId }
|
|
557
678
|
return { project: this.routedProject(project) }
|
|
558
679
|
}
|
|
559
680
|
|
|
560
681
|
async ensureProject(projectPath: string): Promise<void> {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
682
|
+
await this.waitForStartup()
|
|
683
|
+
await this.createProject(projectPath)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private async createProject(projectPath: string): Promise<void> {
|
|
687
|
+
const payload = await this.callStructured("bm_project", {
|
|
688
|
+
command: "create",
|
|
689
|
+
name: this.project,
|
|
690
|
+
path: projectPath,
|
|
564
691
|
set_default: true,
|
|
565
692
|
output_format: "json",
|
|
566
693
|
})
|
|
567
694
|
|
|
568
695
|
if (!isRecord(payload)) {
|
|
569
|
-
throw new Error("invalid
|
|
696
|
+
throw new Error("invalid bm_project response")
|
|
570
697
|
}
|
|
571
698
|
}
|
|
572
699
|
|
|
573
700
|
async listWorkspaces(): Promise<WorkspaceResult[]> {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
701
|
+
const projects = await this.listProjects()
|
|
702
|
+
const workspaces = new Map<string, WorkspaceResult>()
|
|
703
|
+
for (const project of projects) {
|
|
704
|
+
if (project.workspace_tenant_id && project.workspace_name) {
|
|
705
|
+
workspaces.set(project.workspace_tenant_id, {
|
|
706
|
+
tenant_id: project.workspace_tenant_id,
|
|
707
|
+
name: project.workspace_name,
|
|
708
|
+
workspace_type: project.workspace_type ?? null,
|
|
709
|
+
})
|
|
710
|
+
}
|
|
580
711
|
}
|
|
581
|
-
|
|
582
|
-
throw new Error("invalid list_workspaces response")
|
|
712
|
+
return [...workspaces.values()]
|
|
583
713
|
}
|
|
584
714
|
|
|
585
715
|
async listProjects(workspace?: string): Promise<ProjectListResult[]> {
|
|
586
|
-
const payload = await this.callTool("
|
|
716
|
+
const payload = await this.callTool("bm_project", {
|
|
717
|
+
command: "list",
|
|
587
718
|
output_format: "json",
|
|
588
719
|
})
|
|
589
720
|
|
|
@@ -597,7 +728,7 @@ export class BmClient {
|
|
|
597
728
|
return projects
|
|
598
729
|
}
|
|
599
730
|
|
|
600
|
-
throw new Error("invalid
|
|
731
|
+
throw new Error("invalid bm_project response")
|
|
601
732
|
}
|
|
602
733
|
|
|
603
734
|
async search(
|
|
@@ -619,16 +750,23 @@ export class BmClient {
|
|
|
619
750
|
project: this.routedProject(project),
|
|
620
751
|
}
|
|
621
752
|
if (query) args.query = query
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (metadata?.
|
|
753
|
+
const filters = { ...metadata?.filters }
|
|
754
|
+
// Explicit predicates take precedence over convenience shortcuts.
|
|
755
|
+
if (metadata?.tags?.length && !("tags" in filters))
|
|
756
|
+
filters.tags = { $contains: metadata.tags }
|
|
757
|
+
if (metadata?.status && !("status" in filters))
|
|
758
|
+
filters.status = metadata.status
|
|
759
|
+
if (Object.keys(filters).length) args.filters = filters
|
|
625
760
|
if (metadata?.note_types) args.note_types = metadata.note_types
|
|
626
|
-
if (metadata?.entity_types)
|
|
761
|
+
if (metadata?.entity_types)
|
|
762
|
+
args.items = metadata.entity_types.map((item) =>
|
|
763
|
+
item === "entity" ? "note" : item,
|
|
764
|
+
)
|
|
627
765
|
|
|
628
|
-
const payload = await this.callTool("
|
|
766
|
+
const payload = await this.callTool("bm_search", args)
|
|
629
767
|
|
|
630
768
|
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
631
|
-
throw new Error("invalid
|
|
769
|
+
throw new Error("invalid bm_search response")
|
|
632
770
|
}
|
|
633
771
|
|
|
634
772
|
return payload.results as SearchResult[]
|
|
@@ -646,10 +784,10 @@ export class BmClient {
|
|
|
646
784
|
...this.routing(project, options.projectId),
|
|
647
785
|
}
|
|
648
786
|
|
|
649
|
-
const payload = await this.callTool("
|
|
787
|
+
const payload = await this.callTool("bm_read", args)
|
|
650
788
|
|
|
651
789
|
if (!isRecord(payload)) {
|
|
652
|
-
throw new Error("invalid
|
|
790
|
+
throw new Error("invalid bm_read response")
|
|
653
791
|
}
|
|
654
792
|
|
|
655
793
|
const title = asString(payload.title)
|
|
@@ -658,7 +796,7 @@ export class BmClient {
|
|
|
658
796
|
const filePath = asString(payload.file_path)
|
|
659
797
|
|
|
660
798
|
if (!title || !permalink || content === null || !filePath) {
|
|
661
|
-
throw new Error("invalid
|
|
799
|
+
throw new Error("invalid bm_read payload")
|
|
662
800
|
}
|
|
663
801
|
|
|
664
802
|
return {
|
|
@@ -695,36 +833,30 @@ export class BmClient {
|
|
|
695
833
|
|
|
696
834
|
let payload: unknown
|
|
697
835
|
try {
|
|
698
|
-
payload = await this.callTool("
|
|
836
|
+
payload = await this.callTool("bm_write", args)
|
|
699
837
|
} catch (err) {
|
|
700
838
|
const refusal =
|
|
701
839
|
err instanceof BmToolError ? parseRefusalPayload(err.toolMessage) : null
|
|
702
|
-
if (refusal?.error === "
|
|
840
|
+
if (refusal?.error === "ALREADY_EXISTS") {
|
|
703
841
|
throw new NoteAlreadyExistsError(
|
|
704
842
|
asString(refusal.title) ?? title,
|
|
705
843
|
asString(refusal.permalink) ?? "",
|
|
844
|
+
asString(refusal.message) ?? undefined,
|
|
706
845
|
)
|
|
707
846
|
}
|
|
708
|
-
if (refusal?.error === "
|
|
847
|
+
if (refusal?.error === "REVISION_CONFLICT") {
|
|
709
848
|
throw new NoteRevisionConflictError(
|
|
710
849
|
asString(refusal.title) ?? title,
|
|
711
850
|
asString(refusal.permalink),
|
|
712
851
|
asString(refusal.currentChecksum),
|
|
852
|
+
asString(refusal.message) ?? undefined,
|
|
713
853
|
)
|
|
714
854
|
}
|
|
715
855
|
throw err
|
|
716
856
|
}
|
|
717
857
|
|
|
718
858
|
if (!isRecord(payload)) {
|
|
719
|
-
throw new Error("invalid
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Older servers returned the refusal as a successful payload.
|
|
723
|
-
if (payload.error === "NOTE_ALREADY_EXISTS") {
|
|
724
|
-
throw new NoteAlreadyExistsError(
|
|
725
|
-
asString(payload.title) ?? title,
|
|
726
|
-
asString(payload.permalink) ?? "",
|
|
727
|
-
)
|
|
859
|
+
throw new Error("invalid bm_write response")
|
|
728
860
|
}
|
|
729
861
|
|
|
730
862
|
const resultTitle = asString(payload.title)
|
|
@@ -732,7 +864,7 @@ export class BmClient {
|
|
|
732
864
|
const filePath = asString(payload.file_path)
|
|
733
865
|
|
|
734
866
|
if (!resultTitle || !permalink || !filePath) {
|
|
735
|
-
throw new Error("invalid
|
|
867
|
+
throw new Error("invalid bm_write payload")
|
|
736
868
|
}
|
|
737
869
|
|
|
738
870
|
return {
|
|
@@ -754,16 +886,16 @@ export class BmClient {
|
|
|
754
886
|
project?: string,
|
|
755
887
|
): Promise<ContextResult> {
|
|
756
888
|
const args: Record<string, unknown> = {
|
|
757
|
-
url,
|
|
889
|
+
identifier: url,
|
|
758
890
|
depth,
|
|
759
891
|
output_format: "json",
|
|
760
892
|
project: this.routedProject(project),
|
|
761
893
|
}
|
|
762
894
|
|
|
763
|
-
const payload = await this.callTool("
|
|
895
|
+
const payload = await this.callTool("bm_context", args)
|
|
764
896
|
|
|
765
897
|
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
|
766
|
-
throw new Error("invalid
|
|
898
|
+
throw new Error("invalid bm_context response")
|
|
767
899
|
}
|
|
768
900
|
|
|
769
901
|
return payload as unknown as ContextResult
|
|
@@ -774,30 +906,31 @@ export class BmClient {
|
|
|
774
906
|
project?: string,
|
|
775
907
|
): Promise<RecentResult[]> {
|
|
776
908
|
const args: Record<string, unknown> = {
|
|
777
|
-
|
|
909
|
+
command: "recent",
|
|
910
|
+
since: timeframe,
|
|
778
911
|
output_format: "json",
|
|
779
912
|
project: this.routedProject(project),
|
|
780
913
|
}
|
|
781
914
|
|
|
782
|
-
const payload = await this.callTool("
|
|
915
|
+
const payload = await this.callTool("bm_project", args)
|
|
783
916
|
|
|
784
|
-
if (Array.isArray(payload)) {
|
|
785
|
-
return payload as RecentResult[]
|
|
917
|
+
if (isRecord(payload) && Array.isArray(payload.results)) {
|
|
918
|
+
return payload.results as RecentResult[]
|
|
786
919
|
}
|
|
787
920
|
|
|
788
|
-
throw new Error("invalid
|
|
921
|
+
throw new Error("invalid bm_project response")
|
|
789
922
|
}
|
|
790
923
|
|
|
791
924
|
async editNote(
|
|
792
925
|
identifier: string,
|
|
793
|
-
operation:
|
|
926
|
+
operation: EditOperation,
|
|
794
927
|
content: string,
|
|
795
928
|
options: EditNoteOptions = {},
|
|
796
929
|
project?: string,
|
|
797
930
|
): Promise<EditNoteResult> {
|
|
798
931
|
const args: Record<string, unknown> = {
|
|
799
932
|
identifier,
|
|
800
|
-
operation,
|
|
933
|
+
command: operation,
|
|
801
934
|
content,
|
|
802
935
|
output_format: "json",
|
|
803
936
|
project: this.routedProject(project),
|
|
@@ -808,10 +941,10 @@ export class BmClient {
|
|
|
808
941
|
args.expected_replacements = options.expected_replacements
|
|
809
942
|
if (options.metadata !== undefined) args.metadata = options.metadata
|
|
810
943
|
|
|
811
|
-
const payload = await this.callTool("
|
|
944
|
+
const payload = await this.callTool("bm_edit", args)
|
|
812
945
|
|
|
813
946
|
if (!isRecord(payload)) {
|
|
814
|
-
throw new Error("invalid
|
|
947
|
+
throw new Error("invalid bm_edit response")
|
|
815
948
|
}
|
|
816
949
|
|
|
817
950
|
const title = asString(payload.title)
|
|
@@ -819,7 +952,7 @@ export class BmClient {
|
|
|
819
952
|
const filePath = asString(payload.file_path)
|
|
820
953
|
|
|
821
954
|
if (!title || !permalink || !filePath) {
|
|
822
|
-
throw new Error("invalid
|
|
955
|
+
throw new Error("invalid bm_edit payload")
|
|
823
956
|
}
|
|
824
957
|
|
|
825
958
|
return {
|
|
@@ -841,14 +974,14 @@ export class BmClient {
|
|
|
841
974
|
project: this.routedProject(project),
|
|
842
975
|
}
|
|
843
976
|
|
|
844
|
-
const payload = await this.callTool("
|
|
977
|
+
const payload = await this.callTool("bm_delete", args)
|
|
845
978
|
|
|
846
979
|
if (!isRecord(payload)) {
|
|
847
|
-
throw new Error("invalid
|
|
980
|
+
throw new Error("invalid bm_delete response")
|
|
848
981
|
}
|
|
849
982
|
|
|
850
|
-
if (payload.
|
|
851
|
-
throw new Error(`
|
|
983
|
+
if (payload.action !== "deleted") {
|
|
984
|
+
throw new Error(`bm_delete did not delete "${identifier}"`)
|
|
852
985
|
}
|
|
853
986
|
|
|
854
987
|
return {
|
|
@@ -865,21 +998,21 @@ export class BmClient {
|
|
|
865
998
|
): Promise<NoteResult> {
|
|
866
999
|
const args: Record<string, unknown> = {
|
|
867
1000
|
identifier,
|
|
868
|
-
|
|
1001
|
+
destination: `${newFolder.replace(/\/+$/, "")}/`,
|
|
869
1002
|
output_format: "json",
|
|
870
1003
|
project: this.routedProject(project),
|
|
871
1004
|
}
|
|
872
1005
|
|
|
873
|
-
const payload = await this.callTool("
|
|
1006
|
+
const payload = await this.callTool("bm_move", args)
|
|
874
1007
|
|
|
875
1008
|
if (!isRecord(payload)) {
|
|
876
|
-
throw new Error("invalid
|
|
1009
|
+
throw new Error("invalid bm_move response")
|
|
877
1010
|
}
|
|
878
1011
|
|
|
879
|
-
if (payload.
|
|
1012
|
+
if (payload.action !== "moved") {
|
|
880
1013
|
throw new Error(
|
|
881
1014
|
asString(payload.error) ??
|
|
882
|
-
`
|
|
1015
|
+
`bm_move did not move "${identifier}" to "${newFolder}"`,
|
|
883
1016
|
)
|
|
884
1017
|
}
|
|
885
1018
|
|
|
@@ -897,16 +1030,17 @@ export class BmClient {
|
|
|
897
1030
|
project?: string,
|
|
898
1031
|
): Promise<SchemaValidationResult> {
|
|
899
1032
|
const args: Record<string, unknown> = {
|
|
1033
|
+
command: "validate",
|
|
900
1034
|
output_format: "json",
|
|
901
1035
|
project: this.routedProject(project),
|
|
902
1036
|
}
|
|
903
1037
|
if (noteType) args.note_type = noteType
|
|
904
1038
|
if (identifier) args.identifier = identifier
|
|
905
1039
|
|
|
906
|
-
const payload = await this.callTool("
|
|
1040
|
+
const payload = await this.callTool("bm_schema", args)
|
|
907
1041
|
|
|
908
1042
|
if (!isRecord(payload)) {
|
|
909
|
-
throw new Error("invalid
|
|
1043
|
+
throw new Error("invalid bm_schema response")
|
|
910
1044
|
}
|
|
911
1045
|
|
|
912
1046
|
return payload as unknown as SchemaValidationResult
|
|
@@ -918,16 +1052,17 @@ export class BmClient {
|
|
|
918
1052
|
project?: string,
|
|
919
1053
|
): Promise<SchemaInferResult> {
|
|
920
1054
|
const args: Record<string, unknown> = {
|
|
1055
|
+
command: "infer",
|
|
921
1056
|
note_type: noteType,
|
|
922
1057
|
threshold,
|
|
923
1058
|
output_format: "json",
|
|
924
1059
|
project: this.routedProject(project),
|
|
925
1060
|
}
|
|
926
1061
|
|
|
927
|
-
const payload = await this.callTool("
|
|
1062
|
+
const payload = await this.callTool("bm_schema", args)
|
|
928
1063
|
|
|
929
1064
|
if (!isRecord(payload)) {
|
|
930
|
-
throw new Error("invalid
|
|
1065
|
+
throw new Error("invalid bm_schema response")
|
|
931
1066
|
}
|
|
932
1067
|
|
|
933
1068
|
return payload as unknown as SchemaInferResult
|
|
@@ -938,31 +1073,76 @@ export class BmClient {
|
|
|
938
1073
|
project?: string,
|
|
939
1074
|
): Promise<SchemaDiffResult> {
|
|
940
1075
|
const args: Record<string, unknown> = {
|
|
1076
|
+
command: "diff",
|
|
941
1077
|
note_type: noteType,
|
|
942
1078
|
output_format: "json",
|
|
943
1079
|
project: this.routedProject(project),
|
|
944
1080
|
}
|
|
945
1081
|
|
|
946
|
-
const payload = await this.callTool("
|
|
1082
|
+
const payload = await this.callTool("bm_schema", args)
|
|
947
1083
|
|
|
948
1084
|
if (!isRecord(payload)) {
|
|
949
|
-
throw new Error("invalid
|
|
1085
|
+
throw new Error("invalid bm_schema response")
|
|
950
1086
|
}
|
|
951
1087
|
|
|
952
1088
|
return payload as unknown as SchemaDiffResult
|
|
953
1089
|
}
|
|
954
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Append one user/assistant exchange to today's conversation note.
|
|
1093
|
+
*
|
|
1094
|
+
* With a turnKey the capture is idempotent: the entry carries a marker
|
|
1095
|
+
* derived from the key, and a key already present in today's or
|
|
1096
|
+
* yesterday's note is reported as a duplicate instead of written again.
|
|
1097
|
+
* Yesterday is checked because a host retry can cross midnight.
|
|
1098
|
+
*/
|
|
955
1099
|
async indexConversation(
|
|
956
1100
|
userMessage: string,
|
|
957
1101
|
assistantResponse: string,
|
|
958
|
-
|
|
1102
|
+
options: { turnKey?: string } = {},
|
|
1103
|
+
): Promise<ConversationCaptureStatus> {
|
|
1104
|
+
const write = this.conversationWrites.then(() =>
|
|
1105
|
+
this.writeConversationEntry(
|
|
1106
|
+
userMessage,
|
|
1107
|
+
assistantResponse,
|
|
1108
|
+
options.turnKey,
|
|
1109
|
+
),
|
|
1110
|
+
)
|
|
1111
|
+
this.conversationWrites = write.catch(() => undefined)
|
|
1112
|
+
return write
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private async writeConversationEntry(
|
|
1116
|
+
userMessage: string,
|
|
1117
|
+
assistantResponse: string,
|
|
1118
|
+
turnKey: string | undefined,
|
|
1119
|
+
): Promise<ConversationCaptureStatus> {
|
|
959
1120
|
const now = new Date()
|
|
960
|
-
const dateStr = now
|
|
1121
|
+
const dateStr = conversationDate(now)
|
|
961
1122
|
const timeStr = now.toTimeString().slice(0, 5)
|
|
962
1123
|
const title = `conversations-${dateStr}`
|
|
1124
|
+
const marker = turnKey ? conversationTurnMarker(turnKey) : null
|
|
1125
|
+
|
|
1126
|
+
// Trigger: the host supplied an advancement key for this turn.
|
|
1127
|
+
// Why: hosts retry commits after a crash, and a second append would
|
|
1128
|
+
// duplicate the exchange in the note.
|
|
1129
|
+
// Outcome: an already-recorded key short-circuits before any write.
|
|
1130
|
+
if (marker) {
|
|
1131
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
1132
|
+
for (const candidate of [
|
|
1133
|
+
title,
|
|
1134
|
+
`conversations-${conversationDate(yesterday)}`,
|
|
1135
|
+
]) {
|
|
1136
|
+
if (await this.noteContains(candidate, marker)) {
|
|
1137
|
+
log.debug(`conversation turn already recorded in: ${candidate}`)
|
|
1138
|
+
return "duplicate"
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
963
1142
|
|
|
964
1143
|
const entry = [
|
|
965
1144
|
`### ${timeStr}`,
|
|
1145
|
+
...(marker ? [marker] : []),
|
|
966
1146
|
"",
|
|
967
1147
|
"**User:**",
|
|
968
1148
|
userMessage,
|
|
@@ -976,7 +1156,7 @@ export class BmClient {
|
|
|
976
1156
|
try {
|
|
977
1157
|
await this.editNote(title, "append", entry)
|
|
978
1158
|
log.debug(`appended conversation to: ${title}`)
|
|
979
|
-
return
|
|
1159
|
+
return "committed"
|
|
980
1160
|
} catch (err) {
|
|
981
1161
|
if (!isNoteNotFoundError(err)) {
|
|
982
1162
|
log.error(`conversation append failed: ${getErrorMessage(err)}`, err)
|
|
@@ -1001,11 +1181,21 @@ export class BmClient {
|
|
|
1001
1181
|
entry,
|
|
1002
1182
|
].join("\n")
|
|
1003
1183
|
|
|
1184
|
+
await this.writeNote(title, content, "conversations")
|
|
1185
|
+
log.debug(`created conversation note: ${title}`)
|
|
1186
|
+
return "committed"
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
private async noteContains(
|
|
1190
|
+
identifier: string,
|
|
1191
|
+
text: string,
|
|
1192
|
+
): Promise<boolean> {
|
|
1004
1193
|
try {
|
|
1005
|
-
await this.
|
|
1006
|
-
|
|
1194
|
+
const note = await this.readNote(identifier)
|
|
1195
|
+
return note.content.includes(text)
|
|
1007
1196
|
} catch (err) {
|
|
1008
|
-
|
|
1197
|
+
if (isNoteNotFoundError(err)) return false
|
|
1198
|
+
throw err
|
|
1009
1199
|
}
|
|
1010
1200
|
}
|
|
1011
1201
|
|