Skip to content

Commit a148e72

Browse files
phernandezclaude
andauthored
feat(plugins): manual-pages flow — manpage seed schema, flow docs, verification fixes (#971)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 33e741f commit a148e72

16 files changed

Lines changed: 568 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ retention).
590590
| `BASIC_MEMORY_IMPORT_UPLOAD_MAX_BYTES` | `104857600` | Max uploaded import size |
591591

592592
```bash
593-
BASIC_MEMORY_LOG_LEVEL=DEBUG basic-memory sync
593+
BASIC_MEMORY_LOG_LEVEL=DEBUG basic-memory reindex
594594
tail -f ~/.basic-memory/basic-memory.log
595595
```
596596

docs/Docker.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ You can run Basic Memory CLI commands inside the container using `docker exec`:
111111
docker exec basic-memory-server basic-memory status
112112
113113
# Sync files
114-
docker exec basic-memory-server basic-memory sync
114+
docker exec basic-memory-server basic-memory reindex
115115
116116
# Show help
117117
docker exec basic-memory-server basic-memory --help
@@ -137,7 +137,7 @@ When using Docker volumes, you'll need to configure projects to point to your mo
137137

138138
3. **Sync the new project:**
139139
```bash
140-
docker exec basic-memory-server basic-memory sync
140+
docker exec basic-memory-server basic-memory reindex
141141
```
142142

143143
### Example: Setting up an Obsidian Vault
@@ -157,7 +157,7 @@ docker exec basic-memory-server basic-memory project create obsidian /app/data
157157
docker exec basic-memory-server basic-memory project set-default obsidian
158158
159159
# Sync to index all files
160-
docker exec basic-memory-server basic-memory sync
160+
docker exec basic-memory-server basic-memory reindex
161161
```
162162

163163
### Environment Variables

docs/character-handling.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,8 @@ finance/ (lowercase f)
184184
Use Basic Memory's built-in conflict detection:
185185

186186
```bash
187-
# Sync will report conflicts
188-
basic-memory sync
187+
# Index local file changes (conflicts are handled during the scan)
188+
basic-memory reindex
189189

190190
# Check sync status for warnings
191191
basic-memory status

docs/manual-pages.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Manual Pages
2+
3+
Basic Memory's manual is written in the style of Unix man pages — and
4+
implemented as Basic Memory notes ([#952](https://github.com/basicmachines-co/basic-memory/issues/952)).
5+
Every page is a markdown note conforming to the `Manpage` schema, `SEE ALSO`
6+
entries are real knowledge-graph relations, and every example on every page
7+
was executed against a live project before the page shipped. The manual
8+
documents the tools; the tools verify the manual.
9+
10+
## Where it lives
11+
12+
The canonical manual is the **`manual` project in the Basic Memory team
13+
workspace** (cloud, shared). Anyone can build their own: the schema ships as
14+
an opt-in seed at `plugins/claude-code/schemas/manpage.md` — copy it into any
15+
project's folder and start writing pages against it.
16+
17+
Layout:
18+
19+
```
20+
manual/
21+
├── schemas/Manpage.md # the manpage schema (type: schema)
22+
├── man1/ # CLI commands bm(1), bm-status(1), ...
23+
├── man3/ # MCP tools write-note(3), search-notes(3), ...
24+
├── man5/ # file formats bm-note(5), bm-observation(5), ...
25+
├── man7/ # concepts basic-memory(7), semantic-memory(7), ...
26+
├── playground/ # scratch notes for destructive examples
27+
└── diagrams/ # canvas visualizations of the manual graph
28+
```
29+
30+
### Why "man1", "man3", "man5"?
31+
32+
The folder names are Unix's, unchanged since 1971. The manual is divided
33+
into numbered **sections**, pages physically live in directories named
34+
after them (`/usr/share/man/man1`, `man5`, ...), and the number tells you
35+
what *kind* of thing is documented — not importance, not reading order:
36+
37+
- **1** — user commands (`ls`, `grep`)
38+
- **2** — system calls
39+
- **3** — library functions / APIs (`printf(3)`)
40+
- **4** — devices
41+
- **5** — file formats and config files (`crontab(5)`, `passwd(5)`)
42+
- **6** — games (really)
43+
- **7** — miscellanea: concepts, conventions, overviews (`regex(7)`, `signal(7)`)
44+
- **8** — system administration
45+
46+
That's also why man page names carry the parenthesized number —
47+
`crontab(1)` is the command, `crontab(5)` is the file format, same name in
48+
two sections. `man 5 crontab` picks the section explicitly.
49+
50+
This manual copies that layout with the sections that have a Basic Memory
51+
analog:
52+
53+
- **man1/**`bm` CLI commands → `bm-status(1)`
54+
- **man3/** — MCP tools, our equivalent of the "library API" section → `write-note(3)`
55+
- **man5/** — file formats: note syntax, observations, relations, schemas → `bm-note(5)`
56+
- **man7/** — concepts → `basic-memory(7)`, `semantic-memory(7)`
57+
- **8** is reserved for admin/cloud operations but has no pages yet; 2, 4,
58+
and 6 have no analog (no system calls, no devices, and no games — yet)
59+
60+
When a page says `see_also [[bm-note(5)]]`, the `(5)` reads "the
61+
file-format page," exactly the way a Unix manual cross-references — except
62+
here it's a traversable relation in the graph instead of a typographic
63+
convention. The manual explains its own conventions in `man-pages(7)`
64+
fittingly, the same page name Linux uses for this, and that almost nobody
65+
ever reads.
66+
67+
## Page anatomy
68+
69+
Pages use the classic headers where applicable: `NAME`, `SYNOPSIS`,
70+
`DESCRIPTION`, `PARAMETERS`, `MCP USAGE`, `CLI EQUIVALENT`, `EXAMPLES`,
71+
`GOTCHAS`, `SEE ALSO`. Frontmatter (validated by the schema):
72+
73+
```yaml
74+
type: manpage
75+
section: 3 # 1 | 3 | 5 | 7 | 8
76+
name: write-note # page name without section suffix
77+
summary: create or overwrite a markdown note in the knowledge base
78+
generated: hand # hand | registry | typer (regeneration ownership)
79+
tool: write_note # section-3 pages: the MCP tool documented
80+
command: basic-memory status # section-1 pages: the CLI command documented
81+
verified: 0.21.6 mcp+cli # version + path(s) that proved the page
82+
```
83+
84+
Field knowledge accumulates as observations — `[gotcha]`, `[bug]` (with issue
85+
links), `[pattern]` — and `SEE ALSO` entries are `see_also` relations, so the
86+
manual is a navigable graph, not a folder of files.
87+
88+
## How to use it
89+
90+
Man-style reads (any MCP client or the CLI):
91+
92+
```bash
93+
# read a page
94+
bm tool read-note "man3/write-note-3" --project manual
95+
96+
# apropos — find pages by section, tool, or text
97+
bm tool search-notes --project manual # then filter, or via MCP:
98+
# search_notes(project="manual", metadata_filters={"type": "manpage", "section": 3})
99+
# search_notes(project="manual", metadata_filters={"type": "manpage", "tool": "write_note"})
100+
101+
# traverse SEE ALSO from any page
102+
# build_context(url="man3/write-note-3", project="manual")
103+
```
104+
105+
A future `bm man <topic>` command is thin sugar over exactly these calls.
106+
107+
And for the real thing — `man bm` in an actual terminal:
108+
109+
```bash
110+
bm man install # copies bundled groff pages to ~/.local/share/man
111+
man bm # the overview page, rendered by man(1)
112+
man basic-memory # same page via its alias
113+
```
114+
115+
`bm man install` warns with a one-line `MANPATH` fix if the install root
116+
isn't searched by your `man`. Agents with shell access can use `man bm` as
117+
an offline quick reference; the full per-tool detail stays in the manual
118+
project's section-3 pages.
119+
120+
## The verification discipline
121+
122+
Two rules make the manual trustworthy:
123+
124+
1. **Examples must have run.** An `EXAMPLES` (or `MCP USAGE` / `CLI
125+
EQUIVALENT`) block contains only commands that actually executed against
126+
the manual project. Destructive operations (`delete_note`, `move_note`,
127+
destructive `edit_note`) run only against `playground/` notes — never
128+
against pages. The `verified:` field records the version and which path
129+
proved the page: `mcp` (live service), `cli` (dev checkout), or both.
130+
131+
2. **The schema is the linter.** Validate the whole manual any time:
132+
133+
```bash
134+
bm tool schema-validate manpage --project manual
135+
# → {"total_notes": 38, "valid_count": 38, "warning_count": 0, ...}
136+
```
137+
138+
`bm orphans --project manual` confirms every page is connected to the
139+
graph, and `schema_diff`/`schema_infer` report drift between the schema
140+
and how pages are actually written.
141+
142+
Because verification exercises real tool calls against the live service,
143+
building the manual doubles as an end-to-end smoke test. The initial build
144+
found six bugs in one pass (#954–#959) — including the verification rule
145+
catching a test that asserted a bug as expected output (#958).
146+
147+
## Adding or updating a page
148+
149+
1. Run the commands you intend to document; keep the actual output.
150+
2. Write the page with `write_note`, passing frontmatter through the
151+
`metadata` parameter (nested YAML in content frontmatter is unreliable on
152+
some clients):
153+
154+
```
155+
write_note(title="my-tool(3)", directory="man3", project="manual",
156+
note_type="manpage",
157+
metadata={"section": 3, "name": "my-tool",
158+
"summary": "...", "generated": "hand",
159+
"tool": "my_tool", "verified": "<version> mcp"})
160+
```
161+
162+
3. Link related pages in `SEE ALSO` with `see_also [[other-page(3)]]`.
163+
Forward references to pages that don't exist yet are fine — they resolve
164+
automatically when the target is written.
165+
4. Validate: `bm tool schema-validate manpage --project manual`.
166+
167+
For mechanical updates to generated sections, prefer `edit_note` with
168+
`replace_section` / `insert_after_section` so curated content (EXAMPLES,
169+
GOTCHAS, SEE ALSO, observations) survives — that ownership split is what the
170+
`generated:` field declares.
171+
172+
## Roadmap
173+
174+
- **Registry generator** — section-3 SYNOPSIS/PARAMETERS generated from the
175+
MCP tool registry (docstrings + pydantic schemas), section-1 from Typer
176+
help; the hand-written corpus is the template spec. Regenerate-and-diff in
177+
CI becomes the drift gate.
178+
- **`bm man <topic>`** — CLI sugar over `read_note` + metadata search.
179+
(`bm man install` + a hand-written `bm.1` already ship — the first slice
180+
of [#610](https://github.com/basicmachines-co/basic-memory/issues/610);
181+
the generator will produce per-command pages from the same extraction.)
182+
- **Docs site** — the notes remain canonical for sections 5 and 7, code is
183+
canonical for 1 and 3; both render to the hosted docs site.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Manpage
3+
type: schema
4+
entity: Manpage
5+
version: 1
6+
schema:
7+
gotcha?(array): string, sharp edges and surprising behavior learned from live verification
8+
example?(array): string, worked examples beyond the generated synopsis
9+
pattern?(array): string, recommended idioms and usage patterns
10+
bug?(array): string, known defects affecting this surface, with issue links
11+
see_also?(array): Entity, related manual pages — the SEE ALSO graph
12+
settings:
13+
validation: warn
14+
frontmatter:
15+
section(enum, Unix manual section number): [1, 3, 5, 7, 8]
16+
name: string, page name without section suffix (e.g. write-note)
17+
summary: string, one-line NAME description
18+
generated?(enum, who owns the mechanical sections): [registry, typer, hand]
19+
tool?: string, MCP tool this page documents (section 3 pages)
20+
command?: string, CLI command this page documents (section 1 pages)
21+
verified?: string, version and path that verified this page (e.g. 0.21.6 mcp+cli)
22+
since?: string, version this surface first appeared
23+
---
24+
25+
# Manpage
26+
27+
A **ManpageNote** is one page of a Unix-style manual implemented as Basic
28+
Memory notes (issue #952): commands in section 1, MCP tools in section 3,
29+
file formats in section 5, concepts in section 7, admin in section 8. The
30+
manual becomes a knowledge graph — `SEE ALSO` entries are typed relations,
31+
and pages are found by structured recall:
32+
`search_notes(metadata_filters={"type": "manpage", "section": 3})`.
33+
34+
This schema is an opt-in seed for documentation projects; the canonical
35+
manual lives in the Basic Memory team workspace `manual` project.
36+
37+
## What makes a good ManpageNote
38+
39+
- **NAME / SYNOPSIS / DESCRIPTION** — classic man-page structure, with
40+
PARAMETERS, MCP USAGE, CLI EQUIVALENT, EXAMPLES, GOTCHAS, SEE ALSO where
41+
applicable.
42+
- **Verified examples** — EXAMPLES contain only commands that actually ran;
43+
the `verified` field records the version and path (mcp, cli, or both).
44+
- **generated** — declares regeneration ownership: `registry` (from the MCP
45+
tool registry) and `typer` (from CLI help) pages get mechanical sections
46+
rewritten; curated sections (EXAMPLES, GOTCHAS, SEE ALSO, observations)
47+
are never overwritten.
48+
- **gotcha / bug observations** — field knowledge accumulates on pages
49+
without being clobbered by regeneration; bugs link their tracking issues.
50+
51+
## Frontmatter
52+
53+
`type: manpage` plus `section` makes the manual queryable like `man -k`:
54+
by section, by `tool`, by `command`, or by missing/stale `verified` stamps.
55+
Validation is `warn`, never blocking.

src/basic_memory/cli/commands/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from . import (
55
import_claude_projects,
66
import_chatgpt,
7+
man,
78
tool,
89
project,
910
format,
@@ -29,4 +30,5 @@
2930
"schema",
3031
"update",
3132
"workspace",
33+
"man",
3234
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Install the bundled man pages so `man bm` works."""
2+
3+
import shutil
4+
import subprocess
5+
from pathlib import Path
6+
from typing import Annotated, Optional
7+
8+
import typer
9+
from rich.console import Console
10+
11+
from basic_memory.cli.app import app
12+
13+
console = Console()
14+
15+
man_app = typer.Typer(help="Manage the bm man pages.")
16+
app.add_typer(man_app, name="man")
17+
18+
# Bundled groff sources ship inside the package (src/basic_memory/man).
19+
_MAN_SOURCE_DIR = Path(__file__).parent.parent.parent / "man"
20+
21+
22+
def _default_man_root() -> Path:
23+
# Why ~/.local/share/man: manpath(1) derives man directories from PATH
24+
# entries on both man-db (Linux) and BSD man (macOS), so ~/.local/bin on
25+
# PATH — the pipx/uv tool layout — makes this root searchable without any
26+
# MANPATH configuration.
27+
return Path.home() / ".local" / "share" / "man"
28+
29+
30+
def _man_root_on_manpath(man_root: Path) -> Optional[bool]:
31+
"""Best-effort check whether man(1) will search man_root; None if unknown."""
32+
try:
33+
result = subprocess.run(["manpath"], capture_output=True, text=True, timeout=5)
34+
except (FileNotFoundError, subprocess.TimeoutExpired):
35+
return None
36+
if result.returncode != 0:
37+
return None
38+
paths = [entry.rstrip("/") for entry in result.stdout.strip().split(":") if entry]
39+
return str(man_root).rstrip("/") in paths
40+
41+
42+
@man_app.command()
43+
def install(
44+
directory: Annotated[
45+
Optional[Path],
46+
typer.Option(
47+
"--dir",
48+
help="Man root to install into (default: ~/.local/share/man)",
49+
),
50+
] = None,
51+
) -> None:
52+
"""Install the bm man pages, then try `man bm`."""
53+
man_root = (directory or _default_man_root()).expanduser()
54+
man1 = man_root / "man1"
55+
man1.mkdir(parents=True, exist_ok=True)
56+
57+
pages = sorted(_MAN_SOURCE_DIR.glob("*.1"))
58+
if not pages: # pragma: no cover - broken packaging, not a runtime state
59+
console.print("[red]No bundled man pages found — broken installation[/red]")
60+
raise typer.Exit(1)
61+
62+
for page in pages:
63+
shutil.copyfile(page, man1 / page.name)
64+
console.print(f"installed {man1 / page.name}")
65+
66+
# Trigger: the chosen root is provably absent from manpath output.
67+
# Why: a silent install into an unsearched directory looks like success
68+
# but `man bm` still fails; say so and hand over the one-line fix.
69+
# Outcome: actionable hint; unknown (None) stays quiet to avoid false alarms.
70+
if _man_root_on_manpath(man_root) is False:
71+
console.print(
72+
f"\n[yellow]{man_root} is not on your manpath.[/yellow] Add it with:\n"
73+
f' export MANPATH="{man_root}:$MANPATH"'
74+
)
75+
76+
console.print("\nTry: [bold]man bm[/bold]")

src/basic_memory/cli/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def _version_only_invocation(argv: list[str]) -> bool:
2424
import_claude_conversations,
2525
import_claude_projects,
2626
import_memory_json,
27+
man,
2728
mcp,
2829
orphans,
2930
project,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.so man1/bm.1

0 commit comments

Comments
 (0)