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.
- basic_memory/api/v2/routers/project_router.py +22 -0
- basic_memory/cli/commands/doctor.py +19 -3
- basic_memory/mcp/clients/project.py +7 -0
- basic_memory/services/project_service.py +36 -22
- {basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/METADATA +1 -1
- {basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/RECORD +9 -9
- {basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/WHEEL +0 -0
- {basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/licenses/LICENSE +0 -0
|
@@ -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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
395
|
-
basic_memory-0.23.1.
|
|
396
|
-
basic_memory-0.23.1.
|
|
397
|
-
basic_memory-0.23.1.
|
|
398
|
-
basic_memory-0.23.1.
|
|
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,,
|
|
File without changes
|
{basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{basic_memory-0.23.1.dev24.dist-info → basic_memory-0.23.1.dev27.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|