Compare commits

...

594 Commits

Author SHA1 Message Date
phernandez 2ae8115f39 test(cli): keep delete-note search assertions deterministic
Signed-off-by: phernandez <paul@basicmachines.co>
2026-06-06 19:57:39 -05:00
phernandez 86d044818c test(cli): cover delete-note integration paths
Signed-off-by: phernandez <paul@basicmachines.co>
2026-06-06 18:48:43 -05:00
Adit Karode c6c0c011af fix(cli): fail partial directory deletes
Signed-off-by: Adit Karode <adit.karode@gmail.com>
2026-06-03 11:26:33 -04:00
Adit Karode 9fa7cb9155 feat(cli): add delete-note tool command
Surfaces the delete-note command on the CLI tool app, delegating to the mcp_delete_note tool. Supports deleting notes and directories via --is-directory.

Signed-off-by: Adit Karode <adit.karode@gmail.com>
2026-06-03 11:15:28 -04:00
Paul Hernandez fc2ee07076 feat(api): expose owning project on entity resolve
Expose project_external_id on v2 entity resolve responses so cloud callers can authorize cross-project targets without a second tenant DB lookup.
2026-06-02 18:01:17 -05:00
phernandez 61891f2e33 fix(plugins): address PR review — scope captureFolder to checkpoints, ASCII echo
Review feedback on #876:
- Codex (P2): the SessionStart "Where to write" brief said all auto-capture goes to
  captureFolder, but captureFolder is only the PreCompact checkpoint folder. Narrowed
  it — checkpoints go to captureFolder; decisions/tasks/notes follow
  placementConventions (or topic folders when none are set), so proactive captures
  aren't dumped into sessions/ alongside checkpoints.
- Claude review: replaced the em dash in the skills-source-guard echo string with a
  hyphen so the instruction's console output stays ASCII (CLAUDE.md compatibility).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 15:05:01 -05:00
phernandez 5f59ca49a2 docs(plugins): fold in setup debrief fixes — cloud-create, skills-source guard, shared-read cap
- Project mapping: document local vs cloud project creation — a cloud-workspace
  project needs a cloud-connected MCP server and a workspace selector; a purely
  local server fails to mkdir the cloud-style path. Pin cloud primaryProject to the
  external_id UUID.
- Install shared skills: guard against clobbering a source checkout — if ./skills is
  git-tracked and holds memory-* dirs (the skills' own source repo), skip the npx
  install instead of overwriting the working copy with published versions.
- Cloud/teams: warn that the SessionStart brief reads only the first 6 shared
  projects per session, so order the most relevant first when more are configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 15:05:01 -05:00
phernandez 2ba1ca8722 feat(plugins): make setup focus load-bearing, seed schemas via metadata, add smoke test
- Focus question is now load-bearing: it drives an adaptive light folder-structure
  suggestion and is stored. The placement step branches learn (existing project
  with notes) vs suggest (new/empty project) instead of only learning.
- Seed schema notes via the write_note `metadata` param instead of content
  frontmatter. The cloud write path silently coerces nested YAML to the string
  '[object Object]', corrupting schema/settings (basic-memory-cloud#1000); the
  metadata param round-trips correctly on both local and cloud.
- Surface placementConventions + captureFolder in the SessionStart brief so the
  output style's "follow stored placement conventions" reflex has something to
  follow — previously written by setup but never shown to Claude (dead config).
- Add a post-settings smoke test (run the hook's recall query, confirm routing)
  and a restart prompt gated on outputStyle in the close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 15:05:01 -05:00
phernandez 4b6de80faa fix(plugins): hooks fall back to uvx/uv when no CLI binary is on PATH
Codex flagged (review 4397232451) that the hooks locate the CLI via
`command -v basic-memory || command -v bm`, but our own README recommends
connecting the MCP server as `claude mcp add basic-memory -- uvx basic-memory mcp`
— an ephemeral uv run that leaves NO binary on PATH. In that (recommended) setup
both hooks exited before doing anything, so the bridge silently did nothing even
though the MCP server worked.

Both hooks now resolve the CLI invocation as: prefer a `basic-memory`/`bm` binary
(fast), else fall back to `uvx basic-memory`, else `uv tool run basic-memory`, else
silent no-op. The launcher may be multi-token, so the embedded Python splits it with
shlex and prepends it to each command list. The uv cache is already warm from
running the MCP server, so the fallback is cheap.

Verified: with no binary on PATH but uvx present, SessionStart now produces a brief
(previously a silent no-op). Updated the README requirement to note uvx-only works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 1eb4cd154f fix(plugins): route UUID project refs consistently across all skills
Codex flagged /basic-memory:remember passing a UUID primaryProject as `project`
(review 4397206729) — the same routing bug already fixed in the hooks and the
share skill. Rather than fix only the flagged spot, audited every project-ref
site and made them all consistent:

- remember: route primaryProject as project_id when it's a UUID (was the flag)
- status: same for the primaryProject-scoped search_notes queries
- setup: same for the schema-seed write to primaryProject

share/SKILL.md and both hooks (session-start, pre-compact) already handle it.
That's now every place the plugin routes to a project ref — a project ref may be
a workspace-qualified name (-> project) or an external_id UUID (-> project_id),
and all six sites detect and route accordingly. Plugin validates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez bfee8958ad docs(integrations): document the defensive getattr in the Hermes provider
Addresses the remaining review finding (deferred while #859 was open; now merged,
so the hermes module is on main and the comment lands cleanly here). The two
getattr sites read MCP SDK result/tool fields; `mcp` is an unpinned dependency
(plugin.yaml), and its CallToolResult / ListToolsResult / Tool shapes have varied
across SDK versions — so the defensive getattr is intentional, not the speculative
kind CLAUDE.md warns against. Added constraint comments at both sites explaining why.

No behavior change. `just package-check-hermes` passes (244 passed, 12 integration
tests skipped without BM_INTEGRATION).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 63f5c62dfd fix(plugins): address review findings (UUID routing, outputStyle location, line length)
Resolves the in-scope findings from the github-actions review (its prepared commit
1283a46 couldn't push — 403) and the Codex P2 (review 4397168884):

- pre-compact.sh: route a UUID primaryProject via --project-id, not --project.
  This mirrors session-start.sh; without it a UUID-configured project gets correct
  session briefs but SILENT checkpoint failures at every compaction. The substantive
  correctness fix. Verified end-to-end (checkpoint now lands in a UUID-keyed project).
- skills/share/SKILL.md: same UUID routing for /basic-memory:share team targets —
  pass external_id UUIDs as project_id, qualified names as project. (Codex P2.)
- skills/status/SKILL.md: read outputStyle from the ROOT settings object, not the
  basicMemory block — otherwise /basic-memory:status reports capture reflexes as off
  for a correctly-configured user.
- docs/getting-started.md: move outputStyle out of the basicMemory block to root in
  the team example (matches settings.example.json; it's a top-level Claude Code key).
- session-start.sh: split the >100-char project-routing line.

Out of scope here: the hermes/__init__.py getattr-rationale comments the bot
prepared live in the #859 consolidation code (not in this PR's diff) — flagged for
that PR. Plugin validates; hooks smoke-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 451f27cfd9 docs: cross-link MCP-server prereq with the Claude Code plugin install
The MCP server is a hard prerequisite for the plugin (its hooks and skills call
it), but the two were in separate README sections with no link between them.

- The "Claude Code plugin" install section now points to "Connect your AI client"
  and states the MCP server must be connected first.
- The "Connect your AI client → Claude Code" section now points to the plugin for
  the full memory bridge.
- /basic-memory:setup's prerequisite check is now concrete: verify the MCP server
  via list_memory_projects, and if absent, walk the user through installing and
  `claude mcp add basic-memory -- uvx basic-memory mcp` before starting the interview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez df5f0f4c04 docs: fix stale Claude Code plugin description in root README
The umbrella install section still described the plugin as bundling "skills,
hooks, and an agent" — the agent was removed in the v0.4 redesign. Updated to the
current surface: hooks (briefings + checkpoints), the opt-in output style, and the
/basic-memory:* skills.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 731e7baac1 fix(plugins): address Codex review P2s on PR #865
1. SessionStart worker pool was sized 8 but submits up to 9 searches (3 primary
   + MAX_SHARED=6 shared) — the 9th queued behind a possibly-10s call and could
   push the hook past Claude Code's 20s SessionStart timeout before the brief
   printed. Size the pool to 3 + MAX_SHARED so nothing queues. (Introduced in
   Phase 4 when the third primary query was added.)

2. captureChattyness was dead config — written by setup but read by no hook, and
   its "heavy" level advertised "checkpoint without compaction," which no hook
   implements. The real proactivity knob is `outputStyle`. Removed
   captureChattyness entirely and folded the "how active should I be?" question
   into the output-style step (the single, actually-wired toggle); the always-on
   hooks run regardless. Updated setup skill, settings.example.json, and DESIGN.

Both flagged by Codex (review 4395911984); same class as the recallTimeframe
dead-config fix from the earlier self-review. Plugin validates; hook smoke-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 877e52fd73 feat(skills): port useful retired plugin skills into the shared memory-* set
Following the evaluation on #866, the genuinely-useful behavior from the deleted
Claude Code plugin skills is preserved as framework-agnostic shared skills (usable
by any MCP agent — Claude Desktop, OpenClaw, etc.), instead of being lost in the
v0.4 clean-break. The Claude Code plugin itself is unchanged — it deliberately
relies on hooks + the output style for capture/recall and pulls these shared skills
via /basic-memory:setup.

New shared skills (ported from the deleted set, CC-specific glue stripped):
- memory-curate (from knowledge-organize) — the real coverage gap: knowledge-GRAPH
  curation (orphan notes, relation suggestion, duplicate merge, tag/folder audit,
  hub notes, sparse-note enrichment). Distinct from memory-defrag, which is
  agent-memory-FILE hygiene.
- memory-continue (from continue-conversation) — resume prior work by rebuilding
  context from the graph (build_context / recent_activity / search), timeframe table,
  resume playbooks.
- memory-capture (from knowledge-capture) — synthesize a thread's current state into
  one note, rewriting in place via a thread_id key. The Claude-Code-specific session
  UUID is generalized to "any stable thread id your host exposes."

Also completed memory-notes' edit reference (added prepend / replace_section to the
two existing operations). Updated skills/README.md and skills/CLAUDE.md.

`just package-check-skills` validates 13 skills. NOTE: skills/skills-lock.json is a
generated artifact of the `npx skills` CLI (not used by Claude Code or OpenClaw) and
should be regenerated once the set is final — not hand-edited here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez fc8ac86fa1 feat(plugins): setup installs the shared memory-* skills (no repo dup)
Addresses the consolidation direction from issue #866: the Claude Code plugin
should consume the canonical skills/ set, not reinvent a parallel one. Rather
than vendoring copies into the plugin, /basic-memory:setup now offers to install
the shared memory-* skills via `npx skills add basicmachines-co/basic-memory
--path skills` (the existing SPEC-58 distribution path, same source OpenClaw
bundles). The plugin stays "hooks + Claude-Code-specific skills" and pulls the
shared toolkit on demand — skills/ remains the single source of truth, no
duplication in the repo.

Updated the setup interview + apply steps, README, getting-started, and CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez df3eb3208c fix(plugins): address v0.4 plugin code-review findings (PR #865)
Resolves the 8 self-review findings on PR #865:

session-start.sh:
- Wire recallTimeframe into a new "Recent sessions" query (--after_date) — it was
  parsed but unused after the Phase 4 rewrite. The brief now surfaces recent
  session checkpoints (the resume cursor), which is the one query the recency
  window legitimately applies to. (#1)
- Guard secondaryProjects/teamProjects JSON types — a string value was iterated
  character-by-character into bogus per-char queries. (#3)
- A configured-but-unreachable/misnamed primaryProject now emits a one-line
  "couldn't read" signal instead of a silent blank brief. (#7)

pre-compact.sh:
- Filter transcript turns by the `isMeta` and `toolUseResult` flags instead of a
  `text.startswith("<")` heuristic. This stops dropping legitimate user messages
  that start with "<" (#4) and stops capturing tool-result/meta frames as human
  turns (#8) — verified against a real transcript (25 human turns cleanly
  separated from 8 meta + 288 tool-result frames).
- Title now uses second precision so rapid same-minute compactions don't collide
  and silently drop/overwrite a checkpoint; removed the unused stamp/slug. (#2)
- Empty-checkpoint guard now requires at least one real user turn, not just any
  turn, so an assistant-only transcript can't produce a dangling-title note. (#5)

validate_skills.py:
- parse_frontmatter now skips indented lines, capturing only top-level keys, so a
  schema note's nested schema:/settings: children can't overwrite a top-level
  type/entity via last-write-wins. Documented its single-line-only limitation. (#6)

All verified end-to-end; `just package-check-claude-code` and
`package-check-skills` pass; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 2886ab64e1 docs(plugins): add architecture, why, and getting-started docs (v0.4 Phase 5)
Adds user- and contributor-facing documentation under plugins/claude-code/docs/:

- why-combine-memory.md — the value proposition: two-memories/two-jobs, the
  1+1=3 framing, the three personas (thinker / builder / operator) with concrete
  use cases, and the official "use both" stance. The "why would I want this" doc.
- getting-started.md — a ~5-minute guided walkthrough: prerequisites, install,
  /basic-memory:setup, a capture→checkpoint→brief loop to see it work, the team
  workspace path, tuning knobs, and troubleshooting.
- architecture.md — how it works flow by flow, with mermaid diagrams: the bridge
  (working memory <-> durable graph), the four surfaces, SessionStart and
  PreCompact sequence flows, capture reflexes, team read/share, component map.

Linked all three from the README's new Documentation section. Marked Phase 5
docs done in DESIGN. Migration guide intentionally skipped — v0.4 is a clean
break (uninstall old, install new), noted in the CHANGELOG. Dogfood remains the
open human step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 36c245f0d2 feat(plugins): team workspace support + /basic-memory:share (v0.4 Phase 4)
Implements DESIGN.md Phase 4, grounded in a real two-workspace BM Cloud account.

Recall reads across the team:
- hooks/session-start.sh rewritten to read the primary project (active tasks +
  open decisions) AND each configured shared/team project (open decisions) in
  parallel via ThreadPoolExecutor. Routes by workspace-qualified name or
  external_id UUID (project names collide across workspaces, so bare names won't
  route); per-call timeout, capped at 6 shared projects, graceful on any failure.
  Adds a "From shared projects (read-only)" section + the share-vs-capture note.
  Verified against the real my-team-2 workspace (OAuth routing) and local fixtures.

Deliberate team writes:
- skills/share/SKILL.md → /basic-memory:share <note>: copies a note from the
  primary project into a configured teamProjects target's promoteFolder, with
  shared_from attribution and a confirmation step. Preserves the note's type so
  shared decisions stay findable in the team's structured recall. Pulled forward
  from future-work since team usage needs a safe write path.

Safe by default: capture (PreCompact checkpoints, /remember) NEVER writes to a
shared project. The proposed teamProjects.autoWrite flag is deliberately not
shipped — documented as future rather than ship an unenforced flag.

Config: secondaryProjects (read sources) + teamProjects (share targets with
promoteFolder), both requiring qualified names/UUIDs. setup interview step 3 now
configures them via list_workspaces; status reports team read-sources + share
targets; settings.example.json documents the shape. REQUIRED_SKILLS adds share.

Discovery verified: Skills (4): remember, setup, share, status. Passes
`just package-check-claude-code` incl. `claude plugin validate . --strict`.
Updated README (Teams section), CHANGELOG, and DESIGN §6 + Phase 4 status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 2bd52676d9 feat(plugins): add /basic-memory:setup bootstrap interview (v0.4 Phase 3)
Adds the bootstrap interview from DESIGN.md Phase 3 as a prose skill, plus the
first-run nudge that points users to it.

- skills/setup/SKILL.md → /basic-memory:setup: an adaptive ~2-min interview that
  maps the Claude Code project to a Basic Memory project (pick existing or create),
  seeds the session/decision/task schemas into it, optionally learns the project's
  placement conventions (list_directory + sampling, stored in placementConventions),
  writes the basicMemory settings block, and enables the capture output style.
- hooks/session-start.sh: nudges toward /basic-memory:setup on first run (when no
  basicMemory config block exists). The nudge now survives a failed/empty task
  query, so a brand-new user with no project yet still sees it; it stops once setup
  writes the config (config presence is the sentinel — no separate file).
- validate_claude_plugin.py: REQUIRED_SKILLS now includes setup.

Corrects the Phase 1 schema-seeding finding: writing a schema file's content via
write_note (CLI or MCP) indexes it as type: schema AND resolves via schema_validate
— the earlier "must use note_type/metadata" conclusion was confounded by the enum
YAML bug (since fixed). Verified end-to-end: all three schemas seed, index, and
resolve (entity=Session/Decision/Task); nudge verified across all config states;
discovery shows Skills (3): setup, remember, status.

Updated README, CHANGELOG, and DESIGN Phase 3 status. Passes
`just package-check-claude-code` incl. `claude plugin validate . --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 215a2d9f99 feat(plugins): add /basic-memory:remember and :status skills (v0.4 Phase 2)
Adds the two deliberate-gesture skills from DESIGN.md Phase 2, both as prose
skills (skills are prompts) — robust to arbitrary user text and any MCP server
tool-name prefix, no bundled-script path placeholders.

- skills/remember/SKILL.md → /basic-memory:remember <text>: quick capture to the
  rememberFolder (default bm-remember) with a first-line title and manual-capture
  tag, via write_note. Model-invocable so "remember that…" triggers it.
- skills/status/SKILL.md → /basic-memory:status: user-only diagnostic
  (disable-model-invocation) reporting active project, capture/remember folders,
  output-style state, recent session checkpoints, and active-task count.

validate_claude_plugin.py now requires the shipped skill set (REQUIRED_SKILLS)
and validates each skill's name/description frontmatter. Passes
`just package-check-claude-code` incl. `claude plugin validate . --strict`.

Discovery verified end-to-end: installed from a local marketplace and confirmed
via `claude plugin details` — Skills (2): remember, status; Hooks (2):
SessionStart, PreCompact; Agents (0) — namespaced as /basic-memory:<name>.

Updated README (Commands table), CHANGELOG, and DESIGN Phase 2 status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez f485c1ef3f chore(plugins): add scoped version recipes for plugin/agent artifacts
Adds a --scope {all,core,packages} option to scripts/update_versions.py so the
version bump can target just the host-native agent artifacts (Claude Code plugin
+ root/local marketplaces, Hermes, OpenClaw) separately from the Python package
core (__init__.py, server.json). Default scope is "all" — fully backward
compatible.

New justfile recipes:
- set-version <v> [scope]            — write version (all|core|packages)
- set-version-dry-run <v> [scope]    — preview
- set-packages-version <v>           — plugin/agent artifacts only
- set-packages-version-dry-run <v>   — preview

The release / beta / release-dry-run recipes now route through set-version
instead of calling the script inline, so version-setting has one source of
truth and is reusable by the /release skill. Updated the /release command doc
to describe the consolidated version update and the new recipes.

Tests cover the new scope filtering (packages leaves core untouched and vice
versa; invalid scope errors). Existing lockstep behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 3ff40d51fc feat(plugins): rebuild Claude Code plugin as the memory bridge (v0.4 Phase 1)
Reframes the Claude Code plugin as the bridge between Claude's working
memory and Basic Memory's durable graph, per plugins/claude-code/DESIGN.md.
This is the minimal-first vertical slice with a clean break from v0.3.x.

Added:
- hooks/session-start.sh — SessionStart brief: one structured `type: task`
  query against the configured (or default) project + always-on recall
  prompt. Plain stdout, silent if BM is absent.
- hooks/pre-compact.sh — PreCompact checkpoint: writes a `type: session`
  note before compaction (extractive). Only writes when primaryProject is
  set, so it never touches an un-opted-in graph.
- output-styles/basic-memory.md — capture reflexes (search-first, typed
  decision notes, cite permalinks); keep-coding-instructions: true.
- schemas/{session,decision,task}.md — picoschema seeds (validation: warn)
  so plugin-written notes are findable via metadata_filters. task mirrors
  the memory-tasks skill.
- settings.example.json — copyable config with sensible defaults.

Removed (clean break):
- the six bundled skills, the basic-memory-manager agent, the
  PreToolUse/PostToolUse write_note hooks, the basic-memory config-note
  convention, and PLUGIN.md. Equivalent workflows live in top-level skills/.

Other:
- Rewrote scripts/validate_claude_plugin.py for the new layout (hooks +
  output-style + schemas; agent dropped; skills optional). Passes
  `just package-check-claude-code` incl. `claude plugin validate --strict`.
- Rewrote README around the bridge story; updated CHANGELOG, marketplace
  descriptions, and the AGENTS.md package-check note.
- Hooks tested end-to-end against throwaway projects (brief surfaces tasks;
  checkpoint writes a queryable session note; both degrade silently).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 82f6d15946 docs(plugins): add Claude Code plugin v0.4 redesign DESIGN.md
WIP design doc for modernizing the Claude Code plugin around the
"bridge between Claude auto-memory and Basic Memory" positioning.

- Reframes the plugin as connective tissue between Claude's working
  memory and BM's durable graph (per docs/vs-built-in-memory).
- Defines the four core surfaces: SessionStart brief, PreCompact
  checkpoint, output-style reflexes, deliberate skills.
- Leans on BM schemas + structured metadata search for deterministic
  recall (ships session/decision/task picoschemas).
- Maps Claude Code projects to BM projects; team-workspace safety
  (read-across, write-by-gesture); bootstrap interview.
- Records verified findings for 8 open questions (CC v2.1.153 /
  basic-memory 0.21.5) from an investigate→adversarial-verify pass:
  PreCompact 600s budget enables LLM summaries; metadata_filters
  confirmed; path-scoped rules cut (don't load yet); commands are
  plugin-namespaced; bootstrap via SessionStart sentinel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-31 12:31:08 -05:00
phernandez 4e7a217fa7 fix(skills): correct invalid picoschema enum YAML in memory skills
The Task schema in memory-tasks/SKILL.md and the enum examples in
memory-schema/SKILL.md used a bare YAML flow sequence followed by a
trailing description:

    status?(enum): [active, blocked, done, abandoned], current state

A flow sequence ([...]) cannot be followed by ', current state' —
python-frontmatter/PyYAML raises a ParserError. In schema_router.py,
_schema_frontmatter_from_file catches that error and silently falls
back to DB metadata, so the schema's enum constraints never load.

Per the picoschema parser docstring, enum descriptions belong inside
the parens:

    status?(enum, current state): [active, blocked, done, abandoned]

Fixes 4 lines (1 in memory-tasks, 3 in memory-schema). The
memory-literary-analysis schemas use the quoted-string form
("role(enum)": "[...], desc"), which is valid YAML handled by
_parse_enum_string, so they were left unchanged.

Verified: all yaml schema blocks now parse via parse_schema_note,
and just package-check-skills passes (10 skills validated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 15:07:00 -05:00
phernandez 354e1642b2 delete ui dir
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 15:07:00 -05:00
Drew Cain b1bd6d57e8 Update MONKEYPATCH.md with upstream issue link
Add tracking link for upstream issue regarding slash commands.

Signed-off-by: Drew Cain <groksrc@users.noreply.github.com>
2026-05-30 14:16:04 -05:00
phernandez 5365f971ef chore(integrations): consolidate agent packages
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:16:04 -05:00
phernandez 4ea03d2bf1 chore: qualify Phase 2 verdicts schema path with $SKILL_DIR
Follow-up PR review: Phase 2 prose used a CWD-relative
`--output-schema schemas/verdicts.schema.json`, while Phase 1 uses
`"$SKILL_DIR/schemas/..."`. Following Phase 2 from the repo root would
resolve the wrong path and break the Codex refutation call. Now matches the
$SKILL_DIR-qualified pattern used everywhere else; swept the file to confirm
all command/flag path positions are qualified.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez f652a09664 chore: build DIFF as argv array so scoped pathspecs survive (unix-friendly paths)
Addresses Codex PR review (P2): storing the diff in a scalar and running
`$DIFF` unquoted let the shell word-split/glob $SCOPE, so a pathspec with
spaces or glob chars ('docs/notes with spaces') broke or reviewed the wrong
files.

DIFF is now an argv array (`DIFF=(git diff "$BASE...HEAD")`, append
`-- "$SCOPE"` when set). Run via `"${DIFF[@]}"`; embed in subprocess prompts
via a shell-quoted `DIFF_STR=$(printf '%q ' "${DIFF[@]}")`. Verified the
pathspec stays a single intact argument in both bash and zsh.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez d8901efb46 chore: make claude json parsing robust to both output shapes
Follow-up PR review claimed `claude --output-format json` emits a single
object and the array form is stream-json only. Verified empirically against
the installed CLI: `claude -p --output-format json` returns a JSON ARRAY of
event objects (system, rate_limit_event, assistant, result), not a single
object — so the reviewer's suggested fix would have broken parsing here.

The portability concern is still valid (older CLIs emit a single object), so
the parsing note now handles BOTH shapes: if array, take the type=='result'
element; else use the object as-is; then read .result and strip the fence.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez c717d9997e chore: implement path scoping in adversarial-review (was broken-as-advertised)
Addresses follow-up PR review: the Inputs section advertised a path-scope
arg, but BASE=${ARG:-main} treated it only as a ref, so passing a path
produced `git diff <path>...HEAD` → fatal: ambiguous argument.

Root-causes both review findings (this one and the prior $BASE issue): the
diff command was spelled inline in many places and drifted. Now BASE (ref)
and SCOPE (pathspec) are separate inputs, combined once into a canonical
$DIFF command in preflight and reused everywhere — single source of truth.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez f865d39d3d chore: make adversarial-review prompts base-agnostic
Addresses PR review: the prompt bodies hardcoded `git diff main...HEAD`,
which contradicted the orchestrator's parameterized `git diff $BASE...HEAD`
when run against a non-main base. The prompts now defer to the exact diff
command the orchestrator appends, and Phase 2 passes that command alongside
the findings so the refuter judges against the correct base.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez 6a667dd3f8 chore: add cross-vendor adversarial-review skill
Adds an agent skill that runs adversarial code review across two model
families (Claude + Codex/GPT). Whichever runtime invokes it reviews the
branch diff natively and shells out to the other vendor for an independent
pass; each model then tries to refute the other's findings, and survivors
are reported by cross-model confidence. Report-only — never auto-applies
fixes.

- Canonical skill in .agents/skills/, symlinked from .claude/skills/
  (matches the existing instrumentation skill layout); discoverable by
  both Claude Code and Codex.
- Symmetric orchestration: run from Claude -> calls codex exec; run from
  Codex -> calls claude -p. The skill auto-detects which side it is on.
- Deterministic gate layer (ruff + grep) for negation-style house rules,
  since models underweight "never do X" constraints.
- Structured findings/verdicts via JSON schemas (codex --output-schema).

Deliberately omits the loop-until-converged state machine and auto-fix
behavior found in similar tools — convergence between models is not
correctness, so it surfaces a ranked list for a human gate instead.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:14:35 -05:00
phernandez 8d21a38588 fix(cli): handle non-subscription errors in cloud login
`bm cloud login` only caught SubscriptionRequiredError after the
post-login `/proxy/health` subscription check. OAuth succeeds and tokens
are saved, but if that check returns anything else — a 5xx while the
tenant instance is still provisioning, a 403/401 whose body doesn't match
the subscription_required shape, or a transport error — make_api_request
raises a generic CloudAPIError that escaped uncaught, dumping a raw
httpx.raise_for_status traceback. Users read this as "login failed" even
though authentication actually worked.

Add a CloudAPIError handler that prints a clean, actionable message and
exits non-zero. make_api_request wraps every httpx error (status and
transport) in CloudAPIError, so the single handler covers them all.

Fixes #863.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-30 14:13:32 -05:00
Paul Hernandez 597b12fcd1 Update README.md
update all pricing

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-29 08:19:52 -05:00
Paul Hernandez c86f4402f6 Update README.md
add teams info. update pricing

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-29 08:18:01 -05:00
Drew Cain f07643d3a8 feat(cli): expose project sync support metadata
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-28 15:00:05 -05:00
Drew Cain 8bf7bdbc0d fix(cli): limit team workspace guard to bisync
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-28 14:59:48 -05:00
Drew Cain d0ae373f45 docs(cli): clarify team mirror sync guard
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-28 14:59:48 -05:00
Drew Cain 1acec0a69d test(cli): cover workspace sync guard branches
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-28 14:59:48 -05:00
Drew Cain a2276a8a04 fix(cli): block team workspace rclone sync
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-28 14:59:48 -05:00
Drew Cain ff5d872a8c chore: update version to 0.21.5 for v0.21.5 release 2026-05-26 10:58:09 -05:00
Drew Cain 96ee4eafd2 docs: add v0.21.5 changelog entry
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-26 10:57:55 -05:00
Drew Cain b109b7337f fix(mcp): attach local state to one workspace project row (#854)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-26 10:54:44 -05:00
Drew Cain 36b51b676e fix(mcp): return workspace-qualified write permalinks (#853)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-26 00:33:21 -05:00
Drew Cain 9af320187c fix(core): load sqlite-vec before vector table cleanup (#852)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-26 00:27:47 -05:00
Paul Hernandez 5a34a420c9 fix(mcp): preinitialize local ASGI database (#838)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-25 15:57:00 -05:00
Drew Cain a7e2368f9e chore: update version to 0.21.4 for v0.21.4 release 2026-05-23 14:55:49 -05:00
Sean Campbell c755127317 fix(cli): ignore CancelledError in background task done callback (#839) (#842)
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:27:59 -05:00
Sean Campbell 94c04ee456 fix(mcp): restore write_note overwrite schema for external clients (#818) (#841)
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:19:43 -05:00
Drew Cain d4ed02ba74 docs(core): move release process from CONTRIBUTING.md to AGENTS.md (#846)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:00:01 -05:00
Drew Cain 40ed7129c8 chore: update version to 0.21.3 for v0.21.3 release 2026-05-23 13:46:48 -05:00
Drew Cain c4ef7abff5 test(core): isolate XDG_CONFIG_HOME so host env can't leak into tests (#845)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:34:41 -05:00
Paul Hernandez c75f45f3cf Update README.md
update cloud description to remove link to private cloud repo

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-23 12:22:18 -05:00
Drew Cain 5ae8a733ea fix(mcp): route mixed local/cloud projects correctly (#837)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-23 11:31:13 -05:00
Rafael Madriz b94ef01f82 feat(core): add XDG_CONFIG_HOME support (#844)
Signed-off-by: Rafael Madriz <rafa@rafaelmadriz.com>
2026-05-22 15:18:06 -05:00
Sean Campbell 12af7930de docs(installer): use pgvector image for Postgres compose (#840)
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Drew Cain <groksrc@gmail.com>
2026-05-22 08:54:18 -05:00
phernandez 60ec6728de chore: update version to 0.21.1 for v0.21.1 release 2026-05-16 18:56:38 -05:00
phernandez a84e77f4af docs: add v0.21.1 changelog entry
CI-only point release to validate #833's inline Homebrew bump end-to-end
on a real tag push.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-16 18:56:30 -05:00
Paul Hernandez 6910620968 ci(installer): inline Homebrew formula bump (#833)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-16 18:55:25 -05:00
phernandez 214a54d740 chore: update version to 0.21.0 for v0.21.0 release 2026-05-16 17:15:53 -05:00
phernandez 5a90c7cedf docs: add v0.21.0 changelog entry
Promotes the Unreleased breaking-change note and adds the full v0.21.0
section covering ~80 commits since v0.20.3: workspace-routing fixes
across MCP/CLI/API, recent_activity ordering and search opt-in changes,
sync hardening, sqlite-vec graceful degrade, perf wins on CLI startup
and sync, and the project-delete cleanup landed in #832.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-16 17:15:43 -05:00
Paul Hernandez 9e3fe26a83 fix(core): purge SQLite search_index on project delete (#832)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-16 16:47:24 -05:00
Paul Hernandez 47ee982041 perf(cli): defer local ASGI app import (#828)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 17:59:58 -05:00
Paul Hernandez 4d22c398c6 fix(sync): preserve bmignore rclone filters (#827)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 17:48:49 -05:00
phernandez 8ac2d975f9 update README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 17:13:34 -05:00
Paul Hernandez 34830bfad7 Update README.md
update agent matrix

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-15 14:55:00 -05:00
Paul Hernandez f5e0c42047 Update README.md
remove sync commands from cli examples

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-15 14:52:27 -05:00
phernandez 3f98da8c67 update README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 14:50:35 -05:00
phernandez 14ff77d1c2 update to fastmcp 3.3.1
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 12:28:03 -05:00
phernandez 4e4da6128d ci: run github actions on node 24
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 12:14:06 -05:00
Paul Hernandez 3bed6d8890 chore(deps): update deps and harden security (#825)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-15 10:45:14 -05:00
Paul Hernandez 4cba7ba01c fix(core): parse prose wikilinks as inline links (#824)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-14 11:43:11 -05:00
Drew Cain 8eeec64e28 fix: basic-memory project list does not list projects from all workspaces (#822)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-14 09:46:02 -05:00
Paul Hernandez c6fa185bf3 fix(mcp): route edit_note workspace-qualified permalinks (#813)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-11 12:56:26 -05:00
Paul Hernandez 415c2b3d6e feat(cli): add orphan entity command (#816)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-11 12:13:14 -05:00
Paul Hernandez 4aa0cbdd62 fix(sync): ignore hidden paths relative to watched project (#815)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-11 11:14:03 -05:00
Paul Hernandez 55f314237d fix(sync): avoid shell for scan subprocesses (#814)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-11 09:20:11 -05:00
Drew Cain 9862ef5411 fix(core): use updated_at for recent_activity filter and ordering (#812)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-05-11 09:14:06 -05:00
Paul Hernandez df5e8d805f fix(mcp): centralize workspace permalink routing (#808)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-08 16:44:57 -05:00
Paul Hernandez 831dc1ecdc fix(mcp): make multi-project search opt-in (#807)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-08 12:54:20 -05:00
Paul Hernandez 26381aeed1 fix(mcp): use lightweight graph hydration lookup (#806)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-08 10:07:29 -05:00
Paul Hernandez 7918e5c6bf fix(mcp): preserve workspace paths in build_context (#801)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-08 09:53:54 -05:00
Paul Hernandez f312341020 fix(mcp): add workspace routing to delete_project (#803)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-08 09:48:51 -05:00
phernandez e871298e95 Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-05-07 18:50:10 -05:00
phernandez 177ae21ba7 pass context to recent_activity
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-07 18:50:02 -05:00
Drew Cain 3415fd1014 fix(core): parse picoschema modifier descriptions (#796)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-05-06 12:21:35 -05:00
Paul Hernandez b08659e228 fix(api): accept qualified project resolver hints (#795)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-06 10:33:11 -05:00
Paul Hernandez 09a4b09436 feat(api): include search result totals (#791)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-04 14:57:04 -05:00
Paul Hernandez a661e924df feat(mcp): create projects by workspace slug (#789)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-03 19:31:55 -05:00
Paul Hernandez 05adda1502 fix(mcp): route workspace-qualified memory urls (#790)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-03 18:18:10 -05:00
Drew Cain 0a72d81bb3 fix(mcp): cap recent_activity rows with explicit truncation footer (#785)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:43:42 -05:00
Drew Cain 5ccf433cad fix(cli): point bm cloud setup hint at bm cloud sync-setup (#780)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-05-02 17:35:24 -05:00
Paul Hernandez b4bf14ebf7 fix(mcp): list factory projects across workspaces (#778)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-05-02 09:17:25 -05:00
Paul Hernandez 0b335476d6 fix(mcp): resolve projects by external_id, remove workspace from MCP tools (#777)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 05:15:44 -05:00
Paul Hernandez 2fccc74a20 feat(cli): refuse db reset while basic-memory mcp processes run (#776)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-29 15:48:19 -05:00
Paul Hernandez c4956cac16 fix(cli): cleanup local DB state on set-cloud/set-local (#775)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-29 11:50:14 -05:00
Paul Hernandez 128c2da40c fix(cli): clear default_workspace on cloud logout (#773)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-29 09:53:09 -05:00
Paul Hernandez 2bfb9c76df fix(core): degrade gracefully when sqlite-vec cannot load on init (#774)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-29 08:57:31 -05:00
Paul Hernandez 3d927b848f fix(installer): mount docker-compose config volume to appuser home (#772)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-29 08:57:26 -05:00
Paul Hernandez 953fe20aef test(core): regression guard for vector-row cleanup on entity delete (#764) (#771)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-28 23:20:15 -05:00
Paul Hernandez a4282d9f2f fix(core): skip Obsidian callouts in observation parser (#769)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-28 23:15:13 -05:00
Paul Hernandez 799dd6c629 fix(mcp): remove no-op pagination params from read_note and view_note (#768)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-28 23:05:14 -05:00
Paul Hernandez 26e74ea118 test(core): regression guard for long relation_type values (#721) (#770)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-28 22:55:14 -05:00
Paul Hernandez ee1558ea68 feat(mcp): accept training-data-friendly parameter aliases (#766)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-28 20:10:36 -05:00
Viktor Szépe 4d62b623db chore(core): fix typos (#761)
Signed-off-by: Viktor Szépe <viktor@szepe.net>
2026-04-23 10:12:46 -05:00
Paul Hernandez 2fe4488eda fix(sync): constrain watch service to --project scope (#759)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-20 13:46:13 -05:00
Paul Hernandez f3e46d7984 feat(mcp): discover projects across workspaces (#757)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-18 14:04:57 -05:00
jope-bm 56d6f1b4a5 fix(mcp): report cloud projects as source=cloud in factory mode (#752)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 11:10:20 -06:00
Paul Hernandez 1b39062ecd refactor(core): rip telemetry wrappers, use logfire directly (#754)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:58:19 -05:00
Paul Hernandez c4cf0aff1e perf(sync): speed up single markdown file indexing (#751)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-17 07:08:21 -05:00
phernandez 1c343bed66 perf(sync): skip unchanged markdown indexing
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-16 18:27:52 -05:00
phernandez c50d97e548 fix(sync): instrument single markdown indexing
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-16 18:04:17 -05:00
Drew Cain e2e65575d6 fix(core): honor BASIC_MEMORY_CONFIG_DIR across remaining call sites (#744)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:55:25 -05:00
Drew Cain bf9a6b4a75 fix(core): resolve FastEmbed cache under data dir instead of /tmp (#743)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:55:00 -05:00
Paul Hernandez 474100efef ci(core): reduce duplicate CI and normalize Windows assertions (#749)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-15 22:53:33 -05:00
Paul Hernandez b3d5448355 fix(sync): preserve canonical markdown in single-file sync (#746)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-15 20:04:28 -05:00
Paul Hernandez 4e53bb83fd refactor(core): simplify note write flow (#739)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-15 17:56:27 -05:00
phernandez 8f2b25f0e0 test(core): stabilize postgres fixtures
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-13 14:25:26 -05:00
Paul Hernandez 052545b661 chore(core): make ty the default typechecker (#736)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-13 10:34:01 -05:00
phernandez abd4a5a6da Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-04-10 12:23:19 -05:00
Paul Hernandez a872947e03 fix(cli): show cloud index freshness in project info (#734)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-10 09:22:35 -05:00
Paul Hernandez 093c94fea5 fix(core): clean up delete vectors and cloud sync (#733)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-09 21:23:45 -05:00
phernandez cc104f761f Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-04-09 21:09:03 -05:00
Paul Hernandez 7945c1e2f7 perf(core): speed up vector sync and tune fastembed defaults (#731)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-09 00:20:12 -05:00
phernandez d7f3f6a96f add logfire skills 2026-04-08 11:20:13 -05:00
Paul Hernandez 540da418b3 perf(sync): batch file indexing in core (#726)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-08 01:21:49 -05:00
Paul Hernandez 3e40cb9657 fix(core): remove runtime ALTER TABLE from vector init (#728)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-08 00:39:37 -05:00
Paul Hernandez 8c81d3ce17 perf(core): reduce postgres vector sync work (#723)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-07 19:00:19 -05:00
Drew Cain b35d594ef0 fix(core): preserve external_id during entity upsert on re-index (#724)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:26:10 -05:00
phernandez e982900084 fix(core): strip null bytes from markdown content before database insert
PostgreSQL rejects null bytes (0x00) in text columns, causing
CharacterNotInRepertoireError when syncing files like Claude agent
definitions that contain embedded nulls. SQLite silently accepts them,
so this only surfaces in cloud environments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-06 23:14:40 -05:00
Drew Cain b3403e96b3 fix: add workspace routing to cloud upload and API client (#704)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-04-06 18:18:40 -05:00
Paul Hernandez fe04a0b2a2 fix(mcp): pass workspace parameter through client factory (#722)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:57:37 -05:00
jope-bm 86ad639890 fix(cli): show display_name instead of UUID for private projects in CLI (#718)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:14:46 -06:00
Paul Hernandez 88c8f18200 feat(core): add note_content tenant schema primitive (#719)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-04 22:06:37 -05:00
Drew Cain 41a16b93cb fix: Increase brew outdated timeout from 15s to 60s (#695)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:38:31 -05:00
Drew Cain 367fcaac50 perf: eliminate redundant DB queries in upsert_entity_from_markdown (#714)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-04-04 12:40:27 -05:00
Paul Hernandez 69808b23ca perf(core): reuse written note content after writes (#717)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-04 00:16:22 -05:00
Paul Hernandez 6f207c20c0 test(api): add recent activity hydration regression coverage (#716)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-03 17:30:44 -05:00
Paul Hernandez cff31c5797 feat(cli): support cloud project visibility on add (#715)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-03 17:30:09 -05:00
dependabot[bot] 2d1ccfa36c chore(deps): bump the uv group across 1 directory with 2 updates (#697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 17:29:46 -05:00
dependabot[bot] a2e0f935d6 chore(deps): bump picomatch from 4.0.3 to 4.0.4 in /ui/tool-ui-react in the npm_and_yarn group across 1 directory (#696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 17:28:46 -05:00
Paul Hernandez e6b98a15c7 fix(cli): propagate cloud workspace routing and incremental sync (#712)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-04-03 14:25:02 -05:00
Drew Cain 733c4f7514 fix: eliminate N+1 query in search hydrate_results (#713)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:54:24 -05:00
Paul Hernandez cfa70004be fix: concurrent delete race conditions in delete_entity (#702)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:57:30 -05:00
phernandez 7696fca826 fix: restore MCP telemetry compatibility and outcomes
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-28 15:31:13 -05:00
phernandez 98a2a3cbaf Unify MCP telemetry spans across routers and services
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-28 14:42:18 -05:00
phernandez 01cbad1dbe Allow long relation_type values in responses
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-28 09:56:22 -05:00
phernandez a4e0422926 perf fixes
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-27 22:12:44 -05:00
phernandez 552a835669 chore: update version to 0.20.3 for v0.20.3 release 2026-03-26 23:09:57 -05:00
phernandez 888e3c2909 docs: add v0.20.3 changelog entry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-26 23:09:20 -05:00
Paul Hernandez d1320f671e fix: (cloud) CLI cloud commands now use API key when configured (#698)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:56:45 -05:00
phernandez 94bdfe77e4 fix: detect cloud mode in resolve_runtime_mode
BASIC_MEMORY_CLOUD_MODE env var was never checked in resolve_runtime_mode(),
so cloud deployments always ran as LOCAL mode. This caused file sync to start
in the cloud container, which then failed with "DATABASE_URL must be set when
using Postgres backend" because there's no local DB in cloud mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-26 14:53:12 -05:00
Paul Hernandez 4791e19685 feat: add Logfire phased instrumentation (#692)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:39:42 -05:00
Paul Hernandez 36848410a1 feat(core): add default_search_type config setting (#676)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:19:53 -05:00
jope-bm a77b51a28e fix(core): allow double-dot filenames while still blocking path traversal (#673)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 07:47:49 -06:00
Paul Hernandez 1a6a65571e fix(mcp): add project detection from memory:// URLs in edit_note and delete_note (#668)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:14:22 -05:00
Paul Hernandez c8b00449d2 fix(core): exclude stale entity rows from embedding coverage stats (#675)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:45:36 -05:00
Paul Hernandez 013864ebf0 fix(cli): use resolved project path in doctor command (#667)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:43:23 -05:00
Drew Cain dd91b49054 chore: update version to 0.20.2 for v0.20.2 release 2026-03-10 23:13:59 -05:00
Drew Cain 7c96a0777d fix(cli): handle brew outdated exit code 1 as outdated, not error
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-03-10 23:13:54 -05:00
Drew Cain 148e07c580 chore: update version to 0.20.1 for v0.20.1 release 2026-03-10 23:06:21 -05:00
Drew Cain 21334cc29b docs: add v0.20.1 changelog entry
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-03-10 23:06:15 -05:00
Drew Cain db60942267 fix(core): invalidate config cache when file is modified by another process (#662)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:05:55 -05:00
Drew Cain 7bfac158df fix(cli): project list MCP column shows transport type instead of DB presence (#661)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:05:48 -05:00
Drew Cain 87616924ff chore: update version to 0.20.0 for v0.20.0 release 2026-03-10 22:08:00 -05:00
Drew Cain 5cb0502ed2 docs: add v0.20.0 changelog entry
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-03-10 22:07:49 -05:00
Paul Hernandez a94a717b1b feat(cli): add default-on auto-update system and bm update command (#643)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Drew Cain <groksrc@users.noreply.github.com>
Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
2026-03-10 22:06:33 -05:00
phernandez 6e4bb72f10 chore: update version to 0.19.2 for v0.19.2 release 2026-03-09 23:42:10 -05:00
phernandez 11b0e31e24 docs: add v0.19.2 changelog entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-09 23:41:40 -05:00
Paul Hernandez a5c9e77f16 fix: coerce string params to list/dict in MCP tools (#657)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:19:07 -05:00
Paul Hernandez 30a89357cb fix(core): handle SQLite and Windows semantic regressions (#655)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-09 22:18:17 -05:00
phernandez 222ec5d3b6 chore: update version to 0.19.1 for v0.19.1 release 2026-03-08 18:09:04 -05:00
phernandez d42aec7ea9 docs: add v0.19.1 changelog entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-08 18:04:30 -05:00
Paul Hernandez 9809b469c6 fix: enforce strict entity resolution in destructive MCP tools (#650)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:56:07 -05:00
phernandez 76ac880f2d feat(api): add GET /knowledge/graph endpoint for full graph visualization
Returns all entities and resolved relations in a flat node/edge format
optimized for graph rendering. Replaces the frontend's use of the
recent memory endpoint which only returned a subset of relations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-08 11:52:17 -05:00
Paul Hernandez ad3f2650d9 feat: add insert_before_section and insert_after_section edit operations (#648)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:54:10 -05:00
dependabot[bot] d6508d985c chore(deps): bump authlib from 1.6.6 to 1.6.7 in the uv group across 1 directory (#645)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 10:53:52 -05:00
phernandez 7b95b9f37b docs: add What's New in v0.19.0 section to README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-08 10:29:06 -05:00
phernandez 0bce4be1a6 chore: update version to 0.19.0 for v0.19.0 release 2026-03-07 14:27:43 -06:00
phernandez a316424edf docs: add v0.19.0 changelog entry
Comprehensive changelog for 114 commits since v0.18.5 covering semantic
vector search, schema system, per-project cloud routing, FastMCP 3.0
upgrade, CLI overhaul, and numerous bug fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-07 14:27:29 -06:00
phernandez af71cf4896 fix(test): clear search_vector_chunks before embedding backfill test
Test was polluted by other tests leaving rows in search_vector_chunks,
causing _needs_semantic_embedding_backfill to return False.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-07 13:54:58 -06:00
phernandez e846ae85d8 fix: semantic embeddings not generated on fresh DB or upgrade
The previous backfill trigger relied on Alembic revision tracking, but
alembic_version only stores the head revision — intermediate revisions
(like the backfill trigger) are invisible after a multi-step upgrade or
fresh DB creation.

Three changes fix this:

1. Replace Alembic revision check with a simple "entities exist but
   embeddings are empty" check that works regardless of migration path
2. Generate embeddings during sync — after FTS indexing, batch-embed all
   synced entities at the end of the sync operation
3. Add background backfill at MCP startup for the upgrade path (entities
   already exist, no embeddings) without blocking server readiness

Also adds clear startup logging for semantic embedding status so issues
are easy to spot in the logs.

📋 Covers: fresh DB, upgrade from pre-embedding version, db reset,
   interrupted backfill

Signed-off-by: Pedro Hernandez <pedro@basicmachines.co>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-07 13:23:39 -06:00
phernandez 63e4bcdf1d fix: clarify search_notes parameter naming and fix note_types case sensitivity
- Add Annotated descriptions to note_types and entity_types parameters so
  LLMs can distinguish frontmatter type filtering from knowledge graph item
  type filtering (search.py, ui_sdk.py)
- Lowercase note_types values at filter time so "Chapter" matches stored
  "chapter"
- Fix misleading entity_types references in schema.py guidance strings
  (should be note_types)
- Add permalink pattern documentation note about full path matching
- Add test for note_types case-insensitive lowercasing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-05 15:08:52 -06:00
phernandez f23dd0474b fix(test): patch API fallback in project_context tests for Postgres
In Postgres test mode, stale dependency_overrides on the module-level
FastAPI app allow _resolve_default_project_from_api() to query a live
database and return 'test-project' even when the test sets
default_project=None. Monkeypatch the async fallback in the three
affected tests to isolate config-based resolution from API leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-04 15:34:35 -06:00
phernandez fced804438 fix(test): update integration test for DB default project fallback (#644)
The test previously asserted that write_note fails when ConfigManager
has no default_project. With the API fallback, it now correctly
resolves to the database is_default project. Updated the test to
verify this fallback behavior instead of expecting an error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-04 13:27:03 -06:00
phernandez 1fdc9fdc69 fix: resolve_project_parameter falls back to projects API for default (#644)
In cloud mode, ConfigManager has no local config so default_project
is always None. Add API fallback in resolve_project_parameter that
queries /v2/projects/ for the default_project field. This fixes all
MCP tools that rely on project resolution (recent_activity, etc).

Removed discovery mode tests that simulated an invalid state by
clearing is_default — there must always be a default project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-04 13:17:16 -06:00
phernandez 2feecdfaf7 fix: ChatGPT search/fetch tools broken in cloud mode (#644)
Both search() and fetch() read default_project from ConfigManager,
which returns None in cloud mode. Remove the manual ConfigManager
lookup and let the underlying search_notes/read_note resolve the
project via get_project_client(), which works in both modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-04 12:40:40 -06:00
phernandez 195229f78e fix: resolve default_project returning null in cloud mode (#644)
In cloud mode, ConfigManager has no local config file so
default_project always returned None. Add async
get_default_project_name() on ProjectService that falls back
to the database is_default flag when ConfigManager returns None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-04 11:57:15 -06:00
phernandez d15f6a8427 Add batched vector sync orchestration across repositories 2026-03-03 16:36:06 -06:00
phernandez b8a3a14ad2 Add semantic query timing and FastEmbed parallel guardrails
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-03 14:04:03 -06:00
phernandez 9b199c6dcb fix: add FastEmbed runtime tuning knobs and provider caching
Add configurable cache_dir, threads, and parallel settings for FastEmbed
to support cloud deployments where defaults fail. Cache embedding providers
at the process level to avoid re-creating heavy ONNX model instances.

- Add semantic_embedding_cache_dir, semantic_embedding_threads, and
  semantic_embedding_parallel config fields
- Thread-safe provider cache with double-checked locking in factory
- Forward runtime knobs through to TextEmbedding and embed() calls
- Fix if/elif chain in factory for correct error handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-03 10:26:47 -06:00
phernandez fe4a7b1622 fix: update analytics test mocks for non-daemon thread change
Thread constructor no longer receives daemon=True, update mock
signatures to match. Also assert on the new "type": "event" field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-02 21:26:54 -06:00
phernandez 7d2012a82c fix(cli): fix Umami analytics event delivery
Three issues prevented CLI analytics from reaching the Umami dashboard:

1. Wrong API endpoint — cloud.umami.is rejects /api/send, the JS tracker
   uses api-gateway.umami.dev
2. Missing "type": "event" top-level field required by Umami v2 API
3. Non-browser User-Agent ("basic-memory-cli/...") triggers Umami's bot
   detection, which silently drops events with {"beep":"boop"} 🤖
4. Daemon thread was killed before HTTP request completed on fast commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-02 19:24:10 -06:00
phernandez 7f2d4d2a6f fix: three cloud-testing bugs (#640, #641, #642)
🔧 #640 — LinkResolver selects worst match instead of best
Replace `min(results, key=lambda x: x.score)` with `results[0]`.
Both SQLite and Postgres return results sorted best-first in SQL,
so using `results[0]` is backend-agnostic and correct.

🔧 #641 — search_notes output_format="text" returns raw Pydantic model
Add `_format_search_markdown()` that formats SearchResponse as readable
markdown with title, permalink, score, and matched snippet per result.
Update prompts to use `output_format="json"` since they need structured
data for result counting and branching logic.

🔧 #642 — metadata_filters with `note_type` key returns empty results
Add `_METADATA_KEY_ALIASES` mapping at the tool level that aliases
`note_type` → `type` before passing metadata_filters to the search query.
The frontmatter field is `type`, not `note_type`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-02 12:58:22 -06:00
phernandez 8b7f39ee9b docs: update AI assistant guides for v0.19.0, fix multi-tag search parsing
Update both the compact and extended AI assistant guides with v0.19.0 changes:
- 📝 write_note overwrite guard: callout, edit_note examples, overwrite=True
- 🔍 Expanded search section: all search types, tag: shorthand, filter-only
  searches, metadata_filters operators, min_similarity
- ⚠️ "Note already exists" error handling pattern
- 📋 Tool quick reference: updated params, added list_workspaces
- 🔗 memory:// URL: added cross-project format
- ✏️ Best practice: prefer edit_note for updates

Fix tag: shorthand parsing to handle multiple tags anywhere in the query.
Old parser only handled queries starting with "tag:" and broke on
"tag:coffee AND tag:brewing". New parser uses re.findall to extract all
tag:value tokens, strips boolean connectors, and preserves remaining text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-02 09:50:22 -06:00
phernandez a368d06fd2 fix: improve cloud CLI status and error messages
- Simplify `bm cloud status` output: remove verbose health check details
  (status/version/timestamp), show simple "Cloud connected" / "Cloud not
  connected" message instead
- Improve `bm reindex --project` error for cloud projects: distinguish
  between "project not found" and "project is cloud-only" with a helpful
  message explaining reindexing is a local operation
- Improve `bm project list` cloud error message: show the actual error
  and soften the credentials suggestion
- Add tests for cloud status command (5 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 20:24:11 -06:00
phernandez 3a2b80b7e9 fix: remove broken CI coverage infrastructure
The coverage collection had multiple issues:
- Wrong pytest markers caused 0 tests to run
- Postgres jobs silently skipped artifact uploads
- Coverage Summary job failed when artifacts were missing
- uv venv picked wrong Python version for coverage jobs

Simplify: every job just runs tests via `just` recipes. No more
dual code paths, artifact uploads, or summary aggregation job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 13:18:51 -06:00
phernandez bd5099120f fix: CI Postgres coverage jobs used wrong pytest marker (-m postgres)
The coverage code paths for Postgres unit and integration jobs filtered
with `-m postgres`, but no tests use that marker. Postgres is selected
via BASIC_MEMORY_TEST_POSTGRES=1 env var. This caused 0 tests to run
on Python 3.12 (the only version with coverage: true).

- Postgres unit: remove `-m postgres` (matches `just test-unit-postgres`)
- Postgres integration: use `-m "not semantic"` (matches `just test-int-postgres`)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 12:47:38 -06:00
phernandez 8e64caf784 fix: list_workspaces bypasses factory pattern on cloud MCP server (#636)
Add set_workspace_provider() injection point so the cloud MCP server can
list workspaces by querying its own database directly, instead of making
an HTTP round-trip to the control-plane API with credentials it doesn't have.

🔧 Mirrors the existing set_client_factory() pattern in async_client.py
🧪 Adds 3 tests for provider injection, fallback, and context caching
🩹 Updates build_context test assertions for v0.18 backward compat fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 12:08:25 -06:00
phernandez ccb5740920 fix: create backup before config migration overwrites old format (#637)
When load_config() detects a legacy config format and resaves it, the old
config.json was overwritten in-place with no recovery path. Users switching
between dev and released versions would lose their config.

Now creates config.json.bak before the migration resave so users can revert
if needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 12:08:25 -06:00
phernandez 373893a7ee fix: restore API backward compatibility for v0.18.x clients (#638)
v0.18.5 clients (homebrew) fail against v0.19.0 servers because:
- `entity_type` was renamed to `note_type` in EntityResponse
- 13 fields gained `Field(exclude=True)` and vanished from JSON

🔧 EntityResponse: add `entity_type` computed_field mirroring `note_type`
🔧 memory.py: remove `exclude=True` from 13 fields across EntitySummary,
   RelationSummary, ObservationSummary, and MemoryMetadata
🔧 Add v0.18 backward-compat contract tests

Closes #638

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 12:08:25 -06:00
phernandez 1987581ee3 docs: update v0.19.0 release notes and apply formatting fixes
Add #634 (stale schema metadata) to bug fixes section.
Apply ruff formatting to schema.py, write_note.py, test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-03-01 12:08:25 -06:00
Drew Cain 59134affa4 fix: read schema definitions from file instead of stale database metadata (#635)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-03-01 09:41:22 -06:00
phernandez c7d97decd6 docs: update v0.19.0 release notes with recent commits
Add write_note overwrite guard (#632) to new capabilities, 8 bug fixes
(#631, #630, #30, #31, #28, plus schema_validate/Post/frontmatter fixes),
and write_note idempotency breaking change to upgrade notes. Bump commit
count from 80+ to 90+.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-28 13:53:29 -06:00
phernandez 5d5efa02a5 fix: format schema_infer and schema_diff as markdown text (#28)
schema_infer and schema_diff returned raw Pydantic models in text mode,
causing LLMs to render field names as "undefined". Add text formatters
(_format_inference_report, _format_drift_report) matching the existing
_format_validation_report pattern. CLI paths are unaffected — they
always use output_format="json".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-28 12:24:38 -06:00
phernandez 4f12182e28 fix: parse tag: prefix at MCP tool level to avoid hybrid search failure (#30)
When semantic search is enabled (default), `search_notes(query="tag:security")`
failed because the HYBRID retrieval mode requires non-empty text, but the
service-layer tag: parser clears the text after the mode is already set.

Parse tag: prefix at the tool level before search mode selection, converting it
to a tags filter. This works with all search modes (text, hybrid, vector).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-28 11:53:05 -06:00
phernandez 2c5b63c4a2 fix: default search_notes to entity-level results (#31)
search_notes was returning individual observations and relations as
separate top-level results, wasting the result limit and creating
confusing UX. Default entity_types to ["entity"] when the caller
doesn't specify it — the entity row already indexes full file content,
so no matches are lost. Users can still override with explicit
entity_types=["observation"] etc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-28 11:28:51 -06:00
phernandez c4c9f842ea fix: schema_validate identifier resolution and text rendering
Fixes issues #29 and #33 from openclaw-basic-memory.

🔧 Identifier resolution (#33):
- Router used get_by_permalink() which only matched exact permalinks.
  Replaced with link_resolver.resolve_link() so titles, paths, and
  fuzzy matches work consistently with other tools like read_note.
- Set total_entities=1 and total_notes=len(results) on single-note
  path for consistency with batch path.
- Guards for "no notes" and "no schema" now fire for identifier-based
  validation too, not just note_type-based.

🎨 Text rendering (#29):
- Tool returned raw Pydantic model which LLMs rendered as
  "undefined — invalid". Now returns pre-formatted markdown.
- Router uses entity.title (with permalink fallback) as note_identifier
  for human-readable output in both text and JSON modes.
- JSON output (output_format="json") unchanged for CLI compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-28 10:40:01 -06:00
phernandez dc0ca71c18 fix: avoid Post(**metadata) crash when frontmatter contains 'content' or 'handler' keys
frontmatter.Post.__init__ takes `content` and `handler` as positional
parameters. When user YAML frontmatter contains these as field names,
unpacking metadata via **kwargs causes "got multiple values for argument
'content'".

Replace frontmatter.loads() with frontmatter.parse() + Post() + update()
in entity_parser, and replace the **metadata unpacking in
entity_service.update_entity() with the same safe pattern.

Fixes basic-memory-cloud#375

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-27 22:52:16 -06:00
phernandez 236ae268aa fix: coerce list frontmatter values to strings for title and type fields
YAML block sequence syntax can cause PyYAML to parse scalar fields like
`title` and `type` as lists instead of strings. Downstream code calls
.strip()/.casefold() on these values, crashing with
"'list' object has no attribute 'strip'".

Add _coerce_to_string() helper that joins list items with ", " and apply
it in entity_parser.parse_markdown_content() and
entity_service.fast_edit_entity() where these fields are extracted.

Fixes basic-memory-cloud#376

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-27 22:45:09 -06:00
Paul Hernandez bd5923a370 feat: add overwrite guard to write_note tool (#632)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:12:38 -06:00
jope-bm 4ea5396ddd fix: skip workspace resolution when client factory is active (#630)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:40:04 -06:00
Paul Hernandez e97eafa55a fix: build_context related_results schema validation failure (#631)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:30:44 -06:00
bm-clawd 254e30423d docs: update v0.19.0 release notes with post-draft changes (#626)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:36:27 -06:00
phernandez 496af07ced fix: resolve pyright possibly-unbound errors in edit_note
Initialize entity_id and result before the try/except block and
add assertion before the formatting section to help pyright prove
result is always bound on all code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 20:10:35 -06:00
bm-clawd b5667f9b55 docs: Add UTM tracking to README links (#611) 2026-02-26 19:55:33 -06:00
phernandez 54b968b93c fix: reduce excessive log volume by demoting per-request noise to DEBUG (#613)
Demote high-frequency per-request/per-item logs from INFO to DEBUG:
- 🔇 Client routing decisions (async_client.py) — logged every MCP tool call
- 🔇 DB migration checks (db.py) — logged every ASGI client creation
- 🔇 Vector table ensure/ready (sqlite + postgres search repos) — logged every search
- 🔇 Per-entity search index start/complete (search_service.py) — logged every file sync
- 🔇 Incremental scan details + per-file permalink updates (sync_service.py)
- 🔇 MCP search tool params and no-results (search.py)
- 📉 Log retention: "10 days" → 5 files (~50MB cap)

API v2 request/response logs remain at INFO for observability.

Closes #613

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 19:48:24 -06:00
phernandez e59b5cb6d9 feat: edit_note append/prepend auto-create note if not found (#614)
When append or prepend targets a non-existent note, the tool now
creates the file automatically instead of returning an error. This
eliminates silent failures for plugins (like openclaw) that use
edit_note(append) to build daily conversation notes — on the first
message of each day, the note didn't exist yet.

- find_replace and replace_section still require an existing note
- JSON output now includes `fileCreated: bool` in all responses
- Path traversal security check applied to auto-created directories
- Updated error messages to suggest append/prepend for missing notes

🧪 25 unit tests, 14 MCP integration tests, 10 CLI integration tests — all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 19:00:34 -06:00
phernandez f0335b998e fix: handle quoted picoschema enum strings in YAML frontmatter (#612)
The picoschema enum-with-description syntax `[val1, val2], description`
is invalid YAML. Users must quote it so YAML parses it as a string.
This adds `_parse_enum_string()` to extract enum values and description
from the resulting string value (e.g., "[active, blocked], current state").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 18:10:48 -06:00
phernandez 66effb04a7 fix: upgrade cryptography and python-multipart for security advisories
- cryptography 46.0.3 → 46.0.5 (subgroup attack on SECT curves)
- python-multipart 0.0.21 → 0.0.22 (arbitrary file write via non-default config)
- pillow alert dismissed — blocked by fastembed 0.7.4 pinning <12.0 (tracking qdrant/fastembed#606)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 14:42:20 -06:00
phernandez 9331126ba1 feat: improve content hit rate in search results (#609)
Three changes to surface more answer text in search results:

- Populate matched_chunk_text for FTS-only hybrid results from content_snippet,
  preventing fallback to truncated content when vector search has no match
- Increase TOP_CHUNKS_PER_RESULT from 3 to 5, catching answers in deeper chunks
  for large notes (~2700 → ~4500 chars of matched context)
- Increase CONTENT_DISPLAY_LIMIT from 2000 to 4000, doubling the safety-net
  content truncation for results without matched_chunk

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 11:55:38 -06:00
phernandez 3bbb44af0b feat: add --json output to CLI commands for scripting and CI
Add machine-readable JSON output to five CLI commands:
- `bm status --json` — sync report
- `bm project list --json` — structured project list
- `bm schema validate --json` — validation report
- `bm schema infer --json` — inference report
- `bm schema diff --json` — drift report

Refactored `run_status()` to return data instead of printing directly,
improving testability. Follows the established `bm project info --json`
pattern using `print()` for clean JSON (no Rich markup).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 10:33:00 -06:00
phernandez f9b2a075a9 feat: return richer content context in search results (#609)
Two strategies to improve content hit rate when the right document is found:

1. Small notes (<=2000 chars): return full content_snippet as matched_chunk
   so the answer is always present for correctly-retrieved small notes
2. Large notes: return top-3 chunks by similarity joined with \n---\n
   instead of just the single best chunk (~2700 chars vs ~900 chars)

Also raises CONTENT_DISPLAY_LIMIT from 250 to 2000 for richer FTS results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-26 08:36:54 -06:00
phernandez 0a3f3f07f8 fix: run coverage instead of tests on 3.12/ubuntu, not in addition to
Previous commit still ran tests twice on the coverage matrix entry.
Now the 3.12/ubuntu/main combo runs with coverage directly, and all
other matrix entries run without. No duplicate work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 23:36:46 -06:00
phernandez 184ea6d9fa fix: collect coverage from test jobs instead of re-running all tests
The coverage summary job was re-running the entire SQLite + Postgres test
suite from scratch (~60 min), duplicating work already done by upstream jobs.
It consistently timed out.

Now each test job collects coverage data and uploads it as an artifact.
The coverage job just downloads, combines, and reports — should take <1 min.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 23:33:15 -06:00
phernandez f5a0e942b0 fix: replace RRF with score-based fusion in hybrid search (#577)
RRF compressed all fused scores to ~0.016, destroying ranking differentiation.
The new formula `max(vec, fts) + FUSION_BONUS * min(vec, fts)` preserves
dominant signals and rewards dual-source agreement.

Changes:
- Remove RRF_K constant; add FUSION_BONUS (0.3) and FTS_GATE_THRESHOLD (0.0)
- Use raw vector similarity scores instead of re-normalizing by vec_max
- Zero-score results now produce zero fused score (no 0.1 weight floor)
- Rename test_hybrid_rrf.py → test_hybrid_fusion.py with updated assertions
- Update docs and docstrings to reflect score-based fusion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 22:53:00 -06:00
phernandez e46555bf2c fix: guard against closed streams in promo and missing vector tables (#579, #607)
- Wrap isatty() in _is_interactive_session() with try/except ValueError
  so MCP stdio transport shutdown no longer produces noisy tracebacks
- Check both search_vector_chunks AND search_vector_embeddings exist
  before running JOIN queries in get_embedding_status(), fixing
  OperationalError when only the chunks table is present
- Add test for closed-stream scenario

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 18:01:59 -06:00
phernandez 74e6afdc0f fix: create search_vector_chunks in test fixtures for Postgres compatibility
Embedding status tests were creating search_vector_chunks inline using
SQLite-only DDL (AUTOINCREMENT). Added Postgres DDL constants to
models/search.py and wired them into the test fixture so both backends
create the table at setup time — matching what the Alembic migration
does in production.

Also fixed stub search_vector_embeddings to use chunk_id (Postgres
column name) instead of rowid, and added inter-test cleanup to prevent
ordering-dependent failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 17:30:35 -06:00
phernandez aa635b8a8b fix: accept null for expected_replacements in edit_note (#606)
MCP clients may send explicit `null` for unused optional fields.
`expected_replacements: int = 1` caused FastMCP's JSON Schema validation
to reject null before the function body ran. Changed to `Optional[int] = None`
with an effective default resolved inside the function body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 14:18:53 -06:00
phernandez 1856d4b462 fix: update test_project_info assertions for dashboard format
The htop-inspired dashboard (3004d0d1) changed the project info output
from "Basic Memory Project Info" / "Statistics" to project name title
with "Knowledge Graph" section. Update test assertions to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 13:38:01 -06:00
phernandez 73413486bc feat: merge search_by_metadata into search_notes with optional query
Make `query` optional in `search_notes` so it becomes the single search tool.
Remove `search_by_metadata` entirely — it was unreleased and redundant since
`search_notes` already supports `metadata_filters`, `tags`, and `status`.

- 🔧 `query` param is now `Optional[str] = None`
- 🛡️ Added None guards for project detection and URL resolution
-  Added `no_criteria()` validation with helpful error message
- 🗑️ Deleted `search_by_metadata` tool, imports, tests, and contract entry
- 📝 Updated docs, README, and v0.19.0 release notes

Closes #605

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 13:30:42 -06:00
phernandez b09eca1698 feat: add EmbeddingStatus schema and get_embedding_status() service method
Add EmbeddingStatus model to project_info schemas and wire it into
ProjectInfoResponse. ProjectService.get_embedding_status() queries
vector tables for chunk/embedding counts, detects orphaned chunks
and missing embeddings, and recommends reindex when appropriate.
Handles both SQLite and Postgres backends. 🔍

Includes 6 unit tests covering: disabled search, missing vector tables,
entities without chunks, orphaned chunks, healthy state, and integration
with get_project_info().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 11:44:50 -06:00
phernandez 3004d0d1fe feat: replace project info with htop-inspired dashboard
Replace Layout-based display (expanded to terminal width) with a compact
Panel using Table.grid(expand=False). Add horizontal bar charts for note
types (top 5), embedding coverage bar with Unicode blocks, and colored
status dots. Removes verbose sections (most connected, recent activity,
available projects) in favor of a dense, visually engaging dashboard. 📊

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-25 11:11:58 -06:00
phernandez b6369d3d14 fix: cap sqlite-vec knn k parameter at 4096 limit
sqlite-vec enforces k <= 4096 for nearest-neighbor queries. Projects with
>4096 vector chunks would crash all vector/hybrid search because
candidate_limit = max(100, (limit + offset) * 10) exceeded this hard limit.

Clamp the knn k in _run_vector_query while keeping the outer SQL LIMIT
unclamped. Only affects SQLite — pgvector has no such constraint.

Fixes #604

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 23:23:15 -06:00
phernandez db6d0dcd9e docs: add tag: shorthand limitation note to search_notes docstring
The tag: query shorthand doesn't work with hybrid search (default when
semantic search is enabled) because it strips the text query. Document
the workaround: use search_type="text" or the tags parameter instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 20:45:42 -06:00
phernandez 5a5eb443ea docs: remove bm watch from v0.19.0 release notes
The watch command was removed from the codebase — drop the section
from the release notes so it doesn't advertise a non-existent feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 20:26:34 -06:00
phernandez e1df23d793 docs: add v0.19.0 release notes
Covers all 66 commits since v0.18.0: semantic vector search, schema
system, project-prefixed permalinks, per-project cloud routing,
FastMCP 3.0 upgrade, and 16 bug fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 20:20:23 -06:00
phernandez 0462a7d4ba docs: add telemetry disclosure and opt-out instructions to README
Transparent about what we collect (promo/login events only), what we
don't (no PII, no file contents, no per-command tracking), and how
to opt out (BASIC_MEMORY_NO_PROMOS=1).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 19:52:56 -06:00
phernandez 79db876bd6 feat: hardcode Umami analytics defaults for FOSS usage tracking
Bake in cloud.umami.is host and basic-memory-foss site ID so all
open-source installs send anonymous CLI events by default. Users
can still opt out with BASIC_MEMORY_NO_PROMOS=1 or override the
endpoint via env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 19:50:28 -06:00
phernandez 0130573342 fix: prompts call MCP tools directly, sync handles semantic errors, status uses local routing
- Prompts (search, continue_conversation) now call MCP tools directly
  instead of going through API endpoints, matching the recent_activity
  pattern and fixing #526 where prompts returned empty results
- sync_file catches SemanticDependenciesMissingError separately so
  entities are still returned successfully when vector embedding fails,
  with a clear warning instead of silent failure (#578)
- `bm status` and `bm doctor` default to local routing since they scan
  the local filesystem — cloud routing returned Docker-internal paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 19:36:11 -06:00
phernandez c372dfb09f fix: use LinkResolver fallback in build_context for flexible identifier matching (#582)
build_context now falls back to LinkResolver when an exact permalink lookup
returns empty results. This reuses the same resolution pipeline as read_note
(permalink candidates, title match, file path, FTS) so callers no longer
get empty results for valid note identifiers.

Also changes ensure_frontmatter_on_sync default to True — frontmatter is
now added during sync by default. Tests updated accordingly.

🔧 ContextService accepts optional LinkResolver, wired via DI in all 3 factory variants
 2027 unit + 278 integration tests passing

Closes #582

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 18:58:19 -06:00
phernandez d763d86798 fix: unify project path so path is always the local filesystem path
Cloud projects with bisync had a split-brain problem: `path` held a cloud
slug while the actual local directory lived in `local_sync_path`. This caused
`bm status` and file sync to fail for bisync'd cloud projects.

Changes:
- Config migration promotes `local_sync_path` → `path` for entries where
  `path` is a non-absolute cloud slug
- `ensure_project_paths_exists` skips cloud-only projects with slug paths
- `initialize_file_sync` and watch service now keep cloud projects that have
  an absolute local path (bisync copy) instead of skipping all cloud projects
- `sync-setup` and `project add --cloud --local-path` set both `path` and
  `local_sync_path` to the local directory
- `sync-setup` creates the project in the local DB for immediate MCP use
- `_get_sync_project` falls back from `local_sync_path` to `path`
- Config load errors now show user-friendly messages instead of stack traces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 15:08:09 -06:00
phernandez 538af97cba feat: show spinner while fetching cloud projects in bm project list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-24 12:05:33 -06:00
phernandez 054178d155 fix: parameterize SQL queries in search repository type filters
Replace f-string interpolation with parameterized queries for note_types,
search_item_types, and metadata filter paths to prevent SQL injection.

Fixes #591

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-23 22:16:46 -06:00
phernandez 18f00f861d fix: remove hardcoded "main" default from default_project (#575)
When a user's config.json had projects with names other than "main" and
no explicit default_project key, the hardcoded field default of "main"
would not match any project. The model_post_init fixup logic existed but
was untested and only handled the stale-name case, not the None case.

Changes:
- Change default_project field default from "main" to None
- Use model_fields_set to distinguish "config omitted the key" (auto-resolve
  to first project) from "user explicitly set None" (preserve for discovery mode)
- Split model_post_init into two branches: auto-resolve when not explicitly
  provided, correct stale names when explicitly set but invalid
- Remove # pragma: no cover from now-tested branches
- Add 10 new tests covering valid defaults, stale defaults, empty string,
  single project, config file round-trips, and discovery mode preservation

Fixes #575

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-23 21:59:15 -06:00
jope-bm da4d369c32 feat: add created_by and last_updated_by user tracking to Entity (#602)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:20:59 -07:00
Paul Hernandez 0f3889fdd0 fix: Return matched chunk text in search results (#601)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:01:20 -06:00
Paul Hernandez c44291830c chore: rename entity_type to note_type (#600)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:28:57 -06:00
phernandez e1cccba72d docs: add metadata search reference
Document the full structured metadata filter system — operators, MCP tools,
tag shortcuts, and CLI flags — which previously had no dedicated documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-22 00:45:10 -06:00
phernandez 6ff39076a0 fix: double-default display in project list + stale test updates
Use config.default_project as single source of truth for the Default
column in `bm project list`, removing checks against local DB and cloud
API is_default fields that could independently mark multiple projects.

Also updates tests that were out of date after get_project_mode changed
to default unknown projects to CLOUD:
- test_get_client_local_project_uses_asgi_transport: register "main" as LOCAL
- test_run_filters_cloud_projects_each_cycle: register local project in config
- test_new_project_addition_scenario: register projects as LOCAL in config
- test_get_project_mode_defaults_to_cloud: assert new CLOUD default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-22 00:33:25 -06:00
phernandez 56cefbaafd add OSS discount code to README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-21 22:28:13 -06:00
phernandez 3337c7d1ff fix cli test for project move local only
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-21 21:02:13 -06:00
phernandez e248763a73 fix: update tool contract test for list_memory_projects workspace param
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-21 20:12:47 -06:00
phernandez 2e5813d31e feat: CLI refactoring + workspace-aware cloud project listing
Refactor CLI commands to use typed ProjectClient instead of raw HTTP calls,
and add workspace metadata to cloud project listings so users can distinguish
personal vs organization projects.

Key changes:
- 🔧 CLI commands now use ProjectClient typed API clients instead of
  call_get/call_post with manual URL construction
- 🏢 Cloud project listings include workspace_name, workspace_type, and
  workspace_tenant_id for each cloud-sourced project
- Pass config.default_workspace when fetching cloud projects via
  _fetch_cloud_projects() and CLI list_projects
- Add --workspace flag to `bm project list` for explicit workspace override
- Add "Workspace" column to CLI project list table
- Add `bm tool list-projects` and `bm tool list-workspaces` JSON commands
- Comprehensive tests for workspace passthrough, merge behavior, and CLI routing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-21 20:04:03 -06:00
phernandez 2cde8d2659 clean up cli commands
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-21 16:06:43 -06:00
Paul Hernandez 9515130b2a feat: upgrade fastmcp 2.12.3 to 3.0.1 with tool annotations (#598)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 12:28:30 -06:00
Paul Hernandez b86dd6fb53 feat: Fix bm CLI runtime defects and audit regressions (#596)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-20 22:59:29 -06:00
Paul Hernandez 8451f2b1d7 feat: add frontmatter validation to schema system (#597)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 22:57:19 -06:00
Paul Hernandez ee0397513d fix: recent_activity dedup + pagination across MCP tools (#595)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:48:58 -06:00
Drew Cain 9c9ff2931d fix: backend-specific distance-to-similarity conversion (#593)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-02-20 20:15:46 -06:00
Paul Hernandez bbe6c1e8f3 chore: add ty as supplemental type checker (#594)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-20 20:14:46 -06:00
Paul Hernandez edb7991ccf fix: strip NUL bytes from content before PostgreSQL search indexing (#592)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:11:04 -06:00
phernandez d7faeb754e feat: Add destination_folder parameter to move_note tool
Allows callers to move a note into a folder while preserving its original
filename — no need for a separate read_note round-trip to extract the basename.

- destination_folder is mutually exclusive with destination_path
- Rejected for directory moves (is_directory=True)
- Uses Path().name and PureWindowsPath().as_posix() for cross-platform compat
- Validates resolved path against path traversal attacks
- Includes formatting fixes in promo.py and test_analytics.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-20 02:06:04 -06:00
jope-bm deef89a724 feat: Add display_name and is_private to ProjectItem (#574)
Signed-off-by: Joe P <joe@basicmemory.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
2026-02-20 01:44:07 -06:00
Paul Hernandez 30499a9f61 feat: Let stdio MCP honor per-project cloud routing (#590)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:43:45 -06:00
bm-clawd 1ac65b944c feat: CLI analytics via Umami event collector (#572)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-20 00:09:42 -06:00
Paul Hernandez 306e562281 chore: Make semantic deps default, auto-backfill embeddings, and default search to semantic (#586)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-19 18:31:15 -06:00
phernandez ed82b0c417 Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-02-18 22:23:53 -06:00
Paul Hernandez 0cb3f95d67 feat: Add JSON output mode for BM MCP tools (default text) (#585)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-02-18 22:18:29 -06:00
phernandez 0a36256f8a Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-02-18 10:50:58 -06:00
phernandez 36e67e6eec ci: split and speed up PR test matrix
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-18 10:41:02 -06:00
Paul Hernandez f2683291e4 feat: add workspace selection flow for MCP and CLI (#576)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-17 22:48:06 -06:00
phernandez 8c05a9ec80 fix: stabilize semantic search defaults, FTS fallback, and postgres project sync
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-17 15:36:46 -06:00
phernandez a6d8d4c0f6 Add ensure_frontmatter_on_sync with precedence warning
Add a new config option to enforce frontmatter on markdown sync when missing, writing derived title/type/permalink and updating in-memory metadata before upsert. Add startup warning when this option is combined with disable_permalinks to make precedence explicit. Add config/sync/initialization tests for the new behavior, and stabilize project list CLI integration assertions by forcing a wide terminal in tests to avoid Rich truncation.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-16 22:08:09 -06:00
phernandez 0239f4abb4 Simplify local/cloud routing and clarify project targeting
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-16 21:42:25 -06:00
Paul Hernandez 9259a7eb59 feat: min-similarity override, edit-note CLI, and strip-frontmatter (#571)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:56:00 -06:00
Paul Hernandez 55d675e278 feat: min_similarity override, cloud promo improvements (#570)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:44:02 -06:00
Drew Cain 6afe4fd0cc feat: expose external_id in EntityResponse and link resolver (#569)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 20:33:37 -06:00
phernandez 113d1b6f1b formatting fix
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-14 09:56:37 -06:00
Paul Hernandez 545804f194 feat: Add project-prefixed permalinks and memory URL routing (#544)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:45:50 -06:00
Paul Hernandez 8bc03d1357 feat(mcp): add MCP UI variants and TUI output (#545)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-13 15:21:48 -06:00
Paul Hernandez f6e0a5b5bb fix: Speed up bm --version startup (#534)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 11:12:52 -06:00
Paul Hernandez 7624a20d8d feat: isolate default sqlite db by config dir (#567)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-13 11:11:13 -06:00
Paul Hernandez d84708ca7f feat: add per-project local/cloud routing with API key auth (#555)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:52:50 -06:00
Paul Hernandez 1428d18de1 fix: make semantic search dependencies optional extras (#566)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:46:26 -06:00
Paul Hernandez 312662f382 feat: Add cloud discovery touchpoints to CLI and MCP (#546)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-12 21:01:45 -06:00
Paul Hernandez ed9487708e feat: enable default_project_mode by default (#560)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:34:46 -06:00
Paul Hernandez 8df88e4d02 feat: add basic-memory watch CLI command (#559)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:10:09 -06:00
Paul Hernandez 07778790d3 feat: add semantic vector search for SQLite and Postgres (#550)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: bm-clawd <clawd@basicmemory.com>
Co-authored-by: bm-clawd <clawd@basicmemory.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:44:47 -06:00
Paul Hernandez b609c4e531 fix: use global --header for Tigris consistency on all rclone transactions (#564)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:43:05 -06:00
Paul Hernandez f1a065bce3 chore: Release/v0.18.2 (#563)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:41:38 -06:00
phernandez 2b94d9a278 fix formatting for tigris headers
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-12 13:22:31 -06:00
Paul Hernandez 344e651693 fix: use VIRTUAL instead of STORED columns in SQLite migration (#562)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:24:51 -06:00
Paul Hernandez c97733d785 feat: Schema system for Basic Memory (#549)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:04:42 -06:00
phernandez 00537272c6 chore: update version to 0.18.1 for v0.18.1 release 2026-02-11 14:28:52 -06:00
phernandez b057912452 docs: add CHANGELOG entry for v0.18.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-11 14:24:26 -06:00
Paul Hernandez 8489a3d37e fix: add X-Tigris-Consistent headers to all rclone commands (#558)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:22:49 -06:00
Paul Hernandez a47c9c021f feat: add --format json to CLI tool commands (#552)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: bm-clawd <clawd@basicmemory.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: bm-clawd <clawd@basicmemory.com>
2026-02-08 14:56:29 -06:00
phernandez c46d7a6833 fix: add POST legacy compat routes for v0.18.0 CLI
The v0.18.0 CLI (Homebrew) calls POST /projects/projects for project add
and POST /projects/config/sync for config sync. The previous legacy compat
fix (a0e754b) only added GET for list_projects but missed POST endpoints.

This caused 405 Method Not Allowed when running `bm project add` in cloud mode.

🔗 Logfire trace: trace_id=019c398cb257f040c1255821b6d5e385

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-07 13:41:41 -06:00
Paul Hernandez 343a6e118b fix: Handle EntityCreationError as conflict (#541)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-03 22:28:58 -06:00
phernandez a0e754b7ae fix: restore legacy /projects/projects endpoint for older CLI versions
Older versions of basic-memory CLI (v0.17.4 and earlier) call
GET /projects/projects to list projects. This endpoint was removed
when we migrated to v2 routers.

Add explicit route at /projects/projects (without trailing slash) to
avoid 307 redirects that the cloud proxy doesn't follow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-02 22:26:48 -06:00
Paul Hernandez 24ca5f6804 fix: recent_activity prompt defaults (#533)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-02 19:22:33 -06:00
Paul Hernandez f1d50c2ba7 feat: Support tag: query shorthand in search (#535)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-02 19:22:16 -06:00
Paul Hernandez 8072449a78 chore: Add fast feedback loop tooling (#538)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-01 23:04:59 -06:00
phernandez 45d3f58e4d Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-02-01 21:17:39 -06:00
phernandez d9c8923148 fix ci runner for tests
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-01 21:16:10 -06:00
phernandez 15bd6b95ef fix ci runner i
Signed-off-by: phernandez <paul@basicmachines.co>
2026-02-01 21:10:26 -06:00
phernandez 0715dcff3d run ubuntu tests on depot
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-31 23:25:46 -06:00
Paul Hernandez 009e84926d fix: stabilize metadata filters on postgres (#536)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-31 22:31:00 -06:00
phernandez 8838571509 Add metadata filter tests and fix fast write external_id
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-31 15:27:05 -06:00
Paul Hernandez 530cbac73f feat: fast edit entities, refactors for webui, enhance search (#532)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-31 15:16:52 -06:00
Paul Hernandez e3ced49d9d fix: prevent spurious 'metadata: {}' in frontmatter output (#530)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 12:17:10 -06:00
Drew Cain 8f962fdd87 chore: update version to 0.18.0 for v0.18.0 release 2026-01-29 22:41:42 -06:00
Drew Cain fbb497f6dc docs: add CHANGELOG entry for v0.18.0 2026-01-29 22:41:14 -06:00
Drew Cain 0023e736ab feat: add context-aware wiki link resolution with source_path support (#527)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 19:11:41 -06:00
Paul Hernandez 0b2080114b feat: add directory support to move_note and delete_note tools (#518)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 19:52:10 -06:00
Paul Hernandez 8730067f3a feat: Feature/517 local mcp cloud mode (#522)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 19:51:49 -06:00
Drew Cain e14ba92631 fix: resolve MCP prompt rendering errors (#524)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:14:41 -06:00
phernandez 9d98892570 chore: update version to 0.17.9 for v0.17.9 release 2026-01-24 12:55:58 -06:00
phernandez 3be4495723 docs: add CHANGELOG entry for v0.17.9
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-24 12:55:27 -06:00
Paul Hernandez 17c0e0a29b fix: check config default_project only in local mode for remove_project (#523)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 12:53:44 -06:00
phernandez 7ebf16a95d chore: update version to 0.17.8 for v0.17.8 release 2026-01-24 11:43:44 -06:00
phernandez c05075f8d4 docs: add CHANGELOG entry for v0.17.8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-24 11:43:21 -06:00
phernandez 4cef9281ca docs: add CHANGELOG entry for v0.17.7
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-24 11:39:41 -06:00
Paul Hernandez 6888effef2 fix: correct get_default_project() query to check for True instead of not NULL (#521)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 11:37:05 -06:00
Paul Hernandez 38616c345d fix: read default project from database in cloud mode (#520)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 10:44:04 -06:00
phernandez f3c1aa895c fix links in README.md to remove smithery badge
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-22 12:58:03 -06:00
phernandez d978aba09b fix links in README.md to remove glama.ai
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-22 12:56:33 -06:00
phernandez 2aaee734c9 fix links in README.md to point to basicmemory.com instead of basicmachines.co
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-22 12:53:18 -06:00
Paul Hernandez 369ad37b3d feat: add SPEC-29 Phase 3 bucket snapshot CLI commands (#476)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:55:01 -06:00
Drew Cain 4e5f701d22 Fix server.json runtimeArguments format
- Use proper Argument object format instead of plain strings
- Add .mcpregistry tokens to .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
2026-01-19 12:23:29 -06:00
Drew Cain 9d9ea4d61c chore: update version to 0.17.7 for v0.17.7 release 2026-01-19 12:12:35 -06:00
Drew Cain 7a502e6474 feat: Add MCP registry publication files (#515)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 11:57:50 -06:00
Paul Hernandez c7835a9d5c fix: ensure external_id is set on entity creation (#512) (#513)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 11:14:48 -06:00
Paul Hernandez 85835ae533 chore: Remove OpenPanel telemetry (#514)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 22:24:27 -06:00
phernandez 671e3d4db9 chore: update version to 0.17.6 for v0.17.6 release 2026-01-17 15:11:34 -06:00
phernandez e11aeff8d9 docs: add changelog entry for v0.17.6
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-17 15:11:17 -06:00
phernandez 803f3efe53 turn sync logging to debug
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-17 15:10:04 -06:00
phernandez d6dab8552c Merge branch 'main' of github.com:basicmachines-co/basic-memory 2026-01-17 15:09:04 -06:00
phernandez d1d433df15 remove logfire config, and specs docs, turn lifespan logging to debug
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-17 15:08:54 -06:00
Drew Cain 1799c94953 fix: Docker container Python symlink broken at runtime (#510)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 11:59:35 -06:00
Drew Cain 07996181b3 chore: add doc update reminder to release commands
Remind developers to update docs.basicmemory.com and basicmachines.co
after running just release or just beta.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:21:23 -06:00
phernandez a1c37c1dba chore: update version to 0.17.5 for v0.17.5 release 2026-01-11 16:53:55 -06:00
phernandez aff53cca93 docs: add changelog entry for v0.17.5
- Python 3.14 compatibility fix for CLI commands hanging on exit
- Skip nest_asyncio on Python 3.14+
- Update pyright to 1.1.408 for Python 3.14 support
- Fix SQLAlchemy rowcount typing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-11 16:53:38 -06:00
Paul Hernandez 863e0a4e24 fix: prevent CLI commands from hanging on exit (Python 3.14) (#505)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:47:35 -06:00
phernandez eeeade4f07 chore: update version to 0.17.4 for v0.17.4 release 2026-01-05 21:38:03 -06:00
phernandez 03793eaf7c docs: add v0.17.4 changelog entry
- Critical bug fix for search index preservation (#503)
- Major architecture refactor with composition roots (#502)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-05 21:36:14 -06:00
Paul Hernandez 26f7e98932 fix: preserve search index across server restarts (#503)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 21:31:35 -06:00
Paul Hernandez 5947f04bd3 refactor: composition roots, deps split, and typed API clients (#490 roadmap) (#502)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 11:05:58 -06:00
Drew Cain ba1439fefc chore: update version to 0.17.3 for v0.17.3 release 2026-01-03 13:08:11 -06:00
Drew Cain ef411ceb12 docs: add v0.17.3 changelog entry
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 13:07:24 -06:00
Drew Cain c6baf58aa7 fix: update mcp to support protocol version 2025-11-25 (#501)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:39:11 -06:00
phernandez 3c1748cc89 return external_id with directory response in tree
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-02 23:44:03 -06:00
phernandez 9206e7960a return external_id with directory response
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-02 23:43:14 -06:00
Paul Hernandez 53c4c20d22 fix: route ordering for cloud (#499)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-02 21:52:02 -06:00
Paul Hernandez b4486d20bd test: remove stdlib mocks, strengthen integration coverage (#489)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 14:22:01 -06:00
jope-bm a4000f64ce feat: add stable external_id (UUID) to Project and Entity models (#485)
Signed-off-by: Joe P <joe@basicmemory.com>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2026-01-02 13:00:35 -06:00
phernandez 88a1778798 fix(importers): return ImportResult from handle_error
Implement handle_error() in all importers so errors return a concrete ImportResult (success=false, error_message set) instead of None, preventing NoneType.success crashes.

Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-01 11:47:42 -06:00
phernandez 4ce21984a4 fix: use upsert to prevent IntegrityError during parallel search indexing
Replace delete-then-insert pattern with INSERT ... ON CONFLICT for
PostgreSQL search index operations. This fixes race conditions where
parallel entity indexing could cause UniqueViolationError on the
uix_search_index_permalink_project constraint.

Changes:
- Add index_item() override in PostgresSearchRepository with upsert
- Update bulk_index_items() to use ON CONFLICT (permalink, project_id)
- Add CREATE_POSTGRES_SEARCH_INDEX_PERMALINK DDL for test fixtures
- Add tests for upsert behavior on duplicate permalinks

Technical note: Use column-based ON CONFLICT syntax instead of
ON CONSTRAINT (which only works for table constraints, not indexes).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-30 21:55:43 -06:00
phernandez eb7fbaf0bf fix set_default_project in cloud mode
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-30 20:57:45 -06:00
phernandez 8adf1f4ed4 fix: use relative file paths in importers for cloud storage compatibility
Importers now use relative file paths (based on permalink) instead of
absolute paths. This enables proper S3 key generation in cloud environments.

Changes:
- write_entity() accepts str | Path for file_path parameter
- ensure_folder_exists() uses relative paths directly
- All importers pass relative paths to FileService
- FileService handles base_path resolution internally

This prevents S3 keys from including container filesystem paths like
`/app/basic-memory/imports/...` and instead uses clean relative paths
like `imports/20241010-file.md`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-30 20:29:01 -06:00
phernandez 45ce1813e4 refactor: use FileService in importers for cloud compatibility
Refactor importers to use FileService for all file operations instead of
direct filesystem calls. This enables cloud environments to override file
operations via dependency injection (e.g., S3FileService).

Changes:
- Add `to_markdown_string()` method to MarkdownProcessor for content
  serialization without file I/O
- Update Importer base class to accept FileService and use it for:
  - `write_entity()` - now uses FileService.write_file()
  - `ensure_folder_exists()` - now async, uses FileService.ensure_directory()
- Fix direct `mkdir()` calls in:
  - claude_projects_importer.py
  - memory_json_importer.py
- Update deps.py to inject FileService into all importers (v1 and v2)
- Update CLI commands to create and pass FileService to importers
- Update tests to work with new FileService dependency

This follows the pattern used by /knowledge API and SyncService, enabling
cloud to override file operations by providing S3FileService via DI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-30 19:21:32 -06:00
phernandez 2744c4b6a5 add info logging to index_entity_data background task
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-30 12:04:49 -06:00
Paul Hernandez fd732aa6fe fix: set_default_project skips config file update in cloud mode (#486)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 09:05:48 -06:00
Paul Hernandez 537e58ad7d fix: make RelationResponse.from_id optional to handle null permalinks (#484)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 08:12:29 -06:00
phernandez 48e6e84beb chore: update version to 0.17.2 for v0.17.2 release 2025-12-29 16:47:04 -06:00
phernandez 02c14acddb docs: add CHANGELOG entry for v0.17.2
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:46:26 -06:00
phernandez 0b5425f163 don't run full tests in release, just lint, typecheck
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:33:38 -06:00
phernandez 0bcda4a14a fix: allow recent_activity discovery mode in cloud mode
Add `allow_discovery` parameter to `resolve_project_parameter()` that
enables tools like `recent_activity` to work across all projects in
cloud mode without requiring a project parameter.

- Add `allow_discovery: bool = False` param to resolve_project_parameter
- In cloud mode with allow_discovery=True, return None instead of error
- Update recent_activity to use allow_discovery=True
- Fix circular import by deferring call_get import inside functions
- Add comprehensive tests for resolve_project_parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:22:48 -06:00
phernandez 7a49f57dee chore: update version to 0.17.1 for v0.17.1 release
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 10:34:06 -06:00
phernandez 58db2817d2 docs: add CHANGELOG entry for v0.17.1
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 10:20:00 -06:00
Paul Hernandez 98fbd60527 fix: only set BASIC_MEMORY_ENV=test during pytest runs (#482)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-12-29 10:16:45 -06:00
phernandez 6281a81256 chore: update version to 0.17.0 for v0.17.0 release
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-28 16:06:03 -06:00
phernandez be1d0b169f style: format telemetry.py
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-28 16:05:07 -06:00
phernandez 148bf6f75a docs: add CHANGELOG entry for v0.17.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-28 16:03:13 -06:00
phernandez 272a983709 add foss as telementry source, disable analytics for tests
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-28 13:41:40 -06:00
phernandez ef7adb7b99 fix: add cloud_mode check to initialize_app()
MCP server crashes in cloud mode with:
ValueError: DATABASE_URL must be set when using Postgres backend

Root cause: initialize_app() did not check cloud_mode_enabled before
trying to initialize the database. Only ensure_initialization() had
the check. In cloud mode, tenant DBs are per-request via headers,
not via DATABASE_URL environment variable.

Also includes minor formatting fix in telemetry.py.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-28 10:31:17 -06:00
phernandez 3cd9178415 refactor: centralize test environment detection in config.is_test_env
Add is_test_env property to BasicMemoryConfig that checks:
- config.env == "test"
- BASIC_MEMORY_ENV env var is "test"
- PYTEST_CURRENT_TEST is set

Replace duplicated test detection logic in:
- api/app.py
- mcp/server.py
- services/initialization.py
- telemetry.py (disables telemetry during tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-27 12:44:42 -06:00
Paul Hernandez 856737fe3c feat: add anonymous usage telemetry (Homebrew-style opt-out) (#478)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 22:09:48 -06:00
Paul Hernandez 1fd680c3f1 feat: add auto-format files on save with built-in Python formatter (#474)
Signed-off-by: Cedric Hurst <cedric@spantree.net>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Cedric Hurst <cedric@spantree.net>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Sebastian B Otaegui <feniix@users.noreply.github.com>
Co-authored-by: Cedric Hurst <cedric@divideby0.io>
2025-12-24 15:39:22 -06:00
phernandez 38919d11cb docs: update CLAUDE.md with accurate CLI commands and code guidelines
- Add Code Change Guidelines section (full file read, minimize diffs, fail fast, no guessing)
- Add Literate Programming Style section (section headers, decision point comments)
- Fix CLI commands: `tools` -> `tool`, add project management commands
- Fix cloud commands to match current CLI (status, setup)
- Remove non-existent MCP tools (get_current_project, sync_status)
- Add ChatGPT-compatible tools (search, fetch)
- Remove non-existent json_canvas_spec prompt
- Add /importers to codebase architecture
- Remove unused python-developer and system-architect agents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 15:16:02 -06:00
phernandez 85684f848f fix: handle UTF-8 BOM in frontmatter parsing
Fixes #452 - Imported conversations not fully indexed

Files with UTF-8 BOM (Byte Order Mark) at the start would fail frontmatter
detection, causing:
- Title to fall back to filename instead of frontmatter value
- Permalink to be null in the database

Added strip_bom() helper function and updated all frontmatter-related
functions to strip BOM before processing:
- has_frontmatter()
- parse_frontmatter()
- remove_frontmatter()
- EntityParser.parse_markdown_content()

Added comprehensive tests for BOM handling with various scenarios.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 14:28:50 -06:00
Paul Hernandez 14ce5a3bd0 fix: handle null titles in ChatGPT import (#475)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-24 13:50:30 -06:00
phernandez 45d6caf723 fix: remove MaxLen constraint from observation content
The API Pydantic schema had a MaxLen(1000) constraint on observation
content while the database uses SQLAlchemy's Text type (unlimited).
This mismatch caused validation errors when observations exceeded
1000 characters (e.g., JSON schemas with 1458+ chars).

Removed the MaxLen constraint to match the DB schema. Retained:
- BeforeValidator(str.strip) for whitespace cleaning
- MinLen(1) to ensure non-empty content

Added comprehensive tests to verify:
- Long content (10K+ chars) is accepted
- Very long content (50K+ chars) is accepted
- Empty/whitespace-only content is still rejected
- Whitespace stripping still works

Fixes #385

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 13:41:28 -06:00
phernandez 1652f862dd fix: handle FileNotFoundError gracefully during sync
When a file exists in the database but is missing from the filesystem,
the sync worker now treats this as a deletion instead of crashing.

The sync_file() method catches FileNotFoundError specifically and calls
handle_delete() to clean up the orphaned database record. This prevents
the sync from failing on database/filesystem inconsistencies that can
occur due to race conditions, manual file deletions, or cloud storage
caching issues.

Includes a test to verify the graceful handling behavior.

Fixes #386

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 13:34:59 -06:00
phernandez c23927d124 fix: use canonical project names in API response messages
Use database-retrieved project names (new_project.name, old_project.name)
instead of input parameters (project_data.name, name) in v1 API response
messages to ensure consistent project name casing.

The v2 API already did this correctly. This fixes issue #450 where project
names would display with different casing between add and remove operations.

Fixes #450

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 13:28:06 -06:00
jope-bm 1a74d85973 feat: Complete Phase 2 of API v2 migration - Update MCP tools to use v2 endpoints (#447)
Signed-off-by: Joe P <joe@basicmemory.com>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2025-12-24 12:59:14 -06:00
phernandez d71c6e8568 fix: suppress CLI warnings for cleaner output
Suppress DeprecationWarning from aiosqlite and LogfireNotConfiguredWarning
that were cluttering CLI output.

The key fix is applying warnings.filterwarnings("ignore") AFTER all imports
in main.py, because authlib (imported via cloud commands) adds a
DeprecationWarning filter that overrides earlier suppressions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 12:21:14 -06:00
phernandez 63b98491be fix: prevent DEBUG logs from appearing on CLI stdout
Remove loguru's default handler at the very start of cli/app.py,
before any other imports. This prevents module-level code (like
TemplateLoader.__init__) from logging to stdout during import.

The import chain cli/commands/project.py -> mcp/async_client.py ->
api/app.py -> api/routers/prompt_router.py -> api/template_loader.py
triggers TemplateLoader() instantiation at DEBUG level before
init_cli_logging() can remove the default handler.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-24 11:42:25 -06:00
Paul Hernandez 622d37e4a8 fix: detect rclone version for --create-empty-src-dirs support (#473)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 11:27:49 -06:00
Paul Hernandez 916baf8971 fix: prevent CLI commands from hanging on exit (#471)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 11:27:11 -06:00
Drew Cain 95937c6d0a fix: make test-int-postgres compatible with macOS
Use gtimeout (Homebrew) or timeout (Linux), falling back to running
without timeout if neither is available. This fixes 'command not found'
errors on macOS which doesn't have GNU timeout by default.
2025-12-20 10:27:26 -06:00
Drew Cain 24dc9a2931 chore: update version to 0.16.3 for v0.16.3 release 2025-12-20 09:53:37 -06:00
Drew Cain 85c63e5a7a docs: add CHANGELOG entry for v0.16.3 2025-12-20 09:26:00 -06:00
Drew Cain f227ef6a86 fix: Pin FastMCP to 2.12.3 to fix MCP tools visibility (#464)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 08:54:44 -06:00
Paul Hernandez 897b1edaa4 fix: Reduce watch service CPU usage by increasing reload interval (#458)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-12-17 11:37:01 -06:00
Paul Hernandez 0c12a39a98 test: Add integration test for issue #416 (read_note with underscored folders) (#453)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-12-17 09:56:45 -06:00
Paul Hernandez efbc758325 fix: await background sync task cancellation in lifespan shutdown (#456)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-12-17 09:53:34 -06:00
Paul Hernandez a0f20eb102 chore: more Tenantless fixes (#457)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:34:05 -06:00
Paul Hernandez 78673d8e51 chore: Cloud compatibility fixes and performance improvements (#454)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 20:07:55 -06:00
phernandez 126c0495c0 Merge branch 'main' of github.com:basicmachines-co/basic-memory 2025-12-13 15:24:43 -06:00
Paul Hernandez 4a43d7df4a remove logfire instrumentation
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-13 15:22:14 -06:00
Paul Hernandez c462faf046 Replace py-pglite with testcontainers for Postgres testing (#449)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 22:17:56 -06:00
Cedric Hurst 70bb10be1d fix: respect --project flag in background sync (fixes #434) (#436)
Signed-off-by: Cedric Hurst <cedric@spantree.net>
2025-12-08 12:58:18 -06:00
phernandez fbf9045d78 use asyncpg for just db-migrate
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-05 15:01:35 -06:00
phernandez 1094210c52 fix broken sqlite migration
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-02 20:23:55 -06:00
phernandez 391feb639f add delete cascade to entity to delete search_index (postgres only)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-02 10:04:21 -06:00
phernandez a920a9ff29 feat: Add project_id to Relation and Observation for efficient project-scoped queries
Denormalizes project_id onto Relation and Observation tables to enable
efficient project-scoped queries without joins. Migration backfills
from associated entity and adds pg_trgm extension with GIN indexes
for fuzzy link resolution on PostgreSQL.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-01 21:54:56 -06:00
phernandez 05efe8701c test: Verify update() returns entity with eager-loaded relations
Add test confirming entity_repository.update() returns the entity with
observations and relations eagerly loaded, eliminating the need for a
separate find_by_id() call after update.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-01 16:22:21 -06:00
phernandez 0eaf30bb06 remove conflict constraint name from relation_repository.py
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-30 19:31:09 -06:00
phernandez 0818bda565 feat: Add bulk insert with ON CONFLICT handling for relations
Add add_all_ignore_duplicates() method to RelationRepository for bulk
inserting relations with ON CONFLICT DO NOTHING. This handles cases
where the same [[wiki link]] appears multiple times in a document,
silently ignoring duplicates based on the (from_id, to_name, relation_type)
unique constraint.

Works with both SQLite and PostgreSQL dialects.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-30 14:59:52 -06:00
phernandez 6f99d2e551 perf: lightweight permalink resolution to avoid eager loading
Add optimized repository methods for resolve_permalink() that skip
eager loading of observations and relations:

- permalink_exists(): Check existence without loading entity
- get_file_path_for_permalink(): Get only file_path column
- get_permalink_for_file_path(): Get only permalink column
- get_all_permalinks(): Get all permalinks as strings
- get_permalink_to_file_path_map(): Bulk lookup mapping
- get_file_path_to_permalink_map(): Reverse mapping

Updated entity_service.resolve_permalink() to use these lightweight
methods instead of loading full entities with all relationships.

Also added logfire instrumentation to markdown utils.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-30 14:34:31 -06:00
phernandez 73d940e064 fix: observation parsing and permalink limits (#446)
1. Hashtag detection now checks for standalone words starting with #
   instead of just checking if # appears anywhere in content.
   This prevents HTML color codes like #4285F4 from being
   interpreted as hashtags.

2. Observation permalinks now truncate content to 200 chars
   to stay under PostgreSQL's btree index limit of 2704 bytes.

Added tests for both fixes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-30 00:12:04 -06:00
phernandez c3678a11d2 truncate content_stems to fix Postgres 8KB index row limit
Large documents (like ~1MB conversation imports) exceed Postgres's 8KB
index row limit, causing ProgramLimitExceededError. Truncate content_stems
to 6000 characters (with headroom for other columns) before indexing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-29 20:23:09 -06:00
phernandez 203d684c24 fix integrity error handling when setting forward relation refs
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-29 19:03:12 -06:00
phernandez a872220924 disable pooling for postgres db
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-29 13:24:47 -06:00
phernandez 7d763a66ff use entity.mtime for updated at in api
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-29 13:21:58 -06:00
phernandez b5d4fb559c fix: postgres/neon connection settings and search index dedupe
- Reduce db_pool_recycle from 3600s to 180s for Neon scale-to-zero
- Add connect_args for Neon serverless (statement cache, timeouts, app name)
- Dedupe observation permalinks in search indexing to avoid unique constraint violations
- Add tests for duplicate observation permalink handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-29 11:24:08 -06:00
phernandez 830775276d remove record_return=True from logfire spans
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-28 18:36:09 -06:00
phernandez ed894fc3ed get db pool sizes from config
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-28 16:50:57 -06:00
phernandez 704338edcf remove logfire.instrument_fastapi(app) from app.py
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-28 13:52:18 -06:00
phernandez 0ca02a7ebe add logfire instrumentation to services and repository code
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-28 12:47:29 -06:00
jope-bm 28cc5225a7 feat: Implement API v2 with ID-based endpoints (Phase 1) (#441)
Signed-off-by: Joe P <joe@basicmemory.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2025-11-27 10:35:55 -06:00
phernandez 9b7bbc7116 formatting and logic change to resolve_relations, remove fuzzy search 2025-11-25 22:54:37 -06:00
phernandez 138c283d6c add postgres db type 2025-11-25 20:25:56 -06:00
phernandez 7a8954c37e add extra logic for cloud-indexing improvements 2025-11-25 13:52:58 -06:00
phernandez 10c7c19c03 fix db url for sqlite migrations
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-21 13:21:20 -06:00
Paul Hernandez fb5e9e1d77 feat: Add PostgreSQL database backend support (#439)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 11:20:29 -06:00
phernandez 66b91b2847 ci: Add PostgreSQL testing to GitHub Actions workflow
Add Postgres service container and separate test step for PostgreSQL backend testing.
The Postgres tests only run on Linux runners since GitHub Actions service containers
are only available on Linux.

- Add postgres:17 service container with health checks
- Add 'Run tests (Postgres)' step with Linux-only condition
- Rename existing test step to 'Run tests (SQLite)' for clarity

This enables CI testing of dual database backend support introduced in the
postgres-support feature branch.

Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-18 12:25:18 -06:00
Cedric Hurst b004565df9 fix: handle periods in kebab_filenames mode (#424) 2025-11-18 06:49:55 -05:00
Drew Cain a258b73e1d chore: update version to 0.16.2 for v0.16.2 release 2025-11-16 21:30:59 -06:00
Drew Cain 9a845f2906 docs: prepare for v0.16.2 release 2025-11-16 21:28:13 -06:00
Drew Cain 6517e9845f fix: Use platform-native path separators in config.json (#429)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-13 09:12:26 -06:00
Drew Cain 1af05392ee fix: Add rclone installation checks for Windows bisync commands (#427) 2025-11-12 14:22:14 -06:00
Brandon Mayes cad7019c89 fix: main project always recreated on project list command (#421) 2025-11-12 09:57:08 -05:00
phernandez 099c334e3d chore: update version to 0.16.1 for v0.16.1 release 2025-11-11 09:21:47 -06:00
phernandez 7685586178 docs: Add v0.16.1 CHANGELOG entry for Windows line ending fix
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-11 09:10:26 -06:00
Paul Hernandez e9d0a944a9 fix: Handle Windows line endings in rclone bisync (#422)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-11 09:08:20 -06:00
phernandez caf3c14bb1 chore: update version to 0.16.0 for v0.16.0 release 2025-11-10 19:19:48 -06:00
phernandez c5d9067754 docs: Add v0.16.0 CHANGELOG entry with comprehensive release notes
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-10 19:17:29 -06:00
phernandez e0fc59ea97 style: Format upload.py for better readability
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-10 19:15:03 -06:00
Paul Hernandez 49b2adc35c fix: skip archive files during cloud upload (#420)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 19:04:49 -06:00
Paul Hernandez 1646572f69 fix: Rename write_note entity_type to note_type for clarity (#419)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 19:02:50 -06:00
Paul Hernandez f0d7398815 fix: Quote string values in YAML frontmatter to handle special characters (#418)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 18:13:48 -06:00
Paul Hernandez 581b7b17c6 fix: Add explicit type annotations to MCP tool parameters (#394)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 17:02:17 -06:00
Paul Hernandez d775f7bab9 fix: Simplify search_notes schema by removing Optional wrappers (#395)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 16:30:59 -06:00
Drew Cain fc01f6abaf fix: Replace Unicode arrows with ASCII for Windows compatibility (#414) 2025-11-10 16:30:41 -06:00
Paul Hernandez 4614fd09d5 fix: Handle dict objects in write_resource endpoint (#415)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-11-10 16:30:27 -06:00
phernandez 0d4ad7bbf0 remove v0.15.0 info from assistant-guide
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-10 16:25:49 -06:00
jope-bm 7ccec7eba2 feat: Add run_in_background parameter to sync endpoint with tests (#417)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 09:04:43 -07:00
Paul Hernandez 021af74545 fix: Strip duplicate headers in edit_note replace_section (#396)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
2025-11-02 14:20:11 -06:00
phernandez 2ad0ee9d5d fix: Use force_full=true for database sync after project sync/bisync
After rclone synchronizes files between local and cloud storage, the
database needs to perform a full scan to ensure it captures all changes.
Previously, incremental sync (watermark optimization) could miss files
that were changed remotely.

Changes:
- project sync command now calls /project/sync?force_full=true
- project bisync command now calls /project/sync?force_full=true
- Ensures complete database refresh after file synchronization

This guarantees the database is fully in sync with the filesystem
after any rclone sync or bisync operation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-11-02 13:26:08 -06:00
Brandon Mayes c9946ecf1e fix: Various rclone fixes for cloud sync on Windows (#410)
Signed-off-by: Brandon Mayes <5610870+bdmayes@users.noreply.github.com>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2025-11-02 11:26:57 -06:00
Drew Cain 0ba6f219f1 fix: Windows CLI Unicode encoding errors (#411)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-11-02 10:20:48 -06:00
Paul Hernandez 0b3272ae6e feat: SPEC-20 Simplified Project-Scoped Rclone Sync (#405)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-02 09:35:26 -06:00
Paul Hernandez a7d7cc5ee6 fix: Normalize YAML frontmatter types to prevent AttributeError (#236) (#402)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-27 09:19:50 -05:00
Paul Hernandez a7e696b039 Add free trial information to README
Added information about a 7-day free trial.

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-10-24 10:05:11 -05:00
Paul Hernandez 8aaddb6d45 Add free trial information to README
Added information about a 7-day free trial to the README.

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-10-24 10:04:41 -05:00
Paul Hernandez d7565312fc Announce Basic Memory Cloud launch in README
Added a section announcing the launch of Basic Memory Cloud with details on cross-device support and early supporter pricing.

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-10-24 09:53:14 -05:00
Paul Hernandez c7e6eab02f feat: Add delete_notes parameter to remove project endpoint (#391)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-21 14:09:20 -05:00
Paul Hernandez bb8da31472 fix: Handle null, empty, and string 'None' title in markdown frontmatter (#387) (#389)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-10-21 09:29:19 -05:00
Paul Hernandez e78345ff25 feat: Streaming Foundation & Async I/O Consolidation (SPEC-19) (#384)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-21 09:03:59 -05:00
Paul Hernandez 32236cd247 fix: Handle YAML parsing errors gracefully in update_frontmatter (#378) (#379)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 20:17:58 -05:00
Paul Hernandez 4fd6d0c648 fix: Optimize sync memory usage to prevent OOM on large projects (#380)
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-16 20:17:34 -05:00
Paul Hernandez e6c8e3662c fix: preserve mtime webdav upload 376 (#377)
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 17:18:10 -05:00
Paul Hernandez 449b62d947 fix: Prevent deleted projects from being recreated by background sync (#193) (#370)
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-16 15:16:24 -05:00
Paul Hernandez b7497d7484 fix: Use filesystem timestamps for entity sync instead of database operation time (#138) (#369)
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-16 14:21:27 -05:00
Paul Hernandez d1431bdb1b fix: Handle YAML parsing errors and missing entity_type in markdown files (#368)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 13:18:58 -05:00
Paul Hernandez 171bef717f fix: Resolve UNIQUE constraint violation in entity upsert with observations (#187) (#367)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 12:30:56 -05:00
Paul Hernandez 729a5a3b8d fix: Terminate sync immediately when project is deleted (#366)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 11:07:12 -05:00
Paul Hernandez 434cdf24dd feat: Add circuit breaker for file sync failures (#364)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-16 09:47:48 -05:00
Paul Hernandez 7f9c1a97a4 feat: Add --verbose and --no-gitignore options to cloud upload (#362)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-15 20:03:36 -05:00
Paul Hernandez 53fb13b054 fix: Make project creation endpoint idempotent (#357)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-15 19:27:41 -05:00
Paul Hernandez bd6c8348b8 fix: Handle None text values in Claude conversations importer (#353)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-10-15 16:08:57 -05:00
phernandez 994c8b8e7e chore: update version to 0.15.2 for v0.15.2 release 2025-10-14 09:36:47 -05:00
phernandez a78e8c3ac5 style: Apply linter formatting changes 2025-10-14 09:34:10 -05:00
phernandez 53900c5baa fix: Project commands now respect cloud_mode at runtime
- Moved config evaluation from module load time to runtime
- Unified add_project command to handle both cloud and local modes
- Commands (default, sync-config, move) now check cloud_mode at runtime
- Fixes test failures where monkeypatch wasn't applied before command registration
2025-10-14 09:24:13 -05:00
phernandez 02c6de3387 docs: Add v0.15.2 changelog entry 2025-10-14 00:43:14 -05:00
phernandez 9ccf4b6b56 remove extra conole out from sync message after upload
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-14 00:38:02 -05:00
phernandez ba74ca7e18 fix: Update CloudProjectCreateResponse schema to match API response
The /proxy/projects/projects POST endpoint returns a ProjectStatusResponse
with fields: message, status, default, old_project, new_project.

Updated CloudProjectCreateResponse schema to match this format instead of
expecting name, path, message fields.

Also updated all related tests to use the correct response format:
- tests/cli/test_cloud_utils.py (3 tests)
- tests/cli/test_bisync_commands.py (1 test)

Fixes the validation error when creating cloud projects via upload command:
"bm cloud upload --project test --create-project specs"

Signed-off-by: Pablo Hernandez <pablo@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-13 23:56:59 -05:00
Paul Hernandez 5258f45730 feat: Add WebDAV upload command for cloud projects (#356)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Pablo Hernandez <pablo@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-13 23:20:54 -05:00
phernandez e773c002ce chore: update version to 0.15.1 for v0.15.1 release 2025-10-13 11:04:44 -05:00
phernandez e70ba944e7 docs: Add v0.15.1 changelog entry
Add comprehensive changelog for v0.15.1 release including:
- Performance improvements (43% faster sync, 10-100x faster directory ops)
- Bug fixes for cloud mode, project paths, and Claude Desktop compatibility
- New features: async client context manager, BASIC_MEMORY_PROJECT_ROOT
- Documentation updates and SPEC-15/16 additions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 10:58:21 -05:00
phernandez e41579f971 Merge branch 'main' of github.com:basicmachines-co/basic-memory 2025-10-13 10:44:54 -05:00
Paul Hernandez 2b7008d997 fix: Update view_note and ChatGPT tools for Claude Desktop compatibility (#355)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-13 10:44:35 -05:00
phernandez 56e5cc072b Merge branch 'main' of github.com:basicmachines-co/basic-memory 2025-10-13 07:30:08 -05:00
Paul Hernandez c0538ad2dd perf: Optimize sync/indexing for 43% faster performance (#352)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-12 14:41:44 -05:00
phernandez 962d88ea43 add specs-17/18
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-12 10:59:45 -05:00
phernandez cd5efd4a44 perf: exclude null fields from directory endpoint responses
Reduces JSON payload size by 50-70% for directory-heavy responses by omitting
null fields from serialization.

Changes:
- Added response_model_exclude_none=True to all directory endpoints:
  - GET /directory/tree
  - GET /directory/structure
  - GET /directory/list

Impact:
- Directory nodes no longer serialize 7 null fields (title, permalink,
  entity_id, entity_type, content_type, updated_at, file_path)
- For 50+ directories: eliminates 350+ null fields from response
- Payload reduction: ~2.3kb → ~1kb for typical directory trees
- File nodes still include all metadata when present

Example directory node output:
{
  "name": "Tools",
  "directory_path": "/Tools",
  "type": "directory",
  "children": []
}

Testing:
- All 29 directory tests passing
- Type checking passing (0 errors)
- Backward compatible (clients just see missing keys vs null)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-11 09:56:26 -05:00
Paul Hernandez 00b73b0d08 feat: Optimize directory operations for 10-100x performance improvement (#350)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-11 09:11:46 -05:00
jope-bm a09066e0f0 fix: Add permalink normalization to project lookups in deps.py (#348)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2025-10-10 21:21:35 -05:00
Drew Cain be352ab474 fix: Project deletion failing with permalink normalization (#345) 2025-10-10 12:18:03 -05:00
Paul Hernandez 8d2e70cfc8 refactor: async client context manager pattern for cloud consolidation (#344)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 19:09:47 -05:00
Paul Hernandez 53438d1eab feat: Add SPEC-15 for configuration persistence via Tigris (#343)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-08 18:07:00 -05:00
phernandez 032de7e3f2 fix: formatting in test file
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-08 09:13:05 -05:00
phernandez fd2b188645 Revert "feat: add optional logfire instrumentation for cloud mode distributed tracing"
This reverts commit 1fa93ecbd2.
2025-10-08 09:08:15 -05:00
phernandez 453cba94e4 Revert "fix: instrument httpx client at module level for MCP context"
This reverts commit 48cb4be4cd.
2025-10-08 09:07:26 -05:00
phernandez 48cb4be4cd fix: instrument httpx client at module level for MCP context
The lifespan-based instrumentation only runs when FastAPI app starts.
In MCP context, the app never starts but the httpx client is still used.

Solution: Instrument the client immediately after creation at module level.
This works in both contexts:
- MCP: client is instrumented when module is imported
- API: client is instrumented before lifespan runs (lifespan still safe)

This enables distributed tracing from MCP -> Cloud -> API.

Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-08 08:10:54 -05:00
Paul Hernandez 3e876a7549 fix: correct ProjectItem.home property to return path instead of name (#341)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-08 01:03:12 -05:00
phernandez 1fa93ecbd2 feat: add optional logfire instrumentation for cloud mode distributed tracing 2025-10-08 00:23:00 -05:00
Paul Hernandez 73202d1aab fix: add tool use doc to write note for using empty string for root folder (#339)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-07 23:48:02 -05:00
Paul Hernandez 795e339333 fix: prevent nested project paths to avoid data conflicts (#338)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-07 09:44:39 -05:00
Paul Hernandez 07e304ce8e fix: normalize paths to lowercase in cloud mode to prevent case collisions (#336)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-05 17:56:58 -05:00
phernandez 2a1c06d9ad fix link in ai_assistant_guide resource
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-05 17:26:14 -05:00
Paul Hernandez c6f93a0294 chore: v0.15.0 assistant guide (#335)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-05 17:20:54 -05:00
Paul Hernandez ccc4386627 feat: introduce BASIC_MEMORY_PROJECT_ROOT for path constraints (#334)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-05 10:42:06 -05:00
Paul Hernandez 7616b2bb08 fix: cloud mode path validation and sanitization (bmc-issue-103) (#332)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-04 22:10:21 -05:00
phernandez 14c1fe4e89 chore: update version to 0.15.0 for v0.15.0 release 2025-10-04 15:02:28 -05:00
phernandez 367dc6962a style: apply ruff formatting to test files
Auto-format test files for permalink collision tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-04 15:00:19 -05:00
phernandez ee18eb2fea docs: add v0.15.0 changelog entry
Comprehensive changelog for v0.15.0 release covering:
- Critical permalink collision data loss fix
- 10+ bug fixes including #330, #329, #328, #312
- 9 new features including cloud sync and subscription validation
- Platform improvements (Python 3.13, Windows, Docker)
- Enhanced testing and documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-04 14:53:06 -05:00
phernandez 2a050edee4 fix: prevent permalink collision via strict link resolution
Fixes critical data loss bug where creating similar entity names
(e.g., "Node C") would overwrite existing entities (e.g., "Node A.md")
due to fuzzy search incorrectly matching similar file paths.

Changes:
- Add strict=True to resolve_link() calls in entity_service.py
- Disables fuzzy search fallback during entity creation/update
- Prevents false positive matches on similar paths like
  "edge-cases/Node A.md" and "edge-cases/Node C.md"

Testing:
- Added comprehensive integration test reproducing the bug scenario
- Added MCP-level permalink collision tests
- All 55 entity service tests pass
- Manual testing confirms fix prevents file overwrite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-04 14:45:37 -05:00
Paul Hernandez f3b1945e4c fix: remove .env file loading from BasicMemoryConfig (#330)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-04 01:13:06 -05:00
Paul Hernandez 16d7eddbf7 ci: Add Python 3.13 to test matrix (#331)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-10-04 01:12:44 -05:00
Paul Hernandez f5a11f3911 fix: normalize underscores in memory:// URLs for build_context (#329)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-10-04 00:16:06 -05:00
Paul Hernandez ee83b0e5a8 fix: simplify entity upsert to use database-level conflict resolution (#328)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 23:25:34 -05:00
Paul Hernandez a7bf42ef49 fix: Add proper datetime JSON schema format annotations for MCP validation (#312)
Signed-off-by: Claude Code <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-10-03 22:47:02 -05:00
Paul Hernandez 903591384d feat: Add disable_permalinks config flag (#313)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 22:11:47 -05:00
Paul Hernandez 33ee1e0831 feat: integrate ignore_utils to skip .gitignored files in sync process (#314)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 21:39:31 -05:00
Paul Hernandez c83d567917 fix: enable WAL mode and add Windows-specific SQLite optimizations (#316)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 21:10:09 -05:00
Paul Hernandez ace6a0f50d feat: CLI Subscription Validation (SPEC-13 Phase 2) (#327)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 17:59:32 -05:00
Jonathan Nguyen fc38877008 Fix: Corrected dead links in README (#321)
Signed-off-by: Jonathan Nguyen <74562467+jonathan-d-nguyen@users.noreply.github.com>
2025-10-03 10:23:16 -05:00
Paul Hernandez 99a35a7fb4 feat: Cloud CLI cloud sync via rclone bisync (#322)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-03 10:18:43 -05:00
Paul Hernandez ea2e93d926 fix: rework lifecycle management to optimize cloud deployment (#320)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 15:11:38 -05:00
Paul Hernandez 324844a670 fix: resolve entity relations in background to prevent cold start blocking (#319)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 09:24:29 -05:00
Paul Hernandez f818702ab7 fix: enforce minimum 1-day timeframe for recent_activity to handle timezone issues (#318)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-27 23:37:00 -05:00
Paul Hernandez 2efd8f44e2 fix: critical cloud deployment fixes for MCP stability (#317)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-27 21:57:39 -05:00
Paul Hernandez 5da97e4820 feat: implement SPEC-11 API performance optimizations (#315)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 14:34:46 -05:00
Paul Hernandez 17a6733c9d fix: remove obsolete update_current_project function and --project flag reference (#310)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-09-26 11:46:33 -05:00
Drew Cain 3e168b98f3 fix: move_note without file extension (#281)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: phernandez <paul@basicmachines.co>
2025-09-26 11:41:45 -05:00
Paul Hernandez 1091e11322 fix: Make sync operations truly non-blocking with thread pool (#309)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 10:10:58 -05:00
Paul Hernandez f40ab31685 feat: chatgpt tools for search and fetch (#305)
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
2025-09-25 11:55:56 -05:00
phernandez bcf7f40979 fix: Correct GitHub workflow conditions for org member @claude mentions
Fixed the conditional logic in claude.yml to properly handle different event types:
- Use github.event.comment.author_association for issue_comment events
- Use github.event.sender.author_association for other events
- Maintain support for all basicmachines-co org members (OWNER/MEMBER/COLLABORATOR)

This ensures @claude mentions in PR comments trigger the workflow correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-09-23 10:00:51 -05:00
Paul Hernandez 8c7e29e325 chore: Update Claude Code GitHub Workflow (#308)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 09:50:38 -05:00
phernandez 84c0b36dee feat: Add comprehensive cloud mount CLI commands and documentation
This commit implements SPEC-7 Phase 4 by adding local file access capabilities
to the Basic Memory Cloud CLI, enabling users to mount their cloud files locally
for real-time editing.

New features:
- Cloud mount setup with automatic rclone installation
- Mount/unmount/status commands with three performance profiles
- Cross-platform rclone installer with package manager fallbacks
- Mount configuration management with tenant-specific credentials
- Comprehensive documentation with examples and troubleshooting

Mount profiles:
- fast: 5s sync for active development
- balanced: 10-15s sync (recommended)
- safe: 15s+ sync with conflict detection

Technical implementation:
- Uses rclone NFS mount (no FUSE dependencies)
- Tigris object storage with scoped credentials
- Bidirectional sync with configurable cache settings
- Process management and cleanup

Fixes Python module conflict by moving cloud.py commands to cloud/core_commands.py
to resolve typer CLI loading issues with cloud.py file vs cloud/ directory.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-09-22 17:47:06 -05:00
Paul Hernandez 2c5c606a39 feat: Implement cloud mount CLI commands for local file access (#306)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-22 14:19:25 -05:00
Paul Hernandez a1d7792bdb feat: Implement SPEC-6 Stateless Architecture for MCP Tools (#298)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
2025-09-21 20:39:19 -05:00
phernandez 7979b4192e remove no content-encoding: none header
Signed-off-by: phernandez <paul@basicmachines.co>
2025-09-16 16:35:32 -05:00
Drew Cain 52d9b3c752 setting content-encoding to none for mcp
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-09-16 16:09:33 -05:00
Paul Hernandez e0d8aeb149 feat: Basic memory cloud upload (#296)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Drew Cain <groksrc@gmail.com>
2025-09-16 15:07:14 -05:00
Brandon Mayes 17b929446a fix: Sanitize folder names and properly join paths (#292) 2025-09-15 23:26:56 -04:00
Paul Hernandez b00e4ff5a1 fix: replace deprecated json_encoders with Pydantic V2 field serializers (#295)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-09-14 22:19:16 -05:00
Drew Cain 0499319ded fix: rename MCP prompt names to avoid slash command parsing issues (#289)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-09 21:42:35 -05:00
jope-bm 3a6baf80fc feat: Merge Cloud auth (#291)
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-09 14:48:17 -06:00
jope-bm ec2fa07350 chore: apply lint and formatting fixes for 0.14.4 release (#290)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-05 10:00:52 -06:00
Joe P 73cade27ab chore: update version to 0.14.4 for v0.14.4 release 2025-09-04 14:03:09 -06:00
Joe P 7e024a8674 fix: resolve linting errors for release preparation
- Replace bare except clauses with Exception in legal_file_inventory.py
- Remove unused variables in test files
- Prepare codebase for v0.14.4 release
2025-09-04 14:01:06 -06:00
Drew Cain 22f7bfa398 fix: Update YAML frontmatter tag formatting for Obsidian compatibility (#280)
Signed-off-by: Drew Cain <groksrc@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-09-04 09:57:45 -05:00
jope-bm cd7cee650f fix: complete project management special character support (#272) (#279)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 10:57:13 -06:00
Paul Hernandez 105bcaa025 feat: implement non-root Docker container to fix file ownership issues (#277)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Drew Cain <groksrc@gmail.com>
2025-08-28 22:15:14 -05:00
Brandon Mayes 74e12eb782 fix: Sanitize filenames and allow optional kebab case (#260)
Signed-off-by: Brandon Mayes <5610870+bdmayes@users.noreply.github.com>
2025-08-27 19:29:01 -04:00
Drew Cain 7a8b08d11e fix: Windows test failures and add Windows CI support (#273)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-25 08:58:24 -05:00
manuelbliemel 9aa40246a8 Addressed issues when running basic-memory on the Windows platform (#252)
Signed-off-by: Manuel Bliemel <manuel.bliemel@gmail.com>
2025-08-24 19:12:40 -07:00
jope-bm 7aff836c57 fix: Add ISO datetime serialization to MCP schema models (#270)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-23 07:23:07 -06:00
jope-bm 285e96baea fix: Fix observation parsing to exclude markdown and wiki links (#269)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 20:16:05 -06:00
jope-bm 2cd2a62f30 fix: Ensure all datetime operations return timezone-aware objects (#268)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 13:43:55 -06:00
jope-bm f3d8d8d617 fix: Use discriminated unions for MCP schema validation in build_context (#266)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
2025-08-22 09:36:32 -06:00
jope-bm 9743fcd13e fix: Respect BASIC_MEMORY_LOG_LEVEL and BASIC_MEMORY_CONSOLE_LOGGING environment variables (#264)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 07:58:19 -06:00
phernandez 65d1984a53 Update CLA.md to include copyright and license info
Signed-off-by: phernandez <paul@basicmachines.co>
2025-08-21 18:21:24 -05:00
jope-bm b814d40ab1 fix: Add project isolation to ContextService.find_related() method (#261) (#262)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-20 20:07:04 -06:00
Paul Hernandez 2438094914 fix: handle vim atomic write DELETE events without ADD (#249)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-20 14:36:43 -05:00
jope-bm 5d74d7407c fix: Enable string-to-integer conversion for build_context depth parameter (#259)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-20 11:22:14 -06:00
jope-bm b6aeb3217c fix: Add missing foreign key constraints for project removal (#254) (#258)
Signed-off-by: Joe P <joe@basicmemory.com>
Signed-off-by: joe@basicmemory.com
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-20 08:49:07 -06:00
jope-bm 08ee7e1201 fix: Critical search index bug - prevent note disappearing on edit (#257)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
2025-08-19 15:41:27 -06:00
phernandez 63ae9ee0e4 docs: Re-implement external documentation improvements
- Fix typo: 'enviroment' -> 'environment' in CLAUDE.md
- Update HTTP links to HTTPS in README.md
- Add comprehensive VS Code integration instructions
- Maintain correct internal link references

All improvements re-implemented by Basic Machines team for clean IP ownership.
2025-08-08 15:31:04 -05:00
phernandez 0e78751d34 revert: Remove external documentation changes for clean IP
Reverting changes by:
- Ikko Eltociear Ashimine (typo fix)
- Jason Noble (HTTPS links)
- Matias Forbord (link fix)
- Marc Baiza (VS Code instructions)

Will be re-implemented by Basic Machines team for clean IP ownership.
2025-08-08 15:26:29 -05:00
phernandez 59eae34dee fix: Update function name in error messages to use correct search_notes
Corrects error message templates to reference the actual search_notes function name for consistency.
2025-08-08 15:25:33 -05:00
phernandez b1e55e169e revert: Remove external function name fix for clean IP
Original contribution by Amadeusz Wieczorek will be re-implemented by Basic Machines team.
2025-08-08 15:24:58 -05:00
phernandez 173bff35c1 feat: Add Chinese character support to permalink generation
Preserves non-ASCII characters like Chinese in permalinks while maintaining
backward compatibility with ASCII-only processing. This re-implements
functionality that was contributed externally, now with Basic Machines authorship.
2025-08-08 15:24:23 -05:00
phernandez 629c8e47c9 revert: Remove external Chinese character fix for clean IP
Original contribution by andyxinweiminicloud will be re-implemented by Basic Machines team for clean IP ownership.
2025-08-08 15:23:28 -05:00
phernandez 9e4b8bca8f Add legal inventory documentation for IP analysis 2025-08-08 15:16:39 -05:00
Drew Cain b0cc559426 chore: update version to 0.14.3 for v0.14.3 release 2025-08-01 22:06:53 -05:00
Drew Cain 7460a938df fix: make case sensitivity test platform-aware
- Add platform detection to handle case-insensitive file systems
- Test now passes on macOS and Windows while maintaining Linux behavior
- Fixes test failure on case-insensitive file systems
2025-08-01 22:02:53 -05:00
Drew Cain 43fa5762a8 ruff checks
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-08-01 21:50:18 -05:00
Paul Hernandez fb1350b294 fix: enhance character conflict detection and error handling for sync operations (#201)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-08-01 21:35:56 -05:00
Paul Hernandez 7585a29c96 fix: replace recursive _traverse_messages with iterative approach to handle deep conversation threads (#235)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-08-01 21:34:11 -05:00
Drew Cain 752c78c379 chore: minor cleanup (#228)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-07-31 21:32:56 -05:00
jope-bm a4a3b1b689 fix: handle missing 'name' key in memory JSON import (#241)
Signed-off-by: Joe P <joe@basicmemory.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
2025-07-28 14:52:15 -06:00
jope-bm 6361574a20 fix: basic memory home env var not respected when project path is changed. (#239)
Signed-off-by: Joe P <joe@basicmemory.com>
2025-07-28 14:50:47 -06:00
jope-bm 24a1d6195d fix: path traversal security vulnerability in mcp tools (#223)
Signed-off-by: Joe P <joe@basicmemory.com>
2025-07-15 09:05:11 -06:00
jope-bm a0cf62375d docs: improve virtual environment setup instructions (#222)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-10 10:18:43 -06:00
Paul Hernandez 473f70c949 chore: Cloud auth (#213)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-07 21:08:25 -05:00
phernandez 2c29dcc2b2 chore: update version to 0.14.2 for v0.14.2 release 2025-07-03 17:30:40 -05:00
phernandez 448210e552 docs: add v0.14.2 changelog entry
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 17:23:54 -05:00
Drew Cain 3621bb7b4d fix: MCP Error with MCP-Hub #204 (#212)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-07-03 16:57:43 -05:00
Drew Cain f80ac0ee72 fix: replace deprecated datetime.utcnow() with timezone-aware alternatives and suppress SQLAlchemy warnings (#211)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-07-03 16:57:30 -05:00
Drew Cain 23ddf1918c chore: update version to 0.14.1 for v0.14.1 release 2025-07-01 22:08:25 -05:00
Drew Cain 2aca19aa05 chore: apply ruff formatting 2025-07-01 22:05:17 -05:00
Drew Cain 827f7cf3e3 fix: constrain fastmcp version to prevent breaking changes (#203)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-01 22:01:59 -05:00
Drew Cain bd4f55158b fix: Problems with MCP #190 (#202)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-07-01 10:50:44 -05:00
Drew Cain 5360005122 feat: Add to cursor button (#200)
Signed-off-by: Drew Cain <groksrc@gmail.com>
2025-07-01 09:17:48 -05:00
Drew Cain 39f811f8b5 Update README.md
Added Homebrew instructions to README.md

Signed-off-by: Drew Cain <groksrc@users.noreply.github.com>
2025-06-26 21:51:14 -05:00
829 changed files with 164402 additions and 22222 deletions
+159
View File
@@ -0,0 +1,159 @@
---
name: adversarial-review
description: Cross-vendor adversarial code review of the current branch. Two different model families (Claude + Codex/GPT) review the diff independently, then try to refute each other's findings; survivors are reported by confidence. Runs from either Claude Code or Codex. Use when the user asks for an adversarial review, a cross-model / second-opinion review, or wants high-confidence findings before merging. Report-only — never auto-applies fixes.
license: MIT
---
# Adversarial code review
Two reviewers from **different model families****Claude** and **Codex/GPT** — review the
same diff independently, then each tries to **refute** the other's findings. A finding's
confidence comes from whether it survives that cross-examination. This kills the two failure
modes of solo LLM review: self-ratification (a model won't critique its own work) and
confident false positives.
## You are the orchestrator — and one of the two reviewers
This skill runs from **either** Claude Code **or** Codex. First, **identify which model
family you are** (Claude or Codex/GPT). Then:
- **You** are reviewer #1. You review **natively**, in this session, using your own tools.
- **The other family** is reviewer #2. You invoke it as a **subprocess CLI** for an
independent pass: a fresh process, no shared context — that independence is the point.
The CLI for "the other model":
| If you are… | Invoke the other via… |
|-------------|------------------------|
| **Claude** | `codex exec` (GPT) |
| **Codex** | `claude -p` (Claude) |
Everything else in the flow is symmetric. Resolve the `prompts/` and `schemas/` paths
below relative to **this skill's own directory** (where this SKILL.md lives).
## Inputs
Two independent, optional inputs:
- `BASE` — the ref to diff against. Default `main`.
- `SCOPE` — a pathspec to narrow the review (e.g. `src/basic_memory`). Default: none (whole diff).
These are separate: a ref and a pathspec are not interchangeable. Build the **canonical diff
command** once in preflight and reuse it everywhere below — never re-spell the diff inline
(the scattered, inconsistent spelling is what broke earlier). Build it as an **argv array**,
not a string, so a `$SCOPE` containing spaces or glob characters survives intact:
```bash
BASE="${BASE:-main}"
DIFF=(git diff "$BASE...HEAD") # argv array — never a scalar string
[ -n "$SCOPE" ] && DIFF+=(-- "$SCOPE") # pathspec stays one argument even with spaces
DIFF_STR=$(printf '%q ' "${DIFF[@]}") # shell-quoted rendering, for embedding in a prompt
```
To **run** it, use `"${DIFF[@]}"` (quoted, no word-splitting). To **embed** it as text inside
a subprocess prompt, use `$DIFF_STR`.
## Preflight
0. Set `SKILL_DIR` to the directory this SKILL.md lives in. Canonical location is
`.agents/skills/adversarial-review` (the shared agent-skills store); Claude Code reaches it
via the `.claude/skills/adversarial-review` symlink, Codex via its own skills path. The
`prompts/` and `schemas/` subdirs are siblings of this file in every case.
1. Confirm the *other* model's CLI is on PATH (`codex` if you're Claude, `claude` if you're
Codex). If it's missing, tell the user the panel falls back to single-model (which loses
the cross-vendor benefit) and ask whether to proceed or stop.
2. Run `"${DIFF[@]}"`. If it prints nothing, report "nothing to review against $BASE"
(mention `$SCOPE` if set) and stop.
3. `RUN=$(mktemp -d)` — scratch dir for the other model's output. Transient, never committed.
No persisted artifacts, no state file.
## Phase 0 — Deterministic gates (before the models)
Models are statistically blind to negation ("never do X"). Enforce mechanical house rules
with tools, not prompts, and treat hits as high-confidence facts (reported separately from
model findings):
- `just lint` and `just typecheck` if the diff touches `src/`.
- Grep the diff for catchable house-rule violations: `getattr(.*,.*,` defaults, bare
`except:` / `except Exception: pass`, function-scope imports.
## Phase 1 — Independent review (you + the other model, concurrently)
Both reviewers get the same brief: `prompts/review.md` + the repo's `CLAUDE.md` house rules,
reviewing the diff from `"${DIFF[@]}"`. Both emit findings matching `schemas/findings.schema.json`.
**Your native pass:** review as yourself, following `prompts/review.md`. Hold your findings
as that JSON shape.
**The other model's pass** — run, from the repo root, the row that matches you:
Always redirect `codex` stdin from `/dev/null` — if stdin is a pipe (e.g. the call gets
backgrounded), `codex exec` blocks "Reading additional input from stdin..." and fails.
```bash
# You are Claude → run Codex:
codex exec -s read-only \
--output-schema "$SKILL_DIR/schemas/findings.schema.json" \
-o "$RUN/other_findings.json" \
"$(cat "$SKILL_DIR/prompts/review.md")
Review the diff: $DIFF_STR" </dev/null
# You are Codex → run Claude (read-only via plan mode; parse the JSON block it returns):
claude -p --permission-mode plan --output-format json \
"$(cat "$SKILL_DIR/prompts/review.md")
Review the diff: $DIFF_STR
Return ONLY a JSON object matching this schema:
$(cat "$SKILL_DIR/schemas/findings.schema.json")" </dev/null > "$RUN/other_raw.json"
# claude --output-format json output shape varies by CLI version: it may be a JSON ARRAY
# of event objects, OR a single result object. Normalize before reading: if it's an array,
# take the element with type=='result'; otherwise use the object as-is. Then read its
# .result string, strip the ```json fence if present, and parse that.
# (Verified empirically: the CLI in this environment emits the array form.)
```
> Runtime note for Codex orchestrating: `claude -p` needs network access, which Codex's
> default sandbox blocks. Run it from a Codex session whose project is trusted with network
> allowed (or approve the `claude` call when prompted). Keep Codex's own sandbox on — do not
> bypass it just to reach the network.
Tag each finding with its origin (`claude` / `codex`).
## Phase 2 — Cross-refute
Each model tries to refute the *other's* findings, per `prompts/refute.md`
(verdicts match `schemas/verdicts.schema.json`).
- **You** refute the other model's findings natively.
- **The other model** refutes *your* findings — invoke it again the same way (swap
`prompts/review.md` for `prompts/refute.md`, append your findings JSON **and `$DIFF_STR`**
so it judges against the right base and scope, and for Codex use
`--output-schema "$SKILL_DIR/schemas/verdicts.schema.json"`).
Match verdicts to findings by `id`.
## Phase 3 — Synthesize and report (no auto-fix)
Merge, dedupe (same file + overlapping lines + same root cause = one finding), assign
confidence from provenance:
- **High** — both models raised it independently, OR one raised it and the other upheld it.
- **Medium** — one raised it; the other could not refute it but did not independently find it.
- **Low / contested** — one raised it and the other **refuted** it. Keep it, show both sides,
let the human judge. Never silently drop a contested finding.
- Deterministic-gate hits are reported as facts, separate from the model panel.
Rank by `severity × confidence`. Present a compact table: `severity | confidence | file:line
| claim | found-by / upheld-or-refuted-by`. Expand the high-confidence ones with `why` and
any suggested fix.
End by asking which findings, if any, to fix. **Do not edit code until the user picks.**
Convergence between the models is not correctness — your job is to surface a ranked,
cross-examined list, not to declare the branch clean.
## Deliberately NOT done
- No loop-until-both-agree (models converge by going silent, not by being right).
- No persisted artifacts / state machine — the scratch dir is thrown away.
- No auto-applying fixes.
@@ -0,0 +1,22 @@
# Refute the other reviewer
A different reviewer (a different model family) produced the findings below against the
same diff under review (the exact `git diff` command is provided with the findings). Your
job is to try to **refute** each one.
Default to skepticism: assume a finding is wrong until the code proves it right. A finding
that survives a genuine attempt to disprove it is worth far more than one nobody checked.
For each finding, read the actual code it points at and return a verdict:
- **refuted** — the claim is wrong, the code does not do what the finding says, the case
cannot occur, or it is pure style with no correctness impact. Cite the specific code or
fact that disproves it.
- **upheld** — you tried to refute it and could not; the finding is real.
- **partial** — the underlying issue is real but the finding mis-states the severity or
scope. Explain, and set `corrected_severity` if the severity should change.
Do not be agreeable for its own sake, and do not refute for its own sake. Follow the code.
Return ONLY the structured verdicts object conforming to the provided schema. Every
verdict's `id` must match the `id` of the finding it judges.
@@ -0,0 +1,39 @@
# Adversarial reviewer
You are an independent, skeptical code reviewer. Another agent wrote this code; your
job is to find what is actually wrong with it — not to praise it, not to rubber-stamp it.
You are reviewing a specific diff — the exact `git diff` command to run is provided at the
end of this prompt by the orchestrator. Run it, then read the changed files in full for
context, not just the hunks.
## What to look for, in priority order
1. **Correctness** — logic errors, wrong conditions, off-by-one, unhandled `None`,
broken async/await, races, resource leaks, incorrect error handling.
2. **Security** — injection, path traversal, secret leakage, missing authz, unsafe
deserialization.
3. **House rules** (this repo's `CLAUDE.md`/`AGENTS.md` — these are hard rules):
- No swallowed exceptions / no silent fallback logic. Code must fail fast.
- Imports at the top of the file unless deferral is justified in a comment.
- No speculative `getattr(obj, "attr", default)` to paper over unknown attributes.
- Repository pattern for data access; MCP tools talk to API routers via the httpx
ASGI client, not directly to services.
- 100-char lines; full type annotations; async SQLAlchemy 2.0; Pydantic v2.
- New code needs tests (coverage stays at 100%).
4. **Performance** — N+1 queries, work inside hot loops, sync I/O on the async path.
5. **Maintainability** — only when it materially risks a bug. Do not report pure style.
## Rules of engagement
- Every finding MUST be falsifiable: cite the specific file, line, and the code that
triggers it. "This could be cleaner" is not a finding.
- Do not invent issues to seem thorough. An empty findings list is a valid, good result.
- Watch your own negation blindness: when a rule says "never do X," check the diff for X
explicitly rather than trusting a gestalt impression.
- Prefer few high-confidence findings over many speculative ones.
- Assign severity honestly: `critical` = data loss/security/crash in normal use;
`high` = wrong behavior on a common path; `medium` = wrong on an edge case or a real
house-rule violation; `low` = minor.
Return ONLY the structured findings object conforming to the provided schema.
@@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdversarialReviewFindings",
"description": "Structured output for one reviewer's pass over a diff.",
"type": "object",
"additionalProperties": false,
"required": ["findings"],
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "file", "line", "severity", "category", "claim", "why", "suggested_fix"],
"properties": {
"id": {
"type": "string",
"description": "Short stable slug for this finding, e.g. 'swallowed-exc-sync-service'."
},
"file": {
"type": "string",
"description": "Path relative to repo root."
},
"line": {
"type": "integer",
"description": "Best line number in the new file, or 0 if not line-specific."
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"category": {
"type": "string",
"enum": ["correctness", "security", "house-rule", "performance", "maintainability"]
},
"claim": {
"type": "string",
"description": "One sentence: what is wrong."
},
"why": {
"type": "string",
"description": "Concrete reasoning + the specific code that triggers it. Must be falsifiable, not vibes."
},
"suggested_fix": {
"type": ["string", "null"],
"description": "The smallest change that resolves it, or null if none is obvious."
}
}
}
}
}
}
@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdversarialReviewVerdicts",
"description": "One reviewer's attempt to refute another reviewer's findings.",
"type": "object",
"additionalProperties": false,
"required": ["verdicts"],
"properties": {
"verdicts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "verdict", "reasoning", "corrected_severity"],
"properties": {
"id": {
"type": "string",
"description": "The id of the finding being judged (must match the input finding's id)."
},
"verdict": {
"type": "string",
"enum": ["upheld", "refuted", "partial"],
"description": "upheld = the finding is real; refuted = it is wrong or a non-issue; partial = real but mis-scoped/wrong-severity."
},
"reasoning": {
"type": "string",
"description": "Why. For refuted, cite the specific code or fact that disproves the claim."
},
"corrected_severity": {
"type": ["string", "null"],
"enum": ["critical", "high", "medium", "low", null],
"description": "Set only when verdict is 'partial' and severity should change; otherwise null."
}
}
}
}
}
}
+244
View File
@@ -0,0 +1,244 @@
---
name: instrumentation
description: Add Pydantic Logfire observability to applications. Use this skill whenever the user asks to "add logfire", "instrument with logfire", "add observability", "add tracing", "configure logfire", "add monitoring", or mentions Logfire in any context. Supports Python, JavaScript/TypeScript, and Rust. Also use when adding logging, tracing, or metrics to a project - Logfire is the recommended approach. Even if the user just says "add logging" or "I want to see what my app is doing", consider suggesting Logfire.
---
# Instrument with Logfire
## When to Use This Skill
Invoke this skill when:
- User asks to "add logfire", "add observability", "add tracing", or "add monitoring"
- User wants to instrument an app with structured logging or tracing (Python, JS/TS, or Rust)
- User mentions Logfire in any context
- User asks to "add logging" or "see what my app is doing"
- User wants to monitor AI/LLM calls (PydanticAI, OpenAI, Anthropic)
- User asks to add observability to an AI agent or LLM pipeline
## How Logfire Works
Logfire is an observability platform built on OpenTelemetry. It captures traces, logs, and metrics from applications. Logfire has native SDKs for Python, JavaScript/TypeScript, and Rust, plus support for any language via OpenTelemetry.
The reason this skill exists is that Claude tends to get a few things subtly wrong with Logfire - especially the ordering of `configure()` vs `instrument_*()` calls, the structured logging syntax, and which extras to install. These matter because a misconfigured setup silently drops traces.
## Step 1: Detect Language and Frameworks
Identify the project language and instrumentable libraries:
- **Python**: Read `pyproject.toml` or `requirements.txt`. Common instrumentable libraries: FastAPI, httpx, asyncpg, SQLAlchemy, psycopg, Redis, Celery, Django, Flask, requests, PydanticAI.
- **JavaScript/TypeScript**: Read `package.json`. Common frameworks: Express, Next.js, Fastify. Also check for Cloudflare Workers or Deno.
- **Rust**: Read `Cargo.toml`.
Then follow the language-specific steps below.
---
## Python
### Install with Extras
Install `logfire` with extras matching the detected frameworks. Each instrumented library needs its corresponding extra - without it, the `instrument_*()` call will fail at runtime with a missing dependency error.
```bash
uv add 'logfire[fastapi,httpx,asyncpg]'
```
The full list of available extras: `fastapi`, `starlette`, `django`, `flask`, `httpx`, `requests`, `asyncpg`, `psycopg`, `psycopg2`, `sqlalchemy`, `redis`, `pymongo`, `mysql`, `sqlite3`, `celery`, `aiohttp`, `aws-lambda`, `system-metrics`, `litellm`, `dspy`, `google-genai`.
### Configure and Instrument
This is where ordering matters. `logfire.configure()` initializes the SDK and must come before everything else. The `instrument_*()` calls register hooks into each library. If you call `instrument_*()` before `configure()`, the hooks register but traces go nowhere.
```python
import logfire
# 1. Configure first - always
logfire.configure()
# 2. Instrument libraries - after configure, before app starts
logfire.instrument_fastapi(app)
logfire.instrument_httpx()
logfire.instrument_asyncpg()
```
Placement rules:
- `logfire.configure()` goes in the application entry point (`main.py`, or the module that creates the app)
- Call it **once per process** - not inside request handlers, not in library code
- `instrument_*()` calls go right after `configure()`
- Web framework instrumentors (`instrument_fastapi`, `instrument_flask`, `instrument_django`) need the app instance as an argument. HTTP client and database instrumentors (`instrument_httpx`, `instrument_asyncpg`) are global and take no arguments.
- In **Gunicorn** deployments, call `logfire.configure()` inside the `post_fork` hook, not at module level - each worker is a separate process
### Structured Logging
Replace `print()` and `logging.*()` calls with Logfire's structured logging. The key pattern: use `{key}` placeholders with keyword arguments, never f-strings.
```python
# Correct - each {key} becomes a searchable attribute in the Logfire UI
logfire.info("Created user {user_id}", user_id=uid)
logfire.error("Payment failed {amount} {currency}", amount=100, currency="USD")
# Wrong - creates a flat string, nothing is searchable
logfire.info(f"Created user {uid}")
```
For grouping related operations and measuring duration, use spans:
```python
with logfire.span("Processing order {order_id}", order_id=order_id):
items = await fetch_items(order_id)
total = calculate_total(items)
logfire.info("Calculated total {total}", total=total)
```
For exceptions, use `logfire.exception()` which automatically captures the traceback:
```python
try:
await process_order(order_id)
except Exception:
logfire.exception("Failed to process order {order_id}", order_id=order_id)
raise
```
### AI/LLM Instrumentation (Python)
Logfire auto-instruments AI libraries to capture LLM calls, token usage, tool invocations, and agent runs.
```bash
uv add 'logfire[pydantic-ai]'
# or: uv add 'logfire[openai]' / uv add 'logfire[anthropic]'
```
Available AI extras: `pydantic-ai`, `openai`, `anthropic`, `litellm`, `dspy`, `google-genai`.
```python
logfire.configure()
logfire.instrument_pydantic_ai() # captures agent runs, tool calls, LLM request/response
# or:
logfire.instrument_openai() # captures chat completions, embeddings, token counts
logfire.instrument_anthropic() # captures messages, token usage
```
For PydanticAI, each agent run becomes a parent span containing child spans for every tool call and LLM request.
---
## JavaScript / TypeScript
### Install
```bash
# Node.js
npm install @pydantic/logfire-node
# Cloudflare Workers
npm install @pydantic/logfire-cf-workers logfire
# Next.js / generic
npm install logfire
```
### Configure
**Node.js (Express, Fastify, etc.)** - create an `instrumentation.ts` loaded before your app:
```typescript
import * as logfire from '@pydantic/logfire-node'
logfire.configure()
```
Launch with: `node --require ./instrumentation.js app.js`
The SDK auto-instruments common libraries when loaded before the app. Set `LOGFIRE_TOKEN` in your environment or pass `token` to `configure()`.
**Cloudflare Workers** - wrap your handler with `instrument()`:
```typescript
import { instrument } from '@pydantic/logfire-cf-workers'
export default instrument(handler, {
service: { name: 'my-worker', version: '1.0.0' }
})
```
**Next.js** - set environment variables for OpenTelemetry export:
```
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=Authorization=<your-write-token>
```
### Structured Logging (JS/TS)
```typescript
// Structured attributes as second argument
logfire.info('Created user', { user_id: uid })
logfire.error('Payment failed', { amount: 100, currency: 'USD' })
// Spans
logfire.span('Processing order', { order_id }, {}, async () => {
logfire.info('Processing step completed')
})
// Error reporting
logfire.reportError('order processing', error)
```
Log levels: `trace`, `debug`, `info`, `notice`, `warn`, `error`, `fatal`.
---
## Rust
### Install
```toml
[dependencies]
logfire = "0.6"
```
### Configure
```rust
let shutdown_handler = logfire::configure()
.install_panic_handler()
.finish()?;
```
Set `LOGFIRE_TOKEN` in your environment or use the Logfire CLI to select a project.
### Structured Logging (Rust)
The Rust SDK is built on `tracing` and `opentelemetry` - existing `tracing` macros work automatically.
```rust
// Spans
logfire::span!("processing order", order_id = order_id).in_scope(|| {
// traced code
});
// Events
logfire::info!("Created user {user_id}", user_id = uid);
```
Always call `shutdown_handler.shutdown()` before program exit to flush data.
---
## Verify
After instrumentation, verify the setup works:
1. Run `logfire auth` to check authentication (or set `LOGFIRE_TOKEN`)
2. Start the app and trigger a request
3. Check https://logfire.pydantic.dev/ for traces
If traces aren't appearing: check that `configure()` is called before `instrument_*()` (Python), check that `LOGFIRE_TOKEN` is set, and check that the correct packages/extras are installed.
## References
Detailed patterns and integration tables, organized by language:
- **Python**: `${CLAUDE_PLUGIN_ROOT}/skills/instrumentation/references/python/logging-patterns.md` (log levels, spans, stdlib integration, metrics, capfire testing) and `${CLAUDE_PLUGIN_ROOT}/skills/instrumentation/references/python/integrations.md` (full instrumentor table with extras)
- **JavaScript/TypeScript**: `${CLAUDE_PLUGIN_ROOT}/skills/instrumentation/references/javascript/patterns.md` (log levels, spans, error handling, config) and `${CLAUDE_PLUGIN_ROOT}/skills/instrumentation/references/javascript/frameworks.md` (Node.js, Cloudflare Workers, Next.js, Deno setup)
- **Rust**: `${CLAUDE_PLUGIN_ROOT}/skills/instrumentation/references/rust/patterns.md` (macros, spans, tracing/log crate integration, async, shutdown)
@@ -0,0 +1,78 @@
# JavaScript Framework Setup
## Node.js (Express, Fastify, etc.)
Create `instrumentation.ts` and load it before your app:
```typescript
// instrumentation.ts
import * as logfire from '@pydantic/logfire-node'
import 'dotenv/config'
logfire.configure()
```
Launch:
```bash
node --require ./instrumentation.js app.js
# or with ts-node:
npx ts-node --require ./instrumentation.ts app.ts
```
The SDK auto-instruments common libraries (http, fetch, express, etc.) when loaded before the app via `--require`.
## Cloudflare Workers
```typescript
import { instrument } from '@pydantic/logfire-cf-workers'
const handler = {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return new Response('Hello')
},
}
export default instrument(handler, {
service: { name: 'my-worker', version: '1.0.0' },
})
```
Add `LOGFIRE_TOKEN` to `.dev.vars` and enable `nodejs_compat` in `wrangler.toml`:
```toml
compatibility_flags = ["nodejs_compat"]
```
## Next.js / Vercel
Set environment variables in `.env.local` or Vercel dashboard:
```bash
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://logfire-api.pydantic.dev/v1/metrics
OTEL_EXPORTER_OTLP_HEADERS=Authorization=<your-write-token>
```
Optionally use the `logfire` package for manual spans in server components and API routes:
```typescript
import * as logfire from 'logfire'
logfire.info('Server action executed', { action: 'createUser' })
```
## Deno
Deno has built-in OpenTelemetry support. Set environment variables:
```bash
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=Authorization=<your-write-token>
```
Run with telemetry enabled:
```bash
deno run --allow-env --unstable-otel app.ts
```
@@ -0,0 +1,75 @@
# JavaScript / TypeScript Patterns
## Log Levels
From lowest to highest severity:
```typescript
logfire.trace('Detailed trace', { detail: x })
logfire.debug('Debug info', { state: s })
logfire.info('Normal operation', { event: e })
logfire.notice('Notable event', { event: e })
logfire.warn('Warning', { issue: i })
logfire.error('Error occurred', { error: err })
logfire.fatal('Fatal error', { error: err })
```
All methods accept `(message, attributes?, options?)`. Options can include `{ tags: ['tag1'] }`.
## Spans
### Callback-based (auto-closes)
```typescript
await logfire.span('Processing order', { order_id }, {}, async () => {
const items = await fetchItems(order_id)
logfire.info('Fetched items', { count: items.length })
return processItems(items)
})
```
### Manual control
```typescript
const span = logfire.startSpan('Long operation', { job_id })
try {
await doWork()
} finally {
span.end()
}
```
Child spans reference their parent via the `parentSpan` option.
## Error Handling
```typescript
try {
await processOrder(orderId)
} catch (error) {
logfire.reportError('order processing', error)
throw error
}
```
`reportError` automatically extracts stack traces and error details into structured span attributes.
## Configuration
### Environment variables
```bash
LOGFIRE_TOKEN=your-write-token
LOGFIRE_SERVICE_NAME=my-service
LOGFIRE_SERVICE_VERSION=1.0.0
```
### Programmatic
```typescript
logfire.configure({
token: process.env.LOGFIRE_TOKEN,
serviceName: 'my-service',
serviceVersion: '1.0.0',
})
```
@@ -0,0 +1,67 @@
# Python Integration Reference
## Web Frameworks
| Framework | Instrumentor | Needs app instance | Extra |
|-----------|-------------|-------------------|-------|
| FastAPI | `logfire.instrument_fastapi(app)` | Yes | `fastapi` |
| Django | `logfire.instrument_django(app)` | Yes | `django` |
| Flask | `logfire.instrument_flask(app)` | Yes | `flask` |
| Starlette | `logfire.instrument_starlette(app)` | Yes | `starlette` |
| AIOHTTP | `logfire.instrument_aiohttp_client()` | No | `aiohttp` |
## HTTP Clients
| Library | Instrumentor | Extra |
|---------|-------------|-------|
| httpx | `logfire.instrument_httpx()` | `httpx` |
| requests | `logfire.instrument_requests()` | `requests` |
## Databases
| Library | Instrumentor | Extra |
|---------|-------------|-------|
| asyncpg | `logfire.instrument_asyncpg()` | `asyncpg` |
| psycopg | `logfire.instrument_psycopg()` | `psycopg` |
| psycopg2 | `logfire.instrument_psycopg2()` | `psycopg2` |
| SQLAlchemy | `logfire.instrument_sqlalchemy()` | `sqlalchemy` |
| PyMongo | `logfire.instrument_pymongo()` | `pymongo` |
| MySQL | `logfire.instrument_mysql()` | `mysql` |
| SQLite3 | `logfire.instrument_sqlite3()` | `sqlite3` |
| Redis | `logfire.instrument_redis()` | `redis` |
## AI/LLM Frameworks
| Framework | Instrumentor | Extra |
|-----------|-------------|-------|
| PydanticAI | `logfire.instrument_pydantic_ai()` | `pydantic-ai` |
| OpenAI | `logfire.instrument_openai()` | `openai` |
| Anthropic | `logfire.instrument_anthropic()` | `anthropic` |
| LiteLLM | `logfire.instrument_litellm()` | `litellm` |
| DSPy | `logfire.instrument_dspy()` | `dspy` |
| Google GenAI | `logfire.instrument_google_genai()` | `google-genai` |
## Task Queues
| Framework | Instrumentor | Extra |
|-----------|-------------|-------|
| Celery | `logfire.instrument_celery()` | `celery` |
## Other
| Feature | Instrumentor | Extra |
|---------|-------------|-------|
| System Metrics | `logfire.instrument_system_metrics()` | `system-metrics` |
| Pydantic Models | `logfire.instrument_pydantic()` | - (built-in) |
| AWS Lambda | handler wrapper | `aws-lambda` |
## Gunicorn Configuration
```python
# gunicorn.conf.py
import logfire
def post_fork(server, worker):
logfire.configure()
logfire.instrument_fastapi(app)
```
@@ -0,0 +1,101 @@
# Python Logging Patterns
## Log Levels
From lowest to highest severity:
```python
logfire.trace("Detailed trace {detail}", detail=x)
logfire.debug("Debug info {state}", state=s)
logfire.info("Normal operation {event}", event=e)
logfire.notice("Notable event {event}", event=e)
logfire.warn("Warning {issue}", issue=i)
logfire.error("Error occurred {error}", error=err)
logfire.fatal("Fatal error {error}", error=err)
```
## Nested Spans
Spans nest to create a tree visible in the Logfire UI. Use them to show the structure of an operation, not just that it happened:
```python
with logfire.span("HTTP request {method} {url}", method="POST", url=url):
with logfire.span("Serialize payload"):
payload = model.model_dump_json()
with logfire.span("Send request"):
response = await client.post(url, content=payload)
logfire.info("Response {status}", status=response.status_code)
```
## Standard Library Logging Integration
For projects that already use Python's `logging` module, route existing log calls through Logfire rather than rewriting them all:
```python
from logging import basicConfig
import logfire
logfire.configure()
basicConfig(handlers=[logfire.LogfireLoggingHandler()])
```
Or with `dictConfig`:
```python
from logging.config import dictConfig
import logfire
logfire.configure()
dictConfig({
'version': 1,
'handlers': {
'logfire': {'class': 'logfire.LogfireLoggingHandler'},
},
'root': {'handlers': ['logfire']},
})
```
## Suppressing Noisy Libraries
Some libraries emit excessive debug logs. Silence them at the `logging` level:
```python
import logging
logging.getLogger('httpcore').setLevel(logging.WARNING)
logging.getLogger('httpx').setLevel(logging.WARNING)
```
## Custom Metrics
For dashboards and alerting, create metrics:
```python
counter = logfire.metric_counter("orders_processed", unit="1")
counter.add(1, {"status": "success"})
histogram = logfire.metric_histogram("request_duration", unit="s")
histogram.record(0.123, {"endpoint": "/api/users"})
gauge = logfire.metric_gauge("active_connections")
gauge.set(42)
```
## Testing with capfire
Use the `capfire` pytest fixture to assert on emitted spans without sending data to production:
```python
from logfire.testing import CaptureLogfire
def test_order_processing(capfire: CaptureLogfire) -> None:
process_order(order_id=123)
spans = capfire.exporter.exported_spans_as_dict()
assert any(
span['attributes'].get('order_id') == 123
for span in spans
)
```
Configure logfire with `send_to_logfire=False` in test fixtures to prevent production data leakage.
@@ -0,0 +1,106 @@
# Rust Patterns
## Core Macros
The Rust SDK is built on `tracing` and `opentelemetry`. All `tracing` macros work automatically with Logfire.
### Events (log points)
```rust
logfire::trace!("Detailed trace {detail}", detail = x);
logfire::debug!("Debug info {state}", state = s);
logfire::info!("Normal operation {event}", event = e);
logfire::warn!("Warning {issue}", issue = i);
logfire::error!("Error occurred {err}", err = e);
```
### Spans
```rust
// Scoped - span closes when closure completes
logfire::span!("Processing order {order_id}", order_id = id).in_scope(|| {
let items = fetch_items(id);
logfire::info!("Fetched {count} items", count = items.len());
process_items(items)
});
// Guard-based - span closes when guard is dropped
let _guard = logfire::span!("Long operation {job_id}", job_id = id).entered();
do_work();
// span ends when _guard goes out of scope
```
## Configuration
```rust
use logfire;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let shutdown_handler = logfire::configure()
.install_panic_handler() // captures panics as error spans
.finish()?;
// application code...
shutdown_handler.shutdown()?; // flush all pending spans
Ok(())
}
```
Set `LOGFIRE_TOKEN` in your environment or use the Logfire CLI (`logfire auth`).
## Tracing Crate Compatibility
Any library using `tracing` macros automatically sends data through Logfire:
```rust
use tracing;
tracing::info!("This also appears in Logfire");
#[tracing::instrument]
fn my_function(param: &str) {
// automatically creates a span with param as an attribute
}
```
## Log Crate Integration
The `log` crate is automatically captured and forwarded to Logfire. Libraries using `log::info!()`, `log::error!()`, etc. will appear in your Logfire dashboard without any additional configuration.
## Async Spans
```rust
use tracing::Instrument;
async fn process_order(order_id: u64) {
let span = logfire::span!("process order {order_id}", order_id = order_id);
async {
fetch_items(order_id).await;
logfire::info!("Order processed");
}
.instrument(span)
.await;
}
```
## Shutdown
Always call `shutdown()` before program exit to flush pending data:
```rust
// In main()
let shutdown_handler = logfire::configure().finish()?;
// ... app runs ...
// Before exit
shutdown_handler.shutdown()?;
```
For web servers using `tokio`, handle shutdown via signal:
```rust
tokio::signal::ctrl_c().await?;
shutdown_handler.shutdown()?;
```
+23
View File
@@ -0,0 +1,23 @@
{
"name": "basicmachines-co",
"owner": {
"name": "Basic Machines",
"email": "hello@basicmachines.co"
},
"metadata": {
"description": "Official Basic Memory plugins from the canonical basic-memory repository",
"version": "0.3.13"
},
"plugins": [
{
"name": "basic-memory",
"source": "./plugins/claude-code",
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph — session briefings, pre-compaction checkpoints, and capture reflexes",
"version": "0.3.13",
"author": {
"name": "Basic Machines"
},
"keywords": ["memory", "knowledge", "mcp", "specs", "context"]
}
]
}
+132 -19
View File
@@ -15,10 +15,16 @@ Create a stable release using the automated justfile target with comprehensive v
You are an expert release manager for the Basic Memory project. When the user runs `/release`, execute the following steps:
### Step 1: Pre-flight Validation
1. Verify version format matches `v\d+\.\d+\.\d+` pattern
2. Check current git status for uncommitted changes
3. Verify we're on the `main` branch
4. Confirm no existing tag with this version
#### Version Check
1. Check current version in `src/basic_memory/__init__.py`
2. Verify new version format matches `v\d+\.\d+\.\d+` pattern
3. Confirm version is higher than current version
#### Git Status
1. Check current git status for uncommitted changes
2. Verify we're on the `main` branch
3. Confirm no existing tag with this version
#### Documentation Validation
1. **Changelog Check**
@@ -36,22 +42,114 @@ The justfile target handles:
- ✅ Version format validation
- ✅ Git status and branch checks
- ✅ Quality checks (`just check` - lint, format, type-check, tests)
- ✅ Version update in `src/basic_memory/__init__.py`
- ✅ Version update across all consolidated manifests via `just set-version` (Python package + Claude Code plugin/marketplaces + Hermes + OpenClaw)
- ✅ Automatic commit with proper message
- ✅ Tag creation and pushing to GitHub
- ✅ Release workflow trigger
- ✅ Release workflow trigger (automatic on tag push)
The GitHub Actions workflow (`.github/workflows/release.yml`) then:
- ✅ Builds the package using `uv build`
- ✅ Creates GitHub release with auto-generated notes
- ✅ Publishes to PyPI
- ✅ Updates Homebrew formula (stable releases only)
### Step 3: Monitor Release Process
1. Check that GitHub Actions workflow starts successfully
2. Monitor workflow completion at: https://github.com/basicmachines-co/basic-memory/actions
3. Verify PyPI publication
4. Test installation: `uv tool install basic-memory`
1. Verify tag push triggered the workflow (should start automatically within seconds)
2. Monitor workflow progress at: https://github.com/basicmachines-co/basic-memory/actions
3. Watch for successful completion of both jobs:
- `release` - Builds package and publishes to PyPI
- `homebrew` - Updates Homebrew formula (stable releases only)
4. Check for any workflow failures and investigate logs if needed
### Step 4: Post-Release Validation
1. Verify GitHub release is created automatically
2. Check PyPI publication
3. Validate release assets
4. Update any post-release documentation
#### GitHub Release
1. Verify GitHub release is created at: https://github.com/basicmachines-co/basic-memory/releases/tag/<version>
2. Check that release notes are auto-generated from commits
3. Validate release assets (`.whl` and `.tar.gz` files are attached)
#### PyPI Publication
1. Verify package published at: https://pypi.org/project/basic-memory/<version>/
2. Test installation: `uv tool install basic-memory`
3. Verify installed version: `basic-memory --version`
#### Homebrew Formula (Stable Releases Only)
1. Check formula update at: https://github.com/basicmachines-co/homebrew-basic-memory
2. Verify formula version matches release
3. Test Homebrew installation: `brew install basicmachines-co/basic-memory/basic-memory`
#### MCP Registry Publication
After PyPI release is published, update the MCP registry:
1. **Verify PyPI Release**
- Confirm package is live: https://pypi.org/project/basic-memory/<version>/
- The `server.json` version was auto-updated by `just release`
2. **Publish to MCP Registry**
```bash
cd /Users/drew/code/basic-memory
mcp-publisher publish
```
If not authenticated:
```bash
mcp-publisher login github
# Follow device authentication flow
mcp-publisher publish
```
3. **Verify Publication**
```bash
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=basic-memory"
```
**Note:** The `mcp-publisher` CLI can be installed via Homebrew (`brew install mcp-publisher`) or from GitHub releases.
#### Website Updates
**1. basicmachines.co** (`/Users/drew/code/basicmachines.co`)
- **Goal**: Update version number displayed on the homepage
- **Location**: Search for "Basic Memory v0." in the codebase to find version displays
- **What to update**:
- Hero section heading that shows "Basic Memory v{VERSION}"
- "What's New in v{VERSION}" section heading
- Feature highlights array (look for array of features with title/description)
- **Process**:
1. Pull latest from GitHub: `git pull origin main`
2. Create release branch: `git checkout -b release/v{VERSION}`
3. Search codebase for current version number (e.g., "v0.16.1")
4. Update version numbers to new release version
5. Update feature highlights with 3-5 key features from this release (extract from CHANGELOG.md)
6. Commit changes: `git commit -m "chore: update to v{VERSION}"`
7. Push branch: `git push origin release/v{VERSION}`
- **Deploy**: Follow deployment process for basicmachines.co
**2. docs.basicmemory.com** (`/Users/drew/code/docs.basicmemory.com`)
- **Goal**: Add new release notes section to the latest-releases page
- **File**: `src/pages/latest-releases.mdx`
- **What to do**:
1. Pull latest from GitHub: `git pull origin main`
2. Create release branch: `git checkout -b release/v{VERSION}`
3. Read the existing file to understand the format and structure
4. Read `/Users/drew/code/basic-memory/CHANGELOG.md` to get release content
5. Add new release section **at the top** (after MDX imports, before other releases)
6. Follow the existing pattern:
- Heading: `## [v{VERSION}](github-link) — YYYY-MM-DD`
- Focus statement if applicable
- `<Info>` block with highlights (3-5 key items)
- Sections for Features, Bug Fixes, Breaking Changes, etc.
- Link to full changelog at the end
- Separator `---` between releases
7. Commit changes: `git commit -m "docs: add v{VERSION} release notes"`
8. Push branch: `git push origin release/v{VERSION}`
- **Source content**: Extract and format sections from CHANGELOG.md for this version
- **Deploy**: Follow deployment process for docs.basicmemory.com
**4. Announce Release**
- Post to Discord community if significant changes
- Update social media if major release
- Notify users via appropriate channels
## Pre-conditions Check
Before starting, verify:
@@ -74,19 +172,34 @@ Before starting, verify:
🏷️ Tag: v0.13.2
📋 GitHub Release: https://github.com/basicmachines-co/basic-memory/releases/tag/v0.13.2
📦 PyPI: https://pypi.org/project/basic-memory/0.13.2/
🍺 Homebrew: https://github.com/basicmachines-co/homebrew-basic-memory
🔌 MCP Registry: https://registry.modelcontextprotocol.io
🚀 GitHub Actions: Completed
Install with:
uv tool install basic-memory
Install with pip/uv:
uv tool install basic-memory
Install with Homebrew:
brew install basicmachines-co/basic-memory/basic-memory
Users can now upgrade:
uv tool upgrade basic-memory
uv tool upgrade basic-memory
brew upgrade basic-memory
```
## Context
- This creates production releases used by end users
- Must pass all quality gates before proceeding
- Uses the automated justfile target for consistency
- Version is automatically updated in `__init__.py`
- Version is automatically updated across **all** consolidated manifests via
`just set-version <version>` (which calls `scripts/update_versions.py`): the
Python package (`__init__.py`, `server.json`) **and** the plugin/agent artifacts
(Claude Code `plugin.json` + root/local marketplaces, Hermes `plugin.yaml` +
`__init__.py`, OpenClaw `package.json`). To bump only the plugin/agent artifacts
out of band, use `just set-packages-version <version>` (preview with
`just set-packages-version-dry-run <version>`).
- Triggers automated GitHub release with changelog
- Leverages uv-dynamic-versioning for package version management
- Package is published to PyPI for `pip` and `uv` users
- Homebrew formula is automatically updated for stable releases
- MCP Registry is updated manually via `mcp-publisher publish`
- Supports multiple installation methods (uv, pip, Homebrew)
+51
View File
@@ -0,0 +1,51 @@
---
allowed-tools: mcp__basic-memory__write_note, mcp__basic-memory__read_note, mcp__basic-memory__search_notes, mcp__basic-memory__edit_note
argument-hint: [create|status|show|review] [spec-name]
description: Manage specifications in our development process
---
## Context
Specifications are managed in the Basic Memory "specs" project. All specs live in a centralized location accessible across all repositories via MCP tools.
See SPEC-1 and SPEC-2 in the "specs" project for the full specification-driven development process.
Available commands:
- `create [name]` - Create new specification
- `status` - Show all spec statuses
- `show [spec-name]` - Read a specific spec
- `review [spec-name]` - Review implementation against spec
## Your task
Execute the spec command: `/spec $ARGUMENTS`
### If command is "create":
1. Get next SPEC number by searching existing specs in "specs" project
2. Create new spec using template from SPEC-2
3. Use mcp__basic-memory__write_note with project="specs"
4. Include standard sections: Why, What, How, How to Evaluate
### If command is "status":
1. Use mcp__basic-memory__search_notes with project="specs"
2. Display table with spec number, title, and progress
3. Show completion status from checkboxes in content
### If command is "show":
1. Use mcp__basic-memory__read_note with project="specs"
2. Display the full spec content
### If command is "review":
1. Read the specified spec and its "How to Evaluate" section
2. Review current implementation against success criteria with careful evaluation of:
- **Functional completeness** - All specified features working
- **Test coverage analysis** - Actual test files and coverage percentage
- Count existing test files vs required components/APIs/composables
- Verify unit tests, integration tests, and end-to-end tests
- Check for missing test categories (component, API, workflow)
- **Code quality metrics** - TypeScript compilation, linting, performance
- **Architecture compliance** - Component isolation, state management patterns
- **Documentation completeness** - Implementation matches specification
3. Provide honest, accurate assessment - do not overstate completeness
4. Document findings and update spec with review results using mcp__basic-memory__edit_note
5. If gaps found, clearly identify what still needs to be implemented/tested
+74 -47
View File
@@ -11,7 +11,7 @@ All test results are recorded as notes in a dedicated test project.
**Parameters:**
- `phase` (optional): Specific test phase to run (`recent`, `core`, `features`, `edge`, `workflows`, `stress`, or `all`)
- `recent` - Focus on recent changes and new features (recommended for regular testing)
- `core` - Essential tools only (Tier 1: write_note, read_note, search_notes, edit_note, list_projects, switch_project)
- `core` - Essential tools only (Tier 1: write_note, read_note, search_notes, edit_note, list_memory_projects, recent_activity)
- `features` - Core + important workflows (Tier 1 + Tier 2)
- `all` - Comprehensive testing of all tools and scenarios
@@ -24,30 +24,66 @@ When the user runs `/project:test-live`, execute comprehensive test plan:
### **Tier 1: Critical Core (Always Test)**
1. **write_note** - Foundation of all knowledge creation
2. **read_note** - Primary knowledge retrieval mechanism
2. **read_note** - Primary knowledge retrieval mechanism
3. **search_notes** - Essential for finding information
4. **edit_note** - Core content modification capability
5. **list_memory_projects** - Project discovery and status
6. **switch_project** - Context switching for multi-project workflows
5. **list_memory_projects** - Project discovery and session guidance
6. **recent_activity** - Project discovery mode and activity analysis
### **Tier 2: Important Workflows (Usually Test)**
7. **recent_activity** - Understanding what's changed
8. **build_context** - Conversation continuity via memory:// URLs
9. **create_memory_project** - Essential for project setup
10. **move_note** - Knowledge organization
11. **sync_status** - Understanding system state
7. **build_context** - Conversation continuity via memory:// URLs
8. **create_memory_project** - Essential for project setup
9. **move_note** - Knowledge organization
10. **sync_status** - Understanding system state
11. **delete_project** - Project lifecycle management
### **Tier 3: Enhanced Functionality (Sometimes Test)**
12. **view_note** - Claude Desktop artifact display
13. **read_content** - Raw content access
14. **delete_note** - Content removal
15. **list_directory** - File system exploration
16. **set_default_project** - Configuration
17. **delete_project** - Administrative cleanup
16. **edit_note** (advanced modes) - Complex find/replace operations
### **Tier 4: Specialized (Rarely Test)**
18. **canvas** - Obsidian visualization (specialized use case)
19. **MCP Prompts** - Enhanced UX tools (ai_assistant_guide, continue_conversation)
17. **canvas** - Obsidian visualization (specialized use case)
18. **MCP Prompts** - Enhanced UX tools (ai_assistant_guide, continue_conversation)
## Stateless Architecture Testing
### **Project Discovery Workflow (CRITICAL)**
Test the new stateless project selection flow:
1. **Initial Discovery**
- Call `list_memory_projects()` without knowing which project to use
- Verify clear session guidance appears: "Next: Ask which project to use"
- Confirm removal of CLI-specific references
2. **Activity-Based Discovery**
- Call `recent_activity()` without project parameter (discovery mode)
- Verify intelligent project suggestions based on activity
- Test guidance: "Should I use [most-active-project] for this task?"
3. **Session Tracking Validation**
- Verify all tool responses include `[Session: Using project 'name']`
- Confirm guidance reminds about session-wide project tracking
4. **Single Project Constraint Mode**
- Test MCP server with `--project` parameter
- Verify all operations constrained to specified project
- Test project override behavior in constrained mode
### **Explicit Project Parameters (CRITICAL)**
All tools must require explicit project parameters:
1. **Parameter Validation**
- Test all Tier 1 tools require `project` parameter
- Verify clear error messages for missing project
- Test invalid project name handling
2. **No Session State Dependencies**
- Confirm no tool relies on "current project" concept
- Test rapid project switching within conversation
- Verify each call is truly independent
### Pre-Test Setup
@@ -72,7 +108,7 @@ Run the bash `date` command to get the current date/time.
Purpose: Record all test observations and results
```
Make sure to switch to the newly created project with the `switch_project()` tool.
Make sure to use the newly created project for all subsequent test operations by specifying it in the `project` parameter of each tool call.
4. **Baseline Documentation**
Create initial test session note with:
@@ -143,46 +179,42 @@ Test essential MCP tools that form the foundation of Basic Memory:
- ⚠️ Error scenarios (invalid operations)
**5. list_memory_projects Tests (Critical):**
- ✅ Display all projects with status indicators
- ✅ Current and default project identification
- ✅ Display all projects with clear session guidance
- ✅ Project discovery workflow prompts
- ✅ Removal of CLI-specific references
- ✅ Empty project list handling
- ✅ Project metadata accuracy
- ✅ Single project constraint mode display
**6. switch_project Tests (Critical):**
- ✅ Switch between existing projects
- ✅ Context preservation during switch
- ⚠️ Invalid project name handling
- ✅ Confirmation of successful switch
**6. recent_activity Tests (Critical - Discovery Mode):**
- ✅ Discovery mode without project parameter
- ✅ Intelligent project suggestions based on activity
- ✅ Guidance prompts for project selection
- ✅ Session tracking reminders in responses
- ⚠️ Performance with multiple projects
### Phase 2: Important Workflows (Tier 2 Tools)
**7. recent_activity Tests (Important):**
- ✅ Various timeframes ("today", "1 week", "1d")
- ✅ Type filtering capabilities
- ✅ Empty project scenarios
- ⚠️ Performance with many recent changes
**8. build_context Tests (Important):**
**7. build_context Tests (Important):**
- ✅ Different depth levels (1, 2, 3+)
- ✅ Various timeframes for context
- ✅ memory:// URL navigation
- ⚠️ Performance with complex relation graphs
**9. create_memory_project Tests (Important):**
**8. create_memory_project Tests (Important):**
- ✅ Create projects dynamically
- ✅ Set default during creation
- ✅ Path validation and creation
- ⚠️ Invalid paths and names
- ✅ Integration with existing projects
**10. move_note Tests (Important):**
**9. move_note Tests (Important):**
- ✅ Move within same project
- ✅ Cross-project moves with detection (#161)
- ✅ Automatic folder creation
- ✅ Database consistency validation
- ⚠️ Special characters in paths
**11. sync_status Tests (Important):**
**10. sync_status Tests (Important):**
- ✅ Background operation monitoring
- ✅ File synchronization status
- ✅ Project sync state reporting
@@ -190,36 +222,31 @@ Test essential MCP tools that form the foundation of Basic Memory:
### Phase 3: Enhanced Functionality (Tier 3 Tools)
**12. view_note Tests (Enhanced):**
**11. view_note Tests (Enhanced):**
- ✅ Claude Desktop artifact display
- ✅ Title extraction from frontmatter
- ✅ Unicode and emoji content rendering
- ⚠️ Error handling for non-existent notes
**13. read_content Tests (Enhanced):**
**12. read_content Tests (Enhanced):**
- ✅ Raw file content access
- ✅ Binary file handling
- ✅ Image file reading
- ⚠️ Large file performance
**14. delete_note Tests (Enhanced):**
**13. delete_note Tests (Enhanced):**
- ✅ Single note deletion
- ✅ Database consistency after deletion
- ⚠️ Non-existent note handling
- ✅ Confirmation of successful deletion
**15. list_directory Tests (Enhanced):**
**14. list_directory Tests (Enhanced):**
- ✅ Directory content listing
- ✅ Depth control and filtering
- ✅ File name globbing
- ⚠️ Empty directory handling
**16. set_default_project Tests (Enhanced):**
- ✅ Change default project
- ✅ Configuration persistence
- ⚠️ Invalid project handling
**17. delete_project Tests (Enhanced):**
**15. delete_project Tests (Enhanced):**
- ✅ Project removal from config
- ✅ Database cleanup
- ⚠️ Default project protection
@@ -269,7 +296,7 @@ Test essential MCP tools that form the foundation of Basic Memory:
1. Technical documentation project
2. Personal recipe collection project
3. Learning/course notes project
4. Switch contexts during conversation
4. Specify different projects for different operations
5. Cross-reference related concepts
**Content Evolution:**
@@ -281,13 +308,13 @@ Test essential MCP tools that form the foundation of Basic Memory:
### Phase 6: Specialized Tools Testing (Tier 4)
**18. canvas Tests (Specialized):**
**16. canvas Tests (Specialized):**
- ✅ JSON Canvas generation
- ✅ Node and edge creation
- ✅ Obsidian compatibility
- ⚠️ Complex graph handling
**19. MCP Prompts Tests (Specialized):**
**17. MCP Prompts Tests (Specialized):**
- ✅ ai_assistant_guide output
- ✅ continue_conversation functionality
- ✅ Formatted search results
@@ -382,7 +409,7 @@ permalink: test-session-[phase]-[timestamp]
### 📊 Performance Metrics
- Average write_note time: 0.3s
- Search with 100+ notes: 0.6s
- Project switch overhead: 0.1s
- Project parameter overhead: <0.1s
- Memory usage: [observed levels]
## Relations
@@ -402,7 +429,7 @@ permalink: test-session-[phase]-[timestamp]
- Learning curve and intuitiveness
**System Behavior:**
- Context preservation across operations
- Stateless operation independence
- memory:// URL navigation reliability
- Multi-step workflow cohesion
- Edge case graceful handling
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1",
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1",
"CLAUDE_CODE_NO_FLICKER": "1",
"CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING": "1"
},
"permissions": {
"allow": [
"Bash(just fast-check)",
"Bash(just check)",
"Bash(just fix)",
"Bash(just typecheck)",
"Bash(just lint)",
"Bash(just test)"
],
"deny": []
},
"enableAllProjectMcpServers": true
}
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/adversarial-review
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/instrumentation
+28
View File
@@ -0,0 +1,28 @@
# Basic Memory Environment Variables Example
# Copy this file to .env and customize as needed
# Note: .env files are gitignored and should never be committed
# ============================================================================
# PostgreSQL Test Database Configuration
# ============================================================================
# These variables allow you to override the default test database credentials
# Default values match docker-compose-postgres.yml for local development
#
# Only needed if you want to use different credentials or a remote test database
# By default, tests use: postgresql://basic_memory_user:dev_password@localhost:5433/basic_memory_test
# Full PostgreSQL test database URL (used by tests and migrations)
# POSTGRES_TEST_URL=postgresql+asyncpg://basic_memory_user:dev_password@localhost:5433/basic_memory_test
# Individual components (used by justfile postgres-reset command)
# POSTGRES_USER=basic_memory_user
# POSTGRES_TEST_DB=basic_memory_test
# ============================================================================
# Production Database Configuration
# ============================================================================
# For production use, set these in your deployment environment
# DO NOT use the test credentials above in production!
# BASIC_MEMORY_DATABASE_BACKEND=postgres # or "sqlite"
# BASIC_MEMORY_DATABASE_URL=postgresql+asyncpg://user:password@host:port/database
+86
View File
@@ -0,0 +1,86 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
claude-review:
# Only run for organization members and collaborators
if: |
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
track_progress: true # Enable visual progress tracking
allowed_bots: '*'
prompt: |
Review this Basic Memory PR against our team checklist:
## Code Quality & Standards
- [ ] Follows Basic Memory's coding conventions in CLAUDE.md
- [ ] Python 3.12+ type annotations and async patterns
- [ ] SQLAlchemy 2.0 best practices
- [ ] FastAPI and Typer conventions followed
- [ ] 100-character line length limit maintained
- [ ] No commented-out code blocks
## Testing & Documentation
- [ ] Unit tests for new functions/methods
- [ ] Integration tests for new MCP tools
- [ ] Test coverage for edge cases
- [ ] **100% test coverage maintained** (use `# pragma: no cover` only for truly hard-to-test code)
- [ ] Documentation updated (README, docstrings)
- [ ] CLAUDE.md updated if conventions change
## Basic Memory Architecture
- [ ] MCP tools follow atomic, composable design
- [ ] Database changes include Alembic migrations
- [ ] Preserves local-first architecture principles
- [ ] Knowledge graph operations maintain consistency
- [ ] Markdown file handling preserves integrity
- [ ] AI-human collaboration patterns followed
## Security & Performance
- [ ] No hardcoded secrets or credentials
- [ ] Input validation for MCP tools
- [ ] Proper error handling and logging
- [ ] Performance considerations addressed
- [ ] No sensitive data in logs or commits
## Compatability
- [ ] File path comparisons must be windows compatible
- [ ] Avoid using emojis and unicode characters in console and log output
Read the CLAUDE.md file for detailed project context. For each checklist item, verify if it's satisfied and comment on any that need attention. Use inline comments for specific code issues and post a summary with checklist results.
# Allow broader tool access for thorough code review
claude_args: '--allowed-tools "Bash(gh pr:*),Bash(gh issue:*),Bash(gh api:*),Bash(git log:*),Bash(git show:*),Read,Grep,Glob"'
+74
View File
@@ -0,0 +1,74 @@
name: Claude Issue Triage
on:
issues:
types: [opened]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Issue Triage
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
track_progress: true # Show triage progress
prompt: |
Analyze this new Basic Memory issue and perform triage:
**Issue Analysis:**
1. **Type Classification:**
- Bug report (code defect)
- Feature request (new functionality)
- Enhancement (improvement to existing feature)
- Documentation (docs improvement)
- Question/Support (user help)
- MCP tool issue (specific to MCP functionality)
2. **Priority Assessment:**
- Critical: Security issues, data loss, complete breakage
- High: Major functionality broken, affects many users
- Medium: Minor bugs, usability issues
- Low: Nice-to-have improvements, cosmetic issues
3. **Component Classification:**
- CLI commands
- MCP tools
- Database/sync
- Cloud functionality
- Documentation
- Testing
4. **Complexity Estimate:**
- Simple: Quick fix, documentation update
- Medium: Requires some investigation/testing
- Complex: Major feature work, architectural changes
**Actions to Take:**
1. Add appropriate labels using: `gh issue edit ${{ github.event.issue.number }} --add-label "label1,label2"`
2. Check for duplicates using: `gh search issues`
3. If duplicate found, comment mentioning the original issue
4. For feature requests, ask clarifying questions if needed
5. For bugs, request reproduction steps if missing
**Available Labels:**
- Type: bug, enhancement, feature, documentation, question, mcp-tool
- Priority: critical, high, medium, low
- Component: cli, mcp, database, cloud, docs, testing
- Complexity: simple, medium, complex
- Status: needs-reproduction, needs-clarification, duplicate
Read the issue carefully and provide helpful triage with appropriate labels.
claude_args: '--allowed-tools "Bash(gh issue:*),Bash(gh search:*),Read"'
+41 -85
View File
@@ -9,106 +9,62 @@ on:
types: [opened, assigned]
pull_request_review:
types: [submitted]
pull_request_target:
types: [opened, synchronize]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.body, '@claude'))
) && (
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR' ||
github.event.sender.author_association == 'OWNER' ||
github.event.sender.author_association == 'MEMBER' ||
github.event.sender.author_association == 'COLLABORATOR' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Check user permissions
id: check_membership
uses: actions/github-script@v7
with:
script: |
let actor;
if (context.eventName === 'issue_comment') {
actor = context.payload.comment.user.login;
} else if (context.eventName === 'pull_request_review_comment') {
actor = context.payload.comment.user.login;
} else if (context.eventName === 'pull_request_review') {
actor = context.payload.review.user.login;
} else if (context.eventName === 'issues') {
actor = context.payload.issue.user.login;
}
console.log(`Checking permissions for user: ${actor}`);
// List of explicitly allowed users (organization members)
const allowedUsers = [
'phernandez',
'groksrc',
'nellins',
'bm-claudeai'
];
if (allowedUsers.includes(actor)) {
console.log(`User ${actor} is in the allowed list`);
core.setOutput('is_member', true);
return;
}
// Fallback: Check if user has repository permissions
try {
const collaboration = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: actor
});
const permission = collaboration.data.permission;
console.log(`User ${actor} has permission level: ${permission}`);
// Allow if user has push access or higher (write, maintain, admin)
const allowed = ['write', 'maintain', 'admin'].includes(permission);
core.setOutput('is_member', allowed);
if (!allowed) {
core.notice(`User ${actor} does not have sufficient repository permissions (has: ${permission})`);
}
} catch (error) {
console.log(`Error checking permissions: ${error.message}`);
// Final fallback: Check if user is a public member of the organization
try {
const membership = await github.rest.orgs.getMembershipForUser({
org: 'basicmachines-co',
username: actor
});
const allowed = membership.data.state === 'active';
core.setOutput('is_member', allowed);
if (!allowed) {
core.notice(`User ${actor} is not a public member of basicmachines-co organization`);
}
} catch (membershipError) {
console.log(`Error checking organization membership: ${membershipError.message}`);
core.setOutput('is_member', false);
core.notice(`User ${actor} does not have access to this repository`);
}
}
- name: Checkout repository
if: steps.check_membership.outputs.is_member == 'true'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
# For pull_request_target, checkout the PR head to review the actual changes
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 1
- name: Run Claude Code
if: steps.check_membership.outputs.is_member == 'true'
id: claude
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: Bash(uv run pytest),Bash(uv run ruff check . --fix),Bash(uv run ruff format .),Bash(uv run pyright),Bash(just test),Bash(just lint),Bash(just format),Bash(just type-check),Bash(just check),Read,Write,Edit,MultiEdit,Glob,Grep,LS, mcp__web_search
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
track_progress: true # Enable visual progress tracking
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
+110
View File
@@ -0,0 +1,110 @@
name: Consolidated Packages
on:
pull_request:
paths:
- ".claude-plugin/**"
- "plugins/**"
- "skills/**"
- "integrations/**"
- "scripts/update_versions.py"
- "scripts/validate_*.py"
- "justfile"
- ".github/workflows/consolidated-packages.yml"
push:
branches:
- main
paths:
- ".claude-plugin/**"
- "plugins/**"
- "skills/**"
- "integrations/**"
- "scripts/update_versions.py"
- "scripts/validate_*.py"
- "justfile"
- ".github/workflows/consolidated-packages.yml"
permissions:
contents: read
jobs:
claude-code:
name: Claude Code marketplace
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v4
- name: Validate manifests, agent, and bundled skills
run: just --justfile plugins/claude-code/justfile --working-directory plugins/claude-code ci-check
- name: Verify shared version dry run
run: just release-dry-run v0.99.0
skills:
name: Shared skills
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v4
- name: Validate SKILL.md source
run: just --justfile skills/justfile --working-directory skills check
hermes:
name: Hermes unit tests
permissions:
contents: read
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/hermes
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Set up Python
run: uv python install 3.12
- name: Validate manifest and run unit tests
run: just check
openclaw:
name: OpenClaw package
permissions:
contents: read
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/openclaw
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.8"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Release readiness
run: just release-check
+6 -3
View File
@@ -5,6 +5,9 @@ on:
branches: [main]
workflow_dispatch: # Allow manual triggering
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
dev-release:
runs-on: ubuntu-latest
@@ -13,12 +16,12 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -50,4 +53,4 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
skip-existing: true # Don't fail if version already exists
skip-existing: true # Don't fail if version already exists
+6 -6
View File
@@ -9,6 +9,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: basicmachines-co/basic-memory
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
docker:
@@ -19,17 +20,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -37,7 +38,7 @@ jobs:
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -48,7 +49,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
@@ -58,4 +59,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+8 -2
View File
@@ -7,11 +7,14 @@ on:
- edited
- synchronize
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -37,5 +40,8 @@ jobs:
ui
deps
installer
plugins
skills
integrations
# Allow breaking changes (needs "!" after type/scope)
requireScopeForBreakingChange: true
requireScopeForBreakingChange: true
+102 -22
View File
@@ -5,6 +5,9 @@ on:
tags:
- 'v*' # Trigger on version tags like v1.0.0, v0.13.0, etc.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
release:
runs-on: ubuntu-latest
@@ -13,12 +16,12 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -39,7 +42,7 @@ jobs:
echo "Build completed successfully"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: |
dist/*.whl
@@ -53,6 +56,46 @@ jobs:
with:
password: ${{ secrets.PYPI_TOKEN }}
openclaw:
name: Publish OpenClaw npm Package
needs: release
# npm publishes only for stable product tags. Pre-release Python tags use
# versions like 0.21.3b1, which are not valid npm pre-release semver.
if: ${{ !contains(github.ref_name, 'dev') && !contains(github.ref_name, 'b') && !contains(github.ref_name, 'rc') }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: integrations/openclaw
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.8"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Release readiness
run: just release-check
- name: Publish to npm
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
homebrew:
name: Update Homebrew Formula
needs: release
@@ -60,26 +103,63 @@ jobs:
# Only run for stable releases (not dev, beta, or rc versions)
if: ${{ !contains(github.ref_name, 'dev') && !contains(github.ref_name, 'b') && !contains(github.ref_name, 'rc') }}
permissions:
contents: write
actions: read
contents: read
steps:
# Inline bump replaces mislav/bump-homebrew-formula-action@v4.x.
# The action does a HEAD request to api.github.com /repos/.../tarball/<ref>
# with the bearer token and expects a 302 redirect. GitHub now returns
# 303 on that endpoint when authenticated, which the action treats as a
# fatal error. Re-implementing the bump as plain git+sed keeps the same
# contract (update url + sha256, commit, push) with no third-party action.
- name: Update Homebrew formula
uses: mislav/bump-homebrew-formula-action@v3
with:
# Formula name in homebrew-basic-memory repo
formula-name: basic-memory
# The tap repository
homebrew-tap: basicmachines-co/homebrew-basic-memory
# Base branch of the tap repository
base-branch: main
# Download URL will be automatically constructed from the tag
download-url: https://github.com/basicmachines-co/basic-memory/archive/refs/tags/${{ github.ref_name }}.tar.gz
# Commit message for the formula update
commit-message: |
{{formulaName}} {{version}}
Created by https://github.com/basicmachines-co/basic-memory/actions/runs/${{ github.run_id }}
env:
# Personal Access Token with repo scope for homebrew-basic-memory repo
COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
REF: ${{ github.ref_name }}
REPO: ${{ github.repository }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
VERSION="${REF#v}"
ARCHIVE_URL="https://github.com/${REPO}/archive/refs/tags/${REF}.tar.gz"
echo "::group::Compute tarball sha256"
SHA256="$(curl --fail --silent --location "$ARCHIVE_URL" | sha256sum | awk '{print $1}')"
test -n "$SHA256"
echo "sha256: $SHA256"
echo "::endgroup::"
echo "::group::Clone tap"
git clone \
--depth 1 \
"https://x-access-token:${HOMEBREW_TOKEN}@github.com/basicmachines-co/homebrew-basic-memory.git" \
tap
cd tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
echo "::endgroup::"
echo "::group::Patch Formula/basic-memory.rb"
# Pipe-delimited sed because the URL contains slashes. The Formula
# only has one `url` and one `sha256` directive, so a first-match
# replacement is unambiguous. POSIX character classes ([[:space:]])
# keep this portable across BSD and GNU sed.
sed -i -E \
-e "s|^([[:space:]]*url[[:space:]]+)\"[^\"]+\"|\1\"${ARCHIVE_URL}\"|" \
-e "s|^([[:space:]]*sha256[[:space:]]+)\"[^\"]+\"|\1\"${SHA256}\"|" \
Formula/basic-memory.rb
git --no-pager diff Formula/basic-memory.rb
echo "::endgroup::"
if git diff --quiet Formula/basic-memory.rb; then
echo "Formula already at ${REF}; nothing to do."
exit 0
fi
echo "::group::Commit & push"
git add Formula/basic-memory.rb
git commit -m "basic-memory ${VERSION}
Created by ${RUN_URL}"
git push origin HEAD:main
echo "::endgroup::"
+260 -23
View File
@@ -1,32 +1,82 @@
name: Tests
concurrency:
group: bm-ci-${{ github.workflow }}-${{ github.repository }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
on:
# Trigger: PR branch pushes already publish commit statuses that show up on the PR.
# Why: running the full matrix on both push and pull_request doubles CI time for the
# exact same branch head commit.
# Outcome: each branch push runs the test suite once, including PR updates.
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# pull_request_target runs on the BASE of the PR, not the merge result.
# It has write permissions and access to secrets.
# It's useful for PRs from forks or automated PRs but requires careful use for security reasons.
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
pull_request_target:
branches: [ "main" ]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
test:
static-checks:
name: Static Checks (Python 3.12)
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: "pip"
- name: Install uv
run: |
pip install uv
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
- name: Run type checks
run: |
just typecheck
- name: Run linting
run: |
just lint
test-sqlite-unit:
name: Test SQLite Unit (${{ matrix.os }}, Python ${{ matrix.python-version }})
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
python-version: [ "3.12" ]
include:
- os: ubuntu-latest
python-version: "3.12"
- os: ubuntu-latest
python-version: "3.13"
- os: ubuntu-latest
python-version: "3.14"
- os: windows-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -35,9 +85,7 @@ jobs:
run: |
pip install uv
- name: Install just
run: |
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
@@ -45,13 +93,202 @@ jobs:
- name: Install dependencies
run: |
uv pip install -e .[dev]
- name: Run type checks
run: |
just type-check
uv pip install -e ".[dev]"
- name: Run tests
run: |
uv pip install pytest pytest-cov
just test
just test-unit-sqlite
test-sqlite-integration:
name: Test SQLite Integration (${{ matrix.os }}, Python ${{ matrix.python-version }})
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python-version: "3.12"
- os: ubuntu-latest
python-version: "3.13"
- os: ubuntu-latest
python-version: "3.14"
- os: windows-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
run: |
pip install uv
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
- name: Run tests
run: |
just test-int-sqlite
test-postgres-unit:
name: Test Postgres Unit (Python ${{ matrix.python-version }})
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.12"
- python-version: "3.13"
- python-version: "3.14"
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: basic_memory_user
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: basic_memory_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
run: |
pip install uv
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
- name: Run tests
run: |
just test-unit-postgres
test-postgres-integration:
name: Test Postgres Integration (Python ${{ matrix.python-version }})
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.12"
- python-version: "3.13"
- python-version: "3.14"
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: basic_memory_user
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: basic_memory_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
run: |
pip install uv
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
- name: Run tests
run: |
just test-int-postgres
test-semantic:
name: Test Semantic (Python 3.12)
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: "pip"
- name: Install uv
run: |
pip install uv
- uses: extractions/setup-just@v4
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
- name: Run tests
run: |
just test-semantic
+12 -1
View File
@@ -1,6 +1,7 @@
*.py[cod]
__pycache__/
.pytest_cache/
.testmondata*
.coverage
htmlcov/
@@ -52,4 +53,14 @@ ENV/
# claude action
claude-output
**/.claude/settings.local.json
**/.claude/settings.local.json
.mcp.json
.mcpregistry_*
/.testmondata
.benchmarks/
# Consolidated package build artifacts
/integrations/openclaw/node_modules/
/integrations/openclaw/dist/
/integrations/openclaw/skills/
/integrations/openclaw/*.tgz
+1 -1
View File
@@ -1 +1 @@
3.12
3.14
+512
View File
@@ -0,0 +1,512 @@
# AGENTS.md - Basic Memory Project Guide
## Project Overview
Basic Memory is a local-first knowledge management system built on the Model Context Protocol (MCP). It enables
bidirectional communication between LLMs (like Claude) and markdown files, creating a personal knowledge graph that can
be traversed using links between documents.
## CODEBASE DEVELOPMENT
### Project information
See the [README.md](README.md) file for a project overview.
### Build and Test Commands
- Install: `just install` or `pip install -e ".[dev]"`
- Run all tests (SQLite + Postgres): `just test`
- Run all tests against SQLite: `just test-sqlite`
- Run all tests against Postgres: `just test-postgres` (uses testcontainers)
- Run unit tests (SQLite): `just test-unit-sqlite`
- Run unit tests (Postgres): `just test-unit-postgres`
- Run integration tests (SQLite): `just test-int-sqlite`
- Run integration tests (Postgres): `just test-int-postgres`
- Run impacted tests: `just testmon` (pytest-testmon; only tests affected by changed code)
- Run MCP smoke test: `just test-smoke`
- Fast local loop: `just fast-check` (default iteration flow)
- Local consistency check: `just doctor`
- Run all consolidated agent package checks: `just package-check`
- Run Claude Code plugin checks: `just package-check-claude-code`
- Run shared skills checks: `just package-check-skills`
- Run Hermes plugin checks: `just package-check-hermes`
- Run OpenClaw plugin checks: `just package-check-openclaw`
- Run host-native agent harness checks: `just agent-harness-check`
- Generate HTML coverage: `just coverage`
- Single test: `pytest tests/path/to/test_file.py::test_function_name`
- Run benchmarks: `pytest test-int/test_sync_performance_benchmark.py -v -m "benchmark and not slow"`
- Lint: `just lint` or `ruff check . --fix`
- Type check: `just typecheck` or `uv run ty check src tests test-int`
- Type check (pyright): `just typecheck-pyright` or `uv run pyright`
- Format: `just format` or `uv run ruff format .`
- Run all code checks: `just check` (runs lint, format, typecheck, test)
- Create db migration: `just migration "Your migration message"`
- Run development MCP Inspector: `just run-inspector`
**Note:** Project requires Python 3.12+ (uses type parameter syntax and `type` aliases introduced in 3.12)
**Postgres Testing:** Uses [testcontainers](https://testcontainers-python.readthedocs.io/) which automatically spins up a Postgres instance in Docker. No manual database setup required - just have Docker running.
**Doctor Note:** `just doctor` runs with a temporary HOME/config so it won't touch your local Basic Memory settings. It leaves temp dirs in `/tmp` (safe to ignore or remove).
**Testmon Note:** When no files have changed, `just testmon` may collect 0 tests. That's expected and means no impacted tests were detected.
### Code/Test/Verify Loop (fast path)
1) **Code:** make changes.
2) **Test:** `just fast-check` (lint/format/typecheck + pytest-testmon impacted tests for changed code).
3) **Verify:** `just doctor` (end-to-end file ↔ DB loop in a temp project).
4) **Package verify:** `just package-check` when changes touch `plugins/`, `skills/`, `integrations/`, package metadata, or release wiring.
5) **Full gate (when needed):** `just test` or `just check` for SQLite + Postgres.
Run `just test-smoke` when you specifically need the MCP smoke flow.
If testmon is “cold,” the first run may be long. Subsequent runs get much faster.
### Consolidated Agent Package Checks
The monorepo ships several host-native packages alongside the Python core. Use the root justfile as the canonical entry point:
- `just package-check` — validates every copied package and generated bundle path.
- `just package-check-claude-code` — validates the root and plugin-local Claude marketplace manifests, the SessionStart/PreCompact hooks, the bundled output style, and the seed schemas, then runs `claude plugin validate . --strict`.
- `just package-check-skills` — validates every top-level `skills/memory-*/SKILL.md` frontmatter block.
- `just package-check-hermes` — validates `integrations/hermes/plugin.yaml`, the Hermes provider entrypoint, bundled skill, and runs the hermetic unit suite.
- `just package-check-openclaw` — runs the OpenClaw package install, copies top-level skills into the generated bundle, typechecks, lints, builds `dist/`, runs Bun tests, and performs `npm pack --dry-run`.
- `just agent-harness-check` — checks the host-specific harnesses without the shared markdown-only skills target.
Package-local justfiles live in `plugins/claude-code/`, `skills/`, `integrations/hermes/`, and `integrations/openclaw/`. Prefer the root targets for PR verification so command names stay stable as package internals evolve.
### PR CI Gate
Before opening or updating a PR, run the checks that mirror the common required CI failures:
- Run `just typecheck` in addition to targeted `ruff` and `pytest` commands when tests were added or changed.
- Sign commits with `git commit -s` so DCO passes. If a PR branch already has unsigned commits, rewrite the branch with signed-off commits before asking for review.
- Use a semantic PR title accepted by `.github/workflows/pr-title.yml`: `type(scope): summary`.
- Use one of the allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `deps`, `installer`, `plugins`, `skills`, `integrations`.
### Test Structure
- `tests/` - Unit tests for individual components (mocked, fast)
- `test-int/` - Integration tests for real-world scenarios (no mocks, realistic)
- Both directories are covered by unified coverage reporting
- Benchmark tests in `test-int/` are marked with `@pytest.mark.benchmark`
- Slow tests are marked with `@pytest.mark.slow`
- Smoke tests are marked with `@pytest.mark.smoke`
### Code Style Guidelines
- Line length: 100 characters max
- Python 3.12+ with full type annotations (uses type parameters and type aliases)
- Format with ruff (consistent styling)
- Import order: standard lib, third-party, local imports
- Naming: snake_case for functions/variables, PascalCase for classes
- Prefer async patterns with SQLAlchemy 2.0
- Use Pydantic v2 for data validation and schemas
- CLI uses Typer for command structure
- API uses FastAPI for endpoints
- Follow the repository pattern for data access
- Tools communicate to api routers via the httpx ASGI client (in process)
### Code Change Guidelines
- **Full file read before edits**: Before editing any file, read it in full first to ensure complete context; partial reads lead to corrupted edits
- **Minimize diffs**: Prefer the smallest change that satisfies the request. Avoid unrelated refactors or style rewrites unless necessary for correctness
- **No speculative getattr**: Never use `getattr(obj, "attr", default)` when unsure about attribute names. Check the class definition or source code first
- **Fail fast**: Write code with fail-fast logic by default. Do not swallow exceptions with errors or warnings
- **No fallback logic**: Do not add fallback logic unless explicitly told to and agreed with the user
- **No guessing**: Do not say "The issue is..." before you actually know what the issue is. Investigate first.
### Literate Programming Style
Code should tell a story. Comments must explain the "why" and narrative flow, not just the "what".
**Section Headers:**
For files with multiple phases of logic, add section headers so the control flow reads like chapters:
```python
# --- Authentication ---
# ... auth logic ...
# --- Data Validation ---
# ... validation logic ...
# --- Business Logic ---
# ... core logic ...
```
**Decision Point Comments:**
For conditionals that materially change behavior (gates, fallbacks, retries, feature flags), add comments with:
- **Trigger**: what condition causes this branch
- **Why**: the rationale (cost, correctness, UX, determinism)
- **Outcome**: what changes downstream
```python
# Trigger: project has no active sync watcher
# Why: avoid duplicate file system watchers consuming resources
# Outcome: starts new watcher, registers in active_watchers dict
if project_id not in active_watchers:
start_watcher(project_id)
```
**Constraint Comments:**
If code exists because of a constraint (async requirements, rate limits, schema compatibility), explain the constraint near the code:
```python
# SQLite requires WAL mode for concurrent read/write access
connection.execute("PRAGMA journal_mode=WAL")
```
**What NOT to Comment:**
Avoid comments that restate obvious code:
```python
# Bad - restates code
counter += 1 # increment counter
# Good - explains why
counter += 1 # track retries for backoff calculation
```
### Codebase Architecture
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed architecture documentation.
**Directory Structure:**
- `/alembic` - Alembic db migrations
- `/api` - FastAPI REST endpoints + `container.py` composition root
- `/cli` - Typer CLI + `container.py` composition root
- `/deps` - Feature-scoped FastAPI dependencies (config, db, projects, repositories, services, importers)
- `/importers` - Import functionality for Claude, ChatGPT, and other sources
- `/markdown` - Markdown parsing and processing
- `/mcp` - MCP server + `container.py` composition root + `clients/` typed API clients
- `/models` - SQLAlchemy ORM models
- `/repository` - Data access layer
- `/schemas` - Pydantic models for validation
- `/services` - Business logic layer
- `/sync` - File synchronization services + `coordinator.py` for lifecycle management
- `/plugins/claude-code` - Claude Code plugin marketplace package, hooks, skills, and agent harness
- `/skills` - Canonical framework-agnostic Basic Memory `SKILL.md` source
- `/integrations/hermes` - Hermes memory-provider plugin
- `/integrations/openclaw` - OpenClaw npm/TypeScript plugin
**Composition Roots:**
Each entrypoint (API, MCP, CLI) has a composition root that:
- Reads `ConfigManager` (the only place that reads global config)
- Resolves runtime mode via `RuntimeMode` enum (TEST > CLOUD > LOCAL)
- Provides dependencies to downstream code explicitly
**Typed API Clients (MCP):**
MCP tools use typed clients in `mcp/clients/` to communicate with the API:
- `KnowledgeClient` - Entity CRUD operations
- `SearchClient` - Search operations
- `MemoryClient` - Context building
- `DirectoryClient` - Directory listing
- `ResourceClient` - Resource reading
- `ProjectClient` - Project management
Flow: MCP Tool → Typed Client → HTTP API → Router → Service → Repository
### Development Notes
- MCP tools are defined in src/basic_memory/mcp/tools/
- MCP prompts are defined in src/basic_memory/mcp/prompts/
- MCP tools should be atomic, composable operations
- Use `textwrap.dedent()` for multi-line string formatting in prompts and tools
- MCP Prompts are used to invoke tools and format content with instructions for an LLM
- Schema changes require Alembic migrations
- SQLite is used for indexing and full text search, files are source of truth
- Testing uses pytest with asyncio support (strict mode)
- Unit tests (`tests/`) use mocks when necessary; integration tests (`test-int/`) use real implementations
- By default, tests run against SQLite (fast, no Docker needed)
- Set `BASIC_MEMORY_TEST_POSTGRES=1` to run against Postgres (uses testcontainers - Docker required)
- Each test runs in a standalone environment with isolated database and tmp_path directory
- CI runs SQLite and Postgres tests in parallel for faster feedback
- Performance benchmarks are in `test-int/test_sync_performance_benchmark.py`
- Use pytest markers: `@pytest.mark.benchmark` for benchmarks, `@pytest.mark.slow` for slow tests
- **Coverage must stay at 100%**: Write tests for new code. Only use `# pragma: no cover` when tests would require excessive mocking (e.g., TYPE_CHECKING blocks, error handlers that need failure injection, runtime-mode-dependent code paths)
### Async Client Pattern (Important!)
**MCP tools use `get_project_client()` for per-project routing:**
```python
from basic_memory.mcp.project_context import get_project_client
@mcp.tool()
async def my_tool(project: str | None = None, context: Context | None = None):
async with get_project_client(project, context) as (client, active_project):
# client is routed based on project's mode (local ASGI or cloud HTTP)
response = await call_get(client, "/path")
return response
```
**CLI commands and non-project-scoped code use `get_client()` directly:**
```python
from basic_memory.mcp.async_client import get_client
async def my_cli_command():
async with get_client() as client:
response = await call_get(client, "/path")
return response
# Per-project routing (when project name is known):
async with get_client(project_name="research") as client:
...
```
**Do NOT use:**
-`from basic_memory.mcp.async_client import client` (deprecated module-level client)
- ❌ Manual auth header management
-`inject_auth_header()` (deleted)
- ❌ Separate `get_client()` + `get_active_project()` in MCP tools (use `get_project_client()` instead)
**Key principles:**
- Auth happens at client creation, not per-request
- Proper resource management via context managers
- Per-project routing: each project can be LOCAL or CLOUD independently
- Cloud projects use API key (`cloud_api_key` in config) as Bearer token
- Routing priority: factory injection > force-local > per-project cloud > global cloud > local ASGI
- Factory pattern enables dependency injection for cloud consolidation
**For cloud app integration:**
```python
from basic_memory.mcp import async_client
# Set custom factory before importing tools
async_client.set_client_factory(your_custom_factory)
```
See SPEC-16 for full context manager refactor details.
### Release Process
Releases are driven by `just release` / `just beta` — never by a bare `git tag`. The recipes bump version metadata, run pre-flight checks, commit, tag, and push. GitHub Actions then publishes to PyPI and updates the Homebrew formula.
**Stable release:**
```
just release v0.21.3
```
The recipe runs `just lint` + `just typecheck`, then updates every release manifest through `scripts/update_versions.py`: `src/basic_memory/__init__.py`, `server.json`, the root Claude marketplace, the Claude Code plugin manifest and local marketplace, the Hermes `plugin.yaml`, and the OpenClaw `package.json`. It commits as `chore: update version to X.Y.Z for vX.Y.Z release`, creates the `vX.Y.Z` tag, and pushes both the commit and the tag to `origin/main`. After the tag lands, the `Release` workflow builds the Python package, publishes to PyPI, creates the GitHub release with auto-generated notes, publishes the OpenClaw npm package, and updates the Homebrew formula. The recipe finishes by printing the post-release tasks the workflow doesn't cover.
**Beta release:** `just beta v0.21.3b1` — same flow with a beta-suffixed tag. PyPI consumers install with `pip install basic-memory --pre`.
**Release dry run:** `just release-dry-run v0.21.4` previews the consolidated version update without writing files.
**Development builds:** every commit to `main` publishes a `0.21.3.dev26+468a22f`-style version to PyPI automatically via `.github/workflows/dev-release.yml`. No human action.
**Do not tag releases by hand.** A bare `git tag vX.Y.Z` skips the in-code version bump. Package metadata is still correct (uv-dynamic-versioning derives it from the git tag) but `basic-memory --version` reports the previous release, which is what happened with v0.21.2 → v0.21.3.
**Post-release tasks** the recipe surfaces but doesn't run:
- `docs.basicmemory.com` — add notes to `src/pages/latest-releases.mdx`
- `basicmachines.co` — bump version in `src/components/sections/hero.tsx`
- MCP Registry — `mcp-publisher publish` from the repo root
See `.claude/commands/release/release.md` (and `beta.md`, `release-check.md`, `changelog.md` alongside it) for the full release + post-release runbook, including the slash commands.
## BASIC MEMORY PRODUCT USAGE
### Knowledge Structure
- Entity: Any concept, document, or idea represented as a markdown file
- Observation: A categorized fact about an entity (`- [category] content`)
- Relation: A directional link between entities (`- relation_type [[Target]]`)
- Frontmatter: YAML metadata at the top of markdown files
- Knowledge representation follows precise markdown format:
- Observations with [category] prefixes
- Relations with WikiLinks [[Entity]]
- Frontmatter with metadata
### Basic Memory Commands
**Local Commands:**
- Check sync status: `basic-memory status`
- Doctor check (file <-> DB loop): `basic-memory doctor`
- Import from Claude: `basic-memory import claude conversations`
- Import from ChatGPT: `basic-memory import chatgpt`
- Import from Memory JSON: `basic-memory import memory-json`
- Tool access: `basic-memory tool` (provides CLI access to MCP tools)
- Continue: `basic-memory tool continue-conversation --topic="search"`
**Project Management:**
- List projects: `basic-memory project list`
- Add project: `basic-memory project add "name" ~/path`
- Project info: `basic-memory project info`
- Set cloud mode: `basic-memory project set-cloud "name"`
- Set local mode: `basic-memory project set-local "name"`
- One-way sync (local -> cloud): `basic-memory project sync`
- Bidirectional sync: `basic-memory project bisync`
- Integrity check: `basic-memory project check`
**Cloud Commands (requires subscription):**
- Authenticate (global): `basic-memory cloud login`
- Logout (global): `basic-memory cloud logout`
- Check cloud status: `basic-memory cloud status`
- Setup cloud sync: `basic-memory cloud setup`
- Save API key: `basic-memory cloud set-key bmc_...`
- Create API key: `basic-memory cloud create-key "name"`
- Manage snapshots: `basic-memory cloud snapshot [create|list|delete|show|browse]`
- Restore from snapshot: `basic-memory cloud restore <path> --snapshot <id>`
### MCP Capabilities
- Basic Memory exposes these MCP tools to LLMs:
**Content Management:**
- `write_note(title, content, directory, tags)` - Create/update markdown notes with semantic observations and relations
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph awareness
- `read_content(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
- `view_note(identifier, page, page_size)` - View notes as formatted artifacts for better readability
- `edit_note(identifier, operation, content)` - Edit notes incrementally (append, prepend, find/replace, replace_section)
- `move_note(identifier, destination_path, is_directory)` - Move notes or directories to new locations, updating database and maintaining links
- `delete_note(identifier, is_directory)` - Delete notes or directories from the knowledge base
**Knowledge Graph Navigation:**
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation continuity
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "1d", "1 week")
- `list_directory(dir_name, depth, file_name_glob)` - Browse directory contents with filtering and depth control
**Search & Discovery:**
- `search_notes(query, page, page_size, search_type, types, entity_types, after_date)` - Full-text search across all content with advanced filtering options
**Project Management:**
- `list_memory_projects()` - List all available projects with their status
- `create_memory_project(project_name, project_path, set_default)` - Create new Basic Memory projects
- `delete_project(project_name)` - Delete a project from configuration
**Visualization:**
- `canvas(nodes, edges, title, directory)` - Generate Obsidian canvas files for knowledge graph visualization
**ChatGPT-Compatible Tools:**
- `search(query)` - Search across knowledge base (OpenAI actions compatible)
- `fetch(id)` - Fetch full content of a search result document
- MCP Prompts for better AI interaction:
- `ai_assistant_guide()` - Guidance on effectively using Basic Memory tools for AI assistants
- `continue_conversation(topic, timeframe)` - Continue previous conversations with relevant historical context
- `search(query, after_date)` - Search with detailed, formatted results for better context understanding
- `recent_activity(timeframe)` - View recently changed items with formatted output
### Cloud Features (v0.15.0+)
Basic Memory now supports cloud synchronization and storage (requires active subscription):
**Authentication:**
- JWT-based authentication with subscription validation
- Secure session management with token refresh
- Support for multiple cloud projects
**Bidirectional Sync:**
- rclone bisync integration for two-way synchronization
- Conflict resolution and integrity verification
- Real-time sync with change detection
- Mount/unmount cloud storage for direct file access
**Cloud Project Management:**
- Create and manage projects in the cloud
- Toggle between local and cloud modes
- Per-project sync configuration
- Subscription-based access control
**Security & Performance:**
- Removed .env file loading for improved security
- .gitignore integration (respects gitignored files)
- WAL mode for SQLite performance
- Background relation resolution (non-blocking startup)
- API performance optimizations (SPEC-11)
**Per-Project Cloud Routing:**
Individual projects can be routed through the cloud while others stay local, using an API key:
```bash
# Save API key and set project to cloud mode
basic-memory cloud set-key bmc_abc123...
basic-memory project set-cloud research # route through cloud
basic-memory project set-local research # revert to local
```
MCP tools use `get_project_client()` which automatically routes based on the project's mode. Cloud projects use the `cloud_api_key` from config as Bearer token.
**CLI Routing Flags (Global Cloud Mode):**
When global cloud mode is enabled, CLI commands route to the cloud API by default. Use `--local` and `--cloud` flags to override:
```bash
# Force local routing (ignore cloud mode)
basic-memory status --local
basic-memory project list --local
# Force cloud routing (when cloud mode is disabled)
basic-memory status --cloud
basic-memory project info my-project --cloud
```
Key behaviors:
- The local MCP server (`basic-memory mcp`) automatically uses local routing
- This allows simultaneous use of local Claude Desktop and cloud-based clients
- Some commands (like `project default`, `project sync-config`, `project move`) require `--local` in cloud mode since they modify local configuration
- Environment variable `BASIC_MEMORY_FORCE_LOCAL=true` forces local routing globally
- Per-project cloud routing via API key works independently of global cloud mode
## AI-Human Collaborative Development
Basic Memory emerged from and enables a new kind of development process that combines human and AI capabilities. Instead
of using AI just for code generation, we've developed a true collaborative workflow:
1. AI (LLM) writes initial implementation based on specifications and context
2. Human reviews, runs tests, and commits code with any necessary adjustments
3. Knowledge persists across conversations using Basic Memory's knowledge graph
4. Development continues seamlessly across different AI sessions with consistent context
5. Results improve through iterative collaboration and shared understanding
This approach has allowed us to tackle more complex challenges and build a more robust system than either humans or AI
could achieve independently.
**Problem-Solving Guidance:**
- If a solution isn't working after reasonable effort, suggest alternative approaches
- Don't persist with a problematic library or pattern when better alternatives exist
- Example: When py-pglite caused cascading test failures, switching to testcontainers-postgres was the right call
## GitHub Integration
Basic Memory has taken AI-Human collaboration to the next level by integrating Claude directly into the development workflow through GitHub:
### GitHub MCP Tools
Using the GitHub Model Context Protocol server, Claude can now:
- **Repository Management**:
- View repository files and structure
- Read file contents
- Create new branches
- Create and update files
- **Issue Management**:
- Create new issues
- Comment on existing issues
- Close and update issues
- Search across issues
- **Pull Request Workflow**:
- Create pull requests
- Review code changes
- Add comments to PRs
This integration enables Claude to participate as a full team member in the development process, not just as a code generation tool. Claude's GitHub account ([bm-claudeai](https://github.com/bm-claudeai)) is a member of the Basic Machines organization with direct contributor access to the codebase.
### Collaborative Development Process
With GitHub integration, the development workflow includes:
1. **Direct code review** - Claude can analyze PRs and provide detailed feedback
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
3. **Branch management** - Claude can create feature branches for implementations
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
5. **Code Commits**: ALWAYS sign off commits with `git commit -s`
6. **Pull Request Titles**: PR titles must follow the semantic format enforced by `.github/workflows/pr-title.yml`: `type(scope): summary`
- Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`
- Allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `deps`, `installer`, `plugins`, `skills`, `integrations`
- Example: `fix(cli): propagate cloud workspace routing`
This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.
+1561 -6
View File
File diff suppressed because it is too large Load Diff
+63 -26
View File
@@ -1,34 +1,71 @@
Developer Certificate of Origin
Version 1.1
https://developercertificate.org/
# Contributor License Agreement
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
## Copyright Assignment and License Grant
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
By signing this Contributor License Agreement ("Agreement"), you accept and agree to the following terms and conditions
for your present and future Contributions submitted
to Basic Machines LLC. Except for the license granted herein to Basic Machines LLC and recipients of software
distributed by Basic Machines LLC, you reserve all right,
title, and interest in and to your Contributions.
Developer's Certificate of Origin 1.1
### 1. Definitions
By making a contribution to this project, I certify that:
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this
Agreement with Basic Machines LLC.
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work,
that is intentionally submitted by You to Basic
Machines LLC for inclusion in, or documentation of, any of the products owned or managed by Basic Machines LLC (the "
Work").
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
### 2. Grant of Copyright License
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
Subject to the terms and conditions of this Agreement, You hereby grant to Basic Machines LLC and to recipients of
software distributed by Basic Machines LLC a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the
Work, and to permit persons to whom the Work is furnished to do so.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
### 3. Assignment of Copyright
You hereby assign to Basic Machines LLC all right, title, and interest worldwide in all Copyright covering your
Contributions. Basic Machines LLC may license the
Contributions under any license terms, including copyleft, permissive, commercial, or proprietary licenses.
### 4. Grant of Patent License
Subject to the terms and conditions of this Agreement, You hereby grant to Basic Machines LLC and to recipients of
software distributed by Basic Machines LLC a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to
make, have made, use, offer to sell, sell, import, and
otherwise transfer the Work.
### 5. Developer Certificate of Origin
By making a Contribution to this project, You certify that:
(a) The Contribution was created in whole or in part by You and You have the right to submit it under this Agreement; or
(b) The Contribution is based upon previous work that, to the best of Your knowledge, is covered under an appropriate
open source license and You have the right under that
license to submit that work with modifications, whether created in whole or in part by You, under this Agreement; or
(c) The Contribution was provided directly to You by some other person who certified (a), (b) or (c) and You have not
modified it.
(d) You understand and agree that this project and the Contribution are public and that a record of the Contribution (
including all personal information You submit with
it, including Your sign-off) is maintained indefinitely and may be redistributed consistent with this project or the
open source license(s) involved.
### 6. Representations
You represent that you are legally entitled to grant the above license and assignment. If your employer(s) has rights to
intellectual property that you create that
includes your Contributions, you represent that you have received permission to make Contributions on behalf of that
employer, or that your employer has waived such rights
for your Contributions to Basic Machines LLC.
---
This Agreement is effective as of the date you first submit a Contribution to Basic Machines LLC.
-257
View File
@@ -1,257 +0,0 @@
# CLAUDE.md - Basic Memory Project Guide
## Project Overview
Basic Memory is a local-first knowledge management system built on the Model Context Protocol (MCP). It enables
bidirectional communication between LLMs (like Claude) and markdown files, creating a personal knowledge graph that can
be traversed using links between documents.
## CODEBASE DEVELOPMENT
### Project information
See the [README.md](README.md) file for a project overview.
### Build and Test Commands
- Install: `just install` or `pip install -e ".[dev]"`
- Run tests: `uv run pytest -p pytest_mock -v` or `just test`
- Single test: `pytest tests/path/to/test_file.py::test_function_name`
- Lint: `just lint` or `ruff check . --fix`
- Type check: `just type-check` or `uv run pyright`
- Format: `just format` or `uv run ruff format .`
- Run all code checks: `just check` (runs lint, format, type-check, test)
- Create db migration: `just migration "Your migration message"`
- Run development MCP Inspector: `just run-inspector`
### Code Style Guidelines
- Line length: 100 characters max
- Python 3.12+ with full type annotations
- Format with ruff (consistent styling)
- Import order: standard lib, third-party, local imports
- Naming: snake_case for functions/variables, PascalCase for classes
- Prefer async patterns with SQLAlchemy 2.0
- Use Pydantic v2 for data validation and schemas
- CLI uses Typer for command structure
- API uses FastAPI for endpoints
- Follow the repository pattern for data access
- Tools communicate to api routers via the httpx ASGI client (in process)
- avoid using "private" functions in modules or classes (prepended with _)
### Codebase Architecture
- `/alembic` - Alembic db migrations
- `/api` - FastAPI implementation of REST endpoints
- `/cli` - Typer command-line interface
- `/markdown` - Markdown parsing and processing
- `/mcp` - Model Context Protocol server implementation
- `/models` - SQLAlchemy ORM models
- `/repository` - Data access layer
- `/schemas` - Pydantic models for validation
- `/services` - Business logic layer
- `/sync` - File synchronization services
### Development Notes
- MCP tools are defined in src/basic_memory/mcp/tools/
- MCP prompts are defined in src/basic_memory/mcp/prompts/
- MCP tools should be atomic, composable operations
- Use `textwrap.dedent()` for multi-line string formatting in prompts and tools
- MCP Prompts are used to invoke tools and format content with instructions for an LLM
- Schema changes require Alembic migrations
- SQLite is used for indexing and full text search, files are source of truth
- Testing uses pytest with asyncio support (strict mode)
- Test database uses in-memory SQLite
- Avoid creating mocks in tests in most circumstances.
- Each test runs in a standalone environment with in memory SQLite and tmp_file directory
- Do not use mocks in tests if possible. Tests run with an in memory sqlite db, so they are not needed. See fixtures in conftest.py
## BASIC MEMORY PRODUCT USAGE
### Knowledge Structure
- Entity: Any concept, document, or idea represented as a markdown file
- Observation: A categorized fact about an entity (`- [category] content`)
- Relation: A directional link between entities (`- relation_type [[Target]]`)
- Frontmatter: YAML metadata at the top of markdown files
- Knowledge representation follows precise markdown format:
- Observations with [category] prefixes
- Relations with WikiLinks [[Entity]]
- Frontmatter with metadata
### Basic Memory Commands
- Sync knowledge: `basic-memory sync` or `basic-memory sync --watch`
- Import from Claude: `basic-memory import claude conversations`
- Import from ChatGPT: `basic-memory import chatgpt`
- Import from Memory JSON: `basic-memory import memory-json`
- Check sync status: `basic-memory status`
- Tool access: `basic-memory tools` (provides CLI access to MCP tools)
- Guide: `basic-memory tools basic-memory-guide`
- Continue: `basic-memory tools continue-conversation --topic="search"`
### MCP Capabilities
- Basic Memory exposes these MCP tools to LLMs:
**Content Management:**
- `write_note(title, content, folder, tags)` - Create/update markdown notes with semantic observations and relations
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph awareness
- `edit_note(identifier, operation, content)` - Edit notes incrementally (append, prepend, find/replace, section replace)
- `move_note(identifier, destination_path)` - Move notes with database consistency and search reindexing
- `view_note(identifier)` - Display notes as formatted artifacts for better readability in Claude Desktop
- `read_content(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
- `delete_note(identifier)` - Delete notes from knowledge base
**Project Management:**
- `list_memory_projects()` - List all available projects with status indicators
- `switch_project(project_name)` - Switch to different project context during conversations
- `get_current_project()` - Show currently active project with statistics
- `create_memory_project(name, path, set_default)` - Create new Basic Memory projects
- `delete_project(name)` - Delete projects from configuration and database
- `set_default_project(name)` - Set default project in config
- `sync_status()` - Check file synchronization status and background operations
**Knowledge Graph Navigation:**
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation continuity
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "1d", "1 week")
- `list_directory(dir_name, depth, file_name_glob)` - List directory contents with filtering and depth control
**Search & Discovery:**
- `search_notes(query, page, page_size)` - Full-text search across all content with filtering options
**Visualization:**
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
- MCP Prompts for better AI interaction:
- `ai_assistant_guide()` - Guidance on effectively using Basic Memory tools for AI assistants
- `continue_conversation(topic, timeframe)` - Continue previous conversations with relevant historical context
- `search_notes(query, after_date)` - Search with detailed, formatted results for better context understanding
- `recent_activity(timeframe)` - View recently changed items with formatted output
- `json_canvas_spec()` - Full JSON Canvas specification for Obsidian visualization
## AI-Human Collaborative Development
Basic Memory emerged from and enables a new kind of development process that combines human and AI capabilities. Instead
of using AI just for code generation, we've developed a true collaborative workflow:
1. AI (LLM) writes initial implementation based on specifications and context
2. Human reviews, runs tests, and commits code with any necessary adjustments
3. Knowledge persists across conversations using Basic Memory's knowledge graph
4. Development continues seamlessly across different AI sessions with consistent context
5. Results improve through iterative collaboration and shared understanding
This approach has allowed us to tackle more complex challenges and build a more robust system than either humans or AI
could achieve independently.
## GitHub Integration
Basic Memory uses Claude directly into the development workflow through GitHub:
### GitHub MCP Tools
Using the GitHub Model Context Protocol server, Claude can:
- **Repository Management**:
- View repository files and structure
- Read file contents
- Create new branches
- Create and update files
- **Issue Management**:
- Create new issues
- Comment on existing issues
- Close and update issues
- Search across issues
- **Pull Request Workflow**:
- Create pull requests
- Review code changes
- Add comments to PRs
This integration enables Claude to participate as a full team member in the development process, not just as a code
generation tool. Claude's GitHub account ([bm-claudeai](https://github.com/bm-claudeai)) is a member of the Basic
Machines organization with direct contributor access to the codebase.
### Collaborative Development Process
With GitHub integration, the development workflow includes:
1. **Direct code review** - Claude can analyze PRs and provide detailed feedback
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
3. **Branch management** - Claude can create feature branches for implementations
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
With this integration, the AI assistant is a full-fledged team member rather than just a tool for generating code
snippets.
### Basic Memory Pro
Basic Memory Pro is a desktop GUI application that wraps the basic-memory CLI/MCP tools:
- Built with Tauri (Rust), React (TypeScript), and a Python FastAPI sidecar
- Provides visual knowledge graph exploration and project management
- Uses the same core codebase but adds a desktop-friendly interface
- Project configuration is shared between CLI and Pro versions
- Multiple project support with visual switching interface
local repo: /Users/phernandez/dev/basicmachines/basic-memory-pro
github: https://github.com/basicmachines-co/basic-memory-pro
## Release and Version Management
Basic Memory uses `uv-dynamic-versioning` for automatic version management based on git tags:
### Version Types
- **Development versions**: Automatically generated from commits (e.g., `0.12.4.dev26+468a22f`)
- **Beta releases**: Created by tagging with beta suffixes (e.g., `v0.13.0b1`, `v0.13.0rc1`)
- **Stable releases**: Created by tagging with version numbers (e.g., `v0.13.0`)
### Release Workflows
#### Development Builds (Automatic)
- Triggered on every push to `main` branch
- Publishes dev versions like `0.12.4.dev26+468a22f` to PyPI
- Allows continuous testing of latest changes
- Users install with: `pip install basic-memory --pre --force-reinstall`
#### Beta/RC Releases (Manual)
- Create beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
- Automatically builds and publishes to PyPI as pre-release
- Users install with: `pip install basic-memory --pre`
- Use for milestone testing before stable release
#### Stable Releases (Automated)
- Use the automated release system: `just release v0.13.0`
- Includes comprehensive quality checks (lint, format, type-check, tests)
- Automatically updates version in `__init__.py`
- Creates git tag and pushes to GitHub
- Triggers GitHub Actions workflow for:
- PyPI publication
- Homebrew formula update (requires HOMEBREW_TOKEN secret)
**Manual method (legacy):**
- Create version tag: `git tag v0.13.0 && git push origin v0.13.0`
#### Homebrew Formula Updates
- Automatically triggered after successful PyPI release for **stable releases only**
- **Stable releases** (e.g., v0.13.7) automatically update the main `basic-memory` formula
- **Pre-releases** (dev/beta/rc) are NOT automatically updated - users must specify version manually
- Updates formula in `basicmachines-co/homebrew-basic-memory` repo
- Requires `HOMEBREW_TOKEN` secret in GitHub repository settings:
- Create a fine-grained Personal Access Token with `Contents: Read and Write` and `Actions: Read` scopes on `basicmachines-co/homebrew-basic-memory`
- Add as repository secret named `HOMEBREW_TOKEN` in `basicmachines-co/basic-memory`
- Formula updates include new version URL and SHA256 checksum
### For Development
- **Automated releases**: Use `just release v0.13.x` for stable releases and `just beta v0.13.0b1` for beta releases
- **Quality gates**: All releases require passing lint, format, type-check, and test suites
- **Version management**: Versions automatically derived from git tags via `uv-dynamic-versioning`
- **Configuration**: `pyproject.toml` uses `dynamic = ["version"]`
- **Release automation**: `__init__.py` updated automatically during release process
- **CI/CD**: GitHub Actions handles building and PyPI publication
## Development Notes
- make sure you sign off on commits
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+86 -42
View File
@@ -27,13 +27,25 @@ project and how to get started as a developer.
> **Note**: Basic Memory uses [just](https://just.systems) as a modern command runner. Install with `brew install just` or `cargo install just`.
3. **Run the Tests**:
3. **Activate the Virtual Environment**
```bash
# Run all tests
source .venv/bin/activate
```
4. **Run the Tests**:
```bash
# Run all tests with unified coverage (unit + integration)
just test
# or
uv run pytest -p pytest_mock -v
# Run unit tests only (fast, no coverage)
just test-unit
# Run integration tests only (fast, no coverage)
just test-int
# Generate HTML coverage report
just coverage
# Run a specific test
pytest tests/path/to/test_file.py::test_function_name
```
@@ -129,7 +141,7 @@ agreement to the DCO.
## Code Style Guidelines
- **Python Version**: Python 3.12+ with full type annotations
- **Python Version**: Python 3.12+ with full type annotations (3.12+ required for type parameter syntax)
- **Line Length**: 100 characters maximum
- **Formatting**: Use ruff for consistent styling
- **Import Order**: Standard lib, third-party, local imports
@@ -139,46 +151,78 @@ agreement to the DCO.
## Testing Guidelines
- **Coverage Target**: We aim for 100% test coverage for all code
### Test Structure
Basic Memory uses two test directories with unified coverage reporting:
- **`tests/`**: Unit tests that test individual components in isolation
- Fast execution with extensive mocking
- Test individual functions, classes, and modules
- Run with: `just test-unit` (no coverage, fast)
- **`test-int/`**: Integration tests that test real-world scenarios
- Test full workflows with real database and file operations
- Include performance benchmarks
- More realistic but slower than unit tests
- Run with: `just test-int` (no coverage, fast)
### Running Tests
```bash
# Run all tests with unified coverage report
just test
# Run only unit tests (fast iteration)
just test-unit
# Run only integration tests
just test-int
# Generate HTML coverage report
just coverage
# Run specific test
pytest tests/path/to/test_file.py::test_function_name
# Run tests excluding benchmarks
pytest -m "not benchmark"
# Run only benchmark tests
pytest -m benchmark test-int/test_sync_performance_benchmark.py
```
### Performance Benchmarks
The `test-int/test_sync_performance_benchmark.py` file contains performance benchmarks that measure sync and indexing speed:
- `test_benchmark_sync_100_files` - Small repository performance
- `test_benchmark_sync_500_files` - Medium repository performance
- `test_benchmark_sync_1000_files` - Large repository performance (marked slow)
- `test_benchmark_resync_no_changes` - Re-sync performance baseline
Run benchmarks with:
```bash
# Run all benchmarks (excluding slow ones)
pytest test-int/test_sync_performance_benchmark.py -v -m "benchmark and not slow"
# Run all benchmarks including slow ones
pytest test-int/test_sync_performance_benchmark.py -v -m benchmark
# Run specific benchmark
pytest test-int/test_sync_performance_benchmark.py::test_benchmark_sync_100_files -v
```
See `test-int/BENCHMARKS.md` for detailed benchmark documentation.
### Testing Best Practices
- **Coverage Target**: We aim for high test coverage for all code
- **Test Framework**: Use pytest for unit and integration tests
- **Mocking**: Use pytest-mock for mocking dependencies only when necessary
- **Mocking**: Avoid mocking in integration tests; use sparingly in unit tests
- **Edge Cases**: Test both normal operation and edge cases
- **Database Testing**: Use in-memory SQLite for testing database operations
- **Fixtures**: Use async pytest fixtures for setup and teardown
## Release Process
Basic Memory uses automatic versioning based on git tags with `uv-dynamic-versioning`. Here's how releases work:
### Version Management
- **Development versions**: Automatically generated from git commits (e.g., `0.12.4.dev26+468a22f`)
- **Beta releases**: Created by tagging with beta suffixes (e.g., `git tag v0.13.0b1`)
- **Stable releases**: Created by tagging with version numbers (e.g., `git tag v0.13.0`)
### Release Workflows
#### Development Builds
- Automatically published to PyPI on every commit to `main`
- Version format: `0.12.4.dev26+468a22f` (base version + dev + commit count + hash)
- Users install with: `pip install basic-memory --pre --force-reinstall`
#### Beta Releases
1. Create and push a beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
2. GitHub Actions automatically builds and publishes to PyPI
3. Users install with: `pip install basic-memory --pre`
#### Stable Releases
1. Create and push a version tag: `git tag v0.13.0 && git push origin v0.13.0`
2. GitHub Actions automatically:
- Builds the package with version `0.13.0`
- Creates GitHub release with auto-generated notes
- Publishes to PyPI
3. Users install with: `pip install basic-memory`
### For Contributors
- No manual version bumping required
- Versions are automatically derived from git tags
- Focus on code changes, not version management
- **Markers**: Use `@pytest.mark.benchmark` for benchmarks, `@pytest.mark.slow` for slow tests
## Creating Issues
+27 -7
View File
@@ -1,26 +1,46 @@
FROM python:3.12-slim-bookworm
# Build arguments for user ID and group ID (defaults to 1000)
ARG UID=1000
ARG GID=1000
# Copy uv from official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Set environment variables
# UV_PYTHON_INSTALL_DIR ensures Python is installed to a persistent location
# that survives in the final image (not in /root/.local which gets lost)
# UV_PYTHON_PREFERENCE=only-managed tells uv to use its managed Python version
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
PYTHONDONTWRITEBYTECODE=1 \
UV_PYTHON_INSTALL_DIR=/python \
UV_PYTHON_PREFERENCE=only-managed
# Create a group and user with the provided UID/GID
# Check if the GID already exists, if not create appgroup
RUN (getent group ${GID} || groupadd --gid ${GID} appgroup) && \
useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash appuser
# Copy the project into the image
ADD . /app
# Sync the project into a new environment, asserting the lockfile is up to date
# Install Python 3.13 explicitly and sync the project
WORKDIR /app
RUN uv sync --locked
RUN uv python install 3.13
RUN uv sync --locked --python 3.13
# Create data directory
RUN mkdir -p /app/data
# Create necessary directories and set ownership
RUN mkdir -p /app/data/basic-memory /app/.basic-memory && \
chown -R appuser:${GID} /app
# Set default data directory and add venv to PATH
ENV BASIC_MEMORY_HOME=/app/data \
ENV BASIC_MEMORY_HOME=/app/data/basic-memory \
BASIC_MEMORY_PROJECT_ROOT=/app/data \
PATH="/app/.venv/bin:$PATH"
# Switch to the non-root user
USER appuser
# Expose port
EXPOSE 8000
@@ -29,4 +49,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD basic-memory --version || exit 1
# Use the basic-memory entrypoint to run the MCP server with default SSE transport
CMD ["basic-memory", "mcp", "--transport", "sse", "--host", "0.0.0.0", "--port", "8000"]
CMD ["basic-memory", "mcp", "--transport", "sse", "--host", "0.0.0.0", "--port", "8000"]
+494
View File
@@ -0,0 +1,494 @@
# Note Format Reference
Every document in Basic Memory is a plain Markdown file. Files are the source of truth — changes to files automatically update the knowledge graph in the database. You maintain complete ownership, files work with git, and knowledge persists independently of any AI conversation.
## Document Structure
A note has three parts: YAML frontmatter, content (observations), and relations.
```markdown
---
title: Coffee Brewing Methods
type: note
tags: [coffee, brewing]
permalink: coffee-brewing-methods
---
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more flavor clarity than French press
- [technique] Water temperature at 205°F extracts optimal compounds #brewing
- [preference] Ethiopian beans work well with lighter roasts (personal experience)
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- contrasts_with [[Tea Brewing Methods]]
```
The `## Observations` and `## Relations` headings are conventional but not required — the parser detects observations and relations by their syntax patterns anywhere in the document.
## Frontmatter
YAML metadata between `---` fences at the top of the file.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `title` | No | filename stem | Used for linking and references. Auto-set from filename if missing. |
| `type` | No | `note` | Entity type. Used for schema resolution and filtering. |
| `tags` | No | `[]` | List or comma-separated string. Used for organization and search. |
| `permalink` | No | generated from title | Stable identifier. Persists even if the file moves. |
| `schema` | No | none | Schema attachment — dict (inline), string (reference), or omitted (implicit). |
Custom fields are allowed. Any key not in the standard set is stored as `entity_metadata` and indexed for search and filtering.
```yaml
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
permalink: paul-graham
status: active
source: wikipedia
---
```
Here `status` and `source` are custom fields stored in `entity_metadata`.
### Frontmatter Value Handling
YAML automatically converts some values to native types. Basic Memory normalizes them:
- Date strings (`2025-10-24`) → kept as ISO format strings
- Numbers (`1.0`) → converted to strings
- Booleans (`true`) → converted to strings (`"True"`)
- Lists and dicts → preserved, items normalized recursively
This prevents errors when downstream code expects string values.
## Observations
An observation is a categorized fact about the entity. Written as a Markdown list item.
**Syntax:**
```
- [category] content text #tag1 #tag2 (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `[category]` | Yes | Classification in square brackets. Any text except `[]()` chars. |
| content | Yes | The fact or statement. |
| `#tags` | No | Inline tags. Space-separated, each starting with `#`. |
| `(context)` | No | Parenthesized text at end of line. Supporting details or source. |
### Examples
```markdown
- [tech] Uses SQLite for storage #database
- [design] Follows local-first architecture #architecture
- [decision] Selected bcrypt for passwords #security (based on OWASP audit)
- [name] Paul Graham
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
```
Array-like fields use repeated categories — multiple `[expertise]` observations above.
### What Is Not an Observation
The parser excludes these list item patterns:
| Pattern | Example | Reason |
|---------|---------|--------|
| Checkboxes | `- [ ] Todo item`, `- [x] Done`, `- [-] Cancelled` | Task list syntax |
| Markdown links | `- [text](url)` | URL link syntax |
| Bare wiki links | `- [[Target]]` | Treated as a relation instead |
A list item with `#tags` but no `[category]` is still parsed — the tags are extracted and the category defaults to `Note`.
## Relations
Relations connect documents to form the knowledge graph. There are two kinds.
### Explicit Relations
Written as list items with a relation type and a `[[wiki link]]` target.
**Syntax:**
```
- relation_type [[Target Entity]] (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `relation_type` | No | Text before `[[`. Defaults to `relates_to` if omitted. |
| `[[Target]]` | Yes | Wiki link to the target entity. Matched by title or permalink. |
| `(context)` | No | Parenthesized text after `]]`. Supporting details. |
### Examples
```markdown
- implements [[Search Design]]
- depends_on [[Database Schema]]
- works_at [[Y Combinator]] (co-founder)
- [[Some Entity]]
```
The last example — a bare `[[wiki link]]` in a list item — gets relation type `relates_to`.
Common relation types:
- `implements`, `depends_on`, `relates_to`, `inspired_by`
- `extends`, `part_of`, `contains`, `pairs_with`
- `works_at`, `authored`, `collaborated_with`
Any text works as a relation type. These are conventions, not a fixed set.
### Inline References
Wiki links appearing in regular prose (not as list items) create implicit `links_to` relations.
```markdown
This builds on [[Core Design]] and uses [[Utility Functions]].
```
This creates two relations: `links_to [[Core Design]]` and `links_to [[Utility Functions]]`.
### Forward References
Relations can link to entities that don't exist yet. Basic Memory resolves them when the target is created.
## Permalinks and memory:// URLs
Every document has a unique **permalink** — a stable identifier derived from its title. You can set one explicitly in frontmatter, or let the system generate it.
```yaml
permalink: auth-approaches-2024
```
Permalinks form the basis of `memory://` URLs:
```
memory://auth-approaches-2024 # By permalink
memory://Authentication Approaches # By title (auto-resolves)
memory://project/auth-approaches # By path
```
Pattern matching is supported:
```
memory://auth* # Starts with "auth"
memory://*/approaches # Ends with "approaches"
memory://project/*/requirements # Nested wildcard
```
## Schemas
Schemas declare the expected structure of a note — which observation categories and relation types a well-formed note should have. They use Picoschema, a compact notation from Google's Dotprompt that fits naturally in YAML frontmatter.
### Picoschema Syntax
```yaml
schema:
name: string, full name # required field with description
email?: string, contact email # ? = optional
role?: string, job title
works_at?: Organization, employer # capitalized type = entity reference
tags?(array): string, categories # array of type
status?(enum): [active, inactive] # enum with allowed values
metadata?(object): # nested object
updated_at?: string
source?: string
```
| Notation | Meaning | Example |
|----------|---------|---------|
| `field: type` | Required field | `name: string` |
| `field?: type` | Optional field | `role?: string` |
| `field(array): type` | Array of values | `expertise(array): string` |
| `field?(enum): [vals]` | Enum with allowed values | `status?(enum): [active, inactive]` |
| `field?(object):` | Nested object with sub-fields | `metadata?(object):` |
| `, description` | Description after comma | `name: string, full name` |
| `EntityName` | Capitalized type = entity reference | `works_at?: Organization` |
**Scalar types:** `string`, `integer`, `number`, `boolean`, `any`
Any type not in that set whose first letter is uppercase is treated as an entity reference (a relation target).
### Schema-to-Note Mapping
Schemas validate against existing observation/relation syntax. Note authors don't learn new syntax.
| Schema Declaration | Maps To | Example in Note |
|--------------------|---------|-----------------|
| `field: string` | Observation `[field] value` | `- [name] Paul Graham` |
| `field?(array): string` | Multiple `[field]` observations | `- [expertise] Lisp` (repeated) |
| `field?: EntityType` | Relation `field [[Target]]` | `- works_at [[Y Combinator]]` |
| `field?(array): EntityType` | Multiple `field` relations | `- authored [[Book]]` (repeated) |
| `tags` | Frontmatter `tags` array | `tags: [startups, essays]` |
| `field?(enum): [vals]` | Observation `[field] value` where value is in the set | `- [status] active` |
Observations and relations not covered by the schema are valid — schemas describe a subset, not a straitjacket.
### Schema Attachment
Three ways to attach a schema to a note, resolved in priority order:
**1. Inline schema**`schema` is a dict in frontmatter:
```yaml
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
```
Good for one-off structured notes or prototyping a schema before extracting it.
**2. Explicit reference**`schema` is a string naming a schema note:
```yaml
---
title: Basic Memory
schema: SoftwareProject
---
```
or by permalink:
```yaml
---
title: LLM Memory Patterns
schema: schema/research-project
---
```
Use when the note's `type` differs from the schema it should validate against, or when multiple schema variants exist.
**3. Implicit by type** — no `schema` field, resolved by matching `type`:
```yaml
---
title: Paul Graham
type: Person
---
```
The system looks up a schema note where `entity: Person`. If found, it applies. If not, no validation occurs.
**4. No schema** — perfectly fine. Most notes don't need one.
### Schema Notes
A schema is itself a Basic Memory note with `type: schema`. It lives anywhere (though `schema/` is the conventional directory).
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
| Field | Required | Description |
|-------|----------|-------------|
| `type` | Yes | Must be `schema` |
| `entity` | Yes | The entity type this schema describes (e.g., `Person`) |
| `version` | No | Schema version number (default: `1`) |
| `schema` | Yes | Picoschema dict defining the fields |
| `settings.validation` | No | Validation mode (default: `warn`) |
Schema notes are regular notes — they show up in search, can have observations and relations, and participate in the knowledge graph.
### Validation Modes
| Mode | Behavior |
|------|----------|
| `warn` | Warnings in output, doesn't block (default) |
| `strict` | Errors that block sync, for CI/CD enforcement |
| `off` | No validation |
### Validation Output
```
$ bm schema validate people/ada-lovelace.md
⚠ Person schema validation:
- Missing required field: name (expected [name] observation)
- Missing optional field: role
- Missing optional field: works_at (no relation found)
Unmatched observations: [fact] ×2, [born] ×1
Unmatched relations: collaborated_with
```
"Unmatched" items are informational — observations and relations the schema doesn't cover.
### Schema Inference
Generate schemas from existing notes by analyzing observation and relation frequency:
```
$ bm schema infer Person
Analyzing 30 notes with type: Person...
Observations found:
[name] 30/30 100% → name: string
[role] 27/30 90% → role?: string
[expertise] 18/30 60% → expertise?(array): string
[email] 8/30 27% → email?: string
Relations found:
works_at 22/30 73% → works_at?: Organization
Suggested schema:
name: string, full name
role?: string, job title
expertise?(array): string, areas of knowledge
email?: string, contact email
works_at?: Organization, employer
Save to schema/Person.md? [y/n]
```
Frequency thresholds:
- **100% present** → required field
- **25%+ present** → optional field
- **Below 25%** → excluded from suggestion
### Schema Drift Detection
Track how usage patterns shift over time:
```
$ bm schema diff Person
Schema drift detected:
+ expertise: now in 81% of notes (was 12%)
- department: dropped to 3% of notes
~ works_at: cardinality changed (one → many)
Update schema? [y/n/review]
```
## Complete Examples
### Simple Note (No Schema)
```markdown
---
title: Project Ideas
type: note
tags: [ideas, brainstorm]
---
# Project Ideas
## Observations
- [idea] Build a CLI tool for markdown linting #tooling
- [idea] Create a recipe knowledge base #cooking
- [priority] Focus on developer tools first (Q1 goal)
## Relations
- inspired_by [[Developer Workflow Research]]
- part_of [[Q1 Planning]]
```
### Schema-Validated Note
Schema at `schema/Person.md`:
```yaml
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
Note at `people/paul-graham.md`:
```markdown
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
---
# Paul Graham
## Observations
- [name] Paul Graham
- [role] Essayist and investor
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
- [fact] Created Viaweb, the first web app
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
```
The `[fact]` observation and `authored` relation are not in the schema — they're valid, just unmatched. The schema only checks that `[name]` exists (required) and looks for optional fields like `[role]`, `[expertise]`, and `works_at`.
### Inline Schema Note
```markdown
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
# Team Standup 2024-01-15
## Observations
- [attendees] Paul
- [attendees] Sarah
- [decisions] Ship v2 by Friday
- [action_items] Paul to review PR #42
- [blockers] Waiting on API credentials
```
+550 -369
View File
@@ -1,3 +1,4 @@
<!-- mcp-name: io.github.basicmachines-co/basic-memory -->
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![PyPI version](https://badge.fury.io/py/basic-memory.svg)](https://badge.fury.io/py/basic-memory)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
@@ -5,276 +6,313 @@
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![](https://badge.mcpx.dev?type=server 'MCP Server')
![](https://badge.mcpx.dev?type=dev 'MCP Dev')
[![smithery badge](https://smithery.ai/badge/@basicmachines-co/basic-memory)](https://smithery.ai/server/@basicmachines-co/basic-memory)
## Skip the install — try Basic Memory in the cloud
Claude, Codex, or Cursor connected in 30 seconds. No Python, no JSON, no
terminal. **$15.00/mo locked in for life** (12.50/mo yearly pricing). 7-day free
trial — cancel any time before day 7 if it's not for you. Beta pricing —
sign up now and your rate never goes up. OSS users: code `BMFOSS` takes
another 20% off for 3 months.
[Start free trial →](https://basicmemory.com?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=banner)
### Basic Memory Teams is now available!
Give your team a single, shared cloud workspace. Knowledge isn't confined to one person — anything a teammate writes is immediately available to everyone else and to their AI assistants.
Edit a note together in real time, hand work off between humans and agents, and build one connected knowledge base instead of scattered copies. Same pricing - start with one user and add more as needed.
---
# Basic Memory
Basic Memory lets you build persistent knowledge through natural conversations with Large Language Models (LLMs) like
Claude, while keeping everything in simple Markdown files on your computer. It uses the Model Context Protocol (MCP) to
enable any compatible LLM to read and write to your local knowledge base.
### Your AI never forgets again.
- Website: https://basicmemory.com
- Company: https://basicmachines.co
- Documentation: https://memory.basicmachines.co
- Discord: https://discord.gg/tyvKNccgqN
- YouTube: https://www.youtube.com/@basicmachines-co
Pick up right where you left off — in Claude, Codex, Cursor, ChatGPT, or
anything that speaks [MCP](https://modelcontextprotocol.io). Your knowledge
lives as Markdown files that both you and your AI can read, write, and
search.
## Pick up your conversation right where you left off
- **Local-first.** Plain text on your disk. Forever.
- **Two-way.** AI and humans write to the same files; sync keeps them in step.
- **A real knowledge graph.** Observations and wikilinks compound into context.
- **Semantic search.** Find notes by meaning, not just keywords.
- **MCP-native.** Works with every major AI client and IDE.
- **Progressive tool discovery.** Every tool is tagged with behavior hints
(read-only, destructive, idempotent) so agents pick the right tool on
demand — no wasted context trying things to see what they do.
- **Cloud, optional.** Sync across devices when you want — never required.
- AI assistants can load context from local files in a new conversation
- Notes are saved locally as Markdown files in real time
- No project knowledge or special prompting required
## Get started
Pick the path that fits you. Both run the same product on the same Markdown.
<table>
<tr>
<th width="50%">☁️ &nbsp; Cloud</th>
<th width="50%">💻 &nbsp; Local install</th>
</tr>
<tr>
<td valign="top">
**30 seconds.** Sign up, connect your AI client, done.
- Works in any browser
- Mobile, web, desktop
- Cross-device sync built in
- We handle hosting, backups, snapshots
**$15.00/mo locked for life** · 7-day free trial · cancel any time
[**Start free trial →**](https://basicmemory.com?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=quickstart)
</td>
<td valign="top">
**2 minutes.** Install, configure your AI client, run.
- Free forever (AGPL-3.0)
- All data on your disk
- Air-gapped friendly
- Requires Python via [`uv`](https://docs.astral.sh/uv/)
```bash
uv tool install basic-memory
```
[**Configure your client ↓**](#connect-your-ai-client)
</td>
</tr>
</table>
## What people are saying
> Basic Memory changed my whole relationship with LLMs. I switched from GPT
> and Gemini to exclusively Claude and Claude Code because of this
> integration and am completely revamping all our company's processes around
> a Basic Memory workflow.
>
> — **Alex**, TrainerDay
> Basic Memory is the missing 'wow' factor in AI chatbots. Now I can't
> imagine Claude or Claude Code without it.
>
> — **Caleb**, Caleb Picker Consulting
> I don't code without Basic Memory anymore. It's such a time saver to be
> able to refer to projects I don't currently have active and keep a running
> log of all my learnings and ProTips.
>
> — **@groksrc**, Developer
More on [basicmemory.com](https://basicmemory.com?utm_source=github&utm_medium=referral&utm_campaign=readme).
## Basic Memory Cloud
The hosted version of Basic Memory. Same product, same Markdown files, same
MCP tools — we just host the database, run the sync, and put it on your
phone.
### What you get
- **Every device, same brain.** Your knowledge graph on web, mobile, and
desktop. No copy-paste between machines.
- **Connect any MCP client.** Claude Desktop, Claude Code, Codex, Cursor,
ChatGPT (Custom GPTs), VS Code — one-click connect from the web app.
- **Bidirectional sync to local.** Edit on your phone, see it in Obsidian on
your laptop. rclone-powered with conflict resolution.
- **Snapshots and backups.** Point-in-time restore. Browse history. Never
lose a note.
- **No lock-in.** Your notes are plain Markdown. Export to local Markdown any
time — same files, same format, same wikilinks. Cancel anytime, your data
stays yours.
Built on WorkOS AuthKit, Neon Postgres, and Tigris S3.
### Pricing
**$15.00/mo, locked in for the life of your subscription** (regular price
$19). Sign up during beta and the rate never goes up — as long as you stay
subscribed, you keep the price. One plan, no tiers, no surprise upgrades.
Unlimited notes, unlimited projects, every feature.
- 7-day free trial. Cancel any time before day 7 if it's not for you.
- Cancel anytime after that too — export your notes whenever you want.
- OSS users: code `BMFOSS` for another 20% off for 3 months (~$11.40/mo).
[**Start your 7-day free trial →**](https://basicmemory.com?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=cloud-section)
## Cloud vs. local
| | Cloud | Local |
|---|---|---|
| **Setup time** | 30 seconds | 2 minutes (requires Python) |
| **Cost** | $15.00/mo, locked for life (7-day trial) | Free |
| **Storage** | We host (Tigris S3) | Your disk |
| **Cross-device sync** | Built in | Manual (Git, Syncthing, etc.) |
| **Mobile access** | Yes (web + app) | No |
| **Air-gapped** | No | Yes |
| **Your data stays yours** | Yes — export anytime | Yes — already there |
| **Source code** | AGPL-3.0 | AGPL-3.0 |
| **Snapshots & backups** | Built in | Roll your own |
Both paths use the same OSS engine and the same Markdown files. There's no
lock-in either way — flip between them when your needs change.
## Works with the tools you already use
| Client | Transport | Notes |
|---|---|---|
| Cloud web app | https | Sign in at basicmemory.com — no install |
| [Claude Desktop](#claude-desktop) | stdio/https | macOS / Windows / Linux |
| [Claude Code](#claude-code) | stdio/https | `claude mcp add` |
| [Codex](#codex-cli) | stdio/https | OpenAI's coding agent |
| [Cursor](#cursor) | stdio/https | `.cursor/mcp.json` |
| [VS Code](#vs-code) | stdio/https | Native MCP support |
| [ChatGPT](#chatgpt) | https | Custom GPT actions (`search` / `fetch`) |
| [Obsidian](#obsidian) | — | Reads/writes the same Markdown directly |
| Anything MCP | stdio/https | If it speaks MCP, it works |
## Official agent packages
This repository is also the canonical home for Basic Memory's host-native
agent packages. The core Python package, Claude Code plugin, shared skills,
Hermes plugin, and OpenClaw plugin all ship from the same source tree.
Maintainers can verify the whole consolidated surface from the repo root:
```bash
just package-check
```
Package-local justfiles are also available when working inside one host:
```bash
just package-check-claude-code
just package-check-skills
just package-check-hermes
just package-check-openclaw
```
### Claude Code plugin
The Claude Code plugin is the bridge between Claude's working memory and Basic
Memory — session-start briefings, pre-compaction checkpoints, an opt-in capture
output style, and `/basic-memory:setup` · `:remember` · `:share` · `:status`.
**Connect the Basic Memory MCP server first** — see [Connect your AI
client](#connect-your-ai-client). The plugin's hooks and skills call it, so it's a
hard prerequisite. Then add the marketplace and install:
```bash
claude plugin marketplace add basicmachines-co/basic-memory \
--sparse .claude-plugin plugins/claude-code
claude plugin install basic-memory@basicmachines-co
```
Source: [`plugins/claude-code`](plugins/claude-code).
### Shared skills
Framework-agnostic `SKILL.md` files live in [`skills/`](skills). If your
Skills CLI supports subpath installs:
```bash
npx skills add basicmachines-co/basic-memory --path skills
```
If it does not, copy the `memory-*` directories from `skills/` into your
agent's skills directory as a temporary Phase 1 install path.
### Hermes
Hermes keeps its native plugin shape under [`integrations/hermes`](integrations/hermes):
```bash
hermes plugins install basicmachines-co/basic-memory --path integrations/hermes
```
If your Hermes build lacks subpath installs, use the final deprecated
`basicmachines-co/hermes-basic-memory` pointer release until host support
lands.
### OpenClaw
OpenClaw stays package-native and publishes from
[`integrations/openclaw`](integrations/openclaw):
```bash
openclaw plugins install @basicmemory/openclaw-basic-memory
```
## Pick up where you left off
https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
## Quick Start
## Connect your AI client
```bash
# Install with uv (recommended)
uv tool install basic-memory
If you went the [Cloud](#get-started) route, the web app walks you through
client connect. The snippets below are for local installs.
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
# Add this to your config:
### Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp"
]
"args": ["basic-memory", "mcp"]
}
}
}
# Now in Claude Desktop, you can:
# - Write notes with "Create a note about coffee brewing methods"
# - Read notes with "What do I know about pour over coffee?"
# - Search with "Find information about Ethiopian beans"
```
You can view shared context via files in `~/basic-memory` (default directory location).
Restart Claude Desktop. Notes live in `~/basic-memory` by default.
### Alternative Installation via Smithery
<details>
<summary><b>Claude Code, Codex CLI, Cursor, VS Code, ChatGPT, Obsidian</b></summary>
You can use [Smithery](https://smithery.ai/server/@basicmachines-co/basic-memory) to automatically configure Basic
Memory for Claude Desktop:
### Claude Code
```bash
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
claude mcp add basic-memory -- uvx basic-memory mcp
```
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. Note: The Smithery installation uses their hosted MCP server, while your data remains stored locally as Markdown files.
For the full memory bridge — session briefings, pre-compaction checkpoints, and
the `/basic-memory:*` commands — also install the [Claude Code
plugin](#claude-code-plugin) on top of this.
### Glama.ai
### Codex CLI
<a href="https://glama.ai/mcp/servers/o90kttu9ym">
<img width="380" height="200" src="https://glama.ai/mcp/servers/o90kttu9ym/badge" alt="basic-memory MCP server" />
</a>
Add to `~/.codex/config.toml`:
## Why Basic Memory?
Most LLM interactions are ephemeral - you ask a question, get an answer, and everything is forgotten. Each conversation
starts fresh, without the context or knowledge from previous ones. Current workarounds have limitations:
- Chat histories capture conversations but aren't structured knowledge
- RAG systems can query documents but don't let LLMs write back
- Vector databases require complex setups and often live in the cloud
- Knowledge graphs typically need specialized tools to maintain
Basic Memory addresses these problems with a simple approach: structured Markdown files that both humans and LLMs can
read
and write to. The key advantages:
- **Local-first:** All knowledge stays in files you control
- **Bi-directional:** Both you and the LLM read and write to the same files
- **Structured yet simple:** Uses familiar Markdown with semantic patterns
- **Traversable knowledge graph:** LLMs can follow links between topics
- **Standard formats:** Works with existing editors like Obsidian
- **Lightweight infrastructure:** Just local files indexed in a local SQLite database
With Basic Memory, you can:
- Have conversations that build on previous knowledge
- Create structured notes during natural conversations
- Have conversations with LLMs that remember what you've discussed before
- Navigate your knowledge graph semantically
- Keep everything local and under your control
- Use familiar tools like Obsidian to view and edit notes
- Build a personal knowledge base that grows over time
## How It Works in Practice
Let's say you're exploring coffee brewing methods and want to capture your knowledge. Here's how it works:
1. Start by chatting normally:
```
I've been experimenting with different coffee brewing methods. Key things I've learned:
- Pour over gives more clarity in flavor than French press
- Water temperature is critical - around 205°F seems best
- Freshly ground beans make a huge difference
```toml
[mcp_servers.basic-memory]
command = "uvx"
args = ["basic-memory", "mcp"]
```
... continue conversation.
### Cursor
2. Ask the LLM to help structure this knowledge:
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```
"Let's write a note about coffee brewing methods."
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": ["basic-memory", "mcp"]
}
}
}
```
LLM creates a new Markdown file on your system (which you can see instantly in Obsidian or your editor):
### VS Code
```markdown
---
title: Coffee Brewing Methods
permalink: coffee-brewing-methods
tags:
- coffee
- brewing
---
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more clarity and highlights subtle flavors
- [technique] Water temperature at 205°F (96°C) extracts optimal compounds
- [principle] Freshly ground beans preserve aromatics and flavor
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- affects [[Flavor Extraction]]
```
The note embeds semantic content and links to other topics via simple Markdown formatting.
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
- Realtime sync is enabled by default starting with v0.12.0
- Project switching during conversations is supported starting with v0.13.0
4. In a chat with the LLM, you can reference a topic:
```
Look at `coffee-brewing-methods` for context about pour over coffee
```
The LLM can now build rich context from the knowledge graph. For example:
```
Following relation 'relates_to [[Coffee Bean Origins]]':
- Found information about Ethiopian Yirgacheffe
- Notes on Colombian beans' nutty profile
- Altitude effects on bean characteristics
Following relation 'requires [[Proper Grinding Technique]]':
- Burr vs. blade grinder comparisons
- Grind size recommendations for different methods
- Impact of consistent particle size on extraction
```
Each related document can lead to more context, building a rich semantic understanding of your knowledge base.
This creates a two-way flow where:
- Humans write and edit Markdown files
- LLMs read and write through the MCP protocol
- Sync keeps everything consistent
- All knowledge stays in local files.
## Technical Implementation
Under the hood, Basic Memory:
1. Stores everything in Markdown files
2. Uses a SQLite database for searching and indexing
3. Extracts semantic meaning from simple Markdown patterns
- Files become `Entity` objects
- Each `Entity` can have `Observations`, or facts associated with it
- `Relations` connect entities together to form the knowledge graph
4. Maintains the local knowledge graph derived from the files
5. Provides bidirectional synchronization between files and the knowledge graph
6. Implements the Model Context Protocol (MCP) for AI integration
7. Exposes tools that let AI assistants traverse and manipulate the knowledge graph
8. Uses memory:// URLs to reference entities across tools and conversations
The file format is just Markdown with some simple markup:
Each Markdown file has:
### Frontmatter
```markdown
title: <Entity title>
type: <The type of Entity> (e.g. note)
permalink: <a uri slug>
- <optional metadata> (such as tags)
```
### Observations
Observations are facts about a topic.
They can be added by creating a Markdown list with a special format that can reference a `category`, `tags` using a
"#" character, and an optional `context`.
Observation Markdown format:
```markdown
- [category] content #tag (optional context)
```
Examples of observations:
```markdown
- [method] Pour over extracts more floral notes than French press
- [tip] Grind size should be medium-fine for pour over #brewing
- [preference] Ethiopian beans have bright, fruity flavors (especially from Yirgacheffe)
- [fact] Lighter roasts generally contain more caffeine than dark roasts
- [experiment] Tried 1:15 coffee-to-water ratio with good results
- [resource] James Hoffman's V60 technique on YouTube is excellent
- [question] Does water temperature affect extraction of different compounds differently?
- [note] My favorite local shop uses a 30-second bloom time
```
### Relations
Relations are links to other topics. They define how entities connect in the knowledge graph.
Markdown format:
```markdown
- relation_type [[WikiLink]] (optional context)
```
Examples of relations:
```markdown
- pairs_well_with [[Chocolate Desserts]]
- grown_in [[Ethiopia]]
- contrasts_with [[Tea Brewing Methods]]
- requires [[Burr Grinder]]
- improves_with [[Fresh Beans]]
- relates_to [[Morning Routine]]
- inspired_by [[Japanese Coffee Culture]]
- documented_in [[Coffee Journal]]
```
## Using with VS Code
For one-click installation, click one of the install buttons below...
[![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D&quality=insiders)
You can use Basic Memory with VS Code to easily retrieve and store information while coding. Click the installation buttons above for one-click setup, or follow the manual installation instructions below.
### Manual Installation
Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
Add to your User Settings (JSON):
```json
{
@@ -289,159 +327,302 @@ Add the following JSON block to your User Settings (JSON) file in VS Code. You c
}
```
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
### ChatGPT
Basic Memory exposes OpenAI-compatible `search` and `fetch` tools for Custom
GPT actions. See the [ChatGPT integration
guide](https://docs.basicmemory.com/integrations/chatgpt/?utm_source=github&utm_medium=referral&utm_campaign=readme).
### Obsidian
No setup. Point Obsidian at `~/basic-memory` (or your project folder) and the
same wikilinks, frontmatter, and Markdown your AI writes appear in your graph
view. Edit either side — sync handles the rest.
</details>
Try a prompt:
```
"Create a note about our project architecture decisions."
"Find information about JWT auth in my notes."
"What have I been working on this week?"
```
## What's New
- **Automatic updates.** Basic Memory keeps itself up to date for `uv tool`
and Homebrew installs; `bm update` triggers a manual check.
- **Semantic vector search.** Find notes by meaning, not just keywords.
Hybrid full-text + vector ranking with FastEmbed embeddings, on SQLite or
Postgres.
- **Schema system.** Infer, validate, and diff the structure of your
knowledge base with `schema_infer`, `schema_validate`, `schema_diff`.
- **Per-project cloud routing.** Route individual projects through the cloud
while others stay local, via API key (`bm project set-cloud`).
- **Smarter editing.** `edit_note` append/prepend auto-creates notes when
missing; `write_note` guards against accidental overwrites.
- **Richer search results.** Matched chunk text is included so the LLM gets
context, not just hits.
- **FastMCP 3.0 + tool annotations.** Every tool ships with MCP behavior
hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
`openWorldHint`) so agents can discover capabilities progressively at
runtime instead of guessing or burning tokens.
- **CLI overhaul.** `--json` output for scripting, workspace-aware commands,
and an htop-inspired project dashboard.
Full [CHANGELOG](CHANGELOG.md) for v0.18 → v0.20.
## Why Basic Memory
Most LLM conversations are ephemeral. You ask a question, get an answer, then
everything is forgotten. Workarounds have limits:
- **Chat history** captures conversations but isn't structured knowledge.
- **RAG** lets the LLM query your documents but not write back to them.
- **Vector DBs** need complex infra and usually live in someone else's cloud.
- **Knowledge graphs** need specialized tooling to maintain.
Basic Memory takes a simpler path: **structured Markdown files that humans
and LLMs both read and write.**
- All knowledge stays in plain files you control.
- Both sides read and write to the same files.
- Familiar Markdown with semantic patterns — no new format to learn.
- A traversable graph the LLM can follow link by link.
- Works with the editors you already use (Obsidian, VS Code, anything).
- Just files plus a local SQLite index. No servers required.
## How it works
You're chatting normally about coffee:
> I've been experimenting with brewing methods. Pour over gives more clarity
> than French press, water at 205°F seems best, and freshly ground beans
> make a huge difference.
Ask the LLM to capture it:
> "Make a note on coffee brewing methods."
A Markdown file appears in your project directory in real time:
```markdown
---
title: Coffee Brewing Methods
permalink: coffee-brewing-methods
tags: [coffee, brewing]
---
# Coffee Brewing Methods
## Observations
- [method] Pour over highlights subtle flavors over body
- [technique] Water at 205°F (96°C) extracts optimal compounds
- [principle] Freshly ground beans preserve aromatics
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- affects [[Flavor Extraction]]
```
Next session, the LLM picks up the thread. It follows the relations to
surface what you already know about Ethiopian beans and burr grinders, and
builds on it instead of starting over. You see the same files in Obsidian or
your editor. Edit them by hand — the AI sees your changes too.
Real two-way flow: humans edit Markdown, LLMs read/write through MCP, sync
keeps everything consistent, and the source of truth is always your files.
## The Markdown format
Each file is an `Entity`. Entities have `Observations` (facts about them) and
`Relations` (links to other entities). That's the whole grammar.
### Frontmatter
```markdown
---
title: <Entity title>
type: note
permalink: <uri-slug>
tags: [optional, list]
---
```
### Observations
Facts about the entity. Categories in `[brackets]`, tags with `#`, optional
context in parens.
```markdown
- [method] Pour over highlights subtle flavors
- [tip] Grind medium-fine for V60 #brewing
- [fact] Lighter roasts contain more caffeine than dark
- [resource] James Hoffmann's V60 technique on YouTube
- [question] How does temperature affect compound extraction?
```
### Relations
Wiki-style links that form the graph. Single-token relation types, or quote
multi-word ones.
```markdown
- pairs_well_with [[Chocolate Desserts]]
- grown_in [[Ethiopia]]
- requires [[Burr Grinder]]
- "pairs well with" [[Dark Chocolate]]
```
Bare `- [[Target]]` and prose `- Worth checking out [[Target]]` index as
`links_to`. Full reference in the
[docs](https://docs.basicmemory.com/getting-started/note-formatting/?utm_source=github&utm_medium=referral&utm_campaign=readme).
## MCP tools
Basic Memory exposes these tools to any MCP client. Every tool is annotated
with MCP behavior hints (read-only, destructive, idempotent, open-world) so
agents can pick the right one without trial-and-error:
- **Content:** `write_note`, `read_note`, `edit_note`, `move_note`,
`delete_note`, `read_content`, `view_note`
- **Search & discovery:** `search`, `search_notes`, `recent_activity`,
`list_directory`
- **Knowledge graph:** `build_context` (navigates `memory://` URLs),
`canvas` (Obsidian canvas generation)
- **Projects:** `list_memory_projects`, `create_memory_project`,
`get_current_project`, `sync_status`
- **Schema:** `schema_infer`, `schema_validate`, `schema_diff`
- **Cloud:** `cloud_info`, `release_notes`
All MCP tools default to text output; pass `output_format="json"` for
structured responses. Full tool reference in the
[docs](https://docs.basicmemory.com/?utm_source=github&utm_medium=referral&utm_campaign=readme).
## CLI essentials
```bash
# Projects
basic-memory project list
basic-memory project add research ~/research
basic-memory project set-cloud research # route through cloud
basic-memory project set-local research # revert
# Health & maintenance
basic-memory status
basic-memory doctor # file <-> DB consistency check
basic-memory tool edit-note ... # CLI access to MCP tools
basic-memory update # check for and install updates
# Imports
basic-memory import claude conversations
basic-memory import chatgpt
basic-memory import memory-json
```
Routing flags (`--local` / `--cloud`) force a target when you're in mixed
mode. Full CLI reference in the
[docs](https://docs.basicmemory.com/guides/cli-reference/?utm_source=github&utm_medium=referral&utm_campaign=readme).
## Auto-updates
CLI installs check for updates every 24 hours by default and apply them
silently (so the MCP server keeps responding).
- Supported install sources: `uv tool`, Homebrew
- Skipped for `uvx` (ephemeral runtime managed by uv)
- Manual: `bm update` (check + apply) or `bm update --check` (check only)
Disable in `~/.basic-memory/config.json`:
```json
{
"servers": {
"basic-memory": {
"command": "uvx",
"args": ["basic-memory", "mcp"]
}
}
}
{ "auto_update": false }
```
## Using with Claude Desktop
## Telemetry
Basic Memory is built using the MCP (Model Context Protocol) and works with the Claude desktop app (https://claude.ai/):
Minimal, anonymous events to understand the CLI-to-cloud conversion funnel.
1. Configure Claude Desktop to use Basic Memory:
**What we collect:** cloud promo impressions, cloud login attempts and
outcomes, promo opt-out events.
Edit your MCP configuration file (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json`
for OS X):
**What we don't:** file contents, note titles, knowledge base data, PII, IP
addresses, per-command or per-tool tracking.
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp"
]
}
}
}
```
Events go to our [Umami Cloud](https://umami.is) instance (open-source,
privacy-focused) on a background thread — never blocks the CLI.
If you want to use a specific project (see [Multiple Projects](docs/User%20Guide.md#multiple-projects)), update your
Claude Desktop
config:
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"--project",
"your-project-name",
"mcp"
]
}
}
}
```
2. Sync your knowledge:
Basic Memory will sync the files in your project in real time if you make manual edits.
3. In Claude Desktop, the LLM can now use these tools:
```
write_note(title, content, folder, tags) - Create or update notes
read_note(identifier, page, page_size) - Read notes by title or permalink
edit_note(identifier, operation, content) - Edit notes incrementally (append, prepend, find/replace)
move_note(identifier, destination_path) - Move notes with database consistency
view_note(identifier) - Display notes as formatted artifacts for better readability
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
search_notes(query, page, page_size) - Search across your knowledge base
recent_activity(type, depth, timeframe) - Find recently updated information
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
list_memory_projects() - List all available projects with status
switch_project(project_name) - Switch to different project context
get_current_project() - Show current project and statistics
create_memory_project(name, path, set_default) - Create new projects
delete_project(name) - Delete projects from configuration
set_default_project(name) - Set default project
sync_status() - Check file synchronization status
```
5. Example prompts to try:
```
"Create a note about our project architecture decisions"
"Find information about JWT authentication in my notes"
"Create a canvas visualization of my project components"
"Read my notes on the authentication system"
"What have I been working on in the past week?"
"Switch to my work-notes project"
"List all my available projects"
"Edit my coffee brewing note to add a new technique"
"Move my old meeting notes to the archive folder"
```
## Futher info
See the [Documentation](https://memory.basicmachines.co/) for more info, including:
- [Complete User Guide](https://memory.basicmachines.co/docs/user-guide)
- [CLI tools](https://memory.basicmachines.co/docs/cli-reference)
- [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
- [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
## Installation Options
### Stable Release
```bash
pip install basic-memory
```
### Beta/Pre-releases
```bash
pip install basic-memory --pre
```
### Development Builds
Development versions are automatically published on every commit to main with versions like `0.12.4.dev26+468a22f`:
```bash
pip install basic-memory --pre --force-reinstall
```
### Docker
Run Basic Memory in a container with volume mounting for your Obsidian vault:
Opt out:
```bash
# Clone and start with Docker Compose
git clone https://github.com/basicmachines-co/basic-memory.git
cd basic-memory
# Edit docker-compose.yml to point to your Obsidian vault
# Then start the container
docker-compose up -d
export BASIC_MEMORY_NO_PROMOS=1
```
Or use Docker directly:
This disables promos and all telemetry.
## Logging
Basic Memory uses [Loguru](https://github.com/Delgan/loguru). Defaults vary
by entry point:
| Entry point | Default | Why |
|---|---|---|
| CLI commands | File only | Doesn't interfere with command output |
| MCP server | File only | Stdout would corrupt JSON-RPC |
| API server | File (local) or stdout (cloud) | Docker/cloud uses stdout |
Log file: `~/.basic-memory/basic-memory.log` (10MB rotation, 10 days
retention).
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `BASIC_MEMORY_LOG_LEVEL` | `INFO` | DEBUG / INFO / WARNING / ERROR |
| `BASIC_MEMORY_CLOUD_MODE` | `false` | API logs to stdout with structured context |
| `BASIC_MEMORY_FORCE_LOCAL` | `false` | Force local API routing |
| `BASIC_MEMORY_FORCE_CLOUD` | `false` | Force cloud API routing |
| `BASIC_MEMORY_EXPLICIT_ROUTING` | `false` | Mark route selection as explicit |
| `BASIC_MEMORY_ENV` | `dev` | Set to `test` for test mode (stderr only) |
| `BASIC_MEMORY_NO_PROMOS` | `false` | Disable cloud promos and telemetry |
| `BASIC_MEMORY_IMPORT_UPLOAD_MAX_BYTES` | `104857600` | Max uploaded import size |
```bash
docker run -d \
--name basic-memory-server \
-v /path/to/your/obsidian-vault:/data/knowledge:rw \
-v basic-memory-config:/root/.basic-memory:rw \
ghcr.io/basicmachines-co/basic-memory:latest
BASIC_MEMORY_LOG_LEVEL=DEBUG basic-memory sync
tail -f ~/.basic-memory/basic-memory.log
```
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
## Development
Basic Memory supports SQLite (default, fast, no Docker) and Postgres
(via testcontainers — Docker required).
```bash
just install # Install with dev dependencies
just test-sqlite # All tests, SQLite
just test-postgres # All tests, Postgres (testcontainers)
just test # Both backends
just fast-check # fix/format/typecheck + impacted tests
just doctor # File <-> DB consistency check (temp config)
just package-check # Claude Code, skills, Hermes, OpenClaw package checks
just lint
just typecheck # Pyright (primary)
just typecheck-ty # ty (supplemental)
just format
just check # All quality checks
just migration "msg" # New Alembic migration
```
Tests use pytest markers: `windows`, `benchmark`, `smoke`. See
[justfile](justfile) for the full list.
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
## License
AGPL-3.0
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
and submitting PRs.
[AGPL-3.0](LICENSE).
## Star History
@@ -453,4 +634,4 @@ and submitting PRs.
</picture>
</a>
Built with ♥️ by Basic Machines
Built with ♥️ by [Basic Machines](https://basicmachines.co?utm_source=github&utm_medium=referral&utm_campaign=readme)
+67 -2
View File
@@ -8,6 +8,71 @@
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
If you find a vulnerability, please contact hello@basicmachines.co.
If you find a vulnerability, please contact hello@basicmachines.co
Please do not open a public GitHub issue for security vulnerabilities. We aim
to respond within 72 hours and will coordinate a fix and disclosure timeline
with you.
## Threat Model
Basic Memory is a local-first MCP server that reads and writes markdown files
inside configured project directories. It runs on your machine with your user
permissions, so local configuration deserves the same care as any other
developer tool that can access your files.
### What Basic Memory Controls
- Filesystem-touching tools validate paths against the configured project root
with `validate_project_path()`, resolved paths, and `Path.is_relative_to()`.
Path traversal attempts such as `../../etc/passwd` are blocked at this layer.
- Scan optimizations in `sync_service.py` call `find` through
`asyncio.create_subprocess_exec()` with explicit argument lists. Project paths
are passed as data, not interpolated into shell strings.
- Auto-update code uses hardcoded commands, list-form arguments, and
`stdin=DEVNULL`. User-controlled strings do not reach a shell there.
### MCP Client-Side Risk
Recent MCP ecosystem research has highlighted a client-side pattern where an
MCP host can be configured to run arbitrary commands as "servers." That risk is
in the host configuration, not in notes or Basic Memory tool input.
The recommended Basic Memory MCP configuration uses a known command with
explicit arguments:
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": ["basic-memory", "mcp"]
}
}
}
```
Only add MCP server entries from sources you trust. Avoid inline shell scripts
or command strings copied from untrusted sources. Treat third-party MCP server
configuration with the same scrutiny as any locally executed program.
Related ecosystem context:
- OX Security: The Mother of All AI Supply Chains
- CSO Online: RCE by design: MCP architectural choice haunts AI agent ecosystem
### Out Of Scope
- Basic Memory does not execute note content as code. Notes are returned as
data to the LLM.
- Basic Memory does not open network ports by default. The MCP server uses
stdio; the optional REST API is intended for localhost use.
- Basic Memory is designed for single-user local knowledge bases and does not
implement access controls between operating-system users.
## Secure Configuration Checklist
- MCP config `command` points to `uvx` or a trusted binary, not a shell string.
- Project paths in Basic Memory config come from trusted local configuration.
- If exposing the REST API, bind it only to localhost.
- Review any third-party MCP servers before adding them to your host config.
+45
View File
@@ -0,0 +1,45 @@
# Docker Compose configuration for Basic Memory with PostgreSQL
# Use this for local development and testing with Postgres backend.
#
# The Postgres backend requires the pgvector extension (semantic search).
# This image bundles pgvector; plain postgres:17 will not work for vector search.
#
# Usage:
# docker-compose -f docker-compose-postgres.yml up -d
# docker-compose -f docker-compose-postgres.yml down
services:
postgres:
image: pgvector/pgvector:pg17
container_name: basic-memory-postgres
environment:
# Local development/test credentials - NOT for production
# These values are referenced by tests and justfile commands
POSTGRES_DB: basic_memory
POSTGRES_USER: basic_memory_user
POSTGRES_PASSWORD: dev_password # Simple password for local testing only
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U basic_memory_user -d basic_memory"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
# Named volume for Postgres data
postgres_data:
driver: local
# Named volume for persistent configuration
# Database will be stored in Postgres, not in this volume
basic-memory-config:
driver: local
# Network configuration (optional)
# networks:
# basic-memory-net:
# driver: bridge
+3 -1
View File
@@ -17,7 +17,9 @@ services:
volumes:
# Persistent storage for configuration and database
- basic-memory-config:/root/.basic-memory:rw
# Container runs as `appuser` (Dockerfile USER directive), so the CLI
# config dir lives under /home/appuser, not /root.
- basic-memory-config:/home/appuser/.basic-memory:rw
# Mount your knowledge directory (required)
# Change './knowledge' to your actual Obsidian vault or knowledge directory
-431
View File
@@ -1,431 +0,0 @@
---
title: AI Assistant Guide
type: note
permalink: docs/ai-assistant-guide
---
> Note: This is an optional document that can be copy/pasted into the project knowledge for an LLM to provide a full description of how it can work with Basic Memory. It is provided as a helpful resource. The tools contain extensive usage description prompts with enable the LLM to understand them.
You can [download](https://github.com/basicmachines-co/basic-memory/blob/main/docs/AI%20Assistant%20Guide.md) the contents of this file from GitHub
# AI Assistant Guide for Basic Memory
This guide helps you, the AI assistant, use Basic Memory tools effectively when working with users. It covers reading, writing, and navigating knowledge through the Model Context Protocol (MCP).
## Quick Reference
**Essential Tools:**
- `write_note()` - Create/update notes (primary tool)
- `read_note()` - Read existing content
- `search_notes()` - Find information
- `edit_note()` - Modify existing notes incrementally (v0.13.0)
- `move_note()` - Organize files with database consistency (v0.13.0)
**Project Management (v0.13.0):**
- `list_projects()` - Show available projects
- `switch_project()` - Change active project
- `get_current_project()` - Current project info
**Key Principles:**
1. **Build connections** - Rich knowledge graphs > isolated notes
2. **Ask permission** - "Would you like me to record this?"
3. **Use exact titles** - For accurate `[[WikiLinks]]`
4. **Leverage v0.13.0** - Edit incrementally, organize proactively, switch projects contextually
## Overview
Basic Memory allows you and users to record context in local Markdown files, building a rich knowledge base through natural conversations. The system automatically creates a semantic knowledge graph from simple text patterns.
- **Local-First**: All data is stored in plain text files on the user's computer
- **Real-Time**: Users see content updates immediately
- **Bi-Directional**: Both you and users can read and edit notes
- **Semantic**: Simple patterns create a structured knowledge graph
- **Persistent**: Knowledge persists across sessions and conversations
## The Importance of the Knowledge Graph
Basic Memory's value comes from connections between notes, not just the notes themselves. When writing notes, your primary goal should be creating a rich, interconnected knowledge graph.
When creating content, focus on:
1. **Increasing Semantic Density**: Add multiple observations and relations to each note
2. **Using Accurate References**: Aim to reference existing entities by their exact titles
3. **Creating Forward References**: Feel free to reference entities that don't exist yet - Basic Memory will resolve these when they're created later
4. **Creating Bidirectional Links**: When appropriate, connect entities from both directions
5. **Using Meaningful Categories**: Add semantic context with appropriate observation categories
6. **Choosing Precise Relations**: Use specific relation types that convey meaning
Remember that a knowledge graph with 10 heavily connected notes is more valuable than 20 isolated notes. Your job is to help build these connections.
## Core Tools Reference
### Essential Content Management
**Writing knowledge** (most important tool):
```
write_note(
title="Search Design",
content="# Search Design\n...",
folder="specs", # Optional
tags=["search", "design"], # v0.13.0: now searchable!
project="work-notes" # v0.13.0: target specific project
)
```
**Reading knowledge:**
```
read_note("Search Design") # By title
read_note("specs/search-design") # By path
read_note("memory://specs/search") # By memory URL
```
**Viewing notes as formatted artifacts (Claude Desktop):**
```
view_note("Search Design") # Creates readable artifact
view_note("specs/search-design") # By permalink
view_note("memory://specs/search") # By memory URL
```
**Incremental editing** (v0.13.0):
```
edit_note(
identifier="Search Design", # Must be EXACT title/permalink (strict matching)
operation="append", # append, prepend, find_replace, replace_section
content="\n## New Section\nContent here..."
)
```
**⚠️ Important:** `edit_note` requires exact identifiers (no fuzzy matching). Use `search_notes()` first if uncertain.
**File organization** (v0.13.0):
```
move_note(
identifier="Old Note", # Must be EXACT title/permalink (strict matching)
destination="archive/old-note.md" # Folders created automatically
)
```
**⚠️ Important:** `move_note` requires exact identifiers (no fuzzy matching). Use `search_notes()` first if uncertain.
### Project Management (v0.13.0)
```
list_projects() # Show available projects
switch_project("work-notes") # Change active project
get_current_project() # Current project info
```
### Search & Discovery
```
search_notes("authentication system") # v0.13.0: includes frontmatter tags
build_context("memory://specs/search") # Follow knowledge graph connections
recent_activity(timeframe="1 week") # Check what's been updated
```
## memory:// URLs Explained
Basic Memory uses a special URL format to reference entities in the knowledge graph:
- `memory://title` - Reference by title
- `memory://folder/title` - Reference by folder and title
- `memory://permalink` - Reference by permalink
- `memory://path/relation_type/*` - Follow all relations of a specific type
- `memory://path/*/target` - Find all entities with relations to target
## Semantic Markdown Format
Knowledge is encoded in standard markdown using simple patterns:
**Observations** - Facts about an entity:
```markdown
- [category] This is an observation #tag1 #tag2 (optional context)
```
**Relations** - Links between entities:
```markdown
- relation_type [[Target Entity]] (optional context)
```
**Common Categories & Relation Types:**
- Categories: `[idea]`, `[decision]`, `[question]`, `[fact]`, `[requirement]`, `[technique]`, `[recipe]`, `[preference]`
- Relations: `relates_to`, `implements`, `requires`, `extends`, `part_of`, `pairs_with`, `inspired_by`, `originated_from`
## When to Record Context
**Always consider recording context when**:
1. Users make decisions or reach conclusions
2. Important information emerges during conversation
3. Multiple related topics are discussed
4. The conversation contains information that might be useful later
5. Plans, tasks, or action items are mentioned
**Protocol for recording context**:
1. Identify valuable information in the conversation
2. Ask the user: "Would you like me to record our discussion about [topic] in Basic Memory?"
3. If they agree, use `write_note` to capture the information
4. If they decline, continue without recording
5. Let the user know when information has been recorded: "I've saved our discussion about [topic] to Basic Memory."
## Understanding User Interactions
Users will interact with Basic Memory in patterns like:
1. **Creating knowledge**:
```
Human: "Let's write up what we discussed about search."
You: I'll create a note capturing our discussion about the search functionality.
[Use write_note() to record the conversation details]
```
2. **Referencing existing knowledge**:
```
Human: "Take a look at memory://specs/search"
You: I'll examine that information.
[Use build_context() to gather related information]
[Then read_note() to access specific content]
```
3. **Finding information**:
```
Human: "What were our decisions about auth?"
You: Let me find that information for you.
[Use search_notes() to find relevant notes]
[Then build_context() to understand connections]
```
4. **Editing existing notes (v0.13.0)**:
```
Human: "Add a section about deployment to my API documentation"
You: I'll add that section to your existing documentation.
[Use edit_note() with operation="append" to add new content]
```
5. **Project management (v0.13.0)**:
```
Human: "Switch to my work project and show recent activity"
You: I'll switch to your work project and check what's been updated recently.
[Use switch_project() then recent_activity()]
```
6. **File organization (v0.13.0)**:
```
Human: "Move my old meeting notes to the archive folder"
You: I'll organize those notes for you.
[Use move_note() to relocate files with database consistency]
```
## Key Things to Remember
1. **Files are Truth**
- All knowledge lives in local files on the user's computer
- Users can edit files outside your interaction
- Changes need to be synced by the user (usually automatic)
- Always verify information is current with `recent_activity()`
2. **Building Context Effectively**
- Start with specific entities
- Follow meaningful relations
- Check recent changes
- Build context incrementally
- Combine related information
3. **Writing Knowledge Wisely**
- Same title+folder overwrites existing notes
- Structure with clear headings and semantic markup
- Use tags for searchability (v0.13.0: frontmatter tags indexed)
- Keep files organized in logical folders
4. **Leverage v0.13.0 Features**
- **Edit incrementally**: Use `edit_note()` for small changes vs rewriting
- **Switch projects**: Change context when user mentions different work areas
- **Organize proactively**: Move old content to archive folders
- **Cross-project operations**: Create notes in specific projects while maintaining context
## Common Knowledge Patterns
### Capturing Decisions
```markdown
---
title: Coffee Brewing Methods
tags: [coffee, brewing, pour-over, techniques] # v0.13.0: Now searchable!
---
# Coffee Brewing Methods
## Context
I've experimented with various brewing methods including French press, pour over, and espresso.
## Decision
Pour over is my preferred method for light to medium roasts because it highlights subtle flavors and offers more control over the extraction.
## Observations
- [technique] Blooming the coffee grounds for 30 seconds improves extraction #brewing
- [preference] Water temperature between 195-205°F works best #temperature
- [equipment] Gooseneck kettle provides better control of water flow #tools
- [timing] Total brew time of 3-4 minutes produces optimal extraction #process
## Relations
- pairs_with [[Light Roast Beans]]
- contrasts_with [[French Press Method]]
- requires [[Proper Grinding Technique]]
- part_of [[Morning Coffee Routine]]
```
### Recording Project Structure
```markdown
# Garden Planning
## Overview
This document outlines the garden layout and planting strategy for this season.
## Observations
- [structure] Raised beds in south corner for sun exposure #layout
- [structure] Drip irrigation system installed for efficiency #watering
- [pattern] Companion planting used to deter pests naturally #technique
## Relations
- contains [[Vegetable Section]]
- contains [[Herb Garden]]
- implements [[Organic Gardening Principles]]
```
### Technical Discussions
```markdown
# Recipe Improvement Discussion
## Key Points
Discussed strategies for improving the chocolate chip cookie recipe.
## Observations
- [issue] Cookies spread too thin when baked at 350°F #texture
- [solution] Chilling dough for 24 hours improves flavor and reduces spreading #technique
- [decision] Will use brown butter instead of regular butter #flavor
## Relations
- improves [[Basic Cookie Recipe]]
- inspired_by [[Bakery-Style Cookies]]
- pairs_with [[Homemade Ice Cream]]
```
## v0.13.0 Workflow Examples
### Multi-Project Conversations
**User:** "I need to update my work documentation and also add a personal recipe note."
**Workflow:**
1. `list_projects()` - Check available projects
2. `write_note(title="Sprint Planning", project="work-notes")` - Work content
3. `write_note(title="Weekend Recipes", project="personal")` - Personal content
### Incremental Note Building
**User:** "Add a troubleshooting section to my setup guide."
**Workflow:**
1. `edit_note(identifier="Setup Guide", operation="append", content="\n## Troubleshooting\n...")`
**User:** "Update the authentication section in my API docs."
**Workflow:**
1. `edit_note(identifier="API Documentation", operation="replace_section", section="## Authentication")`
### Smart File Organization
**User:** "My notes are getting messy in the main folder."
**Workflow:**
1. `move_note("Old Meeting Notes", "archive/2024/old-meetings.md")`
2. `move_note("Project Notes", "projects/client-work/notes.md")`
### Creating Effective Relations
When creating relations:
1. **Reference existing entities** by their exact title: `[[Exact Title]]`
2. **Create forward references** to entities that don't exist yet - they'll be linked automatically when created
3. **Search first** to find existing entities to reference
4. **Use meaningful relation types**: `implements`, `requires`, `part_of` vs generic `relates_to`
**Example workflow:**
1. `search_notes("travel")` to find existing travel-related notes
2. Reference found entities: `- part_of [[Japan Travel Guide]]`
3. Add forward references: `- located_in [[Tokyo]]` (even if Tokyo note doesn't exist yet)
## Common Issues & Solutions
**Missing Content:**
- Try `search_notes()` with broader terms if `read_note()` fails
- Use fuzzy matching: search for partial titles
**Forward References:**
- These are normal! Basic Memory links them automatically when target notes are created
- Inform users: "I've created forward references that will be linked when you create those notes"
**Sync Issues:**
- If information seems outdated, suggest `basic-memory sync`
- Use `recent_activity()` to check if content is current
**Strict Mode for Edit/Move Operations:**
- `edit_note()` and `move_note()` require **exact identifiers** (no fuzzy matching for safety)
- If identifier not found: use `search_notes()` first to find the exact title/permalink
- Error messages will guide you to find correct identifiers
- Example workflow:
```
# ❌ This might fail if identifier isn't exact
edit_note("Meeting Note", "append", "content")
# ✅ Safe approach: search first, then use exact result
results = search_notes("meeting")
edit_note("Meeting Notes 2024", "append", "content") # Use exact title from search
```
## Best Practices
1. **Proactively Record Context**
- Offer to capture important discussions
- Record decisions, rationales, and conclusions
- Link to related topics
- Ask for permission first: "Would you like me to save our discussion about [topic]?"
- Confirm when complete: "I've saved our discussion to Basic Memory"
2. **Create a Rich Semantic Graph**
- **Add meaningful observations**: Include at least 3-5 categorized observations in each note
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
- **Use existing entities**: Before creating a new relation, search for existing entities
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead of "relates_to")
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
3. **Structure Content Thoughtfully**
- Use clear, descriptive titles
- Organize with logical sections (Context, Decision, Implementation, etc.)
- Include relevant context and background
- Add semantic observations with appropriate categories
- Use a consistent format for similar types of notes
- Balance detail with conciseness
4. **Navigate Knowledge Effectively**
- Start with specific searches
- Follow relation paths
- Combine information from multiple sources
- Verify information is current
- Build a complete picture before responding
5. **Help Users Maintain Their Knowledge**
- Suggest organizing related topics
- Identify potential duplicates
- Recommend adding relations between topics
- Offer to create summaries of scattered information
- Suggest potential missing relations: "I notice this might relate to [topic], would you like me to add that connection?"
Built with ♥️ by Basic Machines
+442
View File
@@ -0,0 +1,442 @@
# Basic Memory Architecture
This document describes the architectural patterns and composition structure of Basic Memory.
## Overview
Basic Memory is a local-first knowledge management system with three entrypoints:
- **API** - FastAPI REST server for HTTP access
- **MCP** - Model Context Protocol server for LLM integration
- **CLI** - Typer command-line interface
Each entrypoint uses a **composition root** pattern to manage configuration and dependencies.
## Composition Roots
### What is a Composition Root?
A composition root is the single place in an application where dependencies are wired together. In Basic Memory, each entrypoint has its own composition root that:
1. Reads configuration from `ConfigManager`
2. Resolves runtime mode (local/test)
3. Creates and provides dependencies to downstream code
**Key principle**: Only composition roots read global configuration. All other modules receive configuration explicitly.
### Container Structure
Each entrypoint has a container dataclass in its package:
```
src/basic_memory/
├── api/
│ └── container.py # ApiContainer
├── mcp/
│ └── container.py # McpContainer
├── cli/
│ └── container.py # CliContainer
└── runtime.py # RuntimeMode enum and resolver
```
### Container Pattern
All containers follow the same structure:
```python
@dataclass
class Container:
config: BasicMemoryConfig
mode: RuntimeMode
@classmethod
def create(cls) -> "Container":
"""Create container by reading ConfigManager."""
config = ConfigManager().config
mode = resolve_runtime_mode(is_test_env=config.is_test_env)
return cls(config=config, mode=mode)
@property
def some_computed_property(self) -> bool:
"""Derived values based on config and mode."""
return self.mode.is_local and self.config.some_setting
# Module-level singleton
_container: Container | None = None
def get_container() -> Container:
if _container is None:
raise RuntimeError("Container not initialized")
return _container
def set_container(container: Container) -> None:
global _container
_container = container
```
### Runtime Mode Resolution
The `RuntimeMode` enum centralizes mode detection:
```python
class RuntimeMode(Enum):
LOCAL = "local"
CLOUD = "cloud"
TEST = "test"
@property
def is_cloud(self) -> bool:
return self == RuntimeMode.CLOUD
@property
def is_local(self) -> bool:
return self == RuntimeMode.LOCAL
@property
def is_test(self) -> bool:
return self == RuntimeMode.TEST
```
Resolution follows this precedence in local app flows: **TEST > LOCAL**
```python
def resolve_runtime_mode(is_test_env: bool) -> RuntimeMode:
if is_test_env:
return RuntimeMode.TEST
return RuntimeMode.LOCAL
```
**Note**: `RuntimeMode` determines global behavior (e.g., whether to start file sync).
Per-project routing is orthogonal: individual projects can be set to `cloud` mode via `ProjectMode`,
which affects client routing in `get_client(project_name=...)` without changing global runtime mode.
`RuntimeMode.CLOUD` may remain for compatibility, but standard local runtime resolution does not select it.
## Dependencies Package
### Structure
The `deps/` package provides FastAPI dependencies organized by feature:
```
src/basic_memory/deps/
├── __init__.py # Re-exports for backwards compatibility
├── config.py # Configuration access
├── db.py # Database/session management
├── projects.py # Project resolution
├── repositories.py # Data access layer
├── services.py # Business logic layer
└── importers.py # Import functionality
```
### Usage in Routers
```python
from basic_memory.deps.services import get_entity_service
from basic_memory.deps.projects import get_project_config
@router.get("/entities/{id}")
async def get_entity(
id: int,
entity_service: EntityService = Depends(get_entity_service),
project: ProjectConfig = Depends(get_project_config),
):
return await entity_service.get(id)
```
### Backwards Compatibility
The old `deps.py` file still exists as a thin re-export shim:
```python
# deps.py - backwards compatibility shim
from basic_memory.deps import *
```
New code should import from specific submodules (`basic_memory.deps.services`) for clarity.
## MCP Tools Architecture
### Typed API Clients
MCP tools communicate with the API through typed clients that encapsulate HTTP paths and response validation:
```
src/basic_memory/mcp/clients/
├── __init__.py # Re-exports all clients
├── base.py # BaseClient with common logic
├── knowledge.py # KnowledgeClient - entity CRUD
├── search.py # SearchClient - search operations
├── memory.py # MemoryClient - context building
├── directory.py # DirectoryClient - directory listing
├── resource.py # ResourceClient - resource reading
└── project.py # ProjectClient - project management
```
### Client Pattern
Each client encapsulates API paths and validates responses:
```python
class KnowledgeClient(BaseClient):
"""Client for knowledge/entity operations."""
async def resolve_entity(self, identifier: str) -> int:
"""Resolve identifier to entity ID."""
response = await call_get(
self.http_client,
f"{self._base_path}/resolve/{identifier}",
)
return int(response.text)
async def get_entity(self, entity_id: int) -> EntityResponse:
"""Get entity by ID."""
response = await call_get(
self.http_client,
f"{self._base_path}/entities/{entity_id}",
)
return EntityResponse.model_validate(response.json())
```
### Tool → Client → API Flow
```
MCP Tool (thin adapter)
Typed Client (encapsulates paths, validates responses)
HTTP API (FastAPI router)
Service Layer (business logic)
Repository Layer (data access)
```
Example tool using typed client:
```python
@mcp.tool()
async def search_notes(
query: str,
project: str | None = None,
metadata_filters: dict | None = None,
tags: list[str] | None = None,
status: str | None = None,
) -> SearchResponse:
async with get_project_client(project, context) as (client, active_project):
# Import client inside function to avoid circular imports
from basic_memory.mcp.clients import SearchClient
from basic_memory.schemas.search import SearchQuery
search_query = SearchQuery(
text=query,
metadata_filters=metadata_filters,
tags=tags,
status=status,
)
search_client = SearchClient(client, active_project.external_id)
return await search_client.search(search_query.model_dump())
```
### Per-Project Client Routing
`get_project_client()` from `mcp/project_context.py` is an async context manager that:
1. Resolves the project name from config (no network call)
2. Creates the correctly-routed client based on the project's mode (local ASGI or cloud HTTP with API key)
3. Validates the project via the API
4. Yields `(client, active_project)` tuple
This solves the bootstrap problem: you need the project name to choose the right client (local vs cloud), but you need the client to validate the project exists.
```python
from basic_memory.mcp.project_context import get_project_client
async with get_project_client(project, context) as (client, active_project):
# client is routed based on project's mode (local or cloud)
# active_project is validated via the API
...
```
## Sync Coordination
### SyncCoordinator
The `SyncCoordinator` centralizes sync/watch lifecycle management:
```python
@dataclass
class SyncCoordinator:
"""Coordinates file sync and watch operations."""
status: SyncStatus = SyncStatus.NOT_STARTED
sync_task: asyncio.Task | None = None
watch_service: WatchService | None = None
async def start(self, ...):
"""Start sync and watch operations."""
async def stop(self):
"""Stop all sync operations gracefully."""
def get_status_info(self) -> dict:
"""Get current sync status for observability."""
```
### Status Enum
```python
class SyncStatus(Enum):
NOT_STARTED = "not_started"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
STOPPED = "stopped"
ERROR = "error"
```
## Project Resolution
### ProjectResolver
Unified project selection across all entrypoints:
```python
class ProjectResolver:
"""Resolves which project to use based on context."""
def resolve(
self,
explicit_project: str | None = None,
) -> ResolvedProject:
"""Resolve project using three-tier hierarchy:
1. Explicit project parameter
2. Default project from config
3. Single available project
"""
```
### Resolution Modes
```python
class ResolutionMode(Enum):
EXPLICIT = "explicit" # User specified project
DEFAULT = "default" # Using configured default
SINGLE_PROJECT = "single" # Only one project exists
FALLBACK = "fallback" # Using first available
```
## Testing Patterns
### Container Testing
Each container has corresponding tests:
```
tests/
├── api/test_api_container.py
├── mcp/test_mcp_container.py
└── cli/test_cli_container.py
```
Tests verify:
- Container creation from config
- Runtime mode properties
- Container accessor functions (get/set)
### Mocking Typed Clients
When testing MCP tools, mock at the client level:
```python
def test_search_notes(monkeypatch):
import basic_memory.mcp.clients as clients_mod
class MockSearchClient:
async def search(self, query):
return SearchResponse(results=[...])
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
```
## Design Principles
### 1. Explicit Dependencies
Modules receive configuration explicitly rather than reading globals:
```python
# Good - explicit injection
async def sync_files(config: BasicMemoryConfig):
...
# Avoid - hidden global access
async def sync_files():
config = ConfigManager().config # Hidden coupling
```
### 2. Single Responsibility
Each layer has a clear responsibility:
- **Containers**: Wire dependencies
- **Clients**: Encapsulate HTTP communication
- **Services**: Business logic
- **Repositories**: Data access
- **Tools/Routers**: Thin adapters
### 3. Deferred Imports
To avoid circular imports, typed clients are imported inside functions:
```python
async def my_tool():
async with get_client() as client:
# Import here to avoid circular dependency
from basic_memory.mcp.clients import KnowledgeClient
knowledge_client = KnowledgeClient(client, project_id)
```
### 4. Backwards Compatibility
When refactoring, maintain backwards compatibility via shims:
```python
# Old module becomes a shim
from basic_memory.new_location import *
# Docstring explains migration path
"""
DEPRECATED: Import from basic_memory.new_location instead.
This shim will be removed in a future version.
"""
```
## File Organization
```
src/basic_memory/
├── api/
│ ├── container.py # API composition root
│ ├── routers/ # FastAPI routers
│ └── ...
├── mcp/
│ ├── container.py # MCP composition root
│ ├── clients/ # Typed API clients
│ ├── tools/ # MCP tool definitions
│ └── server.py # MCP server setup
├── cli/
│ ├── container.py # CLI composition root
│ ├── app.py # Typer app
│ └── commands/ # CLI command groups
├── deps/
│ ├── config.py # Config dependencies
│ ├── db.py # Database dependencies
│ ├── projects.py # Project dependencies
│ ├── repositories.py # Repository dependencies
│ ├── services.py # Service dependencies
│ └── importers.py # Importer dependencies
├── sync/
│ ├── coordinator.py # SyncCoordinator
│ └── ...
├── runtime.py # RuntimeMode resolution
├── project_resolver.py # Unified project selection
└── config.py # Configuration management
```
+47 -16
View File
@@ -15,7 +15,7 @@ Basic Memory provides pre-built Docker images on GitHub Container Registry that
--name basic-memory-server \
-p 8000:8000 \
-v /path/to/your/obsidian-vault:/app/data:rw \
-v basic-memory-config:/root/.basic-memory:rw \
-v basic-memory-config:/app/.basic-memory:rw \
ghcr.io/basicmachines-co/basic-memory:latest
```
@@ -30,7 +30,7 @@ Basic Memory provides pre-built Docker images on GitHub Container Registry that
- "8000:8000"
volumes:
- /path/to/your/obsidian-vault:/app/data:rw
- basic-memory-config:/root/.basic-memory:rw
- basic-memory-config:/app/.basic-memory:rw
environment:
- BASIC_MEMORY_DEFAULT_PROJECT=main
restart: unless-stopped
@@ -67,7 +67,7 @@ docker build -t basic-memory .
docker run -d \
--name basic-memory-server \
-v /path/to/your/obsidian-vault:/app/data:rw \
-v basic-memory-config:/root/.basic-memory:rw \
-v basic-memory-config:/app/.basic-memory:rw \
-e BASIC_MEMORY_DEFAULT_PROJECT=main \
basic-memory
```
@@ -86,11 +86,11 @@ Basic Memory requires several volume mounts for proper operation:
2. **Configuration and Database** (Recommended):
```yaml
- basic-memory-config:/root/.basic-memory:rw
- basic-memory-config:/app/.basic-memory:rw
```
Persistent storage for configuration and SQLite database.
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json after Basic Memory starts.
You can edit the basic-memory config.json file located in the /app/.basic-memory/config.json after Basic Memory starts.
3. **Multiple Projects** (Optional):
```yaml
@@ -98,7 +98,7 @@ You can edit the basic-memory config.json file located in the /root/.basic-memor
- /path/to/project2:/app/data/project2:rw
```
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json
You can edit the basic-memory config.json file located in the /app/.basic-memory/config.json
## CLI Commands via Docker
@@ -123,7 +123,7 @@ When using Docker volumes, you'll need to configure projects to point to your mo
1. **Check current configuration:**
```bash
docker exec basic-memory-server cat /root/.basic-memory/config.json
docker exec basic-memory-server cat /app/.basic-memory/config.json
```
2. **Add a project for your mounted volume:**
@@ -184,16 +184,47 @@ environment:
### Linux/macOS
Ensure your knowledge directories have proper permissions:
The Docker container now runs as a non-root user to avoid file ownership issues. By default, the container uses UID/GID 1000, but you can customize this to match your user:
```bash
# Make directories readable/writable
chmod -R 755 /path/to/your/obsidian-vault
# Build with custom UID/GID to match your user
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t basic-memory .
# If using specific user/group
chown -R $USER:$USER /path/to/your/obsidian-vault
# Or use docker-compose with build args
```
**Example docker-compose.yml with custom user:**
```yaml
version: '3.8'
services:
basic-memory:
build:
context: .
dockerfile: Dockerfile
args:
UID: 1000 # Replace with your UID
GID: 1000 # Replace with your GID
container_name: basic-memory-server
ports:
- "8000:8000"
volumes:
- /path/to/your/obsidian-vault:/app/data:rw
- basic-memory-config:/app/.basic-memory:rw
environment:
- BASIC_MEMORY_DEFAULT_PROJECT=main
restart: unless-stopped
```
**Using pre-built images:**
If using the pre-built image from GitHub Container Registry, files will be created with UID/GID 1000. You can either:
1. Change your local directory ownership to match:
```bash
sudo chown -R 1000:1000 /path/to/your/obsidian-vault
```
2. Or build your own image with custom UID/GID as shown above.
### Windows
When using Docker Desktop on Windows, ensure the directories are shared:
@@ -217,7 +248,7 @@ When using Docker Desktop on Windows, ensure the directories are shared:
```
2. **Configuration Not Persisting:**
- Use named volumes for `/root/.basic-memory`
- Use named volumes for `/app/.basic-memory`
- Check volume mount permissions
3. **Network Connectivity:**
@@ -243,10 +274,10 @@ docker-compose logs -f basic-memory
## Security Considerations
1. **Docker Security:**
The container runs as root for simplicity. For production, consider additional security measures.
The container runs as a non-root user (UID/GID 1000 by default) for improved security. You can customize the user ID using build arguments to match your local user.
2. **Volume Permissions:**
Ensure mounted directories have appropriate permissions and don't expose sensitive data.
Ensure mounted directories have appropriate permissions and don't expose sensitive data. With the non-root container, files will be created with the specified user ownership.
3. **Network Security:**
If using HTTP transport, consider using reverse proxy with SSL/TLS and authentication if the endpoint is available on
@@ -288,7 +319,7 @@ For Docker-specific issues:
1. Check the [troubleshooting section](#troubleshooting) above
2. Review container logs: `docker-compose logs basic-memory`
3. Verify volume mounts: `docker inspect basic-memory-server`
4. Test file permissions: `docker exec basic-memory-server ls -la /root`
4. Test file permissions: `docker exec basic-memory-server ls -la /app`
For general Basic Memory support, see the main [README](../README.md)
and [documentation](https://memory.basicmachines.co/).
+512
View File
@@ -0,0 +1,512 @@
# Note Format Reference
Every document in Basic Memory is a plain Markdown file. Files are the source of truth — changes to files automatically update the knowledge graph in the database. You maintain complete ownership, files work with git, and knowledge persists independently of any AI conversation.
## Document Structure
A note has three parts: YAML frontmatter, content (observations), and relations.
```markdown
---
title: Coffee Brewing Methods
type: note
tags: [coffee, brewing]
permalink: coffee-brewing-methods
---
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more flavor clarity than French press
- [technique] Water temperature at 205°F extracts optimal compounds #brewing
- [preference] Ethiopian beans work well with lighter roasts (personal experience)
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- contrasts_with [[Tea Brewing Methods]]
```
The `## Observations` and `## Relations` headings are conventional but not required — the parser detects observations and relations by their syntax patterns anywhere in the document.
## Frontmatter
YAML metadata between `---` fences at the top of the file.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `title` | No | filename stem | Used for linking and references. Auto-set from filename if missing. |
| `type` | No | `note` | Entity type. Used for schema resolution and filtering. |
| `tags` | No | `[]` | List or comma-separated string. Used for organization and search. |
| `permalink` | No | generated from title | Stable identifier. Persists even if the file moves. |
| `schema` | No | none | Schema attachment — dict (inline), string (reference), or omitted (implicit). |
Custom fields are allowed. Any key not in the standard set is stored as `entity_metadata` and indexed for search and filtering.
```yaml
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
permalink: paul-graham
status: active
source: wikipedia
---
```
Here `status` and `source` are custom fields stored in `entity_metadata`.
### Frontmatter Value Handling
YAML automatically converts some values to native types. Basic Memory normalizes them:
- Date strings (`2025-10-24`) → kept as ISO format strings
- Numbers (`1.0`) → converted to strings
- Booleans (`true`) → converted to strings (`"True"`)
- Lists and dicts → preserved, items normalized recursively
This prevents errors when downstream code expects string values.
## Observations
An observation is a categorized fact about the entity. Written as a Markdown list item.
**Syntax:**
```
- [category] content text #tag1 #tag2 (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `[category]` | Yes | Classification in square brackets. Any text except `[]()` chars. |
| content | Yes | The fact or statement. |
| `#tags` | No | Inline tags. Space-separated, each starting with `#`. |
| `(context)` | No | Parenthesized text at end of line. Supporting details or source. |
### Examples
```markdown
- [tech] Uses SQLite for storage #database
- [design] Follows local-first architecture #architecture
- [decision] Selected bcrypt for passwords #security (based on OWASP audit)
- [name] Paul Graham
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
```
Array-like fields use repeated categories — multiple `[expertise]` observations above.
### What Is Not an Observation
The parser excludes these list item patterns:
| Pattern | Example | Reason |
|---------|---------|--------|
| Checkboxes | `- [ ] Todo item`, `- [x] Done`, `- [-] Cancelled` | Task list syntax |
| Markdown links | `- [text](url)` | URL link syntax |
| Bare wiki links | `- [[Target]]` | Treated as a `links_to` relation instead |
A list item with `#tags` but no `[category]` is still parsed — the tags are extracted and the category defaults to `Note`.
## Relations
Relations connect documents to form the knowledge graph. There are two kinds.
### Explicit Relations
Written as list items with a relation type and a `[[wiki link]]` target. Unquoted
relation types are single tokens. Quote relation types that contain spaces.
**Syntax:**
```
- relation_type [[Target Entity]] (context)
- "multi word relation type" [[Target Entity]] (context)
- 'multi word relation type' [[Target Entity]] (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `relation_type` | Yes | Single unquoted token before `[[`, or quoted text for multi-word labels. |
| `[[Target]]` | Yes | Wiki link to the target entity. Matched by title or permalink. |
| `(context)` | No | Parenthesized text after `]]`. Supporting details. |
### Examples
Explicit relations:
```markdown
- implements [[Search Design]]
- depends_on [[Database Schema]]
- works_at [[Y Combinator]] (co-founder)
- "based on" [[Customer Interview]]
- 'in response to' [[Incident Review]]
```
Bare wiki links and prose list items create implicit `links_to` relations:
```markdown
- [[Some Entity]]
- some other thing [[Some Entity]]
```
Both examples above create `links_to [[Some Entity]]`. Use quotes when the words before
`[[` are meant to be a multi-word relation type.
Common relation types:
- `implements`, `depends_on`, `relates_to`, `inspired_by`
- `extends`, `part_of`, `contains`, `pairs_with`
- `works_at`, `authored`, `collaborated_with`
Any single-token text or quoted text works as a relation type. These are conventions,
not a fixed set.
### Inline References
Wiki links appearing in regular prose create implicit `links_to` relations. This includes
list items that do not match the explicit relation grammar above.
```markdown
This builds on [[Core Design]] and uses [[Utility Functions]].
- We should revisit [[Search Design]] after the API changes.
```
This creates three relations: `links_to [[Core Design]]`, `links_to [[Utility Functions]]`,
and `links_to [[Search Design]]`.
### Forward References
Relations can link to entities that don't exist yet. Basic Memory resolves them when the target is created.
## Permalinks and memory:// URLs
Every document has a unique **permalink** — a stable identifier derived from its title. You can set one explicitly in frontmatter, or let the system generate it.
```yaml
permalink: auth-approaches-2024
```
Permalinks form the basis of `memory://` URLs:
```
memory://auth-approaches-2024 # By permalink
memory://Authentication Approaches # By title (auto-resolves)
memory://project/auth-approaches # By path
```
Pattern matching is supported:
```
memory://auth* # Starts with "auth"
memory://*/approaches # Ends with "approaches"
memory://project/*/requirements # Nested wildcard
```
## Schemas
Schemas declare the expected structure of a note — which observation categories and relation types a well-formed note should have. They use Picoschema, a compact notation from Google's Dotprompt that fits naturally in YAML frontmatter.
### Picoschema Syntax
```yaml
schema:
name: string, full name # required field with description
email?: string, contact email # ? = optional
role?: string, job title
works_at?: Organization, employer # capitalized type = entity reference
tags?(array): string, categories # array of type
status?(enum): [active, inactive] # enum with allowed values
metadata?(object): # nested object
updated_at?: string
source?: string
```
| Notation | Meaning | Example |
|----------|---------|---------|
| `field: type` | Required field | `name: string` |
| `field?: type` | Optional field | `role?: string` |
| `field(array): type` | Array of values | `expertise(array): string` |
| `field?(enum): [vals]` | Enum with allowed values | `status?(enum): [active, inactive]` |
| `field?(object):` | Nested object with sub-fields | `metadata?(object):` |
| `, description` | Description after comma | `name: string, full name` |
| `EntityName` | Capitalized type = entity reference | `works_at?: Organization` |
**Scalar types:** `string`, `integer`, `number`, `boolean`, `any`
Any type not in that set whose first letter is uppercase is treated as an entity reference (a relation target).
### Schema-to-Note Mapping
Schemas validate against existing observation/relation syntax. Note authors don't learn new syntax.
| Schema Declaration | Maps To | Example in Note |
|--------------------|---------|-----------------|
| `field: string` | Observation `[field] value` | `- [name] Paul Graham` |
| `field?(array): string` | Multiple `[field]` observations | `- [expertise] Lisp` (repeated) |
| `field?: EntityType` | Relation `field [[Target]]` | `- works_at [[Y Combinator]]` |
| `field?(array): EntityType` | Multiple `field` relations | `- authored [[Book]]` (repeated) |
| `tags` | Frontmatter `tags` array | `tags: [startups, essays]` |
| `field?(enum): [vals]` | Observation `[field] value` where value is in the set | `- [status] active` |
Observations and relations not covered by the schema are valid — schemas describe a subset, not a straitjacket.
### Schema Attachment
Three ways to attach a schema to a note, resolved in priority order:
**1. Inline schema**`schema` is a dict in frontmatter:
```yaml
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
```
Good for one-off structured notes or prototyping a schema before extracting it.
**2. Explicit reference**`schema` is a string naming a schema note:
```yaml
---
title: Basic Memory
schema: SoftwareProject
---
```
or by permalink:
```yaml
---
title: LLM Memory Patterns
schema: schema/research-project
---
```
Use when the note's `type` differs from the schema it should validate against, or when multiple schema variants exist.
**3. Implicit by type** — no `schema` field, resolved by matching `type`:
```yaml
---
title: Paul Graham
type: Person
---
```
The system looks up a schema note where `entity: Person`. If found, it applies. If not, no validation occurs.
**4. No schema** — perfectly fine. Most notes don't need one.
### Schema Notes
A schema is itself a Basic Memory note with `type: schema`. It lives anywhere (though `schema/` is the conventional directory).
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
| Field | Required | Description |
|-------|----------|-------------|
| `type` | Yes | Must be `schema` |
| `entity` | Yes | The entity type this schema describes (e.g., `Person`) |
| `version` | No | Schema version number (default: `1`) |
| `schema` | Yes | Picoschema dict defining the fields |
| `settings.validation` | No | Validation mode (default: `warn`) |
Schema notes are regular notes — they show up in search, can have observations and relations, and participate in the knowledge graph.
### Validation Modes
| Mode | Behavior |
|------|----------|
| `warn` | Warnings in output, doesn't block (default) |
| `strict` | Errors that block sync, for CI/CD enforcement |
| `off` | No validation |
### Validation Output
```
$ bm schema validate people/ada-lovelace.md
⚠ Person schema validation:
- Missing required field: name (expected [name] observation)
- Missing optional field: role
- Missing optional field: works_at (no relation found)
Unmatched observations: [fact] ×2, [born] ×1
Unmatched relations: collaborated_with
```
"Unmatched" items are informational — observations and relations the schema doesn't cover.
### Schema Inference
Generate schemas from existing notes by analyzing observation and relation frequency:
```
$ bm schema infer Person
Analyzing 30 notes with type: Person...
Observations found:
[name] 30/30 100% → name: string
[role] 27/30 90% → role?: string
[expertise] 18/30 60% → expertise?(array): string
[email] 8/30 27% → email?: string
Relations found:
works_at 22/30 73% → works_at?: Organization
Suggested schema:
name: string, full name
role?: string, job title
expertise?(array): string, areas of knowledge
email?: string, contact email
works_at?: Organization, employer
Save to schema/Person.md? [y/n]
```
Frequency thresholds:
- **100% present** → required field
- **25%+ present** → optional field
- **Below 25%** → excluded from suggestion
### Schema Drift Detection
Track how usage patterns shift over time:
```
$ bm schema diff Person
Schema drift detected:
+ expertise: now in 81% of notes (was 12%)
- department: dropped to 3% of notes
~ works_at: cardinality changed (one → many)
Update schema? [y/n/review]
```
## Complete Examples
### Simple Note (No Schema)
```markdown
---
title: Project Ideas
type: note
tags: [ideas, brainstorm]
---
# Project Ideas
## Observations
- [idea] Build a CLI tool for markdown linting #tooling
- [idea] Create a recipe knowledge base #cooking
- [priority] Focus on developer tools first (Q1 goal)
## Relations
- inspired_by [[Developer Workflow Research]]
- part_of [[Q1 Planning]]
```
### Schema-Validated Note
Schema at `schema/Person.md`:
```yaml
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
Note at `people/paul-graham.md`:
```markdown
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
---
# Paul Graham
## Observations
- [name] Paul Graham
- [role] Essayist and investor
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
- [fact] Created Viaweb, the first web app
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
```
The `[fact]` observation and `authored` relation are not in the schema — they're valid, just unmatched. The schema only checks that `[name]` exists (required) and looks for optional fields like `[role]`, `[expertise]`, and `works_at`.
### Inline Schema Note
```markdown
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
# Team Standup 2024-01-15
## Observations
- [attendees] Paul
- [attendees] Sarah
- [decisions] Ship v2 by Friday
- [action_items] Paul to review PR #42
- [blockers] Waiting on API credentials
```
+147
View File
@@ -0,0 +1,147 @@
# Simplified Local/Cloud Routing
## Context
Basic Memory now uses explicit, project-aware routing without a global cloud-mode toggle.
Routing is determined by command-level flags and project mode, not by a global `cloud_mode` state.
This document is the canonical contract for local/cloud routing behavior in CLI, MCP, and API-adjacent clients.
## Goals
1. Remove global `cloud_mode` from runtime/routing semantics.
2. Keep MCP HTTP/SSE local-only; let stdio honor per-project routing.
3. Make CLI routing explicit and easy to reason about.
4. Support projects that exist in both local and cloud without ambiguity.
## Routing Contract
Routing is resolved in this order:
1. Injected client factory (for composition/integration contexts)
2. Explicit routing override (`--local` / `--cloud` or env vars below)
3. Project-scoped routing (`project.mode`) when a project is known
4. Default local routing
### Routing Environment Variables
- `BASIC_MEMORY_FORCE_LOCAL=true`: force local transport
- `BASIC_MEMORY_FORCE_CLOUD=true`: force cloud proxy transport
- `BASIC_MEMORY_EXPLICIT_ROUTING=true`: marks routing as explicitly chosen for this command
When explicit routing is active, project mode does not override the selected route.
## Config Semantics
- `project.mode` is the only config-based routing signal for project-scoped operations.
- Legacy `cloud_mode` values may be encountered during migration/loading but are not used for routing behavior.
- Normalization saves remove stale `cloud_mode` from `~/.basic-memory/config.json`.
### Example Config
```json
{
"projects": {
"main": {
"path": "/Users/me/basic-memory",
"mode": "local",
"local_sync_path": null,
"bisync_initialized": false,
"last_sync": null
},
"specs": {
"path": "specs",
"mode": "cloud",
"local_sync_path": "/Users/me/dev/specs",
"bisync_initialized": true,
"last_sync": "2026-02-06T17:36:38.544153"
}
},
"default_project": "main",
"cloud_api_key": "bmc_abc123...",
"cloud_host": "https://cloud.basicmemory.com"
}
```
## Cloud Commands Are Auth-Only
`bm cloud login`, `bm cloud logout`, and `bm cloud status` manage authentication state.
- `bm cloud login`
- performs OAuth device flow
- stores/refreshes token material
- may verify cloud health/subscription
- does not change routing defaults
- `bm cloud logout`
- removes stored OAuth session tokens
- does not change routing defaults
- `bm cloud status`
- reports auth state (API key, OAuth token validity)
- runs health checks only when credentials are available
## MCP Transport Routing
### Stdio (default)
`bm mcp --transport stdio` uses natural per-project routing.
- Local-mode projects route through the in-process ASGI transport.
- Cloud-mode projects route to the cloud proxy with Bearer auth (API key).
- No explicit routing env vars are injected by the CLI command.
- Externally-set env vars are honored (e.g. `BASIC_MEMORY_FORCE_CLOUD=true` for cloud deployments).
- Users who need all projects forced local can set `BASIC_MEMORY_FORCE_LOCAL=true` externally.
### HTTP and SSE Transports
`bm mcp --transport streamable-http` and `bm mcp --transport sse` always route locally.
These transports set explicit local routing (`BASIC_MEMORY_FORCE_LOCAL=true` and
`BASIC_MEMORY_EXPLICIT_ROUTING=true`) before starting the server. This prevents cloud
routing regardless of project mode, since HTTP/SSE serve as local API endpoints.
## Project List UX for Dual Presence
Projects may exist in both local and cloud. `bm project list` should display that clearly in one row per logical
project identity, with explicit source/target signals.
Recommended display contract:
1. Keep one row per normalized project name/permalink.
2. Show both local and cloud presence as separate columns/indicators.
3. Show an explicit `MCP (stdio)` target column that always resolves to `local`.
4. Keep CLI route semantics explicit:
- no flags: default local for non-project commands
- `--cloud`: force cloud
- `--local`: force local
## Project LS Targeting
`bm project ls` should clearly identify which project instance is being listed.
Targeting rules:
1. No routing flags: list local project files.
2. `--cloud`: list cloud project files.
3. `--local`: list local project files (explicit override).
4. Output should label the active target (`LOCAL` or `CLOUD`) in heading or status line.
## Runtime Mode
Runtime mode is no longer a cloud/local routing switch for local app flows.
- `resolve_runtime_mode(is_test_env)` resolves to:
- `TEST` when running in test environment
- `LOCAL` otherwise
- `RuntimeMode.CLOUD` may remain for compatibility with existing tests/call sites but is not selected by normal local
runtime resolution.
## Verification Checklist
1. Loading config with legacy `cloud_mode` succeeds.
2. Saving config strips legacy `cloud_mode`.
3. `--local/--cloud` always override per-project mode for that command.
4. No-project + no-flags commands route local by default.
5. `bm cloud login/logout` do not toggle routing behavior.
6. `bm mcp` stdio routes per-project mode; HTTP/SSE remain local-forced.
7. `bm project list` communicates dual local/cloud presence without ambiguity.
8. `bm project ls` output identifies route target explicitly.
File diff suppressed because it is too large Load Diff
+241
View File
@@ -0,0 +1,241 @@
# Character Handling and Conflict Resolution
Basic Memory handles various character encoding scenarios and file naming conventions to provide consistent permalink generation and conflict resolution. This document explains how the system works and how to resolve common character-related issues.
## Overview
Basic Memory uses a sophisticated system to generate permalinks from file paths while maintaining consistency across different operating systems and character encodings. The system normalizes file paths and generates unique permalinks to prevent conflicts.
## Character Normalization Rules
### 1. Permalink Generation
When Basic Memory processes a file path, it applies these normalization rules:
```
Original: "Finance/My Investment Strategy.md"
Permalink: "finance/my-investment-strategy"
```
**Transformation process:**
1. Remove file extension (`.md`)
2. Convert to lowercase (case-insensitive)
3. Replace spaces with hyphens
4. Replace underscores with hyphens
5. Handle international characters (transliteration for Latin, preservation for non-Latin)
6. Convert camelCase to kebab-case
### 2. International Character Support
**Latin characters with diacritics** are transliterated:
- `ø``o` (Søren → soren)
- `ü``u` (Müller → muller)
- `é``e` (Café → cafe)
- `ñ``n` (Niño → nino)
**Non-Latin characters** are preserved:
- Chinese: `中文/测试文档.md``中文/测试文档`
- Japanese: `日本語/文書.md``日本語/文書`
## Common Conflict Scenarios
### 1. Hyphen vs Space Conflicts
**Problem:** Files with existing hyphens conflict with generated permalinks from spaces.
**Example:**
```
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug" (CONFLICT!)
```
**Resolution:** The system automatically resolves this by adding suffixes:
```
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug-1"
```
**Best Practice:** Choose consistent naming conventions within your project.
### 2. Case Sensitivity Conflicts
**Problem:** Different case variations that normalize to the same permalink.
**Example on macOS:**
```
Directory: Finance/investment.md
Directory: finance/investment.md (different on filesystem, same permalink)
```
**Resolution:** Basic Memory detects case conflicts and prevents them during sync operations with helpful error messages.
**Best Practice:** Use consistent casing for directory and file names.
### 3. Character Encoding Conflicts
**Problem:** Different Unicode normalizations of the same logical character.
**Example:**
```
File 1: "café.md" (é as single character)
File 2: "café.md" (e + combining accent)
```
**Resolution:** Basic Memory normalizes Unicode characters using NFD normalization to detect these conflicts.
### 4. Forward Slash Conflicts
**Problem:** Forward slashes in frontmatter or file names interpreted as path separators.
**Example:**
```yaml
---
permalink: finance/investment/strategy
---
```
**Resolution:** Basic Memory validates frontmatter permalinks and warns about path separator conflicts.
## Error Messages and Troubleshooting
### "UNIQUE constraint failed: entity.file_path, entity.project_id"
**Cause:** Two entities trying to use the same file path within a project.
**Common scenarios:**
1. File move operation where destination is already occupied
2. Case sensitivity differences on macOS
3. Character encoding conflicts
4. Concurrent file operations
**Resolution steps:**
1. Check for duplicate file names with different cases
2. Look for files with similar names but different character encodings
3. Rename conflicting files to have unique names
4. Run sync again after resolving conflicts
### "File path conflict detected during move"
**Cause:** Enhanced conflict detection preventing potential database integrity violations.
**What this means:** The system detected that moving a file would create a conflict before attempting the database operation.
**Resolution:** Follow the specific guidance in the error message, which will indicate the type of conflict detected.
## Best Practices
### 1. File Naming Conventions
**Recommended patterns:**
- Use consistent casing (prefer lowercase)
- Use hyphens instead of spaces for multi-word files
- Avoid special characters that could conflict with path separators
- Be consistent with directory structure casing
**Examples:**
```
✅ Good:
- finance/investment-strategy.md
- projects/basic-memory-features.md
- docs/api-reference.md
❌ Problematic:
- Finance/Investment Strategy.md (mixed case, spaces)
- finance/Investment Strategy.md (inconsistent case)
- docs/API/Reference.md (mixed case directories)
```
### 2. Permalink Management
**Custom permalinks in frontmatter:**
```yaml
---
type: knowledge
permalink: custom-permalink-name
---
```
**Guidelines:**
- Use lowercase permalinks
- Use hyphens for word separation
- Avoid path separators unless creating sub-paths
- Ensure uniqueness within your project
### 3. Directory Structure
**Consistent casing:**
```
✅ Good:
finance/
investment-strategies.md
portfolio-management.md
❌ Problematic:
Finance/ (capital F)
investment-strategies.md
finance/ (lowercase f)
portfolio-management.md
```
## Migration and Cleanup
### Identifying Conflicts
Use Basic Memory's built-in conflict detection:
```bash
# Sync will report conflicts
basic-memory sync
# Check sync status for warnings
basic-memory status
```
### Resolving Existing Conflicts
1. **Identify conflicting files** from sync error messages
2. **Choose consistent naming convention** for your project
3. **Rename files** to follow the convention
4. **Re-run sync** to verify resolution
### Bulk Renaming Strategy
For projects with many conflicts:
1. **Backup your project** before making changes
2. **Standardize on lowercase** file and directory names
3. **Replace spaces with hyphens** in file names
4. **Use consistent character encoding** (UTF-8)
5. **Test sync after each batch** of changes
## System Enhancements
### Recent Improvements (v0.13+)
1. **Enhanced conflict detection** before database operations
2. **Improved error messages** with specific resolution guidance
3. **Character normalization utilities** for consistent handling
4. **File swap detection** for complex move scenarios
5. **Proactive conflict warnings** during permalink resolution
### Monitoring and Logging
The system now provides detailed logging for conflict resolution:
```
DEBUG: Detected potential file path conflicts for 'Finance/Investment.md': ['finance/investment.md']
WARNING: File path conflict detected during move: entity_id=123 trying to move from 'old.md' to 'new.md'
```
These logs help identify and resolve conflicts before they cause sync failures.
## Support and Resources
If you encounter character-related conflicts not covered in this guide:
1. **Check the logs** for specific conflict details
2. **Review error messages** for resolution guidance
3. **Report issues** with examples of the conflicting files
4. **Consider the file naming best practices** outlined above
The Basic Memory system is designed to handle most character conflicts automatically while providing clear guidance for manual resolution when needed.
+926
View File
@@ -0,0 +1,926 @@
# Basic Memory Cloud CLI Guide
The Basic Memory Cloud CLI provides seamless integration between local and cloud knowledge bases using **project-scoped synchronization**. Each project can optionally sync with the cloud, giving you fine-grained control over what syncs and where.
## Overview
The cloud CLI enables you to:
- **Authenticate cloud access** - OAuth/API key credentials are stored locally for cloud operations
- **Project-scoped sync** - Each project independently manages its sync configuration
- **Explicit operations** - Sync only what you want, when you want
- **Bidirectional sync** - Keep local and cloud in sync with rclone bisync
- **Offline access** - Work locally, sync when ready
## Prerequisites
Before using Basic Memory Cloud, you need:
- **Active Subscription**: An active Basic Memory Cloud subscription is required to access cloud features
- **Subscribe**: Visit [https://basicmemory.com/subscribe](https://basicmemory.com/subscribe) to sign up
- **Optional**: Cloud is optional. Local-first open-source usage continues without cloud.
- **OSS Discount**: Use code `{{OSS_DISCOUNT_CODE}}` for 20% off for 3 months.
If you attempt to log in without an active subscription, you'll receive a "Subscription Required" error with a link to subscribe.
## Architecture: Project-Scoped Sync
### The Problem
**Old approach (SPEC-8):** All projects lived in a single `~/basic-memory-cloud-sync/` directory. This caused:
- ❌ Directory conflicts between mount and bisync
- ❌ Auto-discovery creating phantom projects
- ❌ Confusion about what syncs and when
- ❌ All-or-nothing sync (couldn't sync just one project)
**New approach (SPEC-20):** Each project independently configures sync.
### How It Works
**Projects can exist in three states:**
1. **Cloud-only** - Project exists on cloud, no local copy
2. **Cloud + Local (synced)** - Project has a local working directory that syncs
3. **Local-only** - Project exists locally and is not routed to cloud
**Example:**
```bash
# You have 3 projects on cloud:
# - research: wants local sync at ~/Documents/research
# - work: wants local sync at ~/work-notes
# - temp: cloud-only, no local sync needed
bm project add research --cloud --local-path ~/Documents/research
bm project add work --cloud --local-path ~/work-notes
bm project add temp --cloud # No local sync
# Now you can sync individually (after initial --resync):
bm project bisync --name research
bm project bisync --name work
# temp stays cloud-only
```
**What happens under the covers:**
- Config stores `cloud_projects` dict mapping project names to local paths
- Each project gets its own bisync state in `~/.basic-memory/bisync-state/{project}/`
- Rclone syncs using single remote: `basic-memory-cloud`
- Projects can live anywhere on your filesystem, not forced into sync directory
## Quick Start
### 1. Authenticate Cloud Access
Authenticate with cloud:
```bash
bm cloud login
```
**What this does:**
1. Opens browser to Basic Memory Cloud authentication page
2. Stores authentication tokens in `~/.basic-memory/basic-memory-cloud.json`
3. Validates your subscription status
4. Leaves routing behavior unchanged (auth only)
**Result:** Cloud credentials are available for cloud-routed commands.
Apply OSS discount code `{{OSS_DISCOUNT_CODE}}` during checkout to receive 20% off for 3 months.
### 2. Set Up Sync
Install rclone and configure credentials:
```bash
bm cloud setup
```
**What this does:**
1. Installs rclone with a supported package manager (if needed)
2. Fetches your tenant information from cloud
3. Generates scoped S3 credentials for sync
4. Configures single rclone remote: `basic-memory-cloud`
**Result:** You're ready to sync projects. No sync directories created yet - those come with project setup.
Rclone setup uses package managers such as Homebrew, MacPorts, apt, dnf, yum, pacman,
zypper, snap, winget, Chocolatey, or Scoop when available. It does not run remote
install scripts with `sudo`; if no supported package manager is found, the CLI prints
manual install instructions.
### 3. Add Projects with Sync
Create projects with optional local sync paths:
```bash
# Create cloud project without local sync
bm project add research --cloud
# Create cloud project WITH local sync
bm project add research --cloud --local-path ~/Documents/research
# Or configure sync for existing project
bm cloud sync-setup research ~/Documents/research
```
**What happens under the covers:**
When you add a project with `--local-path`:
1. Project created on cloud at `/app/data/research`
2. Local path stored in config for that project (`local_sync_path`)
3. Local directory created if it doesn't exist
4. Bisync state directory created at `~/.basic-memory/bisync-state/research/`
**Result:** Project is ready to sync, but no files synced yet.
### 4. Sync Your Project
Establish the initial sync baseline. **Best practice:** Always preview with `--dry-run` first:
```bash
# Step 1: Preview the initial sync (recommended)
bm project bisync --name research --resync --dry-run
# Step 2: If all looks good, run the actual sync
bm project bisync --name research --resync
```
**What happens under the covers:**
1. Rclone reads from `~/Documents/research` (local)
2. Connects to `basic-memory-cloud:bucket-name/app/data/research` (remote)
3. Creates bisync state files in `~/.basic-memory/bisync-state/research/`
4. Syncs files bidirectionally with settings:
- `conflict_resolve=newer` (most recent wins)
- `max_delete=25` (safety limit)
- Respects `.bmignore` patterns
**Result:** Local and cloud are in sync. Baseline established.
**Why `--resync`?** This is an rclone requirement for the first bisync run. It establishes the initial state that future syncs will compare against. After the first sync, never use `--resync` unless you need to force a new baseline.
See: https://rclone.org/bisync/#resync
```
--resync
This will effectively make both Path1 and Path2 filesystems contain a matching superset of all files. By default, Path2 files that do not exist in Path1 will be copied to Path1, and the process will then copy the Path1 tree to Path2.
```
### 5. Subsequent Syncs
After the first sync, just run bisync without `--resync`:
```bash
bm project bisync --name research
```
**What happens:**
1. Rclone compares local and cloud states
2. Syncs changes in both directions
3. Auto-resolves conflicts (newer file wins)
4. Updates `last_sync` timestamp in config
**Result:** Changes flow both ways - edit locally or in cloud, both stay in sync.
### 6. Verify Setup
Check status:
```bash
bm cloud status
```
You should see:
- `OAuth: token valid` (or missing/expired)
- `API Key: configured` (or not set)
- `Cloud instance is healthy`
- Instructions for project sync commands
## Working with Projects
### Understanding Project Commands
**Key concept:** Use regular `bm project` commands (not `bm cloud project`).
```bash
# Local route
bm project list --local
bm project add research ~/Documents/research
# Cloud route
bm project list --cloud
bm project add research --cloud
```
### Creating Projects
**Use case 1: Cloud-only project (no local sync)**
```bash
bm project add temp-notes --cloud
```
**What this does:**
- Creates project on cloud at `/app/data/temp-notes`
- No local directory created
- No sync configuration
**Result:** Project exists on cloud, accessible via MCP tools, but no local copy.
**Use case 2: Cloud project with local sync**
```bash
bm project add research --cloud --local-path ~/Documents/research
```
**What this does:**
- Creates project on cloud at `/app/data/research`
- Creates local directory `~/Documents/research`
- Stores sync config in `~/.basic-memory/config.json`
- Prepares for bisync (but doesn't sync yet)
**Result:** Project ready to sync. Run `bm project bisync --name research --resync` to establish baseline.
**Use case 3: Add sync to existing cloud project**
```bash
# Project already exists on cloud
bm cloud sync-setup research ~/Documents/research
```
**What this does:**
- Updates existing project's sync configuration
- Creates local directory
- Prepares for bisync
**Result:** Existing cloud project now has local sync path. Run bisync to pull files down.
### Listing Projects
View all projects:
```bash
bm project list
```
**What you see:**
- Local projects always
- Cloud projects when credentials are available
- Default project marked
- Route-related metadata (for example, local/cloud presence and sync info)
Example shape (single row for dual-presence projects):
```text
Name Path Local Path Cloud Path CLI Default MCP (stdio)
main /basic-memory ~/basic-memory /basic-memory local local
specs /specs ~/dev/specs /specs cloud local
```
### When a Project Exists in Both Local and Cloud
Use routing flags to disambiguate command targets:
```bash
# Force local target for this command
bm project info main --local
bm project ls --name main --local
# Force cloud target for this command
bm project info main --cloud
bm project ls --name main --cloud
```
Default behavior for no-project, no-flag commands is local.
For MCP stdio, routing is always local.
## File Synchronization
### Understanding the Sync Commands
**There are three sync-related commands:**
1. `bm project sync` - One-way: local → cloud (make cloud match local)
2. `bm project bisync` - Two-way: local ↔ cloud (recommended)
3. `bm project check` - Verify files match (no changes)
### One-Way Sync: Local → Cloud
**Use case:** You made changes locally and want to push to cloud (overwrite cloud).
```bash
bm project sync --name research
```
**What happens:**
1. Reads files from `~/Documents/research` (local)
2. Uses rclone sync to make cloud identical to local
3. Respects `.bmignore` patterns
4. Shows progress bar
**Result:** Cloud now matches local exactly. Any cloud-only changes are overwritten.
**When to use:**
- You know local is the source of truth
- You want to force cloud to match local
- You don't care about cloud changes
### Two-Way Sync: Local ↔ Cloud (Recommended)
**Use case:** You edit files both locally and in cloud UI, want both to stay in sync.
```bash
# First time - establish baseline
bm project bisync --name research --resync
# Subsequent syncs
bm project bisync --name research
```
**What happens:**
1. Compares local and cloud states using bisync metadata
2. Syncs changes in both directions
3. Auto-resolves conflicts (newer file wins)
4. Detects excessive deletes and fails safely (max 25 files)
**Conflict resolution example:**
```bash
# Edit locally
echo "Local change" > ~/Documents/research/notes.md
# Edit same file in cloud UI
# Cloud now has: "Cloud change"
# Run bisync
bm project bisync --name research
# Result: Newer file wins (based on modification time)
# If cloud was more recent, cloud version kept
# If local was more recent, local version kept
```
**When to use:**
- Default workflow for most users
- You edit in multiple places
- You want automatic conflict resolution
### Verify Sync Integrity
**Use case:** Check if local and cloud match without making changes.
```bash
bm project check --name research
```
**What happens:**
1. Compares file checksums between local and cloud
2. Reports differences
3. No files transferred
**Result:** Shows which files differ. Run bisync to sync them.
```bash
# One-way check (faster)
bm project check --name research --one-way
```
### Preview Changes (Dry Run)
**Use case:** See what would change without actually syncing.
```bash
bm project bisync --name research --dry-run
```
**What happens:**
1. Runs bisync logic
2. Shows what would be transferred/deleted
3. No actual changes made
**Result:** Safe preview of sync operations.
### Advanced: List Project Files by Route
**Use case:** Inspect local or cloud project files explicitly.
```bash
# List local project files (default target when no route flag is given)
bm project ls --name research
bm project ls --name research --local
# List cloud project files
bm project ls --name research --cloud
# List files in subdirectory
bm project ls --name research --cloud --path subfolder
```
**What happens:**
1. Resolves route from flags (or local default when no route is given)
2. Lists files for the chosen project instance
3. No files transferred
**Result:** See file listing for the target route.
## Multiple Projects
### Syncing Multiple Projects
**Use case:** You have several projects with local sync, want to sync all at once.
```bash
# Setup multiple projects
bm project add research --cloud --local-path ~/Documents/research
bm project add work --cloud --local-path ~/work-notes
bm project add personal --cloud --local-path ~/personal
# Establish baselines
bm project bisync --name research --resync
bm project bisync --name work --resync
bm project bisync --name personal --resync
# Daily workflow: sync everything
bm project bisync --name research
bm project bisync --name work
bm project bisync --name personal
```
**Future:** `--all` flag will sync all configured projects:
```bash
bm project bisync --all # Coming soon
```
### Mixed Usage
**Use case:** Some projects sync, some stay cloud-only.
```bash
# Projects with sync
bm project add research --cloud --local-path ~/Documents/research
bm project add work --cloud --local-path ~/work
# Cloud-only projects
bm project add archive --cloud
bm project add temp-notes --cloud
# Sync only the configured ones
bm project bisync --name research
bm project bisync --name work
# Archive and temp-notes stay cloud-only
```
**Result:** Fine-grained control over what syncs.
## Per-Project Cloud Routing (API Key)
Route individual projects through cloud using an API key. This lets you keep some projects local while others route through cloud.
### Setting Up API Key Auth
**Option A: Create a key in the web app, then save it locally:**
```bash
bm cloud set-key bmc_abc123...
```
**Option B: Create a key via CLI (requires OAuth login first):**
```bash
bm cloud login # One-time OAuth login
bm cloud create-key "my-laptop" # Creates key and saves it locally
```
The API key is account-level — it grants access to all your cloud projects. It's stored in `~/.basic-memory/config.json` as `cloud_api_key`.
On POSIX systems, Basic Memory writes `~/.basic-memory/` as user-private (`0700`) and
`config.json` as user-read/write only (`0600`). Treat this config file as a credential
file when an API key is saved.
### Setting Project Modes
```bash
# Route a project through cloud
bm project set-cloud research
# Revert to local mode
bm project set-local research
# View project modes
bm project list
```
**What happens:**
- `set-cloud`: validates the API key exists, then sets the project mode to `cloud` in config
- `set-local`: reverts the project to local mode (removes the mode entry from config)
- MCP tools and CLI commands for that project will route to `cloud_host/proxy` with the API key as Bearer token
### How It Works
When an MCP tool or CLI command runs for a cloud-mode project:
1. `get_client(project_name="research")` checks the project's mode in config
2. If mode is `cloud`, creates an HTTP client pointed at `cloud_host/proxy` with `Authorization: Bearer bmc_...`
3. If mode is `local` (default), uses the in-process ASGI transport as usual
**Routing priority** (highest to lowest):
1. Factory injection (cloud app, tests)
2. Explicit route override (`--local` / `--cloud`)
3. Per-project cloud mode (API key)
4. Local ASGI transport (default)
Route override environment variables:
- `BASIC_MEMORY_FORCE_LOCAL=true`
- `BASIC_MEMORY_FORCE_CLOUD=true`
- `BASIC_MEMORY_EXPLICIT_ROUTING=true`
No-project, no-flag CLI commands default to local routing.
### Configuration Example
```json
{
"projects": {
"personal": "/Users/me/notes",
"research": "/Users/me/research"
},
"project_modes": {
"research": "cloud"
},
"cloud_api_key": "bmc_abc123...",
"cloud_host": "https://cloud.basicmemory.com",
"default_project": "personal"
}
```
In this example, `personal` stays local and `research` routes through cloud. Projects not listed in `project_modes` default to local.
### Sync Behavior
Cloud-mode projects are automatically skipped during local file sync (background sync and file watching). Their files live on the cloud instance, not locally.
## OAuth Logout
```bash
bm cloud logout
```
**What this does:**
1. Removes stored OAuth token(s)
2. Does not change per-project route configuration
3. Does not change command routing defaults
**Result:** OAuth session is cleared. API-key-based routing still works if `cloud_api_key` is configured.
## Filter Configuration
### Understanding .bmignore
**The problem:** You don't want to sync everything (e.g., `.git`, `node_modules`, database files).
**The solution:** `.bmignore` file with gitignore-style patterns.
**Location:** `~/.basic-memory/.bmignore`
**Default patterns:**
```gitignore
# Hidden files and directories
.*
# Basic Memory internals
*.db
*.db-shm
*.db-wal
config.json
# Version control
.git
.svn
# Python
__pycache__
*.pyc
*.pyo
*.pyd
.pytest_cache
.coverage
*.egg-info
.tox
.mypy_cache
.ruff_cache
# Virtual environments
.venv
venv
env
.env
# Node.js
node_modules
# Build artifacts
build
dist
.cache
# IDE
.idea
.vscode
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Obsidian
.obsidian
# Temporary files
*.tmp
*.swp
*.swo
*~
```
**How it works:**
1. On first sync, `.bmignore` created with defaults
2. Patterns converted to rclone filter format (`.bmignore.rclone`)
3. Rclone uses filters during sync
4. Same patterns used by all projects
During conversion, file patterns exclude the direct match and recursive contents.
For example, `config.json` becomes both `- config.json` and `- config.json/**`,
while `.*` becomes both `- .*` and `- .*/**`. Directory-only patterns keep
their trailing slash, so `cache/` becomes `- cache/` and `- cache/**`.
**Customizing:**
```bash
# Edit patterns
code ~/.basic-memory/.bmignore
# Add custom patterns
echo "*.tmp" >> ~/.basic-memory/.bmignore
# Next sync uses updated patterns
bm project bisync --name research
```
## Troubleshooting
### Rclone Setup Cannot Install Automatically
**Problem:** `bm cloud setup` cannot find a supported package manager, or package-manager
installation fails.
**Explanation:** The CLI avoids remote privileged install scripts. It only invokes known
package managers and otherwise asks you to install rclone manually.
**Solution:** Install rclone with your OS package manager, then rerun setup:
```bash
# macOS
brew install rclone
# Debian/Ubuntu
sudo apt install rclone
# Fedora
sudo dnf install rclone
# Arch
sudo pacman -S rclone
# After rclone is on PATH
bm cloud setup
```
### Authentication Issues
**Problem:** "Authentication failed" or "Invalid token"
**Solution:** Re-authenticate:
```bash
bm cloud logout
bm cloud login
```
### Subscription Issues
**Problem:** "Subscription Required" error
**Solution:**
1. Visit subscribe URL shown in error
2. Sign up for subscription
3. Run `bm cloud login` again
**Note:** Access is immediate when subscription becomes active.
### Bisync Initialization
**Problem:** "First bisync requires --resync"
**Explanation:** Bisync needs a baseline state before it can sync changes.
**Solution:**
```bash
bm project bisync --name research --resync
```
**What this does:**
- Establishes initial sync state
- Creates baseline in `~/.basic-memory/bisync-state/research/`
- Syncs all files bidirectionally
**Result:** Future syncs work without `--resync`.
### Empty Directory Issues
**Problem:** "Empty prior Path1 listing. Cannot sync to an empty directory"
**Explanation:** Rclone bisync doesn't work well with completely empty directories. It needs at least one file to establish a baseline.
**Solution:** Add at least one file before running `--resync`:
```bash
# Create a placeholder file
echo "# Research Notes" > ~/Documents/research/README.md
# Now run bisync
bm project bisync --name research --resync
```
**Why this happens:** Bisync creates listing files that track the state of each side. When both directories are completely empty, these listing files are considered invalid by rclone.
**Best practice:** Always have at least one file (like a README.md) in your project directory before setting up sync.
### Bisync State Corruption
**Problem:** Bisync fails with errors about corrupted state or listing files
**Explanation:** Sometimes bisync state can become inconsistent (e.g., after mixing dry-run and actual runs, or after manual file operations).
**Solution:** Clear bisync state and re-establish baseline:
```bash
# Clear bisync state
bm project bisync-reset research
# Re-establish baseline
bm project bisync --name research --resync
```
**What this does:**
- Removes all bisync metadata from `~/.basic-memory/bisync-state/research/`
- Forces fresh baseline on next `--resync`
- Safe operation (doesn't touch your files)
**Note:** This command also runs automatically when you remove a project to clean up state directories.
### Too Many Deletes
**Problem:** "Error: max delete limit (25) exceeded"
**Explanation:** Bisync detected you're about to delete more than 25 files. This is a safety check to prevent accidents.
**Solution 1:** Review what you're deleting, then force resync:
```bash
# Check what would be deleted
bm project bisync --name research --dry-run
# If correct, establish new baseline
bm project bisync --name research --resync
```
**Solution 2:** Use one-way sync if you know local is correct:
```bash
bm project sync --name research
```
### Project Not Configured for Sync
**Problem:** "Project research has no local_sync_path configured"
**Explanation:** Project exists on cloud but has no local sync path.
**Solution:**
```bash
bm cloud sync-setup research ~/Documents/research
bm project bisync --name research --resync
```
### Connection Issues
**Problem:** "Cannot connect to cloud instance"
**Solution:** Check status:
```bash
bm cloud status
```
If instance is down, wait a few minutes and retry.
## Security
- **Authentication**: OAuth 2.1 with PKCE flow
- **Tokens**: Stored securely in `~/.basic-memory/basic-memory-cloud.json`
- **API keys**: Stored in `~/.basic-memory/config.json`, which is written with private file permissions on POSIX systems
- **Transport**: All data encrypted in transit (HTTPS)
- **Credentials**: Scoped S3 credentials (read-write to your tenant only)
- **Rclone setup**: Uses package managers or manual instructions; no remote privileged install-script fallback
- **Isolation**: Your data isolated from other tenants
- **Ignore patterns**: Sensitive files automatically excluded via `.bmignore`
## Command Reference
### Cloud Authentication
```bash
bm cloud login # Authenticate and store OAuth credentials
bm cloud logout # Remove stored OAuth credentials
bm cloud status # Check auth state and instance health
bm cloud promo --off # Disable CLI cloud promo notices
```
### API Key Management
```bash
bm cloud set-key <key> # Save a cloud API key (bmc_ prefixed)
bm cloud create-key <name> # Create API key via cloud API (requires OAuth login)
```
### Setup
```bash
bm cloud setup # Install rclone via package manager and configure credentials
```
### Project Management
```bash
bm project list --local # Local project list
bm project list --cloud # Cloud project list
bm project add <name> --cloud # Create cloud project (no sync)
bm project add <name> --cloud --local-path <path> # Create with local sync
bm cloud sync-setup <name> <path> # Add sync to existing project
bm project rm <name> # Delete project
```
### Per-Project Routing
```bash
bm project set-cloud <name> # Route project through cloud (requires API key)
bm project set-local <name> # Revert project to local mode
```
### File Synchronization
```bash
# One-way sync (local → cloud)
bm project sync --name <project>
bm project sync --name <project> --dry-run
bm project sync --name <project> --verbose
# Two-way sync (local ↔ cloud) - Recommended
bm project bisync --name <project> # After first --resync
bm project bisync --name <project> --resync # First time / force baseline
bm project bisync --name <project> --dry-run
bm project bisync --name <project> --verbose
# Integrity check
bm project check --name <project>
bm project check --name <project> --one-way
# List project files by route
bm project ls --name <project> # Default target: local
bm project ls --name <project> --local
bm project ls --name <project> --cloud
bm project ls --name <project> --cloud --path <subpath>
```
## Summary
**Basic Memory Cloud uses project-scoped sync:**
1. **Authenticate cloud access** - `bm cloud login`
2. **Install rclone** - `bm cloud setup`
3. **Add projects with sync** - `bm project add research --cloud --local-path ~/Documents/research`
4. **Preview first sync** - `bm project bisync --name research --resync --dry-run`
5. **Establish baseline** - `bm project bisync --name research --resync`
6. **Daily workflow** - `bm project bisync --name research`
**Key benefits:**
- ✅ Each project independently syncs (or doesn't)
- ✅ Projects can live anywhere on disk
- ✅ Explicit sync operations (no magic)
- ✅ Safe by design (max delete limits, conflict resolution)
- ✅ Full offline access (work locally, sync when ready)
**Future enhancements:**
- `--all` flag to sync all configured projects
- Project list showing sync status
- Watch mode for automatic sync
+91
View File
@@ -0,0 +1,91 @@
# Cloud Semantic Search Value (Customer-Facing Technical Story)
This document explains why teams should buy cloud semantic search even when local search exists.
## Core Promise
Markdown files remain the source of truth in both local and cloud modes.
- Files are portable.
- Search indexes are derived and rebuildable.
- You never get locked into proprietary document storage.
## The Customer Problem
Teams paying for cloud are usually not optimizing for "can this run locally." They are optimizing for:
- finding the right note the first time,
- keeping retrieval quality high as note volume grows,
- avoiding search slowdowns while content is actively changing,
- getting consistent results across users, agents, and sessions.
## Why Cloud Is the Aspirin
Cloud semantic search is the immediate pain reliever because it fixes the problems users feel right now.
### 1) Better hit rate on real queries
Cloud uses stronger managed embeddings than the default local model, which improves semantic recall for paraphrases and vague questions.
Customer outcome:
- fewer "I know this exists but search missed it" moments,
- less query rewording,
- faster time to answer.
### 2) Better behavior under active workloads
Cloud indexing runs out of band in workers, so indexing does not compete with interactive read/write traffic.
Customer outcome:
- stable search responsiveness during heavy updates,
- fresher semantic results shortly after edits,
- less user-visible performance variance.
### 3) Better consistency for shared knowledge
Cloud retrieval runs against a centralized tenant index, so teams and agents resolve against the same semantic state.
Customer outcome:
- fewer "works on my machine" search differences,
- more predictable agent behavior across environments,
- easier cross-user collaboration on large knowledge bases.
### 4) Better quality at higher scale
With Postgres + `pgvector` per tenant, cloud can sustain larger note collections and higher query volumes than typical local setups.
Customer outcome:
- confidence as repositories grow to tens of thousands of notes,
- less need for user-side tuning,
- fewer quality regressions as usage increases.
## Local Is the Vitamin
Local semantic search still matters and should stay strong.
- offline use,
- privacy-first operation,
- no cloud dependency,
- user-controlled runtime.
It compounds long-term ownership and resilience, but does not remove the immediate pain points cloud solves for teams at scale.
## Recommended Messaging
One-liner:
"Cloud semantic search is the aspirin: it fixes retrieval quality and performance pain now. Local semantic search is the vitamin: it builds long-term control and resilience."
Long form:
"Basic Memory keeps markdown as the source of truth everywhere. Local gives privacy and offline control. Cloud adds immediate, measurable improvements in search quality, consistency, and responsiveness for teams and agents running at scale."
## Packaging Guidance
- Base: local FTS plus optional local semantic search.
- Cloud value: higher semantic quality, stable performance under load, and consistent team-wide retrieval.
- Keep interfaces pluggable (`EmbeddingProvider`, vector backend protocol) so implementation can evolve without changing user workflows.
+499
View File
@@ -0,0 +1,499 @@
# Logfire Instrumentation Strategy
## Why
We want Logfire in Basic Memory for two specific use cases:
1. Local development and performance investigation
2. Cloud deployments where Basic Memory runs inside Basic Memory Cloud
This instrumentation must be:
- Disabled by default
- Useful when enabled
- Safe for local-first users
- Searchable in Logfire over time
The previous integration added telemetry, but it leaned too much on generic framework instrumentation. That created noisy spans with weak names and made the trace view harder to navigate. This strategy favors manual instrumentation around Basic Memory's real units of work.
## Core Principles
### 1. Default-off
Basic Memory should ship with Logfire disabled unless the operator explicitly enables it.
That means:
- no required token for normal local usage
- no surprise outbound telemetry
- no behavior change for existing users
### 2. Manual spans over automatic framework spans
We should not rely on broad auto-instrumentation for FastAPI, MCP, SQLAlchemy, or HTTP as the primary experience.
Why:
- auto-generated span names are often generic
- routes and middleware produce too many low-signal spans
- it becomes harder to answer product questions like "why was `write_note` slow?" or "where did sync time go?"
The preferred model is:
- one meaningful root span per high-level operation
- a small number of child spans for important phases
- optional targeted instrumentation only where it adds clear value
### 3. Logs must live inside traces
Basic Memory already uses `loguru` pervasively. The Logfire integration should preserve that and make those logs visible inside the active trace/span context.
If traces exist but the logs are detached from them, the integration is not doing its job.
### 4. Stable names, selective attributes
Span names should describe the operation class, not the specific input.
Good:
- `mcp.tool.write_note`
- `sync.project.scan`
- `search.execute`
- `routing.resolve_project`
Bad:
- `Searching for "foo bar baz"`
- `POST /v2/projects/123/search/`
- `write note to /specs/api.md`
Dynamic values belong in attributes, not in the span name.
## What We Should Not Do
### Avoid broad FastAPI auto-instrumentation
We should not turn on `instrument_fastapi()` and treat that as the main telemetry story.
It may still be useful in narrowly scoped debugging, but it should not define the production trace shape. The meaningful root spans should come from Basic Memory's own entrypoints and service boundaries.
### Avoid per-file spans by default
`sync` can process many files. A span per file will explode trace cardinality and make performance views noisy.
Default behavior should be:
- one span for the project sync
- child spans for scan, move handling, delete handling, markdown sync batch, relation resolution, embedding sync, watermark update
- per-file spans only for failures or very slow outliers
### Avoid high-cardinality attributes on every span
Do not attach large or highly variable values everywhere:
- raw note content
- file bodies
- long search text
- arbitrary metadata blobs
- unique IDs that make every span shape distinct
Prefer compact, queryable attributes:
- `project_name`
- `workspace_id`
- `route_mode`
- `scan_type`
- `file_count`
- `result_count`
- `search_type`
- `retrieval_mode`
- `duration_ms`
## Proposed Architecture
Add a dedicated telemetry module in core Basic Memory, separate from logging setup.
Suggested shape:
```python
# basic_memory/telemetry.py
def configure_telemetry(service_name: str, *, enable_logfire: bool) -> None: ...
def telemetry_enabled() -> bool: ...
def span(name: str, **attrs): ...
def bind_telemetry_context(**attrs): ...
```
This module should:
- configure Logfire only when explicitly enabled
- set up the Logfire `loguru` handler
- expose lightweight helpers so application code does not import `logfire` directly everywhere
- degrade cleanly to no-op behavior when disabled
This keeps the rest of the codebase readable and makes it easy to reason about what telemetry is doing.
## Logging Integration Strategy
### Goal
When a span is active, logs emitted through `loguru` during that operation should show up in the same trace.
### Preferred design
1. Configure Logfire once in the telemetry bootstrap
2. Add the Logfire `loguru` handler to the existing `loguru` configuration
3. At operation boundaries, bind stable contextual fields with `loguru`
4. Let logs emitted inside the span inherit the active trace context
### Context to bind
Bind only the fields that help correlate work across the system:
- `service_name`
- `entrypoint`
- `project_name`
- `workspace_id`
- `route_mode`
- `tool_name`
- `command_name`
This binding should happen at the root of an operation, not deep in leaf functions.
### Important nuance
We should not try to encode the entire trace model into logger extras. The logger context should be a human-meaningful slice of the active operation. Trace linkage comes from the active Logfire/OpenTelemetry context; logger extras are there to improve searchability and readability.
## Span Model
### Root spans
Each user-visible or system-visible operation should get one root span.
Examples:
- `cli.command.status`
- `cli.command.project_sync`
- `api.request.search`
- `mcp.tool.write_note`
- `mcp.tool.read_note`
- `mcp.tool.search_notes`
- `sync.project.run`
- `db.semantic_backfill`
### Child spans
Child spans should represent real phases whose duration we care about.
Examples:
- `routing.client_session`
- `routing.resolve_project`
- `routing.resolve_workspace`
- `api.search.execute`
- `sync.project.scan`
- `sync.project.detect_moves`
- `sync.project.apply_changes`
- `sync.project.resolve_relations`
- `sync.project.sync_embeddings`
- `sync.file.markdown`
- `sync.file.regular`
- `search.execute`
- `search.relaxed_fts_retry`
- `db.init`
- `db.migrate`
### Span naming rules
- Use dot-separated names
- Start with subsystem
- Keep the verb at the end
- Keep names stable across runs
- Never include request-specific text in the span name
## Attribute Taxonomy
### Required attributes on root spans
Every root span should have a small common set:
- `service_name`
- `entrypoint`
- `project_name` when applicable
- `workspace_id` when applicable
- `route_mode` with values like `local_asgi`, `cloud_proxy`, `factory`
### Operation-specific attributes
Examples:
For search:
- `search_type`
- `retrieval_mode`
- `page`
- `page_size`
- `result_count`
- `fallback_used`
For sync:
- `scan_type`
- `force_full`
- `new_count`
- `modified_count`
- `deleted_count`
- `move_count`
- `skipped_count`
- `embeddings_enabled`
For note operations:
- `tool_name`
- `note_type`
- `directory`
- `overwrite`
- `output_format`
### Attributes to avoid by default
- full `query.text`
- full note titles if they create privacy or cardinality issues
- file content
- raw frontmatter
- raw HTTP bodies
If we need richer payloads for a local debugging session, that should be an explicit temporary mode, not the default telemetry shape.
## Instrumentation Plan By Layer
### 1. Entrypoints
Instrument these first:
- `cli.app` callback and major commands
- API lifespan and selected routers
- MCP server lifespan
- MCP tool entrypoints
Why:
- this establishes clean root spans
- it gives us trace boundaries that match how users think about the product
### 2. Routing and context resolution
Instrument:
- client routing decisions
- workspace resolution
- project resolution
- default-project fallback
Why:
- Basic Memory has local/cloud/per-project routing logic
- when something is slow or surprising, we need to know which path was taken
### 3. Sync and indexing
This is the highest-value area to instrument deeply.
Instrument:
- sync root
- scan strategy decision
- filesystem scan
- move detection
- delete handling
- markdown sync phase
- relation resolution
- vector embedding sync
- scan watermark update
Why:
- this is where performance work will happen
- cloud and local both benefit from this visibility
### 4. Search
Instrument:
- search execution
- retrieval mode
- relaxed FTS fallback
- result shaping
Why:
- search is user-facing and latency-sensitive
- hybrid/vector/FTS paths need to be distinguishable
### 5. Database and initialization
Instrument selectively:
- DB init
- migrations
- semantic backfill
- connection mode selection
Avoid full automatic SQL span firehose by default.
## Recommended Rollout Phases
## Task List
- [x] Phase 1: Bootstrap and config gating
- [x] Phase 2: Root spans for entrypoints and primary operations
- [x] Phase 3: Child spans for sync, search, and routing
- [x] Phase 4: Failure-focused detail and final verification
- [x] Phase 5: Loguru context binding and scoped context inheritance
## Recommended Rollout Phases
### Phase 1: Bootstrap and config gating
Add:
- telemetry bootstrap module
- config/env gating
- `loguru` + Logfire handler integration
This gives immediate value with low noise.
### Phase 2: Root spans for entrypoints and primary operations
Add:
- root spans for CLI, API, MCP, and main MCP tools
- stable root attributes for project, workspace, route mode, and operation type
This gives us clean top-level traces that match how users think about the product.
### Phase 3: Child spans for sync, search, and routing
Add child spans to:
- sync
- search
- routing
This is the main performance-investigation layer.
### Phase 4: Failure-focused detail
Add selective deeper spans/log enrichment for:
- sync failures
- relation resolution failures
- slow file operations
- cloud routing/auth failures
This keeps normal traces clean while improving debuggability.
### Phase 5: Loguru context binding and scoped context inheritance
Add:
- context-local telemetry state in `basic_memory.telemetry`
- a shared `scope(...)` helper that opens a span and binds stable logger context together
- context inheritance for routing, sync, and search so downstream `loguru` logs carry the active operation fields
This makes the trace view and the log stream tell the same story without forcing logger rewrites across the codebase.
## Local Dev Playbook
The fastest way to sanity-check the current trace shape is:
```bash
LOGFIRE_TOKEN=lf_... just telemetry-smoke
```
What this does:
- creates an isolated temp home, config dir, and project path
- enables Logfire for the run
- automatically exports to Logfire when `LOGFIRE_TOKEN` is present
- defaults `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=false` so the smoke run stays fast and trace-friendly
- disables promo telemetry so the trace is about Basic Memory work, not analytics noise
- runs a small CLI workflow:
- `project add`
- `tool write-note`
- `tool read-note`
- `tool edit-note`
- `tool build-context`
- `tool search-notes`
- `doctor`
If you want to exercise the instrumentation without exporting anything upstream:
```bash
BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE=false just telemetry-smoke
```
If you want the smoke run to include vector or hybrid retrieval spans too:
```bash
LOGFIRE_TOKEN=lf_... BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true just telemetry-smoke
```
The recipe sets `BASIC_MEMORY_LOGFIRE_ENVIRONMENT=telemetry-smoke` by default so these traces are easy to isolate in Logfire. Override it if you want the smoke traces grouped under a different environment name.
### What to look for
You should see a small set of comparable root spans rather than a framework-generated span forest:
- `cli.command.project`
- `cli.command.tool`
- `mcp.tool.write_note`
- `mcp.tool.read_note`
- `mcp.tool.edit_note`
- `mcp.tool.build_context`
- `mcp.tool.search_notes`
- `sync.project.run`
You should also see correlated logs under those traces with stable fields like:
- `project_name`
- `route_mode`
- `tool_name`
- `entrypoint`
### Expected nuance
`doctor` creates its own temporary project on purpose. That means the sync trace will usually show a different project name than the `telemetry-smoke` write/search traces. That is fine for smoke testing because the goal is to confirm:
- root span names are meaningful
- scoped logs stay attached to the active trace
- routing, tool, search, and sync phases are easy to distinguish
## Validation Checklist
We should consider the integration successful when the following are true:
1. With telemetry disabled, Basic Memory behaves exactly as it does today.
2. With telemetry enabled, one user action produces one obvious root span.
3. Logs emitted during that action are visible inside the same trace.
4. A search in Logfire for `mcp.tool.write_note` or `sync.project.run` returns comparable spans across runs.
5. Trace views show phase timing clearly without drowning in framework noise.
6. Sensitive payloads are not captured by default.
## Immediate Implementation Direction
When we start coding, the first pass should be:
1. Add `basic_memory.telemetry`
2. Add config/env switches for `enabled`, `send_to_logfire`, and service name
3. Wire telemetry bootstrap into CLI, API, and MCP entrypoints
4. Configure `loguru` to emit to both existing sinks and the Logfire handler when enabled
5. Add manual root spans around:
- CLI commands
- API request handlers we care about
- MCP tool entrypoints
- sync root
- search root
6. Add child spans to the sync and routing phases only after the root span model feels clean
That gives us a strong foundation without repeating the earlier "turn on instrumentation everywhere" approach.
+138
View File
@@ -0,0 +1,138 @@
# MCP UI Bakeoff - Instructions & Test Plan
Last updated: 2026-02-02
## Scope
Compare three presentation paths for Basic Memory MCP tools:
1. **ToolUI (React)** via MCP App resources.
2. **MCPUI Python SDK** embedded UI resources (legacy host path).
3. **ASCII/ANSI** output for TUI clients.
This doc is the running instruction set and test plan. Update as implementation progresses.
---
## Prerequisites
- Repo: `basic-memory` (worktree: `basic-memory-mcp-ui-poc`)
- Node for toolui build (already used for POC)
- Python 3.12+ with `uv`
Optional (for MCPUI Python SDK path):
- Local repo: `/Users/phernandez/dev/mcp-ui`
- Install the server SDK into the Basic Memory venv:
- `uv pip install -e /Users/phernandez/dev/mcp-ui/sdks/python/server`
---
## Build / Refresh Steps
### ToolUI React bundle
```bash
cd ui/tool-ui-react
npm install
npm run build
```
This regenerates:
- `src/basic_memory/mcp/ui/html/search-results-tool-ui.html`
- `src/basic_memory/mcp/ui/html/note-preview-tool-ui.html`
---
## How to Run the MCP Server
```bash
basic-memory mcp --transport stdio
```
Optional to pick UI variant for MCP App resources:
```bash
export BASIC_MEMORY_MCP_UI_VARIANT=tool-ui # or vanilla | mcp-ui
```
---
## Test Cases
### 1) MCP App Resource UI (toolui / vanilla / mcpui)
Tools:
- `search_notes`
- `read_note`
Expect:
- Tool meta points to `ui://basic-memory/search-results` and `ui://basic-memory/note-preview`
- Resource content differs by `BASIC_MEMORY_MCP_UI_VARIANT`
- Variantspecific URIs also available:
- `ui://basic-memory/search-results/vanilla`
- `ui://basic-memory/search-results/tool-ui`
- `ui://basic-memory/search-results/mcp-ui`
- `ui://basic-memory/note-preview/vanilla`
- `ui://basic-memory/note-preview/tool-ui`
- `ui://basic-memory/note-preview/mcp-ui`
Manual check:
- Trigger tool in MCPAppcapable host and confirm UI renders.
---
### 2) Text / JSON Output Modes
Tools:
- `search_notes(output_format="text" | "json")`
- `read_note(output_format="text" | "json")`
- `write_note(output_format="text" | "json")`
- `edit_note(output_format="text" | "json")`
- `recent_activity(output_format="text" | "json")`
- `list_memory_projects(output_format="text" | "json")`
- `create_memory_project(output_format="text" | "json")`
- `delete_note(output_format="text" | "json")`
- `move_note(output_format="text" | "json")`
- `build_context(output_format="json" | "text")`
Expect:
- `text` mode preserves existing human-readable responses.
- `json` mode returns structured dict/list payloads for machine-readable clients.
Automated:
- `uv run pytest test-int/mcp/test_output_format_json_integration.py`
---
### 3) MCPUI Python SDK (embedded UI resource)
Tools (embedded resource responses):
- `search_notes_ui` (MCPUI SDK)
- `read_note_ui` (MCPUI SDK)
Expected output:
- Tool response content contains an EmbeddedResource (`type: "resource"`)
- `mimeType` is `text/html`
- `_meta` includes:
- `mcpui.dev/ui-preferred-frame-size`
- `mcpui.dev/ui-initial-render-data`
Manual check:
- Render tool responses using `UIResourceRenderer` (legacy host flow).
Automated (if SDK installed):
- `uv run pytest test-int/mcp/test_ui_sdk_integration.py`
---
## Bakeoff Notes Template
Fill in after running:
- ToolUI (React): __
- MCPUI SDK (embedded): __
- Text/JSON modes: __
Decision + rationale: __
+260
View File
@@ -0,0 +1,260 @@
# Metadata Search Reference
Basic Memory automatically indexes custom frontmatter fields so you can query them with structured filters. Any YAML key in a note's frontmatter beyond the standard set (`title`, `type`, `tags`, `permalink`, `schema`) is stored as `entity_metadata` and becomes searchable.
## Querying with `search_notes`
`search_notes` is the single search tool for all queries — text, metadata filters, or both. The `query` parameter is optional, so you can use metadata filters alone without passing an empty string.
## Filter Syntax
Filters are a JSON dictionary where each key targets a frontmatter field and the value specifies the match condition. Multiple keys combine with **AND** logic — every filter must match.
### Equality
Match a single value exactly.
```json
{"status": "active"}
```
Finds notes whose frontmatter contains `status: active`.
### Array Contains (all)
Pass a list to require **all** listed values to be present in the field.
```json
{"tags": ["security", "oauth"]}
```
Finds notes tagged with both `security` and `oauth`.
### `$in` (any of)
Match if the field equals **any** value in the list.
```json
{"priority": {"$in": ["high", "critical"]}}
```
### `$gt`, `$gte`, `$lt`, `$lte`
Numeric and text comparisons. Numeric values use numeric comparison; strings use lexicographic comparison.
```json
{"confidence": {"$gt": 0.7}}
{"score": {"$lte": 100}}
```
### `$between`
Range filter (inclusive). Takes a `[min, max]` pair.
```json
{"score": {"$between": [0.3, 0.8]}}
```
### Nested Access (dot notation)
Access nested frontmatter values using dots.
```json
{"schema.version": "2"}
```
This queries the `version` key inside a `schema` object in frontmatter.
### Summary Table
| Operator | Syntax | Example |
|----------|--------|---------|
| Equality | `{"field": "value"}` | `{"status": "active"}` |
| Array contains (all) | `{"field": ["a", "b"]}` | `{"tags": ["security", "oauth"]}` |
| `$in` (any of) | `{"field": {"$in": [...]}}` | `{"priority": {"$in": ["high", "critical"]}}` |
| `$gt` / `$gte` | `{"field": {"$gt": N}}` | `{"confidence": {"$gt": 0.7}}` |
| `$lt` / `$lte` | `{"field": {"$lt": N}}` | `{"score": {"$lt": 0.5}}` |
| `$between` | `{"field": {"$between": [min, max]}}` | `{"score": {"$between": [0.3, 0.8]}}` |
| Nested access | `{"a.b": "value"}` | `{"schema.version": "2"}` |
**Key rules:**
- Filter keys must match `[A-Za-z0-9_-]+` (dots separate nesting levels).
- Each operator dict must contain exactly one operator.
- `$in` and array-contains require non-empty lists.
- `$between` requires exactly two values `[min, max]`.
## MCP Tool — `search_notes`
`search_notes` is the single search tool for text queries, metadata filters, or both. The `query` parameter is optional.
**Relevant parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string (optional) | Text search query. Omit for filter-only searches. |
| `metadata_filters` | dict | Structured filter dict (see syntax above) |
| `tags` | list[str] | Convenience shorthand — merged into `metadata_filters["tags"]` |
| `status` | string | Convenience shorthand — merged into `metadata_filters["status"]` |
**Merging rules:** `tags` and `status` are convenience shortcuts. They are merged into `metadata_filters` using `setdefault` — if the same key already exists in `metadata_filters`, the explicit filter wins.
**Examples:**
```python
# Text search filtered by metadata
await search_notes("authentication", metadata_filters={"status": "draft"})
# Filter-only search (no query needed)
await search_notes(metadata_filters={"type": "spec"})
# Combine text, tags shortcut, and metadata
await search_notes(
"oauth flow",
tags=["security"],
metadata_filters={"confidence": {"$gt": 0.7}},
)
# Convenience shortcuts
await search_notes("planning", status="active")
await search_notes(tags=["tier1", "alpha"])
```
## Tag Search Shortcuts
The `tag:` prefix in a search query is a shorthand for tag-based metadata filtering. When `search_notes` receives a query starting with `tag:`, it converts the query into a `tags` filter and clears the text query.
```python
# These are equivalent:
await search_notes("tag:tier1")
await search_notes("", tags=["tier1"])
# Multiple tags (comma or space separated) — all must be present:
await search_notes("tag:tier1,alpha")
await search_notes("tag:tier1 alpha")
```
## CLI Access
The `bm tool search-notes` command exposes metadata filtering via `--meta` and `--filter` flags.
### `--meta` — simple key=value filters
Repeatable flag for equality filters on frontmatter fields.
```bash
# Single filter
bm tool search-notes "my query" --meta status=draft
# Multiple filters (AND logic)
bm tool search-notes "" --meta status=active --meta priority=high
```
### `--filter` — advanced JSON filters
Pass a full JSON filter dictionary for operator-based queries.
```bash
# Range filter
bm tool search-notes "" --filter '{"score": {"$between": [0.3, 0.8]}}'
# $in filter
bm tool search-notes "" --filter '{"priority": {"$in": ["high", "critical"]}}'
```
### `--tag` and `--status` — convenience shortcuts
```bash
bm tool search-notes "query" --tag security --tag oauth
bm tool search-notes "" --status draft
```
### Combined example
```bash
bm tool search-notes "authentication" --tag security --meta status=draft --type spec
```
## Practical Examples
### Example notes with custom frontmatter
**`specs/auth-design.md`:**
```markdown
---
title: Auth Design
type: spec
tags: [security, oauth]
status: in-progress
priority: high
confidence: 0.85
---
# Auth Design
## Observations
- [decision] Use OAuth 2.1 with PKCE for all client types #security
- [requirement] Token refresh must be transparent to the user
## Relations
- implements [[Security Requirements]]
```
**`specs/search-redesign.md`:**
```markdown
---
title: Search Redesign
type: spec
tags: [search, performance]
status: draft
priority: medium
confidence: 0.6
---
# Search Redesign
## Observations
- [goal] Sub-100ms search response times #performance
- [approach] Hybrid FTS + vector retrieval
## Relations
- depends_on [[Database Schema]]
```
### Queries that find them
```python
# Find all in-progress specs
await search_notes(metadata_filters={"status": "in-progress", "type": "spec"})
# → Auth Design
# Find high-confidence specs
await search_notes(metadata_filters={"confidence": {"$gt": 0.7}})
# → Auth Design (confidence: 0.85)
# Find specs with priority high or medium
await search_notes(metadata_filters={"priority": {"$in": ["high", "medium"]}})
# → Auth Design, Search Redesign
# Find specs in a confidence range
await search_notes(metadata_filters={"confidence": {"$between": [0.5, 0.9]}})
# → Auth Design (0.85), Search Redesign (0.6)
# Find notes tagged with security
await search_notes("tag:security")
# → Auth Design
# Combined: text search + metadata filter
await search_notes("OAuth", metadata_filters={"status": "in-progress"})
# → Auth Design
```
### CLI equivalents
```bash
bm tool search-notes "" --meta status=in-progress --type spec
bm tool search-notes "" --filter '{"confidence": {"$gt": 0.7}}'
bm tool search-notes "OAuth" --meta status=in-progress
bm tool search-notes --tag security
```
+344
View File
@@ -0,0 +1,344 @@
# Post-v0.18.0 Test Plan and Acceptance Criteria
## Goal
Define a complete validation plan for all major features merged after `v0.18.0`, combining:
- Coverage-gap-driven automated tests
- Real MCP server integration tests (no mocks for target flows)
- Manual MCP verification via LLM-driven tool calls
This plan is based on commits in `v0.18.0..HEAD` and the latest `just check` coverage output.
## Scope Window
- Start tag: `v0.18.0` (2026-01-28)
- End: current `main`
- Change volume: 12 feature commits + 14 bug-fix commits (+ release chores/hotfixes)
## Execution Strategy
1. Stabilize all feature-level acceptance criteria in automated tests first.
2. Add black-box MCP integration tests for semantic search + schema (real server startup).
3. Run manual MCP tool-call verification to confirm real UX and routing behavior.
4. Re-run full gate: `just check` + targeted integration packs.
## Global Quality Gates
- Feature criteria below must all pass.
- No regressions in existing suites.
- Coverage improves in targeted low-coverage feature modules.
- SQLite and Postgres parity for search/semantic features.
## Priority Coverage Gaps (from latest run)
These are the most important post-`v0.18.0` feature modules currently under-covered:
- `src/basic_memory/mcp/tools/schema.py` (27%)
- `src/basic_memory/mcp/clients/schema.py` (36%)
- `src/basic_memory/mcp/tools/ui_sdk.py` (43%)
- `src/basic_memory/mcp/tools/search.py` (73%)
- `src/basic_memory/repository/postgres_search_repository.py` (63%)
- `src/basic_memory/mcp/async_client.py` (82%)
- `src/basic_memory/api/v2/routers/schema_router.py` (80%)
## Feature Acceptance Criteria and Test Plan
### 1) Schema System (`c97733d`) — DONE
### Acceptance criteria
- `schema_validate`, `schema_infer`, and `schema_diff` produce consistent outcomes across CLI/API/MCP for the same fixture set.
- Strict validation fails deterministically on required-field/type violations.
- Validation warnings are stable and machine-readable in non-strict mode.
- Inference output is deterministic for unchanged input corpus.
- Drift diff output is deterministic and identifies missing/extra/type-mismatch fields correctly.
### Existing coverage anchor points
- `tests/schema/*`
- `tests/api/v2/test_schema_router.py`
- `test-int/test_schema/*`
### Gaps to close — DONE
- ~~MCP schema tool branches (`src/basic_memory/mcp/tools/schema.py`)~~ — 18 tests in `tests/mcp/test_tool_schema.py`
- ~~MCP schema client behavior (`src/basic_memory/mcp/clients/schema.py`)~~ — `tests/mcp/test_client_schema.py`
- ~~Schema router error-path branches (`src/basic_memory/api/v2/routers/schema_router.py`)~~ — `tests/api/v2/test_schema_router.py`
### Planned additions — DONE
- ~~Add MCP tool tests for `schema_validate` strict + non-strict result shapes.~~ **DONE**
- ~~Add MCP tool tests for `schema_infer` with explicit `entity_type` and inferred type fallback.~~ **DONE**
- ~~Add MCP tool tests for `schema_diff` empty-diff and non-empty-diff paths.~~ **DONE**
- ~~Add API tests for schema router invalid payload/edge error handling.~~ **DONE**
- Add integration test that starts MCP server and calls schema tools end-to-end on fixture notes. — deferred to backlog item 4.
### 2) Semantic Search (`0777879`, `1428d18`, `344e651`) — DONE
### Acceptance criteria
- `search_type=text|vector|hybrid` returns expected ranked results on canonical semantic corpus.
- Missing semantic dependencies fail fast with actionable install guidance.
- Reindex and provider/model changes produce valid vectors without dimension mismatch.
- SQLite and Postgres produce equivalent behavior for semantic modes on the same dataset.
- Generated-column migration path is valid on SQLite environments in use.
### Existing coverage anchor points
- `tests/repository/test_sqlite_vector_search_repository.py`
- `tests/repository/test_postgres_search_repository.py`
- `tests/services/test_semantic_search.py`
- `tests/mcp/test_tool_search.py`
- `test-int/test_search_performance_benchmark.py`
### Gaps to close — DONE
- ~~Uncovered Postgres vector/hybrid branches~~ — 20 tests in `tests/repository/test_postgres_search_repository_unit.py` + 5 integration tests in `test-int/semantic/test_semantic_coverage.py`
- ~~MCP search semantic/output branches~~ — expanded `tests/mcp/test_tool_search.py`
### Planned additions — DONE
- ~~Expand Postgres repository tests for vector query composition edge cases.~~ **DONE**
- ~~Expand Postgres repository tests for hybrid fusion ranking and pagination branches.~~ **DONE**
- ~~Expand Postgres repository tests for embedding/provider error handling branches.~~ **DONE**
- ~~Expand MCP search tool tests for vector/hybrid output formatting branches.~~ **DONE**
- ~~Expand MCP search tool tests for semantic-disabled and missing-dependency failures.~~ **DONE**
- Add MCP integration tests that start server and execute semantic `search_notes` tool calls. — deferred to backlog item 4.
### Semantic search quality benchmarks (NEW)
Full benchmark suite in `test-int/semantic/` covering 5 backend×provider combinations:
- `sqlite-fts`, `sqlite-fastembed`, `postgres-fts`, `postgres-fastembed`, `postgres-openai`
- Quality metrics: hit@1, recall@5, MRR@10 with per-query timing
- Realistic corpus with cross-topic vocabulary overlap (240 notes, 4 topics)
- Rich CLI viewer: `just semantic-report`
- JSON artifact output: `just test-semantic-report`
Key finding: **FastEmbed (384-d local ONNX) matches or exceeds OpenAI (1536-d) quality at 30x lower latency.** Recommending FastEmbed as default for both local and cloud deployments.
### 3) Per-Project Local/Cloud Routing + API Key Auth (`d84708c`, `ed94877`, `312662f`) — DONE
### Acceptance criteria
- Project mode (`local`/`cloud`) persists and displays correctly.
- Routing selects ASGI for local projects and HTTP+Bearer for cloud projects.
- Cloud project without key fails with explicit remediation (`cloud set-key`/`cloud create-key`).
- Resolution precedence is correct (factory > force-local > per-project cloud > global fallback > local).
- Watch/sync only run for local projects.
### Existing coverage anchor points
- `tests/mcp/test_async_client_modes.py`
- `tests/cli/test_project_set_cloud_local.py`
- `tests/mcp/test_project_context.py`
- `tests/test_project_resolver.py`
- `tests/sync/test_watch_service_reload.py`
### Gaps to close — DONE
- ~~Cloud routing branch gaps in `src/basic_memory/mcp/async_client.py`~~ — expanded `tests/mcp/test_async_client_modes.py`
### Planned additions — DONE
- ~~Add branch-focused tests for all unresolved routing branches in `get_client()`.~~ **DONE**
- Add MCP integration scenario with mixed local/cloud project config — deferred to backlog item 4.
### 4) Project-Prefixed Permalinks + Memory URL Routing (`545804f`) — DONE
### Acceptance criteria
- Project-prefixed permalinks are generated consistently on create/update/import flows.
- Memory URLs resolve to the correct project/entity even with duplicate note titles.
- `read_note`, `search`, `build_context`, write/edit/move flows preserve project identity correctly.
- Link resolution remains correct for context-aware wikilinks.
### Existing coverage anchor points
- `tests/utils/test_permalink_formatting.py`
- `tests/mcp/test_tool_read_note.py`
- `tests/mcp/test_tool_search.py`
- `tests/services/test_context_service.py`
- `test-int/mcp/test_read_note_integration.py`
### Gaps to close
- No major coverage alarm in report, but keep as regression-critical due broad impact surface.
### Planned additions — DONE
- ~~Add one integration test with colliding titles across two projects and assert URL routing invariants.~~ **DONE**`test-int/mcp/test_permalink_collision_integration.py` (2 tests: collision across projects + memory:// URL routing with project prefix)
### 5) MCP UI Variants + TUI Output (`8bc03d1`) — DONE
### Acceptance criteria
- UI resource variant selection (`tool-ui`, `vanilla`, `mcp-ui`) follows env configuration.
- `search_notes` and `read_note` expose expected resource metadata for UI hosts.
- `ascii`/`ansi` outputs are deterministic and stable for terminal clients.
### Existing coverage anchor points
- `tests/mcp/test_tool_contracts.py`
- `test-int/mcp/test_output_format_json_integration.py`
- `test-int/mcp/test_ui_sdk_integration.py`
### Gaps to close — DONE
- ~~`src/basic_memory/mcp/tools/ui_sdk.py` branch coverage~~ — `tests/mcp/test_ui_sdk.py`
- ~~`src/basic_memory/mcp/ui/sdk.py` and `src/basic_memory/mcp/ui/templates.py` branch coverage~~ — `tests/mcp/test_ui_templates.py` + `tests/mcp/test_ui_resources.py`
### Planned additions — DONE
- ~~Add unit tests for UI SDK metadata generation and template selection branches.~~ **DONE** — 31 tests
- ~~Add integration assertion for variant-specific resource URIs and metadata payload shape.~~ **DONE**
### 6) Watch Command (`8df88e4`) — DONE
### Acceptance criteria
- `basic-memory watch` starts and processes create/update/delete events.
- Watch restart/reload path does not duplicate watchers.
- Cloud-mode projects are excluded from active watcher set.
### Existing coverage anchor points
- `tests/cli/test_watch.py`
- `tests/sync/test_coordinator.py`
- `tests/sync/test_watch_service_reload.py`
### Planned additions — DONE
- ~~Add one stress-style integration test for rapid file changes and watcher stability.~~ **DONE**`tests/sync/test_watch_service_stress.py` (3 tests: 50-file batch, mixed add/modify/delete batch, rapid modifications to same file)
### 7) CLI JSON Output (`a47c9c0`) — DONE
### Acceptance criteria
- `--format json` returns valid JSON with stable keys for success paths.
- Error paths also return JSON-shaped output with correct non-zero exits.
- Default human output remains unchanged.
### Existing coverage anchor points
- `tests/cli/test_cli_tool_json_output.py`
- `test-int/cli/test_cli_tool_json_integration.py`
### Planned additions — DONE
- ~~Add one failure-path integration test per high-use tool command.~~ **DONE**`test-int/cli/test_cli_tool_json_failure_integration.py` (4 tests: read-note not found, write-note missing content, write→read roundtrip, recent-activity empty project)
### 8) Search/Edit and Metadata Fixes (`530cbac`, `f1d50c2`, `8838571`, `009e849`) — DONE
### Acceptance criteria
- Metadata filters produce consistent results on SQLite and Postgres.
- `tag:` shorthand works alone and with mixed query terms.
- Fast write/edit paths preserve `external_id` and metadata integrity.
### Existing coverage anchor points
- `tests/repository/test_metadata_filters.py`
- `tests/repository/test_search_repository.py`
- `tests/services/test_search_service.py`
### Planned additions — DONE
- ~~Add Postgres-specific metadata filter edge-case tests to mirror SQLite assertions exactly.~~ **DONE**`tests/repository/test_metadata_filters_edge_cases.py` (6 tests: missing field, AND logic, contains single-element array, nested path missing intermediate, $gte/$lte boundaries, $between inclusive — all pass on both SQLite and Postgres)
### 9) Compatibility and Hotfix Regression Pack (`c46d7a6`, `a0e754b`, `343a6e1`, `24ca5f6`, `e3ced49`, `8489a3d`, `b609c4e`, `f6e0a5b`, `7624a20`)
### Acceptance criteria
- Legacy endpoints required by older CLI versions function without `405` (`GET /projects/projects`, `POST /projects/projects`, `POST /projects/config/sync`).
- Entity creation conflicts map to conflict status (not 500).
- `recent_activity` prompt defaults are correct.
- No spurious `metadata: {}` in serialized frontmatter.
- Tigris/rclone uses global consistency headers for all transaction types.
- `bm --version` fast path avoids heavy import path and remains responsive.
- Default SQLite DB path is isolated by config dir.
### Gaps to close
- ~~Commits with no direct tests added (`c46d7a6`, `344e651`, `f6e0a5b`) need explicit regression tests.~~ **DONE**
### Planned additions — DONE
- ~~Add API compat test covering all legacy endpoint methods and payloads.~~ **DONE**`test_legacy_v1_add_project_endpoint`, `test_legacy_v1_sync_config_endpoint`
- ~~Add CLI fast-path test for `--version` import behavior/performance guard.~~ **DONE**`test_bm_version_does_not_import_heavy_modules`
- ~~Add empty metadata serialization regression test.~~ **DONE**`test_schema_to_markdown_empty_metadata_no_metadata_key`
- Add migration safety test for SQLite generated columns (`VIRTUAL` expectation) — deferred, low risk.
## MCP Manual Verification Plan (LLM Tool Calls)
Run after automated tests pass.
### Setup
- Start MCP server: `basic-memory mcp --transport stdio`
- Use an MCP-capable client and issue tool calls directly.
### Manual scenarios
- Schema: call `schema_validate`, `schema_infer`, and `schema_diff` on known fixtures.
- Schema: verify error and success payloads match acceptance criteria.
- Semantic search: call `search_notes` with `search_type=text|vector|hybrid`.
- Semantic search: verify ranking relevance on semantic fixture queries.
- Routing: call tools with explicit project on mixed local/cloud setup.
- Routing: verify success/failure paths with and without API key.
- Permalink routing: read/write/search notes across projects with colliding titles.
- Permalink routing: verify memory URL routing correctness.
- UI/TUI: call `search_notes` and `read_note` with UI variants and `output_format=text|json`.
- UI/TUI: verify payload/resource format and metadata completeness.
## Implementation Backlog (Ordered)
1. ~~Fill schema MCP/client/router coverage gaps.~~ **DONE** — 18 tests in `test_tool_schema.py` + `test_client_schema.py`
2. ~~Fill semantic search MCP + Postgres repository gaps.~~ **DONE** — 20 tests in `test_postgres_search_repository_unit.py` + `test_tool_search.py`
3. ~~Add compatibility regression tests (legacy endpoints, migration, version fast path).~~ **DONE** — 5 tests across 3 files (see below)
4. ~~Add feature-level integration tests (permalinks, watch, CLI JSON, metadata filters).~~ **DONE** — 15 tests across 4 files (see items 4, 6, 7, 8 above)
5. ~~Expand UI SDK and template branch tests.~~ **DONE** — 31 tests in `test_ui_templates.py` + `test_ui_sdk.py` + `test_ui_resources.py`
6. ~~Run full gate and capture results in a short release readiness summary.~~ **DONE** — see results below
### Full Gate Results (`just check`)
| Phase | Result |
|-------|--------|
| lint | PASS |
| format | PASS |
| typecheck | PASS |
| Unit tests (SQLite) | 1788 passed, 15 skipped |
| Integration tests (SQLite) | 243 passed, 4 skipped, 10 deselected |
| Unit tests (Postgres) | 1760 passed, 28 skipped |
| Integration tests (Postgres) | 234 passed, 13 skipped, 10 deselected |
**0 failures. 10 deselected = semantic benchmark tests (run separately via `just test-semantic`).**
### Item 3 Details — Compatibility Regression Tests
| Test | File | What it covers |
|------|------|----------------|
| `test_legacy_v1_add_project_endpoint` | `tests/api/v2/test_project_router.py` | POST `/projects/projects` legacy route reachable (idempotent path) |
| `test_legacy_v1_sync_config_endpoint` | `tests/api/v2/test_project_router.py` | POST `/projects/config/sync` legacy route reachable |
| `test_bm_version_does_not_import_heavy_modules` | `tests/cli/test_cli_exit.py` | `bm --version` fast path does not load `basic_memory.mcp` |
| `test_schema_to_markdown_empty_metadata_no_metadata_key` | `tests/markdown/test_entity_parser_error_handling.py` | `schema_to_markdown()` with `entity_metadata={}` emits no `metadata:` key |
| `test_legacy_v1_list_projects_endpoint` | `tests/api/v2/test_project_router.py` | (pre-existing) GET `/projects/projects` legacy route |
**Suite totals after item 3: 1764 passed, 15 skipped, 0 failures.**
## Suggested Commands
- Full suite: `just check`
- Fast loop: `just fast-check`
- E2E consistency: `just doctor`
- SQLite focused: `just test-sqlite`
- Postgres focused: `just test-postgres`
- Schema integration: `pytest test-int/test_schema -q`
- Semantic + repo focus: `pytest tests/repository/test_postgres_search_repository.py tests/mcp/test_tool_search.py tests/services/test_semantic_search.py -q`
- MCP integration focus: `pytest test-int/mcp -q`
## Exit Criteria for This Plan
- All feature acceptance criteria above are validated.
- All identified high-priority coverage gaps are addressed or explicitly documented as intentional.
- Manual MCP verification scenarios complete with no P0/P1 findings.
+318
View File
@@ -0,0 +1,318 @@
# v0.19.0 Release Notes
## Overview
v0.19.0 is a major release that introduces semantic vector search, a schema validation system,
project-prefixed permalinks, per-project cloud routing, and a significant upgrade to FastMCP 3.0.
It includes 90+ commits since v0.18.0 spanning new features, architectural improvements, and
stability fixes across both SQLite and Postgres backends.
---
## Major Features
### Semantic Vector Search
Full vector and hybrid search for SQLite (via sqlite-vec) and Postgres (via pgvector).
- **Hybrid search mode** combines full-text search (FTS) with vector similarity for best results
- **Score-based fusion** replaces RRF for hybrid ranking — `max(vec, fts) + 0.3 * min(vec, fts)` preserves dominant signals and rewards dual-source agreement (#577)
- **Default search mode** is now `hybrid` when semantic search is enabled, `text` when disabled
- Embedding providers: FastEmbed (local, default) or OpenAI API
- Configurable similarity threshold via `semantic_min_similarity` (default 0.55)
- Per-query `min_similarity` override on `search_notes` tool
- Auto-backfill: existing entities get embeddings generated on first startup
- Backend-specific distance-to-similarity conversion (cosine for SQLite, inner product for Postgres)
- FTS fallback: if semantic dependencies are missing, search gracefully degrades to text-only
- sqlite-vec knn `k` parameter capped at 4096 to prevent backend errors
**Configuration:**
```json
{
"semantic_search_enabled": true,
"semantic_embedding_provider": "fastembed",
"semantic_embedding_model": "bge-small-en-v1.5",
"semantic_min_similarity": 0.55
}
```
**Usage:**
```
search_notes("machine learning concepts", search_type="hybrid")
search_notes("similar to my notes on coffee", search_type="vector")
search_notes("exact phrase match", search_type="text")
search_notes("broad search", min_similarity=0.3) # lower threshold for more results
```
### Schema System
Validate note structure against user-defined schemas with frontmatter-based rules.
- Define schemas as YAML in note frontmatter with field types, required fields, and constraints
- Frontmatter validation during sync — malformed notes get clear error messages
- Schema inference from existing notes to bootstrap schemas from your content
- Schema diff to compare two schemas and see changes
- Available via MCP tools and CLI
### Project-Prefixed Permalinks
Permalinks now include the project name for unambiguous cross-project references.
- Memory URLs like `memory://project-name/folder/note` route to the correct project
- Existing non-prefixed permalinks continue to work (backwards compatible)
- Controlled by `permalinks_include_project` config (default: true)
- `build_context` and `search_notes` auto-detect project from URL prefix
### Per-Project Cloud Routing
Individual projects can be routed through the cloud while others stay local.
- Set a project to cloud mode: `bm project set-cloud research`
- Revert to local: `bm project set-local research`
- Uses API key authentication: `bm cloud set-key bmc_abc123...`
- MCP tools automatically route based on each project's mode
- Local MCP server (`bm mcp`) still uses local routing for all projects by default
- `--local` and `--cloud` CLI flags override per-command
### Workspace Selection
Cloud projects can target specific workspaces for multi-tenant environments.
- `workspace` parameter on MCP tools for explicit workspace targeting
- CLI workspace-aware project listing with `bm project list`
- Spinner feedback while fetching cloud projects
---
## New Tools and Capabilities
### Dashboard (`bm project info`)
`bm project info` now displays an htop-inspired compact dashboard with:
- Horizontal bar charts for note types (top 5)
- Embedding coverage bar with Unicode block characters
- Colored status dots for at-a-glance health
- `EmbeddingStatus` schema and `get_embedding_status()` service method for programmatic access
### Unified Metadata Search
`search_by_metadata` has been merged into `search_notes` — one tool for all searches.
`query` is now optional, so you can search purely by frontmatter metadata.
```
search_notes(metadata_filters={"status": "in-progress"})
search_notes(metadata_filters={"tags": ["security", "oauth"]})
search_notes(metadata_filters={"priority": {"$in": ["high", "critical"]}})
search_notes(metadata_filters={"schema.confidence": {"$gt": 0.7}})
search_notes(tags=["security"]) # convenience shorthand
search_notes(status="draft") # convenience shorthand
```
### JSON Output Mode
All MCP tools now support `output_format="json"` for machine-readable responses.
- Default remains `"text"` for human-readable output (no breaking changes)
- `build_context` defaults to `"json"` with slimmed payloads (redundant fields stripped)
- CLI tool commands support `--format json` flag
### `tag:` Search Shorthand
Search by tag using convenient shorthand syntax.
```
search_notes("tag:security")
search_notes("tag:coffee AND tag:brewing")
```
### Entity User Tracking
Entities now track `created_by` and `last_updated_by` fields for attribution.
### Improved Search Result Content (#609)
Search results now surface more relevant context:
- `matched_chunk_text` populated for FTS-only hybrid results (no more fallback to truncated content)
- `TOP_CHUNKS_PER_RESULT` increased from 3 to 5, catching answers deeper in large notes (~2700 → ~4500 chars)
- `CONTENT_DISPLAY_LIMIT` doubled from 2000 to 4000 chars for results without matched chunks
### `write_note` Overwrite Guard (#632)
`write_note` is now non-idempotent by default. If a note already exists, the tool returns an
error instead of silently overwriting. Pass `overwrite=True` to replace, or use `edit_note`
for incremental updates. Config option `write_note_overwrite_default` restores the old upsert
behavior.
---
## Architecture Changes
### Score-Based Hybrid Fusion (#577)
RRF (Reciprocal Rank Fusion) compressed all fused scores to ~0.016, destroying ranking
differentiation. The new formula `max(vec, fts) + FUSION_BONUS * min(vec, fts)` preserves
dominant signals and rewards dual-source agreement. Zero-score results now produce zero
fused score instead of receiving a 0.1 weight floor.
### FastMCP 3.0 Upgrade
Upgraded from FastMCP 2.12.3 to 3.0.1.
- Tool annotations (`readOnlyHint`, `openWorldHint`) for better client integration
- Improved MCP protocol compliance
- Better error handling and context management
### Prompts Call MCP Tools Directly
MCP prompts (`search`, `continue_conversation`) now call MCP tools directly instead of
going through API endpoints. This fixes empty results in discovery mode and ensures prompts
use the same resolution logic as tools (including LinkResolver fallback).
### build_context LinkResolver Fallback
`build_context` now falls back to LinkResolver when an exact permalink lookup returns empty.
This uses the same 7-strategy resolution pipeline as `read_note`, so callers no longer get
empty results for valid note identifiers that don't match exact permalinks.
### Sync Handles Semantic Dependency Errors Gracefully
When sqlite-vec or another embedding provider is unavailable, `sync_file` now catches
`SemanticDependenciesMissingError` separately. The entity is created and FTS-indexed
successfully — only vector embeddings are skipped, with a clear warning:
```
WARNING: Semantic search dependencies missing — vector embeddings skipped for path=note.md.
Run 'bm reindex --embeddings' after resolving the dependency issue.
```
### Unified Project Path
Cloud projects with bisync now store the local filesystem path in `path` (not the Docker
container path). Config migration automatically promotes `local_sync_path``path` for
existing configs.
---
## CLI Improvements
### Status and Doctor Default to Local Routing
`bm status` and `bm doctor` now default to local routing since they scan the local filesystem.
Previously, cloud-mode projects would route these commands to the cloud API, which returned
Docker-internal paths that don't exist locally.
### `--format json` for CLI Tool Commands
All `bm tool` subcommands support `--format json` for machine-readable output, enabling
integration with scripts and plugins.
### `--json` for Top-Level CLI Commands
Five additional CLI commands now support `--json` for machine-readable output:
- `bm status --json` — sync report with new/modified/deleted/moved files and skipped files
- `bm project list --json` — structured project list with name, paths, routing mode, and defaults
- `bm schema validate --json` — validation report with per-note pass/fail, warnings, and errors
- `bm schema infer --json` — field frequency analysis and suggested schema definition
- `bm schema diff --json` — drift report with new fields, dropped fields, and cardinality changes
This complements the existing `bm project info --json` and `bm tool --format json` support,
making all major CLI commands scriptable for CI pipelines and automation.
### Cloud Promo and Analytics
- Cloud promo panel shown on first run or version bump with OSS discount code
- Anonymous usage telemetry via Umami Cloud (promo/login funnel events only)
- Opt out with `BASIC_MEMORY_NO_PROMOS=1`
- No PII, no file contents, no per-command tracking
- See [Telemetry](https://github.com/basicmachines-co/basic-memory#telemetry) in README
---
## Bug Fixes
- **#577**: RRF fusion compressed all hybrid scores to ~0.016, destroying ranking differentiation
- **#582**: build_context returns empty results on valid note identifiers
- **#575**: Remove hardcoded "main" default from default_project
- **#595**: recent_activity dedup and pagination across MCP tools
- **#593**: Backend-specific distance-to-similarity conversion
- **#592**: Strip NUL bytes from content before PostgreSQL search indexing
- **#562**: Use VIRTUAL instead of STORED columns in SQLite migration
- **#558**: Add X-Tigris-Consistent headers to all rclone commands
- **#541**: Handle EntityCreationError as conflict
- **#536**: Stabilize metadata filters on Postgres
- **#533**: Fix recent_activity prompt defaults
- **#530**: Prevent spurious `metadata: {}` in frontmatter output
- **#601**: Return matched chunk text in search results
- **#606**: Accept `null` for `expected_replacements` in `edit_note`
- **#579, #607**: Guard against closed streams in promo panel and missing vector tables on shutdown
- **#609**: FTS-only hybrid results missing `matched_chunk_text`; content limits too conservative
- **#631**: `build_context` related_results schema validation failure — replaced fragile `_slim_context()` stripping with Pydantic `exclude=True` field config
- **#630**: Skip workspace resolution when client factory is active — prevents 401 errors in cloud MCP server mode
- **#30**: `tag:` prefix query fails with hybrid search — moved tag prefix parsing to MCP tool level so it works with all search modes
- **#31**: `search_notes` returns cluttered observation/relation-level results — now defaults to entity-level results
- **#28**: `schema_infer` and `schema_diff` return raw Pydantic models as "undefined" in LLM output — added markdown formatters
- Fix `schema_validate` identifier resolution (now uses LinkResolver) and text rendering (markdown formatter)
- **#634**: `schema_validate` and `schema_diff` use stale database metadata instead of reading schema definitions from file — now reads frontmatter directly from the file with fallback to database metadata
- Fix `Post(**metadata)` crash when frontmatter contains `content` or `handler` keys
- Fix list-valued frontmatter fields (`title`, `type`) crashing on `.strip()` — now coerced to strings
- Cap sqlite-vec knn `k` parameter at 4096 to prevent backend errors
- Parameterize SQL queries in search repository type filters
- Double-default display in project list
- `ensure_frontmatter_on_sync` default changed to `True`
- Status/doctor commands fail with cloud-mode projects (Docker path error)
- Prompts return "0 projects" in discovery mode
---
## Security
- Upgrade `cryptography` for CVE advisory
- Upgrade `python-multipart` for security advisory
---
## Internal / Developer
- **#598**: Upgrade FastMCP 2.12.3 → 3.0.1 with tool annotations
- **#594**: Add `ty` as supplemental type checker
- **#538**: Add fast feedback loop tooling (`just fast-check`, `just doctor`, `just testmon`)
- **#600**: Rename `entity_type` to `note_type` for consistency
- **#596**: Fix CLI runtime defects and audit regressions
- CLI refactoring and workspace-aware cloud project listing
- Split and speed up PR test matrix in CI
- Fix CI: collect coverage from test jobs instead of re-running all tests
- Create `search_vector_chunks` in test fixtures for Postgres compatibility
---
## Configuration Changes
| Setting | Old Default | New Default | Notes |
|---------|-------------|-------------|-------|
| `semantic_search_enabled` | `false` | `true` | Semantic search on by default |
| `ensure_frontmatter_on_sync` | `false` | `true` | Frontmatter added during sync |
| `permalinks_include_project` | `false` | `true` | Project prefix in permalinks |
---
## Upgrade Notes
- **Semantic search dependencies** are now included by default. If sqlite-vec fails to load,
search gracefully falls back to FTS. Run `bm reindex --embeddings` to generate embeddings
for existing content.
- **Hybrid search scoring** has changed from RRF to score-based fusion. Search result ordering
may differ — results should be more accurate with better score differentiation.
- **`search_by_metadata`** is removed as a standalone tool. Use `search_notes` with
`metadata_filters` instead (same parameters, same behavior).
- **Project-prefixed permalinks** are enabled by default. Existing notes keep their current
permalinks until modified. Set `permalinks_include_project: false` to disable.
- **Frontmatter on sync** is now enabled by default. Files without frontmatter will have it
added on next sync. Set `ensure_frontmatter_on_sync: false` to preserve old behavior.
- **Config migration** runs automatically for cloud projects with bisync — `local_sync_path`
is promoted to `path` so filesystem operations work correctly.
- **`write_note` is no longer idempotent** — calls to `write_note` for existing notes now
return an error unless `overwrite=True` is passed. Use `edit_note` for incremental changes,
or set `write_note_overwrite_default: true` in config to restore the old behavior.
+209
View File
@@ -0,0 +1,209 @@
# Semantic Search Manual Test Log
## Overview
Manual test session for semantic (vector) search on the main project.
- Date: 2026-02-15
- Database: ~/.basic-memory/memory.db (SQLite)
- Entities: 456 embedded, 2714 vector chunks
- Search index: 2390 FTS entries
- Embedding model: default (384-dim, sqlite-vec)
## Test Plan
1. **Search Type Routing** — verify vector/hybrid/text dispatch, invalid search_type handling
2. **Conceptual Queries** — natural language where vector should beat FTS
3. **Keyword Queries** — exact terms where FTS should be strong
4. **Hybrid Ranking** — queries where both FTS and vector contribute
5. **Result Types** — entities, observations, relations in vector results
6. **Filters + Vector** — combine vector with types/entity_types/after_date
7. **Edge Cases** — short queries, long queries, empty, special chars, no-match
8. **Pagination** — page > 1, page_size respected
---
## Test Results
### Test 1: Search Type Routing
#### 1a: search_type="semantic" (invalid value)
- **Input:** query="how does the knowledge graph work", search_type="semantic"
- **Expected:** error or explicit fallback
- **Actual:** Silently falls through to text search (else branch in search.py:430)
- **Verdict:** BUG — should either be a recognized alias for "vector" or return an error
#### 1b: search_type="vector"
- **Input:** query="keeping AI context between sessions", search_type="vector"
- **Actual:** 5 results, scores ~0.58-0.59, found "Maintaining context across conversation boundaries" observation
- **Verdict:** PASS
#### 1c: search_type="text" with conceptual query
- **Input:** query="keeping AI context between sessions", search_type="text"
- **Actual:** 0 results (no exact keyword match)
- **Verdict:** PASS (expected — FTS requires token overlap)
#### 1d: search_type="hybrid" with conceptual query
- **Input:** query="keeping AI context between sessions", search_type="hybrid"
- **Actual:** 5 results, same ranking as vector (FTS contributed nothing here)
- **Verdict:** PASS
#### 1e: search_type="text" with keyword query
- **Input:** query="OAuth authentication", search_type="text"
- **Actual:** 3 results — AUTH.md Supabase OAuth, OAuth Rip-and-Replace, OAuth Integration Analysis
- **Verdict:** PASS
#### 1f: search_type="vector" with keyword query
- **Input:** query="OAuth authentication", search_type="vector"
- **Actual:** Same top results as text (keyword-rich content also scores well in vector space)
- **Verdict:** PASS
---
### Test 2: Conceptual Queries (vector advantage)
#### 2a: Natural language question
- **Input:** query="why do AI assistants forget things", search_type="vector"
- **Actual:** 5 results — Manual Testing Session, "Balance security and usability" observation, "Tools should match thought patterns" observation. Scores ~0.56-0.57
- **Vector advantage:** Found conceptually related content despite no exact keyword overlap
- **Verdict:** PASS
#### 2b: Same query, text search
- **Input:** query="why do AI assistants forget things", search_type="text"
- **Actual:** 1 result — "What is Basic Memory?" (likely matched on "AI" token)
- **Verdict:** PASS (demonstrates vector advantage — text barely matched)
#### 2c: Domain concept with no jargon
- **Input:** query="pricing strategy for cloud product", search_type="vector"
- **Actual:** 3 results — SPEC-16 MCP Cloud Service Consolidation, knowledge architecture observation, Visual Knowledge Spaces relation. Scores ~0.56-0.57
- **Verdict:** PASS (found cloud-related content conceptually)
#### 2d: Technical concept, long query
- **Input:** query="SQLite performance optimization WAL mode concurrent writes", search_type="vector"
- **Actual:** 3 results — SPEC-11 API Performance Optimization, Real-Time Updates with WebSockets, marketing status update. Scores ~0.55-0.58
- **Verdict:** PASS (found performance-related content)
---
### Test 3: Keyword Queries (FTS strength)
#### 3a: Exact term match — "OAuth authentication"
- **Text:** 3 results with high relevance (exact matches in titles)
- **Vector:** Same top results (keyword overlap helps vector too)
- **Verdict:** PASS — FTS and vector converge on keyword-rich queries
#### 3b: "OAuth" single keyword, hybrid mode
- **Input:** query="OAuth", search_type="hybrid"
- **Actual:** 5 results — Basic Memory Coding Guide, AI Collaboration Examples, SPEC-18, daily note, Manual Testing Session. FTS + vector blended. Scores ~0.016-0.032
- **Note:** Top hybrid result is "Basic Memory Coding Guide" not an OAuth-specific doc — suggests hybrid scoring may dilute strong FTS matches
- **Verdict:** PASS but hybrid ranking questionable for single-keyword queries
---
### Test 4: Hybrid Ranking
#### 4a: Hybrid vs vector on "OAuth authentication"
- **Hybrid with entity_types=["entity"]:** 5 results — RLS Implementation Lessons, Cloud Readiness Assessment, AUTH.md OAuth, Core Service Implementation, OAuth Rip-and-Replace. Scores ~0.016-0.023
- **Vector with entity_types=["entity"]:** 5 results — Core Service Implementation, SPEC-13 CLI Auth, Coding Guide, Authentication Service, ADR Production Auth. Scores ~0.55-0.60
- **Observation:** Hybrid surfaces different top results than vector-only. Hybrid found RLS and Cloud Readiness docs that vector didn't prioritize. Different ranking is expected from RRF fusion.
- **Verdict:** PASS — hybrid produces meaningfully different ranking
---
### Test 5: Result Types
#### 5a: Vector returns all result types
- **Input:** query="keeping AI context between sessions", search_type="vector"
- **Entities:** SPEC-18 AI Memory Management Tool (type=entity)
- **Relations:** Prompt Builder integrates_with (type=relation)
- **Observations:** "Translation layer is key" (type=observation), "Maintaining context across conversation boundaries" (type=observation)
- **Verdict:** PASS — all three types appear in vector results
#### 5b: Observations carry metadata
- **Observation result:** category="challenge", content="Maintaining context across conversation boundaries", from_entity="research/ai-knowledge-management-research"
- **Verdict:** PASS — category, content, from_entity, tags all present
#### 5c: Relations carry link info
- **Relation result:** relation_type="integrates_with", from_entity="development/features/prompt-builder...", to_entity (present but truncated in some)
- **Verdict:** PASS — relation metadata present
---
### Test 6: Filters + Vector Search
#### 6a: entity_types=["entity"] with vector
- **Input:** query="OAuth authentication", search_type="vector", entity_types=["entity"]
- **Actual:** 5 results, all type="entity" (Core Service Implementation, SPEC-13, Coding Guide, Authentication Service, ADR Auth)
- **Verdict:** PASS — filter correctly restricts to entities only
#### 6b: types=["note"] with vector
- **Input:** query="OAuth authentication", search_type="vector", types=["note"]
- **Actual:** Same 5 results (all have entity_type="note" in metadata)
- **Verdict:** PASS — types filter works with vector search
#### 6c: after_date with vector
- **Input:** query="OAuth authentication", search_type="vector", after_date="2025-06-01"
- **Actual:** 3 results — Core Service Implementation, Cloud Web App analysis observation, SPEC-13. Filtered out older OAuth docs.
- **Verdict:** PASS — date filter applied correctly
#### 6d: entity_types=["entity"] with hybrid
- **Input:** query="OAuth authentication", search_type="hybrid", entity_types=["entity"]
- **Actual:** 5 results, all type="entity" — RLS lessons, Cloud Readiness, AUTH.md OAuth, Core Service, OAuth Rip-and-Replace
- **Verdict:** PASS — filter works with hybrid mode too
#### 6e: types=["entity"] with vector (WRONG filter name)
- **Input:** query="OAuth authentication", search_type="vector", types=["entity"]
- **Actual:** 0 results
- **Note:** `types` filters by entity_type metadata (e.g., "note", "person"), NOT by SearchItemType. Using types=["entity"] looks for entity_type="entity" which few/no notes have. This is a UX confusion point — the param names are ambiguous.
- **Verdict:** PASS (correct behavior) but USABILITY ISSUE — easy to confuse types vs entity_types
---
### Test 7: Edge Cases
#### 7a: Single character query
- **Input:** query="x", search_type="vector"
- **Actual:** 3 results — "Self-contained application bundle" observation, Non-Markdown File Support relation, quick-win-tools entity. Scores ~0.57-0.59
- **Note:** Single character still produces an embedding and returns results. Quality is low/random as expected.
- **Verdict:** PASS (no crash, returns results)
#### 7b: Whitespace-only query
- **Input:** query=" ", search_type="vector"
- **Actual:** 0 results
- **Verdict:** PASS (handled gracefully — _check_vector_eligible strips and rejects empty)
#### 7c: Query with no relevant content
- **Input:** query="quantum computing blockchain", search_type="vector"
- **Actual:** 3 results — Inter-Agent Communication relation, Self-contained bundle observation, JSON-LD interop observation. Scores ~0.54
- **Note:** Still returns results because vector search always finds nearest neighbors. Scores are lower (~0.54) than relevant queries (~0.58-0.60). No relevance threshold applied.
- **Verdict:** PASS (expected behavior) but NOTE — no relevance cutoff means irrelevant queries always return something
---
### Test 8: Pagination
#### 8a: Vector search page 2
- **Input:** query="keeping AI context between sessions", search_type="vector", page=2, page_size=3
- **Actual:** 3 results on page 2, current_page=2. Different results from page 1. Top: "Maintaining context across conversation boundaries" observation (score 0.587)
- **Note:** Interestingly, page 2 had a higher-scoring result than some page 1 results. This may indicate pagination doesn't sort globally — it might be paginating within a pre-scored set.
- **Verdict:** PASS (pagination works) but POSSIBLE ISSUE — result ordering across pages needs investigation
---
## Summary
### Passing Tests: 20/21
### Bugs Found
1. **search_type="semantic" silently falls through** (Test 1a) — Invalid search_type values fall to the `else` branch and default to text search without any warning. Should either alias "semantic" to "vector" or raise an error.
### Usability Issues
2. **types vs entity_types confusion** (Test 6e) — `types` filters by entity_type metadata (note, person, etc.) while `entity_types` filters by SearchItemType (entity, observation, relation). The naming is ambiguous and easy to mix up.
3. **No relevance threshold** (Test 7c) — Vector search always returns nearest neighbors even for completely irrelevant queries. Consider adding a minimum score threshold or at least documenting expected score ranges.
4. **Hybrid ranking for single keywords** (Test 3b) — Hybrid mode on simple keyword queries produced less intuitive rankings than pure FTS or pure vector. The RRF fusion may dilute strong FTS signals.
### Observations
- Vector search successfully finds conceptually related content that FTS misses entirely
- Score ranges: relevant queries ~0.56-0.60, irrelevant queries ~0.54 (narrow spread)
- All three result types (entity, observation, relation) appear correctly in vector results
- Filters (entity_types, types, after_date) all work correctly with vector and hybrid modes
- Pagination works but cross-page ordering may need investigation
+271
View File
@@ -0,0 +1,271 @@
# Semantic Search
This guide covers Basic Memory's semantic (vector) search feature, which adds meaning-based retrieval alongside the existing full-text search.
## Overview
Basic Memory's search supports both full-text search (FTS) and semantic retrieval. Semantic search adds vector embeddings that capture the *meaning* of your content, enabling:
- **Paraphrase matching**: Find "authentication flow" when searching for "login process"
- **Conceptual queries**: Search for "ways to improve performance" and find notes about caching, indexing, and optimization
- **Hybrid retrieval**: Combine the precision of keyword search with the recall of semantic similarity
Semantic search is enabled by default when semantic dependencies are available at runtime. It works on both SQLite (local) and Postgres (cloud) backends.
## Installation
Semantic search dependencies (fastembed, sqlite-vec, openai) are included in the default `basic-memory` install.
```bash
pip install basic-memory
```
You can always override with `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true|false`.
### Platform Compatibility
| Platform | FastEmbed (local) | OpenAI (API) |
|---|---|---|
| macOS ARM64 (Apple Silicon) | Yes | Yes |
| macOS x86_64 (Intel Mac) | No — see workaround below | Yes |
| Linux x86_64 | Yes | Yes |
| Linux ARM64 | Yes | Yes |
| Windows x86_64 | Yes | Yes |
#### Intel Mac Workaround
The default install includes FastEmbed, which depends on ONNX Runtime. ONNX Runtime dropped Intel Mac (x86_64) wheels starting in v1.24, so install with a compatible ONNX Runtime pin first:
```bash
pip install basic-memory 'onnxruntime<1.24'
```
After installation, Intel Mac users have two runtime options:
**Option 1: Use OpenAI embeddings (recommended)**
```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=openai
export OPENAI_API_KEY=sk-...
```
**Option 2: Use FastEmbed locally**
Keep the same pinned installation and use FastEmbed (default provider):
```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=fastembed
```
## Quick Start
1. Install Basic Memory:
```bash
pip install basic-memory
```
2. (Optional) Explicitly enable semantic search:
```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
```
3. Build vector embeddings for your existing content:
```bash
bm reindex --embeddings
```
4. Search using semantic modes:
```python
# Pure vector similarity
search_notes("login process", search_type="vector")
# Hybrid: combines FTS precision with vector recall (recommended)
search_notes("login process", search_type="hybrid")
# Explicit full-text search
search_notes("login process", search_type="text")
```
## Configuration Reference
All settings are fields on `BasicMemoryConfig` and can be set via environment variables (prefixed with `BASIC_MEMORY_`).
| Config Field | Env Var | Default | Description |
|---|---|---|---|
| `semantic_search_enabled` | `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED` | Auto (`true` when semantic deps are available) | Enable semantic search. Required before vector/hybrid modes work. |
| `semantic_embedding_provider` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER` | `"fastembed"` | Embedding provider: `"fastembed"` (local) or `"openai"` (API). |
| `semantic_embedding_model` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL` | `"bge-small-en-v1.5"` | Model identifier. Auto-adjusted per provider if left at default. |
| `semantic_embedding_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS` | Auto-detected | Vector dimensions. 384 for FastEmbed, 1536 for OpenAI. Override only if using a non-default model. |
| `semantic_embedding_batch_size` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE` | `64` | Number of texts to embed per batch. |
| `semantic_vector_k` | `BASIC_MEMORY_SEMANTIC_VECTOR_K` | `100` | Candidate count for vector nearest-neighbour retrieval. Higher values improve recall at the cost of latency. |
## Embedding Providers
### FastEmbed (default)
FastEmbed runs entirely locally using ONNX models — no API key, no network calls, no cost.
- **Model**: `BAAI/bge-small-en-v1.5`
- **Dimensions**: 384
- **Tradeoff**: Smaller model, fast inference, good quality for most use cases
```bash
# Install basic-memory and enable semantic search
pip install basic-memory
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
```
### OpenAI
Uses OpenAI's embeddings API for higher-dimensional vectors. Requires an API key.
- **Model**: `text-embedding-3-small`
- **Dimensions**: 1536
- **Tradeoff**: Higher quality embeddings, requires API calls and an OpenAI key
```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=openai
export OPENAI_API_KEY=sk-...
```
When switching from FastEmbed to OpenAI (or vice versa), you must rebuild embeddings since the vector dimensions differ:
```bash
bm reindex --embeddings
```
## Search Modes
### `text` (default)
Full-text keyword search using FTS5 (SQLite) or tsvector (Postgres). Supports boolean operators (`AND`, `OR`, `NOT`), phrase matching, and prefix wildcards.
```python
search_notes("project AND planning", search_type="text")
```
This is the existing default and does not require semantic search to be enabled.
### `vector`
Pure semantic similarity search. Embeds your query and finds the nearest content vectors. Good for conceptual or paraphrase queries where exact keywords may not appear in the content.
```python
search_notes("how to speed up the app", search_type="vector")
```
Returns results ranked by cosine similarity. Individual observations and relations surface as first-class results, not collapsed into parent entities.
### `hybrid`
Combines FTS and vector results using score-based fusion. This is generally the best mode when you want both keyword precision and semantic recall.
```python
search_notes("authentication security", search_type="hybrid")
```
Score-based fusion uses the formula `max(vec, fts) + bonus * min(vec, fts)` to preserve the dominant signal while rewarding results found by both methods.
### When to Use Which
| Mode | Best For |
|---|---|
| `text` | Exact keyword matching, boolean queries, tag/category searches |
| `vector` | Conceptual queries, paraphrase matching, exploratory searches |
| `hybrid` | General-purpose search combining precision and recall |
## The Reindex Command
The `bm reindex` command rebuilds search indexes without dropping the database.
```bash
# Rebuild everything (FTS + embeddings if semantic is enabled)
bm reindex
# Only rebuild vector embeddings
bm reindex --embeddings
# Only rebuild the full-text search index
bm reindex --search
# Target a specific project
bm reindex -p my-project
```
### When You Need to Reindex
- **Upgrade note**: Migration now performs a one-time automatic embedding backfill on upgrade.
- **Manual enable case**: If you explicitly had `semantic_search_enabled=false` and then turn it on
- **Provider change**: After switching between `fastembed` and `openai`
- **Model change**: After changing `semantic_embedding_model`
- **Dimension change**: After changing `semantic_embedding_dimensions`
The reindex command shows progress with embedded/skipped/error counts:
```
Project: main
Building vector embeddings...
✓ Embeddings complete: 142 entities embedded, 0 skipped, 0 errors
Reindex complete!
```
## How It Works
### Chunking
Each entity in the search index is split into semantic chunks before embedding:
- **Headers**: Markdown headers (`#`, `##`, etc.) start new chunks
- **Bullets**: Each bullet item (`-`, `*`) becomes its own chunk for granular fact retrieval
- **Prose sections**: Non-bullet text is merged up to ~900 characters per chunk
- **Long sections**: Oversized content is split with ~120 character overlap to preserve context at boundaries
Each search index item type (entity, observation, relation) is chunked independently, so observations and relations are embeddable as discrete facts.
### Deduplication
Each chunk has a `source_hash` (SHA-256 of the chunk text). On re-sync, unchanged chunks skip re-embedding entirely. This makes incremental updates fast — only modified content triggers API calls or model inference.
### Hybrid Fusion
Hybrid search uses score-based fusion to merge FTS and vector results:
1. Run FTS search to get keyword-ranked results; normalize scores to [0, 1]
2. Run vector search to get similarity-ranked results (already [0, 1])
3. For each result, compute: `fused = max(vec_score, fts_score) + 0.3 * min(vec_score, fts_score)`
4. Sort by fused score
The dominant signal (whichever source scored higher) is preserved, and dual-source agreement adds a bonus. Unlike rank-based fusion, this approach retains score magnitude — a strong vector match stays strong even without an FTS hit.
### Observation-Level Results
Vector and hybrid modes return individual observations and relations as first-class search results, not just parent entities. This means a search for "water temperature for brewing" can surface the specific observation about 205°F without returning the entire "Coffee Brewing Methods" entity.
## Database Backends
### SQLite (local)
- **Vector storage**: [sqlite-vec](https://github.com/asg017/sqlite-vec) virtual table
- **Table creation**: At runtime when semantic search is first used — no migration needed
- **Embedding table**: `search_vector_embeddings` using `vec0(embedding float[N])` where N is the configured dimensions
- **Chunk metadata**: `search_vector_chunks` table stores chunk text, keys, and source hashes
The sqlite-vec extension is loaded per-connection. Vector tables are created lazily on first use.
### Postgres (cloud)
- **Vector storage**: [pgvector](https://github.com/pgvector/pgvector) with HNSW indexing
- **Local Docker**: use `docker-compose-postgres.yml` (`pgvector/pgvector:pg17`). Plain `postgres:17` lacks the extension; run `CREATE EXTENSION IF NOT EXISTS vector;` on any external instance before first migration.
- **Chunk metadata table**: Created via Alembic migration (`search_vector_chunks` with `BIGSERIAL` primary key)
- **Embedding table**: `search_vector_embeddings` created at runtime (dimension-dependent, same pattern as SQLite)
- **Index**: HNSW index on the embedding column for fast approximate nearest-neighbour queries
The Alembic migration creates the dimension-independent chunks table. The embeddings table and HNSW index are deferred to runtime because they depend on the configured vector dimensions.
+225
View File
@@ -0,0 +1,225 @@
# SPEC-LOCAL-PLUS-PUBLISH: Local+ Published Notes and Privacy Tiers
**Status:** Draft
**Date:** 2026-02-14
**Owner:** Basic Memory
## Summary
Add a paid Local+ feature that lets users publish selected notes to shareable URLs while keeping the
main knowledge base local-first. Use this as a product wedge for users who do not want full cloud
hosting but do want collaboration and distribution features.
This spec also captures a practical position on "zero knowledge" for Local+.
## Context
Basic Memory already has strong local-first primitives and optional cloud routing/sync. A recurring
request is:
- keep knowledge local by default,
- pay for selective value-add,
- share specific outputs externally.
Published Notes fits this model: explicit per-note opt-in, reversible, and easy to understand.
## Goals
1. Provide an Obsidian Publish-style sharing experience for selected notes.
2. Keep local markdown files as source of truth.
3. Make sharing compatible with current cloud/auth/billing primitives.
4. Define clear Local+ packaging that does not degrade OSS local workflows.
5. Document zero-knowledge constraints so product decisions are explicit.
## Non-Goals
1. Full hosted editing for all notes (Cloud Full remains separate).
2. Public website builder/CMS features.
3. Strict cryptographic zero-knowledge server processing for MCP/search in v1.
## Local+ Feature Catalog (Sellable)
Core Local+ candidates:
1. Published Notes (share URL, revoke, expiry, password).
2. Snapshot Time Machine (point-in-time restore for local projects).
3. Recovery Drill Reports (automated restore verification).
4. Device/API Key Governance (per-device keys, revocation, audit trail).
5. BYO Storage Orchestration (managed setup for user-owned object storage).
6. Semantic Boost Add-on (higher quality retrieval options while files remain source-of-truth).
Team-oriented add-ons:
1. Team-owned shared links and domain branding.
2. Role-based publish permissions.
3. Shared workspace policies for what can be published.
## Proposed MVP: Published Notes
### User Experience
Per note actions:
1. Publish.
2. Unpublish.
3. Copy URL.
4. Regenerate URL.
5. Set visibility and controls.
Controls:
1. Visibility: `unlisted` (default) or `public`.
2. Optional password gate.
3. Optional expiration datetime.
4. Optional "disable indexing" flag for public mode.
Behavior:
1. Source note remains local markdown.
2. Publish is explicit opt-in per note.
3. Unpublish removes public access immediately.
4. Republish creates a new URL token unless user chooses to keep current URL.
### URL Model
1. Unlisted share URL: high-entropy token path.
2. Public URL: slug path (optional, later phase).
3. Team plans can support custom domain mapping in later phase.
### Content Model
v1 published page includes:
1. Rendered markdown body.
2. Optional metadata (title, updated_at).
v1 excludes:
1. Full graph traversal expansion.
2. Related note auto-discovery on public pages.
### Sync Model
1. Local file remains canonical.
2. Publish stores a rendered snapshot plus metadata in cloud.
3. Update path:
- manual "update published version", or
- optional auto-update on note change (plan-gated).
## Architecture (v1)
### High-Level Flow
1. Client selects a note to publish.
2. Client sends publish request with note identifier and policy.
3. Service resolves note content (local sync artifact or explicit upload payload).
4. Service stores published artifact and returns share URL.
### Data Model
`published_notes`
1. `id` (uuid)
2. `tenant_id` or `workspace_id`
3. `project_id`
4. `entity_permalink` (or stable external_id)
5. `share_token` (hashed in DB)
6. `visibility` (`unlisted`|`public`)
7. `password_hash` (nullable)
8. `expires_at` (nullable)
9. `is_active`
10. `published_content` (rendered snapshot or reference)
11. `published_at`
12. `updated_at`
### API Shape (Draft)
1. `POST /api/published-notes`
2. `GET /api/published-notes`
3. `GET /api/published-notes/{id}`
4. `PATCH /api/published-notes/{id}`
5. `DELETE /api/published-notes/{id}` (unpublish)
6. `POST /api/published-notes/{id}/regenerate-url`
7. `GET /p/{token}` (public resolver)
### CLI Shape (Draft)
1. `bm cloud publish <identifier>`
2. `bm cloud publish list`
3. `bm cloud publish update <id>`
4. `bm cloud publish unpublish <id>`
5. `bm cloud publish rotate-url <id>`
### Security
1. Default to unlisted URLs.
2. Store only hashed share tokens.
3. Passwords hashed server-side.
4. Enforce expiration at request time.
5. Log publish/unpublish/rotate events for auditability.
## Packaging and Pricing Direction
Suggested split:
1. OSS Local: no publish URLs.
2. Local+ Solo: publish URLs + snapshots + recovery.
3. Local+ Team: solo features + team governance and branding.
4. Cloud Full: hosted app + full cloud workflows.
Key message:
"Keep everything local. Publish only what you choose."
## Rollout Plan
1. Phase 1: Unlisted publish URLs + unpublish + regenerate URL.
2. Phase 2: Password/expiry controls.
3. Phase 3: Auto-update on note change and basic analytics.
4. Phase 4: Team branding/domains/policies.
## Zero-Knowledge Position
### Strict Zero-Knowledge Definition
Strict zero-knowledge means the server cannot decrypt note content at all.
### Why This Conflicts with MCP and Search
If server cannot decrypt:
1. MCP tool execution against cloud content cannot read/write semantic content.
2. Full-text search cannot index plaintext content.
3. Semantic/vector search cannot generate or query embeddings on plaintext.
4. Server-side relation resolution and context building become severely limited.
This matches earlier findings: strict zero-knowledge materially handicaps MCP-driven behavior and
search quality.
### Viable Alternatives (Not Strict Zero-Knowledge)
1. Encryption at rest/in transit with server-side decrypt in trusted runtime.
- Preserves MCP/search quality.
- Not zero-knowledge cryptographically.
2. Client-side retrieval mode.
- Keep MCP/search local; cloud is sync/share/backup relay.
- Best for privacy-first users.
- Requires local agent availability for advanced retrieval.
3. Limited encrypted indexing.
- Blind indexes for exact keywords only.
- No high-quality semantic search.
- Usually poor UX for natural-language memory recall.
### Recommendation
For Local+:
1. Do not promise strict zero-knowledge for cloud MCP/search paths.
2. Offer a privacy-first local mode where advanced retrieval stays local.
3. Clearly label tradeoffs:
- "Local private mode" (best privacy, best local retrieval).
- "Cloud-assisted mode" (best cross-device/MCP consistency, trusted-runtime decrypt).
This keeps messaging honest and avoids repeating the known incompatibility.
+368
View File
@@ -0,0 +1,368 @@
# SPEC-SCHEMA-IMPL: Schema System Implementation Plan
**Status:** Draft
**Created:** 2025-02-06
**Branch:** `feature/schema-system`
**Depends on:** [SPEC-SCHEMA](SPEC-SCHEMA.md)
## Overview
Implementation plan for the Basic Memory Schema System. The system is entirely programmatic —
no LLM agent runtime or API key required. The LLM already in the user's session (Claude Code,
Claude Desktop, etc.) provides the intelligence layer by reading schema notes via existing
MCP tools.
## Architecture
```
┌─────────────────────────────────────────────────┐
│ Entry Points │
│ CLI (bm schema ...) │ MCP (schema_validate) │
└──────────┬────────────┴──────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────┐
│ Schema Service Layer │
│ resolve_schema · validate · infer · diff │
└──────────┬────────────────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌────────────────────────┐
│ Picoschema Parser │ │ Note/Entity Access │
│ YAML → SchemaModel │ │ (existing repository) │
└──────────────────────┘ └────────────────────────┘
```
No new database tables. Schemas are notes with `type: schema` — they're already indexed.
Validation reads observations and relations from existing data.
## Components
### 1. Picoschema Parser
**Location:** `src/basic_memory/schema/parser.py`
Parses Picoschema YAML into an internal representation.
```python
@dataclass
class SchemaField:
name: str
type: str # string, integer, number, boolean, any, or EntityName
required: bool # True unless field name ends with ?
is_array: bool # True if (array) notation
is_enum: bool # True if (enum) notation
enum_values: list[str] # Populated for enums
description: str | None # Text after comma
is_entity_ref: bool # True if type is capitalized (entity reference)
children: list[SchemaField] # For (object) types
@dataclass
class SchemaDefinition:
entity: str # The entity type this schema describes
version: int # Schema version
fields: list[SchemaField] # Parsed fields
validation_mode: str # "warn" | "strict" | "off"
frontmatter_fields: list[SchemaField] # From settings.frontmatter (default: [])
def parse_picoschema(yaml_dict: dict) -> list[SchemaField]:
"""Parse a Picoschema YAML dict into a list of SchemaField objects."""
def parse_schema_note(frontmatter: dict) -> SchemaDefinition:
"""Parse a full schema note's frontmatter into a SchemaDefinition."""
```
**Input/Output:**
```yaml
# Input (YAML dict from frontmatter)
schema:
name: string, full name
role?: string, job title
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
```
```python
# Output
[
SchemaField(name="name", type="string", required=True, description="full name", ...),
SchemaField(name="role", type="string", required=False, description="job title", ...),
SchemaField(name="works_at", type="Organization", required=False, is_entity_ref=True, ...),
SchemaField(name="expertise", type="string", required=False, is_array=True, ...),
]
```
### 2. Schema Resolver
**Location:** `src/basic_memory/schema/resolver.py`
Finds the applicable schema for a note using the resolution order.
```python
async def resolve_schema(
note_frontmatter: dict,
search_fn: Callable, # injected search capability
) -> SchemaDefinition | None:
"""Resolve schema for a note.
Resolution order:
1. Inline schema (frontmatter['schema'] is a dict)
2. Explicit reference (frontmatter['schema'] is a string)
3. Implicit by type (frontmatter['type'] → schema note with matching entity)
4. No schema (returns None)
"""
```
### 3. Schema Validator
**Location:** `src/basic_memory/schema/validator.py`
Validates a note's observations and relations against a resolved schema.
```python
@dataclass
class FieldResult:
field: SchemaField
status: str # "present" | "missing" | "type_mismatch"
values: list[str] # Matched observation values or relation targets
message: str | None # Human-readable detail
@dataclass
class ValidationResult:
note_identifier: str
schema_entity: str
passed: bool # True if no errors (warnings are OK)
field_results: list[FieldResult]
unmatched_observations: dict[str, int] # category → count
unmatched_relations: list[str] # relation types not in schema
warnings: list[str]
errors: list[str]
async def validate_note(
note: Note,
schema: SchemaDefinition,
frontmatter: dict | None = None,
) -> ValidationResult:
"""Validate a note against a schema definition.
Mapping rules:
- field: string → observation [field] exists
- field?(array): type → multiple [field] observations
- field?: EntityType → relation 'field [[...]]' exists
- field?(enum): [v] → observation [field] value ∈ enum values
- settings.frontmatter field → frontmatter key presence/value
"""
```
### 4. Schema Inference Engine
**Location:** `src/basic_memory/schema/inference.py`
Analyzes notes of a given type and suggests a schema based on usage frequency.
```python
@dataclass
class FieldFrequency:
name: str
source: str # "observation" | "relation"
count: int # notes containing this field
total: int # total notes analyzed
percentage: float
sample_values: list[str] # representative values
is_array: bool # True if typically appears multiple times per note
target_type: str | None # For relations, the most common target entity type
@dataclass
class InferenceResult:
entity_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequency]
suggested_schema: dict # Ready-to-use Picoschema YAML dict
suggested_required: list[str]
suggested_optional: list[str]
excluded: list[str] # Below threshold
async def infer_schema(
entity_type: str,
notes: list[Note],
required_threshold: float = 0.95, # 95%+ = required
optional_threshold: float = 0.25, # 25%+ = optional
) -> InferenceResult:
"""Analyze notes and suggest a Picoschema definition."""
```
### 5. Schema Diff
**Location:** `src/basic_memory/schema/diff.py`
Compares current note usage against an existing schema definition.
```python
@dataclass
class SchemaDrift:
new_fields: list[FieldFrequency] # Fields not in schema but common in notes
dropped_fields: list[FieldFrequency] # Fields in schema but rare in notes
cardinality_changes: list[str] # one → many or many → one
type_mismatches: list[str] # observation values don't match declared type
async def diff_schema(
schema: SchemaDefinition,
notes: list[Note],
) -> SchemaDrift:
"""Compare a schema against actual note usage to detect drift."""
```
## Entry Points
### CLI Commands
**Location:** `src/basic_memory/cli/schema.py`
```python
import typer
schema_app = typer.Typer(name="schema", help="Schema management commands")
@schema_app.command()
async def validate(
target: str = typer.Argument(None, help="Note path or entity type"),
strict: bool = typer.Option(False, help="Override to strict mode"),
):
"""Validate notes against their schemas."""
@schema_app.command()
async def infer(
entity_type: str = typer.Argument(..., help="Entity type to analyze"),
threshold: float = typer.Option(0.25, help="Minimum frequency for optional fields"),
save: bool = typer.Option(False, help="Save to schema/ directory"),
):
"""Infer schema from existing notes of a type."""
@schema_app.command()
async def diff(
entity_type: str = typer.Argument(..., help="Entity type to diff"),
):
"""Show drift between schema and actual usage."""
```
Registered as subcommand: `bm schema validate`, `bm schema infer`, `bm schema diff`.
### MCP Tools
**Location:** `src/basic_memory/mcp/tools/schema.py`
```python
@mcp_tool
async def schema_validate(
entity_type: str | None = None,
identifier: str | None = None,
project: str | None = None,
) -> str:
"""Validate notes against their resolved schema."""
@mcp_tool
async def schema_infer(
entity_type: str,
threshold: float = 0.25,
project: str | None = None,
) -> str:
"""Analyze existing notes and suggest a schema definition."""
```
### API Endpoints
**Location:** `src/basic_memory/api/schema_router.py`
```python
router = APIRouter(prefix="/schema", tags=["schema"])
@router.post("/validate")
async def validate_schema(...) -> ValidationReport: ...
@router.post("/infer")
async def infer_schema(...) -> InferenceResult: ...
@router.get("/diff/{entity_type}")
async def diff_schema(...) -> SchemaDrift: ...
```
MCP tools call these endpoints via the typed client pattern (consistent with existing
architecture).
## Implementation Phases
### Phase 1: Parser + Resolver
Build the foundation — can parse Picoschema and find schemas for notes.
**Deliverables:**
- `schema/parser.py` — Picoschema YAML → `SchemaDefinition`
- `schema/resolver.py` — Resolution order (inline → explicit ref → implicit by type → none)
- Unit tests for all Picoschema syntax variations
- Unit tests for resolution order
**No external dependencies.** Pure Python parsing of YAML dicts. Can develop and test
in isolation.
### Phase 2: Validator
Connect schemas to notes and produce validation results.
**Deliverables:**
- `schema/validator.py` — Validate note observations/relations against schema fields
- API endpoint: `POST /schema/validate`
- MCP tool: `schema_validate`
- CLI command: `bm schema validate`
- Integration tests with real notes and schemas
**Depends on:** Phase 1 (parser + resolver)
### Phase 3: Inference
Analyze existing notes to suggest schemas.
**Deliverables:**
- `schema/inference.py` — Frequency analysis across notes of a type
- API endpoint: `POST /schema/infer`
- MCP tool: `schema_infer`
- CLI command: `bm schema infer`
- Option to save inferred schema as a note via `write_note`
**Depends on:** Phase 1 (parser for output format)
### Phase 4: Diff
Compare schemas against current usage.
**Deliverables:**
- `schema/diff.py` — Drift detection between schema and actual notes
- API endpoint: `GET /schema/diff/{entity_type}`
- CLI command: `bm schema diff`
**Depends on:** Phase 1 (parser), Phase 3 (inference, for frequency analysis)
## Testing Strategy
- **Unit tests** (`tests/schema/`): Parser edge cases, resolution logic, validation mapping,
inference thresholds
- **Integration tests** (`test-int/schema/`): End-to-end with real markdown files, schema notes
on disk, CLI invocation
- Coverage target: 100% (consistent with project standard)
## What This Does NOT Include
- No new database tables or migrations
- No new markdown syntax (schemas validate existing observations/relations)
- No LLM agent runtime or API key management
- No hook integration (deferred)
- No schema composition/inheritance (deferred)
- No OWL/RDF export (deferred)
- No built-in templates (deferred)
+492
View File
@@ -0,0 +1,492 @@
# SPEC-SCHEMA: Basic Memory Schema System
**Status:** Draft
**Created:** 2025-02-06
**Branch:** `feature/schema-system`
## Summary
A schema system for Basic Memory that uses [Picoschema](https://genkit.dev/docs/dotprompt/)
syntax in YAML frontmatter. Schemas validate notes against their existing observation/relation
structure — no new data model, no migration, just a declarative lens over what's already there.
## Core Principles
1. **Schemas are just notes** — A schema is a note with `type: schema`, lives anywhere
2. **Use prior art** — Picoschema syntax in YAML frontmatter, no custom notation
3. **Validation maps to existing format** — Observations and relations, not a parallel data model
4. **Validation is soft** — Warnings by default, not blocking errors
5. **Inference over prescription** — Schemas describe reality, emerge from usage
6. **No built-in agent** — Programmatic core; the LLM already in the session provides intelligence
## Picoschema Syntax
Picoschema is a compact schema notation from Google's Dotprompt that fits naturally in YAML
frontmatter.
### Supported Types
| Type | Description |
|------|-------------|
| `string` | Text value |
| `integer` | Whole number |
| `number` | Decimal number |
| `boolean` | True/false |
| `any` | Any scalar type |
| `EntityName` | Reference to another entity (capitalized = entity reference) |
### Syntax Rules
```yaml
schema:
name: string, full name # required field with description
email?: string, contact email # ? = optional
role?: string, job title
works_at?: Organization, employer # capitalized type = entity reference
tags?(array): string, categories # array of type
status?(enum): [active, inactive] # enum with allowed values
metadata?(object): # nested object
updated_at?: string
source?: string
```
- `field: type` — required field
- `field?: type` — optional field
- `field(array): type` — array of values
- `field?(enum): [values]` — enumeration
- `field?(object):` — nested object with sub-fields
- `, description` — description after comma
- `EntityName` as type (capitalized) — reference to another entity
## Schema-to-Note Mapping
Schemas validate against the existing Basic Memory note format. No new syntax for note
authors to learn.
### Mapping Rules
| Schema Declaration | Grounded In | Example Match |
|--------------------|-------------|---------------|
| `field: string` | Observation `[field] value` | `- [name] Paul Graham` |
| `field?(array): string` | Multiple `[field]` observations | `- [expertise] Lisp` (×N) |
| `field?: EntityType` | Relation `field [[Target]]` | `- works_at [[Y Combinator]]` |
| `field?(array): EntityType` | Multiple `field` relations | `- authored [[Book]]` (×N) |
| `tags` | Frontmatter `tags` array | `tags: [startups, essays]` |
| `field?(enum): [values]` | Observation `[field] value` where value ∈ set | `- [status] active` |
| `settings.frontmatter` field | Frontmatter key presence/value | `tags: [python, ai]` |
### Key Insight
Schemas don't introduce a new way to store data. They describe the patterns already present
in observations and relations. A note doesn't have to change how it's written — the schema
just says "a good Person note has a `[name]` observation and a `works_at` relation."
## Schema Definition
### As a Dedicated Schema Note
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
email?: string, contact email
role?: string, job title
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
settings:
validation: warn # warn | strict | off
frontmatter:
tags?(array): string, note categories
status?(enum): [draft, review, published]
---
# Person
A human individual in the knowledge graph.
Any documentation about this entity type goes here as prose.
```
Schema notes are regular Basic Memory notes. They show up in search, can have their own
observations and relations, and can be organized in any folder (though `schema/` is
the suggested convention).
### Inline Schema in a Note
Notes can carry their own schema directly:
```yaml
# meetings/2024-01-15-standup.md
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
# Team Standup 2024-01-15
## Observations
- [attendees] Paul
- [attendees] Sarah
- [decisions] Ship v2 by Friday
- [action_items] Paul to review PR #42
- [blockers] Waiting on API credentials
```
Good for one-off structured notes or prototyping a schema before extracting it.
### Explicit Schema Reference
A note can reference a schema by entity name or permalink:
```yaml
# projects/basic-memory.md
---
title: Basic Memory
schema: SoftwareProject # by entity name
---
# research/llm-memory-patterns.md
---
title: LLM Memory Patterns
schema: schema/research-project # by permalink
---
```
Use cases:
- Note's `type` differs from the schema it should validate against
- Multiple schema variants exist for the same domain
- Applying structure to existing notes without changing their type
## Schema Resolution
When validating a note, schemas resolve in priority order:
```
1. Inline schema → schema: { ... } (dict in frontmatter)
2. Explicit ref → schema: Person (string in frontmatter)
3. Implicit by type → type: Person (lookup schema note with entity: Person)
4. No schema → no validation (perfectly fine)
```
```python
async def resolve_schema(note: Note) -> Schema | None:
schema_value = note.frontmatter.get('schema')
# 1. Inline schema (dict)
if isinstance(schema_value, dict):
return parse_picoschema(schema_value)
# 2. Explicit reference (string)
if isinstance(schema_value, str):
schema_note = await find_schema_note(schema_value)
if schema_note:
return parse_picoschema(schema_note.frontmatter['schema'])
# 3. Implicit by type
note_type = note.frontmatter.get('type')
if note_type:
results = await search_notes(f"type:schema entity:{note_type}")
if results:
return parse_picoschema(results[0].frontmatter['schema'])
# 4. No schema
return None
```
## Validation
### Modes
Configured in the schema's `settings.validation`:
| Mode | Behavior |
|------|----------|
| `off` | No validation |
| `warn` | Warnings in output, doesn't block (default) |
| `strict` | Errors that block sync, for CI/CD enforcement |
### Validation Output
For a note missing required fields:
```
$ bm schema validate people/ada-lovelace.md
⚠ Person schema validation:
- Missing required field: name (expected [name] observation)
- Missing optional field: role
- Missing optional field: works_at (no relation found)
Unmatched observations: [fact] ×2, [born] ×1
Unmatched relations: collaborated_with
```
"Unmatched" items are informational — observations and relations the schema doesn't cover.
They're valid. Schemas are a subset, not a straitjacket.
### Frontmatter Validation
Schema notes can declare validation rules for frontmatter keys under `settings.frontmatter`
using the same Picoschema syntax as the `schema` block:
```yaml
settings:
validation: warn
frontmatter:
tags?(array): string
status?(enum): [draft, review, published]
```
- Frontmatter rules use the same Picoschema key syntax (`?` for optional, `(enum)`, `(array)`)
- Only available on schema notes (inline schemas skip frontmatter validation)
- Checks key presence (required vs optional) and enum value membership
- Unmatched frontmatter keys not in the schema are silently ignored
- Missing required frontmatter keys produce a warning (or error in strict mode)
Example output for a missing required frontmatter key:
```
⚠ Person schema validation:
- Missing required frontmatter key: status
```
### Batch Validation
```
$ bm schema validate Person
Validating 30 notes against Person schema...
✓ people/paul-graham.md — all fields present
✓ people/rich-hickey.md — all fields present
⚠ people/ada-lovelace.md — missing: name
⚠ people/alan-kay.md — missing: name, role
✓ people/linus-torvalds.md — all fields present
...
Summary: 22/30 valid, 8 warnings, 0 errors
```
## Emerging Schemas
### The Problem with Traditional Schemas
Most schema systems require: define schema → create conforming content → fight the schema
when reality doesn't match. This is backwards. Knowledge grows organically.
### The Basic Memory Approach
```
Write notes freely → Patterns emerge → Crystallize into schema → Validate future notes
```
### Schema Inference
Generate schemas from existing notes by analyzing observation and relation frequency:
```
$ bm schema infer Person
Analyzing 30 notes with type: Person...
Observations found:
[name] 30/30 100% → name: string
[role] 27/30 90% → role?: string
[fact] 25/30 83% (generic — no single field)
[expertise] 18/30 60% → expertise?(array): string
[email] 8/30 27% → email?: string
[born] 6/30 20% (below threshold)
Relations found:
works_at 22/30 73% → works_at?: Organization
authored 11/30 37% → authored?(array): string
Suggested schema:
name: string, full name
role?: string, job title
expertise?(array): string, areas of knowledge
email?: string, contact email
works_at?: Organization, employer
Save to schema/Person.md? [y/n]
```
Frequency thresholds:
- 100% present → required field
- 25%+ present → optional field
- Below 25% → excluded from suggestion (but noted)
### Schema Drift Detection
Track how usage patterns shift over time:
```
$ bm schema diff Person
Schema drift detected:
+ expertise: now in 81% of notes (was 12%)
- department: dropped to 3% of notes
~ works_at: cardinality changed (one → many)
Update schema? [y/n/review]
```
## LLM Integration (AI Guidance)
No agent runtime or API key required. The LLM already in the session uses schemas as
context for note creation.
### Flow
1. User asks LLM to "write a note about Rich Hickey"
2. LLM determines `type: Person` is appropriate
3. LLM calls `search_notes("type:schema entity:Person")` → finds schema
4. LLM reads schema fields: required `name`, optional `role`, `works_at`, `expertise`
5. LLM calls `write_note` with observations and relations that satisfy the schema
The schema acts as a creation template. The LLM knows what a "complete" note looks like
without any custom agent infrastructure.
### MCP Tools
```python
@mcp_tool
async def schema_validate(
entity_type: str | None = None,
identifier: str | None = None,
project: str | None = None,
) -> ValidationReport:
"""Validate notes against their resolved schema.
Validates a specific note (by identifier) or all notes of a given type.
Returns warnings/errors based on the schema's validation mode.
"""
@mcp_tool
async def schema_infer(
entity_type: str,
threshold: float = 0.25,
project: str | None = None,
) -> SuggestedSchema:
"""Analyze existing notes and suggest a schema definition.
Examines observation categories and relation types across all notes
of the given type. Returns frequency analysis and suggested Picoschema.
"""
```
## CLI Commands
```bash
# Validate a specific note
bm schema validate people/ada-lovelace.md
# Validate all notes of a type
bm schema validate Person
# Validate everything with a schema
bm schema validate
# Infer schema from existing notes
bm schema infer Person
# Show schema drift from current definition
bm schema diff Person
# List all schema notes
bm search "type:schema"
```
## Examples
### Complete Person Workflow
**Schema:**
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
**Valid note:**
```yaml
# people/paul-graham.md
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
---
# Paul Graham
## Observations
- [name] Paul Graham
- [role] Essayist and investor
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
- [fact] Created Viaweb, the first web app
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
```
**Note with warnings:**
```yaml
# people/ada-lovelace.md
---
title: Ada Lovelace
type: Person
---
# Ada Lovelace
## Observations
- [fact] Wrote the first computer program
- [born] 1815
## Relations
- collaborated_with [[Charles Babbage]]
```
Validation: warns about missing required `[name]` observation. Everything else is optional
or unmatched (which is fine).
## Future Considerations (Deferred)
These are interesting but out of scope for the initial implementation:
- **Multiple schema inheritance** — `schema: [Person, Author]`
- **Hook integration** — Pre-write validation via the hooks system
- **OWL/RDF export** — `bm schema export --format owl`
- **SPARQL queries** — Schema-aware graph queries
- **Built-in templates** — `bm schema use gtd`, `bm schema use zettelkasten`
- **Schema versioning/migration** — Tracking breaking changes across versions
+28
View File
@@ -0,0 +1,28 @@
## Coverage policy (practical 100%)
Basic Memorys test suite intentionally mixes:
- unit tests (fast, deterministic)
- integration tests (real filesystem + real DB via `test-int/`)
To keep the default CI signal **stable and meaningful**, the default `pytest` coverage report targets **core library logic** and **excludes** a small set of modules that are either:
- highly environment-dependent (OS/DB tuning)
- inherently interactive (CLI)
- background-task orchestration (watchers/sync runners)
### What's excluded (and why)
Coverage excludes are configured in `pyproject.toml` under `[tool.coverage.report].omit`.
Current exclusions include:
- `src/basic_memory/cli/**`: interactive wrappers; behavior is validated via higher-level tests and smoke tests.
- `src/basic_memory/db.py`: platform/backend tuning paths (SQLite/Postgres/Windows), covered by integration tests and targeted runs.
- `src/basic_memory/services/initialization.py`: startup orchestration/background tasks; covered indirectly by app/MCP entrypoints.
- `src/basic_memory/sync/sync_service.py`: heavy filesystem↔DB integration; validated in integration suite (not enforced in unit coverage).
### Recommended additional runs
If you want extra confidence locally/CI:
- **Postgres backend**: run tests with `BASIC_MEMORY_TEST_POSTGRES=1`.
- **Strict backend-complete coverage**: run coverage on SQLite + Postgres and combine the results (recommended).
+9
View File
@@ -0,0 +1,9 @@
[run]
source = .
omit =
tests/*
tests/**/*
[report]
fail_under = 85
show_missing = True
+48
View File
@@ -0,0 +1,48 @@
name: integration
# Heavier than test.yml — installs the real `basic-memory` CLI via uv, runs
# every bm_* tool against a live `bm mcp` subprocess. Catches BM-API drift
# (e.g., a bm release renaming a tool argument) before our users see it.
concurrency:
group: hbm-integration-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
workflow_dispatch:
jobs:
integration:
name: Integration tests (real bm + mcp)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install uv
# No cache (no uv.lock to key off — see test.yml comment).
uses: astral-sh/setup-uv@v3
- name: Set up Python 3.12
# basic-memory itself requires 3.12+; the bm install needs that.
# Hermes-runtime compatibility (3.11) is covered by test.yml.
run: uv python install 3.12
- name: Install basic-memory CLI via uv
run: |
uv tool install basic-memory
# uv puts entry-point shims under ~/.local/bin
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Verify bm is on PATH
run: |
which bm
bm --version
- name: Run integration tests
env:
BM_INTEGRATION: "1"
run: |
uv run --with pytest --with mcp --python 3.12 pytest tests/test_integration.py -v
+36
View File
@@ -0,0 +1,36 @@
name: pr-title
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
semantic-pr-title:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Conventional-commit types we accept in PR titles + commit subjects.
types: |
feat
fix
chore
docs
style
refactor
perf
test
build
ci
# Single-file plugin — no real submodule structure. We don't require
# a scope, but if a contributor uses one we accept these:
scopes: |
core
tests
ci
docs
deps
requireScope: false
requireScopeForBreakingChange: true
+194
View File
@@ -0,0 +1,194 @@
name: release
# Manual trigger: Actions → release → Run workflow. Always runs against main
# (the workflow validates GITHUB_REF). Steps:
# 1. Compute the new version from the `version` input (patch/minor/major
# or explicit semver). The *current* version is read from the latest
# git tag (`v*.*.*`) — NOT from __init__.py. This is robust to PRs that
# pre-bump __init__.py: the bump always runs from the last released
# version, not from whatever the working files happen to say.
# 2. Update __version__ in __init__.py and version in plugin.yaml to match
# the new tag — bringing the files in sync if a PR pre-bumped them.
# 3. Commit as `chore(release): vX.Y.Z`, tag, push to main + push the tag.
# 4. Publish a GitHub Release. Body is the matching `## [X.Y.Z]` block from
# CHANGELOG.md when present; otherwise auto-generated release notes.
#
# Recommended flow: land a PR that adds a `## [X.Y.Z]` section to CHANGELOG.md
# first, then run this workflow with the matching version so the release notes
# are the hand-written changelog instead of commit-message-derived notes.
on:
workflow_dispatch:
inputs:
version:
description: "Version bump (`patch`, `minor`, `major`) or explicit semver (`0.3.0`)"
required: true
default: "patch"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
name: Tag and Publish GitHub Release
runs-on: ubuntu-latest
steps:
- name: Validate trigger is main
run: |
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "::error::release must run against main. Got $GITHUB_REF"
exit 1
fi
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Compute new version
id: bump
run: |
set -euo pipefail
VERSION_INPUT="${{ github.event.inputs.version }}"
# Current = latest released tag (sort by version, descending; pick
# the first v*.*.* tag). NOT __init__.py — a PR may have pre-bumped
# the version files, and we don't want to double-bump on top of
# that. The bump is computed from the last *released* version.
LAST_TAG=$(git tag --list 'v*.*.*' --sort=-v:refname | head -1 || true)
if [ -z "$LAST_TAG" ]; then
# First release in the repo. Seed with 0.0.0 so a `patch` bump
# yields v0.0.1, `minor` yields v0.1.0, `major` yields v1.0.0.
# Explicit semver inputs bypass the seed entirely.
CURRENT="0.0.0"
echo "No prior v*.*.* tag found; seeding current=0.0.0"
else
CURRENT="${LAST_TAG#v}"
echo "Latest released tag: $LAST_TAG (current=$CURRENT)"
fi
if [[ "$VERSION_INPUT" =~ ^(patch|minor|major)$ ]]; then
IFS=. read -r MAJ MIN PAT <<< "$CURRENT"
case "$VERSION_INPUT" in
major) MAJ=$((MAJ + 1)); MIN=0; PAT=0 ;;
minor) MIN=$((MIN + 1)); PAT=0 ;;
patch) PAT=$((PAT + 1)) ;;
esac
NEW="${MAJ}.${MIN}.${PAT}"
elif [[ "$VERSION_INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
NEW="$VERSION_INPUT"
else
echo "::error::Invalid version input: '$VERSION_INPUT'"
echo "::error::Use patch|minor|major or explicit X.Y.Z"
exit 1
fi
if [ "$NEW" = "$CURRENT" ]; then
echo "::error::New version equals last released ($NEW). Pick a different version."
exit 1
fi
# Sanity check: refuse if __init__.py is already ahead of the
# version we're about to ship. Catches the "PR bumped to 0.5.0 but
# workflow was asked for a patch that would land 0.1.8" foot-gun
# before it overwrites the files.
FILE_VERSION=$(grep -E '^__version__ = ' __init__.py \
| sed -E 's/^__version__ = "([^"]+)".*/\1/')
if [ -n "$FILE_VERSION" ] && [ "$FILE_VERSION" != "$CURRENT" ] && [ "$FILE_VERSION" != "$NEW" ]; then
echo "::error::__init__.py reports version $FILE_VERSION, but the release would ship $NEW (last tag: $CURRENT)."
echo "::error::Reconcile by picking a version input that matches __init__.py, or roll __init__.py back to $CURRENT."
exit 1
fi
echo "New version: $NEW"
echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "version=$NEW" >> "$GITHUB_OUTPUT"
echo "tag=v$NEW" >> "$GITHUB_OUTPUT"
- name: Refuse if tag already exists
run: |
set -euo pipefail
TAG="${{ steps.bump.outputs.tag }}"
if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists locally. Pick a different version."
exit 1
fi
if git ls-remote --tags origin "$TAG" | grep -q "refs/tags/$TAG$"; then
echo "::error::Tag $TAG already exists on origin. Pick a different version."
exit 1
fi
- name: Update version files
run: |
set -euo pipefail
NEW="${{ steps.bump.outputs.version }}"
sed -i -E "s/^__version__ = \"[^\"]+\"/__version__ = \"${NEW}\"/" __init__.py
sed -i -E "s/^version: .*/version: ${NEW}/" plugin.yaml
# Verify both files changed and that the new version is present.
grep -q "^__version__ = \"${NEW}\"" __init__.py
grep -q "^version: ${NEW}$" plugin.yaml
echo "--- diff ---"
git --no-pager diff -- __init__.py plugin.yaml
- name: Configure Git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Commit and tag
run: |
set -euo pipefail
TAG="${{ steps.bump.outputs.tag }}"
git add __init__.py plugin.yaml
if git diff --cached --quiet; then
# PR already bumped the files to the target version. Tag the
# existing HEAD rather than creating an empty release commit.
echo "Version files already at ${TAG}; tagging current HEAD."
else
git commit -m "chore(release): ${TAG}"
fi
git tag -a "${TAG}" -m "${TAG}"
- name: Push commit and tag
run: |
set -euo pipefail
# HEAD push is a no-op when nothing was committed in this run.
git push origin HEAD:main
git push origin "${{ steps.bump.outputs.tag }}"
- name: Extract CHANGELOG section for this version
id: changelog
run: |
set -euo pipefail
VERSION="${{ steps.bump.outputs.version }}"
# Pull lines between `## [VERSION]` and the next `## [` heading.
SECTION=$(awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { found=1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md)
if [ -z "$SECTION" ]; then
echo "::warning::No CHANGELOG.md section found for v${VERSION} — falling back to auto-generated release notes."
echo "has_section=false" >> "$GITHUB_OUTPUT"
else
echo "has_section=true" >> "$GITHUB_OUTPUT"
{
echo 'body<<EOF_CHANGELOG'
echo "$SECTION"
echo 'EOF_CHANGELOG'
} >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.bump.outputs.tag }}
name: ${{ steps.bump.outputs.tag }}
body: ${{ steps.changelog.outputs.body }}
generate_release_notes: ${{ steps.changelog.outputs.has_section == 'false' }}
token: ${{ secrets.GITHUB_TOKEN }}
+39
View File
@@ -0,0 +1,39 @@
name: tests
# Cancel an in-progress run when a new commit lands on the same branch — the
# latest result is the one we care about.
concurrency:
group: hbm-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
# Branch pushes already cover PRs (the PR branch tip is what's being tested),
# so we don't run the matrix twice for the same commit on push + pull_request.
push:
jobs:
unit:
name: Unit tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
# 3.11 is the runtime Hermes itself ships on today; 3.12-3.14 cover
# forward-compat for whenever Hermes upgrades.
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install uv
# No `enable-cache: true` — the action's default cache key globs for
# `uv.lock`, which we don't ship (we use `uv run --with` instead of
# `uv sync`). Without a lock file the cache step errors out.
uses: astral-sh/setup-uv@v3
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Run unit tests
run: uv run --with pytest --python ${{ matrix.python-version }} pytest -q
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.pyc
.venv/
.DS_Store
.pytest_cache/
*.egg-info/
+133
View File
@@ -0,0 +1,133 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.2] — 2026-05-23
### Fixed
- **Let Basic Memory v0.21.3 self-route workspace-qualified identifiers and URLs.** Hermes no longer injects its configured default project into `bm_read`, `bm_edit`, `bm_delete`, `bm_move`, or `bm_context` calls when the identifier/URL is already workspace-qualified, such as `personal/main/...`, `memory://personal/main/...`, or an organization workspace slug with a 32-character hash suffix. This preserves Basic Memory Cloud's workspace-aware routing while keeping the existing default-project behavior for short/local identifiers.
## [0.3.1] — 2026-05-16
### Changed
- **Documented the Hermes Agent v0.14.0-compatible `/bm-*` slash-command monkeypatch.** `MONKEYPATCH.md` now distinguishes the plugin's runtime version from the Hermes Agent-side compatibility patch: plugin `v0.3.0` remains the correct runtime release for Hermes Agent `v0.13.x`, while Hermes Agent `v0.14.0` still needs the updated two-part core patch so gateway startup command discovery loads the active exclusive memory provider and the memory-provider collector delegates `register_command` / `register_skill`.
- **Clarified install guidance for users and agents.** The README known-issue section now points Hermes `v0.13.x` and `v0.14.0` users at the compatibility matrix in `MONKEYPATCH.md`, so agents do not mistake a plugin update for the required Hermes Agent core patch.
### Notes
- This is a documentation/compatibility-instructions release only. It does not change the plugin runtime code or Basic Memory data behavior. The plugin remains backward-compatible with Hermes Agent `v0.13.x`; the new documentation explains how to patch Hermes Agent `v0.14.0` until the upstream Hermes fix ships.
## [0.3.0] — 2026-05-12
### Added
- **Per-call project routing on every `bm_*` tool.** All eight tools now accept optional `project` (name) and `project_id` (UUID from `bm_projects`) parameters. The agent can write or read against a project other than the Hermes-configured one — useful when the user asks to write into a different cloud project (e.g. a personal `main` project) without reconfiguring the plugin. `project_id` takes precedence over `project`; both fall back to the configured default when omitted. Workspace routing is handled transparently by BM via `project_id` — no separate workspace parameter is needed.
- **`bm_projects` and `bm_workspaces` agent tools.** Promotes the discovery logic previously available only as `/bm-project` and `/bm-workspace` slash commands to agent-facing tools. `bm_projects` returns JSON with `name` and `external_id` (UUID) per project so the agent can hand the UUID to `bm_write` / `bm_read` / etc. via `project_id` — the unambiguous form across cloud workspaces. `bm_workspaces` lists BM Cloud workspaces (name, type, role, default flag). Together with per-call routing, these unblock the workflow Drew's friction note flagged: agent picks the right project + workspace before writing, instead of silently operating against the active Hermes memory project.
- **SKILL.md cross-project workflow** documenting the discovery → route → write → verify recipe end-to-end. Adds a "Permalinks" section covering the three canonical shapes (short, project-qualified, workspace-qualified) and the round-trip property where `bm_write`'s returned permalink self-routes for follow-up reads. A "Cross-project routing" section explains `project` (including workspace-qualified syntax like `"personal/main"`) vs `project_id` and when to use each. Also backfills `bm_recent` documentation (the tool shipped in 0.2.0 but the skill hadn't been updated).
- **SKILL.md "Further reading" section** linking to the official docs at [docs.basicmemory.com](https://docs.basicmemory.com), with raw-markdown URLs (`/raw/<path>.md`) the agent can `WebFetch` on demand for deeper material — knowledge format, observations & relations, memory URL wildcards, semantic search, cloud routing, BM's full MCP tool surface, and the `llms.txt` sitemap.
### Notes
- Addresses the routing, discovery, and documentation gaps in the real-world note "Hermes Basic Memory Cloud Task Experience." A proposed `bm_import` tool was evaluated and dropped — `read_file` + `bm_write` already composes the same operation with no new capability, at the cost of one more tool in the surface.
- The slash commands `/bm-project` and `/bm-workspace` still exist and behave identically — they continue to call `list_memory_projects` / `list_workspaces` directly via the actor. No behavior change for human use.
## [0.2.0] — 2026-05-11
### Added
- **Plugin-owned `/bm-*` slash commands** for CLI/gateway sessions. Eight commands give humans direct memory-graph access without going through the agent: `/bm-search`, `/bm-read`, `/bm-context`, `/bm-recent`, `/bm-status`, `/bm-remember`, `/bm-project`, `/bm-workspace`. Closes #2.
- **`bm_recent` tool** wrapping BM's `recent_activity`. Surfaces notes updated within a timeframe (`7d` default, accepts natural language like `"2 weeks"` or `"yesterday"`). Agent-facing and reused by `/bm-recent`.
- **`remember_folder` config key** (default `"bm-remember"`). Separate from `capture_folder` so manual captures via `/bm-remember` don't intermix with auto-generated session transcripts. Notes are tagged `manual-capture` for further disambiguation.
### Fixed
- **`ctx.register_skill(...)` was silently no-opping since 0.1.5** in real Hermes installs. Hermes loads memory-provider plugins through a stripped-down `_ProviderCollector` context (`plugins/memory/__init__.py`) that captures only `register_memory_provider`; `register_skill` and `register_command` are not delegated. The plugin now writes directly to `PluginManager._plugin_commands` and `_plugin_skills`, matching the entry shape and name normalization `PluginContext.register_command` / `register_skill` produce. This makes both the new slash commands and the bundled SKILL.md work in current Hermes installs. The clean fix lives upstream — a small patch to teach `_ProviderCollector` to delegate — and once that lands, the reach-in becomes a redundant double-write of identical entries. Forward-compat `ctx.register_command` / `ctx.register_skill` calls remain in place for the future code path.
### Notes
- `/bm-remember` derives the title from the first non-empty line of the input, trimmed to 80 chars; falls back to `Note YYYY-MM-DD HHMM UTC`.
- `/bm-workspace` short-circuits in local mode with a one-line explanation. Workspaces are a BM Cloud concept.
- Mid-session project/workspace switching is intentionally not supported in 0.2.0 — auto-capture would land in unexpected places. Tracked as a follow-up.
## [0.1.7] — 2026-05-10
### Changed
- **Stronger nudge in `system_prompt_block()`** to steer agents toward the `bm_*` tools instead of shelling out to `bm` CLI. Pre-v0.1.7 the prompt listed the tools neutrally; given Claude/Hermes models' heavy training-data exposure to `bm tool ...` CLI patterns, neutral language wasn't enough — agents reached for the shell by reflex, paying 1-2s of cold-start per call instead of ~0.1s through our persistent MCP connection. New prompt is explicit (**"Use the `bm_*` tools below directly — do not shell out to the `bm` CLI"**) and gives a one-line latency rationale so the model has a reason to follow it.
- `SKILL.md` mirrors the directive with a "Use `bm_*`, not the `bm` CLI" section + a tool-vs-CLI table.
### Added
- Regression test `test_system_prompt_block_steers_away_from_cli` locks in the directive language so future prompt edits don't accidentally weaken it.
## [0.1.6] — 2026-05-10
### Fixed
- **`bm_*` tools were never registered with Hermes's `MemoryManager._tool_to_provider`.** `get_tool_schemas()` was gated on `self._initialized`, but Hermes captures the schema list at *register* time — before `initialize()` runs. The gate caused every session to start with zero tools registered for our provider, so every LLM-issued `bm_search` (and friends) returned `"Unknown tool: bm_search"` from MemoryManager's dispatch. Symptoms were asymmetric: prefetch (recall injection) worked because it's invoked per-turn after init, but tool calls didn't. Schemas are static — they now return unconditionally, with `handle_tool_call()` doing the runtime "is the actor ready?" gate.
- Regression test pins this so we don't reintroduce it: `test_get_tool_schemas_unconditional` asserts `get_tool_schemas()` returns all 7 schemas on a fresh, uninitialized provider.
## [0.1.5] — 2026-05-10
### Added
- Bundled `SKILL.md` is now auto-registered via `ctx.register_skill("basic-memory", ...)` during plugin load. No more manual symlink to `~/.hermes/skills/`. The skill is opt-in (resolvable via `skill:view basic-memory:basic-memory`); always-on agent guidance still flows through `system_prompt_block()`.
### Changed
- README rewritten for community install. Lead command is now `hermes plugins install basicmachines-co/hermes-basic-memory`. Clone-and-symlink instructions moved to the Development section.
- Added GitHub Actions CI: unit tests on push and PR.
- Added this CHANGELOG.
## [0.1.4] — 2026-05-10
### Added
- `_uv_binary_path()` and `_install_bm_via_uv()`. When `bm` is missing from the host, the plugin runs `uv tool install basic-memory --quiet` once at first `initialize()`. The bm binary lands at `~/.local/bin/bm` — the same canonical path a manual `uv tool install basic-memory` produces, so subsequent manual installs are no-ops rather than creating a second install.
- 8 new unit tests covering `is_available()` with bm/uv combinations, the install subprocess (success / non-zero exit / OSError / no-uv), and `initialize()` install-or-not branching.
### Changed
- `is_available()` now returns `True` when **either** `bm` is on disk **or** `uv` is on disk (we can install the missing CLI ourselves).
- README's prerequisites section: dropped manual basic-memory install requirement; added the one-time ~10s cold-start note.
## [0.1.3] — 2026-05-10
### Fixed
- README's cloud-mode section described the wrong setup (`bm project add ... --cloud --local-path` + `bm cloud bisync`), which gives a local-mode project with file-level cloud sync rather than true cloud routing. Replaced with `bm project set-cloud <name> --workspace <name>`, which flips the project to `ProjectMode.CLOUD` so tool calls route over HTTPS to `<cloud_host>/proxy` directly. No local files involved.
- Documented OAuth / API-key auth options, and the `--workspace` requirement when the user belongs to multiple BM Cloud workspaces.
## [0.1.2] — 2026-05-10
### Changed
- `_default_project()`: `"hermes-memory"` (was `"hermes-{hostname}"`).
- `_default_project_path()`: `~/hermes-memory/` (was `~/.basic-memory/hermes/`). The previous path violated the principle that `~/.basic-memory/` is reserved for BM's app state, not project storage.
### Added
- `_bm_known_projects()` reads bm's `~/.basic-memory/config.json`. `BasicMemoryProvider._verify_project_registered()` uses it to refuse initialization when `mode: cloud` is set against a project that isn't registered with bm. Local mode still auto-creates as before.
- 13 new unit tests for the introspection + bail-out paths.
## [0.1.1] — 2026-05-10
### Added
- `tests/test_actor.py` — 15 tests covering `_BmMcpActor` lifecycle, call dispatch, timeout-with-cancellation, idempotent shutdown.
- `tests/test_capture.py` — 25 tests for `sync_turn` (first-write + append paths), `on_session_end` summary shape, and gating.
- `tests/test_prefetch.py` — 25 tests for `prefetch` / `queue_prefetch` / `_format_prefetch` including forward-compat with unknown response fields.
- `tests/test_integration.py` — 12 gated tests exercising every tool against a real `bm` MCP server (`BM_INTEGRATION=1` + `bm` + `mcp`). Each session uses a throwaway BM project that's torn down on completion.
### Changed
- `_BmMcpActor.call` now refuses calls after `shutdown()` (sets `_running=False`) and cancels the underlying coroutine on timeout instead of leaking it.
- `_format_prefetch` defensively coerces non-string fields and skips non-dict entries.
- Added module-level `__version__`, kept in sync with `plugin.yaml` (verified by a test).
## [0.1.0] — 2026-05-10
### Added
- Initial release of the Hermes Memory Provider plugin for Basic Memory.
- Seven `bm_*` agent tools: `bm_search`, `bm_read`, `bm_write`, `bm_edit`, `bm_context`, `bm_delete`, `bm_move`.
- Per-turn capture (`sync_turn`) and end-of-session summary (`on_session_end`).
- Local mode (default) with auto-created BM project; cloud mode with project-name-based routing.
- Single-file plugin at `__init__.py`, AGPL-3.0-or-later.
- 84-test pytest suite.
[0.3.2]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.3.2
[0.3.1]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.3.1
[0.3.0]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.3.0
[0.2.0]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.2.0
[0.1.7]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.7
[0.1.6]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.6
[0.1.5]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.5
[0.1.4]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.4
[0.1.3]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.3
[0.1.2]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.2
[0.1.1]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.1
[0.1.0]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.0
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+269
View File
@@ -0,0 +1,269 @@
# Hermes slash-command monkeypatch
This document describes the Hermes Agent-side patch needed when `/bm-*` slash commands do not appear in the gateway even though the Basic Memory provider itself is installed and working.
This issue is tracked upstream in https://github.com/NousResearch/hermes-agent/issues/23603
Agents may use this file as a guide to add support for `/bm-*` slash commands in Hermes Agent builds that do not yet include the upstream fix.
## Current status and compatibility
This is a **Hermes Agent-side** compatibility patch, not a Basic Memory plugin runtime patch.
| Hermes Agent version | Basic Memory plugin version | Guidance |
|---|---:|---|
| `v0.13.x` | `v0.3.0` | Plugin `v0.3.0` remains the right runtime release. If `/bm-*` commands are missing, use the Hermes Agent-side monkeypatch below or wait for the upstream Hermes fix. |
| `v0.14.0` / `v2026.5.16` | `v0.3.1` docs, runtime still equivalent to `v0.3.0` | The plugin runtime still works, but Hermes Agent `v0.14.0` still does **not** include the upstream slash-command discovery fix. Use the v0.14.0-compatible Hermes Agent-side patch below. |
| Future Hermes release with upstream fix | Latest plugin | Do **not** apply this monkeypatch unless `/bm-*` commands are still absent; the fix should be redundant once Hermes loads active exclusive memory-provider commands during command discovery. |
Checked against Hermes Agent `v2026.5.16` / `v0.14.0` on 2026-05-16: the upstream Hermes release still does **not** include this fix. After applying the Hermes Agent-side patch below locally, `get_plugin_commands()` returns the expected `/bm-*` commands.
Important nuance: recent `hermes-basic-memory` versions include a best-effort PluginManager reach-in that registers commands when the provider is loaded. That workaround alone is not enough for gateway startup discovery in affected Hermes builds, because `get_plugin_commands()` does not load the active exclusive memory provider. The Hermes Agent-side patch is still needed until upstream command discovery loads the active memory provider and the memory-provider collector delegates command/skill registration.
Release/tagging note for agents: `v0.3.1` is a documentation release that clarifies Hermes Agent `v0.14.0` compatibility instructions. It does not require users on Hermes Agent `v0.13.x` to change plugin runtime behavior, and it should not be interpreted as a Basic Memory data/schema migration.
## Problem
`hermes-basic-memory` is an **exclusive memory-provider plugin**. Hermes loads exclusive memory providers through `plugins.memory`, not through the normal `PluginManager` discovery path.
Gateway adapters register native slash commands during startup by calling Hermes's plugin command discovery APIs. In affected Hermes builds, that startup path only sees commands registered by normal plugins. The active memory provider has not been loaded yet, and the memory-provider loader uses a collector that captures only `register_memory_provider(...)`. As a result, commands registered by this plugin with `ctx.register_command(...)` never reach the central plugin command registry before Discord/native slash-command sync.
Symptoms:
- `hermes memory status` shows `Provider: basic-memory` and `Status: available`.
- Agent tools such as `bm_search`, `bm_read`, and `bm_recent` work.
- Native slash commands such as `/bm-search`, `/bm-read`, and `/bm-context` are missing after `hermes gateway restart`.
## Target behavior
When Hermes builds its plugin command list, it should also load the configured active memory provider once, allowing that provider to register commands and skills into the same central registries used by ordinary plugins.
After the patch, `get_plugin_commands()` should include commands such as:
```text
bm-context
bm-project
bm-read
bm-recent
bm-remember
bm-search
bm-status
bm-workspace
```
## Files to patch in Hermes Agent
Patch these files in the Hermes Agent repository, not in this plugin repository:
```text
hermes_cli/plugins.py
plugins/memory/__init__.py
```
Recommended tests to add/update in Hermes Agent:
```text
tests/hermes_cli/test_plugin_cli_registration.py
tests/hermes_cli/test_plugins.py
```
## Implementation outline
### 1. Load active memory-provider commands from `get_plugin_commands()`
In `hermes_cli/plugins.py`, add module-level idempotency/recursion guards near the global plugin manager:
```python
_plugin_manager: Optional[PluginManager] = None
_memory_provider_command_loads: set[str] = set()
_memory_provider_command_loading = False
```
Update `get_plugin_commands()` so it first ensures normal plugin discovery, then best-effort loads the active memory provider before returning the command registry:
```python
def get_plugin_commands() -> Dict[str, dict]:
"""Return the full plugin commands dict (name → {handler, description, plugin}).
Triggers idempotent plugin discovery so callers can use plugin commands
before any explicit discover_plugins() call. Also initializes the active
memory provider once so exclusive memory-provider plugins can contribute
gateway slash commands during startup discovery.
"""
manager = _ensure_plugins_discovered()
_ensure_active_memory_provider_commands_loaded()
return manager._plugin_commands
```
Add the helper:
```python
def _ensure_active_memory_provider_commands_loaded() -> None:
"""Best-effort load of the active memory provider's slash commands."""
global _memory_provider_command_loading
if _memory_provider_command_loading:
return
try:
from plugins import memory as memory_plugins
active = memory_plugins._get_active_memory_provider()
if not active or active in _memory_provider_command_loads:
return
_memory_provider_command_loading = True
try:
memory_plugins.load_memory_provider(active)
_memory_provider_command_loads.add(active)
finally:
_memory_provider_command_loading = False
except Exception as exc:
logger.debug(
"Failed to load active memory-provider plugin commands: %s",
exc,
exc_info=_PLUGINS_DEBUG,
)
```
Notes:
- This must be best-effort; command discovery should not break Hermes startup if a memory provider is misconfigured.
- The recursion guard prevents `load_memory_provider(...)` → provider `register(...)``ctx.register_command(...)` → plugin manager access from re-entering endlessly.
- The load set prevents duplicate provider command registration work.
### 2. Make the memory-provider collector delegate commands and skills
In `plugins/memory/__init__.py`, import `Callable`:
```python
from typing import Callable, List, Optional, Tuple
```
When loading a provider directory, pass the plugin/provider name to the collector:
```python
collector = _ProviderCollector(plugin_name=name)
```
Replace the collector that only captures `register_memory_provider(...)` with a plugin-context shim that also delegates `register_command(...)` and `register_skill(...)` into the central `PluginManager` registries:
```python
class _ProviderCollector:
"""Plugin-context shim used while loading memory providers.
Memory providers are exclusive plugins and are loaded by this module
instead of the general PluginManager. They still need access to the same
slash-command and skill registries as normal plugins, otherwise active
memory-provider commands are invisible during gateway startup discovery.
"""
def __init__(self, plugin_name: str = "memory-provider"):
self.provider = None
self.plugin_name = plugin_name
def register_memory_provider(self, provider):
self.provider = provider
def register_command(
self,
name: str,
handler: Callable,
description: str = "",
args_hint: str = "",
) -> None:
"""Register a memory-provider slash command with PluginManager."""
try:
from hermes_cli.plugins import _ensure_plugins_discovered
except Exception:
return
clean = name.lower().strip().lstrip("/").replace(" ", "-")
if not clean:
return
try:
manager = _ensure_plugins_discovered()
except Exception:
return
plugin_commands = getattr(manager, "_plugin_commands", None)
if plugin_commands is None:
return
plugin_commands[clean] = {
"handler": handler,
"description": description or "Plugin command",
"plugin": self.plugin_name,
"args_hint": (args_hint or "").strip(),
}
def register_skill(
self,
name: str,
path: Path,
description: str = "",
) -> None:
"""Register a memory-provider skill with PluginManager."""
try:
from agent.skill_utils import _NAMESPACE_RE
from hermes_cli.plugins import _ensure_plugins_discovered
except Exception:
return
if ":" in name or not name or not _NAMESPACE_RE.match(name):
raise ValueError(f"Invalid skill name '{name}'.")
if not path.exists():
raise FileNotFoundError(f"SKILL.md not found at {path}")
try:
manager = _ensure_plugins_discovered()
except Exception:
return
plugin_skills = getattr(manager, "_plugin_skills", None)
if plugin_skills is None:
return
plugin_skills[f"{self.plugin_name}:{name}"] = {
"path": path,
"plugin": self.plugin_name,
"bare_name": name,
"description": description,
}
```
Keep existing no-op methods such as `register_tool(...)` and `register_cli_command(...)` as no-ops unless the target Hermes version expects otherwise.
## Verification
From the Hermes Agent repository, run focused compile/tests:
```bash
python -m py_compile hermes_cli/plugins.py plugins/memory/__init__.py
python -m pytest \
tests/hermes_cli/test_plugins.py::TestPluginCommands::test_get_plugin_commands_loads_active_memory_provider_commands \
tests/hermes_cli/test_plugin_cli_registration.py::TestProviderCollectorRegistration \
-q -o 'addopts='
```
Then verify the active config sees the Basic Memory commands:
```bash
python - <<'PY'
import hermes_cli.plugins as p
p._plugin_manager = None
p._memory_provider_command_loads.clear()
cmds = p.get_plugin_commands()
print(sorted(k for k in cmds if k.startswith('bm-')))
PY
```
Expected output:
```text
['bm-context', 'bm-project', 'bm-read', 'bm-recent', 'bm-remember', 'bm-search', 'bm-status', 'bm-workspace']
```
Finally restart the gateway so native slash commands are synced:
```bash
hermes gateway restart
```
For Discord, global command propagation can lag briefly. If the commands do not show immediately, type `/bm` directly or reload the Discord client.
+235
View File
@@ -0,0 +1,235 @@
# hermes-basic-memory
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
Hermes Memory Provider plugin that gives [Hermes Agent](https://github.com/NousResearch/hermes-agent) a persistent knowledge graph backed by [Basic Memory](https://github.com/basicmachines-co/basic-memory).
The plugin replaces Hermes's "no external memory provider" with a real graph: search-before-answer recall, per-turn capture, end-of-session summaries, and ten `bm_*` tools the agent can call directly. Local mode by default; one CLI flip switches to true cloud routing through Basic Memory Cloud.
## Install
```bash
hermes plugins install basicmachines-co/basic-memory --path integrations/hermes
```
Then activate it in `~/.hermes/config.yaml`:
```yaml
memory:
provider: basic-memory
```
If you run the gateway, restart it (`hermes gateway restart`). Done.
If your installed Hermes build does not support `--path`, use the final deprecated `basicmachines-co/hermes-basic-memory` pointer release until Hermes subpath installs are available. Ongoing development now lives in [`basic-memory/integrations/hermes`](https://github.com/basicmachines-co/basic-memory/tree/main/integrations/hermes).
The plugin self-installs the `basic-memory` CLI on first init via `uv tool install basic-memory` (one-time ~10s pause if it isn't already present). The bm binary lands at `~/.local/bin/bm` — the same location a manual `uv tool install basic-memory` would produce, so a later manual install or upgrade is a no-op rather than a second install.
### Prerequisites
- [Hermes Agent](https://github.com/NousResearch/hermes-agent)
- [`uv`](https://docs.astral.sh/uv/) on PATH (used for the bootstrap install)
- The `mcp` Python package in the Hermes venv. If `hermes plugins install` doesn't auto-install it (it follows `pip_dependencies` in `plugin.yaml`), run:
```bash
uv pip install --python ~/.hermes/hermes-agent/venv/bin/python mcp
```
### Verify
```bash
hermes memory status
```
Expected:
```
Provider: basic-memory
Plugin: installed ✓
Status: available ✓
```
## What the agent gets
Ten tools (curated subset of Basic Memory's MCP surface):
| Tool | Use |
|---|---|
| `bm_search` | Semantic + full-text search; **call this before answering** |
| `bm_read` | Fetch a note by title, permalink, or `memory://` URL |
| `bm_write` | Create a new note (capture decisions, meeting notes, insights) |
| `bm_edit` | Append, prepend, find/replace, replace-section |
| `bm_context` | Navigate via `memory://` URLs to find related notes |
| `bm_delete` | Delete a note |
| `bm_move` | Move a note to a different folder |
| `bm_recent` | List notes updated recently (default `7d`; accepts natural-language timeframes) |
| `bm_projects` | List available projects with their UUIDs (for cross-project routing) |
| `bm_workspaces` | List Basic Memory Cloud workspaces |
Every read/write tool also accepts optional `project` / `project_id` for per-call routing — write or read against a project other than the configured one without reconfiguring the plugin.
Plus automatic capture:
- **Per turn**: every user/assistant exchange appends to a running session-transcript note
- **End of session**: a separate summary note is written, linked back to the transcript via a `summary_of` relation
A bundled skill (`skill:view basic-memory:basic-memory`) gives the agent a longer reference doc on top of the always-on `system_prompt_block`.
## Slash commands
For direct, in-session use without going through the agent (requires Hermes ≥ v0.11.0):
| Command | Use |
|---|---|
| `/bm-search <query>` | Search the knowledge graph; returns compact title/permalink/preview rows. |
| `/bm-read <identifier>` | Read a note by title, permalink, or `memory://` URL. |
| `/bm-context <identifier>` | Build context for a note (target + related). |
| `/bm-recent [timeframe]` | Recently updated notes. Default `7d`; accepts `"2 weeks"`, `"yesterday"`, etc. |
| `/bm-status` | Plugin/provider state: mode, project, capture flags, bm CLI path. |
| `/bm-remember <text>` | Capture a quick note. Title = first line (≤80 chars), folder = `remember_folder` (default `bm-remember`), tagged `manual-capture`. |
| `/bm-project` | List all known projects; the active one is marked. |
| `/bm-workspace` | List BM Cloud workspaces. Cloud mode only — prints an explanatory line in local mode. |
Examples:
```text
/bm-search Q3 OKRs
/bm-read decisions/auth-rewrite
/bm-recent yesterday
/bm-remember Reminder: switch the staging job to the new image after the rebase lands.
```
`/bm-project` and `/bm-workspace` are read-only in 0.2.0 — mid-session switching is intentionally not supported because auto-capture would otherwise land in the wrong place. Tracked as a follow-up.
### Known issue: `/bm-*` commands may not appear in some Hermes gateway builds
Plugin v0.2.0 and later register the commands above, but some Hermes Agent gateway builds do not discover slash commands contributed by an **exclusive memory-provider plugin** during startup. The symptoms are:
- the memory tools work for the agent (`bm_search`, `bm_read`, etc.);
- `hermes memory status` shows `Provider: basic-memory` and `Status: available`; but
- Discord/native slash command pickers do not show `/bm-search`, `/bm-read`, `/bm-context`, and the other `/bm-*` commands after `hermes gateway restart`.
This is a Hermes Agent plugin-discovery issue, not a Basic Memory runtime issue. It is tracked upstream in [NousResearch/hermes-agent#23603](https://github.com/NousResearch/hermes-agent/issues/23603). Updating the Basic Memory plugin alone cannot fix affected gateway startup discovery; Hermes Agent itself must include or receive the compatibility patch. Until the upstream Hermes fix is available in your installed Hermes version, use one of these workarounds:
1. apply the Hermes Agent-side patch described in [MONKEYPATCH.md](MONKEYPATCH.md), which includes compatibility notes for Hermes Agent v0.13.x and v0.14.0; or
2. use the agent tools directly (`bm_search`, `bm_read`, `bm_recent`, etc.) instead of native slash commands.
After applying an updated or patched Hermes build, restart the gateway so Discord/native slash commands are re-synced:
```bash
hermes gateway restart
```
If Discord still does not show the commands immediately, type `/bm` directly or reload the Discord client; global command propagation can lag briefly.
## Configuration
Defaults are reasonable for local use:
| Key | Default | Notes |
|---|---|---|
| `mode` | `local` | `local` (in-process) or `cloud` (route through BM Cloud API) |
| `project` | `hermes-memory` | BM project name |
| `project_path` | `~/hermes-memory/` | Local mode only — where session notes land |
| `capture_folder` | `hermes-sessions` | Folder within the project for session notes |
| `capture_per_turn` | `true` | Append every turn to a session transcript |
| `capture_session_end` | `true` | Write a summary note when the session ends |
| `remember_folder` | `bm-remember` | Folder where `/bm-remember` captures land (kept separate from session transcripts) |
To override, write `~/.hermes/basic-memory.json` or run `hermes memory setup basic-memory`:
```json
{
"mode": "local",
"project": "hermes-memory",
"project_path": "~/hermes-memory/",
"capture_per_turn": true,
"capture_session_end": true,
"capture_folder": "hermes-sessions",
"remember_folder": "bm-remember"
}
```
In local mode the plugin auto-creates the BM project on first init via `bm project add`. In cloud mode it doesn't — you create the cloud-routed project yourself (see below) and the plugin verifies it's registered before initializing.
### Cloud mode
When `mode: cloud`, tool calls route directly through the BM cloud API — no local file mirror, no bisync. You set this up once with the BM CLI:
```bash
# Authenticate (OAuth) or save an API key
bm cloud login # OAuth — interactive
# OR for headless/automation:
bm cloud create-key "hermes"
bm cloud set-key bmc_...
# Create the project, then flip it to cloud routing.
# --workspace is required if you belong to more than one workspace
# (otherwise BM auto-resolves the only one available).
bm project add hermes-memory-cloud
bm project set-cloud hermes-memory-cloud --workspace Personal
# Point the plugin at it
cat > ~/.hermes/basic-memory.json <<EOF
{
"mode": "cloud",
"project": "hermes-memory-cloud",
"capture_per_turn": true,
"capture_session_end": true,
"capture_folder": "hermes-sessions"
}
EOF
hermes gateway restart
```
Tool calls now route from `bm mcp``<cloud_host>/proxy` over HTTPS using your OAuth token (or API key). Notes never touch local disk.
**Don't confuse cloud mode with `bm cloud bisync`.** Bisync is rclone-style two-way file sync between a *local* project and cloud storage, intended for keeping local working copies. For agent-driven capture you want true cloud routing (`set-cloud`), not bisync.
## Updating / removing
```bash
hermes plugins update basic-memory
hermes plugins remove basic-memory # then revert memory.provider in config.yaml
```
## Foot-guns
- **`<memory-context>` tags in notes**: Hermes's streaming output scrubber strips literal `<memory-context>...</memory-context>` blocks from assistant text. If a note contains those tags and the assistant echoes the body verbatim, the echoed copy gets eaten mid-stream. Tool results inbound are unaffected. Avoid those tags in BM notes; if you must include them, fence in a code block.
- **Single external provider**: Hermes accepts only one external memory provider at a time. Activating basic-memory displaces any other.
- **CLI cold start**: `hermes -z ...` invocations spawn `bm mcp` per run (~2-5s). Long-running gateway sessions amortize this.
- **Multiple cloud workspaces**: if your BM Cloud account belongs to more than one workspace, `bm project set-cloud` must be invoked with `--workspace <name>`. Otherwise tool calls fail with "Multiple workspaces are available".
## Development
The plugin is a single-file Python module at `__init__.py`. The Hermes plugin loader expects `register(ctx)` and grep-detects either `register_memory_provider` or `MemoryProvider` in the file.
For local development (point Hermes at your working tree instead of going through `hermes plugins install`):
```bash
git clone https://github.com/basicmachines-co/basic-memory ~/code/basic-memory
mkdir -p ~/.hermes/plugins
ln -snf ~/code/basic-memory/integrations/hermes ~/.hermes/plugins/basic-memory
```
### Running tests
```bash
# From the monorepo root
just package-check-hermes
# Or from integrations/hermes
just check
# Unit tests (fast, hermetic — no Hermes or bm required)
uv run --with pytest pytest
# Integration tests (gated — exercise every tool against a real bm MCP server)
BM_INTEGRATION=1 uv run --with pytest --with mcp pytest tests/test_integration.py
```
The unit suite stubs out Hermes-internal imports (`agent.memory_provider`, `tools.registry`) so it runs without a Hermes install. `mcp` is optional at unit-test time — its absence just makes `is_available()` return False, which the tests verify.
Integration tests require `BM_INTEGRATION=1`, `bm` CLI on PATH, and `mcp` Python package importable. Each session creates a unique throwaway BM project (under `tempfile.mkdtemp`) and removes it on teardown, so they never touch your real BM projects.
## License
AGPL-3.0-or-later, matching [basic-memory](https://github.com/basicmachines-co/basic-memory). See [LICENSE](LICENSE).
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
# Basic Memory Hermes plugin checks
repo_root := "../.."
# Validate plugin.yaml, module entrypoint, bundled skill, and test layout.
manifest-check:
python3 {{repo_root}}/scripts/validate_hermes_plugin.py .
# Unit tests are hermetic and do not require a Hermes install.
test:
uv run --no-project --with pytest --with pytest-cov --python 3.12 pytest -q --cov=. --cov-report=term-missing
# Gated integration test against a real bm MCP server.
test-int:
BM_INTEGRATION=1 uv run --no-project --with pytest --with mcp --python 3.12 pytest tests/test_integration.py -q
# Full local check.
check: manifest-check test
# Show available recipes
default:
@just --list
+11
View File
@@ -0,0 +1,11 @@
name: basic-memory
version: 0.3.2
description: "Basic Memory — persistent knowledge graph backed by the basic-memory MCP server"
pip_dependencies:
- mcp
hooks:
- prefetch
- queue_prefetch
- sync_turn
- on_session_end
- shutdown
+8
View File
@@ -0,0 +1,8 @@
[pytest]
testpaths = tests
addopts = -ra
pythonpath =
.
tests/stubs
filterwarnings =
ignore::DeprecationWarning
+1
View File
@@ -0,0 +1 @@
pytest>=7.0
+242
View File
@@ -0,0 +1,242 @@
---
name: basic-memory
description: Use the Basic Memory knowledge graph for persistent memory across sessions. Search before answering; capture decisions, meetings, and insights as notes.
category: memory
---
# Basic Memory Knowledge Graph
You have access to a persistent knowledge graph backed by Basic Memory. The graph survives across sessions and is shared with other tools (Claude Desktop, Obsidian, the `bm` CLI). Use the `bm_*` tools below to recall and capture information.
## Use `bm_*`, not the `bm` CLI
**Always invoke the `bm_*` tools directly. Do not shell out to the `bm` CLI for note operations.**
The `bm_*` tools route through a persistent MCP connection — roughly 0.1 seconds per call. Running `bm` from the shell spawns a fresh Python process per call (1-2 seconds of cold-start every time) and bypasses Hermes's automatic per-turn capture, so the session-transcript and summary notes won't reflect what you did.
The CLI is fine when you genuinely need a feature these wrappers don't expose (rare). Otherwise, prefer:
| Use case | Tool (not CLI) |
|---|---|
| Search the graph | `bm_search` |
| Read a note | `bm_read` |
| Create / update a note | `bm_write` / `bm_edit` |
| Navigate relations | `bm_context` |
| Move / delete | `bm_move` / `bm_delete` |
| What's been touched lately | `bm_recent` |
| List available projects | `bm_projects` |
| List cloud workspaces | `bm_workspaces` |
## Tool reference
### `bm_search` — search the graph
Use **before** answering questions about prior decisions, projects, meetings, or anything that might already be documented.
```
bm_search({ query: "auth strategy decision", limit: 5 })
```
### `bm_read` — fetch a note's full content
After search shows a relevant note, read it for context.
```
bm_read({ identifier: "decisions/auth-strategy" })
bm_read({ identifier: "memory://projects/api-redesign" })
```
### `bm_context` — navigate via memory:// URLs
Returns the target note plus related notes via traversed relations.
```
bm_context({ url: "memory://projects/api-redesign", depth: 1 })
```
### `bm_write` — capture new knowledge
When the user shares a decision, meeting outcome, or insight worth keeping, capture it. Use clear titles and a folder.
```
bm_write({
title: "API Authentication Decision",
folder: "decisions",
content: "# API Authentication\n\n## Context\n...\n\n## Decision\n..."
})
```
Recommended folders: `projects/`, `decisions/`, `meetings/`, `concepts/`, `weekly/`.
### `bm_edit` — incremental updates
Operations: `append`, `prepend`, `find_replace` (requires `find_text`), `replace_section` (requires `section`).
```
bm_edit({
identifier: "projects/api-redesign",
operation: "append",
content: "\n## Update 2026-05-09\nDeployed to staging."
})
```
### `bm_delete` / `bm_move` — maintenance
Use sparingly. `bm_move` takes `new_folder`.
### `bm_recent` — what's been touched lately
Returns notes updated within a window. Use when there's no specific query yet — e.g. "what was I working on yesterday?"
```
bm_recent({ timeframe: "7d" })
bm_recent({ timeframe: "yesterday", limit: 20 })
bm_recent({ timeframe: "2 weeks", type: "entity" })
```
`timeframe` accepts natural language (`"yesterday"`, `"2 weeks"`, `"last month"`) or compact forms (`"7d"`, `"24h"`). Default is `7d`.
### `bm_projects` — list available projects
Returns name, workspace slug, and `external_id` (UUID) per project across local and cloud. Call this when the user names a project that isn't the active one. Route follow-up tool calls either by workspace-qualified name (`project: "personal/main"`) or by UUID (`project_id: "bf2a4c1e-d77f-..."`) — see Cross-project routing below.
```
bm_projects()
```
### `bm_workspaces` — list BM Cloud workspaces
Workspaces are a BM Cloud concept. Returns name, type, role, and default flag. Pair with `bm_projects` when the same project name might exist in more than one workspace and you need to disambiguate.
```
bm_workspaces()
```
## Permalinks
A permalink is the canonical, URL-friendly identifier for a note. Three shapes exist; the read/write tools accept all of them:
| Shape | Example | When |
|---|---|---|
| **Short** | `decisions/auth-strategy` | Bare `folder/note-slug`. Tools need a `project` (or `project_id`) arg to route — the permalink alone isn't enough. |
| **Project-qualified** | `main/decisions/auth-strategy` | `project-name/folder/note-slug`. Carries enough context to route without a separate `project` arg. |
| **Workspace-qualified** | `personal/main/decisions/auth-strategy` | `workspace-slug/project-name/folder/note-slug`. Fully routes, including across cloud workspaces with same-named projects. |
**Important: the permalink returned by `bm_write` already encodes the routing it needs for follow-up reads.** If you wrote with `project="personal/main"`, you get back `personal/main/folder/note-slug` and can call `bm_read({ identifier: <that permalink> })` with no `project` arg. The permalink self-routes.
`memory://` URLs follow the same shapes: `memory://personal/main/decisions/auth-strategy` is valid. The `memory://` prefix is optional for `bm_read` (any of the three permalink shapes works directly); `bm_context` expects the prefix.
## Cross-project routing
Every read/write tool (`bm_search`, `bm_read`, `bm_write`, `bm_edit`, `bm_context`, `bm_delete`, `bm_move`, `bm_recent`) accepts optional `project` and `project_id`:
- `project` — project name, optionally workspace-qualified. Plain (`"main"`) when the name is globally unique; qualified (`"personal/main"`, `"team-paul/research"`) when you need to pick a specific cloud workspace by slug.
- `project_id` — UUID from `bm_projects` (`external_id` field). The most stable identifier — survives project renames and works across workspaces without qualification. Wins over `project` if both are passed.
Omit both and the call uses the Hermes-configured active project.
```
# Plain project name (unique)
bm_write({ title: "...", folder: "...", content: "...", project: "main" })
# Workspace-qualified name (disambiguates same-named projects across workspaces)
bm_write({ title: "...", folder: "...", content: "...", project: "personal/main" })
# UUID (most stable, survives renames)
bm_write({ title: "...", folder: "...", content: "...", project_id: "bf2a4c1e-d77f-..." })
```
`bm_projects` and `bm_workspaces` themselves do **not** take routing — they list across everything.
## Recipe: writing an existing file into a specific project
When the user asks something like *"save this markdown file to my personal `main` project, return the permalink"*:
1. **Discover the project.** Call `bm_projects()` and find the entry matching the user's described project + workspace. You can route by either the workspace-qualified name (`personal/main`) or the UUID (`external_id`).
```
bm_projects()
# → [{name: "main", external_id: "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d", workspace: "Personal", ...}, ...]
```
If a project name appears in multiple workspaces, use `bm_workspaces()` to confirm which slug you want.
2. **Read the file from disk.** Use Hermes's filesystem tool (not a `bm_*` tool — local files aren't in the graph yet).
3. **Write the note with explicit routing.** Either form works; the workspace-qualified name reads cleaner in logs, the UUID is more durable.
```
bm_write({
title: "StartWithDrew Level 9 Task Queue",
folder: "startwithdrew",
content: <file body>,
project: "personal/main"
})
# → returns "personal/main/startwithdrew/start-with-drew-level-9-task-queue"
# (the returned permalink is workspace-qualified — carries its own routing)
```
4. **Verify by reading back.** No `project` arg needed — the workspace-qualified permalink routes itself.
```
bm_read({ identifier: "personal/main/startwithdrew/start-with-drew-level-9-task-queue" })
```
Return the permalink (and the project name for clarity) to the user.
## When to use each tool
| Situation | Tool |
|---|---|
| User asks about a topic that might already be documented | `bm_search` first, then `bm_read` |
| User exposes a decision, plan, or meeting outcome | offer to `bm_write` |
| Updating prior work | `bm_edit` (append for time-ordered logs, replace_section for living docs) |
| Exploring related concepts | `bm_context` |
| "What was I working on yesterday?" / no specific query yet | `bm_recent` |
| User names a project that isn't the active one | `bm_projects` → call read/write tool with `project: "workspace/name"` or `project_id: "<uuid>"` |
| Same project name might exist in multiple workspaces | `bm_projects` (+ `bm_workspaces` if needed) → route with workspace-qualified `project` or `project_id` |
| Following up on a freshly-written note | Use the returned permalink directly — it already encodes the routing |
## Note structure
BM treats `- [category]` lines as **observations** and WikiLink lines under `## Relations` as **relations**. Categories (`[decision]`, `[insight]`, `[risk]`, `[fact]`, `[todo]`, …) and relation types (`relates_to`, `implements`, `depends_on`, `blocks`, …) are open-ended — use what fits the content. YAML frontmatter is supported with `title`, `type`, `tags`, and `permalink` as standard fields; any custom fields are allowed. See the [knowledge format docs](https://docs.basicmemory.com/raw/concepts/knowledge-format.md) for the full convention.
```markdown
# Clear Title
## Context
Background and current situation.
## Key Points
- Main insights
- Important details
## Observations
- [decision] We chose PostgreSQL for ACID guarantees
- [insight] Users prefer social login
- [risk] Deployment lacks rollback path
## Relations
- relates_to [[Other Note Title]]
- depends_on [[Database Choice]]
## Next Steps
- [ ] Implement
- [ ] Document
```
## Behavior guidelines
1. **Search before answering.** If the user asks "what did we decide about X?", run `bm_search` first.
2. **Offer to capture.** When the user shares decisions or meeting outcomes, ask: "Should I save this as a note?"
3. **Suggest connections.** When a search returns related notes, surface them so the user knows what already exists.
4. **Don't over-capture.** Auto-capture is already running per turn. Don't create a `bm_write` for every response — only for substantive content the user wants preserved.
5. **Sensitive info.** Don't capture credentials or personal data without confirmation.
## Footgun
If a note's body contains literal `<memory-context>...</memory-context>` tags, Hermes's streaming output scrubber will eat those tags (and the text between paired ones) when you echo the note verbatim back to the user. Tool *inputs* are unaffected. If you must include such content, fence it in a code block.
## Further reading
Official docs live at [docs.basicmemory.com](https://docs.basicmemory.com). Every page has an AI-friendly raw markdown view at `/raw/<path>.md` (or send `Accept: text/markdown` to the canonical URL). `WebFetch` any of these when you need detail beyond what this skill covers:
- **[Knowledge format](https://docs.basicmemory.com/raw/concepts/knowledge-format.md)** — observation categories, relation types, frontmatter conventions.
- **[Observations & relations](https://docs.basicmemory.com/raw/concepts/observations-and-relations.md)** — how notes form a graph that's searchable and traversable.
- **[Memory URLs](https://docs.basicmemory.com/raw/concepts/memory-urls.md)** — title-based addressing, wildcards (`memory://docs/*`), and routing resolution order.
- **[Projects & folders](https://docs.basicmemory.com/raw/concepts/projects-and-folders.md)** — multi-project layout, folder organization, cloud routing behavior.
- **[Semantic search](https://docs.basicmemory.com/raw/concepts/semantic-search.md)** — how `bm_search` resolves queries (semantic + full-text).
- **[MCP tools reference](https://docs.basicmemory.com/raw/reference/mcp-tools-reference.md)** — Basic Memory's full MCP surface (the `bm_*` tools here are a curated subset).
- **[Cloud routing](https://docs.basicmemory.com/raw/cloud/routing.md)** — local vs cloud project modes, per-project routing setup.
- **[llms.txt index](https://docs.basicmemory.com/llms.txt)** — full sitemap of raw markdown pages, useful when you need to look up a page not listed above.
+167
View File
@@ -0,0 +1,167 @@
"""
Pytest configuration: stub Hermes-internal imports so the plugin loads
without a Hermes install, and expose the loaded plugin module as a fixture.
"""
from __future__ import annotations
import importlib.util
import json
import os
import sys
import types
import pytest
def _stub_hermes_modules() -> None:
"""
The plugin imports `agent.memory_provider.MemoryProvider` and
`tools.registry.tool_error`. These are provided by Hermes at runtime,
not as a pip-installable package. Stub them before module load so unit
tests don't need Hermes installed.
"""
agent_mod = types.ModuleType("agent")
agent_mp_mod = types.ModuleType("agent.memory_provider")
class _StubMemoryProvider:
"""Stand-in for `agent.memory_provider.MemoryProvider`."""
agent_mp_mod.MemoryProvider = _StubMemoryProvider
sys.modules.setdefault("agent", agent_mod)
sys.modules.setdefault("agent.memory_provider", agent_mp_mod)
tools_mod = types.ModuleType("tools")
tools_registry_mod = types.ModuleType("tools.registry")
def _tool_error(msg: str) -> str:
return json.dumps({"error": str(msg)})
tools_registry_mod.tool_error = _tool_error
sys.modules.setdefault("tools", tools_mod)
sys.modules.setdefault("tools.registry", tools_registry_mod)
_stub_hermes_modules()
_PLUGIN_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "__init__.py")
_spec = importlib.util.spec_from_file_location("hermes_basic_memory_plugin", _PLUGIN_PATH)
assert _spec is not None and _spec.loader is not None
_plugin = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_plugin)
@pytest.fixture
def bm():
"""The loaded plugin module."""
return _plugin
# ---- Synthetic MCP CallToolResult objects (no MCP SDK needed at test time) ----
class FakeContent:
"""Stand-in for `mcp.types.TextContent`."""
def __init__(self, text: str):
self.text = text
class FakeCallToolResult:
"""Stand-in for `mcp.types.CallToolResult`."""
def __init__(self, content_texts, is_error: bool = False):
self.content = [FakeContent(t) for t in content_texts]
self.isError = is_error
@pytest.fixture
def fake_result():
return FakeCallToolResult
# ---- Actor test helpers ----
class FakeSession:
"""
Stand-in for `mcp.ClientSession`. `call_tool` is an async coroutine
that records every call and returns a configurable CallToolResult.
- default_response: dict serialized to JSON in the result text
- hang: when True, call_tool sleeps long enough to force a client-side timeout
- is_error: when True, return value has isError=True
"""
def __init__(self, default_response=None, hang: bool = False, is_error: bool = False):
self.default_response = default_response if default_response is not None else {"ok": True}
self.hang = hang
self.is_error = is_error
self.calls: list = []
self.was_cancelled = False
self._stub_handlers: dict = {}
def stub(self, tool_name: str, handler):
"""Register a per-tool handler. Handler receives (args) and returns a response dict (or raises)."""
self._stub_handlers[tool_name] = handler
async def call_tool(self, name: str, args: dict):
self.calls.append((name, dict(args)))
if self.hang:
try:
import asyncio as _a
await _a.sleep(60)
except BaseException as e:
# Track cooperative cancellation so tests can verify cleanup
if type(e).__name__ in ("CancelledError",):
self.was_cancelled = True
raise
if name in self._stub_handlers:
response = self._stub_handlers[name](args)
else:
response = self.default_response
return FakeCallToolResult([json.dumps(response)], is_error=self.is_error)
def make_scripted_actor(
bm, session=None, raise_at_init: BaseException | None = None, fake_tools: list | None = None
):
"""
Build an actor whose `_main()` is replaced with a deterministic version.
The fake `_main` mirrors the production happy path (sets _session, _stop_future,
fills _tools_cache, signals ready, awaits stop) but skips the stdio subprocess.
raise_at_init: if set, raised inside _main so we can test failure paths.
"""
import asyncio
actor = bm._BmMcpActor(["fake-bm", "mcp"])
sess = session or FakeSession()
async def _fake_main():
if raise_at_init is not None:
actor._init_error = raise_at_init
actor._ready.set()
raise raise_at_init
actor._session = sess
actor._stop_future = asyncio.get_running_loop().create_future()
actor._tools_cache = fake_tools or [
{"name": n, "description": ""}
for n in [
"search_notes",
"read_note",
"write_note",
"edit_note",
"build_context",
"delete_note",
"move_note",
]
]
actor._ready.set()
await actor._stop_future
actor._main = _fake_main # type: ignore[assignment]
actor._test_session = sess # convenience handle for assertions
return actor
@@ -0,0 +1 @@
"""Test-only Hermes agent package stub."""
@@ -0,0 +1,5 @@
"""Test-only stand-in for Hermes's memory provider interface."""
class MemoryProvider:
"""Minimal base class used by the plugin during unit tests."""
@@ -0,0 +1 @@
"""Test-only Hermes tools package stub."""
@@ -0,0 +1,9 @@
"""Test-only stand-in for Hermes's tool registry helpers."""
from __future__ import annotations
import json
def tool_error(msg: str) -> str:
return json.dumps({"error": str(msg)})
+183
View File
@@ -0,0 +1,183 @@
"""Tests for _BmMcpActor: lifecycle, call dispatch, timeout, shutdown."""
from __future__ import annotations
import concurrent.futures
import json
import time
import pytest
from tests.conftest import FakeSession, make_scripted_actor
# ---- Lifecycle ----
def test_new_actor_is_not_running(bm):
actor = bm._BmMcpActor(["fake-bm", "mcp"])
assert actor._running is False
def test_start_brings_actor_up(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
try:
assert actor._running is True
assert actor._session is not None
assert actor._stop_future is not None
assert actor._thread is not None and actor._thread.is_alive()
# Tools cache populated
names = {t["name"] for t in actor.list_tools()}
assert "search_notes" in names
finally:
actor.shutdown(timeout=2.0)
def test_start_is_idempotent_when_thread_alive(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
try:
first_thread = actor._thread
actor.start(timeout=5.0) # second call
assert actor._thread is first_thread # same thread, no new one spawned
finally:
actor.shutdown(timeout=2.0)
def test_start_init_error_raises(bm):
boom = ValueError("BM unreachable")
actor = make_scripted_actor(bm, raise_at_init=boom)
with pytest.raises(RuntimeError, match="BM unreachable"):
actor.start(timeout=5.0)
assert actor._running is False
def test_start_timeout_raises(bm):
"""If _ready never gets set within the timeout, start() raises TimeoutError."""
import asyncio
actor = bm._BmMcpActor(["fake-bm", "mcp"])
async def _hang_forever():
# Never call self._ready.set(); start() should time out waiting.
await asyncio.sleep(60)
actor._main = _hang_forever # type: ignore[assignment]
with pytest.raises(TimeoutError):
actor.start(timeout=0.5)
assert actor._running is False
# ---- Shutdown ----
def test_shutdown_before_start_is_noop(bm):
actor = bm._BmMcpActor(["fake-bm", "mcp"])
# Should not raise even though nothing is running
actor.shutdown(timeout=1.0)
assert actor._running is False
def test_shutdown_stops_running_actor(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
assert actor._thread is not None and actor._thread.is_alive()
actor.shutdown(timeout=5.0)
assert actor._running is False
# Thread should exit shortly after stop_future resolves
actor._thread.join(timeout=2.0)
assert not actor._thread.is_alive()
def test_shutdown_is_idempotent(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
actor.shutdown(timeout=2.0)
# Second call should not raise
actor.shutdown(timeout=2.0)
assert actor._running is False
# ---- call() dispatch ----
def test_call_before_start_raises(bm):
actor = bm._BmMcpActor(["fake-bm", "mcp"])
with pytest.raises(RuntimeError):
actor.call("search_notes", {})
def test_call_after_shutdown_raises(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
actor.shutdown(timeout=2.0)
with pytest.raises(RuntimeError, match="not running"):
actor.call("search_notes", {})
def test_call_dispatches_through_actor_loop(bm):
session = FakeSession(default_response={"results": [], "ok": True})
actor = make_scripted_actor(bm, session=session)
actor.start(timeout=5.0)
try:
out = actor.call("search_notes", {"query": "hi"}, timeout=5.0)
# Output is whatever _extract_mcp_text produces; we just verify it
# contains the response payload we configured.
assert "ok" in out
assert session.calls == [("search_notes", {"query": "hi"})]
finally:
actor.shutdown(timeout=2.0)
def test_call_returns_mcp_extracted_text(bm):
session = FakeSession(default_response={"permalink": "p/q/r", "title": "T"})
actor = make_scripted_actor(bm, session=session)
actor.start(timeout=5.0)
try:
out = actor.call("write_note", {"title": "T"}, timeout=5.0)
parsed = json.loads(out)
assert parsed["permalink"] == "p/q/r"
finally:
actor.shutdown(timeout=2.0)
def test_call_timeout_raises_and_cancels_coroutine(bm):
session = FakeSession(hang=True)
actor = make_scripted_actor(bm, session=session)
actor.start(timeout=5.0)
try:
t0 = time.monotonic()
with pytest.raises(concurrent.futures.TimeoutError):
actor.call("search_notes", {}, timeout=0.3)
elapsed = time.monotonic() - t0
# Sanity: we didn't accidentally wait the full session-side sleep
assert elapsed < 2.0
# Give the actor loop a moment to propagate the cancellation
deadline = time.monotonic() + 2.0
while time.monotonic() < deadline and not session.was_cancelled:
time.sleep(0.05)
assert session.was_cancelled is True, (
"Expected the underlying coroutine to be cancelled when call() times out"
)
finally:
actor.shutdown(timeout=2.0)
def test_list_tools_returns_independent_copy(bm):
actor = make_scripted_actor(bm)
actor.start(timeout=5.0)
try:
snapshot = actor.list_tools()
snapshot.append({"name": "tampered"})
assert "tampered" not in {t["name"] for t in actor.list_tools()}
finally:
actor.shutdown(timeout=2.0)
def test_actor_init_error_logs_and_marks_not_running(bm, caplog):
actor = make_scripted_actor(bm, raise_at_init=RuntimeError("server crashed"))
with pytest.raises(RuntimeError):
actor.start(timeout=5.0)
assert actor._running is False
+337
View File
@@ -0,0 +1,337 @@
"""
Tests for the capture pipeline: sync_turn (per-turn) and on_session_end (summary).
These tests run the *real* sync_turn code path with a mocked actor, so threading
and argument-shape regressions are caught.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
def _provider_with_mock_actor(bm, *, project="test-proj", capture_folder="hermes-sessions"):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = project
p._capture_folder = capture_folder
p._session_id = "20260510_123456_abcdef"
p._session_started_at = datetime(2026, 5, 10, 12, 34, 56, tzinfo=timezone.utc)
actor = MagicMock()
p._actor = actor
return p, actor
def _wait_for_thread(p, attr="_sync_thread", timeout=5.0):
t = getattr(p, attr)
if t is not None:
t.join(timeout=timeout)
assert not t.is_alive(), f"{attr} did not finish within {timeout}s"
# ---- sync_turn first-turn path: write_note ----
def test_sync_turn_first_turn_calls_write_note(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps(
{
"permalink": "test-proj/hermes-sessions/hermes-session-2026-05-10-1234-abcdef",
"title": "Hermes Session 2026-05-10 1234 abcdef",
}
)
p.sync_turn("hello", "hi back")
_wait_for_thread(p, "_sync_thread")
actor.call.assert_called_once()
bm_tool, bm_args = actor.call.call_args[0][:2]
assert bm_tool == "write_note"
assert bm_args["project"] == "test-proj"
assert bm_args["directory"] == "hermes-sessions"
assert "Hermes Session" in bm_args["title"]
assert "hello" in bm_args["content"]
assert "hi back" in bm_args["content"]
assert "## Turns" in bm_args["content"]
assert bm_args["output_format"] == "json"
assert "hermes-session" in bm_args["tags"]
def test_sync_turn_first_turn_stores_extracted_permalink(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps(
{
"permalink": "test-proj/hermes-sessions/hermes-session-foo",
"title": "T",
}
)
p.sync_turn("u", "a")
_wait_for_thread(p, "_sync_thread")
assert p._session_note_id == "test-proj/hermes-sessions/hermes-session-foo"
def test_sync_turn_records_first_user_message(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "x"})
p.sync_turn("the very first user message", "reply")
_wait_for_thread(p, "_sync_thread")
assert p._first_user_msg == "the very first user message"
# A subsequent first-user-msg call should NOT overwrite the original
p.sync_turn("a much later user message", "another reply")
_wait_for_thread(p, "_sync_thread")
assert p._first_user_msg == "the very first user message"
# ---- sync_turn append path: edit_note ----
def test_sync_turn_subsequent_turn_calls_edit_note_append(bm):
p, actor = _provider_with_mock_actor(bm)
p._session_note_id = "test-proj/hermes-sessions/already-exists"
p.sync_turn("turn 2 user", "turn 2 assistant")
_wait_for_thread(p, "_sync_thread")
actor.call.assert_called_once()
bm_tool, bm_args = actor.call.call_args[0][:2]
assert bm_tool == "edit_note"
assert bm_args["identifier"] == "test-proj/hermes-sessions/already-exists"
assert bm_args["operation"] == "append"
assert "turn 2 user" in bm_args["content"]
assert "turn 2 assistant" in bm_args["content"]
def test_sync_turn_session_note_id_stable_across_turns(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "test-proj/folder/note-perma"})
p.sync_turn("u1", "a1")
_wait_for_thread(p, "_sync_thread")
first_id = p._session_note_id
# Reconfigure mock to return something different — should be IGNORED for
# the second turn since we're now using the existing permalink to append.
actor.call.return_value = json.dumps({"permalink": "wrong-id"})
p.sync_turn("u2", "a2")
_wait_for_thread(p, "_sync_thread")
assert p._session_note_id == first_id, "session_note_id mutated on second turn"
# And the second call was edit_note, not write_note
second_call_tool = actor.call.call_args_list[1][0][0]
assert second_call_tool == "edit_note"
# ---- sync_turn gating ----
def test_sync_turn_skipped_when_capture_per_turn_off(bm):
p, actor = _provider_with_mock_actor(bm)
p._capture_per_turn = False
p.sync_turn("u", "a")
actor.call.assert_not_called()
def test_sync_turn_skipped_when_uninitialized(bm):
p = bm.BasicMemoryProvider()
p._actor = MagicMock()
p.sync_turn("u", "a")
p._actor.call.assert_not_called()
def test_sync_turn_skipped_when_actor_none(bm):
p, _ = _provider_with_mock_actor(bm)
p._actor = None
p.sync_turn("u", "a") # should not raise
def test_sync_turn_skipped_when_circuit_open(bm):
import time as _time
p, actor = _provider_with_mock_actor(bm)
p._failure_pause_until = _time.monotonic() + 60.0
p.sync_turn("u", "a")
actor.call.assert_not_called()
def test_sync_turn_records_failure_on_actor_exception(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.side_effect = RuntimeError("boom")
p.sync_turn("u", "a")
_wait_for_thread(p, "_sync_thread")
assert p._failure_count >= 1
def test_sync_turn_thread_is_daemonic(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "x"})
p.sync_turn("u", "a")
assert p._sync_thread is not None
assert p._sync_thread.daemon is True
_wait_for_thread(p, "_sync_thread")
# ---- _capture_turn directly (no thread) ----
def test_capture_turn_first_call_writes_note_with_session_metadata(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "p/folder/note"})
p._capture_turn("u msg", "a msg")
bm_tool, bm_args = actor.call.call_args[0][:2]
assert bm_tool == "write_note"
assert "20260510_123456_abcdef" in bm_args["content"]
# Auto-captured banner present
assert "Auto-captured" in bm_args["content"]
def test_capture_turn_truncates_huge_messages(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "p/x"})
huge = "X" * 10000
p._capture_turn(huge, huge)
bm_args = actor.call.call_args[0][1]
# Body should not contain 10k Xs verbatim — _truncate caps at 4000
assert bm_args["content"].count("X") < 9000
assert "..." in bm_args["content"]
# ---- on_session_end summary ----
def test_on_session_end_writes_summary_note(bm):
p, actor = _provider_with_mock_actor(bm)
p._session_note_id = "p/folder/transcript"
actor.call.return_value = json.dumps({"permalink": "p/folder/summary"})
messages = [
{"role": "user", "content": "first user message"},
{"role": "assistant", "content": "first assistant"},
{"role": "user", "content": "second user"},
{"role": "assistant", "content": "last assistant message"},
]
p.on_session_end(messages)
actor.call.assert_called_once()
bm_tool, bm_args = actor.call.call_args[0][:2]
assert bm_tool == "write_note"
assert "Hermes Session Summary" in bm_args["title"]
assert bm_args["directory"] == "hermes-sessions"
assert "first user message" in bm_args["content"]
assert "last assistant message" in bm_args["content"]
# Summary should link back to the transcript via Relations when
# session_note_id is known
assert "summary_of [[p/folder/transcript]]" in bm_args["content"]
def test_on_session_end_omits_relations_when_no_transcript_id(bm):
p, actor = _provider_with_mock_actor(bm)
p._session_note_id = None
actor.call.return_value = json.dumps({"permalink": "p/folder/summary"})
p.on_session_end([{"role": "user", "content": "u"}, {"role": "assistant", "content": "a"}])
bm_args = actor.call.call_args[0][1]
assert "## Relations" not in bm_args["content"]
def test_on_session_end_handles_list_of_dicts_content(bm):
"""OpenAI-style content blocks: list of {type: text, text: ...}."""
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "p/x"})
messages = [
{"role": "user", "content": [{"type": "text", "text": "hello world"}]},
{"role": "assistant", "content": [{"type": "text", "text": "goodbye"}]},
]
p.on_session_end(messages)
bm_args = actor.call.call_args[0][1]
assert "hello world" in bm_args["content"]
assert "goodbye" in bm_args["content"]
def test_on_session_end_uses_first_user_msg_when_set(bm):
p, actor = _provider_with_mock_actor(bm)
p._first_user_msg = "captured-via-sync_turn"
actor.call.return_value = json.dumps({"permalink": "p/x"})
messages = [
{"role": "user", "content": "in-the-messages-list"},
{"role": "assistant", "content": "ok"},
]
p.on_session_end(messages)
bm_args = actor.call.call_args[0][1]
# _first_user_msg takes priority over messages list
assert "captured-via-sync_turn" in bm_args["content"]
def test_on_session_end_handles_empty_messages(bm):
p, actor = _provider_with_mock_actor(bm)
actor.call.return_value = json.dumps({"permalink": "p/x"})
p.on_session_end([])
bm_args = actor.call.call_args[0][1]
assert "(no user message)" in bm_args["content"]
assert "(no assistant message)" in bm_args["content"]
assert "0 user / 0 assistant" in bm_args["content"]
def test_on_session_end_skipped_when_disabled(bm):
p, actor = _provider_with_mock_actor(bm)
p._capture_session_end = False
p.on_session_end([{"role": "user", "content": "u"}])
actor.call.assert_not_called()
def test_on_session_end_skipped_when_uninitialized(bm):
p = bm.BasicMemoryProvider()
p._actor = MagicMock()
p.on_session_end([{"role": "user", "content": "u"}])
p._actor.call.assert_not_called()
def test_on_session_end_logs_and_swallows_errors(bm, caplog):
p, actor = _provider_with_mock_actor(bm)
actor.call.side_effect = RuntimeError("BM down")
# Should not raise — the summary is best-effort
p.on_session_end([{"role": "user", "content": "u"}])
# ---- shutdown lifecycle ----
def test_shutdown_clears_initialized_flag(bm):
p, actor = _provider_with_mock_actor(bm)
p.shutdown()
assert p._initialized is False
assert p._actor is None
actor.shutdown.assert_called_once()
def test_shutdown_swallows_actor_errors(bm):
p, actor = _provider_with_mock_actor(bm)
actor.shutdown.side_effect = RuntimeError("bad")
# Must not raise
p.shutdown()
assert p._initialized is False
def test_shutdown_idempotent(bm):
p, _ = _provider_with_mock_actor(bm)
p.shutdown()
p.shutdown() # should not raise
def test_shutdown_before_initialize_is_noop(bm):
p = bm.BasicMemoryProvider()
p.shutdown() # _actor is None, shouldn't error
+725
View File
@@ -0,0 +1,725 @@
"""
Tests for the plugin-owned /bm-* slash commands.
Covers:
- registration through ctx.register_command (forward-compat path)
- PluginManager reach-in (production path with current Hermes collector)
- per-handler behavior: usage text, uninitialized provider, happy path,
and exception plain-text error.
"""
from __future__ import annotations
import json
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from .conftest import FakeSession, make_scripted_actor
# ---------------------------------------------------------------------------
# Helpers for the reach-in tests
# ---------------------------------------------------------------------------
class _ProviderCollectorLike:
"""
Mirror of Hermes's real `_ProviderCollector` shape: captures
`register_memory_provider` and no-ops everything else. NOT a MagicMock
`hasattr(collector, "register_command")` must return False, matching the
real collector.
"""
def __init__(self):
self.provider = None
def register_memory_provider(self, provider):
self.provider = provider
class _FakePluginManager:
"""Stand-in for hermes_cli.plugins.PluginManager — just the bits we touch."""
def __init__(self):
self._plugin_commands: dict = {}
self._plugin_skills: dict = {}
def _install_fake_hermes_cli(monkeypatch, *, resolve_returns=None):
"""
Insert a fake `hermes_cli.plugins` (with `_ensure_plugins_discovered`) and
`hermes_cli.commands` (with `resolve_command`) into sys.modules so the
reach-in's lazy imports resolve. Returns the FakePluginManager instance
so tests can assert against its registries.
resolve_returns: optional mapping of command name truthy/falsy value
the fake resolve_command should return. Use a truthy value to simulate a
built-in conflict for that name.
"""
fake_mgr = _FakePluginManager()
plugins_mod = types.ModuleType("hermes_cli.plugins")
def _ensure_plugins_discovered(force: bool = False):
return fake_mgr
plugins_mod._ensure_plugins_discovered = _ensure_plugins_discovered # type: ignore[attr-defined]
commands_mod = types.ModuleType("hermes_cli.commands")
def _resolve_command(name: str):
if resolve_returns and name in resolve_returns:
return resolve_returns[name]
return None
commands_mod.resolve_command = _resolve_command # type: ignore[attr-defined]
hermes_cli = types.ModuleType("hermes_cli")
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli)
monkeypatch.setitem(sys.modules, "hermes_cli.plugins", plugins_mod)
monkeypatch.setitem(sys.modules, "hermes_cli.commands", commands_mod)
return fake_mgr
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
_EXPECTED_COMMANDS = {
"bm-search",
"bm-read",
"bm-context",
"bm-recent",
"bm-status",
"bm-remember",
"bm-project",
"bm-workspace",
}
def test_register_wires_up_all_slash_commands_on_modern_ctx(bm):
"""Forward-compat path: when ctx supports register_command (e.g. after the
upstream collector patch lands, or for plugins loaded via PluginContext),
every /bm-* command is registered through that path."""
ctx = MagicMock()
bm._active_providers.clear()
bm.register(ctx)
names = {call.args[0] for call in ctx.register_command.call_args_list}
assert names == _EXPECTED_COMMANDS
bm._active_providers.clear()
def test_register_command_calls_include_description_and_args_hint(bm):
ctx = MagicMock()
bm._active_providers.clear()
bm.register(ctx)
for call in ctx.register_command.call_args_list:
name, handler = call.args[0], call.args[1]
kwargs = call.kwargs
assert callable(handler)
assert "description" in kwargs and kwargs["description"]
assert "args_hint" in kwargs # may be empty string for no-arg commands
bm._active_providers.clear()
def test_register_tolerates_old_hermes_without_register_command(bm):
"""Plugins must not crash on Hermes < v0.11.0 (no register_command)."""
class _OldCtx:
def __init__(self):
self.memory_calls = []
def register_memory_provider(self, provider):
self.memory_calls.append(provider)
ctx = _OldCtx()
bm._active_providers.clear()
bm.register(ctx) # must not raise
assert len(ctx.memory_calls) == 1
bm._active_providers.clear()
def test_register_swallows_register_command_errors(bm, caplog):
"""If one register_command call fails, the others — and provider
registration still proceed."""
ctx = MagicMock()
ctx.register_command.side_effect = ValueError("name collision with builtin")
bm._active_providers.clear()
with caplog.at_level("WARNING"):
bm.register(ctx)
ctx.register_memory_provider.assert_called_once()
assert ctx.register_command.call_count == len(_EXPECTED_COMMANDS)
assert "register_command" in caplog.text
bm._active_providers.clear()
# ---------------------------------------------------------------------------
# Reach-in (production path): ctx is the no-op _ProviderCollector
# ---------------------------------------------------------------------------
def test_reach_in_writes_all_commands_to_plugin_manager(bm, monkeypatch):
"""Regression for Codex P1: with the real collector shape (no
register_command method), reach into PluginManager and write commands
directly. The unit suite previously used MagicMock, which masked this
silent-skip by making every attribute exist."""
fake_mgr = _install_fake_hermes_cli(monkeypatch)
ctx = _ProviderCollectorLike()
assert not hasattr(ctx, "register_command"), (
"test collector must mirror real _ProviderCollector — no register_command"
)
bm._active_providers.clear()
bm.register(ctx)
try:
assert ctx.provider is not None # memory provider still registered
assert set(fake_mgr._plugin_commands.keys()) == _EXPECTED_COMMANDS
for name, entry in fake_mgr._plugin_commands.items():
assert callable(entry["handler"])
assert entry["plugin"] == "basic-memory"
assert "description" in entry
assert "args_hint" in entry
finally:
bm._active_providers.clear()
def test_reach_in_writes_skill_to_plugin_manager(bm, monkeypatch):
"""Same silent-skip applies to register_skill — the bundled skill never
landed in real installs prior to this fix. Reach-in writes the namespaced
entry directly."""
fake_mgr = _install_fake_hermes_cli(monkeypatch)
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
bm.register(ctx)
try:
assert "basic-memory:basic-memory" in fake_mgr._plugin_skills
skill = fake_mgr._plugin_skills["basic-memory:basic-memory"]
assert skill["plugin"] == "basic-memory"
assert skill["bare_name"] == "basic-memory"
assert isinstance(skill["path"], Path)
assert skill["path"].name == "SKILL.md"
finally:
bm._active_providers.clear()
def test_reach_in_skips_command_conflicting_with_builtin(bm, monkeypatch, caplog):
"""Mirror Hermes's PluginContext.register_command guard — when
resolve_command(name) returns a truthy value, skip that command and
log a warning rather than overwriting a built-in."""
# Simulate /bm-search colliding with a built-in.
fake_mgr = _install_fake_hermes_cli(monkeypatch, resolve_returns={"bm-search": object()})
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
with caplog.at_level("WARNING"):
bm.register(ctx)
try:
assert "bm-search" not in fake_mgr._plugin_commands
# Other commands still landed
assert "bm-read" in fake_mgr._plugin_commands
assert "conflicts with a built-in" in caplog.text
finally:
bm._active_providers.clear()
def test_reach_in_degrades_when_hermes_cli_missing(bm, monkeypatch, caplog):
"""If hermes_cli.plugins isn't importable (e.g. running outside a Hermes
install), the reach-in must log and continue never crash the plugin's
memory-provider registration."""
# Don't install fake modules; force import to fail.
monkeypatch.setitem(sys.modules, "hermes_cli.plugins", None) # type: ignore[arg-type]
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
with caplog.at_level("DEBUG"):
bm.register(ctx) # must not raise
try:
assert ctx.provider is not None
# Either DEBUG message logged or nothing — both acceptable degrade modes.
finally:
bm._active_providers.clear()
def test_reach_in_degrades_when_plugin_manager_missing_attrs(bm, monkeypatch):
"""Forward-compat: if Hermes ever refactors _plugin_commands /
_plugin_skills away, the reach-in must not crash."""
fake_mgr = _FakePluginManager()
# Strip the attrs to simulate the rename/refactor
del fake_mgr._plugin_commands
del fake_mgr._plugin_skills
plugins_mod = types.ModuleType("hermes_cli.plugins")
plugins_mod._ensure_plugins_discovered = lambda force=False: fake_mgr # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "hermes_cli.plugins", plugins_mod)
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
bm.register(ctx) # must not raise
bm._active_providers.clear()
def test_reach_in_normalizes_command_names(bm, monkeypatch):
"""Reach-in must mirror Hermes's name normalization (lowercase, strip,
leading slash removed, spaces hyphens). All our names are already
canonical, so this is a defensive check on the transform itself."""
fake_mgr = _install_fake_hermes_cli(monkeypatch)
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
bm.register(ctx)
try:
for name in fake_mgr._plugin_commands:
assert name == name.lower()
assert not name.startswith("/")
assert " " not in name
finally:
bm._active_providers.clear()
def test_reach_in_entries_match_hermes_internal_shape(bm, monkeypatch):
"""The dict shape PluginContext.register_command writes
(plugins.py:447-452) is the contract Hermes's dispatch reads. Our
reach-in must produce byte-identical entries."""
fake_mgr = _install_fake_hermes_cli(monkeypatch)
ctx = _ProviderCollectorLike()
bm._active_providers.clear()
bm.register(ctx)
try:
entry = fake_mgr._plugin_commands["bm-search"]
assert set(entry.keys()) == {"handler", "description", "plugin", "args_hint"}
assert callable(entry["handler"])
assert entry["description"] # non-empty string
assert entry["plugin"] == "basic-memory"
assert isinstance(entry["args_hint"], str)
finally:
bm._active_providers.clear()
# ---------------------------------------------------------------------------
# Per-handler tests
# ---------------------------------------------------------------------------
def _ready_provider(bm, session: FakeSession | None = None):
"""Build a provider in 'initialized' state with a scripted actor."""
provider = bm.BasicMemoryProvider()
actor = make_scripted_actor(bm, session=session)
actor.start()
provider._actor = actor
provider._initialized = True
provider._project = "test-proj"
return provider, actor
def _handlers_by_name(bm, provider):
return {name: handler for name, handler, _, _ in bm._build_slash_commands(provider)}
# ---- Usage strings ----
@pytest.mark.parametrize(
"name,args",
[
("bm-search", ""),
("bm-search", "help"),
("bm-read", ""),
("bm-read", "-h"),
("bm-context", ""),
("bm-remember", ""),
("bm-remember", "--help"),
# Commands that take no args use 'help' to surface their usage line
("bm-recent", "help"),
("bm-status", "help"),
("bm-project", "help"),
("bm-workspace", "help"),
],
)
def test_usage_returned_for_empty_or_help_args(bm, name, args):
provider = bm.BasicMemoryProvider() # not initialized — usage path shouldn't need it
handlers = _handlers_by_name(bm, provider)
out = handlers[name](args)
assert isinstance(out, str)
assert out.lower().startswith("usage:")
# ---- Uninitialized provider ----
@pytest.mark.parametrize(
"name,args",
[
("bm-search", "hello"),
("bm-read", "some/note"),
("bm-context", "memory://x"),
("bm-recent", ""),
("bm-remember", "a thought"),
("bm-project", ""),
],
)
def test_handler_init_failure_returns_message(bm, monkeypatch, name, args):
provider = bm.BasicMemoryProvider()
monkeypatch.setattr(provider, "initialize", lambda *a, **kw: None)
handlers = _handlers_by_name(bm, provider)
out = handlers[name](args)
assert "not initialized" in out
assert name in out # message includes command name
def test_handler_lazily_initializes_provider_for_slash_command(bm, monkeypatch):
provider = bm.BasicMemoryProvider()
calls = []
def fake_initialize(*args, **kwargs):
calls.append((args, kwargs))
session = FakeSession(default_response={"results": []})
actor = make_scripted_actor(bm, session=session)
actor.start()
provider._actor = actor
provider._initialized = True
monkeypatch.setattr(provider, "initialize", fake_initialize)
out = _handlers_by_name(bm, provider)["bm-search"]("widgets")
assert "No results" in out
assert len(calls) == 1
assert calls[0][1]["session_id"].startswith("slash:bm-search:")
# ---- /bm-status ----
def test_bm_status_renders_provider_state(bm, monkeypatch):
provider = bm.BasicMemoryProvider()
provider._mode = "local"
provider._project = "demo"
provider._project_path = "/tmp/demo"
provider._capture_per_turn = True
provider._capture_session_end = False
provider._capture_folder = "transcripts"
provider._remember_folder = "inbox"
monkeypatch.setattr(bm, "_bm_binary_path", lambda: "/fake/bin/bm")
out = _handlers_by_name(bm, provider)["bm-status"]("")
assert "demo" in out
assert "/tmp/demo" in out
assert "/fake/bin/bm" in out
assert "Initialized: no" in out
assert "transcripts" in out and "inbox" in out
# ---- /bm-search ----
def test_bm_search_happy_path(bm):
session = FakeSession()
session.stub(
"search_notes",
lambda args: {
"results": [
{"title": "Decisions", "permalink": "decisions/foo", "content": "we chose X"},
{"title": "Plan", "permalink": "plans/p1", "preview": "next quarter"},
]
},
)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-search"]("widgets")
assert "Decisions" in out
assert "decisions/foo" in out
assert "we chose X" in out
# Args sent to BM
call = session.calls[-1]
assert call[0] == "search_notes"
assert call[1]["query"] == "widgets"
assert call[1]["project"] == "test-proj"
assert call[1]["output_format"] == "json"
finally:
actor.shutdown()
def test_bm_search_empty_results(bm):
session = FakeSession(default_response={"results": []})
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-search"]("missing")
assert "No results" in out
assert "missing" in out
finally:
actor.shutdown()
def test_bm_search_actor_exception_returns_plain_string(bm):
session = FakeSession()
def _boom(_args):
raise RuntimeError("MCP transport closed")
session.stub("search_notes", _boom)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-search"]("anything")
assert isinstance(out, str)
assert out.startswith("bm-search:")
assert "MCP transport closed" in out
finally:
actor.shutdown()
# ---- /bm-read ----
def test_bm_read_returns_text_body(bm):
"""BM's read_note returns markdown wrapped in {"text": "..."} once our
extractor wraps the non-JSON response. The handler should unwrap and
return the bare markdown."""
session = FakeSession()
session.stub(
"read_note",
# FakeSession serializes whatever the handler returns; emit the JSON
# the wrapper would produce for a markdown response.
lambda args: {"text": "# Foo\n\nbody text"},
)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-read"]("foo")
assert out == "# Foo\n\nbody text"
assert session.calls[-1][1]["identifier"] == "foo"
finally:
actor.shutdown()
# ---- /bm-recent ----
def test_bm_recent_default_timeframe(bm):
session = FakeSession(default_response={"results": []})
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-recent"]("")
assert "7d" in out
assert session.calls[-1][1]["timeframe"] == "7d"
finally:
actor.shutdown()
def test_bm_recent_custom_timeframe(bm):
session = FakeSession()
session.stub(
"recent_activity",
lambda args: {"results": [{"title": "Recent thing", "permalink": "x/y"}]},
)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-recent"]("2 weeks")
assert "2 weeks" in out
assert "Recent thing" in out
assert session.calls[-1][1]["timeframe"] == "2 weeks"
finally:
actor.shutdown()
def test_bm_recent_bare_list_shape(bm):
"""Regression: BM's `recent_activity(output_format="json")` returns a bare
`list[dict]` (signature: `-> str | list[dict]`), not a dict-with-results.
The handler must surface those rows, not report "no activity"."""
session = FakeSession()
session.stub(
"recent_activity",
lambda args: [
{"title": "Edited yesterday", "permalink": "notes/a", "content": "blob"},
{"title": "Edited 3d ago", "permalink": "notes/b"},
],
)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-recent"]("")
assert "Edited yesterday" in out
assert "Edited 3d ago" in out
assert "No activity" not in out
finally:
actor.shutdown()
# ---- /bm-remember ----
def test_bm_remember_derives_title_from_first_line(bm):
captured = {}
def _write(args):
captured.update(args)
return {"permalink": "bm-remember/note-perm"}
session = FakeSession()
session.stub("write_note", _write)
provider, actor = _ready_provider(bm, session)
provider._remember_folder = "bm-remember"
try:
out = _handlers_by_name(bm, provider)["bm-remember"](
"Quarterly OKR review notes\n\nWe agreed to ship X."
)
assert "Saved:" in out
assert "bm-remember/note-perm" in out
assert captured["title"] == "Quarterly OKR review notes"
assert captured["directory"] == "bm-remember"
assert "manual-capture" in captured["tags"]
finally:
actor.shutdown()
def test_bm_remember_long_first_line_truncated_to_80(bm):
session = FakeSession(default_response={"permalink": "x"})
provider, actor = _ready_provider(bm, session)
try:
long_line = "A" * 200
_handlers_by_name(bm, provider)["bm-remember"](long_line)
title = session.calls[-1][1]["title"]
assert len(title) == 80
finally:
actor.shutdown()
def test_bm_remember_uses_configured_folder(bm):
session = FakeSession(default_response={"permalink": "x"})
provider, actor = _ready_provider(bm, session)
provider._remember_folder = "scratch"
try:
_handlers_by_name(bm, provider)["bm-remember"]("hello")
assert session.calls[-1][1]["directory"] == "scratch"
finally:
actor.shutdown()
# ---- /bm-project ----
def test_bm_project_lists_and_marks_active(bm):
session = FakeSession()
session.stub(
"list_memory_projects",
lambda args: {
"projects": [
{"name": "other-proj"},
{"name": "test-proj"},
]
},
)
provider, actor = _ready_provider(bm, session)
try:
out = _handlers_by_name(bm, provider)["bm-project"]("")
assert "other-proj" in out
assert "test-proj" in out
# Active project line includes the marker
active_line = next(line for line in out.splitlines() if "test-proj" in line)
assert "active" in active_line
finally:
actor.shutdown()
# ---- /bm-workspace ----
def test_bm_workspace_local_mode_message(bm):
provider, actor = _ready_provider(bm)
provider._mode = "local"
try:
out = _handlers_by_name(bm, provider)["bm-workspace"]("")
assert "Cloud" in out
assert "local" in out
finally:
actor.shutdown()
def test_bm_workspace_cloud_mode_lists(bm):
session = FakeSession()
session.stub(
"list_workspaces",
lambda args: {
"workspaces": [
{
"name": "Personal",
"workspace_type": "personal",
"role": "owner",
"is_default": True,
},
{"name": "Acme", "workspace_type": "team", "role": "member"},
]
},
)
provider, actor = _ready_provider(bm, session)
provider._mode = "cloud"
try:
out = _handlers_by_name(bm, provider)["bm-workspace"]("")
assert "Personal" in out
assert "Acme" in out
assert "default" in out
finally:
actor.shutdown()
def test_bm_workspace_lazily_initializes_before_mode_check(bm, monkeypatch):
session = FakeSession()
session.stub(
"list_workspaces",
lambda args: {"workspaces": [{"name": "Personal", "workspace_type": "personal"}]},
)
provider = bm.BasicMemoryProvider()
calls = []
def fake_initialize(*args, **kwargs):
calls.append((args, kwargs))
actor = make_scripted_actor(bm, session=session)
actor.start()
provider._actor = actor
provider._initialized = True
provider._mode = "cloud"
monkeypatch.setattr(provider, "initialize", fake_initialize)
try:
out = _handlers_by_name(bm, provider)["bm-workspace"]("")
assert "Personal" in out
assert "no workspaces to list" not in out
assert calls
assert session.calls[-1][0] == "list_workspaces"
finally:
if provider._actor is not None:
provider._actor.shutdown()
# ---- _unwrap_json_or_text helper ----
def test_unwrap_passes_through_raw_string(bm):
assert bm._unwrap_json_or_text("plain text") == "plain text"
def test_unwrap_returns_inner_json_when_text_wraps_json(bm):
outer = json.dumps({"text": json.dumps({"a": 1})})
assert bm._unwrap_json_or_text(outer) == {"a": 1}
def test_unwrap_returns_text_value_when_inner_is_markdown(bm):
outer = json.dumps({"text": "# Heading\n\nbody"})
assert bm._unwrap_json_or_text(outer) == "# Heading\n\nbody"
def test_unwrap_returns_dict_when_top_level_json(bm):
outer = json.dumps({"results": [1, 2]})
assert bm._unwrap_json_or_text(outer) == {"results": [1, 2]}
# ---- _remember_title ----
def test_remember_title_strips_markdown_heading(bm):
assert bm._remember_title("# Decisions\n\nbody") == "Decisions"
def test_remember_title_skips_blank_lines(bm):
assert bm._remember_title("\n\nFirst real line\nrest") == "First real line"
def test_remember_title_falls_back_to_timestamp(bm):
title = bm._remember_title(" \n\n")
assert title.startswith("Note ")
+531
View File
@@ -0,0 +1,531 @@
"""Unit tests for the pure helpers in __init__.py."""
import json
import pytest
# ---- _truncate ----
def test_truncate_short_passes_through(bm):
assert bm._truncate("hello", 10) == "hello"
def test_truncate_long_gets_ellipsis(bm):
out = bm._truncate("a" * 100, 10)
assert out.endswith("...")
assert len(out) == 10
def test_truncate_non_string_coerced(bm):
assert bm._truncate(42, 10) == "42"
def test_truncate_none(bm):
assert bm._truncate(None, 10) == ""
# ---- _join_message_content ----
def test_join_string_content(bm):
assert bm._join_message_content("hello") == "hello"
def test_join_list_of_dicts(bm):
parts = [{"text": "a"}, {"text": "b"}, {"content": "c"}]
assert bm._join_message_content(parts) == "a\nb\nc"
def test_join_list_of_strings(bm):
parts = ["a", "b"]
assert bm._join_message_content(parts) == "a\nb"
def test_join_mixed(bm):
parts = ["a", {"text": "b"}, {"foo": "bar"}, "c"]
assert bm._join_message_content(parts) == "a\nb\nc"
def test_join_none(bm):
assert bm._join_message_content(None) == ""
# ---- _coerce_bool ----
@pytest.mark.parametrize(
"value,expected",
[
(True, True),
(False, False),
("true", True),
("True", True),
("YES", True),
("1", True),
("y", True),
("false", False),
("False", False),
("NO", False),
("0", False),
("n", False),
],
)
def test_coerce_bool(bm, value, expected):
assert bm._coerce_bool(value) is expected
def test_coerce_bool_non_bool_passes_through(bm):
assert bm._coerce_bool(42) == 42
assert bm._coerce_bool("hello") == "hello"
# ---- _extract_mcp_text ----
def test_extract_mcp_text_passes_json_through(bm, fake_result):
payload = json.dumps({"permalink": "foo/bar", "title": "T"})
out = bm._extract_mcp_text(fake_result([payload]))
assert json.loads(out)["permalink"] == "foo/bar"
def test_extract_mcp_text_wraps_markdown(bm, fake_result):
md = "# Created note\npermalink: foo/bar"
out = bm._extract_mcp_text(fake_result([md]))
parsed = json.loads(out)
assert parsed["text"] == md
def test_extract_mcp_text_joins_multiple_blocks(bm, fake_result):
out = bm._extract_mcp_text(fake_result(["a", "b"]))
parsed = json.loads(out)
assert parsed["text"] == "a\nb"
def test_extract_mcp_text_empty(bm, fake_result):
out = bm._extract_mcp_text(fake_result([]))
assert json.loads(out) == {"ok": True}
def test_extract_mcp_text_error(bm, fake_result):
out = bm._extract_mcp_text(fake_result(["something broke"], is_error=True))
assert "error" in json.loads(out)
# ---- _extract_permalink ----
def test_extract_permalink_from_bare_json(bm):
text = json.dumps({"permalink": "proj/folder/note", "title": "T"})
assert bm._extract_permalink(text, "fb") == "proj/folder/note"
def test_extract_permalink_from_wrapped_json(bm):
text = json.dumps({"text": json.dumps({"permalink": "proj/folder/note"})})
assert bm._extract_permalink(text, "fb") == "proj/folder/note"
def test_extract_permalink_from_wrapped_markdown(bm):
md = (
"# Created note\n"
"project: hermes-jodys-imac\n"
"file_path: x/y.md\n"
"permalink: hermes-jodys-imac/folder/slug-name\n"
"checksum: unknown\n"
)
text = json.dumps({"text": md})
assert bm._extract_permalink(text, "fb") == "hermes-jodys-imac/folder/slug-name"
def test_extract_permalink_from_raw_markdown(bm):
md = "# Created note\npermalink: proj/folder/slug\n"
# Raw, not wrapped — strategy 4 path
assert bm._extract_permalink(md, "fb") == "proj/folder/slug"
def test_extract_permalink_no_match(bm):
assert bm._extract_permalink('{"foo":"bar"}', "fallback-title") == "fallback-title"
def test_extract_permalink_empty(bm):
assert bm._extract_permalink("", "fb") == "fb"
def test_extract_permalink_invalid(bm):
assert bm._extract_permalink("not json or markdown", "fb") == "fb"
def test_extract_permalink_strips_trailing_punct(bm):
md = "# Created note\npermalink: proj/folder/slug,"
assert bm._extract_permalink(md, "fb") == "proj/folder/slug"
# ---- _translate_args ----
def test_translate_search(bm):
tool, args = bm._translate_args("bm_search", {"query": "hi", "limit": 7}, "proj")
assert tool == "search_notes"
assert args == {"project": "proj", "query": "hi", "page_size": 7}
def test_translate_search_no_limit(bm):
tool, args = bm._translate_args("bm_search", {"query": "hi"}, "proj")
assert tool == "search_notes"
assert args == {"project": "proj", "query": "hi"}
def test_translate_read(bm):
tool, args = bm._translate_args("bm_read", {"identifier": "x/y"}, "proj")
assert tool == "read_note"
assert args == {"project": "proj", "identifier": "x/y"}
def test_translate_read_workspace_qualified_identifier_self_routes(bm):
tool, args = bm._translate_args(
"bm_read",
{"identifier": "personal/main/scratch/note"},
"hermes-memory",
)
assert tool == "read_note"
assert args == {"identifier": "personal/main/scratch/note"}
def test_translate_read_org_workspace_qualified_identifier_self_routes(bm):
tool, args = bm._translate_args(
"bm_read",
{"identifier": "basic-memory-7020de4e925843c68c9056c60d101d9e/main/scratch/note"},
"hermes-memory",
)
assert tool == "read_note"
assert args == {"identifier": "basic-memory-7020de4e925843c68c9056c60d101d9e/main/scratch/note"}
def test_translate_write(bm):
tool, args = bm._translate_args(
"bm_write",
{"title": "T", "content": "C", "folder": "F", "tags": ["a", "b"]},
"proj",
)
assert tool == "write_note"
assert args == {
"project": "proj",
"title": "T",
"content": "C",
"directory": "F",
"tags": ["a", "b"],
}
def test_translate_write_no_tags(bm):
tool, args = bm._translate_args(
"bm_write",
{"title": "T", "content": "C", "folder": "F"},
"proj",
)
assert tool == "write_note"
assert "tags" not in args
assert args["directory"] == "F"
def test_translate_edit_minimal(bm):
tool, args = bm._translate_args(
"bm_edit",
{"identifier": "x", "operation": "append", "content": "more"},
"proj",
)
assert tool == "edit_note"
assert args == {
"project": "proj",
"identifier": "x",
"operation": "append",
"content": "more",
}
def test_translate_edit_find_replace(bm):
tool, args = bm._translate_args(
"bm_edit",
{
"identifier": "x",
"operation": "find_replace",
"content": "new",
"find_text": "old",
},
"proj",
)
assert args["find_text"] == "old"
def test_translate_edit_replace_section(bm):
tool, args = bm._translate_args(
"bm_edit",
{
"identifier": "x",
"operation": "replace_section",
"content": "new",
"section": "## Notes",
},
"proj",
)
assert args["section"] == "## Notes"
def test_translate_context(bm):
tool, args = bm._translate_args("bm_context", {"url": "memory://x", "depth": 2}, "proj")
assert tool == "build_context"
assert args == {"project": "proj", "url": "memory://x", "depth": 2}
def test_translate_context_workspace_qualified_url_self_routes(bm):
tool, args = bm._translate_args(
"bm_context",
{"url": "memory://personal/main/scratch/note", "depth": 1},
"hermes-memory",
)
assert tool == "build_context"
assert args == {"url": "memory://personal/main/scratch/note", "depth": 1}
def test_translate_context_org_workspace_qualified_url_self_routes(bm):
tool, args = bm._translate_args(
"bm_context",
{
"url": "memory://basic-memory-7020de4e925843c68c9056c60d101d9e/main/scratch/note",
"depth": 1,
},
"hermes-memory",
)
assert tool == "build_context"
assert args == {
"url": "memory://basic-memory-7020de4e925843c68c9056c60d101d9e/main/scratch/note",
"depth": 1,
}
def test_translate_delete(bm):
tool, args = bm._translate_args("bm_delete", {"identifier": "x"}, "proj")
assert tool == "delete_note"
assert args == {"project": "proj", "identifier": "x"}
def test_translate_move(bm):
tool, args = bm._translate_args(
"bm_move", {"identifier": "x", "new_folder": "archive/2026"}, "proj"
)
assert tool == "move_note"
assert args == {
"project": "proj",
"identifier": "x",
"destination_folder": "archive/2026",
}
def test_translate_recent_defaults(bm):
tool, args = bm._translate_args("bm_recent", {}, "proj")
assert tool == "recent_activity"
assert args == {"project": "proj"}
def test_translate_recent_full(bm):
tool, args = bm._translate_args(
"bm_recent",
{"timeframe": "2 weeks", "limit": 25, "type": "entity"},
"proj",
)
assert tool == "recent_activity"
assert args == {
"project": "proj",
"timeframe": "2 weeks",
"page_size": 25,
"type": "entity",
}
# ---- Per-call project routing ----
def test_translate_uses_default_project_when_no_override(bm):
"""Existing behavior preserved: with no project override, the configured
default flows through."""
_, args = bm._translate_args("bm_search", {"query": "hi"}, "default-proj")
assert args["project"] == "default-proj"
assert "project_id" not in args
def test_translate_uses_project_name_override(bm):
"""Agent passes project="main" → that name reaches BM, not the default."""
_, args = bm._translate_args("bm_search", {"query": "hi", "project": "main"}, "default-proj")
assert args["project"] == "main"
assert "project_id" not in args
def test_translate_uses_project_id_override(bm):
"""Agent passes project_id=<uuid> → reaches BM as project_id, with no
project name in the call (would be redundant and risk server-side
precedence surprises)."""
uuid = "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d"
_, args = bm._translate_args("bm_search", {"query": "hi", "project_id": uuid}, "default-proj")
assert args["project_id"] == uuid
assert "project" not in args
def test_translate_project_id_wins_when_both_supplied(bm):
"""If the agent passes both, project_id is the more specific identifier
(UUID across workspaces) and takes precedence. Only project_id reaches BM."""
uuid = "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d"
_, args = bm._translate_args(
"bm_search",
{"query": "hi", "project": "main", "project_id": uuid},
"default-proj",
)
assert args["project_id"] == uuid
assert "project" not in args
def test_translate_routing_coerces_to_string(bm):
"""Defensive: if a model passes a non-string identifier (e.g. an int),
coerce rather than crash. BM accepts strings."""
_, args = bm._translate_args("bm_search", {"query": "hi", "project_id": 12345}, "default-proj")
assert args["project_id"] == "12345"
@pytest.mark.parametrize(
"tool,base_args",
[
("bm_search", {"query": "x"}),
("bm_read", {"identifier": "x"}),
("bm_write", {"title": "t", "content": "c", "folder": "f"}),
("bm_edit", {"identifier": "x", "operation": "append", "content": "c"}),
("bm_context", {"url": "memory://x"}),
("bm_delete", {"identifier": "x"}),
("bm_move", {"identifier": "x", "new_folder": "f"}),
("bm_recent", {}),
],
)
def test_translate_routing_works_for_every_tool(bm, tool, base_args):
"""Routing applies uniformly across every per-project tool. Global
discovery tools (bm_projects, bm_workspaces) are tested separately."""
args_with = dict(base_args, project="main")
_, out = bm._translate_args(tool, args_with, "default-proj")
assert out["project"] == "main"
args_with_id = dict(base_args, project_id="e1d3a5b8-0492-4c1f-8e7d-2a4b6c8d0e2f")
_, out = bm._translate_args(tool, args_with_id, "default-proj")
assert out["project_id"] == "e1d3a5b8-0492-4c1f-8e7d-2a4b6c8d0e2f"
assert "project" not in out
_, out = bm._translate_args(tool, base_args, "default-proj")
assert out["project"] == "default-proj"
# ---- Global discovery tools (bm_projects, bm_workspaces) ----
def test_translate_bm_projects_no_routing(bm):
"""bm_projects is a global discovery tool — it lists across all projects
and workspaces. _translate_args must NOT inject a default project
(would make BM scope the listing) and MUST request JSON so the agent
can parse identifiers out of the response."""
tool, out = bm._translate_args("bm_projects", {}, "default-proj")
assert tool == "list_memory_projects"
assert "project" not in out
assert "project_id" not in out
assert out == {"output_format": "json"}
def test_translate_bm_workspaces_no_routing(bm):
tool, out = bm._translate_args("bm_workspaces", {}, "default-proj")
assert tool == "list_workspaces"
assert "project" not in out
assert "project_id" not in out
assert out == {"output_format": "json"}
def test_translate_global_tools_ignore_project_kwargs(bm):
"""Even if a confused caller passes project/project_id to a global tool,
those args are dropped BM doesn't accept them and silently scoping
the listing would be worse than ignoring the args."""
_, out = bm._translate_args(
"bm_projects",
{"project": "main", "project_id": "e1d3a5b8-0492-4c1f-8e7d-2a4b6c8d0e2f"},
"default-proj",
)
assert "project" not in out
assert "project_id" not in out
# ---- TOOL_SCHEMAS routing properties ----
def test_every_tool_schema_advertises_project_routing(bm):
"""Every per-project bm_* tool must expose `project` and `project_id` so
the agent sees them in the tool surface. Regression: forgetting to add
routing props to a new tool would silently lock the agent into the
active project exactly the friction Drew's note flagged.
Global discovery tools (bm_projects, bm_workspaces) are excluded they
list across projects/workspaces and don't take routing args."""
for schema in bm.TOOL_SCHEMAS:
props = schema["parameters"]["properties"]
if schema["name"] in bm._GLOBAL_TOOLS:
assert "project" not in props, (
f"{schema['name']} is a global tool; should not have project prop"
)
assert "project_id" not in props, (
f"{schema['name']} is a global tool; should not have project_id prop"
)
continue
assert "project" in props, f"{schema['name']} missing project prop"
assert "project_id" in props, f"{schema['name']} missing project_id prop"
# Routing is always optional — never in `required`.
required = schema["parameters"].get("required", [])
assert "project" not in required
assert "project_id" not in required
# ---- _default_project / _hostname ----
def test_default_project_format(bm):
p = bm._default_project()
assert p.startswith("hermes-")
# Hostnames are lowercased and stripped
assert " " not in p
def test_hostname_lowercased(bm, monkeypatch):
monkeypatch.setattr(bm.socket, "gethostname", lambda: "Some.Long.Host")
assert bm._hostname() == "some"
# ---- TOOL_SCHEMAS ----
def test_tool_schemas_complete(bm):
names = {s["name"] for s in bm.TOOL_SCHEMAS}
expected = {
"bm_search",
"bm_read",
"bm_write",
"bm_edit",
"bm_context",
"bm_delete",
"bm_move",
"bm_recent",
"bm_projects",
"bm_workspaces",
}
assert names == expected
def test_tool_schemas_have_descriptions(bm):
for s in bm.TOOL_SCHEMAS:
assert s["description"], f"{s['name']} missing description"
assert "parameters" in s
assert s["parameters"]["type"] == "object"
def test_hermes_to_bm_complete(bm):
assert set(bm._HERMES_TO_BM.keys()) == {s["name"] for s in bm.TOOL_SCHEMAS}
@@ -0,0 +1,423 @@
"""
Gated integration tests against a real basic-memory MCP server.
These tests spin up the real `bm mcp` subprocess via the production actor and
exercise every tool through `handle_tool_call`, mirroring the production code
path. They are skipped unless BOTH:
BM_INTEGRATION=1
AND `bm` is installed AND `mcp` Python package is importable
A throwaway BM project is created for the test session and removed afterward,
so these tests never touch your real BM projects.
Run them with:
BM_INTEGRATION=1 uv run --with pytest --with mcp pytest tests/test_integration.py
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import tempfile
import time
import uuid
import pytest
# ---- Gating ----
_INTEGRATION_ENABLED = os.environ.get("BM_INTEGRATION") == "1"
_BM_BIN = shutil.which("bm") or (
os.path.expanduser("~/.local/bin/bm")
if os.path.isfile(os.path.expanduser("~/.local/bin/bm"))
else None
)
try:
import mcp # noqa: F401
_MCP_OK = True
except Exception:
_MCP_OK = False
pytestmark = [
pytest.mark.skipif(
not _INTEGRATION_ENABLED,
reason="set BM_INTEGRATION=1 to run integration tests",
),
pytest.mark.skipif(_BM_BIN is None, reason="bm CLI not on PATH"),
pytest.mark.skipif(not _MCP_OK, reason="mcp Python package not installed"),
]
# ---- Session-scoped BM project ----
@pytest.fixture(scope="session")
def temp_bm_project():
"""Create a throwaway BM project for the session; remove when done."""
project_name = f"hermes-bm-test-{uuid.uuid4().hex[:8]}"
project_dir = tempfile.mkdtemp(prefix=f"{project_name}-")
# Register
subprocess.run(
[_BM_BIN, "project", "add", project_name, project_dir],
check=False,
capture_output=True,
timeout=20,
)
yield project_name, project_dir
# Tear down
subprocess.run(
[_BM_BIN, "project", "remove", project_name],
check=False,
capture_output=True,
timeout=20,
)
shutil.rmtree(project_dir, ignore_errors=True)
@pytest.fixture
def provider(bm, temp_bm_project, tmp_path):
"""Initialized provider pointing at the temp project."""
project_name, project_dir = temp_bm_project
# Pre-write a config file in this test's hermes_home so initialize picks it up
cfg = {
"mode": "local",
"project": project_name,
"project_path": project_dir,
"capture_per_turn": True,
"capture_session_end": True,
"capture_folder": "test-sessions",
}
(tmp_path / "basic-memory.json").write_text(json.dumps(cfg))
p = bm.BasicMemoryProvider()
p.initialize(
session_id=f"integration-{uuid.uuid4().hex[:6]}",
hermes_home=str(tmp_path),
platform="cli",
)
if not p._initialized:
pytest.fail("Provider failed to initialize against the real bm MCP server")
yield p
p.shutdown()
def _parse_tool_result(raw):
try:
d = json.loads(raw)
except Exception:
return None
return d
# ---- Actor smoke ----
def test_actor_starts_and_lists_expected_tools(provider, bm):
tools = {t["name"] for t in provider._actor.list_tools()}
expected = set(bm._HERMES_TO_BM.values())
missing = expected - tools
assert not missing, f"BM MCP server missing tools we depend on: {missing}"
# ---- Tool surface ----
def test_bm_write_returns_full_permalink(provider, bm):
title = f"Integration Write Test {uuid.uuid4().hex[:6]}"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\n\nbody.\n",
"folder": "tests",
"tags": ["integration"],
},
)
permalink = bm._extract_permalink(raw, "")
assert permalink, f"no permalink extracted from: {raw[:300]}"
# BM permalinks include the project prefix
assert permalink.split("/")[0] == provider._project, (
f"permalink should start with project name: {permalink}"
)
def test_bm_read_round_trips_a_written_note(provider, bm):
title = f"Read RT {uuid.uuid4().hex[:6]}"
body = f"# {title}\n\nMARKER-{uuid.uuid4().hex}\n"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": body,
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
raw = provider.handle_tool_call("bm_read", {"identifier": permalink})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert title in text
def test_bm_edit_append_lands_in_note(provider, bm):
title = f"Append Test {uuid.uuid4().hex[:6]}"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\nseed\n",
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
marker = f"APPEND-MARKER-{uuid.uuid4().hex}"
provider.handle_tool_call(
"bm_edit",
{
"identifier": permalink,
"operation": "append",
"content": f"\n{marker}\n",
},
)
raw = provider.handle_tool_call("bm_read", {"identifier": permalink})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert marker in text
def test_bm_edit_replace_section_swaps_content(provider, bm):
title = f"ReplaceSection {uuid.uuid4().hex[:6]}"
body = f"# {title}\n\n## Notes\noriginal-body\n"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": body,
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
new_marker = f"REPLACED-{uuid.uuid4().hex}"
provider.handle_tool_call(
"bm_edit",
{
"identifier": permalink,
"operation": "replace_section",
"section": "## Notes",
"content": new_marker,
},
)
raw = provider.handle_tool_call("bm_read", {"identifier": permalink})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert new_marker in text
assert "original-body" not in text
def test_bm_search_finds_a_freshly_written_note(provider, bm):
unique = f"SEARCH-MARKER-{uuid.uuid4().hex}"
title = f"Search Test {unique}"
provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\nbody.\n",
"folder": "tests",
},
)
raw = provider.handle_tool_call("bm_search", {"query": unique, "limit": 5})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert unique in text or title in text
def test_bm_context_returns_results(provider, bm):
title = f"Context Test {uuid.uuid4().hex[:6]}"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\n",
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
raw = provider.handle_tool_call(
"bm_context",
{
"url": f"memory://{permalink}",
"depth": 1,
},
)
d = _parse_tool_result(raw)
assert d is not None
# build_context returns a JSON dict with `results` (and other fields)
text_blob = json.dumps(d)
assert "results" in text_blob
def test_bm_move_relocates_note(provider, bm):
"""
BM permalinks are stable IDs that don't change on move — only the
file_path moves. So we verify by:
1. The move response itself reports the new destination
2. Reading by the original permalink still succeeds (note wasn't lost)
"""
title = f"Move Test {uuid.uuid4().hex[:6]}"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\n",
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
assert permalink, "expected a permalink from bm_write"
raw = provider.handle_tool_call(
"bm_move",
{
"identifier": permalink,
"new_folder": "tests/archive",
},
)
d = _parse_tool_result(raw)
move_text = (d or {}).get("text") or json.dumps(d or {})
# Move response text reports both the old and new locations
assert "moved successfully" in move_text.lower() or "moved" in move_text.lower(), (
f"move response missing success indicator: {move_text[:200]}"
)
assert "tests/archive" in move_text, f"move response missing new folder: {move_text[:200]}"
# Permalink is stable — reading by it should still work
raw = provider.handle_tool_call("bm_read", {"identifier": permalink})
d = _parse_tool_result(raw)
read_text = (d or {}).get("text") or json.dumps(d or {})
assert title in read_text, "note should still be readable after move"
def test_bm_delete_removes_note(provider, bm):
title = f"Delete Test {uuid.uuid4().hex[:6]}"
raw = provider.handle_tool_call(
"bm_write",
{
"title": title,
"content": f"# {title}\n",
"folder": "tests",
},
)
permalink = bm._extract_permalink(raw, "")
provider.handle_tool_call("bm_delete", {"identifier": permalink})
# Read should now indicate "not found"
raw = provider.handle_tool_call("bm_read", {"identifier": permalink})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert "not found" in text.lower() or "no notes found" in text.lower(), (
f"expected a 'not found' indication, got: {text[:200]}"
)
# ---- Capture pipeline ----
def test_sync_turn_writes_then_appends_to_same_session_note(provider, bm):
# First turn — creates the session note
provider.sync_turn("integration turn-1 user", "integration turn-1 assistant")
if provider._sync_thread:
provider._sync_thread.join(timeout=20.0)
sid_1 = provider._session_note_id
assert sid_1, "first sync_turn should set _session_note_id"
assert sid_1.startswith(provider._project + "/"), (
f"session_note_id should include project prefix, got: {sid_1}"
)
# Second turn — should append to the same note
marker = f"TURN-2-MARKER-{uuid.uuid4().hex}"
provider.sync_turn("integration turn-2 user", marker)
if provider._sync_thread:
provider._sync_thread.join(timeout=20.0)
sid_2 = provider._session_note_id
assert sid_2 == sid_1, "session_note_id should NOT change between turns"
# Verify both turn markers are present in the persisted note
raw = provider.handle_tool_call("bm_read", {"identifier": sid_1})
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert "integration turn-1 user" in text
assert marker in text
def test_on_session_end_writes_summary_with_relations(provider, bm):
# Seed a session note via sync_turn
provider.sync_turn("first message", "first reply")
if provider._sync_thread:
provider._sync_thread.join(timeout=20.0)
sid = provider._session_note_id
assert sid
provider.on_session_end(
[
{"role": "user", "content": "first message"},
{"role": "assistant", "content": "first reply"},
]
)
# Search for the summary
raw = provider.handle_tool_call(
"bm_search",
{
"query": "Hermes Session Summary",
"limit": 5,
},
)
d = _parse_tool_result(raw)
text = (d or {}).get("text") or json.dumps(d or {})
assert "Hermes Session Summary" in text
def test_prefetch_against_real_bm(provider, bm):
# Seed a recognizable note
unique = f"PREFETCH-MARKER-{uuid.uuid4().hex}"
provider.handle_tool_call(
"bm_write",
{
"title": f"Prefetch Test {unique}",
"content": f"# Prefetch Test\n{unique}\n",
"folder": "tests",
},
)
# BM's FTS index is updated synchronously inside the write_note API
# path (knowledge_router.py:272), so this loop is really only smoothing
# over the round-trip cost of a few RPCs on a slow runner. prefetch
# explicitly requests search_type="text" so we don't get pulled onto
# BM's hybrid path, where vector indexing is async and would race the
# search.
budget_secs = 10.0
deadline = time.monotonic() + budget_secs
out = ""
attempts = 0
while time.monotonic() < deadline:
attempts += 1
out = provider.prefetch(unique)
if out:
break
time.sleep(0.25)
assert out, (
f"prefetch returned nothing after {attempts} attempt(s) over "
f"{budget_secs}s; provider._failure_count={provider._failure_count}, "
f"circuit_open={provider._is_circuit_open()}. "
f"Either BM didn't index the note in time or prefetch's actor.call "
f"is timing out internally."
)
assert "Basic Memory Recall" in out
assert unique in out or "Prefetch Test" in out
+296
View File
@@ -0,0 +1,296 @@
"""Tests for prefetch / queue_prefetch / _format_prefetch."""
from __future__ import annotations
import json
import time
from unittest.mock import MagicMock
import pytest
def _initialized_provider(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "test-proj"
p._actor = MagicMock()
return p
# ---- prefetch ----
def test_prefetch_returns_cached_value_drained(bm):
p = _initialized_provider(bm)
p._pending_prefetch = "## cached recall"
out = p.prefetch("any query")
assert out == "## cached recall"
# Cache must be drained so the next prefetch doesn't return stale results
assert p._pending_prefetch == ""
p._actor.call.assert_not_called()
def test_prefetch_calls_search_when_cache_empty(bm):
p = _initialized_provider(bm)
p._actor.call.return_value = json.dumps(
{"results": [{"title": "T", "permalink": "p/t", "content": "c"}]}
)
out = p.prefetch("hello world")
assert "## Basic Memory Recall" in out
assert "**T**" in out
p._actor.call.assert_called_once()
bm_tool, bm_args = p._actor.call.call_args[0][:2]
assert bm_tool == "search_notes"
assert bm_args["query"] == "hello world"
assert bm_args["page_size"] == 5
assert bm_args["output_format"] == "json"
# Pin search_type=text so BM doesn't fall into the hybrid+async-vector
# path on the prefetch hot path. See prefetch() comment for rationale.
assert bm_args["search_type"] == "text"
def test_prefetch_returns_empty_when_uninitialized(bm):
p = bm.BasicMemoryProvider()
assert p.prefetch("x") == ""
def test_prefetch_returns_empty_when_circuit_open(bm):
p = _initialized_provider(bm)
p._failure_pause_until = time.monotonic() + 60.0
assert p.prefetch("x") == ""
p._actor.call.assert_not_called()
def test_prefetch_records_failure_on_actor_error(bm):
p = _initialized_provider(bm)
p._actor.call.side_effect = RuntimeError("boom")
assert p.prefetch("x") == ""
assert p._failure_count == 1
def test_prefetch_returns_empty_for_empty_results(bm):
p = _initialized_provider(bm)
p._actor.call.return_value = json.dumps({"results": []})
assert p.prefetch("x") == ""
# ---- queue_prefetch ----
def test_queue_prefetch_fills_cache_in_background(bm):
p = _initialized_provider(bm)
p._actor.call.return_value = json.dumps(
{"results": [{"title": "Bg", "permalink": "p/bg", "content": "c"}]}
)
p.queue_prefetch("user typed something")
# Wait for the daemon thread to finish
if p._prefetch_thread:
p._prefetch_thread.join(timeout=5.0)
# Cache should now have the formatted result
assert "**Bg**" in p._pending_prefetch
# And subsequent prefetch returns it without making another call
p._actor.call.reset_mock()
out = p.prefetch("anything")
assert "**Bg**" in out
p._actor.call.assert_not_called()
def test_queue_prefetch_uses_longer_timeout_than_sync_prefetch(bm):
"""queue_prefetch runs in background, so it can afford a longer timeout."""
p = _initialized_provider(bm)
p._actor.call.return_value = json.dumps({"results": []})
p.queue_prefetch("q")
if p._prefetch_thread:
p._prefetch_thread.join(timeout=5.0)
timeout = p._actor.call.call_args.kwargs.get("timeout") or p._actor.call.call_args[1].get(
"timeout"
)
# Background prefetch is more patient than the foreground 3.0s
assert timeout is not None and timeout > 3.0
def test_queue_prefetch_skipped_when_thread_in_flight(bm):
p = _initialized_provider(bm)
# Simulate an already-running prefetch thread
class _StillAlive:
def is_alive(self):
return True
p._prefetch_thread = _StillAlive() # type: ignore[assignment]
p.queue_prefetch("q")
p._actor.call.assert_not_called()
def test_queue_prefetch_skipped_when_circuit_open(bm):
p = _initialized_provider(bm)
p._failure_pause_until = time.monotonic() + 60.0
p.queue_prefetch("q")
p._actor.call.assert_not_called()
def test_queue_prefetch_skipped_when_uninitialized(bm):
p = bm.BasicMemoryProvider()
p._actor = MagicMock()
p.queue_prefetch("q")
p._actor.call.assert_not_called()
def test_queue_prefetch_records_failure_on_bg_error(bm):
p = _initialized_provider(bm)
p._actor.call.side_effect = RuntimeError("backend down")
p.queue_prefetch("q")
if p._prefetch_thread:
p._prefetch_thread.join(timeout=5.0)
assert p._failure_count >= 1
# ---- _format_prefetch ----
def _format(bm, payload):
"""Helper: call _format_prefetch on a fresh provider."""
return bm.BasicMemoryProvider()._format_prefetch(payload)
def test_format_prefetch_with_results(bm):
payload = json.dumps(
{
"results": [
{"title": "A", "permalink": "p/a", "content": "first line"},
{"title": "B", "permalink": "p/b", "content": "second line"},
]
}
)
out = _format(bm, payload)
assert "## Basic Memory Recall" in out
assert "**A**" in out and "**B**" in out
assert "p/a" in out and "p/b" in out
def test_format_prefetch_caps_at_5_entries(bm):
results = [{"title": f"T{i}", "permalink": f"p/{i}", "content": "x"} for i in range(20)]
payload = json.dumps({"results": results})
out = _format(bm, payload)
# Five lines + one heading = 6 lines max
assert out.count("\n- **") == 5
def test_format_prefetch_caps_preview_length(bm):
payload = json.dumps(
{
"results": [
{"title": "T", "permalink": "p/t", "content": "x" * 5000},
]
}
)
out = _format(bm, payload)
# Each result line includes the preview, capped to 200 chars
line = [l for l in out.split("\n") if l.startswith("- ")][0]
# Some boilerplate around the preview, but the long preview is capped
assert len(line) < 400
def test_format_prefetch_collapses_whitespace(bm):
payload = json.dumps(
{
"results": [
{"title": "T", "permalink": "p/t", "content": "first\n\n second\tthird"},
]
}
)
out = _format(bm, payload)
assert "first second third" in out
def test_format_prefetch_falls_back_to_preview_field(bm):
"""BM may use 'preview' instead of 'content' in some response shapes."""
payload = json.dumps(
{
"results": [
{"title": "T", "permalink": "p/t", "preview": "preview-only"},
]
}
)
out = _format(bm, payload)
assert "preview-only" in out
def test_format_prefetch_handles_non_string_content(bm):
"""Defensive: BM could conceivably return non-string content fields."""
payload = json.dumps(
{
"results": [
{"title": "T", "permalink": "p/t", "content": 12345},
]
}
)
# Must not raise
out = _format(bm, payload)
assert "12345" in out
def test_format_prefetch_handles_missing_title_permalink(bm):
payload = json.dumps(
{
"results": [
{"content": "orphan note"},
]
}
)
out = _format(bm, payload)
assert "(untitled)" in out
assert "orphan note" in out
def test_format_prefetch_skips_non_dict_entries(bm):
payload = json.dumps(
{
"results": [
"not-a-dict",
{"title": "Real", "permalink": "p/r", "content": "x"},
]
}
)
out = _format(bm, payload)
assert "Real" in out
assert "not-a-dict" not in out
def test_format_prefetch_with_text_wrapped_results(bm):
"""BM text-format responses arrive wrapped as {"text": "..."} by _extract_mcp_text."""
inner = json.dumps({"results": [{"title": "X", "permalink": "p/x", "content": "c"}]})
payload = json.dumps({"text": inner})
out = _format(bm, payload)
assert "**X**" in out
def test_format_prefetch_empty(bm):
assert _format(bm, json.dumps({"results": []})) == ""
def test_format_prefetch_no_results_key(bm):
assert _format(bm, json.dumps({"foo": "bar"})) == ""
def test_format_prefetch_malformed(bm):
assert _format(bm, "not-json") == ""
def test_format_prefetch_handles_extra_unknown_fields(bm):
"""Forward-compatibility: unknown fields shouldn't break formatting."""
payload = json.dumps(
{
"results": [
{
"title": "T",
"permalink": "p/t",
"content": "c",
"future_field_42": {"nested": "value"},
"score": 0.9,
},
]
}
)
out = _format(bm, payload)
assert "**T**" in out
+637
View File
@@ -0,0 +1,637 @@
"""Tests for BasicMemoryProvider with the MCP actor mocked out."""
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import MagicMock
def test_is_available_no_mcp(bm, monkeypatch):
monkeypatch.setattr(bm, "_MCP_AVAILABLE", False)
p = bm.BasicMemoryProvider()
assert p.is_available() is False
def test_is_available_no_bm_no_uv(bm, monkeypatch):
"""No bm AND no uv → can't install, can't operate → unavailable."""
monkeypatch.setattr(bm, "_MCP_AVAILABLE", True)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: None)
monkeypatch.setattr(bm, "_uv_binary_path", lambda: None)
p = bm.BasicMemoryProvider()
assert p.is_available() is False
def test_is_available_bm_present(bm, monkeypatch):
"""bm already installed → available regardless of uv."""
monkeypatch.setattr(bm, "_MCP_AVAILABLE", True)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: "/fake/bm")
monkeypatch.setattr(bm, "_uv_binary_path", lambda: None)
p = bm.BasicMemoryProvider()
assert p.is_available() is True
def test_is_available_bm_missing_but_uv_present(bm, monkeypatch):
"""bm missing but uv available → we can install bm at init time → available."""
monkeypatch.setattr(bm, "_MCP_AVAILABLE", True)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: None)
monkeypatch.setattr(bm, "_uv_binary_path", lambda: "/fake/uv")
p = bm.BasicMemoryProvider()
assert p.is_available() is True
def test_name(bm):
assert bm.BasicMemoryProvider().name == "basic-memory"
assert bm.BasicMemoryProvider().name == bm.PROVIDER_NAME
def test_get_tool_schemas_unconditional(bm):
"""
Regression: Hermes captures the schema list at *register* time, before
`initialize()` runs. If get_tool_schemas() returns [] when uninitialized,
Hermes builds _tool_to_provider with no entries for us and every
subsequent bm_* invocation returns "Unknown tool: bm_*" forever.
Schemas are static return them unconditionally.
"""
# Fresh provider, never initialized, should still expose all 10 schemas
p = bm.BasicMemoryProvider()
assert p._initialized is False
schemas = p.get_tool_schemas()
assert len(schemas) == 10
names = {s["name"] for s in schemas}
assert names == {
"bm_search",
"bm_read",
"bm_write",
"bm_edit",
"bm_context",
"bm_delete",
"bm_move",
"bm_recent",
"bm_projects",
"bm_workspaces",
}
# Initialized provider also returns 10 (idempotent)
p._initialized = True
assert len(p.get_tool_schemas()) == 10
def test_get_tool_schemas_returns_independent_copies(bm):
"""Mutating the returned list shouldn't affect the next call."""
p = bm.BasicMemoryProvider()
schemas = p.get_tool_schemas()
schemas.clear()
assert len(p.get_tool_schemas()) == 10
def test_handle_tool_call_uninitialized(bm):
p = bm.BasicMemoryProvider()
out = json.loads(p.handle_tool_call("bm_search", {"query": "x"}))
assert "error" in out
def test_handle_tool_call_unknown_tool(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._actor = MagicMock()
out = json.loads(p.handle_tool_call("bm_bogus", {}))
assert "error" in out
def test_handle_tool_call_dispatches(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "proj"
actor = MagicMock()
actor.call.return_value = json.dumps({"results": []})
p._actor = actor
out = p.handle_tool_call("bm_search", {"query": "hi", "limit": 3})
actor.call.assert_called_once()
bm_tool, bm_args = actor.call.call_args[0][:2]
assert bm_tool == "search_notes"
assert bm_args == {"project": "proj", "query": "hi", "page_size": 3}
def test_handle_tool_call_missing_arg(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._actor = MagicMock()
out = json.loads(p.handle_tool_call("bm_write", {"title": "x"}))
assert "error" in out
def test_handle_tool_call_actor_failure(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "proj"
actor = MagicMock()
actor.call.side_effect = RuntimeError("boom")
p._actor = actor
out = json.loads(p.handle_tool_call("bm_search", {"query": "x"}))
assert "error" in out
assert p._failure_count == 1
def test_circuit_breaker_opens_after_5_failures(bm, monkeypatch):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "proj"
actor = MagicMock()
actor.call.side_effect = RuntimeError("boom")
p._actor = actor
for _ in range(5):
p.handle_tool_call("bm_search", {"query": "x"})
assert p._failure_pause_until > 0
assert p._is_circuit_open() is True
def test_circuit_breaker_resets_after_pause(bm, monkeypatch):
p = bm.BasicMemoryProvider()
p._failure_count = 5
p._failure_pause_until = 1.0 # already in the past
monkeypatch.setattr(bm.time, "monotonic", lambda: 9999.0)
assert p._is_circuit_open() is False
assert p._failure_count == 0
def test_session_note_title_with_session_id(bm):
from datetime import datetime, timezone
p = bm.BasicMemoryProvider()
p._session_started_at = datetime(2026, 5, 10, 13, 5, tzinfo=timezone.utc)
p._session_id = "20260510_080249_571920"
title = p._session_note_title()
# Date appears once; trailing random component is the disambiguator
assert title == "Hermes Session 2026-05-10 1305 571920"
def test_session_note_title_no_session_id(bm):
from datetime import datetime, timezone
p = bm.BasicMemoryProvider()
p._session_started_at = datetime(2026, 5, 10, 13, 5, 42, tzinfo=timezone.utc)
p._session_id = ""
title = p._session_note_title()
# Falls back to seconds for disambiguation
assert title == "Hermes Session 2026-05-10 1305 42"
def test_session_note_title_short_session_id(bm):
from datetime import datetime, timezone
p = bm.BasicMemoryProvider()
p._session_started_at = datetime(2026, 5, 10, 13, 5, tzinfo=timezone.utc)
p._session_id = "abcdef"
title = p._session_note_title()
# No `_` in id → use last 6 chars
assert title == "Hermes Session 2026-05-10 1305 abcdef"
def test_system_prompt_block_uninitialized(bm):
p = bm.BasicMemoryProvider()
assert p.system_prompt_block() == ""
def test_system_prompt_block_mentions_tools(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "test-proj"
p._mode = "local"
out = p.system_prompt_block()
assert "bm_search" in out
assert "test-proj" in out
assert "local" in out
def test_system_prompt_block_steers_away_from_cli(bm):
"""
Felix's training data is biased toward `bm tool ...` CLI patterns. The
system_prompt_block must explicitly direct the model to the bm_* tools
AND give a reason (latency) so it has a justification for following the
directive. Without this nudge, agents reach for bash/terminal tools and
pay 1-2s per call instead of ~0.1s.
"""
p = bm.BasicMemoryProvider()
p._initialized = True
p._project = "test-proj"
p._mode = "local"
out = p.system_prompt_block().lower()
# Directive: don't use the bm CLI
assert "do not shell out" in out or "do not use the" in out or "do not run" in out
assert "bm" in out and "cli" in out
# Reason given (latency / capture bypass)
assert "mcp" in out # the persistent connection is the mechanism we cite
assert "spawn" in out or "process" in out # cold-start cost is mentioned
def test_save_config_writes_json(bm, tmp_path):
p = bm.BasicMemoryProvider()
p.save_config({"mode": "local", "project": "x", "capture_per_turn": "true"}, str(tmp_path))
written = json.loads((tmp_path / "basic-memory.json").read_text())
assert written["mode"] == "local"
assert written["project"] == "x"
assert written["capture_per_turn"] is True # coerced from "true"
def test_save_config_merges_existing(bm, tmp_path):
cfg = tmp_path / "basic-memory.json"
cfg.write_text(json.dumps({"mode": "cloud", "project": "old"}))
p = bm.BasicMemoryProvider()
p.save_config({"project": "new"}, str(tmp_path))
after = json.loads(cfg.read_text())
assert after["mode"] == "cloud" # preserved
assert after["project"] == "new" # updated
def test_load_config_missing_returns_empty(bm, tmp_path):
assert bm._load_config(str(tmp_path)) == {}
def test_load_config_corrupt_returns_empty(bm, tmp_path):
(tmp_path / "basic-memory.json").write_text("{not json")
assert bm._load_config(str(tmp_path)) == {}
def test_get_config_schema_shape(bm):
schema = bm.BasicMemoryProvider().get_config_schema()
keys = {entry["key"] for entry in schema}
assert {
"mode",
"project",
"project_path",
"capture_per_turn",
"capture_session_end",
"capture_folder",
}.issubset(keys)
def test_register_appends_to_active_providers(bm):
fake_ctx = MagicMock()
bm._active_providers.clear()
bm.register(fake_ctx)
fake_ctx.register_memory_provider.assert_called_once()
assert len(bm._active_providers) == 1
bm._active_providers.clear()
def test_register_also_registers_bundled_skill(bm):
"""Plugin's bundled SKILL.md should auto-register when `hermes plugins install`
drops the repo into ~/.hermes/plugins/. Avoids the manual symlink step."""
fake_ctx = MagicMock()
bm._active_providers.clear()
bm.register(fake_ctx)
fake_ctx.register_skill.assert_called_once()
args, kwargs = fake_ctx.register_skill.call_args
# First positional arg is the bare skill name
assert args[0] == "basic-memory"
# Second positional arg is the SKILL.md path; it should resolve to a real file
assert args[1].name == "SKILL.md"
assert args[1].is_file()
bm._active_providers.clear()
def test_register_tolerates_old_hermes_without_register_skill(bm):
"""Older Hermes versions don't have ctx.register_skill; we shouldn't crash."""
class _OldCtx:
def __init__(self):
self.calls = []
def register_memory_provider(self, provider):
self.calls.append(("memory", provider))
ctx = _OldCtx()
bm._active_providers.clear()
bm.register(ctx) # must not raise
assert any(c[0] == "memory" for c in ctx.calls)
bm._active_providers.clear()
def test_register_swallows_register_skill_errors(bm, caplog):
"""If register_skill raises (path validation, ABC mismatch, etc.) we log
and continue don't break the memory-provider registration."""
fake_ctx = MagicMock()
fake_ctx.register_skill.side_effect = ValueError("invalid skill name")
bm._active_providers.clear()
with caplog.at_level("WARNING"):
bm.register(fake_ctx)
fake_ctx.register_memory_provider.assert_called_once()
assert "register_skill failed" in caplog.text
bm._active_providers.clear()
# ---- Edge cases ----
def test_handle_tool_call_with_none_args(bm):
"""args=None must not crash; should be coerced to {} and surface a missing-arg error."""
p = bm.BasicMemoryProvider()
p._initialized = True
p._actor = MagicMock()
out = json.loads(p.handle_tool_call("bm_search", None)) # type: ignore[arg-type]
assert "error" in out
def test_handle_tool_call_unknown_does_not_invoke_actor(bm):
p = bm.BasicMemoryProvider()
p._initialized = True
p._actor = MagicMock()
p.handle_tool_call("bm_does_not_exist", {"x": 1})
p._actor.call.assert_not_called()
def test_translate_args_unknown_tool_raises(bm):
"""Unknown tool names should raise KeyError so callers handle it explicitly."""
import pytest
with pytest.raises(KeyError):
bm._translate_args("not_a_tool", {}, "proj")
# ---- Version metadata ----
def test_module_version_present(bm):
import re
assert hasattr(bm, "__version__")
assert isinstance(bm.__version__, str)
# Matches the Python release versions written by scripts/update_versions.py.
assert re.fullmatch(r"\d+\.\d+\.\d+(?:(?:b|rc)\d+)?", bm.__version__), bm.__version__
def test_module_version_matches_plugin_yaml(bm):
"""plugin.yaml ships to Hermes; __version__ is what tooling reads. Keep them in sync."""
import os
import re
plugin_yaml = os.path.join(
os.path.dirname(os.path.abspath(bm.__file__)),
"plugin.yaml",
)
text = open(plugin_yaml).read()
m = re.search(r"^\s*version\s*:\s*(\S+)\s*$", text, re.MULTILINE)
assert m is not None, "plugin.yaml is missing a version field"
assert m.group(1) == bm.__version__, (
f"plugin.yaml version ({m.group(1)}) doesn't match __version__ ({bm.__version__})"
)
# ---- uv bootstrap ----
def test_install_bm_via_uv_no_uv(bm, monkeypatch):
"""If uv isn't available, install returns None without trying to spawn."""
monkeypatch.setattr(bm, "_uv_binary_path", lambda: None)
assert bm._install_bm_via_uv() is None
def test_install_bm_via_uv_runs_uv_tool_install(bm, monkeypatch):
"""Install shells out to `uv tool install basic-memory`."""
calls: list = []
class _Result:
returncode = 0
stdout = b""
stderr = b""
def _fake_run(argv, **kwargs):
calls.append((argv, kwargs))
return _Result()
monkeypatch.setattr(bm, "_uv_binary_path", lambda: "/fake/uv")
monkeypatch.setattr(bm.subprocess, "run", _fake_run)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: "/fake/bm-after-install")
result = bm._install_bm_via_uv()
assert result == "/fake/bm-after-install"
assert len(calls) == 1
argv = calls[0][0]
assert argv[0] == "/fake/uv"
assert argv[1:] == ["tool", "install", "basic-memory", "--quiet"]
def test_install_bm_via_uv_failed_returncode(bm, monkeypatch):
"""Non-zero exit logs and returns None — doesn't pretend success."""
class _Result:
returncode = 2
stdout = b""
stderr = b"network unreachable"
monkeypatch.setattr(bm, "_uv_binary_path", lambda: "/fake/uv")
monkeypatch.setattr(bm.subprocess, "run", lambda *a, **kw: _Result())
assert bm._install_bm_via_uv() is None
def test_install_bm_via_uv_subprocess_exception(bm, monkeypatch):
"""If subprocess raises (timeout, OSError, etc.) we degrade to None, not crash."""
monkeypatch.setattr(bm, "_uv_binary_path", lambda: "/fake/uv")
def _raise(*a, **kw):
raise OSError("boom")
monkeypatch.setattr(bm.subprocess, "run", _raise)
assert bm._install_bm_via_uv() is None
def test_initialize_invokes_uv_install_when_bm_missing(bm, monkeypatch, tmp_path):
"""Cold-start path: bm absent, uv present → initialize triggers install."""
install_calls: list = []
def _fake_install():
install_calls.append(True)
return None # install reports failure; initialize logs and returns
monkeypatch.setattr(bm, "_MCP_AVAILABLE", True)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: None)
monkeypatch.setattr(bm, "_uv_binary_path", lambda: "/fake/uv")
monkeypatch.setattr(bm, "_install_bm_via_uv", _fake_install)
p = bm.BasicMemoryProvider()
p.initialize(session_id="test", hermes_home=str(tmp_path))
assert install_calls == [True], "expected initialize() to attempt the install"
assert p._initialized is False # install reported failure → don't start actor
def test_initialize_skips_uv_install_when_bm_present(bm, monkeypatch, tmp_path):
"""Steady-state path: bm already installed → no install attempt."""
install_calls: list = []
monkeypatch.setattr(bm, "_bm_binary_path", lambda: "/fake/bm")
def _fake_install():
install_calls.append(True)
return "/should-not-be-called"
monkeypatch.setattr(bm, "_install_bm_via_uv", _fake_install)
monkeypatch.setattr(bm, "_MCP_AVAILABLE", False) # short-circuit actor start
p = bm.BasicMemoryProvider()
p.initialize(session_id="test", hermes_home=str(tmp_path))
assert install_calls == [], "should not invoke install when bm is already present"
def test_initialize_bails_when_no_bm_no_uv(bm, monkeypatch, tmp_path, caplog):
"""No bm, no uv → log clear error, don't try to install, don't initialize."""
monkeypatch.setattr(bm, "_MCP_AVAILABLE", True)
monkeypatch.setattr(bm, "_bm_binary_path", lambda: None)
monkeypatch.setattr(bm, "_uv_binary_path", lambda: None)
install_calls: list = []
monkeypatch.setattr(bm, "_install_bm_via_uv", lambda: install_calls.append(True) or None)
p = bm.BasicMemoryProvider()
with caplog.at_level("ERROR"):
p.initialize(session_id="test", hermes_home=str(tmp_path))
assert install_calls == [] # never attempted — no uv to call
assert p._initialized is False
assert "uv is not installed" in caplog.text or "uv" in caplog.text.lower()
# ---- Defaults: stay out of bm's app dir ----
def test_default_project_name_is_hermes_memory(bm):
"""The default project name no longer carries a hostname suffix.
Each machine has its own isolated local store with this same name."""
assert bm._default_project() == "hermes-memory"
def test_default_project_path_is_in_user_space(bm):
"""~/.basic-memory/ is reserved for bm's app state. Projects live in user space."""
p = bm._default_project_path()
assert ".basic-memory" not in p, (
f"default project path must not live inside ~/.basic-memory/, got: {p}"
)
assert p.rstrip("/").endswith("hermes-memory")
# ---- bm config introspection ----
def test_bm_known_projects_missing_file(bm, monkeypatch, tmp_path):
"""When bm has never been run, return None (callers should treat as 'unknown')."""
monkeypatch.setattr(bm, "_bm_config_path", lambda: tmp_path / "config.json")
assert bm._bm_known_projects() is None
def test_bm_known_projects_corrupt_file(bm, monkeypatch, tmp_path):
cfg = tmp_path / "config.json"
cfg.write_text("{not json")
monkeypatch.setattr(bm, "_bm_config_path", lambda: cfg)
assert bm._bm_known_projects() is None
def test_bm_known_projects_returns_dict(bm, monkeypatch, tmp_path):
cfg = tmp_path / "config.json"
cfg.write_text(json.dumps({"projects": {"main": {}, "hermes-memory": {}}}))
monkeypatch.setattr(bm, "_bm_config_path", lambda: cfg)
result = bm._bm_known_projects()
assert isinstance(result, dict)
assert set(result.keys()) == {"main", "hermes-memory"}
def test_bm_known_projects_handles_non_dict_root(bm, monkeypatch, tmp_path):
cfg = tmp_path / "config.json"
cfg.write_text(json.dumps(["not a dict"]))
monkeypatch.setattr(bm, "_bm_config_path", lambda: cfg)
assert bm._bm_known_projects() is None
# ---- Project verification ----
def test_verify_project_registered_no_bm_config(bm, monkeypatch):
"""No bm config yet → assume registration is fine; let downstream surface real failures."""
monkeypatch.setattr(bm, "_bm_known_projects", lambda: None)
p = bm.BasicMemoryProvider()
p._project = "anything-goes"
assert p._verify_project_registered() is True
def test_verify_project_registered_present(bm, monkeypatch):
monkeypatch.setattr(bm, "_bm_known_projects", lambda: {"hermes-memory": {}, "main": {}})
p = bm.BasicMemoryProvider()
p._project = "hermes-memory"
assert p._verify_project_registered() is True
def test_verify_project_registered_missing(bm, monkeypatch):
monkeypatch.setattr(bm, "_bm_known_projects", lambda: {"main": {}, "other": {}})
p = bm.BasicMemoryProvider()
p._project = "hermes-memory-cloud"
assert p._verify_project_registered() is False
def test_log_missing_project_local_hint_includes_path(bm, caplog):
p = bm.BasicMemoryProvider()
p._mode = "local"
p._project = "hermes-memory"
p._project_path = "/tmp/somewhere"
with caplog.at_level("ERROR"):
p._log_missing_project()
msg = caplog.text
assert "hermes-memory" in msg
assert "/tmp/somewhere" in msg
assert "--cloud" not in msg
def test_log_missing_project_cloud_hint_uses_cloud_flag(bm, caplog):
p = bm.BasicMemoryProvider()
p._mode = "cloud"
p._project = "hermes-memory-cloud"
with caplog.at_level("ERROR"):
p._log_missing_project()
msg = caplog.text
assert "hermes-memory-cloud" in msg
assert "--cloud" in msg
# ---- initialize() bail-out on missing project ----
def test_initialize_bails_when_project_missing(bm, monkeypatch, tmp_path):
"""If bm config says the project doesn't exist, refuse to initialize."""
# bm config exists, but our project isn't in it
bm_cfg = tmp_path / ".basic-memory" / "config.json"
bm_cfg.parent.mkdir(parents=True)
bm_cfg.write_text(json.dumps({"projects": {"main": {}}}))
monkeypatch.setattr(bm, "_bm_config_path", lambda: bm_cfg)
# Cloud mode so _ensure_local_project doesn't auto-create
plugin_cfg = tmp_path / "basic-memory.json"
plugin_cfg.write_text(json.dumps({"mode": "cloud", "project": "not-registered"}))
p = bm.BasicMemoryProvider()
p.initialize(session_id="test", hermes_home=str(tmp_path))
assert p._initialized is False
assert p._actor is None
def test_initialize_proceeds_when_bm_config_absent(bm, monkeypatch, tmp_path):
"""If bm config doesn't exist (fresh install), don't false-reject — let actor try."""
monkeypatch.setattr(bm, "_bm_config_path", lambda: tmp_path / "no-such" / "config.json")
monkeypatch.setattr(bm, "_MCP_AVAILABLE", False) # shortcut: actor won't actually start
plugin_cfg = tmp_path / "basic-memory.json"
plugin_cfg.write_text(json.dumps({"mode": "cloud", "project": "anything"}))
p = bm.BasicMemoryProvider()
# We don't fully assert _initialized here because the actor won't start without
# MCP — but we DO assert _verify_project_registered didn't gate us out before
# actor-start was attempted.
p.initialize(session_id="test", hermes_home=str(tmp_path))
# Initialization fails at actor-start (MCP unavailable), not at verify.
assert p._initialized is False # expected — actor couldn't start
# _project should have been set despite the failure (proves we got past verify)
assert p._project == "anything"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v bun >/dev/null 2>&1; then
echo "bun is required to run pre-commit checks." >&2
exit 1
fi
echo "Running pre-commit checks..."
bun run lint
bun run check-types
+37
View File
@@ -0,0 +1,37 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.8"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Fetch skills
run: bun scripts/fetch-skills.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Lint
run: bun run lint
- name: Typecheck
run: bun run check-types
- name: Unit tests
run: bun test
+99
View File
@@ -0,0 +1,99 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Version bump (`patch`, `minor`, `major`) or explicit semver (`0.2.0`)"
required: true
default: "patch"
permissions:
contents: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.8"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Fetch skills
run: bun scripts/fetch-skills.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release checks
run: bun run release:check
- name: Configure Git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump version and create tag
id: bump
run: |
set -euo pipefail
VERSION_INPUT="${{ github.event.inputs.version }}"
if [[ "$VERSION_INPUT" =~ ^(patch|minor|major)$ ]]; then
npm version "$VERSION_INPUT" -m "chore(release): %s"
elif [[ "$VERSION_INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
npm version "$VERSION_INPUT" -m "chore(release): %s"
else
echo "Unsupported version input: $VERSION_INPUT" >&2
echo "Use patch|minor|major or explicit semver like 0.2.0 or 0.2.0-alpha.1" >&2
exit 1
fi
TAG="$(git describe --tags --abbrev=0)"
VERSION="$(node -p "require('./package.json').version")"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Push commit and tag
run: |
set -euo pipefail
git push origin HEAD:${GITHUB_REF_NAME}
git push origin "${{ steps.bump.outputs.tag }}"
- name: Publish to npm
run: |
VERSION="${{ steps.bump.outputs.version }}"
if [[ "$VERSION" == *-* ]]; then
DIST_TAG="${VERSION##*-}" # e.g. alpha.6 -> alpha
DIST_TAG="${DIST_TAG%%.*}" # strip .N suffix
npm publish --provenance --access public --tag "$DIST_TAG"
else
npm publish --provenance --access public
fi
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.bump.outputs.tag }}
name: ${{ steps.bump.outputs.tag }}
generate_release_notes: true
+146
View File
@@ -0,0 +1,146 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.idea/
benchmark/datasets/
benchmark/corpus-locomo/
# Generated from ../../../skills by scripts/fetch-skills.ts
skills/
+1
View File
@@ -0,0 +1 @@
CLAUDE.md
+208
View File
@@ -0,0 +1,208 @@
# Basic Memory CLI & Cloud Setup
## How the Plugin Connects
Basic Memory is **local-first** — everything works out of the box with no cloud account, no internet connection, and no external services. Your notes live as markdown files on disk, indexed locally with SQLite.
The plugin spawns a Basic Memory MCP session via stdio:
```
bm mcp --transport stdio --project <name>
```
All tool calls route through this MCP session. No cloud configuration is required for normal use.
## Cloud Configuration (Optional)
Cloud sync is entirely optional. If you want to sync your local knowledge base to Basic Memory Cloud for backup or cross-device access, you can configure per-project cloud routing.
### Why Cloud?
- **Your agent's memory travels with you** — laptop, desktop, hosted environment. Same knowledge graph everywhere, synced bidirectionally.
- **Team knowledge sharing** — org workspaces let multiple agents and team members build on a shared knowledge base.
- **Durable memory for production agents** — CI runners, containers, and hosted environments are ephemeral. Cloud gives agents persistent memory that survives teardowns.
- **Multi-agent coordination** — multiple agents (or the same agent across services) can read and write to a shared graph.
Cloud extends local-first — it doesn't replace it. Your notes are still plain markdown, still editable locally, still yours. Start with a [7-day free trial](https://basicmemory.com) — no credit card required. Use code `BMCLAW` for 20% off for 3 months.
### Setup
```bash
# Authenticate with Basic Memory Cloud
bm cloud login
# Save API key for per-project cloud routing
bm cloud set-key bmc_...
# Route a project through the cloud
bm project set-cloud <name>
# Revert a project to local routing
bm project set-local <name>
# Check cloud connection state
bm cloud status
```
When a project is set to cloud mode, the MCP server routes tool calls for that project through the cloud API using the saved API key as a Bearer token. Local projects (the default) continue to use the local SQLite index. You can mix local and cloud projects freely.
## Project Management
```bash
# List all projects
bm project list
# Add a new project
bm project add "name" ~/path
# Show current project details
bm project info
# Set the default project
bm project default "name"
# One-way sync (local -> cloud)
bm project sync
# Bidirectional sync
bm project bisync
```
## Cross-Project Operations
All plugin tools accept an optional `project` parameter to operate on a different project:
```
search_notes(query="authentication", project="other-project")
read_note(identifier="notes/api-design", project="docs")
write_note(title="New Note", content="...", folder="notes", project="research")
```
## Workspace Support
Workspaces group projects by owner (personal or organization):
```
list_workspaces()
list_memory_projects(workspace="my-org")
```
## Auto-Recall
When `autoRecall` is enabled (the default), the plugin injects relevant context at the start of each agent session by listening for the `agent_start` event. On each trigger it:
1. **Queries active tasks** — searches the knowledge graph for notes with `type: Task` and `status: active` (up to 5 results)
2. **Fetches recent activity** — gets notes modified in the last 24 hours
3. **Formats and injects context** — returns the results as structured context for the agent
The injected context looks like:
```
## Active Tasks
- **Fix login bug** — Description of Fix login bug
- **Update API docs** — Description of Update API docs
## Recent Activity
- Daily standup notes (memory/daily-standup-notes.md)
- API design decisions (memory/api-design-decisions.md)
---
Check for active tasks and recent activity. Summarize anything relevant to the current session.
```
The trailing instruction (after `---`) is the `recallPrompt`, which you can customize to change what the agent focuses on. For example:
```json
{
"recallPrompt": "Focus on blocked tasks and any decisions made in the last 24 hours."
}
```
To disable auto-recall entirely:
```json
{
"autoRecall": false
}
```
## Auto-Capture
When `autoCapture` is enabled (the default), the plugin automatically records agent conversations after each turn:
1. Extracts the last user + assistant messages
2. Appends them as timestamped entries to a daily conversation note (`conversations-YYYY-MM-DD`)
3. Skips very short exchanges (< `captureMinChars` chars each, default 10)
This builds a searchable history of agent interactions in the knowledge graph without any manual effort.
## Slash Commands
### Memory commands
- **`/remember <text>`** — Save a quick note to the knowledge graph
- **`/recall <query>`** — Search the knowledge graph (top 5 results)
### Skill workflows
These commands inject step-by-step workflow instructions from the bundled skill files:
| Command | What it does |
|---------|-------------|
| `/tasks` | Task management — create, track, resume structured tasks that survive context compaction |
| `/reflect` | Memory reflection — review recent activity and consolidate insights into long-term memory |
| `/defrag` | Memory defrag — reorganize, split, prune, and clean up memory files |
| `/schema` | Schema management — infer, create, validate, and evolve Picoschema definitions |
Each command accepts optional arguments for context:
```
/tasks create a task for the API migration
/reflect focus on decisions from this week
/defrag clean up completed tasks older than 2 weeks
/schema infer a schema for Meeting notes
```
When invoked without arguments, the agent receives the full workflow instructions and follows them interactively.
## Plugin Configuration
The plugin accepts these config fields in `openclaw.config.json`:
```json
{
"plugins": {
"entries": {
"openclaw-basic-memory": {
"enabled": true,
"config": {
"project": "my-project",
"memoryDir": "memory/",
"memoryFile": "MEMORY.md",
"autoCapture": true,
"autoRecall": true,
"recallPrompt": "Check for active tasks and recent activity. Summarize anything relevant.",
"debug": false
}
}
},
"slots": {
"memory": "openclaw-basic-memory"
}
}
}
```
| Field | Default | Description |
|-------|---------|-------------|
| `project` | `openclaw-<hostname>` | BM project name |
| `bmPath` | `bm` | Path to BM CLI binary |
| `memoryDir` | `memory/` | Relative path for memory files |
| `memoryFile` | `MEMORY.md` | Working memory file name |
| `projectPath` | same as `memoryDir` | Absolute path to project root |
| `autoCapture` | `true` | Auto-index conversations after each turn |
| `captureMinChars` | `10` | Min chars to trigger capture |
| `autoRecall` | `true` | Inject context (active tasks, recent activity) at session start |
| `recallPrompt` | *(see above)* | Instruction appended to recalled context |
| `debug` | `false` | Verbose logging |
+632
View File
@@ -0,0 +1,632 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with the OpenClaw package inside the Basic Memory monorepo.
## Project Overview
`@basicmemory/openclaw-basic-memory` is a TypeScript OpenClaw plugin that integrates [Basic Memory](https://github.com/basicmachines-co/basic-memory) with the OpenClaw agent framework. It lives under `integrations/openclaw/` in the monorepo, manages a persistent MCP stdio session to a `bm mcp` process, exposes 14 agent tools (including workspace/project management and cross-project operations), composited memory search/get providers, slash commands, CLI commands, and optional auto-capture of conversations.
## Development Commands
```bash
# Install dependencies (uses Bun)
bun install
# Run all unit tests (Bun native test runner)
bun test
# Run a single test file
bun test tools/search-notes.test.ts
# Integration tests (requires basic-memory CLI installed)
bun run test:int
# Type checking (no emit)
bun run check-types
# Lint (Biome)
bun run lint
# Lint + auto-fix
bun run lint:fix
# All quality checks (fetch skills + type-check + lint + build + tests)
just check
# Release readiness (check + npm pack dry-run)
just release-check
```
## Architecture
### Plugin Lifecycle (`index.ts`)
The default export is an OpenClaw plugin object (`id: "openclaw-basic-memory"`, `kind: "memory"`). The `register(api)` function:
1. Parses config via `parseConfig()` from `config.ts`
2. Creates a `BmClient` instance (the MCP stdio client)
3. Registers all tools, providers, hooks, commands, and the service lifecycle
4. The service `start()` launches the MCP process (`bm mcp --transport stdio`), ensures the project exists, and sets the workspace directory
5. The service `stop()` tears down the MCP connection
### MCP Client (`bm-client.ts` — largest file, ~675 lines)
Central orchestration layer that:
- Spawns and manages a **persistent** `bm mcp --transport stdio` child process via `@modelcontextprotocol/sdk`
- Validates 15 required MCP tools at connect time
- Implements reconnection with bounded retries (500ms, 1s, 2s exponential backoff)
- Distinguishes recoverable errors (broken pipe, transport closed) from fatal errors
- All tool calls require `output_format: "json"` and extract `structuredContent.result`
- Public methods: `search`, `readNote`, `writeNote`, `editNote`, `deleteNote`, `moveNote`, `buildContext`, `recentActivity`, `indexConversation`, `ensureProject`, `listProjects`, `listWorkspaces`, `schemaValidate`, `schemaInfer`, `schemaDiff`
- All content methods accept an optional `project` parameter for cross-project operations
- `listProjects` accepts an optional `workspace` parameter for workspace-scoped listing
### Tools (`tools/`)
Each tool file exports a function that calls `api.registerTool()` with a TypeBox schema and handler. Tools delegate to `BmClient` methods and return OpenClaw-standard responses (`{ content: [{type: "text", text}], details? }`).
- `search-notes.ts`, `read-note.ts`, `write-note.ts`, `edit-note.ts`, `delete-note.ts`, `move-note.ts`, `build-context.ts`, `list-memory-projects.ts`, `list-workspaces.ts`, `schema-validate.ts`, `schema-infer.ts`, `schema-diff.ts` — thin wrappers around `BmClient`; all content tools accept an optional `project` param for cross-project operations
- `memory-provider.ts` — composited `memory_search` + `memory_get` providers. `memory_search` queries 3 sources in parallel: MEMORY.md (grep), BM knowledge graph (FTS + vector), and active task notes (YAML frontmatter scan)
### Commands & Hooks
- `commands/slash.ts``/remember` and `/recall` slash commands
- `commands/cli.ts``openclaw basic-memory <subcommand>` CLI registration
- `hooks/capture.ts` — auto-capture hook on `agent_end` events, writes timestamped daily conversation notes
### Configuration (`config.ts`)
Flexible config with defaults, snake_case aliases (`memory_dir`/`memory_file`), tilde/relative/absolute path resolution, and unknown-key validation. Cloud routing is configured through `bm cloud` and per-project BM settings, not plugin config.
## Key Patterns
- **TypeBox schemas** (`@sinclair/typebox`) for all tool parameter validation
- **Bun-native test runner** with `describe`/`it`/`expect` and `jest.fn()` mocking
- **ES modules** (`"type": "module"` in package.json)
- **Biome** for linting and formatting (configured in `biome.json`)
- **Build output**`bun run build` emits `dist/` for `runtimeExtensions`; TypeScript source also stays in the package for source-compatible hosts
- **Strict TypeScript** with `noEmit` (type-checking only)
## Testing
- Unit tests live alongside source files (`*.test.ts`) and mock `BmClient` / `OpenClawPluginApi`
- Integration tests in `integration/` launch a real `bm mcp` process against a temp project
- `scripts/bm-local.sh` runs BM from the monorepo root via `uv run --project ...` when available, then falls back to `bm` on PATH
## CI/CD
- **Package CI** (root `.github/workflows/consolidated-packages.yml`): validates skills, typechecks, lints, builds, tests, and runs `npm pack --dry-run`.
- **Release** (root `.github/workflows/release.yml`): runs from Basic Memory tags and publishes this npm package after the Python release job. Version bumps are handled by the root `just release` / `just beta` recipes.
## Dependencies
- **Runtime**: `@modelcontextprotocol/sdk` (MCP client/transport), `@sinclair/typebox` (schema validation)
- **Peer**: `openclaw` (>=2026.5.2)
- **Dev**: `typescript`, `@biomejs/biome`, `@types/node`
- **External**: Basic Memory CLI (`bm`) must be installed separately (Python, installed via `uv`)
---
# Basic Memory Plugin — Agent Instructions
This plugin provides sophisticated knowledge management through Basic Memory's knowledge graph. Use these tools and guidelines to help users build and navigate their persistent knowledge base.
## Cross-Project Operations
All content tools (`search_notes`, `read_note`, `write_note`, `edit_note`, `delete_note`, `move_note`, `build_context`, `schema_validate`, `schema_infer`, `schema_diff`) accept an optional `project` parameter to operate on a different project than the default. Use this when the user needs to work across multiple knowledge bases.
```
# Search in a different project
search_notes(query="meeting notes", project="team-wiki")
# Read a note from another project
read_note(identifier="decisions/auth-strategy", project="backend")
# Write to a shared project
write_note(title="Shared Insight", content="...", folder="insights", project="shared")
```
## Available Tools
### `list_workspaces`
**Purpose**: List all workspaces (personal and organization) accessible to this user
**When to use**: When the user wants to see what workspaces are available, or before filtering projects by workspace
**Returns**: Workspace names, types (personal/organization), roles, and subscription status
**Examples**:
```
list_workspaces()
```
### `list_memory_projects`
**Purpose**: List all Basic Memory projects, optionally filtered by workspace
**When to use**: When the user wants to see available projects, discover projects in a specific workspace, or before cross-project operations
**Returns**: Project names, paths, default status, and workspace metadata
**Examples**:
```
# List all projects
list_memory_projects()
# List projects in a specific workspace
list_memory_projects(workspace="my-organization")
```
### `search_notes`
**Purpose**: Search the knowledge graph for relevant notes, concepts, and connections
**When to use**: When the user asks about topics, seeks information, or you need context for a discussion
**Returns**: Ranked results with titles, content previews, and relevance scores
**Examples**:
```
# User asks "What did we decide about the API design?"
search_notes(query="API design decisions", limit=5)
# Looking for context on a project
search_notes(query="authentication implementation", limit=3)
# Exploring a broad topic
search_notes(query="meeting notes client feedback", limit=10)
# Search in a different project
search_notes(query="API endpoints", project="backend-docs")
```
### `read_note`
**Purpose**: Read full content of specific notes
**When to use**: When search results show relevant notes that need detailed reading, or when you have a specific note identifier
**Returns**: Complete note content with metadata
**Examples**:
```
# Read a note found in search results
read_note(identifier="projects/api-redesign")
# Navigate to a memory URL
read_note(identifier="memory://agents/decisions/auth-strategy")
# Read by exact title
read_note(identifier="Weekly Review 2024-02-01")
# Read raw markdown including YAML frontmatter
read_note(identifier="projects/api-redesign", include_frontmatter=true)
# Read from another project
read_note(identifier="decisions/auth-strategy", project="backend")
```
### `write_note`
**Purpose**: Create new notes in the knowledge graph
**When to use**: When users share important information, make decisions, or want to save insights for later
**Best practices**: Use clear titles, organize in appropriate folders, structure with headings
**Examples**:
```
# Save a decision or insight
write_note(
title="API Authentication Decision",
folder="decisions",
content="""
# API Authentication Decision
## Context
The team discussed authentication options for the new API.
## Decision
We chose JWT tokens with refresh token rotation.
## Reasoning
- Better security than simple JWTs
- Familiar to the team
- Good ecosystem support
## Next Steps
- [ ] Implement JWT middleware
- [ ] Set up token refresh logic
- [ ] Update API documentation
"""
)
# Document a meeting
write_note(
title="Client Meeting - February 8, 2024",
folder="meetings",
content="""
# Client Meeting - February 8, 2024
## Attendees
- John (client)
- Sarah (product)
- Me (engineering)
## Key Points
- Client wants faster search functionality
- Budget approved for additional features
- Timeline moved up to March 15
## Action Items
- [ ] Prototype search improvements
- [ ] Prepare feature estimate
- [ ] Schedule follow-up meeting
"""
)
```
### `edit_note`
**Purpose**: Modify existing notes incrementally
**When to use**: To add updates, fix information, or organize existing content
**Operations**: append, prepend, find_replace, replace_section
**Examples**:
```
# Add an update to an existing note
edit_note(
identifier="projects/api-redesign",
operation="append",
content="""
## Update - February 8, 2024
Authentication implementation is complete. All tests passing.
Next: Deploy to staging environment.
"""
)
# Update a specific section
edit_note(
identifier="weekly-review",
operation="replace_section",
section="## This Week",
content="""## This Week
- Completed API authentication
- Client meeting went well
- Working on search improvements
- Delayed deployment due to testing issues
"""
)
# Fix a specific detail
edit_note(
identifier="team-contacts",
operation="find_replace",
find_text="sarah@oldcompany.com",
content="sarah@newcompany.com",
expected_replacements=1
)
```
### `delete_note`
**Purpose**: Remove notes from the knowledge graph
**When to use**: When content is outdated, duplicated, or no longer needed
**Returns**: Confirmation of deletion
**Examples**:
```
# Remove an old draft
delete_note(identifier="notes/old-draft")
# Clean up test notes
delete_note(identifier="tests/test-1.0")
```
### `move_note`
**Purpose**: Move notes between folders for organization
**When to use**: When reorganizing knowledge, archiving old content, or correcting folder placement
**Returns**: Updated note with new location
**Examples**:
```
# Archive a completed project
move_note(identifier="projects/api-redesign", newFolder="archive/projects")
# Reorganize into a better folder
move_note(identifier="notes/meeting-notes", newFolder="meetings")
```
### `build_context`
**Purpose**: Navigate the knowledge graph through semantic connections
**When to use**: To explore related concepts, find connected information, or build comprehensive understanding
**Returns**: Target note plus related notes with relationship information
**Examples**:
```
# Explore connections around a project
build_context(url="memory://projects/api-redesign", depth=1)
# Deep dive into related concepts
build_context(url="memory://concepts/authentication", depth=2)
# Discover decision context
build_context(url="memory://decisions/database-choice", depth=1)
```
### `schema_validate`
**Purpose**: Validate notes against their Picoschema definitions
**When to use**: When checking note consistency, after schema changes, or when the user wants to audit note quality
**Examples**:
```
# Validate all notes of a type
schema_validate(noteType="person")
# Validate a single note
schema_validate(identifier="notes/john-doe")
# Validate in another project
schema_validate(noteType="meeting", project="team")
```
### `schema_infer`
**Purpose**: Analyze existing notes and suggest a Picoschema definition
**When to use**: When creating a new schema from existing notes, or exploring what structure notes of a type share
**Examples**:
```
schema_infer(noteType="meeting")
schema_infer(noteType="person", threshold=0.5)
```
### `schema_diff`
**Purpose**: Detect drift between a schema definition and actual note usage
**When to use**: When checking if a schema is still accurate, or after adding new fields to notes
**Examples**:
```
schema_diff(noteType="person")
schema_diff(noteType="Task", project="work")
```
## Knowledge Graph Structure
### Understanding the Graph
Basic Memory organizes information as a **semantic knowledge graph** where:
- **Notes** are documents with content, titles, and metadata
- **Observations** are structured insights extracted from notes
- **Relations** connect related concepts, topics, and decisions
### Memory URLs
Use `memory://` URLs to navigate semantically:
- `memory://projects/api-redesign` - Direct reference to a note
- `memory://agents/decisions` - Category of decision-related notes
- `memory://concepts/authentication` - All content related to authentication
### Organizational Patterns
**Recommended folder structure**:
- `projects/` - Project-specific documentation
- `decisions/` - Important decisions and rationale
- `meetings/` - Meeting notes and action items
- `concepts/` - Technical concepts and explanations
- `agent/` - Agent-captured observations and insights
- `weekly/` - Regular review notes
## Writing Best Practices
### Note Structure
Use consistent markdown structure for better organization:
```markdown
# Clear, Descriptive Title
## Context
Background information and current situation.
## Key Points
- Main insights or decisions
- Important details
- Relevant constraints
## Next Steps
- [ ] Specific action items
- [ ] Follow-up tasks
- [ ] Future considerations
```
### Observation Format
When capturing insights, use this structure:
```markdown
## Observations
- [Decision] We chose PostgreSQL over MongoDB for better ACID guarantees
- [Insight] User authentication patterns suggest social login preference
- [Risk] Current deployment process lacks proper rollback mechanism
- [Opportunity] Search performance could improve with better indexing
```
### Linking and Relations
Create connections between notes:
- Reference other notes by title: `As discussed in [[API Design Principles]]`
- Use consistent terminology for better semantic linking
- Tag important concepts with clear labels
- Cross-reference related decisions and implementations
## When to Use Each Tool
### Discover Workspaces and Projects
Use `list_workspaces` and `list_memory_projects` when:
- User asks what workspaces or projects are available
- Before cross-project operations, to confirm project names
- When switching between personal and organization contexts
### Start with Search
**Always begin with `search_notes`** when:
- User asks about any topic
- You need context for a discussion
- Looking for relevant previous decisions
- Exploring what information already exists
### Read for Details
Use `read_note` when:
- Search results show relevant notes that need full content
- Following up on specific references
- User asks for complete information on a known topic
- Exploring context relationships found in search
### Write for Capture
Use `write_note` when:
- User shares important information to remember
- Decisions are made that should be documented
- Meeting notes or insights need to be preserved
- Creating structured documentation
### Edit for Updates
Use `edit_note` when:
- Adding updates to existing notes
- Fixing or updating specific information
- Organizing existing content better
- Appending new insights to previous notes
### Context for Exploration
Use `build_context` when:
- Exploring relationships between concepts
- Building comprehensive understanding
- Finding related information user might not know exists
- Navigating complex topic areas
## User Interaction Guidelines
### Be Proactive
- **Search first**: Before answering questions, search the knowledge graph
- **Suggest connections**: Point out related notes and concepts
- **Offer to save**: When users share important info, offer to document it
- **Recommend organization**: Help users structure their knowledge well
### Helpful Patterns
```
User: "What did we decide about the database?"
1. Search: search_notes(query="database decision", limit=5)
2. Read relevant: read_note(identifier="decisions/database-choice")
3. Provide answer with context
4. Ask: "Should I add any updates to this decision note?"
User: "I just had a great meeting with the client"
1. Ask for details
2. Offer: "Would you like me to create a meeting note to capture this?"
3. Write: write_note(title="Client Meeting - [date]", ...)
4. Suggest: "I'll also add this to your weekly review notes"
```
### Memory URL Navigation
Help users discover their knowledge:
```
# After finding a note about "API design"
"I found your API design notes. Let me explore related concepts..."
build_context(url="memory://projects/api-design", depth=2)
# Show user what's connected to their decisions
build_context(url="memory://decisions", depth=1)
```
## Working with User Memory Patterns
### Daily/Weekly Reviews
If users maintain review notes, help them:
```
# Update weekly review
edit_note(
identifier="weekly-review",
operation="replace_section",
section="## This Week",
content="Updated accomplishments and next steps"
)
```
### Project Documentation
Keep project notes current:
```
# Add project updates
edit_note(
identifier="projects/current-sprint",
operation="append",
content="""
## Sprint Review
- Completed authentication
- Started search feature
"""
)
```
### Decision Tracking
Document important decisions:
```
write_note(
title="Technical Decision: Database Migration Approach",
folder="decisions",
content="""
# Database Migration Decision
## Problem
Current SQLite database can't handle increased load.
## Options Considered
1. Upgrade to PostgreSQL
2. Switch to MongoDB
3. Migrate to cloud database
## Decision
PostgreSQL with staged migration.
## Rationale
- Better performance characteristics
- Team expertise exists
- Strong ACID guarantees needed
- Migration path is well-understood
"""
)
```
## Error Handling
### Tool Failures
If Basic Memory tools fail:
1. Check if the Basic Memory service is running
2. Suggest user verify `bm` CLI installation
3. Recommend checking OpenClaw plugin configuration
4. Fall back to built-in memory tools if available
### Search No Results
When searches return empty:
- Try broader terms
- Suggest creating a new note for the topic
- Look for related concepts that might exist
- Offer to help organize information differently
### Note Not Found
When reading fails:
- Verify the identifier exists
- Suggest searching for similar titles
- Offer to create the note if it should exist
- Check for typos in memory URLs
## Integration Tips
### With Other Tools
The knowledge graph complements other tools:
- **Web search**: Save research findings as notes
- **File operations**: Reference files in knowledge notes
- **Calendar**: Link meeting notes to calendar events
- **Task management**: Connect tasks to project notes
### With User Workflows
Support user patterns:
- **Morning review**: Search for yesterday's notes and updates
- **End of day**: Capture insights and plan next steps
- **Weekly planning**: Review project notes and decisions
- **Knowledge sharing**: Help organize information for others
## Privacy and Content Guidelines
### Sensitive Information
- Don't automatically save sensitive data (passwords, personal info)
- Ask before documenting confidential business information
- Respect user preferences for what to capture
- Use appropriate folder organization for different privacy levels
### Content Quality
- Encourage clear, structured writing
- Help users create searchable content
- Suggest consistent terminology and naming
- Promote good information architecture
---
Remember: The knowledge graph becomes more valuable over time. Help users build it systematically and navigate it effectively. Focus on creating connections between ideas and making information easily discoverable.
@@ -0,0 +1,275 @@
# Basic Memory ContextEngine Plan
## Goal
Complete the Basic Memory integration with OpenClaw's native memory lifecycle so BM works as a decorator around the default OpenClaw flow instead of relying on `agent_start` / `agent_end` shims.
The target model is:
- OpenClaw owns session state, context assembly pipeline, and compaction.
- Basic Memory owns durable knowledge, cross-session recall, and long-term capture.
- This plugin enriches the default flow without replacing it.
## Scope
This plan is for [issue #34](https://github.com/basicmachines-co/openclaw-basic-memory/issues/34), updated to match the "complement, don't replace" direction discussed there.
We will use the new OpenClaw `ContextEngine` lifecycle introduced in OpenClaw `2026.3.7` on March 6, 2026, but we will not implement a custom compaction strategy.
## Non-Goals
- Do not replace or override OpenClaw compaction behavior.
- Do not compete with lossless-claw or other alternate context engines.
- Do not turn BM into the canonical source of current-session state.
- Do not remove existing BM tools such as `memory_search`, `memory_get`, `search_notes`, `read_note`, and note CRUD tools.
- Do not add aggressive semantic retrieval on every turn.
## Design Principles
### Decorator, not replacement
The plugin should behave like a wrapper around the default OpenClaw memory model:
- OpenClaw tracks the live conversation.
- BM stores durable notes, tasks, decisions, and cross-session context.
- The plugin bridges the two systems at official lifecycle boundaries.
### Keep the baseline flow intact
Where the ContextEngine API requires behavior that OpenClaw already provides well, we should pass through to the default behavior instead of re-implementing it.
### Add value only where BM is strongest
BM should improve:
- session bootstrap recall
- durable post-turn capture
- subagent memory inheritance
- cross-session continuity through notes and graph search
BM should not try to improve:
- session-local compaction
- low-level pruning logic
- runtime token budgeting heuristics
## Current State
Today the plugin uses:
- `api.on("agent_start", ...)` for recall
- `api.on("agent_end", ...)` for capture
- composited `memory_search` / `memory_get` tools for explicit retrieval
This works, but it lives beside OpenClaw's memory lifecycle instead of inside it.
Relevant current files:
- `index.ts`
- `hooks/recall.ts`
- `hooks/capture.ts`
- `tools/memory-provider.ts`
- `types/openclaw.d.ts`
Current dependency constraint:
- `package.json` currently pins `openclaw` peer support to `>=2026.1.29`
- the local installed dependency is `openclaw@2026.2.6`
- ContextEngine work requires moving to the `2026.3.7+` SDK surface
## Target Architecture
Add a `BasicMemoryContextEngine` that composes with the default OpenClaw flow.
Expected lifecycle usage:
- `bootstrap`
- initialize BM session-side recall state
- gather small, high-signal context such as active tasks and recent activity
- `assemble`
- pass through OpenClaw messages
- optionally add a compact BM recall block when useful
- `afterTurn`
- persist durable takeaways from the completed turn into BM
- `prepareSubagentSpawn`
- prepare a minimal BM handoff for a child session
- `onSubagentEnded`
- capture child results back into BM
- `compact`
- do not customize
- use legacy/default pass-through behavior only if the interface requires it
## Phase Plan
## Phase 1
### Commit goal
`feat(context-engine): move recall and capture into native lifecycle`
### Deliverables
- bump OpenClaw compatibility to `2026.3.7+`
- replace the local SDK shim with the real ContextEngine-capable SDK types where possible
- add a `BasicMemoryContextEngine`
- register the engine through `api.registerContextEngine(...)`
- migrate recall behavior from `agent_start` into `bootstrap`
- migrate capture behavior from `agent_end` into `afterTurn`
- keep existing BM tools and service startup behavior intact
- keep compaction fully default
### Expected behavior
- session startup still recalls active tasks and recent activity
- turns still get captured into BM
- plugin behavior is functionally similar to today, but now uses official lifecycle hooks
### Test coverage
- engine registration works
- `bootstrap` returns expected initialized state when recall finds data
- `bootstrap` is a no-op when recall finds nothing
- `afterTurn` captures only valid turn content
- `afterTurn` handles failures without breaking the run
- existing service startup and BM client lifecycle tests still pass
## Phase 2
### Commit goal
`feat(context-engine): add bounded assemble-time BM recall`
### Deliverables
- implement a minimal `assemble` hook
- preserve incoming OpenClaw messages in order
- add an optional BM recall block only when there is useful context
- bound the size of injected BM context so it stays cheap and predictable
- avoid per-turn graph-heavy retrieval unless explicitly configured later
### Expected behavior
- the model sees a small BM memory summary automatically when helpful
- explicit `memory_search` and `memory_get` remain available for deeper retrieval
- OpenClaw remains in charge of the actual context pipeline and compaction
### Test coverage
- `assemble` returns original messages unchanged when no recall block exists
- `assemble` adds a BM block when recall content exists
- injected content is size-bounded
- assembly remains stable across repeated turns when recall content is unchanged
## Phase 3
### Commit goal
`feat(context-engine): add subagent memory handoff`
### Deliverables
- implement `prepareSubagentSpawn`
- implement `onSubagentEnded`
- create a small BM handoff model for parent to child context transfer
- capture child outputs or summaries back into the parent knowledge base
- keep subagent integration lightweight and failure-tolerant
### Expected behavior
- subagents start with relevant BM context instead of a cold memory start
- useful child outputs become durable BM knowledge after completion
- failures in handoff/capture do not break subagent execution
### Test coverage
- child handoff is created for subagent sessions
- rollback path works if spawn fails after preparation
- child completion writes back expected BM artifacts
- delete/release/sweep paths are handled safely
## Implementation Notes
### Engine shape
Prefer a small, explicit implementation instead of pushing logic back into `index.ts`.
Likely new files:
- `context-engine/basic-memory-context-engine.ts`
- `context-engine/basic-memory-context-engine.test.ts`
- optional small helper modules for recall/capture formatting
### Hook migration
After Phase 1 lands, the old event-hook path in `index.ts` should be removed or disabled so we do not double-capture or double-recall.
### Tool preservation
The BM tool surface remains part of the product even after lifecycle integration:
- composited `memory_search` and `memory_get`
- graph CRUD tools
- schema tools
- slash commands and CLI commands
Lifecycle integration complements explicit retrieval; it does not replace it.
### Compatibility posture
This work should be shipped as the canonical BM integration path for OpenClaw `2026.3.7+`.
If we need a temporary compatibility story for older OpenClaw versions, keep it shallow and time-boxed. The long-term target should be one code path based on the native lifecycle.
## Risks
### Single-slot context engine model
OpenClaw currently resolves one `contextEngine` slot, not a middleware stack.
Implication:
- our engine must behave like "default behavior plus BM enrichment"
- we should not assume we can stack with other context engines automatically
### Over-injection
If `assemble` injects too much, BM could bloat prompt cost and work against the default system.
Mitigation:
- keep Phase 2 narrow
- bound injected size
- prefer summaries over raw note dumps
### Double-processing during migration
If old hooks and new lifecycle paths run together, recall and capture may happen twice.
Mitigation:
- Phase 1 should explicitly remove or disable the legacy hook wiring
- add tests that assert only one capture path is active
## Success Criteria
This feature is complete when:
- recall and capture happen through ContextEngine lifecycle hooks, not event shims
- BM enriches default session context without taking over compaction
- subagents inherit and return useful durable memory
- explicit BM tools remain intact
- the architecture clearly reflects "BM decorates OpenClaw memory"
## Commit Sequence
1. `feat(context-engine): move recall and capture into native lifecycle`
2. `feat(context-engine): add bounded assemble-time BM recall`
3. `feat(context-engine): add subagent memory handoff`
## Out of Scope for This Stack
- custom `compact` logic
- BM-driven token budgeting
- replacing post-compaction context reinjection
- new retrieval heuristics beyond a compact recall block
- multi-engine composition support inside OpenClaw core
+130
View File
@@ -0,0 +1,130 @@
# Development
## Local Setup
Clone and link locally for plugin development:
```bash
git clone https://github.com/basicmachines-co/basic-memory.git
cd basic-memory/integrations/openclaw
bun install
bun run fetch-skills
openclaw plugins install -l "$PWD"
openclaw plugins enable openclaw-basic-memory --slot memory
openclaw gateway restart
```
Or load directly from a path in your OpenClaw config:
```json5
{
plugins: {
load: {
paths: ["~/dev/basic-memory/integrations/openclaw"]
},
entries: {
"openclaw-basic-memory": {
enabled: true
}
},
slots: {
memory: "openclaw-basic-memory"
}
}
}
```
## Commands
```bash
just check # fetch skills, typecheck, lint, build, unit tests
just release-check # check + npm pack --dry-run
bun run check-types # Type checking
bun run build # Compile package runtime to dist/
bun run lint # Linting
bun test # Run tests
bun run test:int # Real BM MCP integration tests
```
## Integration Tests
Real end-to-end tests for `BmClient` in `integration/bm-client.integration.test.ts`. These launch a real `bm mcp --transport stdio` process and assert actual filesystem/index results.
```bash
bun run test:int
```
By default this uses `./scripts/bm-local.sh`, which runs BM from the monorepo root via `uv run --project ...` when present, and falls back to `bm` on `PATH` otherwise.
Optional overrides:
```bash
BM_BIN=/absolute/path/to/bm bun run test:int
BASIC_MEMORY_REPO=/absolute/path/to/basic-memory bun run test:int
```
## Publishing to npm
This package is published as `@basicmemory/openclaw-basic-memory`.
```bash
# Verify release readiness (types + build + tests + npm pack dry run)
just release-check
# Inspect publish payload
just release-pack
# Authenticate once (if needed)
npm login
# Publish current version from package.json
just release-publish
```
For a full release (version bump + publish + push tag):
```bash
just release patch # or: minor, major, 0.2.0, etc.
```
### GitHub Actions CI/CD
- CI workflow: `.github/workflows/ci.yml` runs on PRs and `main` pushes
- Release workflow: `.github/workflows/release.yml` runs manually (`workflow_dispatch`)
1. Runs release checks
2. Bumps version and creates a git tag
3. Pushes commit + tag
4. Publishes to npm
5. Creates a GitHub release
Publishing uses npm OIDC trusted publishing — no secrets required.
## Project Structure
```
openclaw-basic-memory/
├── index.ts # Plugin entry — manages MCP lifecycle, registers tools
├── config.ts # Configuration parsing
├── bm-client.ts # Persistent Basic Memory MCP stdio client
├── tools/ # Agent tools
│ ├── search-notes.ts # search_notes
│ ├── read-note.ts # read_note
│ ├── write-note.ts # write_note
│ ├── edit-note.ts # edit_note
│ ├── delete-note.ts # delete_note
│ ├── move-note.ts # move_note
│ ├── build-context.ts # build_context
│ ├── list-memory-projects.ts # list_memory_projects
│ ├── list-workspaces.ts # list_workspaces
│ ├── schema-validate.ts # schema_validate
│ ├── schema-infer.ts # schema_infer
│ ├── schema-diff.ts # schema_diff
│ └── memory-provider.ts # Composited memory_search + memory_get
├── commands/
│ ├── slash.ts # /remember, /recall
│ ├── skills.ts # /tasks, /reflect, /defrag, /schema
│ └── cli.ts # openclaw basic-memory CLI
└── hooks/
├── capture.ts # Auto-capture conversations
└── recall.ts # Auto-recall (active tasks + recent activity)
```

Some files were not shown because too many files have changed in this diff Show More