@basicmemory/openclaw-basic-memory 0.23.2 → 0.24.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 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-schema
|
|
3
|
-
description: "
|
|
3
|
+
description: "Manage Basic Memory schemas: find unschemaed notes, infer and edit schemas, validate notes, detect drift. Use when structured note types (Task, Person, Meeting) need consistency across the graph."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Memory Schema
|
|
@@ -112,12 +112,12 @@ settings:
|
|
|
112
112
|
|
|
113
113
|
Look for clusters of notes that share structure but have no schema:
|
|
114
114
|
|
|
115
|
-
1. **Search by type**: `search_notes(
|
|
115
|
+
1. **Search by type**: `search_notes(note_types=["meeting"])` — if many notes share a `type` but no `schema/Meeting.md` exists, it's a candidate.
|
|
116
116
|
|
|
117
117
|
2. **Infer a schema**: Use `schema_infer` to analyze existing notes and generate a suggested schema:
|
|
118
118
|
```python
|
|
119
|
-
schema_infer(
|
|
120
|
-
schema_infer(
|
|
119
|
+
schema_infer(note_type="Meeting")
|
|
120
|
+
schema_infer(note_type="Meeting", threshold=0.5) # fields in 50%+ of notes
|
|
121
121
|
```
|
|
122
122
|
The threshold (0.0–1.0) controls how common a field must be to be included. Default is usually fine; lower it to catch rarer fields.
|
|
123
123
|
|
|
@@ -168,7 +168,7 @@ Check how well existing notes conform to their schema:
|
|
|
168
168
|
|
|
169
169
|
```python
|
|
170
170
|
# Validate all notes of a type
|
|
171
|
-
schema_validate(
|
|
171
|
+
schema_validate(note_type="Meeting")
|
|
172
172
|
|
|
173
173
|
# Validate a single note
|
|
174
174
|
schema_validate(identifier="meetings/2026-02-10-standup")
|
|
@@ -176,11 +176,10 @@ schema_validate(identifier="meetings/2026-02-10-standup")
|
|
|
176
176
|
|
|
177
177
|
**Important:** `schema_validate` checks for schema fields as **observation categories** in the note body — e.g., a `status` field expects `- [status] active` as an observation. Fields stored only in frontmatter metadata won't satisfy validation. To pass cleanly, include schema fields as both frontmatter values (for metadata search) and observations (for schema validation).
|
|
178
178
|
|
|
179
|
-
Validation reports
|
|
180
|
-
-
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
- **Invalid enum values** — a value isn't in the allowed set
|
|
179
|
+
Validation reports **missing required fields** (as observation categories, or relations
|
|
180
|
+
for entity-reference fields) and **invalid enum values**. Undeclared observation categories
|
|
181
|
+
and relations are listed as informational "unmatched" items. Scalar values are not
|
|
182
|
+
type-checked.
|
|
184
183
|
|
|
185
184
|
### Handling Validation Results
|
|
186
185
|
|
|
@@ -192,32 +191,41 @@ Validation reports:
|
|
|
192
191
|
Over time, notes evolve and schemas lag behind. Use `schema_diff` to find divergence:
|
|
193
192
|
|
|
194
193
|
```python
|
|
195
|
-
schema_diff(
|
|
194
|
+
schema_diff(note_type="Meeting")
|
|
196
195
|
```
|
|
197
196
|
|
|
198
197
|
Diff reports:
|
|
199
198
|
- **Fields in notes but not in schema** — candidates for adding to the schema (as optional)
|
|
200
199
|
- **Schema fields rarely used** — consider making optional or removing
|
|
201
|
-
- **
|
|
200
|
+
- **Cardinality changes** — a field that moved between single-value and array
|
|
202
201
|
|
|
203
202
|
## Schema Evolution
|
|
204
203
|
|
|
205
204
|
When note structure changes:
|
|
206
205
|
|
|
207
|
-
1. **Run diff** to see current state: `schema_diff(
|
|
208
|
-
2. **Update the schema note** via `edit_note
|
|
206
|
+
1. **Run diff** to see current state: `schema_diff(note_type="Meeting")`
|
|
207
|
+
2. **Update the schema note** via `edit_note` with the `metadata` parameter. Top-level
|
|
208
|
+
keys are replaced whole, so pass the complete updated `schema` map along with the new
|
|
209
|
+
`version`:
|
|
209
210
|
```python
|
|
210
211
|
edit_note(
|
|
211
212
|
identifier="schema/Meeting",
|
|
212
|
-
operation="
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
operation="append",
|
|
214
|
+
content="",
|
|
215
|
+
metadata={
|
|
216
|
+
"version": 2,
|
|
217
|
+
"schema": {
|
|
218
|
+
"topic": "string, what was discussed",
|
|
219
|
+
"date": "string, when it happened",
|
|
220
|
+
"attendees?(array)": "Person, who attended",
|
|
221
|
+
"decisions?(array)": "string, decisions made",
|
|
222
|
+
"location?": "string, where it happened"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
216
225
|
)
|
|
217
226
|
```
|
|
218
|
-
3. **
|
|
219
|
-
4. **
|
|
220
|
-
5. **Fix outliers** — update notes that don't conform to the new schema
|
|
227
|
+
3. **Re-validate** to confirm existing notes still pass: `schema_validate(note_type="Meeting")`
|
|
228
|
+
4. **Fix outliers** — update notes that don't conform to the new schema
|
|
221
229
|
|
|
222
230
|
### Evolution Guidelines
|
|
223
231
|
|
|
@@ -16,7 +16,7 @@ Manage work-in-progress using Basic Memory's schema system. Tasks are just notes
|
|
|
16
16
|
|
|
17
17
|
## Task Schema
|
|
18
18
|
|
|
19
|
-
Tasks use the BM schema system
|
|
19
|
+
Tasks use the BM schema system. The schema note lives at `schema/Task.md`:
|
|
20
20
|
|
|
21
21
|
```yaml
|
|
22
22
|
---
|
|
@@ -52,6 +52,7 @@ write_note(
|
|
|
52
52
|
metadata={
|
|
53
53
|
"status": "active",
|
|
54
54
|
"priority": "high",
|
|
55
|
+
"assigned_to": "claude",
|
|
55
56
|
"current_step": 1,
|
|
56
57
|
"steps": ["First step", "Second step", "Third step"]
|
|
57
58
|
},
|
|
@@ -85,7 +86,7 @@ What future-you needs to pick up this work. Include:
|
|
|
85
86
|
- **Steps are concrete and checkable** — "Implement X in file Y", not "figure out stuff"
|
|
86
87
|
- **Context is for post-amnesia resumption** — Write it as if explaining to a smart person who knows nothing about what you've been doing
|
|
87
88
|
- **Relations link to other entities** — `parent_task [[Other Task]]`, `related_to [[Some Note]]`
|
|
88
|
-
-
|
|
89
|
+
- **Note types are normalized to snake_case** — `write_note(note_type="Task")` stores `type: task`, and `note_types=["Task"]` or `note_types=["task"]` both match it.
|
|
89
90
|
|
|
90
91
|
## Resuming After Compaction
|
|
91
92
|
|
|
@@ -116,9 +117,21 @@ As work progresses, update the task note:
|
|
|
116
117
|
Updated context reflecting current state...
|
|
117
118
|
```
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
Read the task as JSON first and pass its checksum as `expected_checksum` on each
|
|
121
|
+
edit. Two sessions often update the same task, for example in a pre-compaction
|
|
122
|
+
flush; a revision conflict means the other one wrote first, so re-read before
|
|
123
|
+
writing.
|
|
124
|
+
|
|
125
|
+
Update frontmatter too, with the `metadata` parameter (empty `content` leaves the body alone):
|
|
126
|
+
```python
|
|
127
|
+
task = read_note(identifier="tasks/descriptive-task-name", output_format="json")
|
|
128
|
+
edit_note(
|
|
129
|
+
identifier="tasks/descriptive-task-name",
|
|
130
|
+
operation="append",
|
|
131
|
+
content="",
|
|
132
|
+
metadata={"current_step": 3},
|
|
133
|
+
expected_checksum=task["checksum"]
|
|
134
|
+
)
|
|
122
135
|
```
|
|
123
136
|
|
|
124
137
|
## Completing Tasks
|
|
@@ -140,7 +153,7 @@ When a compaction event is imminent:
|
|
|
140
153
|
- `current_step` to reflect actual progress
|
|
141
154
|
- `context` with everything needed to resume
|
|
142
155
|
- Step checkboxes to show what's done
|
|
143
|
-
3.
|
|
156
|
+
3. Context that isn't written down is lost at compaction
|
|
144
157
|
|
|
145
158
|
## Querying Tasks
|
|
146
159
|
|
|
@@ -153,14 +166,27 @@ With BM's schema system, tasks are fully queryable:
|
|
|
153
166
|
| `search_notes(note_types=["task"], status="blocked")` | Blocked tasks |
|
|
154
167
|
| `search_notes(note_types=["task"], metadata_filters={"assigned_to": "claude"})` | My tasks |
|
|
155
168
|
| `search_notes("blockers", note_types=["task"])` | Tasks with blockers |
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
169
|
+
| `search_notes(note_types=["task"], time_kind="due", valid_overlaps="[2026-10-14,2026-10-21)")` | Tasks with a step due this week |
|
|
170
|
+
| `schema_validate(note_type="Task")` | Validate all tasks against schema |
|
|
171
|
+
| `schema_diff(note_type="Task")` | Detect drift between schema and actual task notes |
|
|
172
|
+
|
|
173
|
+
### Due Dates
|
|
174
|
+
|
|
175
|
+
Give a step or the task a due date with a qualifier (see **memory-notes**), so
|
|
176
|
+
"what's due" is one search:
|
|
177
|
+
|
|
178
|
+
```markdown
|
|
179
|
+
- [deadline] @due:2026-10-20 Draft ready for review
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A day-precision due date counts from that day onward, so a due-this-week search
|
|
183
|
+
also returns overdue items.
|
|
158
184
|
|
|
159
185
|
## Guidelines
|
|
160
186
|
|
|
161
187
|
- **One task per unit of work** — Don't cram multiple projects into one task
|
|
162
|
-
- **Externalize early** —
|
|
188
|
+
- **Externalize early** — write things down when you notice them
|
|
163
189
|
- **Context > steps** — Steps tell you what to do; context tells you why and how
|
|
164
190
|
- **Close finished tasks** — Don't leave completed work as `active`
|
|
165
191
|
- **Link related tasks** — Use `parent_task [[X]]` or relations to connect related work
|
|
166
|
-
- **Schema validation is your friend** — Run `schema_validate(
|
|
192
|
+
- **Schema validation is your friend** — Run `schema_validate(note_type="Task")` periodically to catch incomplete tasks
|
package/tools/edit-note.ts
CHANGED
|
@@ -14,7 +14,9 @@ export function registerEditTool(
|
|
|
14
14
|
description:
|
|
15
15
|
"Incrementally edit an existing note in the Basic Memory knowledge graph. " +
|
|
16
16
|
"Supports append, prepend, find/replace, and section replacement " +
|
|
17
|
-
"without rewriting the entire note."
|
|
17
|
+
"without rewriting the entire note. The result returns the note's " +
|
|
18
|
+
"permalink and file_path: cite those exact values when confirming " +
|
|
19
|
+
"the edit — never report a save that no tool call performed.",
|
|
18
20
|
parameters: Type.Object({
|
|
19
21
|
identifier: Type.String({
|
|
20
22
|
description: "Note title, permalink, or memory:// URL to edit",
|
|
@@ -57,6 +59,16 @@ export function registerEditTool(
|
|
|
57
59
|
description: "Target project name (defaults to current project)",
|
|
58
60
|
}),
|
|
59
61
|
),
|
|
62
|
+
metadata: Type.Optional(
|
|
63
|
+
Type.Object(
|
|
64
|
+
{},
|
|
65
|
+
{
|
|
66
|
+
additionalProperties: true,
|
|
67
|
+
description:
|
|
68
|
+
"Frontmatter fields to merge independently of the edit operation. Nested objects are supported.",
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
),
|
|
60
72
|
}),
|
|
61
73
|
async execute(
|
|
62
74
|
_toolCallId: string,
|
|
@@ -68,6 +80,7 @@ export function registerEditTool(
|
|
|
68
80
|
section?: string
|
|
69
81
|
expected_replacements?: number
|
|
70
82
|
project?: string
|
|
83
|
+
metadata?: Record<string, unknown>
|
|
71
84
|
},
|
|
72
85
|
) {
|
|
73
86
|
log.debug(`edit_note: id="${params.identifier}" op=${params.operation}`)
|
|
@@ -81,6 +94,7 @@ export function registerEditTool(
|
|
|
81
94
|
find_text: params.find_text,
|
|
82
95
|
section: params.section,
|
|
83
96
|
expected_replacements: params.expected_replacements,
|
|
97
|
+
metadata: params.metadata,
|
|
84
98
|
},
|
|
85
99
|
params.project,
|
|
86
100
|
)
|
|
@@ -6,6 +6,9 @@ import { log } from "../logger.ts"
|
|
|
6
6
|
function normalizeProject(project: ProjectListResult) {
|
|
7
7
|
return {
|
|
8
8
|
name: project.name,
|
|
9
|
+
external_id: project.external_id ?? null,
|
|
10
|
+
qualified_name: project.qualified_name ?? null,
|
|
11
|
+
source: project.source ?? null,
|
|
9
12
|
path: project.path,
|
|
10
13
|
display_name: project.display_name ?? null,
|
|
11
14
|
is_private: project.is_private === true,
|
|
@@ -25,7 +28,13 @@ export function registerProjectListTool(
|
|
|
25
28
|
{
|
|
26
29
|
name: "list_memory_projects",
|
|
27
30
|
label: "List Projects",
|
|
28
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
"List Basic Memory projects accessible to this agent, optionally filtered by " +
|
|
33
|
+
"workspace. Returns each project's name, external_id, source (local, cloud, " +
|
|
34
|
+
"or local+cloud), path, default flag, workspace, and qualified name. " +
|
|
35
|
+
"Call this before passing `project` to another tool when the user names a " +
|
|
36
|
+
"project that isn't the default. Pass a project's external_id as " +
|
|
37
|
+
"`project_id` to route to that exact project.",
|
|
29
38
|
parameters: Type.Object({
|
|
30
39
|
workspace: Type.Optional(
|
|
31
40
|
Type.String({
|
|
@@ -58,10 +67,20 @@ export function registerProjectListTool(
|
|
|
58
67
|
const text = normalized
|
|
59
68
|
.map((project, idx) => {
|
|
60
69
|
const defaultSuffix = project.is_default ? " (default)" : ""
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
|
|
70
|
+
const lines = [
|
|
71
|
+
`${idx + 1}. **${project.name}**${defaultSuffix}`,
|
|
72
|
+
project.external_id ? ` ID: ${project.external_id}` : null,
|
|
73
|
+
project.source ? ` Source: ${project.source}` : null,
|
|
74
|
+
` Path: ${project.path}`,
|
|
75
|
+
` Private: ${project.is_private}`,
|
|
76
|
+
project.display_name
|
|
77
|
+
? ` Display Name: ${project.display_name}`
|
|
78
|
+
: null,
|
|
79
|
+
project.qualified_name
|
|
80
|
+
? ` Qualified Name: ${project.qualified_name}`
|
|
81
|
+
: null,
|
|
82
|
+
]
|
|
83
|
+
return lines.filter((line) => line !== null).join("\n")
|
|
65
84
|
})
|
|
66
85
|
.join("\n\n")
|
|
67
86
|
|
package/tools/list-workspaces.ts
CHANGED
|
@@ -20,7 +20,9 @@ export function registerWorkspaceListTool(
|
|
|
20
20
|
name: "list_workspaces",
|
|
21
21
|
label: "List Workspaces",
|
|
22
22
|
description:
|
|
23
|
-
"List all Basic Memory workspaces (personal and organization) accessible to this user"
|
|
23
|
+
"List all Basic Memory workspaces (personal and organization) accessible to this user. " +
|
|
24
|
+
"Use it to find the workspace to pass to list_memory_projects when the same " +
|
|
25
|
+
"project name may exist in more than one workspace.",
|
|
24
26
|
parameters: Type.Object({}),
|
|
25
27
|
async execute(_toolCallId: string, _params: Record<string, never>) {
|
|
26
28
|
log.debug("list_workspaces")
|
package/tools/memory-provider.ts
CHANGED
|
@@ -175,7 +175,8 @@ export function registerMemoryProvider(
|
|
|
175
175
|
description:
|
|
176
176
|
"Search across all memory sources: MEMORY.md (working memory), " +
|
|
177
177
|
"Basic Memory knowledge graph (long-term archive), and active tasks. " +
|
|
178
|
-
"Returns composited results from all sources."
|
|
178
|
+
"Returns composited results from all sources. Use it for broad recall; use " +
|
|
179
|
+
"search_notes when you need metadata, tag, or status filters or a specific project.",
|
|
179
180
|
parameters: Type.Object({
|
|
180
181
|
query: Type.String({
|
|
181
182
|
description: "Search query — natural language or keywords",
|
package/tools/read-note.ts
CHANGED
|
@@ -13,7 +13,9 @@ export function registerReadTool(
|
|
|
13
13
|
label: "Read Note",
|
|
14
14
|
description:
|
|
15
15
|
"Read a specific note from the Basic Memory knowledge graph by title or permalink. " +
|
|
16
|
-
"Returns the full note content including observations and relations
|
|
16
|
+
"Returns the full note content including observations and relations, " +
|
|
17
|
+
"followed by the note's checksum: pass it to write_note as " +
|
|
18
|
+
"expected_checksum (with overwrite=true) to replace only this revision.",
|
|
17
19
|
parameters: Type.Object({
|
|
18
20
|
identifier: Type.String({
|
|
19
21
|
description: "Note title, permalink, or memory:// URL to read",
|
|
@@ -29,6 +31,13 @@ export function registerReadTool(
|
|
|
29
31
|
description: "Target project name (defaults to current project)",
|
|
30
32
|
}),
|
|
31
33
|
),
|
|
34
|
+
project_id: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
description:
|
|
37
|
+
"Target project external_id from list_memory_projects. Routes to " +
|
|
38
|
+
"that exact project and takes precedence over `project`.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
32
41
|
}),
|
|
33
42
|
async execute(
|
|
34
43
|
_toolCallId: string,
|
|
@@ -36,6 +45,7 @@ export function registerReadTool(
|
|
|
36
45
|
identifier: string
|
|
37
46
|
include_frontmatter?: boolean
|
|
38
47
|
project?: string
|
|
48
|
+
project_id?: string
|
|
39
49
|
},
|
|
40
50
|
) {
|
|
41
51
|
log.debug(`read_note: identifier="${params.identifier}"`)
|
|
@@ -43,15 +53,23 @@ export function registerReadTool(
|
|
|
43
53
|
try {
|
|
44
54
|
const note = await client.readNote(
|
|
45
55
|
params.identifier,
|
|
46
|
-
{
|
|
56
|
+
{
|
|
57
|
+
includeFrontmatter: params.include_frontmatter === true,
|
|
58
|
+
projectId: params.project_id,
|
|
59
|
+
},
|
|
47
60
|
params.project,
|
|
48
61
|
)
|
|
62
|
+
// The checksum identifies the revision just read; the model sees only
|
|
63
|
+
// `content`, so it has to travel in the text to reach a guarded write.
|
|
64
|
+
const text = note.checksum
|
|
65
|
+
? `${note.content}\n\nChecksum: ${note.checksum}`
|
|
66
|
+
: note.content
|
|
49
67
|
|
|
50
68
|
return {
|
|
51
69
|
content: [
|
|
52
70
|
{
|
|
53
71
|
type: "text" as const,
|
|
54
|
-
text
|
|
72
|
+
text,
|
|
55
73
|
},
|
|
56
74
|
],
|
|
57
75
|
details: {
|
|
@@ -59,6 +77,7 @@ export function registerReadTool(
|
|
|
59
77
|
permalink: note.permalink,
|
|
60
78
|
file_path: note.file_path,
|
|
61
79
|
frontmatter: note.frontmatter ?? null,
|
|
80
|
+
checksum: note.checksum ?? null,
|
|
62
81
|
},
|
|
63
82
|
}
|
|
64
83
|
} catch (err) {
|
package/tools/search-notes.ts
CHANGED
|
@@ -14,7 +14,9 @@ export function registerSearchTool(
|
|
|
14
14
|
description:
|
|
15
15
|
"Search the Basic Memory knowledge graph for relevant notes, concepts, and connections. " +
|
|
16
16
|
"Returns matching notes with titles, content previews, and relevance scores. " +
|
|
17
|
-
"Optionally filter by frontmatter metadata fields, tags, or status
|
|
17
|
+
"Optionally filter by frontmatter metadata fields, tags, or status, or target " +
|
|
18
|
+
"another project. For a quick cross-source recall that also covers MEMORY.md " +
|
|
19
|
+
"and active tasks, use memory_search.",
|
|
18
20
|
parameters: Type.Object({
|
|
19
21
|
query: Type.String({ description: "Search query" }),
|
|
20
22
|
limit: Type.Optional(
|
package/tools/write-note.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox"
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
|
|
3
3
|
import type { BmClient } from "../bm-client.ts"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
NoteAlreadyExistsError,
|
|
6
|
+
NoteRevisionConflictError,
|
|
7
|
+
} from "../bm-client.ts"
|
|
5
8
|
import { log } from "../logger.ts"
|
|
6
9
|
|
|
7
10
|
export function registerWriteTool(
|
|
@@ -14,8 +17,14 @@ export function registerWriteTool(
|
|
|
14
17
|
label: "Write Note",
|
|
15
18
|
description:
|
|
16
19
|
"Create a note in the Basic Memory knowledge graph. " +
|
|
20
|
+
"Call this whenever the user asks to remember, record, save, or note " +
|
|
21
|
+
"something — saying 'saved' without this call saves nothing. The result " +
|
|
22
|
+
"returns the note's permalink and file_path: cite those exact values " +
|
|
23
|
+
"when confirming, never a recalled filename. " +
|
|
17
24
|
"If the note already exists, returns an error by default. " +
|
|
18
|
-
"Pass overwrite=true to replace, or use edit_note for incremental updates."
|
|
25
|
+
"Pass overwrite=true to replace, or use edit_note for incremental updates. " +
|
|
26
|
+
"To replace only the revision you read, also pass expected_checksum " +
|
|
27
|
+
"(the checksum read_note reports); a newer revision is then left untouched.",
|
|
19
28
|
parameters: Type.Object({
|
|
20
29
|
title: Type.String({ description: "Note title" }),
|
|
21
30
|
content: Type.String({
|
|
@@ -27,12 +36,47 @@ export function registerWriteTool(
|
|
|
27
36
|
description: "Target project name (defaults to current project)",
|
|
28
37
|
}),
|
|
29
38
|
),
|
|
39
|
+
project_id: Type.Optional(
|
|
40
|
+
Type.String({
|
|
41
|
+
description:
|
|
42
|
+
"Target project external_id from list_memory_projects. Routes to " +
|
|
43
|
+
"that exact project and takes precedence over `project`.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
30
46
|
overwrite: Type.Optional(
|
|
31
47
|
Type.Boolean({
|
|
32
48
|
description:
|
|
33
49
|
"Set to true to replace an existing note. Defaults to false.",
|
|
34
50
|
}),
|
|
35
51
|
),
|
|
52
|
+
metadata: Type.Optional(
|
|
53
|
+
Type.Object(
|
|
54
|
+
{},
|
|
55
|
+
{
|
|
56
|
+
additionalProperties: true,
|
|
57
|
+
description:
|
|
58
|
+
"Extra frontmatter fields to merge into the note. Nested objects are supported.",
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
tags: Type.Optional(
|
|
63
|
+
Type.Union([Type.Array(Type.String()), Type.String()], {
|
|
64
|
+
description:
|
|
65
|
+
"Tags for the note, as an array or comma-separated string.",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
note_type: Type.Optional(
|
|
69
|
+
Type.String({
|
|
70
|
+
description: 'Note type stored in frontmatter. Defaults to "note".',
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
expected_checksum: Type.Optional(
|
|
74
|
+
Type.String({
|
|
75
|
+
description:
|
|
76
|
+
"Requires overwrite=true. The checksum of the note as you read it; " +
|
|
77
|
+
"the note is replaced only while it is still that revision.",
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
36
80
|
}),
|
|
37
81
|
async execute(
|
|
38
82
|
_toolCallId: string,
|
|
@@ -41,11 +85,31 @@ export function registerWriteTool(
|
|
|
41
85
|
content: string
|
|
42
86
|
folder: string
|
|
43
87
|
project?: string
|
|
88
|
+
project_id?: string
|
|
44
89
|
overwrite?: boolean
|
|
90
|
+
metadata?: Record<string, unknown>
|
|
91
|
+
tags?: string[] | string
|
|
92
|
+
note_type?: string
|
|
93
|
+
expected_checksum?: string
|
|
45
94
|
},
|
|
46
95
|
) {
|
|
47
96
|
log.debug(`write_note: title=${params.title} folder=${params.folder}`)
|
|
48
97
|
|
|
98
|
+
const options =
|
|
99
|
+
params.metadata !== undefined ||
|
|
100
|
+
params.tags !== undefined ||
|
|
101
|
+
params.note_type !== undefined ||
|
|
102
|
+
params.project_id !== undefined ||
|
|
103
|
+
params.expected_checksum !== undefined
|
|
104
|
+
? {
|
|
105
|
+
metadata: params.metadata,
|
|
106
|
+
tags: params.tags,
|
|
107
|
+
noteType: params.note_type,
|
|
108
|
+
projectId: params.project_id,
|
|
109
|
+
expectedChecksum: params.expected_checksum,
|
|
110
|
+
}
|
|
111
|
+
: undefined
|
|
112
|
+
|
|
49
113
|
try {
|
|
50
114
|
const note = await client.writeNote(
|
|
51
115
|
params.title,
|
|
@@ -53,9 +117,13 @@ export function registerWriteTool(
|
|
|
53
117
|
params.folder,
|
|
54
118
|
params.project,
|
|
55
119
|
params.overwrite,
|
|
120
|
+
options,
|
|
56
121
|
)
|
|
57
122
|
|
|
58
|
-
const
|
|
123
|
+
const checksumSuffix = note.checksum
|
|
124
|
+
? `\nChecksum: ${note.checksum}`
|
|
125
|
+
: ""
|
|
126
|
+
const msg = `Note saved: ${note.title} (${note.permalink})${checksumSuffix}`
|
|
59
127
|
return {
|
|
60
128
|
content: [
|
|
61
129
|
{
|
|
@@ -67,6 +135,7 @@ export function registerWriteTool(
|
|
|
67
135
|
title: note.title,
|
|
68
136
|
permalink: note.permalink,
|
|
69
137
|
file_path: note.file_path,
|
|
138
|
+
checksum: note.checksum ?? null,
|
|
70
139
|
},
|
|
71
140
|
}
|
|
72
141
|
} catch (err) {
|
|
@@ -78,7 +147,7 @@ export function registerWriteTool(
|
|
|
78
147
|
` - edit_note("${err.permalink}", operation="append", content="...")`,
|
|
79
148
|
` - edit_note("${err.permalink}", operation="replace_section", section="...", content="...")`,
|
|
80
149
|
` - write_note("${params.title}", ..., overwrite=true) to fully replace`,
|
|
81
|
-
` - read_note("${err.permalink}") to inspect
|
|
150
|
+
` - read_note("${err.permalink}", include_frontmatter=true) to inspect it first; pass its checksum as expected_checksum with overwrite=true to replace only that revision`,
|
|
82
151
|
].join("\n")
|
|
83
152
|
|
|
84
153
|
return {
|
|
@@ -91,6 +160,27 @@ export function registerWriteTool(
|
|
|
91
160
|
}
|
|
92
161
|
}
|
|
93
162
|
|
|
163
|
+
if (err instanceof NoteRevisionConflictError) {
|
|
164
|
+
const current = err.currentChecksum
|
|
165
|
+
? `Its current checksum is ${err.currentChecksum}.`
|
|
166
|
+
: "No note exists at that path any more."
|
|
167
|
+
const text = [
|
|
168
|
+
`Note revision conflict: "${params.title}" changed since it was read, so nothing was replaced. ${current}`,
|
|
169
|
+
"",
|
|
170
|
+
"Read the note again, confirm it should still be replaced, then retry with the new checksum as expected_checksum.",
|
|
171
|
+
].join("\n")
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text" as const, text }],
|
|
175
|
+
details: {
|
|
176
|
+
title: params.title,
|
|
177
|
+
permalink: err.permalink,
|
|
178
|
+
current_checksum: err.currentChecksum,
|
|
179
|
+
error: "note_revision_conflict",
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
94
184
|
log.error("write_note failed", err)
|
|
95
185
|
return {
|
|
96
186
|
content: [
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: memory-ci-capture
|
|
3
|
-
description: Synthesize GitHub delivery context into a concise Basic Memory project update. Use in CI after `bm ci collect` prepares a ProjectUpdateContext; return only structured AgentSynthesis JSON for `bm ci publish`.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Memory CI Capture
|
|
7
|
-
|
|
8
|
-
Turn a meaningful GitHub delivery moment into project memory. GitHub records the
|
|
9
|
-
mechanics. Basic Memory remembers what changed and why.
|
|
10
|
-
|
|
11
|
-
## Inputs
|
|
12
|
-
|
|
13
|
-
Read the `ProjectUpdateContext` JSON produced by `bm ci collect` at
|
|
14
|
-
`.github/basic-memory/project-update-context.json`. Treat it as the immutable
|
|
15
|
-
source of truth for repository, event type, PR number, workflow run, SHA, source
|
|
16
|
-
URL, timestamps, and deployment environment.
|
|
17
|
-
|
|
18
|
-
Do not invent tests, deploy checks, linked issues, product impact, or decisions.
|
|
19
|
-
If evidence is absent, say so briefly in `verification` or leave the field empty.
|
|
20
|
-
|
|
21
|
-
## Output
|
|
22
|
-
|
|
23
|
-
Return only JSON matching the `AgentSynthesis` shape:
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"summary": "What changed.",
|
|
28
|
-
"why_it_matters": "Why this update matters for future humans and agents.",
|
|
29
|
-
"user_facing_changes": [],
|
|
30
|
-
"internal_changes": [],
|
|
31
|
-
"verification": [],
|
|
32
|
-
"follow_ups": [],
|
|
33
|
-
"decision_candidates": [],
|
|
34
|
-
"task_candidates": []
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Synthesis Rules
|
|
39
|
-
|
|
40
|
-
- Prefer a short explanation over a commit-by-commit changelog.
|
|
41
|
-
- Preserve intent, changed behavior, source links, verification evidence present
|
|
42
|
-
in the context, and concrete follow-ups.
|
|
43
|
-
- Put explicit product or architecture decisions in `decision_candidates` only
|
|
44
|
-
when the source context clearly contains them.
|
|
45
|
-
- Put future work in `task_candidates` only when it is concrete enough to act on.
|
|
46
|
-
- Keep the tone factual and useful. This is project memory, not marketing copy.
|
|
47
|
-
|
|
48
|
-
## Event Guidance
|
|
49
|
-
|
|
50
|
-
For merged pull requests, focus on why the PR existed, what area changed, what
|
|
51
|
-
issues it advanced or closed, and what verification evidence appears in the
|
|
52
|
-
context.
|
|
53
|
-
|
|
54
|
-
For production deploys, focus on what reached production, the deployed SHA,
|
|
55
|
-
environment, workflow run, and verification evidence. Do not overclaim success
|
|
56
|
-
beyond the workflow and source facts.
|