feat(cli): add Rich human-readable output to bm tool commands#967
feat(cli): add Rich human-readable output to bm tool commands#967groksrc wants to merge 11 commits into
Conversation
search-notes, read-note, build-context, and recent-activity now display formatted Rich output (tables, panels, Markdown rendering) when stdout is an interactive TTY. When piped or redirected the commands continue to emit raw JSON exactly as before. A new --json flag is available on each command to force JSON output even in a TTY. Follows the bm status / bm project list precedent: Rich by default for humans, JSON for machines. Closes #678 Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
…Result shape
_display_build_context was reading top-level item.get("title")/item.get("type")
on each results[i], but the real GraphContext.model_dump() shape wraps every item
as a ContextResult with primary_result + related_results nested inside. This
made every related note render as an empty tree node.
Fix:
- Rewrite _display_build_context to iterate context_items, build a primary-result
node from item["primary_result"], then add each item["related_results"] entry as
a child with relation_type/type/title rendered.
- Update _display_search_results to use the real SearchResponse key "current_page"
(not "page"), pass query from the CLI argument, add Score and Snippet columns
(score + matched_chunk/content truncated to 200 chars) as the issue requested.
- Update test fixtures in test_cli_tool_rich_output.py to match the real payload
shapes (nested ContextResult, current_page, score/matched_chunk fields, no
updated_at in recent-activity).
- Fix test_build_context_json_flag_overrides_tty assertion to navigate the nested
primary_result/related_results shape.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
MCP tools return str | dict depending on output_format; the Rich display helpers require the dict (or list) shape. String payloads keep main's behavior of printing through the JSON path. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 457cd0ed3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| title = result.get("title", "") | ||
| permalink = result.get("permalink", "") | ||
| content = result.get("content", "") |
There was a problem hiding this comment.
Preserve frontmatter requested by the flag
When bm tool read-note ... --include-frontmatter runs in an interactive terminal, this new Rich path renders only title, permalink, and content, so the explicitly requested frontmatter disappears unless the user also knows to add --json. That regresses the documented --include-frontmatter behavior for the default TTY path; render result["frontmatter"] when present or keep JSON output for that flag.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in e079eca.
Verified: _display_read_note was reading title, permalink, and content but never accessing result["frontmatter"], so --include-frontmatter data was silently dropped in the Rich path.
Fix: when frontmatter is non-empty, render a dim key/value panel (using a borderless Table inside a Panel) above the note content. Added test_read_note_rich_include_frontmatter asserting the frontmatter panel header and key/value (tags/test) appear in output when --include-frontmatter is passed.
| # --- Related items as children --- | ||
| related: list[dict[str, Any]] = list(context_result.get("related_results", [])) |
There was a problem hiding this comment.
Include observations in context output
For build-context responses where an entity has observations, the new TTY default only adds related_results under each primary item and never renders context_result["observations"]. Since the previous JSON default exposed those observation summaries, users running the command interactively now lose core context facts whenever they do not pass --json.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in e079eca.
Verified: _display_build_context iterated related_results under each primary_result node but never accessed context_result["observations"], so the ObservationSummary facts exposed in JSON output were completely absent in the Rich path.
Fix: after rendering the primary node, iterate observations (real ContextResult field: list of ObservationSummary with category + content) and add each as a dim [category] content leaf, truncating at 120 chars. Updated the BUILD_CONTEXT_RESULT fixture to include a real ObservationSummary shape (type/category/content/permalink/file_path/created_at). Also updated the subtitle to show N observations. Added test_build_context_rich_renders_observations asserting the observation category and content text appear in output.
- `_display_read_note`: render frontmatter key/value panel when present so `--include-frontmatter` is not silently dropped in the Rich path - `_display_build_context`: render ContextResult.observations under each primary node (category + truncated content) so interactive users see the same core facts as `--json` output; update subtitle to include count - Update BUILD_CONTEXT_RESULT fixture with real ObservationSummary shape (type/category/content/permalink/file_path/created_at) - Add test_read_note_rich_include_frontmatter asserting frontmatter keys appear - Add test_build_context_rich_renders_observations asserting category/content visible Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
…er flag gate
Bug 1 — Rich markup injection: user-sourced titles, permalinks, snippets,
observation categories/content, and frontmatter keys/values were interpolated
directly into Rich markup strings, causing bracketed text (e.g. "[draft]",
"[fact]") to be silently swallowed or restyled. Apply markup_escape() at
every injection point in _display_search_results, _display_read_note,
_display_build_context, and _display_recent_activity. Observation labels use
markup_escape on the full "[category] content" fragment so the literal brackets
around the category are also escaped.
Bug 2 — frontmatter panel ignores flag: _display_read_note rendered the
frontmatter panel whenever result["frontmatter"] was non-empty, but the JSON
payload always carries that key regardless of --include-frontmatter. Thread
the boolean flag as a keyword argument and gate the panel on both the flag and
non-empty content.
Bug 3 — "0 result(s)" subtitle: the search API returns total=0 even when
results is non-empty. result.get("total", len(results)) never triggered its
default because the key exists; fall back to len(results) when total is falsy
but results is non-empty, keeping page-count math consistent.
Tests: add cases for bracketed title surviving search output, "[fact]" category
surviving build-context tree, read-note without --include-frontmatter asserting
no frontmatter panel, and search with total=0 asserting correct subtitle count.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
The four interactive bm tool commands (search-notes, read-note,
build-context, recent-activity) now support three output modes instead of
two:
- JSON — raw machine-readable output (--json, or automatically when piped).
- Rich — colored Panel/Table/Tree/Markdown (the default TTY experience).
- Plain — undecorated, greppable text with no ANSI colors, box-drawing, or
markup (--plain, forced even when piped).
Precedence, highest first: --json > --plain > non-TTY (JSON) > TTY (config
style). Passing both --json and --plain is a typer error with a non-zero
exit. Each TTY default is governed by the new BasicMemoryConfig field
cli_output_style ("rich"/"plain", default "rich", env
BASIC_MEMORY_CLI_OUTPUT_STYLE), mirroring the existing CLI-behavior config
conventions; its Field description documents the setting since the repo has
no general CLI-settings reference doc.
Plain renderers deliberately do NOT apply rich.markup.escape — escaping is
only correct on the Rich path and would corrupt literal brackets — so
[draft]/[fact] survive verbatim. The plain search path keeps the total=0
fallback so the corrected result count matches the Rich path. String tool
results still route to JSON to preserve ty narrowing.
Module docstring and each command's help text now document the three modes
and the config default.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
|
The API content field keeps the blank line left by frontmatter stripping; plain print() rendered it as a double gap under the header. JSON mode stays byte-faithful. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
With the flag the API returns the literal file as content, so both display paths printed the frontmatter twice (synthesized block/panel + the block inside content). Plain now prints the file verbatim; Rich keeps the panel and strips the block from the Markdown body. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
…de-frontmatter The file's own frontmatter carries title/permalink, so the header line duplicated it. Plain without the flag keeps the header. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
Drop the synthesized title/permalink header and the (no content) placeholder: plain mode is the content verbatim (note body, or the literal file with --include-frontmatter). Decoration belongs to the Rich path. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
Keep --include-frontmatter as a deprecated alias for back-compat (the old name shipped in 0.22.0). The MCP tool parameter include_frontmatter is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
|
This is good to go, but needs to ship in v0.next |
Closes #678
Summary
Adds human-readable terminal output to four
bm toolcommands —search-notes,read-note,build-context,recent-activity— with three output modes and a configurable interactive default.Output modes & precedence
--json, or any non-TTY stdout (piped/redirected) — unchanged script compat--plain— greppable text, no ANSI colors, no box-drawing, no markup (dumb terminals, screen readers, CI logs)Precedence:
--json>--plain(forces plain even when piped) > non-TTY → JSON > TTY → configured style.--json --plaintogether is an error. Resolution is centralized in_resolve_output_mode()rather than per-command branching.New config option
cli_output_style: "rich" | "plain"(defaultrich, envBASIC_MEMORY_CLI_OUTPUT_STYLE) sets the interactive default for folks whose terminals don't render Rich well. Documented via the config Field description (the repo has no general CLI-settings doc yet — noted on #977).Renderers
len(results)when the API returns the knowntotal=0-with-results quirk--include-frontmatter(the flag makes the API return the literal file as content; the Rich path strips the block from the body so it renders once). Plain: the payload verbatim — note body, or the literal file with the flag — no header, no placeholders, soread-note X --plain --include-frontmatter > note.mdround-trips a note byte-faithfullyContextResultshape: primary entities, their observations ([category] content), and related results with relation typesHardening (from manual QA + Codex review)
rich.markup.escape— a note titledSpec [draft] v2renders literally instead of being eaten as a style tag (and[red]can't restyle your terminal). Plain mode deliberately does NOT escape (it isn't markup).ty'sstr | dictnarrowing clean).Tests
test_cli_tool_rich_output.py: 40 tests covering Rich + plain + JSON modes per command, the full precedence matrix (incl.--plainwhile piped, config-driven defaults,--json --plainconflict), bracket-survival in both Rich and plain, frontmatter flag gating, and thetotal=0count fallback. Fixtures use the real payload shapes (current_page, nestedprimary_result/observations/related_results). All existing JSON-path tests unchanged and green.Flag rename
bm tool read-note --include-frontmatteris renamed to--frontmatter; the old name remains as a deprecated alias (it shipped in 0.22.0). MCP tool parameterinclude_frontmatteris unchanged.Manual updates
manual/man3/read-note-3(team workspace) — CLI synopsis uses--frontmatter, ARGUMENTS notes the deprecated alias.--json/--plain/ Rich default,cli_output_styleconfig, plain read-note's faithful/round-trip behavior) —read-note-3's "json mode only" framing changes once this PR's display modes ship.Review notes
RECENT_ACTIVITY_RESULTfixture documents that the real_extract_recent_rowsoutput has noupdated_atkey (display falls back tocreated_at).🤖 Generated with Claude Code