basic-memory 0.14.2__py3-none-any.whl → 0.14.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 (69) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/env.py +3 -1
  3. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +53 -0
  4. basic_memory/api/app.py +4 -1
  5. basic_memory/api/routers/management_router.py +3 -1
  6. basic_memory/api/routers/project_router.py +21 -13
  7. basic_memory/api/routers/resource_router.py +3 -3
  8. basic_memory/cli/app.py +3 -3
  9. basic_memory/cli/commands/__init__.py +1 -2
  10. basic_memory/cli/commands/db.py +5 -5
  11. basic_memory/cli/commands/import_chatgpt.py +3 -2
  12. basic_memory/cli/commands/import_claude_conversations.py +3 -1
  13. basic_memory/cli/commands/import_claude_projects.py +3 -1
  14. basic_memory/cli/commands/import_memory_json.py +5 -2
  15. basic_memory/cli/commands/mcp.py +3 -15
  16. basic_memory/cli/commands/project.py +46 -6
  17. basic_memory/cli/commands/status.py +4 -1
  18. basic_memory/cli/commands/sync.py +10 -2
  19. basic_memory/cli/main.py +0 -1
  20. basic_memory/config.py +61 -34
  21. basic_memory/db.py +2 -6
  22. basic_memory/deps.py +3 -2
  23. basic_memory/file_utils.py +65 -0
  24. basic_memory/importers/chatgpt_importer.py +20 -10
  25. basic_memory/importers/memory_json_importer.py +22 -7
  26. basic_memory/importers/utils.py +2 -2
  27. basic_memory/markdown/entity_parser.py +2 -2
  28. basic_memory/markdown/markdown_processor.py +2 -2
  29. basic_memory/markdown/plugins.py +42 -26
  30. basic_memory/markdown/utils.py +1 -1
  31. basic_memory/mcp/async_client.py +22 -2
  32. basic_memory/mcp/project_session.py +6 -4
  33. basic_memory/mcp/prompts/__init__.py +0 -2
  34. basic_memory/mcp/server.py +8 -71
  35. basic_memory/mcp/tools/build_context.py +12 -2
  36. basic_memory/mcp/tools/move_note.py +24 -12
  37. basic_memory/mcp/tools/project_management.py +22 -7
  38. basic_memory/mcp/tools/read_content.py +16 -0
  39. basic_memory/mcp/tools/read_note.py +17 -2
  40. basic_memory/mcp/tools/sync_status.py +3 -2
  41. basic_memory/mcp/tools/write_note.py +9 -1
  42. basic_memory/models/knowledge.py +13 -2
  43. basic_memory/models/project.py +3 -3
  44. basic_memory/repository/entity_repository.py +2 -2
  45. basic_memory/repository/project_repository.py +19 -1
  46. basic_memory/repository/search_repository.py +7 -3
  47. basic_memory/schemas/base.py +40 -10
  48. basic_memory/schemas/importer.py +1 -0
  49. basic_memory/schemas/memory.py +23 -11
  50. basic_memory/services/context_service.py +12 -2
  51. basic_memory/services/directory_service.py +7 -0
  52. basic_memory/services/entity_service.py +56 -10
  53. basic_memory/services/initialization.py +0 -75
  54. basic_memory/services/project_service.py +93 -36
  55. basic_memory/sync/background_sync.py +4 -3
  56. basic_memory/sync/sync_service.py +53 -4
  57. basic_memory/sync/watch_service.py +31 -8
  58. basic_memory/utils.py +234 -71
  59. {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/METADATA +21 -92
  60. {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/RECORD +63 -68
  61. basic_memory/cli/commands/auth.py +0 -136
  62. basic_memory/mcp/auth_provider.py +0 -270
  63. basic_memory/mcp/external_auth_provider.py +0 -321
  64. basic_memory/mcp/prompts/sync_status.py +0 -112
  65. basic_memory/mcp/supabase_auth_provider.py +0 -463
  66. basic_memory/services/migration_service.py +0 -168
  67. {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/WHEEL +0 -0
  68. {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/entry_points.txt +0 -0
  69. {basic_memory-0.14.2.dist-info → basic_memory-0.14.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,321 +0,0 @@
1
- """External OAuth provider integration for Basic Memory MCP server."""
2
-
3
- import os
4
- from typing import Optional, Dict, Any
5
- from dataclasses import dataclass
6
-
7
- import httpx
8
- from loguru import logger
9
- from mcp.server.auth.provider import (
10
- OAuthAuthorizationServerProvider,
11
- AuthorizationParams,
12
- AuthorizationCode,
13
- RefreshToken,
14
- AccessToken,
15
- construct_redirect_uri,
16
- )
17
- from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
18
-
19
-
20
- @dataclass
21
- class ExternalAuthorizationCode(AuthorizationCode):
22
- """Authorization code with external provider metadata."""
23
-
24
- external_code: Optional[str] = None
25
- state: Optional[str] = None
26
-
27
-
28
- @dataclass
29
- class ExternalRefreshToken(RefreshToken):
30
- """Refresh token with external provider metadata."""
31
-
32
- external_token: Optional[str] = None
33
-
34
-
35
- @dataclass
36
- class ExternalAccessToken(AccessToken):
37
- """Access token with external provider metadata."""
38
-
39
- external_token: Optional[str] = None
40
-
41
-
42
- class ExternalOAuthProvider(
43
- OAuthAuthorizationServerProvider[
44
- ExternalAuthorizationCode, ExternalRefreshToken, ExternalAccessToken
45
- ]
46
- ):
47
- """OAuth provider that delegates to external OAuth providers.
48
-
49
- This provider can integrate with services like:
50
- - GitHub OAuth
51
- - Google OAuth
52
- - Auth0
53
- - Okta
54
- """
55
-
56
- def __init__(
57
- self,
58
- issuer_url: str,
59
- external_provider: str,
60
- external_client_id: str,
61
- external_client_secret: str,
62
- external_authorize_url: str,
63
- external_token_url: str,
64
- external_userinfo_url: Optional[str] = None,
65
- ):
66
- self.issuer_url = issuer_url
67
- self.external_provider = external_provider
68
- self.external_client_id = external_client_id
69
- self.external_client_secret = external_client_secret
70
- self.external_authorize_url = external_authorize_url
71
- self.external_token_url = external_token_url
72
- self.external_userinfo_url = external_userinfo_url
73
-
74
- # In-memory storage - in production, use a database
75
- self.clients: Dict[str, OAuthClientInformationFull] = {}
76
- self.codes: Dict[str, ExternalAuthorizationCode] = {}
77
- self.tokens: Dict[str, Any] = {}
78
-
79
- self.http_client = httpx.AsyncClient()
80
-
81
- async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
82
- """Get a client by ID."""
83
- return self.clients.get(client_id)
84
-
85
- async def register_client(self, client_info: OAuthClientInformationFull) -> None:
86
- """Register a new OAuth client."""
87
- self.clients[client_info.client_id] = client_info
88
- logger.info(f"Registered external OAuth client: {client_info.client_id}")
89
-
90
- async def authorize(
91
- self, client: OAuthClientInformationFull, params: AuthorizationParams
92
- ) -> str:
93
- """Create authorization URL redirecting to external provider."""
94
- # Store authorization request
95
- import secrets
96
-
97
- state = secrets.token_urlsafe(32)
98
-
99
- self.codes[state] = ExternalAuthorizationCode(
100
- code=state,
101
- scopes=params.scopes or [],
102
- expires_at=0, # Will be set by external provider
103
- client_id=client.client_id,
104
- code_challenge=params.code_challenge,
105
- redirect_uri=params.redirect_uri,
106
- redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
107
- state=params.state,
108
- )
109
-
110
- # Build external provider URL
111
- external_params = {
112
- "client_id": self.external_client_id,
113
- "redirect_uri": f"{self.issuer_url}/callback",
114
- "response_type": "code",
115
- "state": state,
116
- "scope": " ".join(params.scopes or []),
117
- }
118
-
119
- return construct_redirect_uri(self.external_authorize_url, **external_params)
120
-
121
- async def handle_callback(self, code: str, state: str) -> str:
122
- """Handle callback from external provider."""
123
- # Get original authorization request
124
- auth_code = self.codes.get(state)
125
- if not auth_code:
126
- raise ValueError("Invalid state parameter")
127
-
128
- # Exchange code with external provider
129
- token_data = {
130
- "grant_type": "authorization_code",
131
- "code": code,
132
- "redirect_uri": f"{self.issuer_url}/callback",
133
- "client_id": self.external_client_id,
134
- "client_secret": self.external_client_secret,
135
- }
136
-
137
- response = await self.http_client.post(
138
- self.external_token_url,
139
- data=token_data,
140
- )
141
- response.raise_for_status()
142
- external_tokens = response.json()
143
-
144
- # Store external tokens
145
- import secrets
146
-
147
- internal_code = secrets.token_urlsafe(32)
148
-
149
- self.codes[internal_code] = ExternalAuthorizationCode(
150
- code=internal_code,
151
- scopes=auth_code.scopes,
152
- expires_at=0,
153
- client_id=auth_code.client_id,
154
- code_challenge=auth_code.code_challenge,
155
- redirect_uri=auth_code.redirect_uri,
156
- redirect_uri_provided_explicitly=auth_code.redirect_uri_provided_explicitly,
157
- external_code=code,
158
- state=auth_code.state,
159
- )
160
-
161
- self.tokens[internal_code] = external_tokens
162
-
163
- # Redirect to original client
164
- return construct_redirect_uri(
165
- str(auth_code.redirect_uri),
166
- code=internal_code,
167
- state=auth_code.state,
168
- )
169
-
170
- async def load_authorization_code(
171
- self, client: OAuthClientInformationFull, authorization_code: str
172
- ) -> Optional[ExternalAuthorizationCode]:
173
- """Load an authorization code."""
174
- code = self.codes.get(authorization_code)
175
- if code and code.client_id == client.client_id:
176
- return code
177
- return None
178
-
179
- async def exchange_authorization_code(
180
- self, client: OAuthClientInformationFull, authorization_code: ExternalAuthorizationCode
181
- ) -> OAuthToken:
182
- """Exchange authorization code for tokens."""
183
- # Get stored external tokens
184
- external_tokens = self.tokens.get(authorization_code.code)
185
- if not external_tokens:
186
- raise ValueError("No tokens found for authorization code")
187
-
188
- # Map external tokens to MCP tokens
189
- access_token = external_tokens.get("access_token")
190
- refresh_token = external_tokens.get("refresh_token")
191
- expires_in = external_tokens.get("expires_in", 3600)
192
-
193
- # Store the mapping
194
- self.tokens[access_token] = {
195
- "client_id": client.client_id,
196
- "external_token": access_token,
197
- "scopes": authorization_code.scopes,
198
- }
199
-
200
- if refresh_token:
201
- self.tokens[refresh_token] = {
202
- "client_id": client.client_id,
203
- "external_token": refresh_token,
204
- "scopes": authorization_code.scopes,
205
- }
206
-
207
- # Clean up authorization code
208
- del self.codes[authorization_code.code]
209
-
210
- return OAuthToken(
211
- access_token=access_token,
212
- token_type="bearer",
213
- expires_in=expires_in,
214
- refresh_token=refresh_token,
215
- scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
216
- )
217
-
218
- async def load_refresh_token(
219
- self, client: OAuthClientInformationFull, refresh_token: str
220
- ) -> Optional[ExternalRefreshToken]:
221
- """Load a refresh token."""
222
- token_info = self.tokens.get(refresh_token)
223
- if token_info and token_info["client_id"] == client.client_id:
224
- return ExternalRefreshToken(
225
- token=refresh_token,
226
- client_id=client.client_id,
227
- scopes=token_info["scopes"],
228
- external_token=token_info.get("external_token"),
229
- )
230
- return None
231
-
232
- async def exchange_refresh_token(
233
- self,
234
- client: OAuthClientInformationFull,
235
- refresh_token: ExternalRefreshToken,
236
- scopes: list[str],
237
- ) -> OAuthToken:
238
- """Exchange refresh token for new tokens."""
239
- # Exchange with external provider
240
- token_data = {
241
- "grant_type": "refresh_token",
242
- "refresh_token": refresh_token.external_token or refresh_token.token,
243
- "client_id": self.external_client_id,
244
- "client_secret": self.external_client_secret,
245
- }
246
-
247
- response = await self.http_client.post(
248
- self.external_token_url,
249
- data=token_data,
250
- )
251
- response.raise_for_status()
252
- external_tokens = response.json()
253
-
254
- # Update stored tokens
255
- new_access_token = external_tokens.get("access_token")
256
- new_refresh_token = external_tokens.get("refresh_token", refresh_token.token)
257
- expires_in = external_tokens.get("expires_in", 3600)
258
-
259
- self.tokens[new_access_token] = {
260
- "client_id": client.client_id,
261
- "external_token": new_access_token,
262
- "scopes": scopes or refresh_token.scopes,
263
- }
264
-
265
- if new_refresh_token != refresh_token.token:
266
- self.tokens[new_refresh_token] = {
267
- "client_id": client.client_id,
268
- "external_token": new_refresh_token,
269
- "scopes": scopes or refresh_token.scopes,
270
- }
271
- del self.tokens[refresh_token.token]
272
-
273
- return OAuthToken(
274
- access_token=new_access_token,
275
- token_type="bearer",
276
- expires_in=expires_in,
277
- refresh_token=new_refresh_token,
278
- scope=" ".join(scopes or refresh_token.scopes),
279
- )
280
-
281
- async def load_access_token(self, token: str) -> Optional[ExternalAccessToken]:
282
- """Load and validate an access token."""
283
- token_info = self.tokens.get(token)
284
- if token_info:
285
- return ExternalAccessToken(
286
- token=token,
287
- client_id=token_info["client_id"],
288
- scopes=token_info["scopes"],
289
- external_token=token_info.get("external_token"),
290
- )
291
- return None
292
-
293
- async def revoke_token(self, token: ExternalAccessToken | ExternalRefreshToken) -> None:
294
- """Revoke a token."""
295
- self.tokens.pop(token.token, None)
296
-
297
-
298
- def create_github_provider() -> ExternalOAuthProvider:
299
- """Create an OAuth provider for GitHub integration."""
300
- return ExternalOAuthProvider(
301
- issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
302
- external_provider="github",
303
- external_client_id=os.getenv("GITHUB_CLIENT_ID", ""),
304
- external_client_secret=os.getenv("GITHUB_CLIENT_SECRET", ""),
305
- external_authorize_url="https://github.com/login/oauth/authorize",
306
- external_token_url="https://github.com/login/oauth/access_token",
307
- external_userinfo_url="https://api.github.com/user",
308
- )
309
-
310
-
311
- def create_google_provider() -> ExternalOAuthProvider:
312
- """Create an OAuth provider for Google integration."""
313
- return ExternalOAuthProvider(
314
- issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
315
- external_provider="google",
316
- external_client_id=os.getenv("GOOGLE_CLIENT_ID", ""),
317
- external_client_secret=os.getenv("GOOGLE_CLIENT_SECRET", ""),
318
- external_authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
319
- external_token_url="https://oauth2.googleapis.com/token",
320
- external_userinfo_url="https://www.googleapis.com/oauth2/v1/userinfo",
321
- )
@@ -1,112 +0,0 @@
1
- """Sync status prompt for Basic Memory MCP server."""
2
-
3
- from basic_memory.mcp.server import mcp
4
-
5
-
6
- @mcp.prompt(
7
- description="""Get sync status with recommendations for AI assistants.
8
-
9
- This prompt provides both current sync status and guidance on how
10
- AI assistants should respond when sync operations are in progress or completed.
11
- """,
12
- )
13
- async def sync_status_prompt() -> str:
14
- """Get sync status with AI assistant guidance.
15
- Returns:
16
- Formatted sync status with AI assistant guidance
17
- """
18
- try: # pragma: no cover
19
- from basic_memory.services.migration_service import migration_manager
20
-
21
- state = migration_manager.state
22
-
23
- # Build status report
24
- lines = [
25
- "# Basic Memory Sync Status",
26
- "",
27
- f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
28
- f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
29
- "",
30
- ]
31
-
32
- if migration_manager.is_ready:
33
- lines.extend(
34
- [
35
- "✅ **All sync operations completed** - System is fully operational",
36
- "",
37
- "All Basic Memory tools are available and functioning normally.",
38
- "File indexing is complete and knowledge graphs are up to date.",
39
- "You can proceed with any knowledge management tasks.",
40
- ]
41
- )
42
- else:
43
- lines.append(f"**Status Message**: {state.message}")
44
-
45
- if state.status.value == "in_progress":
46
- if state.projects_total > 0:
47
- progress = f" ({state.projects_migrated}/{state.projects_total})"
48
- lines.append(f"**Progress**: {progress}")
49
-
50
- lines.extend(
51
- [
52
- "",
53
- "🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
54
- "",
55
- "**Impact**: Some tools may show status messages instead of normal responses",
56
- "until sync completes (usually 1-3 minutes).",
57
- ]
58
- )
59
-
60
- elif state.status.value == "failed":
61
- lines.extend(
62
- [
63
- "",
64
- f"❌ **Sync failed**: {state.error or 'Unknown error'}",
65
- "",
66
- "**Impact**: System may have limited functionality until issue is resolved.",
67
- ]
68
- )
69
-
70
- # Add AI assistant recommendations
71
- if not migration_manager.is_ready:
72
- lines.extend(
73
- [
74
- "",
75
- "---",
76
- "",
77
- "## AI Assistant Recommendations",
78
- "",
79
- "**When sync is in progress:**",
80
- "- Inform the user about the background file processing",
81
- "- Suggest using `sync_status()` tool to check progress",
82
- "- Explain that tools will work normally once sync completes",
83
- "- Avoid creating complex workflows until sync is done",
84
- "",
85
- "**What to tell users:**",
86
- "- 'Basic Memory is processing your files and building knowledge graphs'",
87
- "- 'This usually takes 1-3 minutes depending on your content size'",
88
- "- 'You can check progress anytime with the sync_status tool'",
89
- "- 'Full functionality will be available once processing completes'",
90
- "",
91
- "**User-friendly language:**",
92
- "- Say 'processing files' instead of 'migration' or 'sync'",
93
- "- Say 'building knowledge graphs' instead of 'indexing'",
94
- "- Say 'setting up your knowledge base' instead of 'running migrations'",
95
- ]
96
- )
97
-
98
- return "\n".join(lines)
99
-
100
- except Exception as e: # pragma: no cover
101
- return f"""# Sync Status - Error
102
-
103
- ❌ **Unable to check sync status**: {str(e)}
104
-
105
- ## AI Assistant Recommendations
106
-
107
- **When status is unavailable:**
108
- - Assume the system is likely working normally
109
- - Try proceeding with normal operations
110
- - If users report issues, suggest checking logs or restarting
111
- - Use user-friendly language about 'setting up the knowledge base'
112
- """