Skip to content

feat(mcp): move notes and folders into another project - #2048

Merged
phernandez merged 2 commits into
mainfrom
feat/1842-cross-project-move
Nov 4, 2026
Merged

phernandez merged 2 commits into
mainfrom
feat/1842-cross-project-move

Conversation

@phernandez

Copy link
Copy Markdown
Member

Closes #1842.

What it does

bm_move(..., destination_project="research") moves a note (identifier) or a directory (path) into another project. destination is then a path in that project, and a trailing / keeps the name, as it does within a project.

As the issue specifies, a cross-project move is a write in the target followed by a delete in the source:

  • The moved note is new in the target: new id, its own version history.
  • Links in either project are not rewritten.
  • A note already at the destination is never replaced (ALREADY_EXISTS, nothing moved).
  • Permalink collisions resolve the way any new write does.
  • destination_project naming the source's own project is an ordinary move, which keeps id and history.

Safety

New module: src/basic_memory/mcp/cross_project_move.py.

Order of operations:

  1. Take the note's checksum, then read its content.
  2. Write the copy in the target project with overwrite=false.
  3. Delete the source, guarded by that checksum.

If the note changes at any point, the guarded delete refuses.

When the source delete fails:

  • Definite refusal (changed note, 403): the copy is deleted again, guarded by its own checksum, and the move refuses with "Nothing was moved."
  • Unknown outcome (no answer): the copy stays and the refusal says where it is, because the source may already be gone. Removing the copy then could lose the note.
  • Copy can't be removed: PARTIAL_FAILURE says plainly that the note is now in both projects.

Up-front refusals:

  • A locked note (locked: true) is refused before anything is written. Its copy would be locked too and could not be removed.
  • A destination that isn't a Markdown note path.

Directories move note by note. Files that aren't notes (attachments, .txt) stay. A directory with anything left behind ends in PARTIAL_FAILURE listing each file that stayed.

Write access: a caller who can't write the target, or delete in the source, is refused through the API's own answers. To support that, refusals.py now maps 403 to a new FORBIDDEN code instead of OUTCOME_UNKNOWN. Before this, a 403 read as "may or may not have happened".

Interface

  • New destination_project parameter on bm_move, and --destination-project on bm tool move.
  • A destination that starts with another project's name, without destination_project, is still refused. The hint now points at destination_project.
  • bm-move(3) describes the behavior and has an example.
  • No API changes: the move reuses the write, entity read and delete routes.

Not included

  • Attachments and other non-note files don't move between projects.
  • Cross-workspace moves.
  • Keeping ids, history or links.

Tests

tests/mcp/test_tool_bm_move_across_projects.py, 20 tests on the real ASGI stack. Covers:

  • notes and the trailing-slash name
  • occupied destination, stale checksum, locked note, non-Markdown destination
  • the same-project shortcut, and the refusal hint
  • a refused source delete (copy removed), an unanswered source delete (copy kept), a copy that can't be removed, a refused target write
  • directories: full, partial, text output, a non-note file, the project root, empty, and paging

The failure cases use an httpx transport in front of the real app that answers 403 or drops the request for one HTTP method. No monkeypatching.

Coverage: 100% for cross_project_move.py and tools/move.py. tests/mcp + tests/cli pass (2,524). The new tests also pass on Postgres. just fast-check passes.

๐Ÿค– Generated with Claude Code

https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo

bm_move(destination_project=...) moves a note or a directory into
another project. Per #1842, a cross-project move is a write in the
target followed by a delete in the source: the note is new there (new
id, its own history), and links are not rewritten.

- The source is deleted only after the copy is written, and only while
  it is still the copied revision. A refused delete removes the copy
  again, so a refused move changes neither project. When the delete's
  outcome is unknown the copy stays, since the source may be gone.
- A note already at the destination is never replaced; a locked note is
  refused before anything is written.
- A directory moves note by note; files that are not notes stay and the
  result lists what did not move.
- destination_project naming the source project is an ordinary move.
- 403 answers now map to a FORBIDDEN refusal instead of OUTCOME_UNKNOWN.
- bm tool move gets --destination-project; bm-move(3) documents it.

Closes #1842

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
๐Ÿ“ Code Review โœ… Completed 2026-11-04T09:19:28.583015Z fe69e1d New commits
โ„น๏ธ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with ๐Ÿ‘€ while any review is running, comments if it has suggestions, and reacts with ๐Ÿ‘ once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ’ก Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a15c1aa4e

โ„น๏ธ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with ๐Ÿ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The copy took its title from the destination file name, so a move that
also renamed the file rewrote the title. The file name sets the path;
the title comes from the source note.

Update the integration test for the destination_project hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez merged commit 8afa744 into main Nov 4, 2026
53 checks passed
@phernandez
phernandez deleted the feat/1842-cross-project-move branch November 4, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move notes and folders between projects

1 participant