Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<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 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
Expand Down
123 changes: 118 additions & 5 deletions skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: <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 |
Expand All @@ -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: "<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

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
Expand All @@ -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.
Expand All @@ -127,3 +227,16 @@ Background and current situation.
## 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.
10 changes: 5 additions & 5 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ 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 = "01HXYZ123ABC456DEF789GHI"
uuid = "bf2a4c1e-d77f-4b7a-9c3e-5d8a1f0e2b6d"
_, args = bm._translate_args(
"bm_search", {"query": "hi", "project_id": uuid}, "default-proj"
)
Expand All @@ -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},
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Loading