basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 +145 -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.0b1.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b1.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.0b1.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -100,27 +100,41 @@ class MemoryMetadata(BaseModel):
100
100
  uri: Optional[str] = None
101
101
  types: Optional[List[SearchItemType]] = None
102
102
  depth: int
103
- timeframe: str
103
+ timeframe: Optional[str] = None
104
104
  generated_at: datetime
105
- total_results: int
106
- total_relations: int
105
+ primary_count: Optional[int] = None # Changed field name
106
+ related_count: Optional[int] = None # Changed field name
107
+ total_results: Optional[int] = None # For backward compatibility
108
+ total_relations: Optional[int] = None
109
+ total_observations: Optional[int] = None
107
110
 
108
111
 
109
- class GraphContext(BaseModel):
110
- """Complete context response."""
112
+ class ContextResult(BaseModel):
113
+ """Context result containing a primary item with its observations and related items."""
111
114
 
112
- # Direct matches
113
- primary_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
114
- description="results directly matching URI"
115
+ primary_result: EntitySummary | RelationSummary | ObservationSummary = Field(
116
+ description="Primary item"
117
+ )
118
+
119
+ observations: Sequence[ObservationSummary] = Field(
120
+ description="Observations belonging to this entity", default_factory=list
115
121
  )
116
122
 
117
- # Related entities
118
123
  related_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
119
- description="related results"
124
+ description="Related items", default_factory=list
125
+ )
126
+
127
+
128
+ class GraphContext(BaseModel):
129
+ """Complete context response."""
130
+
131
+ # hierarchical results
132
+ results: Sequence[ContextResult] = Field(
133
+ description="Hierarchical results with related items nested", default_factory=list
120
134
  )
121
135
 
122
136
  # Context metadata
123
137
  metadata: MemoryMetadata
124
138
 
125
- page: int = 1
126
- page_size: int = 1
139
+ page: Optional[int] = None
140
+ page_size: Optional[int] = None
@@ -1,5 +1,6 @@
1
1
  """Schema for project info response."""
2
2
 
3
+ import os
3
4
  from datetime import datetime
4
5
  from typing import Dict, List, Optional, Any
5
6
 
@@ -75,14 +76,24 @@ class SystemStatus(BaseModel):
75
76
  timestamp: datetime = Field(description="Timestamp when the information was collected")
76
77
 
77
78
 
79
+ class ProjectDetail(BaseModel):
80
+ """Detailed information about a project."""
81
+
82
+ path: str = Field(description="Path to the project directory")
83
+ active: bool = Field(description="Whether the project is active")
84
+ id: Optional[int] = Field(description="Database ID of the project if available")
85
+ is_default: bool = Field(description="Whether this is the default project")
86
+ permalink: str = Field(description="URL-friendly identifier for the project")
87
+
88
+
78
89
  class ProjectInfoResponse(BaseModel):
79
90
  """Response for the project_info tool."""
80
91
 
81
92
  # Project configuration
82
93
  project_name: str = Field(description="Name of the current project")
83
94
  project_path: str = Field(description="Path to the current project files")
84
- available_projects: Dict[str, str] = Field(
85
- description="Map of configured project names to paths"
95
+ available_projects: Dict[str, Dict[str, Any]] = Field(
96
+ description="Map of configured project names to detailed project information"
86
97
  )
87
98
  default_project: str = Field(description="Name of the default project")
88
99
 
@@ -94,3 +105,102 @@ class ProjectInfoResponse(BaseModel):
94
105
 
95
106
  # System status
96
107
  system: SystemStatus = Field(description="System and service status information")
108
+
109
+
110
+ class ProjectInfoRequest(BaseModel):
111
+ """Request model for switching projects."""
112
+
113
+ name: str = Field(..., description="Name of the project to switch to")
114
+ path: str = Field(..., description="Path to the project directory")
115
+ set_default: bool = Field(..., description="Set the project as the default")
116
+
117
+
118
+ class WatchEvent(BaseModel):
119
+ timestamp: datetime
120
+ path: str
121
+ action: str # new, delete, etc
122
+ status: str # success, error
123
+ checksum: Optional[str]
124
+ error: Optional[str] = None
125
+
126
+
127
+ class WatchServiceState(BaseModel):
128
+ # Service status
129
+ running: bool = False
130
+ start_time: datetime = datetime.now() # Use directly with Pydantic model
131
+ pid: int = os.getpid() # Use directly with Pydantic model
132
+
133
+ # Stats
134
+ error_count: int = 0
135
+ last_error: Optional[datetime] = None
136
+ last_scan: Optional[datetime] = None
137
+
138
+ # File counts
139
+ synced_files: int = 0
140
+
141
+ # Recent activity
142
+ recent_events: List[WatchEvent] = [] # Use directly with Pydantic model
143
+
144
+ def add_event(
145
+ self,
146
+ path: str,
147
+ action: str,
148
+ status: str,
149
+ checksum: Optional[str] = None,
150
+ error: Optional[str] = None,
151
+ ) -> WatchEvent: # pragma: no cover
152
+ event = WatchEvent(
153
+ timestamp=datetime.now(),
154
+ path=path,
155
+ action=action,
156
+ status=status,
157
+ checksum=checksum,
158
+ error=error,
159
+ )
160
+ self.recent_events.insert(0, event)
161
+ self.recent_events = self.recent_events[:100] # Keep last 100
162
+ return event
163
+
164
+ def record_error(self, error: str): # pragma: no cover
165
+ self.error_count += 1
166
+ self.add_event(path="", action="sync", status="error", error=error)
167
+ self.last_error = datetime.now()
168
+
169
+
170
+ class ProjectWatchStatus(BaseModel):
171
+ """Project with its watch status."""
172
+
173
+ name: str = Field(..., description="Name of the project")
174
+ path: str = Field(..., description="Path to the project")
175
+ watch_status: Optional[WatchServiceState] = Field(
176
+ None, description="Watch status information for the project"
177
+ )
178
+
179
+
180
+ class ProjectItem(BaseModel):
181
+ """Simple representation of a project."""
182
+
183
+ name: str
184
+ path: str
185
+ is_default: bool = False
186
+
187
+
188
+ class ProjectList(BaseModel):
189
+ """Response model for listing projects."""
190
+
191
+ projects: List[ProjectItem]
192
+ default_project: str
193
+
194
+
195
+ class ProjectStatusResponse(BaseModel):
196
+ """Response model for switching projects."""
197
+
198
+ message: str = Field(..., description="Status message about the project switch")
199
+ status: str = Field(..., description="Status of the switch (success or error)")
200
+ default: bool = Field(..., description="True if the project was set as the default")
201
+ old_project: Optional[ProjectItem] = Field(
202
+ None, description="Information about the project being switched from"
203
+ )
204
+ new_project: Optional[ProjectItem] = Field(
205
+ None, description="Information about the project being switched to"
206
+ )
@@ -0,0 +1,90 @@
1
+ """Request and response schemas for prompt-related operations."""
2
+
3
+ from typing import Optional, List, Any, Dict
4
+ from pydantic import BaseModel, Field
5
+
6
+ from basic_memory.schemas.base import TimeFrame
7
+ from basic_memory.schemas.memory import EntitySummary, ObservationSummary, RelationSummary
8
+
9
+
10
+ class PromptContextItem(BaseModel):
11
+ """Container for primary and related results to render in a prompt."""
12
+
13
+ primary_results: List[EntitySummary]
14
+ related_results: List[EntitySummary | ObservationSummary | RelationSummary]
15
+
16
+
17
+ class ContinueConversationRequest(BaseModel):
18
+ """Request for generating a continue conversation prompt.
19
+
20
+ Used to provide context for continuing a conversation on a specific topic
21
+ or with recent activity from a given timeframe.
22
+ """
23
+
24
+ topic: Optional[str] = Field(None, description="Topic or keyword to search for")
25
+ timeframe: Optional[TimeFrame] = Field(
26
+ None, description="How far back to look for activity (e.g. '1d', '1 week')"
27
+ )
28
+ # Limit depth to max 2 for performance reasons - higher values cause significant slowdown
29
+ search_items_limit: int = Field(
30
+ 5,
31
+ description="Maximum number of search results to include in context (max 10)",
32
+ ge=1,
33
+ le=10,
34
+ )
35
+ depth: int = Field(
36
+ 1,
37
+ description="How many relationship 'hops' to follow when building context (max 5)",
38
+ ge=1,
39
+ le=5,
40
+ )
41
+ # Limit related items to prevent overloading the context
42
+ related_items_limit: int = Field(
43
+ 5, description="Maximum number of related items to include in context (max 10)", ge=1, le=10
44
+ )
45
+
46
+
47
+ class SearchPromptRequest(BaseModel):
48
+ """Request for generating a search results prompt.
49
+
50
+ Used to format search results into a prompt with context and suggestions.
51
+ """
52
+
53
+ query: str = Field(..., description="The search query text")
54
+ timeframe: Optional[TimeFrame] = Field(
55
+ None, description="Optional timeframe to limit results (e.g. '1d', '1 week')"
56
+ )
57
+
58
+
59
+ class PromptMetadata(BaseModel):
60
+ """Metadata about a prompt response.
61
+
62
+ Contains statistical information about the prompt generation process
63
+ and results, useful for debugging and UI display.
64
+ """
65
+
66
+ query: Optional[str] = Field(None, description="The original query or topic")
67
+ timeframe: Optional[str] = Field(None, description="The timeframe used for filtering")
68
+ search_count: int = Field(0, description="Number of search results found")
69
+ context_count: int = Field(0, description="Number of context items retrieved")
70
+ observation_count: int = Field(0, description="Total number of observations included")
71
+ relation_count: int = Field(0, description="Total number of relations included")
72
+ total_items: int = Field(0, description="Total number of all items included in the prompt")
73
+ search_limit: int = Field(0, description="Maximum search results requested")
74
+ context_depth: int = Field(0, description="Context depth used")
75
+ related_limit: int = Field(0, description="Maximum related items requested")
76
+ generated_at: str = Field(..., description="ISO timestamp when this prompt was generated")
77
+
78
+
79
+ class PromptResponse(BaseModel):
80
+ """Response containing the rendered prompt.
81
+
82
+ Includes both the rendered prompt text and the context that was used
83
+ to render it, for potential client-side use.
84
+ """
85
+
86
+ prompt: str = Field(..., description="The rendered prompt text")
87
+ context: Dict[str, Any] = Field(..., description="The context used to render the prompt")
88
+ metadata: PromptMetadata = Field(
89
+ ..., description="Metadata about the prompt generation process"
90
+ )
@@ -1,9 +1,9 @@
1
1
  """Request schemas for interacting with the knowledge graph."""
2
2
 
3
- from typing import List, Optional, Annotated
3
+ from typing import List, Optional, Annotated, Literal
4
4
  from annotated_types import MaxLen, MinLen
5
5
 
6
- from pydantic import BaseModel
6
+ from pydantic import BaseModel, field_validator
7
7
 
8
8
  from basic_memory.schemas.base import (
9
9
  Relation,
@@ -56,3 +56,57 @@ class GetEntitiesRequest(BaseModel):
56
56
 
57
57
  class CreateRelationsRequest(BaseModel):
58
58
  relations: List[Relation]
59
+
60
+
61
+ class EditEntityRequest(BaseModel):
62
+ """Request schema for editing an existing entity's content.
63
+
64
+ This allows for targeted edits without requiring the full entity content.
65
+ Supports various operation types for different editing scenarios.
66
+ """
67
+
68
+ operation: Literal["append", "prepend", "find_replace", "replace_section"]
69
+ content: str
70
+ section: Optional[str] = None
71
+ find_text: Optional[str] = None
72
+ expected_replacements: int = 1
73
+
74
+ @field_validator("section")
75
+ @classmethod
76
+ def validate_section_for_replace_section(cls, v, info):
77
+ """Ensure section is provided for replace_section operation."""
78
+ if info.data.get("operation") == "replace_section" and not v:
79
+ raise ValueError("section parameter is required for replace_section operation")
80
+ return v
81
+
82
+ @field_validator("find_text")
83
+ @classmethod
84
+ def validate_find_text_for_find_replace(cls, v, info):
85
+ """Ensure find_text is provided for find_replace operation."""
86
+ if info.data.get("operation") == "find_replace" and not v:
87
+ raise ValueError("find_text parameter is required for find_replace operation")
88
+ return v
89
+
90
+
91
+ class MoveEntityRequest(BaseModel):
92
+ """Request schema for moving an entity to a new file location.
93
+
94
+ This allows moving notes to different paths while maintaining project
95
+ consistency and optionally updating permalinks based on configuration.
96
+ """
97
+
98
+ identifier: Annotated[str, MinLen(1), MaxLen(200)]
99
+ destination_path: Annotated[str, MinLen(1), MaxLen(500)]
100
+ project: Optional[str] = None
101
+
102
+ @field_validator("destination_path")
103
+ @classmethod
104
+ def validate_destination_path(cls, v):
105
+ """Ensure destination path is relative and valid."""
106
+ if v.startswith("/"):
107
+ raise ValueError("destination_path must be relative, not absolute")
108
+ if ".." in v:
109
+ raise ValueError("destination_path cannot contain '..' path components")
110
+ if not v.strip():
111
+ raise ValueError("destination_path cannot be empty or whitespace only")
112
+ return v.strip()
@@ -90,7 +90,7 @@ class SearchResult(BaseModel):
90
90
  title: str
91
91
  type: SearchItemType
92
92
  score: float
93
- entity: Optional[Permalink]
93
+ entity: Optional[Permalink] = None
94
94
  permalink: Optional[str]
95
95
  content: Optional[str] = None
96
96
  file_path: str
@@ -3,5 +3,6 @@
3
3
  from .service import BaseService
4
4
  from .file_service import FileService
5
5
  from .entity_service import EntityService
6
+ from .project_service import ProjectService
6
7
 
7
- __all__ = ["BaseService", "FileService", "EntityService"]
8
+ __all__ = ["BaseService", "FileService", "EntityService", "ProjectService"]