basic-memory 0.23.1.dev24__py3-none-any.whl → 0.23.1.dev27__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.
@@ -221,6 +221,28 @@ async def add_project(
221
221
  raise HTTPException(status_code=400, detail=str(e))
222
222
 
223
223
 
224
+ @router.post("/doctor", response_model=ProjectStatusResponse, status_code=201)
225
+ async def add_doctor_project(project_service: ProjectServiceDep) -> ProjectStatusResponse:
226
+ """Create a server-generated disposable project for the local doctor check."""
227
+ try:
228
+ new_project = await project_service.add_doctor_project()
229
+ except ValueError as e:
230
+ raise HTTPException(status_code=400, detail=str(e))
231
+
232
+ return ProjectStatusResponse(
233
+ message=f"Project '{new_project.name}' added successfully",
234
+ status="success",
235
+ default=new_project.is_default or False,
236
+ new_project=ProjectItem(
237
+ id=new_project.id,
238
+ external_id=new_project.external_id,
239
+ name=new_project.name,
240
+ path=new_project.path,
241
+ is_default=new_project.is_default or False,
242
+ ),
243
+ )
244
+
245
+
224
246
  @router.post("/config/sync", response_model=ProjectStatusResponse)
225
247
  async def synchronize_projects(
226
248
  project_service: ProjectServiceDep,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import tempfile
6
6
  import uuid
7
7
  from pathlib import Path
8
+ from typing import Protocol
8
9
 
9
10
  from loguru import logger
10
11
  from rich.console import Console
@@ -23,6 +24,14 @@ from basic_memory.schemas import ProjectIndexRunResponse
23
24
  console = Console()
24
25
 
25
26
 
27
+ class DoctorProjectClient(Protocol):
28
+ """Project-client capability required to clean up doctor state."""
29
+
30
+ async def delete_project(
31
+ self, project_external_id: str, delete_notes: bool = False
32
+ ) -> object: ...
33
+
34
+
26
35
  def _is_default_project_delete_error(error: Exception) -> bool:
27
36
  """Return True only for the API guard that blocks deleting the default project."""
28
37
  error_text = str(error)
@@ -61,14 +70,14 @@ async def _delete_doctor_project_locally(project_name: str, project_id: str) ->
61
70
 
62
71
 
63
72
  async def _delete_doctor_project(
64
- project_client: ProjectClient, project_name: str, project_id: str
73
+ project_client: DoctorProjectClient, project_name: str, project_id: str
65
74
  ) -> None:
66
75
  """Delete the generated doctor project without weakening the public API guard."""
67
76
  # Deferred: ToolError lives in FastMCP's runtime, which must not load at CLI startup (#886).
68
77
  from fastmcp.exceptions import ToolError
69
78
 
70
79
  try:
71
- await project_client.delete_project(project_id)
80
+ await project_client.delete_project(project_id, delete_notes=True)
72
81
  except ToolError as exc:
73
82
  if not _is_default_project_delete_error(exc):
74
83
  raise
@@ -127,9 +136,16 @@ async def run_doctor() -> None:
127
136
  project_id: str | None = None
128
137
 
129
138
  try:
130
- status = await project_client.create_project(project_request.model_dump())
139
+ from basic_memory.config import ConfigManager
140
+
141
+ status = (
142
+ await project_client.create_doctor_project()
143
+ if ConfigManager().config.project_root
144
+ else await project_client.create_project(project_request.model_dump())
145
+ )
131
146
  if not status.new_project:
132
147
  raise ValueError("Failed to create doctor project")
148
+ project_name = status.new_project.name
133
149
  project_id = status.new_project.external_id
134
150
  # Use the resolved path from the server — when project_root is configured,
135
151
  # the actual project directory differs from the requested temp_path
@@ -78,6 +78,13 @@ class ProjectClient:
78
78
  )
79
79
  return ProjectStatusResponse.model_validate(response.json())
80
80
 
81
+ async def create_doctor_project(self) -> ProjectStatusResponse:
82
+ """Create the server-generated disposable project used by doctor."""
83
+ from basic_memory.mcp.tools.utils import call_post
84
+
85
+ response = await call_post(self.http_client, "/v2/projects/doctor")
86
+ return ProjectStatusResponse.model_validate(response.json())
87
+
81
88
  async def delete_project(
82
89
  self, project_external_id: str, delete_notes: bool = False
83
90
  ) -> ProjectStatusResponse:
@@ -4,6 +4,7 @@ import asyncio
4
4
  import json
5
5
  import os
6
6
  import shutil
7
+ import uuid
7
8
  from collections.abc import Callable
8
9
  from datetime import datetime
9
10
  from pathlib import Path
@@ -190,7 +191,9 @@ class ProjectService:
190
191
  # Not nested in either direction
191
192
  return False
192
193
 
193
- async def add_project(self, name: str, path: str, set_default: bool = False) -> None:
194
+ async def add_project(
195
+ self, name: str, path: str, set_default: bool = False, *, allow_nested: bool = False
196
+ ) -> None:
194
197
  """Add a new project to the configuration and database.
195
198
 
196
199
  Args:
@@ -240,27 +243,25 @@ class ProjectService:
240
243
  f"In cloud mode, paths are normalized to lowercase to prevent case-sensitivity issues."
241
244
  ) # pragma: no cover
242
245
 
243
- # Check for nested paths with existing projects
244
- for existing in existing_projects:
245
- if self._check_nested_paths(resolved_path, existing.path):
246
- # Determine which path is nested within which for appropriate error message
247
- p_new = Path(resolved_path).resolve()
248
- p_existing = Path(existing.path).resolve()
249
-
250
- # Check if new path is nested under existing project
251
- if p_new.is_relative_to(p_existing):
252
- raise ValueError(
253
- f"Cannot create project at '{resolved_path}': "
254
- f"path is nested within existing project '{existing.name}' at '{existing.path}'. "
255
- f"Projects cannot share directory trees."
256
- )
257
- else:
258
- # Existing project is nested under new path
259
- raise ValueError(
260
- f"Cannot create project at '{resolved_path}': "
261
- f"existing project '{existing.name}' at '{existing.path}' is nested within this path. "
262
- f"Projects cannot share directory trees."
263
- )
246
+ # Doctor creates a disposable project inside the configured project root.
247
+ # It must be allowed to coexist with the existing root project.
248
+ if not allow_nested:
249
+ for existing in existing_projects:
250
+ if self._check_nested_paths(resolved_path, existing.path):
251
+ p_new = Path(resolved_path).resolve()
252
+ p_existing = Path(existing.path).resolve()
253
+ if p_new.is_relative_to(p_existing):
254
+ raise ValueError(
255
+ f"Cannot create project at '{resolved_path}': "
256
+ f"path is nested within existing project '{existing.name}' at '{existing.path}'. "
257
+ f"Projects cannot share directory trees."
258
+ )
259
+ else:
260
+ raise ValueError(
261
+ f"Cannot create project at '{resolved_path}': "
262
+ f"existing project '{existing.name}' at '{existing.path}' is nested within this path. "
263
+ f"Projects cannot share directory trees."
264
+ )
264
265
 
265
266
  # Ensure the project directory exists on disk.
266
267
  # Trigger: project_root not set means local filesystem mode (not S3/cloud)
@@ -332,6 +333,19 @@ class ProjectService:
332
333
 
333
334
  logger.info(f"Project '{name}' added at {resolved_path}")
334
335
 
336
+ async def add_doctor_project(self) -> Project:
337
+ """Create a server-generated disposable project under the configured root."""
338
+ project_root = self.config_manager.config.project_root
339
+ if not project_root:
340
+ raise ValueError("Doctor projects require BASIC_MEMORY_PROJECT_ROOT")
341
+
342
+ project_name = f"doctor-{uuid.uuid4().hex[:8]}"
343
+ await self.add_project(project_name, project_root, allow_nested=True)
344
+ project = await self.get_project(project_name)
345
+ if project is None: # pragma: no cover
346
+ raise ValueError("Failed to retrieve doctor project")
347
+ return project
348
+
335
349
  async def remove_project(self, name: str, delete_notes: bool = False) -> None:
336
350
  """Remove a project from configuration and database.
337
351
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: basic-memory
3
- Version: 0.23.1.dev24
3
+ Version: 0.23.1.dev27
4
4
  Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
5
5
  Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
6
6
  Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
@@ -56,7 +56,7 @@ basic_memory/api/v2/routers/importer_router.py,sha256=LHdi0q7FKKTeynMl4qg4itOw51
56
56
  basic_memory/api/v2/routers/inspect_router.py,sha256=4QDM89AzWShF1UTE6CDjX1R8_4aNs3DIm4SpUUsbWzA,8637
57
57
  basic_memory/api/v2/routers/knowledge_router.py,sha256=-irLLXtYTUtlbqjHUwN6OhWn1jG8SN9mbeUp3uOUpwk,49572
58
58
  basic_memory/api/v2/routers/memory_router.py,sha256=2BvV7zA_39Ld3ySjY5twy7AzaMncn70fUVN7IdhGX_o,7308
59
- basic_memory/api/v2/routers/project_router.py,sha256=8pHBw3UmVx0CPWr1eCJWHquK0oV3LzTOGTYBtohzFi4,24886
59
+ basic_memory/api/v2/routers/project_router.py,sha256=LGXW0ijvz68jZw0I5AWpXKfAOUgu2egDQymMFGJql88,25737
60
60
  basic_memory/api/v2/routers/prompt_router.py,sha256=QE69XMKE6MB-yhzvr1mONkkeBZwKA_Ui9Kt-5ZVKvdY,10449
61
61
  basic_memory/api/v2/routers/resource_router.py,sha256=t_tXxqGgxYJqcPFKU9jbFBhJzSIjTnfzo6SQMGFV2Mo,7571
62
62
  basic_memory/api/v2/routers/schema_router.py,sha256=B_T2OXXFKvC-VaBzzDa2IbBmIHnJr_fPN43_8n9rMYo,22254
@@ -77,7 +77,7 @@ basic_memory/cli/commands/ci.py,sha256=MW4pepm9472iMzbryA4p-r-SG-OPnFftshB-0S8LB
77
77
  basic_memory/cli/commands/command_utils.py,sha256=9_xUeeg1-nsnUw3MdNXOc_rhVROqhXqEDcTD3E_ga3A,5008
78
78
  basic_memory/cli/commands/config.py,sha256=8IBqPrcgklhvHkVFG8v9laO0PqZkvfLsZ_jgbPkFCb4,8700
79
79
  basic_memory/cli/commands/db.py,sha256=aQcjHpff15pfpDTKKpV-zz38TIJNfkOFQsCWggqbbU8,22596
80
- basic_memory/cli/commands/doctor.py,sha256=84AifKWTxcCibUOkZvJQ8P7PKuPK_P36KLy5d6aNvsQ,10755
80
+ basic_memory/cli/commands/doctor.py,sha256=51fp0Keazaid-KPaIKaR9xKU9ELSALxFurvXKNuVnVo,11324
81
81
  basic_memory/cli/commands/format.py,sha256=pgXkk0ptU2quN0XzjURuapA5AqrIBu1y4j6J1t52ovI,6753
82
82
  basic_memory/cli/commands/hook.py,sha256=tNtGzf4E40bhZQL6-SJwQpq8ehQmxm7EIae7O7-3jJg,60641
83
83
  basic_memory/cli/commands/import_chatgpt.py,sha256=7SUGG_mmme0BuL4ov0lhb98VkTUldunCv-ehQirtwZE,3870
@@ -224,7 +224,7 @@ basic_memory/mcp/clients/directory.py,sha256=C_fY7xo300ceF-heRBAktv7OffEnNAuPWx1
224
224
  basic_memory/mcp/clients/inspect.py,sha256=TtiEOiJITDuFLtrdwMcoY-XBNuCK4C3NobzpQHdHoRU,2311
225
225
  basic_memory/mcp/clients/knowledge.py,sha256=DtEsAkqCtZFc-v93KIuPyOFCRLFbbOQGPqEwOSMJytw,13757
226
226
  basic_memory/mcp/clients/memory.py,sha256=qKSjKS7jJOKu7y-Lldsm561KU0G9OpRzA0tLf7l9FmI,4794
227
- basic_memory/mcp/clients/project.py,sha256=rYYtw58rSwuie_JIwWZ6Wdx2Y6JCQ70NNb79UNcf6hw,7604
227
+ basic_memory/mcp/clients/project.py,sha256=0er3M6QMjepuKpbn4G6oz7IuVf4veQhDTHJL6iUE09c,7955
228
228
  basic_memory/mcp/clients/resource.py,sha256=_FI7bOBAQwlEuv24wU1tsGIFR-riHcemgZSMJqOLBN4,2218
229
229
  basic_memory/mcp/clients/schema.py,sha256=OnLDJnh-8OkELdMVDRBObjOsGfgaDzroryjzSP8aWyc,3618
230
230
  basic_memory/mcp/clients/search.py,sha256=I7mJxVaQ8-igJzuMzxYaZgHGCwJAkjMHf2QIgYuZBjI,3030
@@ -385,14 +385,14 @@ basic_memory/services/note_content_reads.py,sha256=3o6GI2VJFkA5PScQD4IlZDO_4rcXL
385
385
  basic_memory/services/note_content_writes.py,sha256=wqnPnaUII2k_xlTBYMZAVlrc7zGX13M3el334F6z6hY,20530
386
386
  basic_memory/services/note_preparation.py,sha256=POGmk0L_9a-eLIMmXmuKtN81QRabF0JyipZiIz0eNqY,38262
387
387
  basic_memory/services/project_deletes.py,sha256=bnq8la1ZKsZEkcAg3Ew2onXHku9CWChr-BWDZA6lvuI,6010
388
- basic_memory/services/project_service.py,sha256=JB-pNRZfRhVGIRUlSt7IiI-BllxgFs8A7xtGK3klFcA,61803
388
+ basic_memory/services/project_service.py,sha256=E6rpEwKz6ihynZjXxBlvxUbG69YqE8jxOgBI3zT1XWQ,62460
389
389
  basic_memory/services/retrieval_inspect.py,sha256=gJq94oYV6ISeh-JeUAB8srMw8K3oGeMgzVXEOWAEzxA,23844
390
390
  basic_memory/services/search_service.py,sha256=8A1vXo5IIpKXL2gxEsCEP_tcKyRcnz4R5xb6zijRuW0,40254
391
391
  basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
392
392
  basic_memory/templates/prompts/continue_conversation.hbs,sha256=trrDHSXA5S0JCbInMoUJL04xvCGRB_ku1RHNQHtl6ZI,3076
393
393
  basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
394
- basic_memory-0.23.1.dev24.dist-info/METADATA,sha256=0aAtGPFW1HeEVmf3nfY_ctjaRL_KmF8iw_601_PGvZM,26848
395
- basic_memory-0.23.1.dev24.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
396
- basic_memory-0.23.1.dev24.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
397
- basic_memory-0.23.1.dev24.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
398
- basic_memory-0.23.1.dev24.dist-info/RECORD,,
394
+ basic_memory-0.23.1.dev27.dist-info/METADATA,sha256=j0HNhCqLZhDGmlKdwxHIf7eWLNDXeKyhOgF8UEzKrPI,26848
395
+ basic_memory-0.23.1.dev27.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
396
+ basic_memory-0.23.1.dev27.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
397
+ basic_memory-0.23.1.dev27.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
398
+ basic_memory-0.23.1.dev27.dist-info/RECORD,,