basic-memory 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (89) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +23 -1
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  7. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
  8. basic_memory/api/app.py +9 -10
  9. basic_memory/api/routers/__init__.py +2 -1
  10. basic_memory/api/routers/knowledge_router.py +31 -5
  11. basic_memory/api/routers/memory_router.py +18 -17
  12. basic_memory/api/routers/project_info_router.py +275 -0
  13. basic_memory/api/routers/resource_router.py +105 -4
  14. basic_memory/api/routers/search_router.py +22 -4
  15. basic_memory/cli/app.py +54 -5
  16. basic_memory/cli/commands/__init__.py +15 -2
  17. basic_memory/cli/commands/db.py +9 -13
  18. basic_memory/cli/commands/import_chatgpt.py +26 -30
  19. basic_memory/cli/commands/import_claude_conversations.py +27 -29
  20. basic_memory/cli/commands/import_claude_projects.py +29 -31
  21. basic_memory/cli/commands/import_memory_json.py +26 -28
  22. basic_memory/cli/commands/mcp.py +7 -1
  23. basic_memory/cli/commands/project.py +119 -0
  24. basic_memory/cli/commands/project_info.py +167 -0
  25. basic_memory/cli/commands/status.py +14 -28
  26. basic_memory/cli/commands/sync.py +63 -22
  27. basic_memory/cli/commands/tool.py +253 -0
  28. basic_memory/cli/main.py +39 -1
  29. basic_memory/config.py +166 -4
  30. basic_memory/db.py +19 -4
  31. basic_memory/deps.py +10 -3
  32. basic_memory/file_utils.py +37 -19
  33. basic_memory/markdown/entity_parser.py +3 -3
  34. basic_memory/markdown/utils.py +5 -0
  35. basic_memory/mcp/async_client.py +1 -1
  36. basic_memory/mcp/main.py +24 -0
  37. basic_memory/mcp/prompts/__init__.py +19 -0
  38. basic_memory/mcp/prompts/ai_assistant_guide.py +26 -0
  39. basic_memory/mcp/prompts/continue_conversation.py +111 -0
  40. basic_memory/mcp/prompts/recent_activity.py +88 -0
  41. basic_memory/mcp/prompts/search.py +182 -0
  42. basic_memory/mcp/prompts/utils.py +155 -0
  43. basic_memory/mcp/server.py +2 -6
  44. basic_memory/mcp/tools/__init__.py +12 -21
  45. basic_memory/mcp/tools/build_context.py +85 -0
  46. basic_memory/mcp/tools/canvas.py +97 -0
  47. basic_memory/mcp/tools/delete_note.py +28 -0
  48. basic_memory/mcp/tools/project_info.py +51 -0
  49. basic_memory/mcp/tools/read_content.py +229 -0
  50. basic_memory/mcp/tools/read_note.py +190 -0
  51. basic_memory/mcp/tools/recent_activity.py +100 -0
  52. basic_memory/mcp/tools/search.py +56 -17
  53. basic_memory/mcp/tools/utils.py +245 -16
  54. basic_memory/mcp/tools/write_note.py +124 -0
  55. basic_memory/models/knowledge.py +27 -11
  56. basic_memory/models/search.py +2 -1
  57. basic_memory/repository/entity_repository.py +3 -2
  58. basic_memory/repository/project_info_repository.py +9 -0
  59. basic_memory/repository/repository.py +24 -7
  60. basic_memory/repository/search_repository.py +47 -14
  61. basic_memory/schemas/__init__.py +10 -9
  62. basic_memory/schemas/base.py +4 -1
  63. basic_memory/schemas/memory.py +14 -4
  64. basic_memory/schemas/project_info.py +96 -0
  65. basic_memory/schemas/search.py +29 -33
  66. basic_memory/services/context_service.py +3 -3
  67. basic_memory/services/entity_service.py +26 -13
  68. basic_memory/services/file_service.py +145 -26
  69. basic_memory/services/link_resolver.py +9 -46
  70. basic_memory/services/search_service.py +95 -22
  71. basic_memory/sync/__init__.py +3 -2
  72. basic_memory/sync/sync_service.py +523 -117
  73. basic_memory/sync/watch_service.py +258 -132
  74. basic_memory/utils.py +51 -36
  75. basic_memory-0.9.0.dist-info/METADATA +736 -0
  76. basic_memory-0.9.0.dist-info/RECORD +99 -0
  77. basic_memory/alembic/README +0 -1
  78. basic_memory/cli/commands/tools.py +0 -157
  79. basic_memory/mcp/tools/knowledge.py +0 -68
  80. basic_memory/mcp/tools/memory.py +0 -170
  81. basic_memory/mcp/tools/notes.py +0 -202
  82. basic_memory/schemas/discovery.py +0 -28
  83. basic_memory/sync/file_change_scanner.py +0 -158
  84. basic_memory/sync/utils.py +0 -31
  85. basic_memory-0.7.0.dist-info/METADATA +0 -378
  86. basic_memory-0.7.0.dist-info/RECORD +0 -82
  87. {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
  88. {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
  89. {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,229 @@
1
+ """File reading tool for Basic Memory MCP server.
2
+
3
+ This module provides tools for reading raw file content directly,
4
+ supporting various file types including text, images, and other binary files.
5
+ Files are read directly without any knowledge graph processing.
6
+ """
7
+
8
+ from loguru import logger
9
+
10
+ from basic_memory.mcp.server import mcp
11
+ from basic_memory.mcp.async_client import client
12
+ from basic_memory.mcp.tools.utils import call_get
13
+ from basic_memory.schemas.memory import memory_url_path
14
+
15
+ import base64
16
+ import io
17
+ from PIL import Image as PILImage
18
+
19
+
20
+ def calculate_target_params(content_length):
21
+ """Calculate initial quality and size based on input file size"""
22
+ target_size = 350000 # Reduced target for more safety margin
23
+ ratio = content_length / target_size
24
+
25
+ logger.debug(
26
+ "Calculating target parameters",
27
+ content_length=content_length,
28
+ ratio=ratio,
29
+ target_size=target_size,
30
+ )
31
+
32
+ if ratio > 4:
33
+ # Very large images - start very aggressive
34
+ return 50, 600 # Lower initial quality and size
35
+ elif ratio > 2:
36
+ return 60, 800
37
+ else:
38
+ return 70, 1000
39
+
40
+
41
+ def resize_image(img, max_size):
42
+ """Resize image maintaining aspect ratio"""
43
+ original_dimensions = {"width": img.width, "height": img.height}
44
+
45
+ if img.width > max_size or img.height > max_size:
46
+ ratio = min(max_size / img.width, max_size / img.height)
47
+ new_size = (int(img.width * ratio), int(img.height * ratio))
48
+ logger.debug("Resizing image", original=original_dimensions, target=new_size, ratio=ratio)
49
+ return img.resize(new_size, PILImage.Resampling.LANCZOS)
50
+
51
+ logger.debug("No resize needed", dimensions=original_dimensions)
52
+ return img
53
+
54
+
55
+ def optimize_image(img, content_length, max_output_bytes=350000):
56
+ """Iteratively optimize image with aggressive size reduction"""
57
+ stats = {
58
+ "dimensions": {"width": img.width, "height": img.height},
59
+ "mode": img.mode,
60
+ "estimated_memory": (img.width * img.height * len(img.getbands())),
61
+ }
62
+
63
+ initial_quality, initial_size = calculate_target_params(content_length)
64
+
65
+ logger.debug(
66
+ "Starting optimization",
67
+ image_stats=stats,
68
+ content_length=content_length,
69
+ initial_quality=initial_quality,
70
+ initial_size=initial_size,
71
+ max_output_bytes=max_output_bytes,
72
+ )
73
+
74
+ quality = initial_quality
75
+ size = initial_size
76
+
77
+ # Convert to RGB if needed
78
+ if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
79
+ img = img.convert("RGB")
80
+ logger.debug("Converted to RGB mode")
81
+
82
+ iteration = 0
83
+ min_size = 300 # Absolute minimum size
84
+ min_quality = 20 # Absolute minimum quality
85
+
86
+ while True:
87
+ iteration += 1
88
+ buf = io.BytesIO()
89
+ resized = resize_image(img, size)
90
+
91
+ resized.save(
92
+ buf,
93
+ format="JPEG",
94
+ quality=quality,
95
+ optimize=True,
96
+ progressive=True,
97
+ subsampling="4:2:0",
98
+ )
99
+
100
+ output_size = buf.getbuffer().nbytes
101
+ reduction_ratio = output_size / content_length
102
+
103
+ logger.debug(
104
+ "Optimization attempt",
105
+ iteration=iteration,
106
+ quality=quality,
107
+ size=size,
108
+ output_bytes=output_size,
109
+ target_bytes=max_output_bytes,
110
+ reduction_ratio=f"{reduction_ratio:.2f}",
111
+ )
112
+
113
+ if output_size < max_output_bytes:
114
+ logger.info(
115
+ "Image optimization complete",
116
+ final_size=output_size,
117
+ quality=quality,
118
+ dimensions={"width": resized.width, "height": resized.height},
119
+ reduction_ratio=f"{reduction_ratio:.2f}",
120
+ )
121
+ return buf.getvalue()
122
+
123
+ # Very aggressive reduction for large files
124
+ if content_length > 2000000: # 2MB+ # pragma: no cover
125
+ quality = max(min_quality, quality - 20)
126
+ size = max(min_size, int(size * 0.6))
127
+ elif content_length > 1000000: # 1MB+ # pragma: no cover
128
+ quality = max(min_quality, quality - 15)
129
+ size = max(min_size, int(size * 0.7))
130
+ else:
131
+ quality = max(min_quality, quality - 10) # pragma: no cover
132
+ size = max(min_size, int(size * 0.8)) # pragma: no cover
133
+
134
+ logger.debug("Reducing parameters", new_quality=quality, new_size=size) # pragma: no cover
135
+
136
+ # If we've hit minimum values and still too big
137
+ if quality <= min_quality and size <= min_size: # pragma: no cover
138
+ logger.warning(
139
+ "Reached minimum parameters",
140
+ final_size=output_size,
141
+ over_limit_by=output_size - max_output_bytes,
142
+ )
143
+ return buf.getvalue()
144
+
145
+
146
+ @mcp.tool(description="Read a file's raw content by path or permalink")
147
+ async def read_content(path: str) -> dict:
148
+ """Read a file's raw content by path or permalink.
149
+
150
+ This tool provides direct access to file content in the knowledge base,
151
+ handling different file types appropriately:
152
+ - Text files (markdown, code, etc.) are returned as plain text
153
+ - Images are automatically resized/optimized for display
154
+ - Other binary files are returned as base64 if below size limits
155
+
156
+ Args:
157
+ path: The path or permalink to the file. Can be:
158
+ - A regular file path (docs/example.md)
159
+ - A memory URL (memory://docs/example)
160
+ - A permalink (docs/example)
161
+
162
+ Returns:
163
+ A dictionary with the file content and metadata:
164
+ - For text: {"type": "text", "text": "content", "content_type": "text/markdown", "encoding": "utf-8"}
165
+ - For images: {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "base64_data"}}
166
+ - For other files: {"type": "document", "source": {"type": "base64", "media_type": "content_type", "data": "base64_data"}}
167
+ - For errors: {"type": "error", "error": "error message"}
168
+
169
+ Examples:
170
+ # Read a markdown file
171
+ result = await read_file("docs/project-specs.md")
172
+
173
+ # Read an image
174
+ image_data = await read_file("assets/diagram.png")
175
+
176
+ # Read using memory URL
177
+ content = await read_file("memory://docs/architecture")
178
+ """
179
+ logger.info("Reading file", path=path)
180
+
181
+ url = memory_url_path(path)
182
+ response = await call_get(client, f"/resource/{url}")
183
+ content_type = response.headers.get("content-type", "application/octet-stream")
184
+ content_length = int(response.headers.get("content-length", 0))
185
+
186
+ logger.debug("Resource metadata", content_type=content_type, size=content_length, path=path)
187
+
188
+ # Handle text or json
189
+ if content_type.startswith("text/") or content_type == "application/json":
190
+ logger.debug("Processing text resource")
191
+ return {
192
+ "type": "text",
193
+ "text": response.text,
194
+ "content_type": content_type,
195
+ "encoding": "utf-8",
196
+ }
197
+
198
+ # Handle images
199
+ elif content_type.startswith("image/"):
200
+ logger.debug("Processing image")
201
+ img = PILImage.open(io.BytesIO(response.content))
202
+ img_bytes = optimize_image(img, content_length)
203
+
204
+ return {
205
+ "type": "image",
206
+ "source": {
207
+ "type": "base64",
208
+ "media_type": "image/jpeg",
209
+ "data": base64.b64encode(img_bytes).decode("utf-8"),
210
+ },
211
+ }
212
+
213
+ # Handle other file types
214
+ else:
215
+ logger.debug(f"Processing binary resource content_type {content_type}")
216
+ if content_length > 350000: # pragma: no cover
217
+ logger.warning("Document too large for response", size=content_length)
218
+ return {
219
+ "type": "error",
220
+ "error": f"Document size {content_length} bytes exceeds maximum allowed size",
221
+ }
222
+ return {
223
+ "type": "document",
224
+ "source": {
225
+ "type": "base64",
226
+ "media_type": content_type,
227
+ "data": base64.b64encode(response.content).decode("utf-8"),
228
+ },
229
+ }
@@ -0,0 +1,190 @@
1
+ """Read note tool for Basic Memory MCP server."""
2
+
3
+ from textwrap import dedent
4
+
5
+ from loguru import logger
6
+
7
+ from basic_memory.mcp.async_client import client
8
+ from basic_memory.mcp.server import mcp
9
+ from basic_memory.mcp.tools.search import search
10
+ from basic_memory.mcp.tools.utils import call_get
11
+ from basic_memory.schemas.memory import memory_url_path
12
+ from basic_memory.schemas.search import SearchQuery
13
+
14
+
15
+ @mcp.tool(
16
+ description="Read a markdown note by title or permalink.",
17
+ )
18
+ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
19
+ """Read a markdown note from the knowledge base.
20
+
21
+ This tool finds and retrieves a note by its title, permalink, or content search,
22
+ returning the raw markdown content including observations, relations, and metadata.
23
+ It will try multiple lookup strategies to find the most relevant note.
24
+
25
+ Args:
26
+ identifier: The title or permalink of the note to read
27
+ Can be a full memory:// URL, a permalink, a title, or search text
28
+ page: Page number for paginated results (default: 1)
29
+ page_size: Number of items per page (default: 10)
30
+
31
+ Returns:
32
+ The full markdown content of the note if found, or helpful guidance if not found.
33
+
34
+ Examples:
35
+ # Read by permalink
36
+ read_note("specs/search-spec")
37
+
38
+ # Read by title
39
+ read_note("Search Specification")
40
+
41
+ # Read with memory URL
42
+ read_note("memory://specs/search-spec")
43
+
44
+ # Read with pagination
45
+ read_note("Project Updates", page=2, page_size=5)
46
+ """
47
+ # Get the file via REST API - first try direct permalink lookup
48
+ entity_path = memory_url_path(identifier)
49
+ path = f"/resource/{entity_path}"
50
+ logger.info(f"Attempting to read note from URL: {path}")
51
+
52
+ try:
53
+ # Try direct lookup first
54
+ response = await call_get(client, path, params={"page": page, "page_size": page_size})
55
+
56
+ # If successful, return the content
57
+ if response.status_code == 200:
58
+ logger.info("Returning read_note result from resource: {path}", path=entity_path)
59
+ return response.text
60
+ except Exception as e: # pragma: no cover
61
+ logger.info(f"Direct lookup failed for '{path}': {e}")
62
+ # Continue to fallback methods
63
+
64
+ # Fallback 1: Try title search via API
65
+ logger.info(f"Search title for: {identifier}")
66
+ title_results = await search(SearchQuery(title=identifier))
67
+
68
+ if title_results and title_results.results:
69
+ result = title_results.results[0] # Get the first/best match
70
+ if result.permalink:
71
+ try:
72
+ # Try to fetch the content using the found permalink
73
+ path = f"/resource/{result.permalink}"
74
+ response = await call_get(
75
+ client, path, params={"page": page, "page_size": page_size}
76
+ )
77
+
78
+ if response.status_code == 200:
79
+ logger.info(f"Found note by title search: {result.permalink}")
80
+ return response.text
81
+ except Exception as e: # pragma: no cover
82
+ logger.info(
83
+ f"Failed to fetch content for found title match {result.permalink}: {e}"
84
+ )
85
+ else:
86
+ logger.info(f"No results in title search for: {identifier}")
87
+
88
+ # Fallback 2: Text search as a last resort
89
+ logger.info(f"Title search failed, trying text search for: {identifier}")
90
+ text_results = await search(SearchQuery(text=identifier))
91
+
92
+ # We didn't find a direct match, construct a helpful error message
93
+ if not text_results or not text_results.results:
94
+ # No results at all
95
+ return format_not_found_message(identifier)
96
+ else:
97
+ # We found some related results
98
+ return format_related_results(identifier, text_results.results[:5])
99
+
100
+
101
+ def format_not_found_message(identifier: str) -> str:
102
+ """Format a helpful message when no note was found."""
103
+ return dedent(f"""
104
+ # Note Not Found: "{identifier}"
105
+
106
+ I couldn't find any notes matching "{identifier}". Here are some suggestions:
107
+
108
+ ## Check Identifier Type
109
+ - If you provided a title, try using the exact permalink instead
110
+ - If you provided a permalink, check for typos or try a broader search
111
+
112
+ ## Search Instead
113
+ Try searching for related content:
114
+ ```
115
+ search(query="{identifier}")
116
+ ```
117
+
118
+ ## Recent Activity
119
+ Check recently modified notes:
120
+ ```
121
+ recent_activity(timeframe="7d")
122
+ ```
123
+
124
+ ## Create New Note
125
+ This might be a good opportunity to create a new note on this topic:
126
+ ```
127
+ write_note(
128
+ title="{identifier.capitalize()}",
129
+ content='''
130
+ # {identifier.capitalize()}
131
+
132
+ ## Overview
133
+ [Your content here]
134
+
135
+ ## Observations
136
+ - [category] [Observation about {identifier}]
137
+
138
+ ## Relations
139
+ - relates_to [[Related Topic]]
140
+ ''',
141
+ folder="notes"
142
+ )
143
+ ```
144
+ """)
145
+
146
+
147
+ def format_related_results(identifier: str, results) -> str:
148
+ """Format a helpful message with related results when an exact match wasn't found."""
149
+ message = dedent(f"""
150
+ # Note Not Found: "{identifier}"
151
+
152
+ I couldn't find an exact match for "{identifier}", but I found some related notes:
153
+
154
+ """)
155
+
156
+ for i, result in enumerate(results):
157
+ message += dedent(f"""
158
+ ## {i + 1}. {result.title}
159
+ - **Type**: {result.type.value}
160
+ - **Permalink**: {result.permalink}
161
+
162
+ You can read this note with:
163
+ ```
164
+ read_note("{result.permalink}")
165
+ ```
166
+
167
+ """)
168
+
169
+ message += dedent("""
170
+ ## Try More Specific Lookup
171
+ For exact matches, try using the full permalink from one of the results above.
172
+
173
+ ## Search For More Results
174
+ To see more related content:
175
+ ```
176
+ search(query="{identifier}")
177
+ ```
178
+
179
+ ## Create New Note
180
+ If none of these match what you're looking for, consider creating a new note:
181
+ ```
182
+ write_note(
183
+ title="[Your title]",
184
+ content="[Your content]",
185
+ folder="notes"
186
+ )
187
+ ```
188
+ """)
189
+
190
+ return message
@@ -0,0 +1,100 @@
1
+ """Recent activity tool for Basic Memory MCP server."""
2
+
3
+ from typing import Optional, List
4
+
5
+ from loguru import logger
6
+
7
+ from basic_memory.mcp.async_client import client
8
+ from basic_memory.mcp.server import mcp
9
+ from basic_memory.mcp.tools.utils import call_get
10
+ from basic_memory.schemas.base import TimeFrame
11
+ from basic_memory.schemas.memory import GraphContext
12
+ from basic_memory.schemas.search import SearchItemType
13
+
14
+
15
+ @mcp.tool(
16
+ description="""Get recent activity from across the knowledge base.
17
+
18
+ Timeframe supports natural language formats like:
19
+ - "2 days ago"
20
+ - "last week"
21
+ - "yesterday"
22
+ - "today"
23
+ - "3 weeks ago"
24
+ Or standard formats like "7d"
25
+ """,
26
+ )
27
+ async def recent_activity(
28
+ type: Optional[List[SearchItemType]] = None,
29
+ depth: Optional[int] = 1,
30
+ timeframe: Optional[TimeFrame] = "7d",
31
+ page: int = 1,
32
+ page_size: int = 10,
33
+ max_related: int = 10,
34
+ ) -> GraphContext:
35
+ """Get recent activity across the knowledge base.
36
+
37
+ Args:
38
+ type: Filter by content type(s). Valid options:
39
+ - ["entity"] for knowledge entities
40
+ - ["relation"] for connections between entities
41
+ - ["observation"] for notes and observations
42
+ Multiple types can be combined: ["entity", "relation"]
43
+ depth: How many relation hops to traverse (1-3 recommended)
44
+ timeframe: Time window to search. Supports natural language:
45
+ - Relative: "2 days ago", "last week", "yesterday"
46
+ - Points in time: "2024-01-01", "January 1st"
47
+ - Standard format: "7d", "24h"
48
+ page: Page number of results to return (default: 1)
49
+ page_size: Number of results to return per page (default: 10)
50
+ max_related: Maximum number of related results to return (default: 10)
51
+
52
+ Returns:
53
+ GraphContext containing:
54
+ - primary_results: Latest activities matching the filters
55
+ - related_results: Connected content via relations
56
+ - metadata: Query details and statistics
57
+
58
+ Examples:
59
+ # Get all entities for the last 10 days (default)
60
+ recent_activity()
61
+
62
+ # Get all entities from yesterday
63
+ recent_activity(type=["entity"], timeframe="yesterday")
64
+
65
+ # Get recent relations and observations
66
+ recent_activity(type=["relation", "observation"], timeframe="today")
67
+
68
+ # Look back further with more context
69
+ recent_activity(type=["entity"], depth=2, timeframe="2 weeks ago")
70
+
71
+ Notes:
72
+ - Higher depth values (>3) may impact performance with large result sets
73
+ - For focused queries, consider using build_context with a specific URI
74
+ - Max timeframe is 1 year in the past
75
+ """
76
+ logger.info(
77
+ f"Getting recent activity from type={type}, depth={depth}, timeframe={timeframe}, page={page}, page_size={page_size}, max_related={max_related}"
78
+ )
79
+ params = {
80
+ "page": page,
81
+ "page_size": page_size,
82
+ "max_related": max_related,
83
+ }
84
+ if depth:
85
+ params["depth"] = depth
86
+ if timeframe:
87
+ params["timeframe"] = timeframe # pyright: ignore
88
+
89
+ # send enum values if we have an enum, else send string value
90
+ if type:
91
+ params["type"] = [ # pyright: ignore
92
+ type.value if isinstance(type, SearchItemType) else type for type in type
93
+ ]
94
+
95
+ response = await call_get(
96
+ client,
97
+ "/memory/recent",
98
+ params=params,
99
+ )
100
+ return GraphContext.model_validate(response.json())
@@ -1,6 +1,5 @@
1
1
  """Search tools for Basic Memory MCP server."""
2
2
 
3
- import logfire
4
3
  from loguru import logger
5
4
 
6
5
  from basic_memory.mcp.server import mcp
@@ -15,24 +14,64 @@ from basic_memory.mcp.async_client import client
15
14
  async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
16
15
  """Search across all content in basic-memory.
17
16
 
17
+ This tool searches the knowledge base using full-text search, pattern matching,
18
+ or exact permalink lookup. It supports filtering by content type, entity type,
19
+ and date.
20
+
18
21
  Args:
19
22
  query: SearchQuery object with search parameters including:
20
- - text: Search text (required)
21
- - types: Optional list of content types to search ("document" or "entity")
22
- - entity_types: Optional list of entity types to filter by
23
- - after_date: Optional date filter for recent content
24
- page: the page number of results to return (default 1)
25
- page_size: the number of results to return per page (default 10)
23
+ - text: Full-text search (e.g., "project planning")
24
+ Supports boolean operators: AND, OR, NOT and parentheses for grouping
25
+ - title: Search only in titles (e.g., "Meeting notes")
26
+ - permalink: Exact permalink match (e.g., "docs/meeting-notes")
27
+ - permalink_match: Pattern matching for permalinks (e.g., "docs/*-notes")
28
+ - types: Optional list of content types to search (e.g., ["entity", "observation"])
29
+ - entity_types: Optional list of entity types to filter by (e.g., ["note", "person"])
30
+ - after_date: Optional date filter for recent content (e.g., "1 week", "2d")
31
+ page: The page number of results to return (default 1)
32
+ page_size: The number of results to return per page (default 10)
26
33
 
27
34
  Returns:
28
- SearchResponse with search results and metadata
35
+ SearchResponse with results and pagination info
36
+
37
+ Examples:
38
+ # Basic text search
39
+ results = await search(SearchQuery(text="project planning"))
40
+
41
+ # Boolean AND search (both terms must be present)
42
+ results = await search(SearchQuery(text="project AND planning"))
43
+
44
+ # Boolean OR search (either term can be present)
45
+ results = await search(SearchQuery(text="project OR meeting"))
46
+
47
+ # Boolean NOT search (exclude terms)
48
+ results = await search(SearchQuery(text="project NOT meeting"))
49
+
50
+ # Boolean search with grouping
51
+ results = await search(SearchQuery(text="(project OR planning) AND notes"))
52
+
53
+ # Search with type filter
54
+ results = await search(SearchQuery(
55
+ text="meeting notes",
56
+ types=["entity"],
57
+ ))
58
+
59
+ # Search for recent content
60
+ results = await search(SearchQuery(
61
+ text="bug report",
62
+ after_date="1 week"
63
+ ))
64
+
65
+ # Pattern matching on permalinks
66
+ results = await search(SearchQuery(
67
+ permalink_match="docs/meeting-*"
68
+ ))
29
69
  """
30
- with logfire.span("Searching for {query}", query=query): # pyright: ignore [reportGeneralTypeIssues]
31
- logger.info(f"Searching for {query}")
32
- response = await call_post(
33
- client,
34
- "/search/",
35
- json=query.model_dump(),
36
- params={"page": page, "page_size": page_size},
37
- )
38
- return SearchResponse.model_validate(response.json())
70
+ logger.info(f"Searching for {query}")
71
+ response = await call_post(
72
+ client,
73
+ "/search/",
74
+ json=query.model_dump(),
75
+ params={"page": page, "page_size": page_size},
76
+ )
77
+ return SearchResponse.model_validate(response.json())