Skip to content

Commit 03881f6

Browse files
committed
Add --project-id passthrough to CLI tool commands
Removing --workspace without exposing a --project-id alternative left `bm tool` commands unable to disambiguate same-named projects across cloud workspaces. In multi-workspace accounts, name resolution falls back to the default workspace, so write-note (and the other wrappers) could read/write the wrong tenant with no override available to the CLI caller. Adds --project-id (UUID) to all 9 project-scoped CLI tool commands and forwards it to the underlying MCP tool's project_id parameter: - write-note, read-note, edit-note - build-context, recent-activity, search-notes - schema-validate, schema-infer, schema-diff Test coverage: - New test_write_note_project_id_passthrough verifies --project-id reaches the MCP tool's project_id kwarg Closes the P1 finding from Codex review. Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 6c7d7d5 commit 03881f6

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

src/basic_memory/cli/commands/tool.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ def write_note(
6262
help="The project to write to. If not provided, the default project will be used."
6363
),
6464
] = None,
65+
project_id: Annotated[
66+
Optional[str],
67+
typer.Option(
68+
"--project-id",
69+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
70+
),
71+
] = None,
6572
local: bool = typer.Option(
6673
False, "--local", help="Force local API routing (ignore cloud mode)"
6774
),
@@ -102,6 +109,7 @@ def write_note(
102109
content=content,
103110
directory=folder,
104111
project=project,
112+
project_id=project_id,
105113
tags=tags,
106114
output_format="json",
107115
)
@@ -127,6 +135,13 @@ def read_note(
127135
Optional[str],
128136
typer.Option(help="The project to use. If not provided, the default project will be used."),
129137
] = None,
138+
project_id: Annotated[
139+
Optional[str],
140+
typer.Option(
141+
"--project-id",
142+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
143+
),
144+
] = None,
130145
local: bool = typer.Option(
131146
False, "--local", help="Force local API routing (ignore cloud mode)"
132147
),
@@ -147,6 +162,7 @@ def read_note(
147162
mcp_read_note(
148163
identifier=identifier,
149164
project=project,
165+
project_id=project_id,
150166
include_frontmatter=include_frontmatter,
151167
output_format="json",
152168
)
@@ -185,6 +201,13 @@ def edit_note(
185201
help="The project to edit. If not provided, the default project will be used."
186202
),
187203
] = None,
204+
project_id: Annotated[
205+
Optional[str],
206+
typer.Option(
207+
"--project-id",
208+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
209+
),
210+
] = None,
188211
local: bool = typer.Option(
189212
False, "--local", help="Force local API routing (ignore cloud mode)"
190213
),
@@ -208,6 +231,7 @@ def edit_note(
208231
operation=operation,
209232
content=content,
210233
project=project,
234+
project_id=project_id,
211235
section=section,
212236
find_text=find_text,
213237
expected_replacements=expected_replacements,
@@ -245,6 +269,13 @@ def build_context(
245269
Optional[str],
246270
typer.Option(help="The project to use. If not provided, the default project will be used."),
247271
] = None,
272+
project_id: Annotated[
273+
Optional[str],
274+
typer.Option(
275+
"--project-id",
276+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
277+
),
278+
] = None,
248279
local: bool = typer.Option(
249280
False, "--local", help="Force local API routing (ignore cloud mode)"
250281
),
@@ -265,6 +296,7 @@ def build_context(
265296
mcp_build_context(
266297
url=url,
267298
project=project,
299+
project_id=project_id,
268300
depth=depth,
269301
timeframe=timeframe,
270302
page=page,
@@ -297,6 +329,13 @@ def recent_activity(
297329
Optional[str],
298330
typer.Option(help="The project to use. If not provided, the default project will be used."),
299331
] = None,
332+
project_id: Annotated[
333+
Optional[str],
334+
typer.Option(
335+
"--project-id",
336+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
337+
),
338+
] = None,
300339
local: bool = typer.Option(
301340
False, "--local", help="Force local API routing (ignore cloud mode)"
302341
),
@@ -322,6 +361,7 @@ def recent_activity(
322361
page=page,
323362
page_size=page_size,
324363
project=project,
364+
project_id=project_id,
325365
output_format="json",
326366
)
327367
)
@@ -383,6 +423,13 @@ def search_notes(
383423
Optional[str],
384424
typer.Option(help="The project to use. If not provided, the default project will be used."),
385425
] = None,
426+
project_id: Annotated[
427+
Optional[str],
428+
typer.Option(
429+
"--project-id",
430+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
431+
),
432+
] = None,
386433
local: bool = typer.Option(
387434
False, "--local", help="Force local API routing (ignore cloud mode)"
388435
),
@@ -453,6 +500,7 @@ def search_notes(
453500
mcp_search(
454501
query=query or None,
455502
project=project,
503+
project_id=project_id,
456504
search_type=search_type,
457505
output_format="json",
458506
page=page,
@@ -562,6 +610,13 @@ def schema_validate(
562610
Optional[str],
563611
typer.Option(help="The project to use. If not provided, the default project will be used."),
564612
] = None,
613+
project_id: Annotated[
614+
Optional[str],
615+
typer.Option(
616+
"--project-id",
617+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
618+
),
619+
] = None,
565620
local: bool = typer.Option(
566621
False, "--local", help="Force local API routing (ignore cloud mode)"
567622
),
@@ -595,6 +650,7 @@ def schema_validate(
595650
note_type=note_type,
596651
identifier=identifier,
597652
project=project,
653+
project_id=project_id,
598654
output_format="json",
599655
)
600656
)
@@ -625,6 +681,13 @@ def schema_infer(
625681
Optional[str],
626682
typer.Option(help="The project to use. If not provided, the default project will be used."),
627683
] = None,
684+
project_id: Annotated[
685+
Optional[str],
686+
typer.Option(
687+
"--project-id",
688+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
689+
),
690+
] = None,
628691
local: bool = typer.Option(
629692
False, "--local", help="Force local API routing (ignore cloud mode)"
630693
),
@@ -647,6 +710,7 @@ def schema_infer(
647710
note_type=note_type,
648711
threshold=threshold,
649712
project=project,
713+
project_id=project_id,
650714
output_format="json",
651715
)
652716
)
@@ -674,6 +738,13 @@ def schema_diff(
674738
Optional[str],
675739
typer.Option(help="The project to use. If not provided, the default project will be used."),
676740
] = None,
741+
project_id: Annotated[
742+
Optional[str],
743+
typer.Option(
744+
"--project-id",
745+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
746+
),
747+
] = None,
677748
local: bool = typer.Option(
678749
False, "--local", help="Force local API routing (ignore cloud mode)"
679750
),
@@ -694,6 +765,7 @@ def schema_diff(
694765
mcp_schema_diff(
695766
note_type=note_type,
696767
project=project,
768+
project_id=project_id,
697769
output_format="json",
698770
)
699771
)

tests/cli/test_cli_tool_json_output.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,38 @@ def test_write_note_json_output(mock_mcp_write):
113113
assert mock_mcp_write.call_args.kwargs["output_format"] == "json"
114114

115115

116+
@patch(
117+
"basic_memory.cli.commands.tool.mcp_write_note",
118+
new_callable=AsyncMock,
119+
return_value=WRITE_NOTE_RESULT,
120+
)
121+
def test_write_note_project_id_passthrough(mock_mcp_write):
122+
"""--project-id forwards to the MCP tool's project_id parameter.
123+
124+
Regression: removing --workspace without exposing --project-id left CLI
125+
callers unable to disambiguate same-named projects across cloud workspaces.
126+
"""
127+
uuid = "11111111-1111-1111-1111-111111111111"
128+
result = runner.invoke(
129+
cli_app,
130+
[
131+
"tool",
132+
"write-note",
133+
"--title",
134+
"Test Note",
135+
"--folder",
136+
"notes",
137+
"--content",
138+
"hello",
139+
"--project-id",
140+
uuid,
141+
],
142+
)
143+
144+
assert result.exit_code == 0, f"CLI failed: {result.output}"
145+
assert mock_mcp_write.call_args.kwargs["project_id"] == uuid
146+
147+
116148
@patch(
117149
"basic_memory.cli.commands.tool.mcp_write_note",
118150
new_callable=AsyncMock,

0 commit comments

Comments
 (0)