Skip to content

Commit ed82b0c

Browse files
committed
Merge branch 'main' of github.com:basicmachines-co/basic-memory
2 parents 0a36256 + 0cb3f95 commit ed82b0c

22 files changed

Lines changed: 1453 additions & 302 deletions

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -418,19 +418,19 @@ basic-memory tool edit-note docs/setup --operation append --content $'\n- Added
418418

419419
**Content Management:**
420420
```
421-
write_note(title, content, folder, tags) - Create or update notes
422-
read_note(identifier, page, page_size) - Read notes by title or permalink
421+
write_note(title, content, folder, tags, output_format="text"|"json") - Create or update notes
422+
read_note(identifier, page, page_size, output_format="text"|"json") - Read notes by title or permalink
423423
read_content(path) - Read raw file content (text, images, binaries)
424424
view_note(identifier) - View notes as formatted artifacts
425-
edit_note(identifier, operation, content) - Edit notes incrementally
426-
move_note(identifier, destination_path) - Move notes with database consistency
427-
delete_note(identifier) - Delete notes from knowledge base
425+
edit_note(identifier, operation, content, output_format="text"|"json") - Edit notes incrementally
426+
move_note(identifier, destination_path, output_format="text"|"json") - Move notes with database consistency
427+
delete_note(identifier, output_format="text"|"json") - Delete notes from knowledge base
428428
```
429429

430430
**Knowledge Graph Navigation:**
431431
```
432-
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
433-
recent_activity(type, depth, timeframe) - Find recently updated information
432+
build_context(url, depth, timeframe, output_format="json"|"text") - Navigate knowledge graph via memory:// URLs
433+
recent_activity(type, depth, timeframe, output_format="text"|"json") - Find recently updated information
434434
list_directory(dir_name, depth) - Browse directory contents with filtering
435435
```
436436

@@ -443,12 +443,15 @@ search_by_metadata(filters, limit, offset, project) - Structured frontmatter sea
443443

444444
**Project Management:**
445445
```
446-
list_memory_projects() - List all available projects
447-
create_memory_project(project_name, project_path) - Create new projects
446+
list_memory_projects(output_format="text"|"json") - List all available projects
447+
create_memory_project(project_name, project_path, output_format="text"|"json") - Create new projects
448448
get_current_project() - Show current project stats
449449
sync_status() - Check synchronization status
450450
```
451451

452+
`output_format` defaults to `"text"` for these tools, preserving current human-readable responses.
453+
`build_context` defaults to `"json"` and can be switched to `"text"` when compact markdown output is preferred.
454+
452455
**Cloud Discovery (opt-in):**
453456
```
454457
cloud_info() - Show optional Cloud overview and setup guidance

docs/mcp-ui-bakeoff-instructions.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,26 @@ Manual check:
8383

8484
---
8585

86-
### 2) ASCII / ANSI TUI Output
86+
### 2) Text / JSON Output Modes
8787

8888
Tools:
89-
- `search_notes(output_format="ascii" | "ansi")`
90-
- `read_note(output_format="ascii" | "ansi")`
89+
- `search_notes(output_format="text" | "json")`
90+
- `read_note(output_format="text" | "json")`
91+
- `write_note(output_format="text" | "json")`
92+
- `edit_note(output_format="text" | "json")`
93+
- `recent_activity(output_format="text" | "json")`
94+
- `list_memory_projects(output_format="text" | "json")`
95+
- `create_memory_project(output_format="text" | "json")`
96+
- `delete_note(output_format="text" | "json")`
97+
- `move_note(output_format="text" | "json")`
98+
- `build_context(output_format="json" | "text")`
9199

92100
Expect:
93-
- ASCII table for search, header + content preview for note.
94-
- ANSI variants include color escape codes.
101+
- `text` mode preserves existing human-readable responses.
102+
- `json` mode returns structured dict/list payloads for machine-readable clients.
95103

96104
Automated:
97-
- `uv run pytest test-int/mcp/test_output_format_ascii_integration.py`
105+
- `uv run pytest test-int/mcp/test_output_format_json_integration.py`
98106

99107
---
100108

@@ -125,6 +133,6 @@ Fill in after running:
125133

126134
- Tool‑UI (React): __
127135
- MCP‑UI SDK (embedded): __
128-
- ASCII/ANSI: __
136+
- Text/JSON modes: __
129137

130138
Decision + rationale: __

docs/post-v0.18.0-test-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Key finding: **FastEmbed (384-d local ONNX) matches or exceeds OpenAI (1536-d) q
180180
### Existing coverage anchor points
181181

182182
- `tests/mcp/test_tool_contracts.py`
183-
- `test-int/mcp/test_output_format_ascii_integration.py`
183+
- `test-int/mcp/test_output_format_json_integration.py`
184184
- `test-int/mcp/test_ui_sdk_integration.py`
185185

186186
### Gaps to close — DONE
@@ -288,7 +288,7 @@ Run after automated tests pass.
288288
- Routing: verify success/failure paths with and without API key.
289289
- Permalink routing: read/write/search notes across projects with colliding titles.
290290
- Permalink routing: verify memory URL routing correctness.
291-
- UI/TUI: call `search_notes` and `read_note` with UI variants and `output_format=ascii|ansi`.
291+
- UI/TUI: call `search_notes` and `read_note` with UI variants and `output_format=text|json`.
292292
- UI/TUI: verify payload/resource format and metadata completeness.
293293

294294
## Implementation Backlog (Ordered)

src/basic_memory/cli/commands/tool.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,14 @@ async def _read_note_json(
160160
search_type="title",
161161
project=project_name,
162162
workspace=workspace,
163+
output_format="json",
163164
)
164-
if title_results and hasattr(title_results, "results") and title_results.results:
165-
result = title_results.results[0]
166-
if result.permalink:
167-
entity_id = await knowledge_client.resolve_entity(result.permalink)
165+
results = title_results.get("results", []) if isinstance(title_results, dict) else []
166+
if results:
167+
result = results[0]
168+
permalink = result.get("permalink")
169+
if permalink:
170+
entity_id = await knowledge_client.resolve_entity(permalink)
168171

169172
if entity_id is None:
170173
raise ValueError(f"Could not find note matching: {identifier}")
@@ -635,10 +638,13 @@ def build_context(
635638
page=page,
636639
page_size=page_size,
637640
max_related=max_related,
641+
output_format="text" if format == "text" else "json",
638642
)
639643
)
640-
# build_context now returns a slimmed dict (already serializable)
641-
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
644+
if format == "json":
645+
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
646+
else:
647+
print(result)
642648
except ValueError as e:
643649
typer.echo(f"Error: {e}", err=True)
644650
raise typer.Exit(1)

src/basic_memory/mcp/tools/build_context.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Build context tool for Basic Memory MCP server."""
22

3-
from typing import Optional
3+
from typing import Optional, Literal
44

55
from loguru import logger
66
from fastmcp import Context
@@ -190,7 +190,7 @@ def _format_context_markdown(graph: GraphContext, project: str) -> str:
190190
191191
Format options:
192192
- "json" (default): Slimmed JSON with redundant fields removed
193-
- "markdown": Compact markdown text for LLM consumption
193+
- "text": Compact markdown text for LLM consumption
194194
""",
195195
)
196196
async def build_context(
@@ -202,7 +202,7 @@ async def build_context(
202202
page: int = 1,
203203
page_size: int = 10,
204204
max_related: int = 10,
205-
format: str = "json",
205+
output_format: Literal["json", "text"] = "json",
206206
context: Context | None = None,
207207
) -> dict | str:
208208
"""Get context needed to continue a discussion within a specific project.
@@ -225,12 +225,13 @@ async def build_context(
225225
page: Page number of results to return (default: 1)
226226
page_size: Number of results to return per page (default: 10)
227227
max_related: Maximum number of related results to return (default: 10)
228-
format: Response format - "json" for slimmed JSON dict, "markdown" for compact text
228+
output_format: Response format - "json" for slimmed JSON dict,
229+
"text" for compact markdown text
229230
context: Optional FastMCP context for performance caching.
230231
231232
Returns:
232-
dict (format="json"): Slimmed JSON with redundant fields removed
233-
str (format="markdown"): Compact markdown representation
233+
dict (output_format="json"): Slimmed JSON with redundant fields removed
234+
str (output_format="text"): Compact markdown representation
234235
235236
Examples:
236237
# Continue a specific discussion
@@ -239,8 +240,8 @@ async def build_context(
239240
# Get deeper context about a component
240241
build_context("work-docs", "memory://components/memory-service", depth=2)
241242
242-
# Get markdown output for compact context
243-
build_context("research", "memory://specs/search", format="markdown")
243+
# Get text output for compact context
244+
build_context("research", "memory://specs/search", output_format="text")
244245
245246
Raises:
246247
ToolError: If project doesn't exist or depth parameter is invalid
@@ -276,7 +277,7 @@ async def build_context(
276277
max_related=max_related,
277278
)
278279

279-
if format == "markdown":
280+
if output_format == "text":
280281
return _format_context_markdown(graph, active_project.name)
281282

282283
return _slim_context(graph)

src/basic_memory/mcp/tools/chatgpt_tools.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,41 @@
1313
from basic_memory.mcp.server import mcp
1414
from basic_memory.mcp.tools.search import search_notes
1515
from basic_memory.mcp.tools.read_note import read_note
16-
from basic_memory.schemas.search import SearchResponse
1716
from basic_memory.config import ConfigManager
17+
from basic_memory.schemas.search import SearchResponse, SearchResult
1818

1919

20-
def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
20+
def _format_search_results_for_chatgpt(
21+
results: SearchResponse | list[SearchResult] | list[dict[str, Any]] | dict[str, Any],
22+
) -> List[Dict[str, Any]]:
2123
"""Format search results according to ChatGPT's expected schema.
2224
2325
Returns a list of result objects with id, title, and url fields.
2426
"""
27+
if isinstance(results, SearchResponse):
28+
raw_results: list[SearchResult] | list[dict[str, Any]] = results.results
29+
elif isinstance(results, dict):
30+
nested_results = results.get("results")
31+
raw_results = nested_results if isinstance(nested_results, list) else []
32+
else:
33+
raw_results = results
34+
2535
formatted_results = []
2636

27-
for result in results.results:
37+
for result in raw_results:
38+
if isinstance(result, SearchResult):
39+
title = result.title
40+
permalink = result.permalink
41+
elif isinstance(result, dict):
42+
title = result.get("title")
43+
permalink = result.get("permalink")
44+
else:
45+
raise TypeError(f"Unexpected result type: {type(result).__name__}")
46+
2847
formatted_result = {
29-
"id": result.permalink or f"doc-{len(formatted_results)}",
30-
"title": result.title if result.title and result.title.strip() else "Untitled",
31-
"url": result.permalink or "",
48+
"id": permalink or f"doc-{len(formatted_results)}",
49+
"title": title if isinstance(title, str) and title.strip() else "Untitled",
50+
"url": permalink or "",
3251
}
3352
formatted_results.append(formatted_result)
3453

@@ -102,6 +121,7 @@ async def search(
102121
page=1,
103122
page_size=10, # Reasonable default for ChatGPT consumption
104123
search_type="text", # Default to full-text search
124+
output_format="json",
105125
context=context,
106126
)
107127

@@ -115,10 +135,11 @@ async def search(
115135
}
116136
else:
117137
# Format successful results for ChatGPT
118-
formatted_results = _format_search_results_for_chatgpt(results)
138+
raw_results = results.get("results", []) if isinstance(results, dict) else []
139+
formatted_results = _format_search_results_for_chatgpt(raw_results)
119140
search_results = {
120141
"results": formatted_results,
121-
"total_count": len(results.results), # Use actual count from results
142+
"total_count": len(raw_results), # Use actual count from results
122143
"query": query,
123144
}
124145
logger.info(f"Search completed: {len(formatted_results)} results returned")

0 commit comments

Comments
 (0)