basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__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 (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +144 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,25 @@
1
+ import asyncio
2
+
3
+ from loguru import logger
4
+
5
+ from basic_memory.config import config as project_config
6
+ from basic_memory.sync import SyncService, WatchService
7
+
8
+
9
+ async def sync_and_watch(
10
+ sync_service: SyncService, watch_service: WatchService
11
+ ): # pragma: no cover
12
+ """Run sync and watch service."""
13
+
14
+ logger.info(f"Starting watch service to sync file changes in dir: {project_config.home}")
15
+ # full sync
16
+ await sync_service.sync(project_config.home)
17
+
18
+ # watch changes
19
+ await watch_service.run()
20
+
21
+
22
+ async def create_background_sync_task(
23
+ sync_service: SyncService, watch_service: WatchService
24
+ ): # pragma: no cover
25
+ return asyncio.create_task(sync_and_watch(sync_service, watch_service))
@@ -10,7 +10,7 @@ from typing import Dict, Optional, Set, Tuple
10
10
  from loguru import logger
11
11
  from sqlalchemy.exc import IntegrityError
12
12
 
13
- from basic_memory.config import ProjectConfig
13
+ from basic_memory.config import BasicMemoryConfig
14
14
  from basic_memory.file_utils import has_frontmatter
15
15
  from basic_memory.markdown import EntityParser
16
16
  from basic_memory.models import Entity
@@ -64,7 +64,7 @@ class SyncService:
64
64
 
65
65
  def __init__(
66
66
  self,
67
- config: ProjectConfig,
67
+ app_config: BasicMemoryConfig,
68
68
  entity_service: EntityService,
69
69
  entity_parser: EntityParser,
70
70
  entity_repository: EntityRepository,
@@ -72,7 +72,7 @@ class SyncService:
72
72
  search_service: SearchService,
73
73
  file_service: FileService,
74
74
  ):
75
- self.config = config
75
+ self.app_config = app_config
76
76
  self.entity_service = entity_service
77
77
  self.entity_parser = entity_parser
78
78
  self.entity_repository = entity_repository
@@ -133,7 +133,7 @@ class SyncService:
133
133
  """Scan directory for changes compared to database state."""
134
134
 
135
135
  db_paths = await self.get_db_file_state()
136
- logger.debug(f"Found {len(db_paths)} db paths")
136
+ logger.info(f"Scanning directory {directory}. Found {len(db_paths)} db paths")
137
137
 
138
138
  # Track potentially moved files by checksum
139
139
  scan_result = await self.scan_directory(directory)
@@ -173,6 +173,7 @@ class SyncService:
173
173
  # deleted
174
174
  else:
175
175
  report.deleted.add(db_path)
176
+ logger.info(f"Completed scan for directory {directory}, found {report.total} changes.")
176
177
  return report
177
178
 
178
179
  async def get_db_file_state(self) -> Dict[str, str]:
@@ -218,7 +219,7 @@ class SyncService:
218
219
  return entity, checksum
219
220
 
220
221
  except Exception as e: # pragma: no cover
221
- logger.exception("Failed to sync file", path=path, error=str(e))
222
+ logger.error(f"Failed to sync file: path={path}, error={str(e)}")
222
223
  return None, None
223
224
 
224
225
  async def sync_markdown_file(self, path: str, new: bool = True) -> Tuple[Optional[Entity], str]:
@@ -378,7 +379,7 @@ class SyncService:
378
379
  updates = {"file_path": new_path}
379
380
 
380
381
  # If configured, also update permalink to match new path
381
- if self.config.update_permalinks_on_move:
382
+ if self.app_config.update_permalinks_on_move and self.file_service.is_markdown(new_path):
382
383
  # generate new permalink value
383
384
  new_permalink = await self.entity_service.resolve_permalink(new_path)
384
385
 
@@ -426,7 +427,7 @@ class SyncService:
426
427
  logger.info("Resolving forward references", count=len(unresolved_relations))
427
428
 
428
429
  for relation in unresolved_relations:
429
- logger.debug(
430
+ logger.trace(
430
431
  "Attempting to resolve relation "
431
432
  f"relation_id={relation.id} "
432
433
  f"from_id={relation.from_id} "
@@ -494,7 +495,7 @@ class SyncService:
494
495
  result.files[rel_path] = checksum
495
496
  result.checksums[checksum] = rel_path
496
497
 
497
- logger.debug("Found file", path=rel_path, checksum=checksum)
498
+ logger.trace(f"Found file, path={rel_path}, checksum={checksum}")
498
499
 
499
500
  duration_ms = int((time.time() - start_time) * 1000)
500
501
  logger.debug(
@@ -504,4 +505,4 @@ class SyncService:
504
505
  f"duration_ms={duration_ms}"
505
506
  )
506
507
 
507
- return result
508
+ return result
@@ -1,21 +1,21 @@
1
1
  """Watch service for Basic Memory."""
2
2
 
3
+ import asyncio
3
4
  import os
5
+ from collections import defaultdict
4
6
  from datetime import datetime
5
7
  from pathlib import Path
6
8
  from typing import List, Optional, Set
7
9
 
8
- from basic_memory.config import ProjectConfig
9
- from basic_memory.services.file_service import FileService
10
- from basic_memory.sync.sync_service import SyncService
10
+ from basic_memory.config import BasicMemoryConfig, WATCH_STATUS_JSON
11
+ from basic_memory.models import Project
12
+ from basic_memory.repository import ProjectRepository
11
13
  from loguru import logger
12
14
  from pydantic import BaseModel
13
15
  from rich.console import Console
14
16
  from watchfiles import awatch
15
17
  from watchfiles.main import FileChange, Change
16
18
 
17
- WATCH_STATUS_JSON = "watch-status.json"
18
-
19
19
 
20
20
  class WatchEvent(BaseModel):
21
21
  timestamp: datetime
@@ -72,16 +72,14 @@ class WatchServiceState(BaseModel):
72
72
  class WatchService:
73
73
  def __init__(
74
74
  self,
75
- sync_service: SyncService,
76
- file_service: FileService,
77
- config: ProjectConfig,
75
+ app_config: BasicMemoryConfig,
76
+ project_repository: ProjectRepository,
78
77
  quiet: bool = False,
79
78
  ):
80
- self.sync_service = sync_service
81
- self.file_service = file_service
82
- self.config = config
79
+ self.app_config = app_config
80
+ self.project_repository = project_repository
83
81
  self.state = WatchServiceState()
84
- self.status_path = config.home / ".basic-memory" / WATCH_STATUS_JSON
82
+ self.status_path = Path.home() / ".basic-memory" / WATCH_STATUS_JSON
85
83
  self.status_path.parent.mkdir(parents=True, exist_ok=True)
86
84
 
87
85
  # quiet mode for mcp so it doesn't mess up stdout
@@ -89,10 +87,14 @@ class WatchService:
89
87
 
90
88
  async def run(self): # pragma: no cover
91
89
  """Watch for file changes and sync them"""
90
+
91
+ projects = await self.project_repository.get_active_projects()
92
+ project_paths = [project.path for project in projects]
93
+
92
94
  logger.info(
93
95
  "Watch service started",
94
- f"directory={str(self.config.home)}",
95
- f"debounce_ms={self.config.sync_delay}",
96
+ f"directories={project_paths}",
97
+ f"debounce_ms={self.app_config.sync_delay}",
96
98
  f"pid={os.getpid()}",
97
99
  )
98
100
 
@@ -102,15 +104,30 @@ class WatchService:
102
104
 
103
105
  try:
104
106
  async for changes in awatch(
105
- self.config.home,
106
- debounce=self.config.sync_delay,
107
+ *project_paths,
108
+ debounce=self.app_config.sync_delay,
107
109
  watch_filter=self.filter_changes,
108
110
  recursive=True,
109
111
  ):
110
- await self.handle_changes(self.config.home, changes)
112
+ # group changes by project
113
+ project_changes = defaultdict(list)
114
+ for change, path in changes:
115
+ for project in projects:
116
+ if self.is_project_path(project, path):
117
+ project_changes[project].append((change, path))
118
+ break
119
+
120
+ # create coroutines to handle changes
121
+ change_handlers = [
122
+ self.handle_changes(project, changes) # pyright: ignore
123
+ for project, changes in project_changes.items()
124
+ ]
125
+
126
+ # process changes
127
+ await asyncio.gather(*change_handlers)
111
128
 
112
129
  except Exception as e:
113
- logger.exception("Watch service error", error=str(e), directory=str(self.config.home))
130
+ logger.exception("Watch service error", error=str(e))
114
131
 
115
132
  self.state.record_error(str(e))
116
133
  await self.write_status()
@@ -119,7 +136,6 @@ class WatchService:
119
136
  finally:
120
137
  logger.info(
121
138
  "Watch service stopped",
122
- f"directory={str(self.config.home)}",
123
139
  f"runtime_seconds={int((datetime.now() - self.state.start_time).total_seconds())}",
124
140
  )
125
141
 
@@ -132,15 +148,9 @@ class WatchService:
132
148
  Returns:
133
149
  True if the file should be watched, False if it should be ignored
134
150
  """
135
- # Skip if path is invalid
136
- try:
137
- relative_path = Path(path).relative_to(self.config.home)
138
- except ValueError:
139
- # This is a defensive check for paths outside our home directory
140
- return False
141
151
 
142
152
  # Skip hidden directories and files
143
- path_parts = relative_path.parts
153
+ path_parts = Path(path).parts
144
154
  for part in path_parts:
145
155
  if part.startswith("."):
146
156
  return False
@@ -155,14 +165,30 @@ class WatchService:
155
165
  """Write current state to status file"""
156
166
  self.status_path.write_text(WatchServiceState.model_dump_json(self.state, indent=2))
157
167
 
158
- async def handle_changes(self, directory: Path, changes: Set[FileChange]):
168
+ def is_project_path(self, project: Project, path):
169
+ """
170
+ Checks if path is a subdirectory or file within a project
171
+ """
172
+ project_path = Path(project.path).resolve()
173
+ sub_path = Path(path).resolve()
174
+ return project_path in sub_path.parents
175
+
176
+ async def handle_changes(self, project: Project, changes: Set[FileChange]) -> None:
159
177
  """Process a batch of file changes"""
160
178
  import time
161
179
  from typing import List, Set
162
180
 
163
- start_time = time.time()
181
+ # Lazily initialize sync service for project changes
182
+ from basic_memory.cli.commands.sync import get_sync_service
183
+
184
+ sync_service = await get_sync_service(project)
185
+ file_service = sync_service.file_service
164
186
 
165
- logger.info(f"Processing file changes, change_count={len(changes)}, directory={directory}")
187
+ start_time = time.time()
188
+ directory = Path(project.path).resolve()
189
+ logger.info(
190
+ f"Processing project: {project.name} changes, change_count={len(changes)}, directory={directory}"
191
+ )
166
192
 
167
193
  # Group changes by type
168
194
  adds: List[str] = []
@@ -190,7 +216,7 @@ class WatchService:
190
216
 
191
217
  # because of our atomic writes on updates, an add may be an existing file
192
218
  for added_path in adds: # pragma: no cover TODO add test
193
- entity = await self.sync_service.entity_repository.get_by_file_path(added_path)
219
+ entity = await sync_service.entity_repository.get_by_file_path(added_path)
194
220
  if entity is not None:
195
221
  logger.debug(f"Existing file will be processed as modified, path={added_path}")
196
222
  adds.remove(added_path)
@@ -218,9 +244,7 @@ class WatchService:
218
244
  continue # pragma: no cover
219
245
 
220
246
  # Skip directories for deleted paths (based on entity type in db)
221
- deleted_entity = await self.sync_service.entity_repository.get_by_file_path(
222
- deleted_path
223
- )
247
+ deleted_entity = await sync_service.entity_repository.get_by_file_path(deleted_path)
224
248
  if deleted_entity is None:
225
249
  # If this was a directory, it wouldn't have an entity
226
250
  logger.debug("Skipping unknown path for move detection", path=deleted_path)
@@ -229,10 +253,10 @@ class WatchService:
229
253
  if added_path != deleted_path:
230
254
  # Compare checksums to detect moves
231
255
  try:
232
- added_checksum = await self.file_service.compute_checksum(added_path)
256
+ added_checksum = await file_service.compute_checksum(added_path)
233
257
 
234
258
  if deleted_entity and deleted_entity.checksum == added_checksum:
235
- await self.sync_service.handle_move(deleted_path, added_path)
259
+ await sync_service.handle_move(deleted_path, added_path)
236
260
  self.state.add_event(
237
261
  path=f"{deleted_path} -> {added_path}",
238
262
  action="moved",
@@ -261,7 +285,7 @@ class WatchService:
261
285
  for path in deletes:
262
286
  if path not in processed:
263
287
  logger.debug("Processing deleted file", path=path)
264
- await self.sync_service.handle_delete(path)
288
+ await sync_service.handle_delete(path)
265
289
  self.state.add_event(path=path, action="deleted", status="success")
266
290
  self.console.print(f"[red]✕[/red] {path}")
267
291
  logger.info(f"deleted: {path}")
@@ -281,7 +305,7 @@ class WatchService:
281
305
  continue # pragma: no cover
282
306
 
283
307
  logger.debug(f"Processing new file, path={path}")
284
- entity, checksum = await self.sync_service.sync_file(path, new=True)
308
+ entity, checksum = await sync_service.sync_file(path, new=True)
285
309
  if checksum:
286
310
  self.state.add_event(
287
311
  path=path, action="new", status="success", checksum=checksum
@@ -314,7 +338,7 @@ class WatchService:
314
338
  continue
315
339
 
316
340
  logger.debug(f"Processing modified file: path={path}")
317
- entity, checksum = await self.sync_service.sync_file(path, new=False)
341
+ entity, checksum = await sync_service.sync_file(path, new=False)
318
342
  self.state.add_event(
319
343
  path=path, action="modified", status="success", checksum=checksum
320
344
  )
@@ -335,7 +359,7 @@ class WatchService:
335
359
  repeat_count = 0
336
360
  modify_count += 1
337
361
 
338
- logger.debug(
362
+ logger.debug( # pragma: no cover
339
363
  "Modified file processed, "
340
364
  f"path={path} "
341
365
  f"entity_id={entity.id if entity else None} "
@@ -0,0 +1,110 @@
1
+ # Continuing conversation on: {{ topic }}
2
+
3
+ This is a memory retrieval session.
4
+
5
+ Please use the available basic-memory tools to gather relevant context before responding. Start by executing one of the suggested commands below to retrieve content.
6
+
7
+ > **Knowledge Capture Recommendation:** As you continue this conversation, actively look for opportunities to record new information, decisions, or insights that emerge. Use `write_note()` to document important context.
8
+
9
+ Here's what I found from previous conversations:
10
+
11
+ {{#if has_results}}
12
+ {{#each hierarchical_results}}
13
+ <memory>
14
+ --- memory://{{ primary_result.permalink }}
15
+
16
+ ## {{ primary_result.title }}
17
+ - **Type**: {{ primary_result.type }}
18
+ - **Created**: {{date primary_result.created_at "%Y-%m-%d %H:%M"}}
19
+
20
+ {{#if primary_result.content}}
21
+ **Excerpt**:
22
+ <excerpt>
23
+ {{ primary_result.content }}
24
+ </excerpt>
25
+ {{/if}}
26
+
27
+ {{#if observations}}
28
+ ## Observations
29
+ {{#each observations}}
30
+ <observation>
31
+ - [{{ category }}] {{ content }}
32
+ </observation>
33
+ {{/each}}
34
+ {{/if}}
35
+
36
+ You can read this document with: `read_note("{{ primary_result.permalink }}")`
37
+
38
+ {{#if related_results}}
39
+ ## Related Context
40
+
41
+ {{#each related_results}}
42
+ <related>
43
+ - type: **{{ type }}**
44
+ - title: {{ title }}
45
+
46
+ {{#if permalink}}
47
+ You can view this document with: `read_note("{{ permalink }}")`
48
+ {{else}}
49
+ You can view this file with: `read_file("{{ file_path }}")`
50
+ {{/if}}
51
+ </related>
52
+ {{/each}}
53
+ {{/if}}
54
+
55
+ </memory>
56
+ {{/each}}
57
+ {{else}}
58
+ The supplied query did not return any information specifically on this topic.
59
+
60
+ ## Opportunity to Capture New Knowledge!
61
+
62
+ This is an excellent chance to start documenting this topic:
63
+
64
+ ```python
65
+ await write_note(
66
+ title="{{ topic }}",
67
+ content=f'''
68
+ # {{ topic }}
69
+
70
+ ## Overview
71
+ [Summary of what we know about {{ topic }}]
72
+
73
+ ## Key Points
74
+ [Main aspects or components of {{ topic }}]
75
+
76
+ ## Observations
77
+ - [category] [First important observation about {{ topic }}]
78
+ - [category] [Second observation about {{ topic }}]
79
+
80
+ ## Relations
81
+ - relates_to [[Related Topic]]
82
+ - part_of [[Broader Context]]
83
+ '''
84
+ )
85
+ ```
86
+
87
+ ## Other Options
88
+
89
+ Please use the available basic-memory tools to gather relevant context before responding.
90
+ You can also:
91
+ - Try a different search term
92
+ - Check recent activity with `recent_activity(timeframe="1w")`
93
+ {{/if}}
94
+ ## Next Steps
95
+ <instructions>
96
+ You can:
97
+ - Explore more with: `search_notes("{{ topic }}")`
98
+ - See what's changed: `recent_activity(timeframe="{{default timeframe "7d"}}")`
99
+ - **Record new learnings or decisions from this conversation:** `write_note(folder="[Chose a folder]" title="[Create a meaningful title]", content="[Content with observations and relations]")`
100
+
101
+ ## Knowledge Capture Recommendation
102
+
103
+ As you continue this conversation, **actively look for opportunities to:**
104
+ 1. Record key information, decisions, or insights that emerge
105
+ 2. Link new knowledge to existing topics
106
+ 3. Suggest capturing important context when appropriate
107
+ 4. Create forward references to topics that might be created later
108
+
109
+ Remember that capturing knowledge during conversations is one of the most valuable aspects of Basic Memory.
110
+ </instructions>
@@ -0,0 +1,101 @@
1
+ # Search Results for: "{{ query }}"{{#if timeframe}} (after {{ timeframe }}){{/if}}
2
+
3
+ This is a memory search session.
4
+ Please use the available basic-memory tools to gather relevant context before responding.
5
+ I found {{ result_count }} result(s) that match your query.
6
+
7
+ {{#if has_results}}
8
+ Here are the most relevant results:
9
+
10
+ {{#each results}}
11
+ {{#if_cond (lt @index 5)}}
12
+ {{#dedent}}
13
+ ## {{math @index "+" 1}}. {{ title }}
14
+ - **Type**: {{ type.value }}
15
+ {{#if metadata.created_at}}
16
+ - **Created**: {{date metadata.created_at "%Y-%m-%d %H:%M"}}
17
+ {{/if}}
18
+ - **Relevance Score**: {{round score 2}}
19
+
20
+ {{#if content}}
21
+ - **Excerpt**:
22
+ {{ content }}
23
+ {{/if}}
24
+
25
+ {{#if permalink}}
26
+ You can view this content with: `read_note("{{ permalink }}")`
27
+ Or explore its context with: `build_context("memory://{{ permalink }}")`
28
+ {{else}}
29
+ You can view this file with: `read_file("{{ file_path }}")`
30
+ {{/if}}
31
+ {{/dedent}}
32
+ {{/if_cond}}
33
+ {{/each}}
34
+
35
+ ## Next Steps
36
+
37
+ You can:
38
+ - Refine your search: `search_notes("{{ query }} AND additional_term")`
39
+ - Exclude terms: `search_notes("{{ query }} NOT exclude_term")`
40
+ - View more results: `search_notes("{{ query }}", after_date=None)`
41
+ - Check recent activity: `recent_activity()`
42
+
43
+ ## Synthesize and Capture Knowledge
44
+
45
+ Consider creating a new note that synthesizes what you've learned:
46
+
47
+ ```python
48
+ await write_note(
49
+ title="Synthesis of {{capitalize query}} Information",
50
+ content='''
51
+ # Synthesis of {{capitalize query}} Information
52
+
53
+ ## Overview
54
+ [Synthesis of the search results and your conversation]
55
+
56
+ ## Key Insights
57
+ [Summary of main points learned from these results]
58
+
59
+ ## Observations
60
+ - [insight] [Important observation from search results]
61
+ - [connection] [How this connects to other topics]
62
+
63
+ ## Relations
64
+ - relates_to [[{{#if results.length}}{{#if results.0.title}}{{results.0.title}}{{else}}Related Topic{{/if}}{{else}}Related Topic{{/if}}]]
65
+ - extends [[Another Relevant Topic]]
66
+ '''
67
+ )
68
+ ```
69
+
70
+ Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
71
+ {{else}}
72
+ I couldn't find any results for this query.
73
+
74
+ ## Opportunity to Capture Knowledge!
75
+
76
+ This is an excellent opportunity to create new knowledge on this topic. Consider:
77
+
78
+ ```python
79
+ await write_note(
80
+ title="{{capitalize query}}",
81
+ content='''
82
+ # {{capitalize query}}
83
+
84
+ ## Overview
85
+ [Summary of what we've discussed about {{ query }}]
86
+
87
+ ## Observations
88
+ - [category] [First observation about {{ query }}]
89
+ - [category] [Second observation about {{ query }}]
90
+
91
+ ## Relations
92
+ - relates_to [[Other Relevant Topic]]
93
+ '''
94
+ )
95
+ ```
96
+
97
+ ## Other Suggestions
98
+ - Try a different search term
99
+ - Broaden your search criteria
100
+ - Check recent activity with `recent_activity(timeframe="1w")`
101
+ {{/if}}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-memory
3
- Version: 0.12.3
3
+ Version: 0.13.0b2
4
4
  Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
5
5
  Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
6
6
  Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
@@ -13,15 +13,19 @@ Requires-Dist: aiosqlite>=0.20.0
13
13
  Requires-Dist: alembic>=1.14.1
14
14
  Requires-Dist: dateparser>=1.2.0
15
15
  Requires-Dist: fastapi[standard]>=0.115.8
16
+ Requires-Dist: fastmcp>=2.3.4
16
17
  Requires-Dist: greenlet>=3.1.1
17
18
  Requires-Dist: icecream>=2.1.3
18
19
  Requires-Dist: loguru>=0.7.3
19
20
  Requires-Dist: markdown-it-py>=3.0.0
20
21
  Requires-Dist: mcp>=1.2.0
21
22
  Requires-Dist: pillow>=11.1.0
23
+ Requires-Dist: pybars3>=0.9.7
22
24
  Requires-Dist: pydantic-settings>=2.6.1
23
25
  Requires-Dist: pydantic[email,timezone]>=2.10.3
26
+ Requires-Dist: pyjwt>=2.10.1
24
27
  Requires-Dist: pyright>=1.1.390
28
+ Requires-Dist: python-dotenv>=1.1.0
25
29
  Requires-Dist: python-frontmatter>=1.1.0
26
30
  Requires-Dist: pyyaml>=6.0.1
27
31
  Requires-Dist: qasync>=0.27.1
@@ -410,6 +414,24 @@ See the [Documentation](https://memory.basicmachines.co/) for more info, includi
410
414
  - [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
411
415
  - [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
412
416
 
417
+ ## Installation Options
418
+
419
+ ### Stable Release
420
+ ```bash
421
+ pip install basic-memory
422
+ ```
423
+
424
+ ### Beta/Pre-releases
425
+ ```bash
426
+ pip install basic-memory --pre
427
+ ```
428
+
429
+ ### Development Builds
430
+ Development versions are automatically published on every commit to main with versions like `0.12.4.dev26+468a22f`:
431
+ ```bash
432
+ pip install basic-memory --pre --force-reinstall
433
+ ```
434
+
413
435
  ## License
414
436
 
415
437
  AGPL-3.0