basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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 (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,170 +0,0 @@
1
- """Discussion context tools for Basic Memory MCP server."""
2
-
3
- from typing import Optional, Literal, List
4
-
5
- from loguru import logger
6
- import logfire
7
-
8
- from basic_memory.mcp.async_client import client
9
- from basic_memory.mcp.server import mcp
10
- from basic_memory.mcp.tools.utils import call_get
11
- from basic_memory.schemas.memory import (
12
- GraphContext,
13
- MemoryUrl,
14
- memory_url_path,
15
- normalize_memory_url,
16
- )
17
- from basic_memory.schemas.base import TimeFrame
18
-
19
-
20
- @mcp.tool(
21
- description="""Build context from a memory:// URI to continue conversations naturally.
22
-
23
- Use this to follow up on previous discussions or explore related topics.
24
- Timeframes support natural language like:
25
- - "2 days ago"
26
- - "last week"
27
- - "today"
28
- - "3 months ago"
29
- Or standard formats like "7d", "24h"
30
- """,
31
- )
32
- async def build_context(
33
- url: MemoryUrl,
34
- depth: Optional[int] = 1,
35
- timeframe: Optional[TimeFrame] = "7d",
36
- page: int = 1,
37
- page_size: int = 10,
38
- max_related: int = 10,
39
- ) -> GraphContext:
40
- """Get context needed to continue a discussion.
41
-
42
- This tool enables natural continuation of discussions by loading relevant context
43
- from memory:// URIs. It uses pattern matching to find relevant content and builds
44
- a rich context graph of related information.
45
-
46
- Args:
47
- url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
48
- depth: How many relation hops to traverse (1-3 recommended for performance)
49
- timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
50
- page: Page number of results to return (default: 1)
51
- page_size: Number of results to return per page (default: 10)
52
- max_related: Maximum number of related results to return (default: 10)
53
-
54
- Returns:
55
- GraphContext containing:
56
- - primary_results: Content matching the memory:// URI
57
- - related_results: Connected content via relations
58
- - metadata: Context building details
59
-
60
- Examples:
61
- # Continue a specific discussion
62
- build_context("memory://specs/search")
63
-
64
- # Get deeper context about a component
65
- build_context("memory://components/memory-service", depth=2)
66
-
67
- # Look at recent changes to a specification
68
- build_context("memory://specs/document-format", timeframe="today")
69
-
70
- # Research the history of a feature
71
- build_context("memory://features/knowledge-graph", timeframe="3 months ago")
72
- """
73
- with logfire.span("Building context", url=url, depth=depth, timeframe=timeframe): # pyright: ignore [reportGeneralTypeIssues]
74
- logger.info(f"Building context from {url}")
75
- url = normalize_memory_url(url)
76
- response = await call_get(
77
- client,
78
- f"/memory/{memory_url_path(url)}",
79
- params={
80
- "depth": depth,
81
- "timeframe": timeframe,
82
- "page": page,
83
- "page_size": page_size,
84
- "max_related": max_related,
85
- },
86
- )
87
- return GraphContext.model_validate(response.json())
88
-
89
-
90
- @mcp.tool(
91
- description="""Get recent activity from across the knowledge base.
92
-
93
- Timeframe supports natural language formats like:
94
- - "2 days ago"
95
- - "last week"
96
- - "yesterday"
97
- - "today"
98
- - "3 weeks ago"
99
- Or standard formats like "7d"
100
- """,
101
- )
102
- async def recent_activity(
103
- type: List[Literal["entity", "observation", "relation"]] = [],
104
- depth: Optional[int] = 1,
105
- timeframe: Optional[TimeFrame] = "7d",
106
- page: int = 1,
107
- page_size: int = 10,
108
- max_related: int = 10,
109
- ) -> GraphContext:
110
- """Get recent activity across the knowledge base.
111
-
112
- Args:
113
- type: Filter by content type(s). Valid options:
114
- - ["entity"] for knowledge entities
115
- - ["relation"] for connections between entities
116
- - ["observation"] for notes and observations
117
- Multiple types can be combined: ["entity", "relation"]
118
- depth: How many relation hops to traverse (1-3 recommended)
119
- timeframe: Time window to search. Supports natural language:
120
- - Relative: "2 days ago", "last week", "yesterday"
121
- - Points in time: "2024-01-01", "January 1st"
122
- - Standard format: "7d", "24h"
123
- page: Page number of results to return (default: 1)
124
- page_size: Number of results to return per page (default: 10)
125
- max_related: Maximum number of related results to return (default: 10)
126
-
127
- Returns:
128
- GraphContext containing:
129
- - primary_results: Latest activities matching the filters
130
- - related_results: Connected content via relations
131
- - metadata: Query details and statistics
132
-
133
- Examples:
134
- # Get all entities for the last 10 days (default)
135
- recent_activity()
136
-
137
- # Get all entities from yesterday
138
- recent_activity(type=["entity"], timeframe="yesterday")
139
-
140
- # Get recent relations and observations
141
- recent_activity(type=["relation", "observation"], timeframe="today")
142
-
143
- # Look back further with more context
144
- recent_activity(type=["entity"], depth=2, timeframe="2 weeks ago")
145
-
146
- Notes:
147
- - Higher depth values (>3) may impact performance with large result sets
148
- - For focused queries, consider using build_context with a specific URI
149
- - Max timeframe is 1 year in the past
150
- """
151
- with logfire.span("Getting recent activity", type=type, depth=depth, timeframe=timeframe): # pyright: ignore [reportGeneralTypeIssues]
152
- logger.info(
153
- f"Getting recent activity from {type}, depth={depth}, timeframe={timeframe}, page={page}, page_size={page_size}, max_related={max_related}"
154
- )
155
- params = {
156
- "depth": depth,
157
- "timeframe": timeframe,
158
- "page": page,
159
- "page_size": page_size,
160
- "max_related": max_related,
161
- }
162
- if type:
163
- params["type"] = type
164
-
165
- response = await call_get(
166
- client,
167
- "/memory/recent",
168
- params=params,
169
- )
170
- return GraphContext.model_validate(response.json())
@@ -1,202 +0,0 @@
1
- """Note management tools for Basic Memory MCP server.
2
-
3
- These tools provide a natural interface for working with markdown notes
4
- while leveraging the underlying knowledge graph structure.
5
- """
6
-
7
- from typing import Optional, List
8
-
9
- from loguru import logger
10
- import logfire
11
-
12
- from basic_memory.mcp.server import mcp
13
- from basic_memory.mcp.async_client import client
14
- from basic_memory.schemas import EntityResponse, DeleteEntitiesResponse
15
- from basic_memory.schemas.base import Entity
16
- from basic_memory.mcp.tools.utils import call_get, call_put, call_delete
17
- from basic_memory.schemas.memory import memory_url_path
18
-
19
-
20
- @mcp.tool(
21
- description="Create or update a markdown note. Returns a markdown formatted summary of the semantic content.",
22
- )
23
- async def write_note(
24
- title: str,
25
- content: str,
26
- folder: str,
27
- tags: Optional[List[str]] = None,
28
- ) -> str:
29
- """Write a markdown note to the knowledge base.
30
-
31
- The content can include semantic observations and relations using markdown syntax.
32
- Relations can be specified either explicitly or through inline wiki-style links:
33
-
34
- Observations format:
35
- `- [category] Observation text #tag1 #tag2 (optional context)`
36
-
37
- Examples:
38
- `- [design] Files are the source of truth #architecture (All state comes from files)`
39
- `- [tech] Using SQLite for storage #implementation`
40
- `- [note] Need to add error handling #todo`
41
-
42
- Relations format:
43
- - Explicit: `- relation_type [[Entity]] (optional context)`
44
- - Inline: Any `[[Entity]]` reference creates a relation
45
-
46
- Examples:
47
- `- depends_on [[Content Parser]] (Need for semantic extraction)`
48
- `- implements [[Search Spec]] (Initial implementation)`
49
- `- This feature extends [[Base Design]] and uses [[Core Utils]]`
50
-
51
- Args:
52
- title: The title of the note
53
- content: Markdown content for the note, can include observations and relations
54
- folder: the folder where the file should be saved
55
- tags: Optional list of tags to categorize the note
56
-
57
- Returns:
58
- A markdown formatted summary of the semantic content, including:
59
- - Creation/update status
60
- - File path and checksum
61
- - Observation counts by category
62
- - Relation counts (resolved/unresolved)
63
- - Tags if present
64
- """
65
- with logfire.span("Writing note", title=title, folder=folder): # pyright: ignore [reportGeneralTypeIssues]
66
- logger.info(f"Writing note folder:'{folder}' title: '{title}'")
67
-
68
- # Create the entity request
69
- metadata = {"tags": [f"#{tag}" for tag in tags]} if tags else None
70
- entity = Entity(
71
- title=title,
72
- folder=folder,
73
- entity_type="note",
74
- content_type="text/markdown",
75
- content=content,
76
- entity_metadata=metadata,
77
- )
78
-
79
- # Create or update via knowledge API
80
- logger.info(f"Creating {entity.permalink}")
81
- url = f"/knowledge/entities/{entity.permalink}"
82
- response = await call_put(client, url, json=entity.model_dump())
83
- result = EntityResponse.model_validate(response.json())
84
-
85
- # Format semantic summary based on status code
86
- action = "Created" if response.status_code == 201 else "Updated"
87
- assert result.checksum is not None
88
- summary = [
89
- f"# {action} {result.file_path} ({result.checksum[:8]})",
90
- f"permalink: {result.permalink}",
91
- ]
92
-
93
- if result.observations:
94
- categories = {}
95
- for obs in result.observations:
96
- categories[obs.category] = categories.get(obs.category, 0) + 1
97
-
98
- summary.append("\n## Observations")
99
- for category, count in sorted(categories.items()):
100
- summary.append(f"- {category}: {count}")
101
-
102
- if result.relations:
103
- unresolved = sum(1 for r in result.relations if not r.to_id)
104
- resolved = len(result.relations) - unresolved
105
-
106
- summary.append("\n## Relations")
107
- summary.append(f"- Resolved: {resolved}")
108
- if unresolved:
109
- summary.append(f"- Unresolved: {unresolved}")
110
- summary.append("\nUnresolved relations will be retried on next sync.")
111
-
112
- if tags:
113
- summary.append(f"\n## Tags\n- {', '.join(tags)}")
114
-
115
- return "\n".join(summary)
116
-
117
-
118
- @mcp.tool(description="Read note content by title, permalink, relation, or pattern")
119
- async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
120
- """Get note content in unified diff format.
121
-
122
- The content is returned in a unified diff inspired format:
123
- ```
124
- --- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
125
- <document content>
126
- ```
127
-
128
- Multiple documents (from relations or pattern matches) are separated by
129
- additional headers.
130
-
131
- Args:
132
- identifier: Can be one of:
133
- - Note title ("Project Planning")
134
- - Note permalink ("docs/example")
135
- - Relation path ("docs/example/depends-on/other-doc")
136
- - Pattern match ("docs/*-architecture")
137
- page: the page number of results to return (default 1)
138
- page_size: the number of results to return per page (default 10)
139
-
140
- Returns:
141
- Document content in unified diff format. For single documents, returns
142
- just that document's content. For relations or pattern matches, returns
143
- multiple documents separated by unified diff headers.
144
-
145
- Examples:
146
- # Single document
147
- content = await read_note("Project Planning")
148
-
149
- # Read by permalink
150
- content = await read_note("docs/architecture/file-first")
151
-
152
- # Follow relation
153
- content = await read_note("docs/architecture/depends-on/docs/content-parser")
154
-
155
- # Pattern matching
156
- content = await read_note("docs/*-architecture") # All architecture docs
157
- content = await read_note("docs/*/implements/*") # Find implementations
158
-
159
- Output format:
160
- ```
161
- --- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
162
- <first document content>
163
-
164
- --- memory://docs/other 2025-01-30T15:45:22 a1b2c3d4
165
- <second document content>
166
- ```
167
-
168
- The headers include:
169
- - Full memory:// URI for the document
170
- - Last modified timestamp
171
- - Content checksum
172
- """
173
- with logfire.span("Reading note", identifier=identifier): # pyright: ignore [reportGeneralTypeIssues]
174
- logger.info(f"Reading note {identifier}")
175
- url = memory_url_path(identifier)
176
- response = await call_get(
177
- client, f"/resource/{url}", params={"page": page, "page_size": page_size}
178
- )
179
- return response.text
180
-
181
-
182
- @mcp.tool(description="Delete a note by title or permalink")
183
- async def delete_note(identifier: str) -> bool:
184
- """Delete a note from the knowledge base.
185
-
186
- Args:
187
- identifier: Note title or permalink
188
-
189
- Returns:
190
- True if note was deleted, False otherwise
191
-
192
- Examples:
193
- # Delete by title
194
- delete_note("Meeting Notes: Project Planning")
195
-
196
- # Delete by permalink
197
- delete_note("notes/project-planning")
198
- """
199
- with logfire.span("Deleting note", identifier=identifier): # pyright: ignore [reportGeneralTypeIssues]
200
- response = await call_delete(client, f"/knowledge/entities/{identifier}")
201
- result = DeleteEntitiesResponse.model_validate(response.json())
202
- return result.deleted
@@ -1,28 +0,0 @@
1
- """Schemas for knowledge discovery and analytics endpoints."""
2
-
3
- from typing import List, Optional
4
- from pydantic import BaseModel, Field
5
-
6
- from basic_memory.schemas.response import EntityResponse
7
-
8
-
9
- class EntityTypeList(BaseModel):
10
- """List of unique entity types in the system."""
11
-
12
- types: List[str]
13
-
14
-
15
- class ObservationCategoryList(BaseModel):
16
- """List of unique observation categories in the system."""
17
-
18
- categories: List[str]
19
-
20
-
21
- class TypedEntityList(BaseModel):
22
- """List of entities of a specific type."""
23
-
24
- entity_type: str = Field(..., description="Type of entities in the list")
25
- entities: List[EntityResponse]
26
- total: int = Field(..., description="Total number of entities")
27
- sort_by: Optional[str] = Field(None, description="Field used for sorting")
28
- include_related: bool = Field(False, description="Whether related entities are included")
@@ -1,158 +0,0 @@
1
- """Service for detecting changes between filesystem and database."""
2
-
3
- from dataclasses import dataclass, field
4
- from pathlib import Path
5
- from typing import Dict, Sequence
6
-
7
- from loguru import logger
8
-
9
- from basic_memory.file_utils import compute_checksum
10
- from basic_memory.models import Entity
11
- from basic_memory.repository.entity_repository import EntityRepository
12
- from basic_memory.sync.utils import SyncReport
13
-
14
-
15
- @dataclass
16
- class FileState:
17
- """State of a file including file path, permalink and checksum info."""
18
-
19
- file_path: str
20
- permalink: str
21
- checksum: str
22
-
23
-
24
- @dataclass
25
- class ScanResult:
26
- """Result of scanning a directory."""
27
-
28
- # file_path -> checksum
29
- files: Dict[str, str] = field(default_factory=dict)
30
- # file_path -> error message
31
- errors: Dict[str, str] = field(default_factory=dict)
32
-
33
-
34
- class FileChangeScanner:
35
- """
36
- Service for detecting changes between filesystem and database.
37
- The filesystem is treated as the source of truth.
38
- """
39
-
40
- def __init__(self, entity_repository: EntityRepository):
41
- self.entity_repository = entity_repository
42
-
43
- async def scan_directory(self, directory: Path) -> ScanResult:
44
- """
45
- Scan directory for markdown files and their checksums.
46
- Only processes .md files, logs and skips others.
47
-
48
- Args:
49
- directory: Directory to scan
50
-
51
- Returns:
52
- ScanResult containing found files and any errors
53
- """
54
- logger.debug(f"Scanning directory: {directory}")
55
- result = ScanResult()
56
-
57
- if not directory.exists():
58
- logger.debug(f"Directory does not exist: {directory}")
59
- return result
60
-
61
- for path in directory.rglob("*"):
62
- if not path.is_file() or not path.name.endswith(".md"):
63
- if path.is_file():
64
- logger.debug(f"Skipping non-markdown file: {path}")
65
- continue
66
-
67
- try:
68
- # Get relative path first - used in error reporting if needed
69
- rel_path = str(path.relative_to(directory))
70
- content = path.read_text()
71
- checksum = await compute_checksum(content)
72
- result.files[rel_path] = checksum
73
-
74
- except Exception as e:
75
- rel_path = str(path.relative_to(directory))
76
- result.errors[rel_path] = str(e)
77
- logger.error(f"Failed to read {rel_path}: {e}")
78
-
79
- logger.debug(f"Found {len(result.files)} markdown files")
80
- if result.errors:
81
- logger.warning(f"Encountered {len(result.errors)} errors while scanning")
82
-
83
- return result
84
-
85
- async def find_changes(
86
- self, directory: Path, db_file_state: Dict[str, FileState]
87
- ) -> SyncReport:
88
- """Find changes between filesystem and database."""
89
- # Get current files and checksums
90
- scan_result = await self.scan_directory(directory)
91
- current_files = scan_result.files
92
-
93
- # Build report
94
- report = SyncReport(total=len(current_files))
95
-
96
- # Track potentially moved files by checksum
97
- files_by_checksum = {} # checksum -> file_path
98
-
99
- # First find potential new files and record checksums
100
- for file_path, checksum in current_files.items():
101
- logger.debug(f"{file_path} ({checksum[:8]})")
102
-
103
- if file_path not in db_file_state:
104
- # Could be new or could be the destination of a move
105
- report.new.add(file_path)
106
- files_by_checksum[checksum] = file_path
107
- elif checksum != db_file_state[file_path].checksum:
108
- report.modified.add(file_path)
109
-
110
- report.checksums[file_path] = checksum
111
-
112
- # Now detect moves and deletions
113
- for db_file_path, db_state in db_file_state.items():
114
- if db_file_path not in current_files:
115
- if db_state.checksum in files_by_checksum:
116
- # Found a move - file exists at new path with same checksum
117
- new_path = files_by_checksum[db_state.checksum]
118
- report.moves[db_file_path] = new_path
119
- # Remove from new files since it's a move
120
- report.new.remove(new_path)
121
- else:
122
- # Actually deleted
123
- report.deleted.add(db_file_path)
124
-
125
- # Log summary
126
- logger.debug(f"Total files: {report.total}")
127
- logger.debug(f"Changes found: {report.total_changes}")
128
- logger.debug(f" New: {len(report.new)}")
129
- logger.debug(f" Modified: {len(report.modified)}")
130
- logger.debug(f" Moved: {len(report.moves)}")
131
- logger.debug(f" Deleted: {len(report.deleted)}")
132
-
133
- if scan_result.errors: # pragma: no cover
134
- logger.warning("Files skipped due to errors:")
135
- for file_path, error in scan_result.errors.items():
136
- logger.warning(f" {file_path}: {error}")
137
-
138
- return report
139
-
140
- async def get_db_file_state(self, db_records: Sequence[Entity]) -> Dict[str, FileState]:
141
- """Get file_path and checksums from database.
142
- Args:
143
- db_records: database records
144
- Returns:
145
- Dict mapping file paths to FileState
146
- :param db_records: the data from the db
147
- """
148
- return {
149
- r.file_path: FileState(
150
- file_path=r.file_path, permalink=r.permalink, checksum=r.checksum or ""
151
- )
152
- for r in db_records
153
- }
154
-
155
- async def find_knowledge_changes(self, directory: Path) -> SyncReport:
156
- """Find changes in knowledge directory."""
157
- db_file_state = await self.get_db_file_state(await self.entity_repository.find_all())
158
- return await self.find_changes(directory=directory, db_file_state=db_file_state)
@@ -1,31 +0,0 @@
1
- """Types and utilities for file sync."""
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Set, Dict
5
-
6
-
7
- @dataclass
8
- class SyncReport:
9
- """Report of file changes found compared to database state.
10
-
11
- Attributes:
12
- total: Total number of files in directory being synced
13
- new: Files that exist on disk but not in database
14
- modified: Files that exist in both but have different checksums
15
- deleted: Files that exist in database but not on disk
16
- moves: Files that have been moved from one location to another
17
- checksums: Current checksums for files on disk
18
- """
19
-
20
- total: int = 0
21
- # We keep paths as strings in sets/dicts for easier serialization
22
- new: Set[str] = field(default_factory=set)
23
- modified: Set[str] = field(default_factory=set)
24
- deleted: Set[str] = field(default_factory=set)
25
- moves: Dict[str, str] = field(default_factory=dict) # old_path -> new_path
26
- checksums: Dict[str, str] = field(default_factory=dict) # path -> checksum
27
-
28
- @property
29
- def total_changes(self) -> int:
30
- """Total number of changes."""
31
- return len(self.new) + len(self.modified) + len(self.deleted) + len(self.moves)