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.
- basic_memory/__init__.py +1 -1
- basic_memory/api/routers/utils.py +1 -1
- basic_memory/cli/commands/db.py +8 -1
- basic_memory/cli/commands/mcp.py +1 -0
- basic_memory/cli/commands/project.py +3 -7
- basic_memory/config.py +6 -2
- basic_memory/db.py +5 -4
- basic_memory/markdown/utils.py +3 -1
- basic_memory/mcp/project_session.py +16 -1
- basic_memory/mcp/prompts/sync_status.py +0 -4
- basic_memory/mcp/server.py +0 -1
- basic_memory/mcp/tools/build_context.py +6 -3
- basic_memory/mcp/tools/move_note.py +155 -1
- basic_memory/mcp/tools/read_note.py +6 -3
- basic_memory/mcp/tools/search.py +115 -38
- basic_memory/mcp/tools/utils.py +27 -4
- basic_memory/mcp/tools/write_note.py +6 -2
- basic_memory/repository/entity_repository.py +46 -43
- basic_memory/repository/search_repository.py +153 -23
- basic_memory/schemas/memory.py +1 -1
- basic_memory/schemas/response.py +1 -1
- basic_memory/services/entity_service.py +10 -5
- basic_memory/services/initialization.py +11 -5
- basic_memory/services/project_service.py +18 -0
- basic_memory/services/sync_status_service.py +17 -0
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/METADATA +26 -1
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/RECORD +30 -30
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.7.dev1.dist-info → basic_memory-0.14.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
|
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
|
)
|
basic_memory/cli/commands/db.py
CHANGED
|
@@ -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")
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -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
|
-
#
|
|
132
|
-
|
|
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: {
|
|
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(
|
|
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
|
basic_memory/markdown/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
"""
|
basic_memory/mcp/server.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -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("
|
|
53
|
+
search_notes("{clean_query}")
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
|
|
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
|
-
##
|
|
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
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
##
|
|
155
|
-
1. **
|
|
156
|
-
2. **
|
|
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. **
|
|
159
|
-
|
|
160
|
-
## Alternative approaches:
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
295
|
+
# Search with entity type filter
|
|
226
296
|
results = await search_notes(
|
|
227
297
|
query="meeting notes",
|
|
228
|
-
|
|
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()
|