basic-memory 0.13.7.dev1__py3-none-any.whl → 0.14.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 (30) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/api/routers/utils.py +1 -1
  3. basic_memory/cli/commands/db.py +8 -1
  4. basic_memory/cli/commands/mcp.py +1 -0
  5. basic_memory/cli/commands/project.py +3 -7
  6. basic_memory/config.py +6 -2
  7. basic_memory/db.py +5 -4
  8. basic_memory/markdown/utils.py +3 -1
  9. basic_memory/mcp/project_session.py +16 -1
  10. basic_memory/mcp/prompts/sync_status.py +0 -4
  11. basic_memory/mcp/server.py +0 -1
  12. basic_memory/mcp/tools/build_context.py +6 -3
  13. basic_memory/mcp/tools/move_note.py +155 -1
  14. basic_memory/mcp/tools/read_note.py +6 -3
  15. basic_memory/mcp/tools/search.py +115 -38
  16. basic_memory/mcp/tools/utils.py +27 -4
  17. basic_memory/mcp/tools/write_note.py +6 -2
  18. basic_memory/repository/entity_repository.py +46 -43
  19. basic_memory/repository/search_repository.py +153 -23
  20. basic_memory/schemas/memory.py +1 -1
  21. basic_memory/schemas/response.py +1 -1
  22. basic_memory/services/entity_service.py +10 -5
  23. basic_memory/services/initialization.py +11 -5
  24. basic_memory/services/project_service.py +18 -0
  25. basic_memory/services/sync_status_service.py +17 -0
  26. {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/METADATA +26 -1
  27. {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/RECORD +30 -30
  28. {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/WHEEL +0 -0
  29. {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/entry_points.txt +0 -0
  30. {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
3
  # Package version - updated by release automation
4
- __version__ = "0.13.6"
4
+ __version__ = "0.14.0"
5
5
 
6
6
  # API version for FastAPI - independent of package version
7
7
  __api_version__ = "v0"
@@ -52,7 +52,7 @@ async def to_graph_context(
52
52
  file_path=item.file_path,
53
53
  permalink=item.permalink, # pyright: ignore
54
54
  relation_type=item.relation_type, # pyright: ignore
55
- from_entity=from_entity.title, # pyright: ignore
55
+ from_entity=from_entity.title if from_entity else None,
56
56
  to_entity=to_entity.title if to_entity else None,
57
57
  created_at=item.created_at,
58
58
  )
@@ -1,13 +1,14 @@
1
1
  """Database management commands."""
2
2
 
3
3
  import asyncio
4
+ from pathlib import Path
4
5
 
5
6
  import typer
6
7
  from loguru import logger
7
8
 
8
9
  from basic_memory import db
9
10
  from basic_memory.cli.app import app
10
- from basic_memory.config import app_config
11
+ from basic_memory.config import app_config, config_manager
11
12
 
12
13
 
13
14
  @app.command()
@@ -25,6 +26,12 @@ def reset(
25
26
  db_path.unlink()
26
27
  logger.info(f"Database file deleted: {db_path}")
27
28
 
29
+ # Reset project configuration
30
+ config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
31
+ config_manager.config.default_project = "main"
32
+ config_manager.save_config(config_manager.config)
33
+ logger.info("Project configuration reset to default")
34
+
28
35
  # Create a new empty database
29
36
  asyncio.run(db.run_migrations(app_config))
30
37
  logger.info("Database reset complete")
@@ -85,4 +85,5 @@ def mcp(
85
85
  host=host,
86
86
  port=port,
87
87
  path=path,
88
+ log_level="INFO",
88
89
  )
@@ -120,7 +120,7 @@ def set_default_project(
120
120
  try:
121
121
  project_name = generate_permalink(name)
122
122
 
123
- response = asyncio.run(call_put(client, f"projects/{project_name}/default"))
123
+ response = asyncio.run(call_put(client, f"/projects/{project_name}/default"))
124
124
  result = ProjectStatusResponse.model_validate(response.json())
125
125
 
126
126
  console.print(f"[green]{result.message}[/green]")
@@ -128,12 +128,8 @@ def set_default_project(
128
128
  console.print(f"[red]Error setting default project: {str(e)}[/red]")
129
129
  raise typer.Exit(1)
130
130
 
131
- # Reload configuration to apply the change
132
- from importlib import reload
133
- from basic_memory import config as config_module
134
-
135
- reload(config_module)
136
-
131
+ # The API call above should have updated both config and MCP session
132
+ # No need for manual reload - the project service handles this automatically
137
133
  console.print("[green]Project activated for current session[/green]")
138
134
 
139
135
 
basic_memory/config.py CHANGED
@@ -45,7 +45,9 @@ class BasicMemoryConfig(BaseSettings):
45
45
  env: Environment = Field(default="dev", description="Environment name")
46
46
 
47
47
  projects: Dict[str, str] = Field(
48
- default_factory=lambda: {"main": str(Path.home() / "basic-memory")},
48
+ default_factory=lambda: {
49
+ "main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
50
+ },
49
51
  description="Mapping of project names to their filesystem paths",
50
52
  )
51
53
  default_project: str = Field(
@@ -92,7 +94,9 @@ class BasicMemoryConfig(BaseSettings):
92
94
  """Ensure configuration is valid after initialization."""
93
95
  # Ensure main project exists
94
96
  if "main" not in self.projects: # pragma: no cover
95
- self.projects["main"] = str(Path.home() / "basic-memory")
97
+ self.projects["main"] = str(
98
+ Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
99
+ )
96
100
 
97
101
  # Ensure default project is valid
98
102
  if self.default_project not in self.projects: # pragma: no cover
basic_memory/db.py CHANGED
@@ -95,11 +95,12 @@ async def get_or_create_db(
95
95
 
96
96
  if _engine is None:
97
97
  _engine, _session_maker = _create_engine_and_session(db_path, db_type)
98
-
98
+
99
99
  # Run migrations automatically unless explicitly disabled
100
100
  if ensure_migrations:
101
101
  if app_config is None:
102
102
  from basic_memory.config import app_config as global_app_config
103
+
103
104
  app_config = global_app_config
104
105
  await run_migrations(app_config, db_type)
105
106
 
@@ -170,12 +171,12 @@ async def run_migrations(
170
171
  ): # pragma: no cover
171
172
  """Run any pending alembic migrations."""
172
173
  global _migrations_completed
173
-
174
+
174
175
  # Skip if migrations already completed unless forced
175
176
  if _migrations_completed and not force:
176
177
  logger.debug("Migrations already completed in this session, skipping")
177
178
  return
178
-
179
+
179
180
  logger.info("Running database migrations...")
180
181
  try:
181
182
  # Get the absolute path to the alembic directory relative to this file
@@ -206,7 +207,7 @@ async def run_migrations(
206
207
  # initialize the search Index schema
207
208
  # the project_id is not used for init_search_index, so we pass a dummy value
208
209
  await SearchRepository(session_maker, 1).init_search_index()
209
-
210
+
210
211
  # Mark migrations as completed
211
212
  _migrations_completed = True
212
213
  except Exception as e: # pragma: no cover
@@ -38,7 +38,9 @@ def entity_model_from_markdown(
38
38
  # Update basic fields
39
39
  model.title = markdown.frontmatter.title
40
40
  model.entity_type = markdown.frontmatter.type
41
- model.permalink = markdown.frontmatter.permalink
41
+ # Only update permalink if it exists in frontmatter, otherwise preserve existing
42
+ if markdown.frontmatter.permalink is not None:
43
+ model.permalink = markdown.frontmatter.permalink
42
44
  model.file_path = str(file_path)
43
45
  model.content_type = "text/markdown"
44
46
  model.created_at = markdown.created
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
  from typing import Optional
9
9
  from loguru import logger
10
10
 
11
- from basic_memory.config import ProjectConfig, get_project_config
11
+ from basic_memory.config import ProjectConfig, get_project_config, config_manager
12
12
 
13
13
 
14
14
  @dataclass
@@ -64,6 +64,21 @@ class ProjectSession:
64
64
  self.current_project = self.default_project # pragma: no cover
65
65
  logger.info(f"Reset project context to default: {self.default_project}") # pragma: no cover
66
66
 
67
+ def refresh_from_config(self) -> None:
68
+ """Refresh session state from current configuration.
69
+
70
+ This method reloads the default project from config and reinitializes
71
+ the session. This should be called when the default project is changed
72
+ via CLI or API to ensure MCP session stays in sync.
73
+ """
74
+ # Reload config to get latest default project
75
+ current_config = config_manager.load_config()
76
+ new_default = current_config.default_project
77
+
78
+ # Reinitialize with new default
79
+ self.initialize(new_default)
80
+ logger.info(f"Refreshed project session from config, new default: {new_default}")
81
+
67
82
 
68
83
  # Global session instance
69
84
  session = ProjectSession()
@@ -12,10 +12,6 @@ from basic_memory.mcp.server import mcp
12
12
  )
13
13
  async def sync_status_prompt() -> str:
14
14
  """Get sync status with AI assistant guidance.
15
-
16
- This prompt provides detailed sync status information along with
17
- recommendations for how AI assistants should handle different sync states.
18
-
19
15
  Returns:
20
16
  Formatted sync status with AI assistant guidance
21
17
  """
@@ -105,6 +105,5 @@ auth_settings, auth_provider = create_auth_config()
105
105
  # Create the shared server instance
106
106
  mcp = FastMCP(
107
107
  name="Basic Memory",
108
- log_level="DEBUG",
109
108
  auth=auth_provider,
110
109
  )
@@ -82,10 +82,15 @@ async def build_context(
82
82
  logger.info(f"Building context from {url}")
83
83
  # URL is already validated and normalized by MemoryUrl type annotation
84
84
 
85
+ # Get the active project first to check project-specific sync status
86
+ active_project = get_active_project(project)
87
+
85
88
  # Check migration status and wait briefly if needed
86
89
  from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
87
90
 
88
- migration_status = await wait_for_migration_or_return_status(timeout=5.0)
91
+ migration_status = await wait_for_migration_or_return_status(
92
+ timeout=5.0, project_name=active_project.name
93
+ )
89
94
  if migration_status: # pragma: no cover
90
95
  # Return a proper GraphContext with status message
91
96
  from basic_memory.schemas.memory import MemoryMetadata
@@ -102,8 +107,6 @@ async def build_context(
102
107
  uri=migration_status, # Include status in metadata
103
108
  ),
104
109
  )
105
-
106
- active_project = get_active_project(project)
107
110
  project_url = active_project.project_url
108
111
 
109
112
  response = await call_get(
@@ -7,9 +7,155 @@ from loguru import logger
7
7
 
8
8
  from basic_memory.mcp.async_client import client
9
9
  from basic_memory.mcp.server import mcp
10
- from basic_memory.mcp.tools.utils import call_post
10
+ from basic_memory.mcp.tools.utils import call_post, call_get
11
11
  from basic_memory.mcp.project_session import get_active_project
12
12
  from basic_memory.schemas import EntityResponse
13
+ from basic_memory.schemas.project_info import ProjectList
14
+
15
+
16
+ async def _detect_cross_project_move_attempt(
17
+ identifier: str, destination_path: str, current_project: str
18
+ ) -> Optional[str]:
19
+ """Detect potential cross-project move attempts and return guidance.
20
+
21
+ Args:
22
+ identifier: The note identifier being moved
23
+ destination_path: The destination path
24
+ current_project: The current active project
25
+
26
+ Returns:
27
+ Error message with guidance if cross-project move is detected, None otherwise
28
+ """
29
+ try:
30
+ # Get list of all available projects to check against
31
+ response = await call_get(client, "/projects/projects")
32
+ project_list = ProjectList.model_validate(response.json())
33
+ project_names = [p.name.lower() for p in project_list.projects]
34
+
35
+ # Check if destination path contains any project names
36
+ dest_lower = destination_path.lower()
37
+ path_parts = dest_lower.split("/")
38
+
39
+ # Look for project names in the destination path
40
+ for part in path_parts:
41
+ if part in project_names and part != current_project.lower():
42
+ # Found a different project name in the path
43
+ matching_project = next(
44
+ p.name for p in project_list.projects if p.name.lower() == part
45
+ )
46
+ return _format_cross_project_error_response(
47
+ identifier, destination_path, current_project, matching_project
48
+ )
49
+
50
+ # Check if the destination path looks like it might be trying to reference another project
51
+ # (e.g., contains common project-like patterns)
52
+ if any(keyword in dest_lower for keyword in ["project", "workspace", "repo"]):
53
+ # This might be a cross-project attempt, but we can't be sure
54
+ # Return a general guidance message
55
+ available_projects = [
56
+ p.name for p in project_list.projects if p.name != current_project
57
+ ]
58
+ if available_projects:
59
+ return _format_potential_cross_project_guidance(
60
+ identifier, destination_path, current_project, available_projects
61
+ )
62
+
63
+ except Exception as e:
64
+ # If we can't detect, don't interfere with normal error handling
65
+ logger.debug(f"Could not check for cross-project move: {e}")
66
+ return None
67
+
68
+ return None
69
+
70
+
71
+ def _format_cross_project_error_response(
72
+ identifier: str, destination_path: str, current_project: str, target_project: str
73
+ ) -> str:
74
+ """Format error response for detected cross-project move attempts."""
75
+ return dedent(f"""
76
+ # Move Failed - Cross-Project Move Not Supported
77
+
78
+ Cannot move '{identifier}' to '{destination_path}' because it appears to reference a different project ('{target_project}').
79
+
80
+ **Current project:** {current_project}
81
+ **Target project:** {target_project}
82
+
83
+ ## Cross-project moves are not supported directly
84
+
85
+ Notes can only be moved within the same project. To move content between projects, use this workflow:
86
+
87
+ ### Recommended approach:
88
+ ```
89
+ # 1. Read the note content from current project
90
+ read_note("{identifier}")
91
+
92
+ # 2. Switch to the target project
93
+ switch_project("{target_project}")
94
+
95
+ # 3. Create the note in the target project
96
+ write_note("Note Title", "content from step 1", "target-folder")
97
+
98
+ # 4. Switch back to original project (optional)
99
+ switch_project("{current_project}")
100
+
101
+ # 5. Delete the original note if desired
102
+ delete_note("{identifier}")
103
+ ```
104
+
105
+ ### Alternative: Stay in current project
106
+ If you want to move the note within the **{current_project}** project only:
107
+ ```
108
+ move_note("{identifier}", "new-folder/new-name.md")
109
+ ```
110
+
111
+ ## Available projects:
112
+ Use `list_projects()` to see all available projects and `switch_project("project-name")` to change projects.
113
+ """).strip()
114
+
115
+
116
+ def _format_potential_cross_project_guidance(
117
+ identifier: str, destination_path: str, current_project: str, available_projects: list[str]
118
+ ) -> str:
119
+ """Format guidance for potentially cross-project moves."""
120
+ other_projects = ", ".join(available_projects[:3]) # Show first 3 projects
121
+ if len(available_projects) > 3:
122
+ other_projects += f" (and {len(available_projects) - 3} others)"
123
+
124
+ return dedent(f"""
125
+ # Move Failed - Check Project Context
126
+
127
+ Cannot move '{identifier}' to '{destination_path}' within the current project '{current_project}'.
128
+
129
+ ## If you intended to move within the current project:
130
+ The destination path should be relative to the project root:
131
+ ```
132
+ move_note("{identifier}", "folder/filename.md")
133
+ ```
134
+
135
+ ## If you intended to move to a different project:
136
+ Cross-project moves require switching projects first. Available projects: {other_projects}
137
+
138
+ ### To move to another project:
139
+ ```
140
+ # 1. Read the content
141
+ read_note("{identifier}")
142
+
143
+ # 2. Switch to target project
144
+ switch_project("target-project-name")
145
+
146
+ # 3. Create note in target project
147
+ write_note("Title", "content", "folder")
148
+
149
+ # 4. Switch back and delete original if desired
150
+ switch_project("{current_project}")
151
+ delete_note("{identifier}")
152
+ ```
153
+
154
+ ### To see all projects:
155
+ ```
156
+ list_projects()
157
+ ```
158
+ """).strip()
13
159
 
14
160
 
15
161
  def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
@@ -258,6 +404,14 @@ async def move_note(
258
404
  active_project = get_active_project(project)
259
405
  project_url = active_project.project_url
260
406
 
407
+ # Check for potential cross-project move attempts
408
+ cross_project_error = await _detect_cross_project_move_attempt(
409
+ identifier, destination_path, active_project.name
410
+ )
411
+ if cross_project_error:
412
+ logger.info(f"Detected cross-project move attempt: {identifier} -> {destination_path}")
413
+ return cross_project_error
414
+
261
415
  try:
262
416
  # Prepare move request
263
417
  move_data = {
@@ -52,14 +52,17 @@ async def read_note(
52
52
  read_note("Meeting Notes", project="work-project")
53
53
  """
54
54
 
55
+ # Get the active project first to check project-specific sync status
56
+ active_project = get_active_project(project)
57
+
55
58
  # Check migration status and wait briefly if needed
56
59
  from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
57
60
 
58
- migration_status = await wait_for_migration_or_return_status(timeout=5.0)
61
+ migration_status = await wait_for_migration_or_return_status(
62
+ timeout=5.0, project_name=active_project.name
63
+ )
59
64
  if migration_status: # pragma: no cover
60
65
  return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
61
-
62
- active_project = get_active_project(project)
63
66
  project_url = active_project.project_url
64
67
 
65
68
  # Get the file via REST API - first try direct permalink lookup
@@ -45,13 +45,18 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
45
45
  - Boolean OR: `meeting OR discussion`
46
46
  - Boolean NOT: `project NOT archived`
47
47
  - Grouped: `(project OR planning) AND notes`
48
+ - Exact phrases: `"weekly standup meeting"`
49
+ - Content-specific: `tag:example` or `category:observation`
48
50
 
49
51
  ## Try again with:
50
52
  ```
51
- search_notes("INSERT_CLEAN_QUERY_HERE")
53
+ search_notes("{clean_query}")
52
54
  ```
53
55
 
54
- Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
56
+ ## Alternative search strategies:
57
+ - Break into simpler terms: `search_notes("{' '.join(clean_query.split()[:2])}")`
58
+ - Try different search types: `search_notes("{clean_query}", search_type="title")`
59
+ - Use filtering: `search_notes("{clean_query}", types=["entity"])`
55
60
  """).strip()
56
61
 
57
62
  # Project not found errors (check before general "not found")
@@ -85,24 +90,39 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
85
90
 
86
91
  No content found matching '{query}' in the current project.
87
92
 
88
- ## Suggestions to try:
93
+ ## Search strategy suggestions:
89
94
  1. **Broaden your search**: Try fewer or more general terms
90
95
  - Instead of: `{query}`
91
96
  - Try: `{simplified_query}`
92
97
 
93
- 2. **Check spelling**: Verify terms are spelled correctly
94
- 3. **Try different search types**:
95
- - Text search: `search_notes("{query}", search_type="text")`
96
- - Title search: `search_notes("{query}", search_type="title")`
97
- - Permalink search: `search_notes("{query}", search_type="permalink")`
98
-
99
- 4. **Use boolean operators**:
100
- - Try OR search for broader results
101
-
102
- ## Check what content exists:
103
- - Recent activity: `recent_activity(timeframe="7d")`
104
- - List files: `list_directory("/")`
105
- - Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
98
+ 2. **Check spelling and try variations**:
99
+ - Verify terms are spelled correctly
100
+ - Try synonyms or related terms
101
+
102
+ 3. **Use different search approaches**:
103
+ - **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
104
+ - **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
105
+ - **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
106
+
107
+ 4. **Try boolean operators for broader results**:
108
+ - OR search: `search_notes("{' OR '.join(query.split()[:3])}")`
109
+ - Remove restrictive terms: Focus on the most important keywords
110
+
111
+ 5. **Use filtering to narrow scope**:
112
+ - By content type: `search_notes("{query}", types=["entity"])`
113
+ - By recent content: `search_notes("{query}", after_date="1 week")`
114
+ - By entity type: `search_notes("{query}", entity_types=["observation"])`
115
+
116
+ 6. **Try advanced search patterns**:
117
+ - Tag search: `search_notes("tag:your-tag")`
118
+ - Category search: `search_notes("category:observation")`
119
+ - Pattern matching: `search_notes("*{query}*", search_type="permalink")`
120
+
121
+ ## Explore what content exists:
122
+ - **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
123
+ - **List directories**: `list_directory("/")` - Browse all content
124
+ - **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
125
+ - **Check project**: `get_current_project()` - Verify you're in the right project
106
126
  """).strip()
107
127
 
108
128
  # Server/API errors
@@ -151,25 +171,36 @@ You don't have permission to search in the current project: {error_message}
151
171
 
152
172
  Error searching for '{query}': {error_message}
153
173
 
154
- ## General troubleshooting:
155
- 1. **Check your query**: Ensure it uses valid search syntax
156
- 2. **Try simpler terms**: Use basic words without special characters
174
+ ## Troubleshooting steps:
175
+ 1. **Simplify your query**: Try basic words without special characters
176
+ 2. **Check search syntax**: Ensure boolean operators are correctly formatted
157
177
  3. **Verify project access**: Make sure you can access the current project
158
- 4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
159
-
160
- ## Alternative approaches:
161
- - Browse files: `list_directory("/")`
162
- - Try different search type: `search_notes("{query}", search_type="title")`
163
- - Search with filters: `search_notes("{query}", types=["entity"])`
164
-
165
- ## Need help?
166
- - View recent changes: `recent_activity()`
167
- - List projects: `list_projects()`
168
- - Check current project: `get_current_project()`"""
178
+ 4. **Test with simple search**: Try `search_notes("test")` to verify search is working
179
+
180
+ ## Alternative search approaches:
181
+ - **Different search types**:
182
+ - Title only: `search_notes("{query}", search_type="title")`
183
+ - Permalink patterns: `search_notes("{query}*", search_type="permalink")`
184
+ - **With filters**: `search_notes("{query}", types=["entity"])`
185
+ - **Recent content**: `search_notes("{query}", after_date="1 week")`
186
+ - **Boolean variations**: `search_notes("{' OR '.join(query.split()[:2])}")`
187
+
188
+ ## Explore your content:
189
+ - **Browse files**: `list_directory("/")` - See all available content
190
+ - **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
191
+ - **Project info**: `get_current_project()` - Verify current project
192
+ - **All projects**: `list_projects()` - Switch to different project if needed
193
+
194
+ ## Search syntax reference:
195
+ - **Basic**: `keyword` or `multiple words`
196
+ - **Boolean**: `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`
197
+ - **Phrases**: `"exact phrase"`
198
+ - **Grouping**: `(term1 OR term2) AND term3`
199
+ - **Patterns**: `tag:example`, `category:observation`"""
169
200
 
170
201
 
171
202
  @mcp.tool(
172
- description="Search across all content in the knowledge base.",
203
+ description="Search across all content in the knowledge base with advanced syntax support.",
173
204
  )
174
205
  async def search_notes(
175
206
  query: str,
@@ -181,24 +212,60 @@ async def search_notes(
181
212
  after_date: Optional[str] = None,
182
213
  project: Optional[str] = None,
183
214
  ) -> SearchResponse | str:
184
- """Search across all content in the knowledge base.
215
+ """Search across all content in the knowledge base with comprehensive syntax support.
185
216
 
186
217
  This tool searches the knowledge base using full-text search, pattern matching,
187
218
  or exact permalink lookup. It supports filtering by content type, entity type,
188
- and date.
219
+ and date, with advanced boolean and phrase search capabilities.
220
+
221
+ ## Search Syntax Examples
222
+
223
+ ### Basic Searches
224
+ - `search_notes("keyword")` - Find any content containing "keyword"
225
+ - `search_notes("exact phrase")` - Search for exact phrase match
226
+
227
+ ### Advanced Boolean Searches
228
+ - `search_notes("term1 term2")` - Find content with both terms (implicit AND)
229
+ - `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
230
+ - `search_notes("term1 OR term2")` - Either term can be present
231
+ - `search_notes("term1 NOT term2")` - Include term1 but exclude term2
232
+ - `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
233
+
234
+ ### Content-Specific Searches
235
+ - `search_notes("tag:example")` - Search within specific tags (if supported by content)
236
+ - `search_notes("category:observation")` - Filter by observation categories
237
+ - `search_notes("author:username")` - Find content by author (if metadata available)
238
+
239
+ ### Search Type Examples
240
+ - `search_notes("Meeting", search_type="title")` - Search only in titles
241
+ - `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
242
+ - `search_notes("keyword", search_type="text")` - Full-text search (default)
243
+
244
+ ### Filtering Options
245
+ - `search_notes("query", types=["entity"])` - Search only entities
246
+ - `search_notes("query", types=["note", "person"])` - Multiple content types
247
+ - `search_notes("query", entity_types=["observation"])` - Filter by entity type
248
+ - `search_notes("query", after_date="2024-01-01")` - Recent content only
249
+ - `search_notes("query", after_date="1 week")` - Relative date filtering
250
+
251
+ ### Advanced Pattern Examples
252
+ - `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
253
+ - `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
254
+ - `search_notes("bug NOT fixed")` - Exclude resolved issues
255
+ - `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
189
256
 
190
257
  Args:
191
- query: The search query string
258
+ query: The search query string (supports boolean operators, phrases, patterns)
192
259
  page: The page number of results to return (default 1)
193
260
  page_size: The number of results to return per page (default 10)
194
261
  search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
195
262
  types: Optional list of note types to search (e.g., ["note", "person"])
196
263
  entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
197
- after_date: Optional date filter for recent content (e.g., "1 week", "2d")
264
+ after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
198
265
  project: Optional project name to search in. If not provided, uses current active project.
199
266
 
200
267
  Returns:
201
- SearchResponse with results and pagination info
268
+ SearchResponse with results and pagination info, or helpful error guidance if search fails
202
269
 
203
270
  Examples:
204
271
  # Basic text search
@@ -216,16 +283,19 @@ async def search_notes(
216
283
  # Boolean search with grouping
217
284
  results = await search_notes("(project OR planning) AND notes")
218
285
 
286
+ # Exact phrase search
287
+ results = await search_notes("\"weekly standup meeting\"")
288
+
219
289
  # Search with type filter
220
290
  results = await search_notes(
221
291
  query="meeting notes",
222
292
  types=["entity"],
223
293
  )
224
294
 
225
- # Search with entity type filter, e.g., note vs
295
+ # Search with entity type filter
226
296
  results = await search_notes(
227
297
  query="meeting notes",
228
- types=["entity"],
298
+ entity_types=["observation"],
229
299
  )
230
300
 
231
301
  # Search for recent content
@@ -242,6 +312,13 @@ async def search_notes(
242
312
 
243
313
  # Search in specific project
244
314
  results = await search_notes("meeting notes", project="work-project")
315
+
316
+ # Complex search with multiple filters
317
+ results = await search_notes(
318
+ query="(bug OR issue) AND NOT resolved",
319
+ types=["entity"],
320
+ after_date="2024-01-01"
321
+ )
245
322
  """
246
323
  # Create a SearchQuery object based on the parameters
247
324
  search_query = SearchQuery()