mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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>
This commit is contained in:
@@ -4,6 +4,7 @@ from . import ci, status, db, doctor, import_memory_json, mcp, import_claude_con
|
||||
from . import (
|
||||
import_claude_projects,
|
||||
import_chatgpt,
|
||||
man,
|
||||
tool,
|
||||
project,
|
||||
format,
|
||||
@@ -29,4 +30,5 @@ __all__ = [
|
||||
"schema",
|
||||
"update",
|
||||
"workspace",
|
||||
"man",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Install the bundled man pages so `man bm` works."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
|
||||
console = Console()
|
||||
|
||||
man_app = typer.Typer(help="Manage the bm man pages.")
|
||||
app.add_typer(man_app, name="man")
|
||||
|
||||
# Bundled groff sources ship inside the package (src/basic_memory/man).
|
||||
_MAN_SOURCE_DIR = Path(__file__).parent.parent.parent / "man"
|
||||
|
||||
|
||||
def _default_man_root() -> Path:
|
||||
# Why ~/.local/share/man: manpath(1) derives man directories from PATH
|
||||
# entries on both man-db (Linux) and BSD man (macOS), so ~/.local/bin on
|
||||
# PATH — the pipx/uv tool layout — makes this root searchable without any
|
||||
# MANPATH configuration.
|
||||
return Path.home() / ".local" / "share" / "man"
|
||||
|
||||
|
||||
def _man_root_on_manpath(man_root: Path) -> Optional[bool]:
|
||||
"""Best-effort check whether man(1) will search man_root; None if unknown."""
|
||||
try:
|
||||
result = subprocess.run(["manpath"], capture_output=True, text=True, timeout=5)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
paths = [entry.rstrip("/") for entry in result.stdout.strip().split(":") if entry]
|
||||
return str(man_root).rstrip("/") in paths
|
||||
|
||||
|
||||
@man_app.command()
|
||||
def install(
|
||||
directory: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--dir",
|
||||
help="Man root to install into (default: ~/.local/share/man)",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install the bm man pages, then try `man bm`."""
|
||||
man_root = (directory or _default_man_root()).expanduser()
|
||||
man1 = man_root / "man1"
|
||||
man1.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pages = sorted(_MAN_SOURCE_DIR.glob("*.1"))
|
||||
if not pages: # pragma: no cover - broken packaging, not a runtime state
|
||||
console.print("[red]No bundled man pages found — broken installation[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
for page in pages:
|
||||
shutil.copyfile(page, man1 / page.name)
|
||||
console.print(f"installed {man1 / page.name}")
|
||||
|
||||
# Trigger: the chosen root is provably absent from manpath output.
|
||||
# Why: a silent install into an unsearched directory looks like success
|
||||
# but `man bm` still fails; say so and hand over the one-line fix.
|
||||
# Outcome: actionable hint; unknown (None) stays quiet to avoid false alarms.
|
||||
if _man_root_on_manpath(man_root) is False:
|
||||
console.print(
|
||||
f"\n[yellow]{man_root} is not on your manpath.[/yellow] Add it with:\n"
|
||||
f' export MANPATH="{man_root}:$MANPATH"'
|
||||
)
|
||||
|
||||
console.print("\nTry: [bold]man bm[/bold]")
|
||||
@@ -24,6 +24,7 @@ if not _version_only_invocation(sys.argv[1:]):
|
||||
import_claude_conversations,
|
||||
import_claude_projects,
|
||||
import_memory_json,
|
||||
man,
|
||||
mcp,
|
||||
orphans,
|
||||
project,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.so man1/bm.1
|
||||
@@ -0,0 +1,134 @@
|
||||
.TH BM 1 "2026-06-11" "basic-memory" "Basic Memory Manual"
|
||||
.SH NAME
|
||||
bm \- local-first knowledge base for humans and AI agents
|
||||
.SH SYNOPSIS
|
||||
.B bm
|
||||
.I COMMAND
|
||||
.RI [ ARGS ]...
|
||||
.br
|
||||
.B basic-memory
|
||||
.I COMMAND
|
||||
.RI [ ARGS ]...
|
||||
.SH DESCRIPTION
|
||||
.B bm
|
||||
manages Basic Memory projects: plain markdown files that form a knowledge
|
||||
graph. Files are the source of truth; SQLite provides indexing and
|
||||
full-text search; the same operations are exposed to AI agents over the
|
||||
Model Context Protocol (MCP) and to humans and scripts through this CLI.
|
||||
.PP
|
||||
Notes use semantic markdown: observations
|
||||
.RB ( "\- [category] text #tag" )
|
||||
and relations
|
||||
.RB ( "\- relation_type [[Target]]" )
|
||||
become queryable graph structure. Projects route independently to the
|
||||
local API or to Basic Memory Cloud.
|
||||
.SH COMMANDS
|
||||
Knowledge operations:
|
||||
.TP
|
||||
.B bm tool
|
||||
CLI access to the MCP tools (write-note, read-note, search-notes,
|
||||
build-context, ...). These emit JSON and are the scriptable surface.
|
||||
.TP
|
||||
.B bm status
|
||||
Show sync status between files and the database.
|
||||
.TP
|
||||
.B bm reindex
|
||||
Index local file changes and rebuild search/embeddings. This is the manual
|
||||
sync trigger when no MCP server is running.
|
||||
.TP
|
||||
.B bm doctor
|
||||
Run end-to-end file/database consistency checks.
|
||||
.TP
|
||||
.B bm orphans
|
||||
List entities with no relations in the knowledge graph.
|
||||
.TP
|
||||
.B bm format
|
||||
Run configured formatters over note files.
|
||||
.PP
|
||||
Projects and schemas:
|
||||
.TP
|
||||
.B bm project
|
||||
Add, remove, list projects; set the default; flip a project between local
|
||||
and cloud routing.
|
||||
.TP
|
||||
.B bm schema
|
||||
List, validate, infer, and drift-check Picoschema note-type contracts.
|
||||
.PP
|
||||
Data and cloud:
|
||||
.TP
|
||||
.B bm import
|
||||
Import from ChatGPT, Claude, or memory.json exports. Imports write files;
|
||||
run
|
||||
.B bm reindex
|
||||
afterwards.
|
||||
.TP
|
||||
.B bm cloud
|
||||
Authenticate, sync (push/pull/sync/bisync), snapshots, and team workspace
|
||||
administration.
|
||||
.PP
|
||||
Infrastructure:
|
||||
.TP
|
||||
.B bm mcp
|
||||
Run the MCP server (hosts the live file watcher).
|
||||
.TP
|
||||
.B bm man
|
||||
Manage these man pages
|
||||
.RB ( "bm man install" ).
|
||||
.TP
|
||||
.B bm reset
|
||||
Drop and recreate the database (destructive).
|
||||
.PP
|
||||
Most commands accept
|
||||
.B \-\-project
|
||||
.I NAME
|
||||
to target a project and
|
||||
.BR \-\-local / \-\-cloud
|
||||
to override routing.
|
||||
.SH EXAMPLES
|
||||
Write and find a note from the shell:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
echo "# Standup notes" | bm tool write-note \\
|
||||
\-\-title "Standup" \-\-folder notes
|
||||
bm tool search-notes "standup"
|
||||
.RE
|
||||
.fi
|
||||
.PP
|
||||
Pick up files created outside the tools:
|
||||
.PP
|
||||
.nf
|
||||
.RS
|
||||
bm status # shows pending changes
|
||||
bm reindex # indexes them
|
||||
.RE
|
||||
.fi
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.basic-memory/config.json
|
||||
Projects, default project, per-project routing modes, cloud settings.
|
||||
.TP
|
||||
.I ~/.basic-memory/memory.db
|
||||
SQLite index (derived; safe to rebuild with bm reindex).
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B BASIC_MEMORY_FORCE_LOCAL
|
||||
Force local routing regardless of cloud mode.
|
||||
.TP
|
||||
.B BASIC_MEMORY_LOG_LEVEL
|
||||
Logging verbosity (e.g. DEBUG).
|
||||
.SH SEE ALSO
|
||||
Full manual (machine-readable, agent-traversable): the
|
||||
.I manual
|
||||
Basic Memory project \(em see docs/manual-pages.md in the repository.
|
||||
Documentation: https://docs.basicmemory.com
|
||||
.PP
|
||||
For AI agents: the complete tool reference lives in the manual project as
|
||||
section\-3 pages (write-note(3), search-notes(3), ...), queryable via
|
||||
.B search_notes
|
||||
with
|
||||
.BR "metadata_filters={\(dqtype\(dq: \(dqmanpage\(dq}" .
|
||||
.SH BUGS
|
||||
https://github.com/basicmachines-co/basic-memory/issues
|
||||
.SH AUTHORS
|
||||
Basic Machines (https://basicmachines.co)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Shared loader for the bundled cloud-discovery markdown resources."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_discovery_resource(filename: str) -> str:
|
||||
"""Read a bundled discovery markdown file with promo placeholders rendered.
|
||||
|
||||
The markdown carries a {{OSS_DISCOUNT_CODE}} placeholder so the promo code
|
||||
has one source of truth (cli.promo); substitute before it reaches users.
|
||||
"""
|
||||
# Import here to avoid pulling CLI promo machinery (analytics, rich, config)
|
||||
# into the MCP server import graph at module load.
|
||||
from basic_memory.cli.promo import OSS_DISCOUNT_CODE
|
||||
|
||||
content = (Path(__file__).parent / filename).read_text(encoding="utf-8")
|
||||
return content.replace("{{OSS_DISCOUNT_CODE}}", OSS_DISCOUNT_CODE)
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Cloud information MCP tool."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from basic_memory.mcp.resources.discovery import load_discovery_resource
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@@ -13,5 +12,4 @@ from basic_memory.mcp.server import mcp
|
||||
)
|
||||
def cloud_info() -> str:
|
||||
"""Return optional Basic Memory Cloud information and setup guidance."""
|
||||
content_path = Path(__file__).parent.parent / "resources" / "cloud_info.md"
|
||||
return content_path.read_text(encoding="utf-8")
|
||||
return load_discovery_resource("cloud_info.md")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Release notes MCP tool."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from basic_memory.mcp.resources.discovery import load_discovery_resource
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@@ -13,5 +12,4 @@ from basic_memory.mcp.server import mcp
|
||||
)
|
||||
def release_notes() -> str:
|
||||
"""Return the latest product release notes for optional user review."""
|
||||
content_path = Path(__file__).parent.parent / "resources" / "release_notes.md"
|
||||
return content_path.read_text(encoding="utf-8")
|
||||
return load_discovery_resource("release_notes.md")
|
||||
|
||||
@@ -828,6 +828,11 @@ async def search_notes(
|
||||
Formatted markdown text (output_format="text"), dict (output_format="json"),
|
||||
or helpful error guidance string if search fails
|
||||
|
||||
Pagination note: `total` is exact only for text/title/permalink searches.
|
||||
Vector and hybrid searches skip the count query (it would cost a second
|
||||
semantic retrieval pass) and report `total: 0` even when results are
|
||||
returned — use `has_more` for pagination in those modes.
|
||||
|
||||
Examples:
|
||||
# Basic text search
|
||||
results = await search_notes("project planning")
|
||||
|
||||
Reference in New Issue
Block a user