basic-memory 0.8.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 (75) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/migrations.py +4 -9
  3. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
  4. basic_memory/api/app.py +9 -6
  5. basic_memory/api/routers/__init__.py +2 -1
  6. basic_memory/api/routers/knowledge_router.py +30 -4
  7. basic_memory/api/routers/memory_router.py +3 -2
  8. basic_memory/api/routers/project_info_router.py +275 -0
  9. basic_memory/api/routers/search_router.py +22 -4
  10. basic_memory/cli/app.py +54 -3
  11. basic_memory/cli/commands/__init__.py +15 -2
  12. basic_memory/cli/commands/db.py +9 -13
  13. basic_memory/cli/commands/import_chatgpt.py +26 -30
  14. basic_memory/cli/commands/import_claude_conversations.py +27 -29
  15. basic_memory/cli/commands/import_claude_projects.py +29 -31
  16. basic_memory/cli/commands/import_memory_json.py +26 -28
  17. basic_memory/cli/commands/mcp.py +7 -1
  18. basic_memory/cli/commands/project.py +119 -0
  19. basic_memory/cli/commands/project_info.py +167 -0
  20. basic_memory/cli/commands/status.py +7 -9
  21. basic_memory/cli/commands/sync.py +54 -9
  22. basic_memory/cli/commands/{tools.py → tool.py} +92 -19
  23. basic_memory/cli/main.py +40 -1
  24. basic_memory/config.py +155 -7
  25. basic_memory/db.py +19 -4
  26. basic_memory/deps.py +10 -3
  27. basic_memory/file_utils.py +32 -16
  28. basic_memory/markdown/utils.py +5 -0
  29. basic_memory/mcp/main.py +1 -2
  30. basic_memory/mcp/prompts/__init__.py +6 -2
  31. basic_memory/mcp/prompts/ai_assistant_guide.py +6 -8
  32. basic_memory/mcp/prompts/continue_conversation.py +65 -126
  33. basic_memory/mcp/prompts/recent_activity.py +55 -13
  34. basic_memory/mcp/prompts/search.py +72 -17
  35. basic_memory/mcp/prompts/utils.py +139 -82
  36. basic_memory/mcp/server.py +1 -1
  37. basic_memory/mcp/tools/__init__.py +11 -22
  38. basic_memory/mcp/tools/build_context.py +85 -0
  39. basic_memory/mcp/tools/canvas.py +17 -19
  40. basic_memory/mcp/tools/delete_note.py +28 -0
  41. basic_memory/mcp/tools/project_info.py +51 -0
  42. basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
  43. basic_memory/mcp/tools/read_note.py +190 -0
  44. basic_memory/mcp/tools/recent_activity.py +100 -0
  45. basic_memory/mcp/tools/search.py +56 -17
  46. basic_memory/mcp/tools/utils.py +245 -17
  47. basic_memory/mcp/tools/write_note.py +124 -0
  48. basic_memory/models/search.py +2 -1
  49. basic_memory/repository/entity_repository.py +3 -2
  50. basic_memory/repository/project_info_repository.py +9 -0
  51. basic_memory/repository/repository.py +23 -6
  52. basic_memory/repository/search_repository.py +33 -10
  53. basic_memory/schemas/__init__.py +12 -0
  54. basic_memory/schemas/memory.py +3 -2
  55. basic_memory/schemas/project_info.py +96 -0
  56. basic_memory/schemas/search.py +27 -32
  57. basic_memory/services/context_service.py +3 -3
  58. basic_memory/services/entity_service.py +8 -2
  59. basic_memory/services/file_service.py +105 -53
  60. basic_memory/services/link_resolver.py +5 -45
  61. basic_memory/services/search_service.py +45 -16
  62. basic_memory/sync/sync_service.py +274 -39
  63. basic_memory/sync/watch_service.py +160 -30
  64. basic_memory/utils.py +40 -40
  65. basic_memory-0.9.0.dist-info/METADATA +736 -0
  66. basic_memory-0.9.0.dist-info/RECORD +99 -0
  67. basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
  68. basic_memory/mcp/tools/knowledge.py +0 -68
  69. basic_memory/mcp/tools/memory.py +0 -177
  70. basic_memory/mcp/tools/notes.py +0 -201
  71. basic_memory-0.8.0.dist-info/METADATA +0 -379
  72. basic_memory-0.8.0.dist-info/RECORD +0 -91
  73. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
  74. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
  75. {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,13 @@
2
2
 
3
3
  import hashlib
4
4
  from pathlib import Path
5
- from typing import Dict, Any, Union
5
+ from typing import Any, Dict, Union
6
6
 
7
7
  import yaml
8
8
  from loguru import logger
9
9
 
10
+ from basic_memory.utils import FilePath
11
+
10
12
 
11
13
  class FileError(Exception):
12
14
  """Base exception for file operations."""
@@ -48,42 +50,47 @@ async def compute_checksum(content: Union[str, bytes]) -> str:
48
50
  raise FileError(f"Failed to compute checksum: {e}")
49
51
 
50
52
 
51
- async def ensure_directory(path: Path) -> None:
53
+ async def ensure_directory(path: FilePath) -> None:
52
54
  """
53
55
  Ensure directory exists, creating if necessary.
54
56
 
55
57
  Args:
56
- path: Directory path to ensure
58
+ path: Directory path to ensure (Path or string)
57
59
 
58
60
  Raises:
59
61
  FileWriteError: If directory creation fails
60
62
  """
61
63
  try:
62
- path.mkdir(parents=True, exist_ok=True)
64
+ # Convert string to Path if needed
65
+ path_obj = Path(path) if isinstance(path, str) else path
66
+ path_obj.mkdir(parents=True, exist_ok=True)
63
67
  except Exception as e: # pragma: no cover
64
- logger.error(f"Failed to create directory: {path}: {e}")
68
+ logger.error("Failed to create directory", path=str(path), error=str(e))
65
69
  raise FileWriteError(f"Failed to create directory {path}: {e}")
66
70
 
67
71
 
68
- async def write_file_atomic(path: Path, content: str) -> None:
72
+ async def write_file_atomic(path: FilePath, content: str) -> None:
69
73
  """
70
74
  Write file with atomic operation using temporary file.
71
75
 
72
76
  Args:
73
- path: Target file path
77
+ path: Target file path (Path or string)
74
78
  content: Content to write
75
79
 
76
80
  Raises:
77
81
  FileWriteError: If write operation fails
78
82
  """
79
- temp_path = path.with_suffix(".tmp")
83
+ # Convert string to Path if needed
84
+ path_obj = Path(path) if isinstance(path, str) else path
85
+ temp_path = path_obj.with_suffix(".tmp")
86
+
80
87
  try:
81
88
  temp_path.write_text(content)
82
- temp_path.replace(path)
83
- logger.debug(f"wrote file: {path}")
89
+ temp_path.replace(path_obj)
90
+ logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
84
91
  except Exception as e: # pragma: no cover
85
92
  temp_path.unlink(missing_ok=True)
86
- logger.error(f"Failed to write file: {path}: {e}")
93
+ logger.error("Failed to write file", path=str(path_obj), error=str(e))
87
94
  raise FileWriteError(f"Failed to write file {path}: {e}")
88
95
 
89
96
 
@@ -173,7 +180,7 @@ def remove_frontmatter(content: str) -> str:
173
180
  return parts[2].strip()
174
181
 
175
182
 
176
- async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
183
+ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
177
184
  """Update frontmatter fields in a file while preserving all content.
178
185
 
179
186
  Only modifies the frontmatter section, leaving all content untouched.
@@ -181,7 +188,7 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
181
188
  Returns checksum of updated file.
182
189
 
183
190
  Args:
184
- path: Path to markdown file
191
+ path: Path to markdown file (Path or string)
185
192
  updates: Dict of frontmatter fields to update
186
193
 
187
194
  Returns:
@@ -192,8 +199,11 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
192
199
  ParseError: If frontmatter parsing fails
193
200
  """
194
201
  try:
202
+ # Convert string to Path if needed
203
+ path_obj = Path(path) if isinstance(path, str) else path
204
+
195
205
  # Read current content
196
- content = path.read_text()
206
+ content = path_obj.read_text()
197
207
 
198
208
  # Parse current frontmatter
199
209
  current_fm = {}
@@ -208,9 +218,15 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
208
218
  yaml_fm = yaml.dump(new_fm, sort_keys=False)
209
219
  final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
210
220
 
211
- await write_file_atomic(path, final_content)
221
+ logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
222
+
223
+ await write_file_atomic(path_obj, final_content)
212
224
  return await compute_checksum(final_content)
213
225
 
214
226
  except Exception as e: # pragma: no cover
215
- logger.error(f"Failed to update frontmatter in {path}: {e}")
227
+ logger.error(
228
+ "Failed to update frontmatter",
229
+ path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
230
+ error=str(e),
231
+ )
216
232
  raise FileError(f"Failed to update frontmatter: {e}")
@@ -5,6 +5,7 @@ from typing import Optional, Any
5
5
 
6
6
  from frontmatter import Post
7
7
 
8
+ from basic_memory.file_utils import has_frontmatter, remove_frontmatter
8
9
  from basic_memory.markdown import EntityMarkdown
9
10
  from basic_memory.models import Entity, Observation as ObservationModel
10
11
  from basic_memory.utils import generate_permalink
@@ -78,6 +79,10 @@ async def schema_to_markdown(schema: Any) -> Post:
78
79
  content = schema.content or ""
79
80
  frontmatter_metadata = dict(schema.entity_metadata or {})
80
81
 
82
+ # if the content contains frontmatter, remove it and merge
83
+ if has_frontmatter(content):
84
+ content = remove_frontmatter(content)
85
+
81
86
  # Remove special fields for ordered frontmatter
82
87
  for field in ["type", "title", "permalink"]:
83
88
  frontmatter_metadata.pop(field, None)
basic_memory/mcp/main.py CHANGED
@@ -17,9 +17,8 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
17
17
  import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
18
18
 
19
19
 
20
-
21
20
  if __name__ == "__main__": # pragma: no cover
22
21
  home_dir = config.home
23
22
  logger.info("Starting Basic Memory MCP server")
24
23
  logger.info(f"Home directory: {home_dir}")
25
- mcp.run()
24
+ mcp.run()
@@ -10,6 +10,10 @@ from basic_memory.mcp.prompts import continue_conversation
10
10
  from basic_memory.mcp.prompts import recent_activity
11
11
  from basic_memory.mcp.prompts import search
12
12
  from basic_memory.mcp.prompts import ai_assistant_guide
13
- from basic_memory.mcp.prompts import json_canvas_spec
14
13
 
15
- __all__ = ["ai_assistant_guide", "continue_conversation", "json_canvas_spec", "recent_activity", "search"]
14
+ __all__ = [
15
+ "ai_assistant_guide",
16
+ "continue_conversation",
17
+ "recent_activity",
18
+ "search",
19
+ ]
@@ -1,6 +1,5 @@
1
1
  from pathlib import Path
2
2
 
3
- import logfire
4
3
  from loguru import logger
5
4
 
6
5
  from basic_memory.mcp.server import mcp
@@ -8,7 +7,7 @@ from basic_memory.mcp.server import mcp
8
7
 
9
8
  @mcp.resource(
10
9
  uri="memory://ai_assistant_guide",
11
- name="ai_assistant_guide",
10
+ name="ai assistant guide",
12
11
  description="Give an AI assistant guidance on how to use Basic Memory tools effectively",
13
12
  )
14
13
  def ai_assistant_guide() -> str:
@@ -20,9 +19,8 @@ def ai_assistant_guide() -> str:
20
19
  Returns:
21
20
  A focused guide on Basic Memory usage.
22
21
  """
23
- with logfire.span("Getting Basic Memory guide"): # pyright: ignore
24
- logger.info("Loading AI assistant guide resource")
25
- guide_doc = Path(__file__).parent.parent.parent.parent.parent / "data/ai_assistant_guide.md"
26
- content = guide_doc.read_text()
27
- logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
28
- return content
22
+ logger.info("Loading AI assistant guide resource")
23
+ guide_doc = Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
24
+ content = guide_doc.read_text()
25
+ logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
26
+ return content
@@ -5,22 +5,23 @@ providing context from previous interactions to maintain continuity.
5
5
  """
6
6
 
7
7
  from textwrap import dedent
8
- from typing import Optional, List, Annotated
8
+ from typing import Optional, Annotated
9
9
 
10
10
  from loguru import logger
11
- import logfire
12
11
  from pydantic import Field
13
12
 
13
+ from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
14
14
  from basic_memory.mcp.server import mcp
15
- from basic_memory.mcp.tools.memory import build_context, recent_activity
15
+ from basic_memory.mcp.tools.build_context import build_context
16
+ from basic_memory.mcp.tools.recent_activity import recent_activity
16
17
  from basic_memory.mcp.tools.search import search
17
18
  from basic_memory.schemas.base import TimeFrame
18
19
  from basic_memory.schemas.memory import GraphContext
19
- from basic_memory.schemas.search import SearchQuery
20
+ from basic_memory.schemas.search import SearchQuery, SearchItemType
20
21
 
21
22
 
22
23
  @mcp.prompt(
23
- name="continue_conversation",
24
+ name="Continue Conversation",
24
25
  description="Continue a previous conversation",
25
26
  )
26
27
  async def continue_conversation(
@@ -42,131 +43,69 @@ async def continue_conversation(
42
43
  Returns:
43
44
  Context from previous sessions on this topic
44
45
  """
45
- with logfire.span("Continuing session", topic=topic, timeframe=timeframe): # pyright: ignore
46
- logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
47
-
48
- # If topic provided, search for it
49
- if topic:
50
- search_results = await search(SearchQuery(text=topic, after_date=timeframe))
51
-
52
- # Build context from top results
53
- contexts = []
54
- for result in search_results.results[:3]:
55
- if hasattr(result, "permalink") and result.permalink:
56
- context = await build_context(f"memory://{result.permalink}")
57
- contexts.append(context)
58
-
59
- return format_continuation_context(topic, contexts, timeframe)
60
-
46
+ logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
47
+
48
+ # If topic provided, search for it
49
+ if topic:
50
+ search_results = await search(
51
+ SearchQuery(text=topic, after_date=timeframe, types=[SearchItemType.ENTITY])
52
+ )
53
+
54
+ # Build context from results
55
+ contexts = []
56
+ for result in search_results.results:
57
+ if hasattr(result, "permalink") and result.permalink:
58
+ context: GraphContext = await build_context(f"memory://{result.permalink}")
59
+ if context.primary_results:
60
+ contexts.append(
61
+ PromptContextItem(
62
+ primary_results=context.primary_results[:1], # pyright: ignore
63
+ related_results=context.related_results[:3], # pyright: ignore
64
+ )
65
+ )
66
+
67
+ # get context for the top 3 results
68
+ prompt_context = format_prompt_context(
69
+ PromptContext(topic=topic, timeframe=timeframe, results=contexts) # pyright: ignore
70
+ )
71
+
72
+ else:
61
73
  # If no topic, get recent activity
62
- recent = await recent_activity(timeframe=timeframe)
63
- return format_continuation_context("Recent Activity", [recent], timeframe)
64
-
65
-
66
- def format_continuation_context(
67
- topic: str, contexts: List[GraphContext], timeframe: TimeFrame | None
68
- ) -> str:
69
- """Format continuation context into a helpful summary.
70
-
71
- Args:
72
- topic: The topic or focus of continuation
73
- contexts: List of context graphs
74
- timeframe: How far back to look for activity
75
-
76
- Returns:
77
- Formatted continuation summary
78
- """
79
- if not contexts or all(not context.primary_results for context in contexts):
80
- return dedent(f"""
81
- # Continuing conversation on: {topic}
82
-
83
- This is a memory retrieval session.
84
- Please use the available basic-memory tools to gather relevant context before responding.
85
- Start by executing one of the suggested commands below to retrieve content.
86
-
87
- I couldn't find any recent work specifically on this topic.
88
-
89
- ## Suggestions
90
- - Try a different search term
91
- - Check recent activity with `recent_activity(timeframe="1w")`
92
- - Start a new topic with `write_note(...)`
93
- """)
94
-
95
- # Start building our summary with header
96
- summary = dedent(f"""
97
- # Continuing conversation on: {topic}
98
-
99
- This is a memory retrieval session.
100
- Please use the available basic-memory tools to gather relevant context before responding.
101
- Start by executing one of the suggested commands below to retrieve content.
102
-
103
- Here's what I found about the previous conversation:
104
- """)
105
-
106
- # Track what we've added to avoid duplicates
107
- added_permalinks = set()
108
- sections = []
109
-
110
- # Process each context
111
- for context in contexts:
112
- # Add primary results
113
- for primary in context.primary_results:
114
- if hasattr(primary, "permalink") and primary.permalink not in added_permalinks:
115
- added_permalinks.add(primary.permalink)
116
-
117
- section = dedent(f"""
118
- ## {primary.title}
119
- - **Type**: {primary.type}
120
- """)
121
-
122
- # Add creation date if available
123
- if hasattr(primary, "created_at"):
124
- section += f"- **Created**: {primary.created_at.strftime('%Y-%m-%d %H:%M')}\n"
125
-
126
- section += dedent(f"""
127
-
128
- You can read this document with: `read_note("{primary.permalink}")`
129
- """)
130
-
131
- # Add related documents if available
132
- related_by_type = {}
133
- if context.related_results:
134
- for related in context.related_results:
135
- if hasattr(related, "relation_type") and related.relation_type: # pyright: ignore
136
- if related.relation_type not in related_by_type: # pyright: ignore
137
- related_by_type[related.relation_type] = [] # pyright: ignore
138
- related_by_type[related.relation_type].append(related) # pyright: ignore
139
-
140
- if related_by_type:
141
- section += dedent("""
142
- ### Related Documents
143
- """)
144
- for rel_type, relations in related_by_type.items():
145
- display_type = rel_type.replace("_", " ").title()
146
- section += f"- **{display_type}**:\n"
147
- for rel in relations[:3]: # Limit to avoid overwhelming
148
- if hasattr(rel, "to_id") and rel.to_id:
149
- section += f" - `{rel.to_id}`\n"
150
-
151
- sections.append(section)
152
-
153
- # Add all sections
154
- summary += "\n".join(sections)
155
-
156
- # Add next steps
74
+ timeframe = timeframe or "7d"
75
+ recent: GraphContext = await recent_activity(
76
+ timeframe=timeframe, type=[SearchItemType.ENTITY]
77
+ )
78
+ prompt_context = format_prompt_context(
79
+ PromptContext(
80
+ topic=f"Recent Activity from ({timeframe})",
81
+ timeframe=timeframe,
82
+ results=[
83
+ PromptContextItem(
84
+ primary_results=recent.primary_results[:5], # pyright: ignore
85
+ related_results=recent.related_results[:2], # pyright: ignore
86
+ )
87
+ ],
88
+ )
89
+ )
90
+
91
+ # Add next steps with strong encouragement to write
157
92
  next_steps = dedent(f"""
158
93
  ## Next Steps
159
-
94
+
160
95
  You can:
161
96
  - Explore more with: `search({{"text": "{topic}"}})`
162
- - See what's changed: `recent_activity(timeframe="{timeframe}")`
97
+ - See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
98
+ - **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
99
+
100
+ ## Knowledge Capture Recommendation
101
+
102
+ As you continue this conversation, **actively look for opportunities to:**
103
+ 1. Record key information, decisions, or insights that emerge
104
+ 2. Link new knowledge to existing topics
105
+ 3. Suggest capturing important context when appropriate
106
+ 4. Create forward references to topics that might be created later
107
+
108
+ Remember that capturing knowledge during conversations is one of the most valuable aspects of Basic Memory.
163
109
  """)
164
110
 
165
- # Add specific exploration based on what we found
166
- if added_permalinks:
167
- first_permalink = next(iter(added_permalinks))
168
- next_steps += dedent(f"""
169
- - Continue the conversation: `build_context("memory://{first_permalink}")`
170
- """)
171
-
172
- return summary + next_steps
111
+ return prompt_context + next_steps
@@ -3,27 +3,27 @@
3
3
  These prompts help users see what has changed in their knowledge base recently.
4
4
  """
5
5
 
6
- from typing import Annotated, Optional
6
+ from typing import Annotated
7
7
 
8
8
  from loguru import logger
9
- import logfire
10
9
  from pydantic import Field
11
10
 
12
- from basic_memory.mcp.prompts.utils import format_context_summary
11
+ from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
13
12
  from basic_memory.mcp.server import mcp
14
- from basic_memory.mcp.tools.memory import recent_activity as recent_activity_tool
13
+ from basic_memory.mcp.tools.recent_activity import recent_activity
15
14
  from basic_memory.schemas.base import TimeFrame
15
+ from basic_memory.schemas.search import SearchItemType
16
16
 
17
17
 
18
18
  @mcp.prompt(
19
- name="recent_activity",
19
+ name="Share Recent Activity",
20
20
  description="Get recent activity from across the knowledge base",
21
21
  )
22
22
  async def recent_activity_prompt(
23
23
  timeframe: Annotated[
24
- Optional[TimeFrame],
24
+ TimeFrame,
25
25
  Field(description="How far back to look for activity (e.g. '1d', '1 week')"),
26
- ] = None,
26
+ ] = "7d",
27
27
  ) -> str:
28
28
  """Get recent activity from across the knowledge base.
29
29
 
@@ -36,11 +36,53 @@ async def recent_activity_prompt(
36
36
  Returns:
37
37
  Formatted summary of recent activity
38
38
  """
39
- with logfire.span("Getting recent activity", timeframe=timeframe): # pyright: ignore
40
- logger.info(f"Getting recent activity, timeframe: {timeframe}")
39
+ logger.info(f"Getting recent activity, timeframe: {timeframe}")
41
40
 
42
- results = await recent_activity_tool(timeframe=timeframe)
41
+ recent = await recent_activity(timeframe=timeframe, type=[SearchItemType.ENTITY])
43
42
 
44
- time_display = f" ({timeframe})" if timeframe else ""
45
- header = f"# Recent Activity{time_display}"
46
- return format_context_summary(header, results)
43
+ prompt_context = format_prompt_context(
44
+ PromptContext(
45
+ topic=f"Recent Activity from ({timeframe})",
46
+ timeframe=timeframe,
47
+ results=[
48
+ PromptContextItem(
49
+ primary_results=recent.primary_results[:5],
50
+ related_results=recent.related_results[:2],
51
+ )
52
+ ],
53
+ )
54
+ )
55
+
56
+ # Add suggestions for summarizing recent activity
57
+ capture_suggestions = f"""
58
+ ## Opportunity to Capture Activity Summary
59
+
60
+ Consider creating a summary note of recent activity:
61
+
62
+ ```python
63
+ await write_note(
64
+ title="Activity Summary {timeframe}",
65
+ content='''
66
+ # Activity Summary for {timeframe}
67
+
68
+ ## Overview
69
+ [Summary of key changes and developments over this period]
70
+
71
+ ## Key Updates
72
+ [List main updates and their significance]
73
+
74
+ ## Observations
75
+ - [trend] [Observation about patterns in recent activity]
76
+ - [insight] [Connection between different activities]
77
+
78
+ ## Relations
79
+ - summarizes [[{recent.primary_results[0].title if recent.primary_results else "Recent Topic"}]]
80
+ - relates_to [[Project Overview]]
81
+ '''
82
+ )
83
+ ```
84
+
85
+ Summarizing periodic activity helps create high-level insights and connections between topics.
86
+ """
87
+
88
+ return prompt_context + capture_suggestions
@@ -7,17 +7,16 @@ from textwrap import dedent
7
7
  from typing import Annotated, Optional
8
8
 
9
9
  from loguru import logger
10
- import logfire
11
10
  from pydantic import Field
12
11
 
13
12
  from basic_memory.mcp.server import mcp
14
13
  from basic_memory.mcp.tools.search import search as search_tool
15
- from basic_memory.schemas.search import SearchQuery, SearchResponse
16
14
  from basic_memory.schemas.base import TimeFrame
15
+ from basic_memory.schemas.search import SearchQuery, SearchResponse
17
16
 
18
17
 
19
18
  @mcp.prompt(
20
- name="search",
19
+ name="Search Knowledge Base",
21
20
  description="Search across all content in basic-memory",
22
21
  )
23
22
  async def search_prompt(
@@ -39,11 +38,10 @@ async def search_prompt(
39
38
  Returns:
40
39
  Formatted search results with context
41
40
  """
42
- with logfire.span("Searching knowledge base", query=query, timeframe=timeframe): # pyright: ignore
43
- logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
41
+ logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
44
42
 
45
- search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
46
- return format_search_results(query, search_results, timeframe)
43
+ search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
44
+ return format_search_results(query, search_results, timeframe)
47
45
 
48
46
 
49
47
  def format_search_results(
@@ -65,11 +63,33 @@ def format_search_results(
65
63
 
66
64
  I couldn't find any results for this query.
67
65
 
68
- ## Suggestions
66
+ ## Opportunity to Capture Knowledge!
67
+
68
+ This is an excellent opportunity to create new knowledge on this topic. Consider:
69
+
70
+ ```python
71
+ await write_note(
72
+ title="{query.capitalize()}",
73
+ content=f'''
74
+ # {query.capitalize()}
75
+
76
+ ## Overview
77
+ [Summary of what we've discussed about {query}]
78
+
79
+ ## Observations
80
+ - [category] [First observation about {query}]
81
+ - [category] [Second observation about {query}]
82
+
83
+ ## Relations
84
+ - relates_to [[Other Relevant Topic]]
85
+ '''
86
+ )
87
+ ```
88
+
89
+ ## Other Suggestions
69
90
  - Try a different search term
70
91
  - Broaden your search criteria
71
92
  - Check recent activity with `recent_activity(timeframe="1w")`
72
- - Create new content with `write_note(...)`
73
93
  """)
74
94
 
75
95
  # Start building our summary with header
@@ -88,32 +108,38 @@ def format_search_results(
88
108
  for i, result in enumerate(results.results[:5]): # Limit to top 5 results
89
109
  summary += dedent(f"""
90
110
  ## {i + 1}. {result.title}
91
- - **Type**: {result.type}
111
+ - **Type**: {result.type.value}
92
112
  """)
93
113
 
94
114
  # Add creation date if available in metadata
95
- if hasattr(result, "metadata") and result.metadata and "created_at" in result.metadata:
115
+ if result.metadata and "created_at" in result.metadata:
96
116
  created_at = result.metadata["created_at"]
97
117
  if hasattr(created_at, "strftime"):
98
- summary += f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n"
118
+ summary += (
119
+ f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n" # pragma: no cover
120
+ )
99
121
  elif isinstance(created_at, str):
100
122
  summary += f"- **Created**: {created_at}\n"
101
123
 
102
124
  # Add score and excerpt
103
125
  summary += f"- **Relevance Score**: {result.score:.2f}\n"
126
+
104
127
  # Add excerpt if available in metadata
105
- if hasattr(result, "metadata") and result.metadata and "excerpt" in result.metadata:
106
- summary += f"- **Excerpt**: {result.metadata['excerpt']}\n"
128
+ if result.content:
129
+ summary += f"- **Excerpt**:\n{result.content}\n"
107
130
 
108
131
  # Add permalink for retrieving content
109
- if hasattr(result, "permalink") and result.permalink:
132
+ if result.permalink:
110
133
  summary += dedent(f"""
111
-
112
134
  You can view this content with: `read_note("{result.permalink}")`
113
135
  Or explore its context with: `build_context("memory://{result.permalink}")`
114
136
  """)
137
+ else:
138
+ summary += dedent(f"""
139
+ You can view this file with: `read_file("{result.file_path}")`
140
+ """) # pragma: no cover
115
141
 
116
- # Add next steps
142
+ # Add next steps with strong write encouragement
117
143
  summary += dedent(f"""
118
144
  ## Next Steps
119
145
 
@@ -122,6 +148,35 @@ def format_search_results(
122
148
  - Exclude terms: `search("{query} NOT exclude_term")`
123
149
  - View more results: `search("{query}", after_date=None)`
124
150
  - Check recent activity: `recent_activity()`
151
+
152
+ ## Synthesize and Capture Knowledge
153
+
154
+ Consider creating a new note that synthesizes what you've learned:
155
+
156
+ ```python
157
+ await write_note(
158
+ title="Synthesis of {query.capitalize()} Information",
159
+ content='''
160
+ # Synthesis of {query.capitalize()} Information
161
+
162
+ ## Overview
163
+ [Synthesis of the search results and your conversation]
164
+
165
+ ## Key Insights
166
+ [Summary of main points learned from these results]
167
+
168
+ ## Observations
169
+ - [insight] [Important observation from search results]
170
+ - [connection] [How this connects to other topics]
171
+
172
+ ## Relations
173
+ - relates_to [[{results.results[0].title if results.results else "Related Topic"}]]
174
+ - extends [[Another Relevant Topic]]
175
+ '''
176
+ )
177
+ ```
178
+
179
+ Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
125
180
  """)
126
181
 
127
182
  return summary