diff --git a/CHANGELOG.md b/CHANGELOG.md index d294365..0e96987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### 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/.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 and discovery friction in the real-world note "Hermes Basic Memory Cloud Task Experience." A SKILL.md update with the worked discovery → route → write → verify workflow is the remaining follow-up. 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. +- 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 diff --git a/skill/SKILL.md b/skill/SKILL.md index 0ae9f89..4fdc1a5 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -23,6 +23,9 @@ The CLI is fine when you genuinely need a feature these wrappers don't expose (r | 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 @@ -75,6 +78,103 @@ bm_edit({ ### `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: })` 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: , + 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 | @@ -83,10 +183,14 @@ Use sparingly. `bm_move` takes `new_folder`. | 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: ""` | +| 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 -Use consistent markdown: +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 @@ -112,10 +216,6 @@ Background and current situation. - [ ] Document ``` -## Memory URLs - -`memory://projects/api-redesign` — direct reference. Used in `bm_context`, `bm_read`. The `memory://` prefix is optional for `bm_read`. - ## Behavior guidelines 1. **Search before answering.** If the user asks "what did we decide about X?", run `bm_search` first. @@ -127,3 +227,16 @@ Background and current situation. ## Footgun If a note's body contains literal `...` 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/.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. diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 4c780ab..f4bff58 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -314,7 +314,7 @@ def test_translate_uses_project_id_override(bm): """Agent passes project_id= → reaches BM as project_id, with no project name in the call (would be redundant and risk server-side precedence surprises).""" - uuid = "01HXYZ123ABC456DEF789GHI" + uuid = "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d" _, args = bm._translate_args( "bm_search", {"query": "hi", "project_id": uuid}, "default-proj" ) @@ -325,7 +325,7 @@ def test_translate_uses_project_id_override(bm): 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 = "01HXYZ123ABC456DEF789GHI" + uuid = "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d" _, args = bm._translate_args( "bm_search", {"query": "hi", "project": "main", "project_id": uuid}, @@ -361,9 +361,9 @@ def test_translate_routing_works_for_every_tool(bm, tool, base_args): _, out = bm._translate_args(tool, args_with, "default-proj") assert out["project"] == "main" - args_with_id = dict(base_args, project_id="uuid-1") + 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"] == "uuid-1" + assert out["project_id"] == "e1d3a5b8-0492-4c1f-8e7d-2a4b6c8d0e2f" assert "project" not in out _, out = bm._translate_args(tool, base_args, "default-proj") @@ -398,7 +398,7 @@ def test_translate_global_tools_ignore_project_kwargs(bm): the listing would be worse than ignoring the args.""" _, out = bm._translate_args( "bm_projects", - {"project": "main", "project_id": "uuid-1"}, + {"project": "main", "project_id": "e1d3a5b8-0492-4c1f-8e7d-2a4b6c8d0e2f"}, "default-proj", ) assert "project" not in out