@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.
Files changed (87) hide show
  1. package/README.md +24 -12
  2. package/bm-client.ts +302 -112
  3. package/commands/cli.ts +5 -6
  4. package/commands/slash.ts +4 -4
  5. package/context-engine/basic-memory-context-engine.ts +74 -9
  6. package/context-engine/types.ts +21 -0
  7. package/dist/bm-client.d.ts +61 -12
  8. package/dist/bm-client.js +250 -101
  9. package/dist/commands/cli.d.ts +1 -1
  10. package/dist/commands/cli.js +5 -5
  11. package/dist/commands/slash.d.ts +1 -1
  12. package/dist/commands/slash.js +4 -3
  13. package/dist/context-engine/basic-memory-context-engine.d.ts +20 -1
  14. package/dist/context-engine/basic-memory-context-engine.js +50 -2
  15. package/dist/context-engine/types.d.ts +9 -0
  16. package/dist/context-engine/types.js +1 -0
  17. package/dist/index.d.ts +10 -4
  18. package/dist/index.js +79 -69
  19. package/dist/memory/basic-memory-capability.d.ts +19 -0
  20. package/dist/memory/basic-memory-capability.js +190 -0
  21. package/dist/tools/build-context.d.ts +1 -1
  22. package/dist/tools/build-context.js +12 -4
  23. package/dist/tools/delete-note.d.ts +1 -1
  24. package/dist/tools/delete-note.js +2 -1
  25. package/dist/tools/edit-note.d.ts +1 -1
  26. package/dist/tools/edit-note.js +22 -12
  27. package/dist/tools/list-memory-projects.d.ts +1 -1
  28. package/dist/tools/list-memory-projects.js +2 -1
  29. package/dist/tools/list-workspaces.d.ts +1 -1
  30. package/dist/tools/list-workspaces.js +4 -4
  31. package/dist/tools/move-note.d.ts +1 -1
  32. package/dist/tools/move-note.js +2 -1
  33. package/dist/tools/read-note.d.ts +1 -1
  34. package/dist/tools/read-note.js +2 -1
  35. package/dist/tools/schema-diff.d.ts +1 -1
  36. package/dist/tools/schema-diff.js +2 -1
  37. package/dist/tools/schema-infer.d.ts +1 -1
  38. package/dist/tools/schema-infer.js +2 -1
  39. package/dist/tools/schema-validate.d.ts +1 -1
  40. package/dist/tools/schema-validate.js +2 -1
  41. package/dist/tools/search-notes.d.ts +1 -1
  42. package/dist/tools/search-notes.js +13 -6
  43. package/dist/tools/write-note.d.ts +1 -1
  44. package/dist/tools/write-note.js +11 -12
  45. package/index.ts +93 -84
  46. package/memory/basic-memory-capability.ts +253 -0
  47. package/openclaw.plugin.json +0 -2
  48. package/package.json +6 -5
  49. package/skills/manifest.json +5 -0
  50. package/skills/memory-agents/SKILL.md +23 -10
  51. package/skills/memory-capture/SKILL.md +29 -18
  52. package/skills/memory-comark/SKILL.md +17 -6
  53. package/skills/memory-continue/SKILL.md +47 -27
  54. package/skills/memory-curate/SKILL.md +40 -24
  55. package/skills/memory-defrag/SKILL.md +18 -7
  56. package/skills/memory-doctor/SKILL.md +93 -0
  57. package/skills/memory-ingest/SKILL.md +33 -17
  58. package/skills/memory-lifecycle/SKILL.md +35 -22
  59. package/skills/memory-literary-analysis/SKILL.md +112 -75
  60. package/skills/memory-metadata-search/SKILL.md +39 -28
  61. package/skills/memory-notes/SKILL.md +75 -50
  62. package/skills/memory-okf/SKILL.md +27 -15
  63. package/skills/memory-onboarding/SKILL.md +20 -9
  64. package/skills/memory-onboarding/references/assistant-setup.md +1 -1
  65. package/skills/memory-onboarding/references/conventions.md +4 -4
  66. package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
  67. package/skills/memory-onboarding/references/schema-guide.md +9 -8
  68. package/skills/memory-quest/SKILL.md +18 -7
  69. package/skills/memory-reflect/SKILL.md +25 -13
  70. package/skills/memory-research/SKILL.md +28 -13
  71. package/skills/memory-schema/SKILL.md +36 -24
  72. package/skills/memory-tasks/SKILL.md +34 -22
  73. package/tools/build-context.ts +14 -5
  74. package/tools/delete-note.ts +2 -2
  75. package/tools/edit-note.ts +27 -14
  76. package/tools/list-memory-projects.ts +6 -2
  77. package/tools/list-workspaces.ts +8 -5
  78. package/tools/move-note.ts +2 -2
  79. package/tools/read-note.ts +2 -2
  80. package/tools/schema-diff.ts +2 -2
  81. package/tools/schema-infer.ts +2 -2
  82. package/tools/schema-validate.ts +2 -2
  83. package/tools/search-notes.ts +18 -7
  84. package/tools/write-note.ts +12 -12
  85. package/dist/tools/memory-provider.d.ts +0 -15
  86. package/dist/tools/memory-provider.js +0 -275
  87. package/tools/memory-provider.ts +0 -336
@@ -7,6 +7,17 @@ description: "Analyze a complete literary work into a structured Basic Memory kn
7
7
 
8
8
  Transform a complete literary work into a structured knowledge graph. Characters, themes, chapters, locations, symbols, and literary devices become interconnected notes — searchable, validatable, and traversable.
9
9
 
10
+ ## Project scope
11
+
12
+ If the project is unknown, discover it with `bm_project(command="list")`.
13
+ Pass a nonempty `project` on every note, folder, context and schema call, even
14
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
15
+ `workspace/project`, or external ID; `<work-name>` below is a placeholder for it.
16
+ Recent activity also requires `project`. Search accepts `project` for one
17
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
18
+ Discovery and bundled manuals need no project. Keep the same scope when following
19
+ returned paths, paginating, or recovering from a refusal.
20
+
10
21
  ## When to Use
11
22
 
12
23
  - Analyzing a novel, play, poem, or non-fiction book end-to-end
@@ -28,10 +39,9 @@ Phase 5: Explore → traverse the graph, write synthesis notes
28
39
 
29
40
  ## Tools
30
41
 
31
- Writing always goes through `write_note` and `edit_note`. For *reading* — which is most of
32
- the work in a long analysis — prefer the POSIX read verbs where they are available
33
- (the MCP `cat`, `find`, `grep`, and `ls` tools are on by default; the `bm` CLI verbs are always
34
- available):
42
+ Writing goes through `bm_write` and `bm_edit`. MCP reads use `bm_read`, `bm_find`,
43
+ `bm_search`, and `bm_list`. The CLI keeps its POSIX verbs; the table and shell
44
+ examples below are CLI commands (prefix each verb with `bm`):
35
45
 
36
46
  | Need | Use | Instead of |
37
47
  |------|-----|-----------|
@@ -55,7 +65,7 @@ answer, exit 0, no warning — so learn it here rather than from a graph you tho
55
65
  audited:
56
66
 
57
67
  - **`find` pages, and the default page is 10.** Any query whose answer is "all N chapters"
58
- needs `--page-size 200` (the maximum) — see [Coverage Checks](#coverage-checks).
68
+ needs `--page-size 100` (the maximum) and paging — see [Coverage Checks](#coverage-checks).
59
69
  - **`--name` cannot combine with `--meta`.** The metadata search has no filename glob. Scope a
60
70
  `--meta` query with the positional path instead: `find /characters --meta
61
71
  'note_type=character'`. That path is matched on a directory boundary against the *file path*
@@ -64,15 +74,19 @@ audited:
64
74
  `/characters` reaches everything filed under `characters/` (including `characters/major/`),
65
75
  and never `characters-cut/`.
66
76
 
67
- If the POSIX verbs are unavailable, every step below still works with `search_notes`,
68
- `read_note`, and `list_directory` — it just costs more.
77
+ For MCP, use `bm_read(identifier="<note>", section="Observations", project="<work-name>")` or
78
+ `start_line`/`end_line` for slices. Use `bm_find(filters={"type": "chapter"}, fields=["chapter_number", "pov"], project="<work-name>")`
79
+ for metadata and `bm_search(query="doubloon", lines=2, project="<work-name>")`
80
+ for matching lines. `bm_list` lists one folder level; `bm_find` recurses.
81
+ MCP list/find/search pages cap at 100; follow `has_more` through every page.
82
+ CLI page sizes and flags below are separate from the MCP shapes.
69
83
 
70
84
  ## Phase 0: Setup
71
85
 
72
86
  ### Create the Project
73
87
 
74
88
  ```python
75
- create_memory_project(project_name="<work-name>", project_path="~/basic-memory/<work-name>")
89
+ bm_project(command="create", name="<work-name>", path="~/basic-memory/<work-name>")
76
90
  ```
77
91
 
78
92
  Use a kebab-case slug of the work's title (e.g., `great-gatsby`, `hamlet`, `beloved`).
@@ -84,7 +98,7 @@ Write 6 schema notes to `schema/`. Each schema defines the entity type's fields,
84
98
  #### Character Schema
85
99
 
86
100
  ```python
87
- write_note(
101
+ bm_write(
88
102
  title="Character",
89
103
  directory="schema",
90
104
  note_type="schema",
@@ -106,7 +120,8 @@ Schema for character entity notes.
106
120
  ## Observations
107
121
  - [convention] Major characters in characters/major/, minor in characters/minor/
108
122
  - [convention] Observation categories: trait, motivation, arc, quote, appearance, relationship, symbolism, fate
109
- - [convention] Relations: appears_in, contrasts_with, allied_with, commands, symbolizes, associated_with"""
123
+ - [convention] Relations: appears_in, contrasts_with, allied_with, commands, symbolizes, associated_with""",
124
+ project="<work-name>",
110
125
  )
111
126
  ```
112
127
 
@@ -115,7 +130,7 @@ Add work-specific fields as needed — e.g., `rank` for military fiction, `house
115
130
  #### Theme Schema
116
131
 
117
132
  ```python
118
- write_note(
133
+ bm_write(
119
134
  title="Theme",
120
135
  directory="schema",
121
136
  note_type="schema",
@@ -135,14 +150,15 @@ Schema for thematic analysis notes.
135
150
 
136
151
  ## Observations
137
152
  - [convention] Observation categories: definition, manifestation, evolution, counterpoint, quote, interpretation
138
- - [convention] Relations: embodied_by, contrasts_with, reinforced_by, explored_in, expressed_through"""
153
+ - [convention] Relations: embodied_by, contrasts_with, reinforced_by, explored_in, expressed_through""",
154
+ project="<work-name>",
139
155
  )
140
156
  ```
141
157
 
142
158
  #### Chapter Schema
143
159
 
144
160
  ```python
145
- write_note(
161
+ bm_write(
146
162
  title="Chapter",
147
163
  directory="schema",
148
164
  note_type="schema",
@@ -164,14 +180,15 @@ Schema for chapter-level analysis notes.
164
180
  ## Observations
165
181
  - [convention] Chapters stored in chapters/ directory
166
182
  - [convention] Observation categories: summary, event, tone, technique, quote, significance, foreshadowing
167
- - [convention] Relations: features, set_in, explores, contains, employs, follows, precedes, parallels"""
183
+ - [convention] Relations: features, set_in, explores, contains, employs, follows, precedes, parallels""",
184
+ project="<work-name>",
168
185
  )
169
186
  ```
170
187
 
171
188
  #### Location Schema
172
189
 
173
190
  ```python
174
- write_note(
191
+ bm_write(
175
192
  title="Location",
176
193
  directory="schema",
177
194
  note_type="schema",
@@ -191,14 +208,15 @@ Schema for location and setting notes.
191
208
 
192
209
  ## Observations
193
210
  - [convention] Observation categories: description, atmosphere, symbolism, significance, geography
194
- - [convention] Relations: setting_for, associated_with, symbolizes, contains, part_of"""
211
+ - [convention] Relations: setting_for, associated_with, symbolizes, contains, part_of""",
212
+ project="<work-name>",
195
213
  )
196
214
  ```
197
215
 
198
216
  #### Symbol Schema
199
217
 
200
218
  ```python
201
- write_note(
219
+ bm_write(
202
220
  title="Symbol",
203
221
  directory="schema",
204
222
  note_type="schema",
@@ -218,14 +236,15 @@ Schema for symbolic element notes.
218
236
 
219
237
  ## Observations
220
238
  - [convention] Observation categories: meaning, appearance, ambiguity, interpretation, quote, evolution
221
- - [convention] Relations: represents, associated_with, appears_in, contrasts_with, located_at"""
239
+ - [convention] Relations: represents, associated_with, appears_in, contrasts_with, located_at""",
240
+ project="<work-name>",
222
241
  )
223
242
  ```
224
243
 
225
244
  #### LiteraryDevice Schema
226
245
 
227
246
  ```python
228
- write_note(
247
+ bm_write(
229
248
  title="LiteraryDevice",
230
249
  directory="schema",
231
250
  note_type="schema",
@@ -245,7 +264,8 @@ Schema for literary technique and device notes.
245
264
 
246
265
  ## Observations
247
266
  - [convention] Observation categories: definition, usage, effect, example, significance
248
- - [convention] Relations: used_in, characterizes, expresses, related_to"""
267
+ - [convention] Relations: used_in, characterizes, expresses, related_to""",
268
+ project="<work-name>",
249
269
  )
250
270
  ```
251
271
 
@@ -276,7 +296,7 @@ Before processing chapters, create stub notes for major entities so `[[wiki-link
276
296
  For each major character, create a stub with known metadata:
277
297
 
278
298
  ```python
279
- write_note(
299
+ bm_write(
280
300
  title="<Character Name>",
281
301
  directory="characters/major",
282
302
  note_type="Character",
@@ -290,7 +310,8 @@ write_note(
290
310
 
291
311
  ## Relations
292
312
  - associated_with [[<Related Character>]]
293
- - appears_in [[<Key Location>]]"""
313
+ - appears_in [[<Key Location>]]""",
314
+ project="<work-name>",
294
315
  )
295
316
  ```
296
317
 
@@ -340,14 +361,14 @@ re-reading the whole book into context:
340
361
 
341
362
  ```bash
342
363
  grep -n '^CHAPTER ' ~/basic-memory/moby-dick/moby-dick.txt # heading -> line number
343
- bm cat moby-dick/moby-dick.txt --lines 4200-4890 --plain # one chapter, not the whole text
364
+ bm cat moby-dick.txt --lines 4200-4890 --project moby-dick # one chapter, not the whole text
344
365
  ```
345
366
 
346
367
  `grep -n` here is the shell's grep on a filesystem path (this is the map-building step, and
347
- it needs the file). `bm cat` then takes the *note identifier* and returns exactly that slice plus a
348
- `lines 4200-4890 of N` footer. Spell the identifier project-qualified
349
- (`<work>/<work>.txt`) so it routes to the right project. `bm head moby-dick/moby-dick.txt -n 40` is the cheap
350
- way to eyeball the heading format before writing the grep pattern.
368
+ it needs the file). `bm cat` then takes the *note identifier* and returns exactly that slice,
369
+ numbered, under a `Lines 4200-4890 of N` header. Pass `--project <work>` so it reads from the
370
+ right project. `bm head moby-dick.txt -n 40 --project moby-dick` is the cheap way to eyeball
371
+ the heading format before writing the grep pattern.
351
372
 
352
373
  Store the map in the project (a note or a small JSON file) so later batches — and a resumed
353
374
  run after context compaction — do not have to rediscover it. On a long work this is the
@@ -378,7 +399,7 @@ never work from memory or a summary; textual evidence is the entire point.
378
399
  **2. Create the chapter note:**
379
400
 
380
401
  ```python
381
- write_note(
402
+ bm_write(
382
403
  title="Chapter <N> - <Title>",
383
404
  directory="chapters",
384
405
  note_type="Chapter",
@@ -407,19 +428,21 @@ write_note(
407
428
  - contains [[<Symbol>]]
408
429
  - employs [[<Literary Device>]]
409
430
  - follows [[Chapter <N-1> - <Previous Title>]]
410
- - precedes [[Chapter <N+1> - <Next Title>]]"""
431
+ - precedes [[Chapter <N+1> - <Next Title>]]""",
432
+ project="<work-name>",
411
433
  )
412
434
  ```
413
435
 
414
436
  **3. Enrich related entities:**
415
437
 
416
438
  ```python
417
- edit_note(
439
+ bm_edit(
418
440
  identifier="characters/major/<character-slug>",
419
- operation="insert_after_section",
441
+ command="insert_after_section",
420
442
  section="Observations",
421
443
  content="""- [arc] Ch.<N>: <What happens to this character>
422
- - [quote] "<Attributed quote>" (Ch.<N>)"""
444
+ - [quote] "<Attributed quote>" (Ch.<N>)""",
445
+ project="<work-name>",
423
446
  )
424
447
  ```
425
448
 
@@ -452,7 +475,7 @@ As each chapter is processed, append observations to relevant entities:
452
475
 
453
476
  ### Adding Prose and Interpretation
454
477
 
455
- After the structured observations are in place, consider adding interpretive prose to major entity notes. Insert 2-4 paragraphs of critical essay before the Observations section with `edit_note(operation="insert_before_section", section="Observations", content=...)`. This prose should:
478
+ After the structured observations are in place, consider adding interpretive prose to major entity notes. Insert 2-4 paragraphs of critical essay before the Observations section with `bm_edit(command="insert_before_section", section="Observations", content=..., project="<work-name>")`. This prose should:
456
479
 
457
480
  - Argue for a reading of the character, theme, or symbol — not just describe it
458
481
  - Connect the entity to the work's larger concerns and to literary tradition
@@ -470,17 +493,17 @@ After all chapters are processed:
470
493
  Do not re-read every note to decide what is thin. Query for it:
471
494
 
472
495
  ```bash
473
- bm find --meta 'note_type=chapter' --fields chapter_number,pov,setting --page-size 200
474
- bm find --meta 'note_type=character' --fields role,status --page-size 200 # who is still a stub
475
- bm find --meta 'chapter_number>100' --fields pov --page-size 200 # late-book POV drift
496
+ bm find --meta 'note_type=chapter' --fields chapter_number,pov,setting --page-size 100
497
+ bm find --meta 'note_type=character' --fields role,status --page-size 100 # who is still a stub
498
+ bm find --meta 'chapter_number>100' --fields pov --page-size 100 # late-book POV drift
476
499
  ```
477
500
 
478
- A field a note never set comes back as a blank cell (`null` under `--json`), so rows with
501
+ A field a note never set comes back as `None` (`null` under `--json`), so rows with
479
502
  blanks are the work queue. This turns "audit the graph" from a read of every note into one
480
503
  call per question.
481
504
 
482
- `write_note` snake-cases `note_type` (`Chapter` is stored as `chapter`), and `--meta`
483
- type predicates are normalized the same way, so either spelling matches. `--page-size 200`
505
+ `bm_write` snake-cases `note_type` (`Chapter` is stored as `chapter`), and `--meta`
506
+ type predicates are normalized the same way, so either spelling matches. `--page-size 100`
484
507
  is not decoration: without it these return the first 10 rows and the work queue looks ten
485
508
  items long.
486
509
 
@@ -497,7 +520,7 @@ Add `parallels` and `contrasts_with` relations between structurally similar chap
497
520
  Create synthesis notes in `analysis/`:
498
521
 
499
522
  ```python
500
- write_note(
523
+ bm_write(
501
524
  title="Narrative Structure",
502
525
  directory="analysis",
503
526
  note_type="note",
@@ -515,7 +538,8 @@ Analysis of the work's narrative architecture.
515
538
  - analyzes [[<Protagonist>]]
516
539
  - analyzes [[<Key Character>]]
517
540
  - explores [[<Central Theme>]]
518
- ..."""
541
+ ...""",
542
+ project="<work-name>",
519
543
  )
520
544
  ```
521
545
 
@@ -533,23 +557,23 @@ During chapter processing, new minor characters, locations, and symbols will eme
533
557
 
534
558
  ```python
535
559
  # Validate each entity type
536
- schema_validate(note_type="Character")
537
- schema_validate(note_type="Theme")
538
- schema_validate(note_type="Chapter")
539
- schema_validate(note_type="Location")
540
- schema_validate(note_type="Symbol")
541
- schema_validate(note_type="LiteraryDevice")
560
+ bm_schema(command="validate", note_type="Character", project="<work-name>")
561
+ bm_schema(command="validate", note_type="Theme", project="<work-name>")
562
+ bm_schema(command="validate", note_type="Chapter", project="<work-name>")
563
+ bm_schema(command="validate", note_type="Location", project="<work-name>")
564
+ bm_schema(command="validate", note_type="Symbol", project="<work-name>")
565
+ bm_schema(command="validate", note_type="LiteraryDevice", project="<work-name>")
542
566
  ```
543
567
 
544
568
  ### Drift Detection
545
569
 
546
570
  ```python
547
- schema_diff(note_type="Character")
571
+ bm_schema(command="diff", note_type="Character", project="<work-name>")
548
572
  # ... for each type
549
573
  ```
550
574
 
551
575
  Fix issues found — common fixes:
552
- - Missing required observation categories → add them via `edit_note`
576
+ - Missing required observation categories → add them via `bm_edit`
553
577
  - Enum values outside allowed set → correct metadata
554
578
  - Fields in notes but not schema → add as optional to schema if legitimate
555
579
 
@@ -558,14 +582,14 @@ Fix issues found — common fixes:
558
582
  Schema validation proves notes match their shape. These prove the graph is *complete*:
559
583
 
560
584
  ```bash
561
- bm find --meta 'note_type=chapter' --fields chapter_number --page-size 200 # every chapter present?
562
- bm find --meta 'note_type=chapter' --fields pov,setting --page-size 200 # missing context?
563
- bm find /characters --meta 'note_type=character' --fields role --page-size 200 # inventory vs. seed list
585
+ bm find --meta 'note_type=chapter' --fields chapter_number --page-size 100 # every chapter present?
586
+ bm find --meta 'note_type=chapter' --fields pov,setting --page-size 100 # missing context?
587
+ bm find /characters --meta 'note_type=character' --fields role --page-size 100 # inventory vs. seed list
564
588
  ```
565
589
 
566
590
  **A coverage check that pages is not a coverage check.** `bm find` defaults to
567
591
  `--page-size 10`, so the un-sized form of the first query "proves" a 138-chapter work has 10
568
- chapters. 200 is the maximum page size; past that, iterate with `--page 2`, `--page 3`, … .
592
+ chapters. 100 is the maximum page size; past that, iterate with `--page 2`, `--page 3`, … .
569
593
  Scope a `--meta` query with the positional path (`/characters`), never `--name` — the two
570
594
  options are mutually exclusive, because the metadata search has no filename glob. The
571
595
  positional path scopes by the *file path* a note is indexed under, matched on a directory
@@ -573,12 +597,10 @@ boundary: `/characters` admits `characters/major/ahab.md` but never `characters-
573
597
  not a permalink match, so a note that pins its own `permalink:` is still found where its file
574
598
  lives.
575
599
 
576
- Read the count off the footer, not off the rows you can see. In a terminal every `find`
577
- result reports `page 1 • total 138`, and appends `• more available (--page)` when the page
578
- truncated the answer — that suffix appearing is the check *failing*, whatever the visible
579
- rows say. **The footer is a TTY feature.** Piped output without `--plain` is JSON, which
580
- carries `total` and `has_more`; with `--plain` the rows go to stdout and a has-more notice
581
- goes to stderr. For scripts, read `has_more` from `--json`.
600
+ Read the count off the result, not off the rows you can see. The text result opens with the
601
+ total (`138 notes under '/' match:`) and ends with `More results available.` when the page
602
+ truncated the answer — that line appearing is the check *failing*, whatever the visible rows
603
+ say. For scripts, read `total` and `has_more` from `--json`.
582
604
 
583
605
  For the sequence gap — the failure that a count alone cannot catch — take the numbers from
584
606
  `--json`, which carries `total`, `total_is_exact`, and `has_more`:
@@ -587,7 +609,7 @@ For the sequence gap — the failure that a count alone cannot catch — take th
587
609
  expected=138; page=1; rows='[]'
588
610
  while :; do
589
611
  resp=$(bm find --meta 'note_type=chapter' --fields chapter_number \
590
- --page-size 200 --page $page --project <work> --json)
612
+ --page-size 100 --page $page --project <work> --json)
591
613
  rows=$(jq -n --argjson acc "$rows" --argjson r "$resp" \
592
614
  '$acc + [$r.results[] | {title, n: .fields.chapter_number}]')
593
615
  [ "$(jq -r '.has_more' <<<"$resp")" = "true" ] || break
@@ -603,9 +625,9 @@ jq -n --argjson rows "$rows" --argjson expected "$expected" '
603
625
  out_of_range: ($n | map(select(. < 1 or . > $expected)) | unique) }'
604
626
  ```
605
627
 
606
- The loop is not ceremony. `--page-size` caps at 200, so a single call cannot inventory a work
607
- with more than 200 chapters — and rerunning it with `--page 2` *replaces* the numbers rather
608
- than accumulating them, which reports chapters 1-200 as missing on a corpus that is complete.
628
+ The loop is not ceremony. `--page-size` caps at 100, so a single call cannot inventory a work
629
+ with more than 100 chapters — and rerunning it with `--page 2` *replaces* the numbers rather
630
+ than accumulating them, which reports chapters 1-100 as missing on a corpus that is complete.
609
631
  Walk until `has_more` is false and check the union.
610
632
 
611
633
  `--fields` returns YAML-native types, so `chapter_number: 63` comes back as `63`. The
@@ -643,7 +665,7 @@ bm orphans # entities with no relations in the graph
643
665
  ```
644
666
 
645
667
  `orphans` finds notes with no relations. It does not find a `[[target]]` that resolves to
646
- nothing — a misspelled or renamed entity name. Check a chapter's links with `bm tool build-context memory://chapters/<slug> --depth 1
668
+ nothing — a misspelled or renamed entity name. Check a chapter's links with `bm tool context memory://chapters/<slug> --depth 1
647
669
  --project <work> --json` and look for relations whose `to_entity_id` is `null`; fix the
648
670
  spelling or add the alias, then re-run the chapter.
649
671
 
@@ -655,18 +677,32 @@ orphans behind has made the graph worse.
655
677
  With the graph complete, traverse it to find what the chapter-by-chapter pass could not see:
656
678
 
657
679
  ```bash
658
- bm grep "features [[" --page-size 200 --project <work> --json # every chapter's cast, one call
659
- bm find --meta 'note_type=theme' --fields prevalence --page-size 200 # thematic weight
680
+ bm find --meta 'note_type=theme' --fields prevalence --page-size 100 # thematic weight
660
681
  bm grep "doubloon" --page-size 100 --project <work> # every mention of a symbol
661
682
  ```
662
683
 
663
- "Which characters share the most chapters" is the first line plus a local parse of each
664
- row's `content` for `features [[...]]` — one call instead of a read per chapter. Do not
665
- reach for `bm tool build-context 'memory://characters/major/*'` here: a
684
+ "Which characters share the most chapters" is a search for `features [[` plus a local parse
685
+ of each row's `content` for `features [[...]]` — one call per 100 chapters instead of a read
686
+ per chapter. Page until `has_more` is false, as the coverage check does; a 138-chapter work
687
+ needs two pages:
688
+
689
+ ```bash
690
+ page=1; rows='[]'
691
+ while :; do
692
+ resp=$(bm grep "features [[" --page-size 100 --page $page --project <work> --json)
693
+ rows=$(jq -n --argjson acc "$rows" --argjson r "$resp" '$acc + $r.results')
694
+ [ "$(jq -r '.has_more' <<<"$resp")" = "true" ] || break
695
+ page=$((page + 1))
696
+ done
697
+ jq 'length' <<<"$rows" # one row per chapter that lists its cast
698
+ ```
699
+
700
+ Do not
701
+ reach for `bm tool context 'memory://characters/major/*'` here: a
666
702
  wildcard context is capped at 100 related rows across all primaries and returns one primary
667
703
  row per indexed observation, so it neither enumerates the cast nor walks the web.
668
- `build-context` on a *single* note is the right tool for a different question, below.
669
- `build-context` takes its URL as a positional argument — there is no `--url` option.
704
+ `context` on a *single* note is the right tool for a different question, below.
705
+ `context` takes its URL as a positional argument — there is no `--url` option.
670
706
 
671
707
  `grep` matches keywords and returns a page of 10, which quietly truncates "where does this
672
708
  appear?" — a symbol in 40 chapters comes back as 10. For symbol tracing, raise
@@ -675,14 +711,15 @@ the default page would have dropped. `--semantic` ranks by meaning instead, whic
675
711
  "what is this about?" rather than "where does this word appear?".
676
712
 
677
713
  Two more facts about `grep` rows. Matching is case-insensitive and note-level: a hit is a
678
- note, not a line, and there is no `-n` or context. And each row's `content` is capped at
714
+ note, not a line, unless you pass `-C N` for matching lines with context. And each row's
715
+ `content` is capped at
679
716
  4000 characters; `content_truncated: true` marks a cut row, so a long note's tail (such as a
680
717
  final chapter's `follows`/`precedes` relations) may be missing from a grep-driven scan.
681
718
  When a parse depends on the end of a note, `cat` that note.
682
719
 
683
720
  `--page-size` raises the ceiling, it does not remove it. A symbol in a long work can exceed
684
- even 100, so check whether the last page was full and walk `--page 2`, `--page 3` until it is
685
- not. A truncated symbol search fails the same silent way as an unpaginated `find`: a plausible
721
+ even 100, so walk `--page 2`, `--page 3` until `has_more` is false, as in the cast loop
722
+ above. A truncated symbol search fails the same silent way as an unpaginated `find`: a plausible
686
723
  answer, exit 0, and no sign that the tail is missing.
687
724
 
688
725
  And `grep` searches **your notes, not the source**. The `<work>.txt` is indexed as an entity,
@@ -738,7 +775,7 @@ This pipeline works for any literary text. Adjust schemas for genre:
738
775
  - **Relations are your web.** Every chapter should link to characters, themes, locations, and devices. Every entity should link back to chapters where it appears.
739
776
  - **Enrich iteratively.** Entity notes grow richer with each chapter. Don't try to write the perfect character note upfront — append as you go.
740
777
  - **Add prose for depth.** After structured data is in place, add interpretive essays to major notes. The prose captures what observations cannot: argument, nuance, opinion, and voice.
741
- - **Validate periodically.** Run `schema_validate` after each batch, not just at the end. Catch drift early.
778
+ - **Validate periodically.** Run `bm_schema(command="validate", project="<work-name>")` after each batch, not just at the end. Catch drift early.
742
779
  - **Quote generously.** Literary analysis lives on textual evidence. Include significant quotes as `[quote]` observations with chapter attribution.
743
780
  - **Review and revise.** After completing all chapters, review the full graph from an external perspective. Look for thin notes, missing connections, and gaps in coverage. The first pass is never the last.
744
781
  - **Analysis comes last.** Synthesis notes in `analysis/` should be written after all chapters are processed, when you have the full picture.
@@ -7,6 +7,17 @@ description: "Search Basic Memory notes by custom frontmatter fields with equali
7
7
 
8
8
  Find notes by their structured frontmatter fields instead of (or in addition to) free-text content. Any custom YAML key in a note's frontmatter beyond the standard set (`title`, `type`, `tags`, `permalink`, `schema`) is automatically indexed as `entity_metadata` and becomes queryable.
9
9
 
10
+ ## Project scope
11
+
12
+ If the project is unknown, discover it with `bm_project(command="list")`.
13
+ Pass a nonempty `project` on every note, folder, context and schema call, even
14
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
15
+ `workspace/project`, or external ID; `<project>` below is a placeholder for it.
16
+ Recent activity also requires `project`. Search accepts `project` for one
17
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
18
+ Discovery and bundled manuals need no project. Keep the same scope when following
19
+ returned paths, paginating, or recovering from a refusal.
20
+
10
21
  ## When to Use
11
22
 
12
23
  - **Filtering by status or priority** — find all notes with `status: draft` or `priority: high`
@@ -18,7 +29,7 @@ Find notes by their structured frontmatter fields instead of (or in addition to)
18
29
 
19
30
  ## The Tool
20
31
 
21
- All metadata searching uses `search_notes`. Pass filters via `metadata_filters`, or use the `tags` and `status` convenience shortcuts. Omit `query` (or pass `None`) for filter-only searches.
32
+ Use `bm_search` for ranked results or `bm_find` for file discovery with selected fields. Pass frontmatter conditions via `filters`, including tags and status. Omit `query` (or pass `None`) for filter-only searches.
22
33
 
23
34
  ## Filter Syntax
24
35
 
@@ -104,43 +115,43 @@ against the value and a comparison with null is never true.
104
115
 
105
116
  Frontmatter fields say when a note was filed or reviewed. To ask what a note
106
117
  *claims was true* on a date, use observation qualifiers (see **memory-notes**)
107
- and `search_notes(valid_at="2026-07-01")` or
108
- `search_notes(valid_overlaps="[2026-07-01,2026-08-01)")`.
118
+ and `bm_search(valid_at="2026-07-01", project="<project>")` or
119
+ `bm_search(valid_overlaps="[2026-07-01,2026-08-01)", project="<project>")`.
109
120
 
110
- ## Using `search_notes` with Metadata
121
+ ## Using `bm_search` with Metadata
111
122
 
112
- Pass `metadata_filters`, `tags`, or `status` to `search_notes`. Omit `query` for filter-only searches, or combine text and filters together.
123
+ Pass `filters` to `bm_search`. Omit `query` for filter-only searches, or combine text and filters together.
113
124
 
114
125
  ```python
115
126
  # Filter-only — find all notes with a given status
116
- search_notes(metadata_filters={"status": "in-progress"})
127
+ bm_search(filters={"status": "in-progress"}, project="<project>")
117
128
 
118
129
  # Filter-only — high-priority specs in a specific project
119
- search_notes(
120
- metadata_filters={"type": "spec", "priority": {"$in": ["high", "critical"]}},
130
+ bm_search(
131
+ filters={"type": "spec", "priority": {"$in": ["high", "critical"]}},
121
132
  project="research",
122
133
  page_size=10,
123
134
  )
124
135
 
125
136
  # Filter-only — notes with confidence above a threshold
126
- search_notes(metadata_filters={"confidence": {"$gt": 0.7}})
137
+ bm_search(filters={"confidence": {"$gt": 0.7}}, project="<project>")
127
138
 
128
- # Convenience shortcuts for tags and status
129
- search_notes(status="active")
130
- search_notes(tags=["security", "oauth"])
139
+ # Tags and status use the same filter syntax
140
+ bm_search(filters={"status": "active"}, project="<project>")
141
+ bm_search(filters={"tags": ["security", "oauth"]}, project="<project>")
131
142
 
132
143
  # Text search narrowed by metadata
133
- search_notes("authentication", metadata_filters={"status": "draft"})
144
+ bm_search("authentication", filters={"status": "draft"}, project="<project>")
134
145
 
135
- # Mix text, tag shortcut, and advanced filter
136
- search_notes(
146
+ # Mix text, tags, and an advanced filter
147
+ bm_search(
137
148
  "oauth flow",
138
- tags=["security"],
139
- metadata_filters={"confidence": {"$gt": 0.7}},
149
+ filters={"tags": ["security"], "confidence": {"$gt": 0.7}},
150
+ project="<project>",
140
151
  )
141
152
  ```
142
153
 
143
- **Merging rules:** `tags` and `status` are convenience shortcuts merged into `metadata_filters` via `setdefault`. If the same key exists in `metadata_filters`, the explicit filter wins.
154
+ Use one `filters` object for all frontmatter conditions; its keys combine with AND.
144
155
 
145
156
  ## Tag Search Shorthand
146
157
 
@@ -148,11 +159,11 @@ The `tag:` prefix in a query converts to a tag filter automatically:
148
159
 
149
160
  ```python
150
161
  # These are equivalent:
151
- search_notes("tag:tier1")
152
- search_notes("", tags=["tier1"])
162
+ bm_search("tag:tier1", project="<project>")
163
+ bm_search(filters={"tags": ["tier1"]}, project="<project>")
153
164
 
154
165
  # Multiple tags (comma or space separated) — all must match:
155
- search_notes("tag:tier1,alpha")
166
+ bm_search("tag:tier1,alpha", project="<project>")
156
167
  ```
157
168
 
158
169
  ## Example: Custom Frontmatter in Practice
@@ -183,19 +194,19 @@ Queries that find it:
183
194
 
184
195
  ```python
185
196
  # By status and type
186
- search_notes(metadata_filters={"status": "in-progress", "type": "spec"})
197
+ bm_search(filters={"status": "in-progress", "type": "spec"}, project="<project>")
187
198
 
188
199
  # By numeric threshold
189
- search_notes(metadata_filters={"confidence": {"$gt": 0.7}})
200
+ bm_search(filters={"confidence": {"$gt": 0.7}}, project="<project>")
190
201
 
191
202
  # By priority set
192
- search_notes(metadata_filters={"priority": {"$in": ["high", "critical"]}})
203
+ bm_search(filters={"priority": {"$in": ["high", "critical"]}}, project="<project>")
193
204
 
194
205
  # By tag shorthand
195
- search_notes("tag:security")
206
+ bm_search("tag:security", project="<project>")
196
207
 
197
208
  # Combined text + metadata
198
- search_notes("OAuth", metadata_filters={"status": "in-progress"})
209
+ bm_search("OAuth", filters={"status": "in-progress"}, project="<project>")
199
210
  ```
200
211
 
201
212
  ## Guidelines
@@ -204,6 +215,6 @@ search_notes("OAuth", metadata_filters={"status": "in-progress"})
204
215
  - **Use text search for content queries.** If you're looking for notes *about* something, text search is better. Combine both when you need precision.
205
216
  - **Custom fields are free.** Any YAML key you put in frontmatter becomes queryable — no schema or configuration required.
206
217
  - **Multiple filters are AND.** `{"status": "active", "priority": "high"}` requires both conditions.
207
- - **Omit `query` for filter-only searches.** `search_notes(metadata_filters={"status": "active"})` works without a text query.
218
+ - **Omit `query` for filter-only searches.** `bm_search(filters={"status": "active"}, project="<project>")` works without a text query.
208
219
  - **Dot notation for nesting.** Access nested YAML structures with dots: `{"schema.version": "2"}` queries the `version` key inside a `schema` object.
209
- - **Tags shortcut is convenient but limited.** `tags` and `status` are sugar for common fields. For anything else, use `metadata_filters` directly.
220
+ - **Use the same filters for file discovery.** `bm_find(filters={"status": "active"}, fields=["title", "priority"], project="<project>")` returns selected frontmatter values without reading each note.