basic-memory 0.15.0__py3-none-any.whl → 0.15.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 (47) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/api/routers/directory_router.py +23 -2
  3. basic_memory/api/routers/project_router.py +1 -0
  4. basic_memory/cli/auth.py +2 -2
  5. basic_memory/cli/commands/command_utils.py +11 -28
  6. basic_memory/cli/commands/mcp.py +72 -67
  7. basic_memory/cli/commands/project.py +54 -49
  8. basic_memory/cli/commands/status.py +6 -15
  9. basic_memory/config.py +55 -9
  10. basic_memory/deps.py +7 -5
  11. basic_memory/ignore_utils.py +7 -7
  12. basic_memory/mcp/async_client.py +102 -4
  13. basic_memory/mcp/prompts/continue_conversation.py +16 -15
  14. basic_memory/mcp/prompts/search.py +12 -11
  15. basic_memory/mcp/resources/ai_assistant_guide.md +185 -453
  16. basic_memory/mcp/resources/project_info.py +9 -7
  17. basic_memory/mcp/tools/build_context.py +40 -39
  18. basic_memory/mcp/tools/canvas.py +21 -20
  19. basic_memory/mcp/tools/chatgpt_tools.py +11 -2
  20. basic_memory/mcp/tools/delete_note.py +22 -21
  21. basic_memory/mcp/tools/edit_note.py +105 -104
  22. basic_memory/mcp/tools/list_directory.py +98 -95
  23. basic_memory/mcp/tools/move_note.py +127 -125
  24. basic_memory/mcp/tools/project_management.py +101 -98
  25. basic_memory/mcp/tools/read_content.py +64 -63
  26. basic_memory/mcp/tools/read_note.py +88 -88
  27. basic_memory/mcp/tools/recent_activity.py +139 -135
  28. basic_memory/mcp/tools/search.py +27 -26
  29. basic_memory/mcp/tools/sync_status.py +133 -128
  30. basic_memory/mcp/tools/utils.py +0 -15
  31. basic_memory/mcp/tools/view_note.py +14 -28
  32. basic_memory/mcp/tools/write_note.py +97 -87
  33. basic_memory/repository/entity_repository.py +60 -0
  34. basic_memory/repository/repository.py +16 -3
  35. basic_memory/repository/search_repository.py +42 -0
  36. basic_memory/schemas/project_info.py +1 -1
  37. basic_memory/services/directory_service.py +124 -3
  38. basic_memory/services/entity_service.py +31 -9
  39. basic_memory/services/project_service.py +97 -10
  40. basic_memory/services/search_service.py +16 -8
  41. basic_memory/sync/sync_service.py +28 -13
  42. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/METADATA +51 -4
  43. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/RECORD +46 -47
  44. basic_memory/mcp/tools/headers.py +0 -44
  45. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
  46. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
  47. {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,9 +11,9 @@ DEFAULT_IGNORE_PATTERNS = {
11
11
  # Hidden files (files starting with dot)
12
12
  ".*",
13
13
  # Basic Memory internal files
14
- "memory.db",
15
- "memory.db-shm",
16
- "memory.db-wal",
14
+ "*.db",
15
+ "*.db-shm",
16
+ "*.db-wal",
17
17
  "config.json",
18
18
  # Version control
19
19
  ".git",
@@ -84,10 +84,10 @@ def create_default_bmignore() -> None:
84
84
  # Hidden files (files starting with dot)
85
85
  .*
86
86
 
87
- # Basic Memory internal files
88
- memory.db
89
- memory.db-shm
90
- memory.db-wal
87
+ # Basic Memory internal files (includes test databases)
88
+ *.db
89
+ *.db-shm
90
+ *.db-wal
91
91
  config.json
92
92
 
93
93
  # Version control
@@ -1,3 +1,6 @@
1
+ from contextlib import asynccontextmanager, AbstractAsyncContextManager
2
+ from typing import AsyncIterator, Callable, Optional
3
+
1
4
  from httpx import ASGITransport, AsyncClient, Timeout
2
5
  from loguru import logger
3
6
 
@@ -5,9 +8,108 @@ from basic_memory.api.app import app as fastapi_app
5
8
  from basic_memory.config import ConfigManager
6
9
 
7
10
 
11
+ # Optional factory override for dependency injection
12
+ _client_factory: Optional[Callable[[], AbstractAsyncContextManager[AsyncClient]]] = None
13
+
14
+
15
+ def set_client_factory(factory: Callable[[], AbstractAsyncContextManager[AsyncClient]]) -> None:
16
+ """Override the default client factory (for cloud app, testing, etc).
17
+
18
+ Args:
19
+ factory: An async context manager that yields an AsyncClient
20
+
21
+ Example:
22
+ @asynccontextmanager
23
+ async def custom_client_factory():
24
+ async with AsyncClient(...) as client:
25
+ yield client
26
+
27
+ set_client_factory(custom_client_factory)
28
+ """
29
+ global _client_factory
30
+ _client_factory = factory
31
+
32
+
33
+ @asynccontextmanager
34
+ async def get_client() -> AsyncIterator[AsyncClient]:
35
+ """Get an AsyncClient as a context manager.
36
+
37
+ This function provides proper resource management for HTTP clients,
38
+ ensuring connections are closed after use. It supports three modes:
39
+
40
+ 1. **Factory injection** (cloud app, tests):
41
+ If a custom factory is set via set_client_factory(), use that.
42
+
43
+ 2. **CLI cloud mode**:
44
+ When cloud_mode_enabled is True, create HTTP client with auth
45
+ token from CLIAuth for requests to cloud proxy endpoint.
46
+
47
+ 3. **Local mode** (default):
48
+ Use ASGI transport for in-process requests to local FastAPI app.
49
+
50
+ Usage:
51
+ async with get_client() as client:
52
+ response = await client.get("/path")
53
+
54
+ Yields:
55
+ AsyncClient: Configured HTTP client for the current mode
56
+
57
+ Raises:
58
+ RuntimeError: If cloud mode is enabled but user is not authenticated
59
+ """
60
+ if _client_factory:
61
+ # Use injected factory (cloud app, tests)
62
+ async with _client_factory() as client:
63
+ yield client
64
+ else:
65
+ # Default: create based on config
66
+ config = ConfigManager().config
67
+ timeout = Timeout(
68
+ connect=10.0, # 10 seconds for connection
69
+ read=30.0, # 30 seconds for reading response
70
+ write=30.0, # 30 seconds for writing request
71
+ pool=30.0, # 30 seconds for connection pool
72
+ )
73
+
74
+ if config.cloud_mode_enabled:
75
+ # CLI cloud mode: inject auth when creating client
76
+ from basic_memory.cli.auth import CLIAuth
77
+
78
+ auth = CLIAuth(client_id=config.cloud_client_id, authkit_domain=config.cloud_domain)
79
+ token = await auth.get_valid_token()
80
+
81
+ if not token:
82
+ raise RuntimeError(
83
+ "Cloud mode enabled but not authenticated. "
84
+ "Run 'basic-memory cloud login' first."
85
+ )
86
+
87
+ # Auth header set ONCE at client creation
88
+ proxy_base_url = f"{config.cloud_host}/proxy"
89
+ logger.info(f"Creating HTTP client for cloud proxy at: {proxy_base_url}")
90
+ async with AsyncClient(
91
+ base_url=proxy_base_url,
92
+ headers={"Authorization": f"Bearer {token}"},
93
+ timeout=timeout,
94
+ ) as client:
95
+ yield client
96
+ else:
97
+ # Local mode: ASGI transport for in-process calls
98
+ logger.info("Creating ASGI client for local Basic Memory API")
99
+ async with AsyncClient(
100
+ transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
101
+ ) as client:
102
+ yield client
103
+
104
+
8
105
  def create_client() -> AsyncClient:
9
106
  """Create an HTTP client based on configuration.
10
107
 
108
+ DEPRECATED: Use get_client() context manager instead for proper resource management.
109
+
110
+ This function is kept for backward compatibility but will be removed in a future version.
111
+ The returned client should be closed manually by calling await client.aclose().
112
+
11
113
  Returns:
12
114
  AsyncClient configured for either local ASGI or remote proxy
13
115
  """
@@ -34,7 +136,3 @@ def create_client() -> AsyncClient:
34
136
  return AsyncClient(
35
137
  transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
36
138
  )
37
-
38
-
39
- # Create shared async client
40
- client = create_client()
@@ -10,7 +10,7 @@ from loguru import logger
10
10
  from pydantic import Field
11
11
 
12
12
  from basic_memory.config import get_project_config
13
- from basic_memory.mcp.async_client import client
13
+ from basic_memory.mcp.async_client import get_client
14
14
  from basic_memory.mcp.server import mcp
15
15
  from basic_memory.mcp.tools.utils import call_post
16
16
  from basic_memory.schemas.base import TimeFrame
@@ -42,20 +42,21 @@ async def continue_conversation(
42
42
  """
43
43
  logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
44
44
 
45
- # Create request model
46
- request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
47
- topic=topic, timeframe=timeframe
48
- )
45
+ async with get_client() as client:
46
+ # Create request model
47
+ request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
48
+ topic=topic, timeframe=timeframe
49
+ )
49
50
 
50
- project_url = get_project_config().project_url
51
+ project_url = get_project_config().project_url
51
52
 
52
- # Call the prompt API endpoint
53
- response = await call_post(
54
- client,
55
- f"{project_url}/prompt/continue-conversation",
56
- json=request.model_dump(exclude_none=True),
57
- )
53
+ # Call the prompt API endpoint
54
+ response = await call_post(
55
+ client,
56
+ f"{project_url}/prompt/continue-conversation",
57
+ json=request.model_dump(exclude_none=True),
58
+ )
58
59
 
59
- # Extract the rendered prompt from the response
60
- result = response.json()
61
- return result["prompt"]
60
+ # Extract the rendered prompt from the response
61
+ result = response.json()
62
+ return result["prompt"]
@@ -9,7 +9,7 @@ from loguru import logger
9
9
  from pydantic import Field
10
10
 
11
11
  from basic_memory.config import get_project_config
12
- from basic_memory.mcp.async_client import client
12
+ from basic_memory.mcp.async_client import get_client
13
13
  from basic_memory.mcp.server import mcp
14
14
  from basic_memory.mcp.tools.utils import call_post
15
15
  from basic_memory.schemas.base import TimeFrame
@@ -41,16 +41,17 @@ async def search_prompt(
41
41
  """
42
42
  logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
43
43
 
44
- # Create request model
45
- request = SearchPromptRequest(query=query, timeframe=timeframe)
44
+ async with get_client() as client:
45
+ # Create request model
46
+ request = SearchPromptRequest(query=query, timeframe=timeframe)
46
47
 
47
- project_url = get_project_config().project_url
48
+ project_url = get_project_config().project_url
48
49
 
49
- # Call the prompt API endpoint
50
- response = await call_post(
51
- client, f"{project_url}/prompt/search", json=request.model_dump(exclude_none=True)
52
- )
50
+ # Call the prompt API endpoint
51
+ response = await call_post(
52
+ client, f"{project_url}/prompt/search", json=request.model_dump(exclude_none=True)
53
+ )
53
54
 
54
- # Extract the rendered prompt from the response
55
- result = response.json()
56
- return result["prompt"]
55
+ # Extract the rendered prompt from the response
56
+ result = response.json()
57
+ return result["prompt"]