@basicmemory/openclaw-basic-memory 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -7
- package/bm-client.ts +107 -7
- package/dist/bm-client.d.ts +33 -1
- package/dist/bm-client.js +77 -6
- package/dist/index.js +9 -1
- package/dist/tools/edit-note.js +8 -1
- package/dist/tools/list-memory-projects.js +23 -5
- package/dist/tools/list-workspaces.js +3 -1
- package/dist/tools/memory-provider.js +2 -1
- package/dist/tools/read-note.js +18 -3
- package/dist/tools/search-notes.js +3 -1
- package/dist/tools/write-note.js +65 -5
- package/index.ts +9 -1
- package/openclaw.plugin.json +2 -0
- package/package.json +1 -1
- package/scripts/fetch-skills.ts +87 -36
- package/scripts/setup-bm.sh +4 -1
- package/skills/manifest.json +31 -16
- package/skills/memory-agents/SKILL.md +170 -0
- package/skills/memory-capture/SKILL.md +7 -5
- package/skills/memory-comark/SKILL.md +204 -0
- package/skills/memory-comark/references/components.md +740 -0
- package/skills/memory-continue/SKILL.md +11 -1
- package/skills/memory-curate/SKILL.md +32 -39
- package/skills/memory-defrag/SKILL.md +106 -57
- package/skills/memory-ingest/SKILL.md +14 -3
- package/skills/memory-lifecycle/SKILL.md +21 -21
- package/skills/memory-literary-analysis/SKILL.md +258 -28
- package/skills/memory-metadata-search/SKILL.md +29 -4
- package/skills/memory-notes/SKILL.md +69 -17
- package/skills/memory-okf/SKILL.md +135 -0
- package/skills/memory-onboarding/SKILL.md +2 -2
- package/skills/memory-onboarding/references/assistant-setup.md +8 -0
- package/skills/memory-onboarding/references/conventions.md +3 -3
- package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
- package/skills/memory-onboarding/references/schema-guide.md +3 -3
- package/skills/memory-quest/SKILL.md +129 -0
- package/skills/memory-quest/agents/openai.yaml +9 -0
- package/skills/memory-reflect/SKILL.md +125 -44
- package/skills/memory-research/SKILL.md +9 -15
- package/skills/memory-schema/SKILL.md +29 -21
- package/skills/memory-tasks/SKILL.md +36 -10
- package/tools/edit-note.ts +15 -1
- package/tools/list-memory-projects.ts +24 -5
- package/tools/list-workspaces.ts +3 -1
- package/tools/memory-provider.ts +2 -1
- package/tools/read-note.ts +22 -3
- package/tools/search-notes.ts +3 -1
- package/tools/write-note.ts +94 -4
- package/skills/memory-ci-capture/SKILL.md +0 -56
package/README.md
CHANGED
|
@@ -161,8 +161,8 @@ All tools accept an optional `project` parameter for cross-project operations.
|
|
|
161
161
|
| `memory_get` | Read a specific note by title or path |
|
|
162
162
|
| `search_notes` | Search the knowledge graph directly |
|
|
163
163
|
| `read_note` | Read a note by title, permalink, or `memory://` URL |
|
|
164
|
-
| `write_note` | Create or
|
|
165
|
-
| `edit_note` | Append, prepend,
|
|
164
|
+
| `write_note` | Create or overwrite a note with optional tags, note type, and custom frontmatter metadata |
|
|
165
|
+
| `edit_note` | Append, prepend, replace content, and merge custom frontmatter metadata |
|
|
166
166
|
| `delete_note` | Delete a note |
|
|
167
167
|
| `move_note` | Move a note to a different folder |
|
|
168
168
|
| `build_context` | Navigate the knowledge graph — follow relations and connections |
|
|
@@ -172,6 +172,22 @@ All tools accept an optional `project` parameter for cross-project operations.
|
|
|
172
172
|
| `schema_infer` | Analyze notes and suggest a schema |
|
|
173
173
|
| `schema_diff` | Detect drift between schema and actual usage |
|
|
174
174
|
|
|
175
|
+
Use the dedicated frontmatter parameters when creating structured notes:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
write_note(
|
|
179
|
+
title="auth-middleware-rollout",
|
|
180
|
+
folder="tasks",
|
|
181
|
+
note_type="Task",
|
|
182
|
+
tags=["auth", "rollout"],
|
|
183
|
+
metadata={"status": "active", "current_step": 2},
|
|
184
|
+
content="""## Context
|
|
185
|
+
Rolling JWT middleware to all API routes."""
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`edit_note` accepts `metadata` alongside any edit operation, so content and frontmatter state can be updated in one call. Metadata keys supplied by the caller are merged into existing frontmatter; unrelated fields remain unchanged.
|
|
190
|
+
|
|
175
191
|
## Slash commands
|
|
176
192
|
|
|
177
193
|
| Command | Description |
|
|
@@ -196,14 +212,17 @@ openclaw basic-memory status
|
|
|
196
212
|
|
|
197
213
|
## Bundled skills
|
|
198
214
|
|
|
199
|
-
|
|
215
|
+
These skills ship with the plugin — no installation needed:
|
|
200
216
|
|
|
201
|
-
- **memory-
|
|
217
|
+
- **memory-comark** — charts, tables, boards, and live views in notes with `::bm-*` components
|
|
218
|
+
- **memory-defrag** — cleanup and reorganization of a project's notes
|
|
202
219
|
- **memory-ingest** — import existing material into Basic Memory
|
|
203
220
|
- **memory-lifecycle** — manage note/project lifecycle workflows
|
|
204
221
|
- **memory-literary-analysis** — analyze texts and reading notes
|
|
205
222
|
- **memory-metadata-search** — query notes by frontmatter fields
|
|
206
223
|
- **memory-notes** — guidance for writing well-structured notes
|
|
224
|
+
- **memory-okf** — make a project ready for OKF export
|
|
225
|
+
- **memory-onboarding** — guided setup of a new knowledge system
|
|
207
226
|
- **memory-reflect** — periodic consolidation of recent notes into durable memory
|
|
208
227
|
- **memory-research** — research synthesis into durable notes
|
|
209
228
|
- **memory-schema** — schema lifecycle (infer, create, validate, diff)
|
|
@@ -253,14 +272,12 @@ Cloud extends local-first — still plain Markdown, still yours. [Start a free t
|
|
|
253
272
|
|
|
254
273
|
## Troubleshooting
|
|
255
274
|
|
|
256
|
-
**`bm` not found** — Install uv, then restart the gateway. Or install manually: `uv tool install basic-memory`
|
|
275
|
+
**`bm` not found** — Install uv, then restart the gateway. Or install manually: `uv tool install basic-memory --prerelease=allow` (the flag is required — basic-memory depends on a FastMCP pre-release, and without it uv silently installs an older release)
|
|
257
276
|
|
|
258
277
|
**Search returns nothing** — Check that Basic Memory connected (look for `connected to BM` in logs). Verify files exist in the project directory.
|
|
259
278
|
|
|
260
279
|
**Jiti cache issues** — `rm -rf /tmp/jiti/ "$TMPDIR/jiti/"` then restart the gateway.
|
|
261
280
|
|
|
262
|
-
**Disable semantic search** — Set `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=false` to fall back to full-text only.
|
|
263
|
-
|
|
264
281
|
## More
|
|
265
282
|
|
|
266
283
|
- [Memory + Task Flow](./MEMORY_TASK_FLOW.md) — practical runbook
|
package/bm-client.ts
CHANGED
|
@@ -14,6 +14,40 @@ export class NoteAlreadyExistsError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* write_note refused a conditional overwrite: the note is no longer the
|
|
19
|
+
* revision named by expected_checksum. Nothing was written; currentChecksum is
|
|
20
|
+
* the revision to re-read and retry against (null once no note owns the path).
|
|
21
|
+
*/
|
|
22
|
+
export class NoteRevisionConflictError extends Error {
|
|
23
|
+
readonly permalink: string | null
|
|
24
|
+
readonly currentChecksum: string | null
|
|
25
|
+
constructor(
|
|
26
|
+
title: string,
|
|
27
|
+
permalink: string | null,
|
|
28
|
+
currentChecksum: string | null,
|
|
29
|
+
) {
|
|
30
|
+
super(`Note revision conflict: "${title}" changed since it was read`)
|
|
31
|
+
this.name = "NoteRevisionConflictError"
|
|
32
|
+
this.permalink = permalink
|
|
33
|
+
this.currentChecksum = currentChecksum
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A BM MCP tool returned isError; toolMessage is the tool's own error text. */
|
|
38
|
+
export class BmToolError extends Error {
|
|
39
|
+
readonly toolName: string
|
|
40
|
+
readonly toolMessage: string
|
|
41
|
+
constructor(toolName: string, toolMessage: string) {
|
|
42
|
+
super(
|
|
43
|
+
`BM MCP tool ${toolName} failed${toolMessage ? `: ${toolMessage}` : ""}`,
|
|
44
|
+
)
|
|
45
|
+
this.name = "BmToolError"
|
|
46
|
+
this.toolName = toolName
|
|
47
|
+
this.toolMessage = toolMessage
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
const REQUIRED_TOOLS = [
|
|
18
52
|
"search_notes",
|
|
19
53
|
"read_note",
|
|
@@ -59,12 +93,25 @@ export interface EditNoteResult {
|
|
|
59
93
|
|
|
60
94
|
interface ReadNoteOptions {
|
|
61
95
|
includeFrontmatter?: boolean
|
|
96
|
+
/** Project external_id; routes to that exact project instead of by name. */
|
|
97
|
+
projectId?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface WriteNoteOptions {
|
|
101
|
+
metadata?: Record<string, unknown>
|
|
102
|
+
tags?: string[] | string
|
|
103
|
+
noteType?: string
|
|
104
|
+
/** Project external_id; routes to that exact project instead of by name. */
|
|
105
|
+
projectId?: string
|
|
106
|
+
/** Checksum of the revision read; overwrite only while it is still current. */
|
|
107
|
+
expectedChecksum?: string
|
|
62
108
|
}
|
|
63
109
|
|
|
64
110
|
interface EditNoteOptions {
|
|
65
111
|
find_text?: string
|
|
66
112
|
section?: string
|
|
67
113
|
expected_replacements?: number
|
|
114
|
+
metadata?: Record<string, unknown>
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
export interface ContextResult {
|
|
@@ -95,6 +142,9 @@ export interface RecentResult {
|
|
|
95
142
|
export interface ProjectListResult {
|
|
96
143
|
name: string
|
|
97
144
|
path: string
|
|
145
|
+
external_id?: string | null
|
|
146
|
+
source?: string | null
|
|
147
|
+
qualified_name?: string | null
|
|
98
148
|
display_name?: string | null
|
|
99
149
|
is_private?: boolean
|
|
100
150
|
is_default?: boolean
|
|
@@ -214,6 +264,19 @@ function asString(value: unknown): string | null {
|
|
|
214
264
|
return typeof value === "string" ? value : null
|
|
215
265
|
}
|
|
216
266
|
|
|
267
|
+
// In JSON mode BM reports a refused write as a tool error whose text is the
|
|
268
|
+
// refusal payload itself ({"error": "NOTE_ALREADY_EXISTS", ...}). Any other
|
|
269
|
+
// error text (plain-text failures, older servers) is not a refusal payload.
|
|
270
|
+
function parseRefusalPayload(text: string): Record<string, unknown> | null {
|
|
271
|
+
let parsed: unknown
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(text)
|
|
274
|
+
} catch {
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
return isRecord(parsed) && typeof parsed.error === "string" ? parsed : null
|
|
278
|
+
}
|
|
279
|
+
|
|
217
280
|
function projectMatchesWorkspace(
|
|
218
281
|
project: ProjectListResult,
|
|
219
282
|
workspace: string,
|
|
@@ -432,10 +495,7 @@ export class BmClient {
|
|
|
432
495
|
})
|
|
433
496
|
|
|
434
497
|
if (isRecord(result) && result.isError === true) {
|
|
435
|
-
|
|
436
|
-
throw new Error(
|
|
437
|
-
`BM MCP tool ${name} failed${message ? `: ${message}` : ""}`,
|
|
438
|
-
)
|
|
498
|
+
throw new BmToolError(name, extractTextFromContent(result.content))
|
|
439
499
|
}
|
|
440
500
|
|
|
441
501
|
return result
|
|
@@ -486,6 +546,17 @@ export class BmClient {
|
|
|
486
546
|
return project ?? this.project
|
|
487
547
|
}
|
|
488
548
|
|
|
549
|
+
// project_id names one exact project, so it is sent alone: pairing it with
|
|
550
|
+
// a name (or the plugin's default project) would make routing depend on
|
|
551
|
+
// server-side precedence rather than on what the caller asked for.
|
|
552
|
+
private routing(
|
|
553
|
+
project?: string,
|
|
554
|
+
projectId?: string,
|
|
555
|
+
): { project: string } | { project_id: string } {
|
|
556
|
+
if (projectId !== undefined) return { project_id: projectId }
|
|
557
|
+
return { project: this.routedProject(project) }
|
|
558
|
+
}
|
|
559
|
+
|
|
489
560
|
async ensureProject(projectPath: string): Promise<void> {
|
|
490
561
|
const payload = await this.callTool("create_memory_project", {
|
|
491
562
|
project_name: this.project,
|
|
@@ -572,7 +643,7 @@ export class BmClient {
|
|
|
572
643
|
identifier,
|
|
573
644
|
include_frontmatter: options.includeFrontmatter === true,
|
|
574
645
|
output_format: "json",
|
|
575
|
-
|
|
646
|
+
...this.routing(project, options.projectId),
|
|
576
647
|
}
|
|
577
648
|
|
|
578
649
|
const payload = await this.callTool("read_note", args)
|
|
@@ -596,6 +667,7 @@ export class BmClient {
|
|
|
596
667
|
content,
|
|
597
668
|
file_path: filePath,
|
|
598
669
|
frontmatter: isRecord(payload.frontmatter) ? payload.frontmatter : null,
|
|
670
|
+
checksum: asString(payload.checksum),
|
|
599
671
|
}
|
|
600
672
|
}
|
|
601
673
|
|
|
@@ -605,22 +677,49 @@ export class BmClient {
|
|
|
605
677
|
folder: string,
|
|
606
678
|
project?: string,
|
|
607
679
|
overwrite?: boolean,
|
|
680
|
+
options: WriteNoteOptions = {},
|
|
608
681
|
): Promise<NoteResult> {
|
|
609
682
|
const args: Record<string, unknown> = {
|
|
610
683
|
title,
|
|
611
684
|
content,
|
|
612
685
|
directory: folder,
|
|
613
686
|
output_format: "json",
|
|
614
|
-
|
|
687
|
+
...this.routing(project, options.projectId),
|
|
615
688
|
}
|
|
616
689
|
if (overwrite !== undefined) args.overwrite = overwrite
|
|
690
|
+
if (options.metadata !== undefined) args.metadata = options.metadata
|
|
691
|
+
if (options.tags !== undefined) args.tags = options.tags
|
|
692
|
+
if (options.noteType !== undefined) args.note_type = options.noteType
|
|
693
|
+
if (options.expectedChecksum !== undefined)
|
|
694
|
+
args.expected_checksum = options.expectedChecksum
|
|
617
695
|
|
|
618
|
-
|
|
696
|
+
let payload: unknown
|
|
697
|
+
try {
|
|
698
|
+
payload = await this.callTool("write_note", args)
|
|
699
|
+
} catch (err) {
|
|
700
|
+
const refusal =
|
|
701
|
+
err instanceof BmToolError ? parseRefusalPayload(err.toolMessage) : null
|
|
702
|
+
if (refusal?.error === "NOTE_ALREADY_EXISTS") {
|
|
703
|
+
throw new NoteAlreadyExistsError(
|
|
704
|
+
asString(refusal.title) ?? title,
|
|
705
|
+
asString(refusal.permalink) ?? "",
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
if (refusal?.error === "NOTE_REVISION_CONFLICT") {
|
|
709
|
+
throw new NoteRevisionConflictError(
|
|
710
|
+
asString(refusal.title) ?? title,
|
|
711
|
+
asString(refusal.permalink),
|
|
712
|
+
asString(refusal.currentChecksum),
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
throw err
|
|
716
|
+
}
|
|
619
717
|
|
|
620
718
|
if (!isRecord(payload)) {
|
|
621
719
|
throw new Error("invalid write_note response")
|
|
622
720
|
}
|
|
623
721
|
|
|
722
|
+
// Older servers returned the refusal as a successful payload.
|
|
624
723
|
if (payload.error === "NOTE_ALREADY_EXISTS") {
|
|
625
724
|
throw new NoteAlreadyExistsError(
|
|
626
725
|
asString(payload.title) ?? title,
|
|
@@ -707,6 +806,7 @@ export class BmClient {
|
|
|
707
806
|
if (options.section) args.section = options.section
|
|
708
807
|
if (options.expected_replacements != null)
|
|
709
808
|
args.expected_replacements = options.expected_replacements
|
|
809
|
+
if (options.metadata !== undefined) args.metadata = options.metadata
|
|
710
810
|
|
|
711
811
|
const payload = await this.callTool("edit_note", args)
|
|
712
812
|
|
package/dist/bm-client.d.ts
CHANGED
|
@@ -2,6 +2,22 @@ export declare class NoteAlreadyExistsError extends Error {
|
|
|
2
2
|
readonly permalink: string;
|
|
3
3
|
constructor(title: string, permalink: string);
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* write_note refused a conditional overwrite: the note is no longer the
|
|
7
|
+
* revision named by expected_checksum. Nothing was written; currentChecksum is
|
|
8
|
+
* the revision to re-read and retry against (null once no note owns the path).
|
|
9
|
+
*/
|
|
10
|
+
export declare class NoteRevisionConflictError extends Error {
|
|
11
|
+
readonly permalink: string | null;
|
|
12
|
+
readonly currentChecksum: string | null;
|
|
13
|
+
constructor(title: string, permalink: string | null, currentChecksum: string | null);
|
|
14
|
+
}
|
|
15
|
+
/** A BM MCP tool returned isError; toolMessage is the tool's own error text. */
|
|
16
|
+
export declare class BmToolError extends Error {
|
|
17
|
+
readonly toolName: string;
|
|
18
|
+
readonly toolMessage: string;
|
|
19
|
+
constructor(toolName: string, toolMessage: string);
|
|
20
|
+
}
|
|
5
21
|
export interface SearchResult {
|
|
6
22
|
title: string;
|
|
7
23
|
permalink: string;
|
|
@@ -27,11 +43,23 @@ export interface EditNoteResult {
|
|
|
27
43
|
}
|
|
28
44
|
interface ReadNoteOptions {
|
|
29
45
|
includeFrontmatter?: boolean;
|
|
46
|
+
/** Project external_id; routes to that exact project instead of by name. */
|
|
47
|
+
projectId?: string;
|
|
48
|
+
}
|
|
49
|
+
interface WriteNoteOptions {
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
tags?: string[] | string;
|
|
52
|
+
noteType?: string;
|
|
53
|
+
/** Project external_id; routes to that exact project instead of by name. */
|
|
54
|
+
projectId?: string;
|
|
55
|
+
/** Checksum of the revision read; overwrite only while it is still current. */
|
|
56
|
+
expectedChecksum?: string;
|
|
30
57
|
}
|
|
31
58
|
interface EditNoteOptions {
|
|
32
59
|
find_text?: string;
|
|
33
60
|
section?: string;
|
|
34
61
|
expected_replacements?: number;
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
35
63
|
}
|
|
36
64
|
export interface ContextResult {
|
|
37
65
|
results: Array<{
|
|
@@ -59,6 +87,9 @@ export interface RecentResult {
|
|
|
59
87
|
export interface ProjectListResult {
|
|
60
88
|
name: string;
|
|
61
89
|
path: string;
|
|
90
|
+
external_id?: string | null;
|
|
91
|
+
source?: string | null;
|
|
92
|
+
qualified_name?: string | null;
|
|
62
93
|
display_name?: string | null;
|
|
63
94
|
is_private?: boolean;
|
|
64
95
|
is_default?: boolean;
|
|
@@ -149,6 +180,7 @@ export declare class BmClient {
|
|
|
149
180
|
private callToolRaw;
|
|
150
181
|
private callTool;
|
|
151
182
|
private routedProject;
|
|
183
|
+
private routing;
|
|
152
184
|
ensureProject(projectPath: string): Promise<void>;
|
|
153
185
|
listWorkspaces(): Promise<WorkspaceResult[]>;
|
|
154
186
|
listProjects(workspace?: string): Promise<ProjectListResult[]>;
|
|
@@ -160,7 +192,7 @@ export declare class BmClient {
|
|
|
160
192
|
entity_types?: string[];
|
|
161
193
|
}): Promise<SearchResult[]>;
|
|
162
194
|
readNote(identifier: string, options?: ReadNoteOptions, project?: string): Promise<NoteResult>;
|
|
163
|
-
writeNote(title: string, content: string, folder: string, project?: string, overwrite?: boolean): Promise<NoteResult>;
|
|
195
|
+
writeNote(title: string, content: string, folder: string, project?: string, overwrite?: boolean, options?: WriteNoteOptions): Promise<NoteResult>;
|
|
164
196
|
buildContext(url: string, depth?: number, project?: string): Promise<ContextResult>;
|
|
165
197
|
recentActivity(timeframe?: string, project?: string): Promise<RecentResult[]>;
|
|
166
198
|
editNote(identifier: string, operation: "append" | "prepend" | "find_replace" | "replace_section", content: string, options?: EditNoteOptions, project?: string): Promise<EditNoteResult>;
|
package/dist/bm-client.js
CHANGED
|
@@ -11,6 +11,32 @@ export class NoteAlreadyExistsError extends Error {
|
|
|
11
11
|
this.permalink = permalink;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* write_note refused a conditional overwrite: the note is no longer the
|
|
16
|
+
* revision named by expected_checksum. Nothing was written; currentChecksum is
|
|
17
|
+
* the revision to re-read and retry against (null once no note owns the path).
|
|
18
|
+
*/
|
|
19
|
+
export class NoteRevisionConflictError extends Error {
|
|
20
|
+
permalink;
|
|
21
|
+
currentChecksum;
|
|
22
|
+
constructor(title, permalink, currentChecksum) {
|
|
23
|
+
super(`Note revision conflict: "${title}" changed since it was read`);
|
|
24
|
+
this.name = "NoteRevisionConflictError";
|
|
25
|
+
this.permalink = permalink;
|
|
26
|
+
this.currentChecksum = currentChecksum;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** A BM MCP tool returned isError; toolMessage is the tool's own error text. */
|
|
30
|
+
export class BmToolError extends Error {
|
|
31
|
+
toolName;
|
|
32
|
+
toolMessage;
|
|
33
|
+
constructor(toolName, toolMessage) {
|
|
34
|
+
super(`BM MCP tool ${toolName} failed${toolMessage ? `: ${toolMessage}` : ""}`);
|
|
35
|
+
this.name = "BmToolError";
|
|
36
|
+
this.toolName = toolName;
|
|
37
|
+
this.toolMessage = toolMessage;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
14
40
|
const REQUIRED_TOOLS = [
|
|
15
41
|
"search_notes",
|
|
16
42
|
"read_note",
|
|
@@ -65,6 +91,19 @@ function isNoteNotFoundError(err) {
|
|
|
65
91
|
function asString(value) {
|
|
66
92
|
return typeof value === "string" ? value : null;
|
|
67
93
|
}
|
|
94
|
+
// In JSON mode BM reports a refused write as a tool error whose text is the
|
|
95
|
+
// refusal payload itself ({"error": "NOTE_ALREADY_EXISTS", ...}). Any other
|
|
96
|
+
// error text (plain-text failures, older servers) is not a refusal payload.
|
|
97
|
+
function parseRefusalPayload(text) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return isRecord(parsed) && typeof parsed.error === "string" ? parsed : null;
|
|
106
|
+
}
|
|
68
107
|
function projectMatchesWorkspace(project, workspace) {
|
|
69
108
|
const requested = workspace.trim().toLowerCase();
|
|
70
109
|
if (!requested)
|
|
@@ -235,8 +274,7 @@ export class BmClient {
|
|
|
235
274
|
arguments: args,
|
|
236
275
|
});
|
|
237
276
|
if (isRecord(result) && result.isError === true) {
|
|
238
|
-
|
|
239
|
-
throw new Error(`BM MCP tool ${name} failed${message ? `: ${message}` : ""}`);
|
|
277
|
+
throw new BmToolError(name, extractTextFromContent(result.content));
|
|
240
278
|
}
|
|
241
279
|
return result;
|
|
242
280
|
}
|
|
@@ -272,6 +310,14 @@ export class BmClient {
|
|
|
272
310
|
routedProject(project) {
|
|
273
311
|
return project ?? this.project;
|
|
274
312
|
}
|
|
313
|
+
// project_id names one exact project, so it is sent alone: pairing it with
|
|
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.
|
|
316
|
+
routing(project, projectId) {
|
|
317
|
+
if (projectId !== undefined)
|
|
318
|
+
return { project_id: projectId };
|
|
319
|
+
return { project: this.routedProject(project) };
|
|
320
|
+
}
|
|
275
321
|
async ensureProject(projectPath) {
|
|
276
322
|
const payload = await this.callTool("create_memory_project", {
|
|
277
323
|
project_name: this.project,
|
|
@@ -335,7 +381,7 @@ export class BmClient {
|
|
|
335
381
|
identifier,
|
|
336
382
|
include_frontmatter: options.includeFrontmatter === true,
|
|
337
383
|
output_format: "json",
|
|
338
|
-
|
|
384
|
+
...this.routing(project, options.projectId),
|
|
339
385
|
};
|
|
340
386
|
const payload = await this.callTool("read_note", args);
|
|
341
387
|
if (!isRecord(payload)) {
|
|
@@ -354,22 +400,45 @@ export class BmClient {
|
|
|
354
400
|
content,
|
|
355
401
|
file_path: filePath,
|
|
356
402
|
frontmatter: isRecord(payload.frontmatter) ? payload.frontmatter : null,
|
|
403
|
+
checksum: asString(payload.checksum),
|
|
357
404
|
};
|
|
358
405
|
}
|
|
359
|
-
async writeNote(title, content, folder, project, overwrite) {
|
|
406
|
+
async writeNote(title, content, folder, project, overwrite, options = {}) {
|
|
360
407
|
const args = {
|
|
361
408
|
title,
|
|
362
409
|
content,
|
|
363
410
|
directory: folder,
|
|
364
411
|
output_format: "json",
|
|
365
|
-
|
|
412
|
+
...this.routing(project, options.projectId),
|
|
366
413
|
};
|
|
367
414
|
if (overwrite !== undefined)
|
|
368
415
|
args.overwrite = overwrite;
|
|
369
|
-
|
|
416
|
+
if (options.metadata !== undefined)
|
|
417
|
+
args.metadata = options.metadata;
|
|
418
|
+
if (options.tags !== undefined)
|
|
419
|
+
args.tags = options.tags;
|
|
420
|
+
if (options.noteType !== undefined)
|
|
421
|
+
args.note_type = options.noteType;
|
|
422
|
+
if (options.expectedChecksum !== undefined)
|
|
423
|
+
args.expected_checksum = options.expectedChecksum;
|
|
424
|
+
let payload;
|
|
425
|
+
try {
|
|
426
|
+
payload = await this.callTool("write_note", args);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
const refusal = err instanceof BmToolError ? parseRefusalPayload(err.toolMessage) : null;
|
|
430
|
+
if (refusal?.error === "NOTE_ALREADY_EXISTS") {
|
|
431
|
+
throw new NoteAlreadyExistsError(asString(refusal.title) ?? title, asString(refusal.permalink) ?? "");
|
|
432
|
+
}
|
|
433
|
+
if (refusal?.error === "NOTE_REVISION_CONFLICT") {
|
|
434
|
+
throw new NoteRevisionConflictError(asString(refusal.title) ?? title, asString(refusal.permalink), asString(refusal.currentChecksum));
|
|
435
|
+
}
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
370
438
|
if (!isRecord(payload)) {
|
|
371
439
|
throw new Error("invalid write_note response");
|
|
372
440
|
}
|
|
441
|
+
// Older servers returned the refusal as a successful payload.
|
|
373
442
|
if (payload.error === "NOTE_ALREADY_EXISTS") {
|
|
374
443
|
throw new NoteAlreadyExistsError(asString(payload.title) ?? title, asString(payload.permalink) ?? "");
|
|
375
444
|
}
|
|
@@ -429,6 +498,8 @@ export class BmClient {
|
|
|
429
498
|
args.section = options.section;
|
|
430
499
|
if (options.expected_replacements != null)
|
|
431
500
|
args.expected_replacements = options.expected_replacements;
|
|
501
|
+
if (options.metadata !== undefined)
|
|
502
|
+
args.metadata = options.metadata;
|
|
432
503
|
const payload = await this.callTool("edit_note", args);
|
|
433
504
|
if (!isRecord(payload)) {
|
|
434
505
|
throw new Error("invalid edit_note response");
|
package/dist/index.js
CHANGED
|
@@ -77,7 +77,15 @@ export default definePluginEntry({
|
|
|
77
77
|
throw new Error("uv not found");
|
|
78
78
|
}
|
|
79
79
|
log.info("installing basic-memory via uv (this may take a minute)...");
|
|
80
|
-
|
|
80
|
+
// basic-memory pins a FastMCP pre-release; without this flag uv
|
|
81
|
+
// silently installs the last release that had none (#1338).
|
|
82
|
+
const result = execFileSync("uv", [
|
|
83
|
+
"tool",
|
|
84
|
+
"install",
|
|
85
|
+
"basic-memory",
|
|
86
|
+
"--prerelease=allow",
|
|
87
|
+
"--force",
|
|
88
|
+
], {
|
|
81
89
|
encoding: "utf-8",
|
|
82
90
|
timeout: 120_000,
|
|
83
91
|
stdio: ["ignore", "pipe", "pipe"],
|
package/dist/tools/edit-note.js
CHANGED
|
@@ -6,7 +6,9 @@ export function registerEditTool(api, client) {
|
|
|
6
6
|
label: "Edit Note",
|
|
7
7
|
description: "Incrementally edit an existing note in the Basic Memory knowledge graph. " +
|
|
8
8
|
"Supports append, prepend, find/replace, and section replacement " +
|
|
9
|
-
"without rewriting the entire note."
|
|
9
|
+
"without rewriting the entire note. The result returns the note's " +
|
|
10
|
+
"permalink and file_path: cite those exact values when confirming " +
|
|
11
|
+
"the edit — never report a save that no tool call performed.",
|
|
10
12
|
parameters: Type.Object({
|
|
11
13
|
identifier: Type.String({
|
|
12
14
|
description: "Note title, permalink, or memory:// URL to edit",
|
|
@@ -35,6 +37,10 @@ export function registerEditTool(api, client) {
|
|
|
35
37
|
project: Type.Optional(Type.String({
|
|
36
38
|
description: "Target project name (defaults to current project)",
|
|
37
39
|
})),
|
|
40
|
+
metadata: Type.Optional(Type.Object({}, {
|
|
41
|
+
additionalProperties: true,
|
|
42
|
+
description: "Frontmatter fields to merge independently of the edit operation. Nested objects are supported.",
|
|
43
|
+
})),
|
|
38
44
|
}),
|
|
39
45
|
async execute(_toolCallId, params) {
|
|
40
46
|
log.debug(`edit_note: id="${params.identifier}" op=${params.operation}`);
|
|
@@ -43,6 +49,7 @@ export function registerEditTool(api, client) {
|
|
|
43
49
|
find_text: params.find_text,
|
|
44
50
|
section: params.section,
|
|
45
51
|
expected_replacements: params.expected_replacements,
|
|
52
|
+
metadata: params.metadata,
|
|
46
53
|
}, params.project);
|
|
47
54
|
return {
|
|
48
55
|
content: [
|
|
@@ -3,6 +3,9 @@ import { log } from "../logger.js";
|
|
|
3
3
|
function normalizeProject(project) {
|
|
4
4
|
return {
|
|
5
5
|
name: project.name,
|
|
6
|
+
external_id: project.external_id ?? null,
|
|
7
|
+
qualified_name: project.qualified_name ?? null,
|
|
8
|
+
source: project.source ?? null,
|
|
6
9
|
path: project.path,
|
|
7
10
|
display_name: project.display_name ?? null,
|
|
8
11
|
is_private: project.is_private === true,
|
|
@@ -17,7 +20,12 @@ export function registerProjectListTool(api, client) {
|
|
|
17
20
|
api.registerTool({
|
|
18
21
|
name: "list_memory_projects",
|
|
19
22
|
label: "List Projects",
|
|
20
|
-
description: "List
|
|
23
|
+
description: "List Basic Memory projects accessible to this agent, optionally filtered by " +
|
|
24
|
+
"workspace. Returns each project's name, external_id, source (local, cloud, " +
|
|
25
|
+
"or local+cloud), path, default flag, workspace, and qualified name. " +
|
|
26
|
+
"Call this before passing `project` to another tool when the user names a " +
|
|
27
|
+
"project that isn't the default. Pass a project's external_id as " +
|
|
28
|
+
"`project_id` to route to that exact project.",
|
|
21
29
|
parameters: Type.Object({
|
|
22
30
|
workspace: Type.Optional(Type.String({
|
|
23
31
|
description: "Filter by workspace name, slug, or tenant_id",
|
|
@@ -45,10 +53,20 @@ export function registerProjectListTool(api, client) {
|
|
|
45
53
|
const text = normalized
|
|
46
54
|
.map((project, idx) => {
|
|
47
55
|
const defaultSuffix = project.is_default ? " (default)" : "";
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
56
|
+
const lines = [
|
|
57
|
+
`${idx + 1}. **${project.name}**${defaultSuffix}`,
|
|
58
|
+
project.external_id ? ` ID: ${project.external_id}` : null,
|
|
59
|
+
project.source ? ` Source: ${project.source}` : null,
|
|
60
|
+
` Path: ${project.path}`,
|
|
61
|
+
` Private: ${project.is_private}`,
|
|
62
|
+
project.display_name
|
|
63
|
+
? ` Display Name: ${project.display_name}`
|
|
64
|
+
: null,
|
|
65
|
+
project.qualified_name
|
|
66
|
+
? ` Qualified Name: ${project.qualified_name}`
|
|
67
|
+
: null,
|
|
68
|
+
];
|
|
69
|
+
return lines.filter((line) => line !== null).join("\n");
|
|
52
70
|
})
|
|
53
71
|
.join("\n\n");
|
|
54
72
|
return {
|
|
@@ -9,7 +9,9 @@ export function registerWorkspaceListTool(api, client) {
|
|
|
9
9
|
api.registerTool({
|
|
10
10
|
name: "list_workspaces",
|
|
11
11
|
label: "List Workspaces",
|
|
12
|
-
description: "List all Basic Memory workspaces (personal and organization) accessible to this user"
|
|
12
|
+
description: "List all Basic Memory workspaces (personal and organization) accessible to this user. " +
|
|
13
|
+
"Use it to find the workspace to pass to list_memory_projects when the same " +
|
|
14
|
+
"project name may exist in more than one workspace.",
|
|
13
15
|
parameters: Type.Object({}),
|
|
14
16
|
async execute(_toolCallId, _params) {
|
|
15
17
|
log.debug("list_workspaces");
|
|
@@ -140,7 +140,8 @@ export function registerMemoryProvider(api, client, cfg) {
|
|
|
140
140
|
label: "Memory Search",
|
|
141
141
|
description: "Search across all memory sources: MEMORY.md (working memory), " +
|
|
142
142
|
"Basic Memory knowledge graph (long-term archive), and active tasks. " +
|
|
143
|
-
"Returns composited results from all sources."
|
|
143
|
+
"Returns composited results from all sources. Use it for broad recall; use " +
|
|
144
|
+
"search_notes when you need metadata, tag, or status filters or a specific project.",
|
|
144
145
|
parameters: Type.Object({
|
|
145
146
|
query: Type.String({
|
|
146
147
|
description: "Search query — natural language or keywords",
|
package/dist/tools/read-note.js
CHANGED
|
@@ -5,7 +5,9 @@ export function registerReadTool(api, client) {
|
|
|
5
5
|
name: "read_note",
|
|
6
6
|
label: "Read Note",
|
|
7
7
|
description: "Read a specific note from the Basic Memory knowledge graph by title or permalink. " +
|
|
8
|
-
"Returns the full note content including observations and relations
|
|
8
|
+
"Returns the full note content including observations and relations, " +
|
|
9
|
+
"followed by the note's checksum: pass it to write_note as " +
|
|
10
|
+
"expected_checksum (with overwrite=true) to replace only this revision.",
|
|
9
11
|
parameters: Type.Object({
|
|
10
12
|
identifier: Type.String({
|
|
11
13
|
description: "Note title, permalink, or memory:// URL to read",
|
|
@@ -16,16 +18,28 @@ export function registerReadTool(api, client) {
|
|
|
16
18
|
project: Type.Optional(Type.String({
|
|
17
19
|
description: "Target project name (defaults to current project)",
|
|
18
20
|
})),
|
|
21
|
+
project_id: Type.Optional(Type.String({
|
|
22
|
+
description: "Target project external_id from list_memory_projects. Routes to " +
|
|
23
|
+
"that exact project and takes precedence over `project`.",
|
|
24
|
+
})),
|
|
19
25
|
}),
|
|
20
26
|
async execute(_toolCallId, params) {
|
|
21
27
|
log.debug(`read_note: identifier="${params.identifier}"`);
|
|
22
28
|
try {
|
|
23
|
-
const note = await client.readNote(params.identifier, {
|
|
29
|
+
const note = await client.readNote(params.identifier, {
|
|
30
|
+
includeFrontmatter: params.include_frontmatter === true,
|
|
31
|
+
projectId: params.project_id,
|
|
32
|
+
}, params.project);
|
|
33
|
+
// The checksum identifies the revision just read; the model sees only
|
|
34
|
+
// `content`, so it has to travel in the text to reach a guarded write.
|
|
35
|
+
const text = note.checksum
|
|
36
|
+
? `${note.content}\n\nChecksum: ${note.checksum}`
|
|
37
|
+
: note.content;
|
|
24
38
|
return {
|
|
25
39
|
content: [
|
|
26
40
|
{
|
|
27
41
|
type: "text",
|
|
28
|
-
text
|
|
42
|
+
text,
|
|
29
43
|
},
|
|
30
44
|
],
|
|
31
45
|
details: {
|
|
@@ -33,6 +47,7 @@ export function registerReadTool(api, client) {
|
|
|
33
47
|
permalink: note.permalink,
|
|
34
48
|
file_path: note.file_path,
|
|
35
49
|
frontmatter: note.frontmatter ?? null,
|
|
50
|
+
checksum: note.checksum ?? null,
|
|
36
51
|
},
|
|
37
52
|
};
|
|
38
53
|
}
|
|
@@ -6,7 +6,9 @@ export function registerSearchTool(api, client) {
|
|
|
6
6
|
label: "Knowledge Search",
|
|
7
7
|
description: "Search the Basic Memory knowledge graph for relevant notes, concepts, and connections. " +
|
|
8
8
|
"Returns matching notes with titles, content previews, and relevance scores. " +
|
|
9
|
-
"Optionally filter by frontmatter metadata fields, tags, or status
|
|
9
|
+
"Optionally filter by frontmatter metadata fields, tags, or status, or target " +
|
|
10
|
+
"another project. For a quick cross-source recall that also covers MEMORY.md " +
|
|
11
|
+
"and active tasks, use memory_search.",
|
|
10
12
|
parameters: Type.Object({
|
|
11
13
|
query: Type.String({ description: "Search query" }),
|
|
12
14
|
limit: Type.Optional(Type.Number({ description: "Max results (default: 10)" })),
|