basic-memory 0.2.12__py3-none-any.whl → 0.16.1__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 (149) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +63 -31
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +165 -28
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +28 -67
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +219 -14
  23. basic_memory/api/routers/search_router.py +21 -13
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +52 -1
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +26 -7
  41. basic_memory/cli/commands/import_chatgpt.py +83 -0
  42. basic_memory/cli/commands/import_claude_conversations.py +86 -0
  43. basic_memory/cli/commands/import_claude_projects.py +85 -0
  44. basic_memory/cli/commands/import_memory_json.py +35 -92
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +47 -30
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +13 -6
  50. basic_memory/config.py +481 -22
  51. basic_memory/db.py +192 -32
  52. basic_memory/deps.py +252 -22
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -14
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +437 -59
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +188 -23
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +57 -3
  115. basic_memory/schemas/response.py +9 -1
  116. basic_memory/schemas/search.py +33 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +251 -106
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +595 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +50 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +172 -34
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1176 -96
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +388 -28
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -203
  140. basic_memory/mcp/tools/knowledge.py +0 -56
  141. basic_memory/mcp/tools/memory.py +0 -151
  142. basic_memory/mcp/tools/notes.py +0 -122
  143. basic_memory/schemas/discovery.py +0 -28
  144. basic_memory/sync/file_change_scanner.py +0 -158
  145. basic_memory/sync/utils.py +0 -34
  146. basic_memory-0.2.12.dist-info/METADATA +0 -291
  147. basic_memory-0.2.12.dist-info/RECORD +0 -78
  148. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  149. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,13 @@
1
1
  """Service for resolving markdown links to permalinks."""
2
2
 
3
- from typing import Optional, Tuple, List
3
+ from typing import Optional, Tuple
4
4
 
5
5
  from loguru import logger
6
6
 
7
- from basic_memory.repository.entity_repository import EntityRepository
8
- from basic_memory.repository.search_repository import SearchIndexRow
9
- from basic_memory.services.search_service import SearchService
10
7
  from basic_memory.models import Entity
8
+ from basic_memory.repository.entity_repository import EntityRepository
11
9
  from basic_memory.schemas.search import SearchQuery, SearchItemType
10
+ from basic_memory.services.search_service import SearchService
12
11
 
13
12
 
14
13
  class LinkResolver:
@@ -17,8 +16,9 @@ class LinkResolver:
17
16
  Uses a combination of exact matching and search-based resolution:
18
17
  1. Try exact permalink match (fastest)
19
18
  2. Try exact title match
20
- 3. Fall back to search for fuzzy matching
21
- 4. Generate new permalink if no match found
19
+ 3. Try exact file path match
20
+ 4. Try file path with .md extension (for folder/title patterns)
21
+ 5. Fall back to search for fuzzy matching
22
22
  """
23
23
 
24
24
  def __init__(self, entity_repository: EntityRepository, search_service: SearchService):
@@ -26,9 +26,17 @@ class LinkResolver:
26
26
  self.entity_repository = entity_repository
27
27
  self.search_service = search_service
28
28
 
29
- async def resolve_link(self, link_text: str, use_search: bool = True) -> Optional[Entity]:
30
- """Resolve a markdown link to a permalink."""
31
- logger.debug(f"Resolving link: {link_text}")
29
+ async def resolve_link(
30
+ self, link_text: str, use_search: bool = True, strict: bool = False
31
+ ) -> Optional[Entity]:
32
+ """Resolve a markdown link to a permalink.
33
+
34
+ Args:
35
+ link_text: The link text to resolve
36
+ use_search: Whether to use search-based fuzzy matching as fallback
37
+ strict: If True, only exact matches are allowed (no fuzzy search fallback)
38
+ """
39
+ logger.trace(f"Resolving link: {link_text}")
32
40
 
33
41
  # Clean link text and extract any alias
34
42
  clean_text, alias = self._normalize_link_text(link_text)
@@ -40,26 +48,47 @@ class LinkResolver:
40
48
  return entity
41
49
 
42
50
  # 2. Try exact title match
43
- entity = await self.entity_repository.get_by_title(clean_text)
44
- if entity:
51
+ found = await self.entity_repository.get_by_title(clean_text)
52
+ if found:
53
+ # Return first match if there are duplicates (consistent behavior)
54
+ entity = found[0]
45
55
  logger.debug(f"Found title match: {entity.title}")
46
56
  return entity
47
57
 
48
- if use_search:
49
- # 3. Fall back to search for fuzzy matching on title if specified
58
+ # 3. Try file path
59
+ found_path = await self.entity_repository.get_by_file_path(clean_text)
60
+ if found_path:
61
+ logger.debug(f"Found entity with path: {found_path.file_path}")
62
+ return found_path
63
+
64
+ # 4. Try file path with .md extension if not already present
65
+ if not clean_text.endswith(".md") and "/" in clean_text:
66
+ file_path_with_md = f"{clean_text}.md"
67
+ found_path_md = await self.entity_repository.get_by_file_path(file_path_with_md)
68
+ if found_path_md:
69
+ logger.debug(f"Found entity with path (with .md): {found_path_md.file_path}")
70
+ return found_path_md
71
+
72
+ # In strict mode, don't try fuzzy search - return None if no exact match found
73
+ if strict:
74
+ return None
75
+
76
+ # 5. Fall back to search for fuzzy matching (only if not in strict mode)
77
+ if use_search and "*" not in clean_text:
50
78
  results = await self.search_service.search(
51
- query=SearchQuery(title=clean_text, types=[SearchItemType.ENTITY]),
79
+ query=SearchQuery(text=clean_text, entity_types=[SearchItemType.ENTITY]),
52
80
  )
53
81
 
54
82
  if results:
55
83
  # Look for best match
56
- best_match = self._select_best_match(clean_text, results)
57
- logger.debug(
84
+ best_match = min(results, key=lambda x: x.score) # pyright: ignore
85
+ logger.trace(
58
86
  f"Selected best match from {len(results)} results: {best_match.permalink}"
59
87
  )
60
- return await self.entity_repository.get_by_permalink(best_match.permalink)
88
+ if best_match.permalink:
89
+ return await self.entity_repository.get_by_permalink(best_match.permalink)
61
90
 
62
- # if we couldn't find anything then return None
91
+ # if we couldn't find anything then return None
63
92
  return None
64
93
 
65
94
  def _normalize_link_text(self, link_text: str) -> Tuple[str, Optional[str]]:
@@ -84,43 +113,8 @@ class LinkResolver:
84
113
  text, alias = text.split("|", 1)
85
114
  text = text.strip()
86
115
  alias = alias.strip()
116
+ else:
117
+ # Strip whitespace from text even if no alias
118
+ text = text.strip()
87
119
 
88
120
  return text, alias
89
-
90
- def _select_best_match(self, search_text: str, results: List[SearchIndexRow]) -> Entity:
91
- """Select best match from search results.
92
-
93
- Uses multiple criteria:
94
- 1. Word matches in title field
95
- 2. Word matches in path
96
- 3. Overall search score
97
- """
98
- # Get search terms for matching
99
- terms = search_text.lower().split()
100
-
101
- # Score each result
102
- scored_results = []
103
- for result in results:
104
- # Start with base score (lower is better)
105
- score = result.score
106
- assert score is not None
107
-
108
- # Parse path components
109
- path_parts = result.permalink.lower().split("/")
110
- last_part = path_parts[-1] if path_parts else ""
111
-
112
- # Title word match boosts
113
- term_matches = [term for term in terms if term in last_part]
114
- if term_matches:
115
- score *= 0.5 # Boost for each matching term
116
-
117
- # Exact title match is best
118
- if last_part == search_text.lower():
119
- score *= 0.2
120
-
121
- scored_results.append((score, result))
122
-
123
- # Sort by score (lowest first) and return best
124
- scored_results.sort(key=lambda x: x[0], reverse=True)
125
-
126
- return scored_results[0][1]