basic-memory 0.7.0__py3-none-any.whl → 0.17.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.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +87 -20
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +180 -23
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- basic_memory/api/routers/project_router.py +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +155 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""V2 Resource Router - ID-based resource content operations.
|
|
2
|
+
|
|
3
|
+
This router uses entity external_ids (UUIDs) for all operations, with file paths
|
|
4
|
+
in request bodies when needed. This is consistent with v2's external_id-first design.
|
|
5
|
+
|
|
6
|
+
Key differences from v1:
|
|
7
|
+
- Uses UUID external_ids in URL paths instead of integer IDs or file paths
|
|
8
|
+
- File paths are in request bodies for create/update operations
|
|
9
|
+
- More RESTful: POST for create, PUT for update, GET for read
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path as PathLib
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, HTTPException, Response, Path
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from basic_memory.deps import (
|
|
18
|
+
ProjectConfigV2ExternalDep,
|
|
19
|
+
FileServiceV2ExternalDep,
|
|
20
|
+
EntityRepositoryV2ExternalDep,
|
|
21
|
+
SearchServiceV2ExternalDep,
|
|
22
|
+
)
|
|
23
|
+
from basic_memory.models.knowledge import Entity as EntityModel
|
|
24
|
+
from basic_memory.schemas.v2.resource import (
|
|
25
|
+
CreateResourceRequest,
|
|
26
|
+
UpdateResourceRequest,
|
|
27
|
+
ResourceResponse,
|
|
28
|
+
)
|
|
29
|
+
from basic_memory.utils import validate_project_path
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/resource", tags=["resources-v2"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get("/{entity_id}")
|
|
35
|
+
async def get_resource_content(
|
|
36
|
+
config: ProjectConfigV2ExternalDep,
|
|
37
|
+
entity_repository: EntityRepositoryV2ExternalDep,
|
|
38
|
+
file_service: FileServiceV2ExternalDep,
|
|
39
|
+
project_id: str = Path(..., description="Project external UUID"),
|
|
40
|
+
entity_id: str = Path(..., description="Entity external UUID"),
|
|
41
|
+
) -> Response:
|
|
42
|
+
"""Get raw resource content by entity external_id.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
project_id: Project external UUID from URL path
|
|
46
|
+
entity_id: Entity external UUID
|
|
47
|
+
config: Project configuration
|
|
48
|
+
entity_repository: Entity repository for fetching entity data
|
|
49
|
+
file_service: File service for reading file content
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Response with entity content
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
HTTPException: 404 if entity or file not found
|
|
56
|
+
"""
|
|
57
|
+
logger.debug(f"V2 Getting content for project {project_id}, entity_id: {entity_id}")
|
|
58
|
+
|
|
59
|
+
# Get entity by external_id
|
|
60
|
+
entity = await entity_repository.get_by_external_id(entity_id)
|
|
61
|
+
if not entity:
|
|
62
|
+
raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
|
|
63
|
+
|
|
64
|
+
# Validate entity file path to prevent path traversal
|
|
65
|
+
project_path = PathLib(config.home)
|
|
66
|
+
if not validate_project_path(entity.file_path, project_path):
|
|
67
|
+
logger.error( # pragma: no cover
|
|
68
|
+
f"Invalid file path in entity {entity.id}: {entity.file_path}"
|
|
69
|
+
)
|
|
70
|
+
raise HTTPException( # pragma: no cover
|
|
71
|
+
status_code=500,
|
|
72
|
+
detail="Entity contains invalid file path",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Check file exists via file_service (for cloud compatibility)
|
|
76
|
+
if not await file_service.exists(entity.file_path):
|
|
77
|
+
raise HTTPException( # pragma: no cover
|
|
78
|
+
status_code=404,
|
|
79
|
+
detail=f"File not found: {entity.file_path}",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Read content via file_service as bytes (works with both local and S3)
|
|
83
|
+
content = await file_service.read_file_bytes(entity.file_path)
|
|
84
|
+
content_type = file_service.content_type(entity.file_path)
|
|
85
|
+
|
|
86
|
+
return Response(content=content, media_type=content_type)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.post("", response_model=ResourceResponse)
|
|
90
|
+
async def create_resource(
|
|
91
|
+
data: CreateResourceRequest,
|
|
92
|
+
config: ProjectConfigV2ExternalDep,
|
|
93
|
+
file_service: FileServiceV2ExternalDep,
|
|
94
|
+
entity_repository: EntityRepositoryV2ExternalDep,
|
|
95
|
+
search_service: SearchServiceV2ExternalDep,
|
|
96
|
+
project_id: str = Path(..., description="Project external UUID"),
|
|
97
|
+
) -> ResourceResponse:
|
|
98
|
+
"""Create a new resource file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
project_id: Project external UUID from URL path
|
|
102
|
+
data: Create resource request with file_path and content
|
|
103
|
+
config: Project configuration
|
|
104
|
+
file_service: File service for writing files
|
|
105
|
+
entity_repository: Entity repository for creating entities
|
|
106
|
+
search_service: Search service for indexing
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ResourceResponse with file information including entity_id and external_id
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
HTTPException: 400 for invalid file paths, 409 if file already exists
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
# Validate path to prevent path traversal attacks
|
|
116
|
+
project_path = PathLib(config.home)
|
|
117
|
+
if not validate_project_path(data.file_path, project_path):
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Invalid file path attempted: {data.file_path} in project {config.name}"
|
|
120
|
+
)
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=400,
|
|
123
|
+
detail=f"Invalid file path: {data.file_path}. "
|
|
124
|
+
"Path must be relative and stay within project boundaries.",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Check if entity already exists
|
|
128
|
+
existing_entity = await entity_repository.get_by_file_path(data.file_path)
|
|
129
|
+
if existing_entity:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=409,
|
|
132
|
+
detail=f"Resource already exists at {data.file_path} with entity_id {existing_entity.external_id}. "
|
|
133
|
+
f"Use PUT /resource/{existing_entity.external_id} to update it.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Cloud compatibility: avoid assuming a local filesystem path.
|
|
137
|
+
# Delegate directory creation + writes to FileService (local or S3).
|
|
138
|
+
await file_service.ensure_directory(PathLib(data.file_path).parent)
|
|
139
|
+
checksum = await file_service.write_file(data.file_path, data.content)
|
|
140
|
+
|
|
141
|
+
# Get file info
|
|
142
|
+
file_metadata = await file_service.get_file_metadata(data.file_path)
|
|
143
|
+
|
|
144
|
+
# Determine file details
|
|
145
|
+
file_name = PathLib(data.file_path).name
|
|
146
|
+
content_type = file_service.content_type(data.file_path)
|
|
147
|
+
entity_type = "canvas" if data.file_path.endswith(".canvas") else "file"
|
|
148
|
+
|
|
149
|
+
# Create a new entity model
|
|
150
|
+
entity = EntityModel(
|
|
151
|
+
title=file_name,
|
|
152
|
+
entity_type=entity_type,
|
|
153
|
+
content_type=content_type,
|
|
154
|
+
file_path=data.file_path,
|
|
155
|
+
checksum=checksum,
|
|
156
|
+
created_at=file_metadata.created_at,
|
|
157
|
+
updated_at=file_metadata.modified_at,
|
|
158
|
+
)
|
|
159
|
+
entity = await entity_repository.add(entity)
|
|
160
|
+
|
|
161
|
+
# Index the file for search
|
|
162
|
+
await search_service.index_entity(entity) # pyright: ignore
|
|
163
|
+
|
|
164
|
+
# Return success response
|
|
165
|
+
return ResourceResponse(
|
|
166
|
+
entity_id=entity.id,
|
|
167
|
+
external_id=entity.external_id,
|
|
168
|
+
file_path=data.file_path,
|
|
169
|
+
checksum=checksum,
|
|
170
|
+
size=file_metadata.size,
|
|
171
|
+
created_at=file_metadata.created_at.timestamp(),
|
|
172
|
+
modified_at=file_metadata.modified_at.timestamp(),
|
|
173
|
+
)
|
|
174
|
+
except HTTPException:
|
|
175
|
+
# Re-raise HTTP exceptions without wrapping
|
|
176
|
+
raise
|
|
177
|
+
except Exception as e: # pragma: no cover
|
|
178
|
+
logger.error(f"Error creating resource {data.file_path}: {e}")
|
|
179
|
+
raise HTTPException(status_code=500, detail=f"Failed to create resource: {str(e)}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.put("/{entity_id}", response_model=ResourceResponse)
|
|
183
|
+
async def update_resource(
|
|
184
|
+
data: UpdateResourceRequest,
|
|
185
|
+
config: ProjectConfigV2ExternalDep,
|
|
186
|
+
file_service: FileServiceV2ExternalDep,
|
|
187
|
+
entity_repository: EntityRepositoryV2ExternalDep,
|
|
188
|
+
search_service: SearchServiceV2ExternalDep,
|
|
189
|
+
project_id: str = Path(..., description="Project external UUID"),
|
|
190
|
+
entity_id: str = Path(..., description="Entity external UUID"),
|
|
191
|
+
) -> ResourceResponse:
|
|
192
|
+
"""Update an existing resource by entity external_id.
|
|
193
|
+
|
|
194
|
+
Can update content and optionally move the file to a new path.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
project_id: Project external UUID from URL path
|
|
198
|
+
entity_id: Entity external UUID of the resource to update
|
|
199
|
+
data: Update resource request with content and optional new file_path
|
|
200
|
+
config: Project configuration
|
|
201
|
+
file_service: File service for writing files
|
|
202
|
+
entity_repository: Entity repository for updating entities
|
|
203
|
+
search_service: Search service for indexing
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
ResourceResponse with updated file information
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
HTTPException: 404 if entity not found, 400 for invalid paths
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# Get existing entity by external_id
|
|
213
|
+
entity = await entity_repository.get_by_external_id(entity_id)
|
|
214
|
+
if not entity:
|
|
215
|
+
raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
|
|
216
|
+
|
|
217
|
+
# Determine target file path
|
|
218
|
+
target_file_path = data.file_path if data.file_path else entity.file_path
|
|
219
|
+
|
|
220
|
+
# Validate path to prevent path traversal attacks
|
|
221
|
+
project_path = PathLib(config.home)
|
|
222
|
+
if not validate_project_path(target_file_path, project_path):
|
|
223
|
+
logger.warning(
|
|
224
|
+
f"Invalid file path attempted: {target_file_path} in project {config.name}"
|
|
225
|
+
)
|
|
226
|
+
raise HTTPException(
|
|
227
|
+
status_code=400,
|
|
228
|
+
detail=f"Invalid file path: {target_file_path}. "
|
|
229
|
+
"Path must be relative and stay within project boundaries.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# If moving file, handle the move
|
|
233
|
+
if data.file_path and data.file_path != entity.file_path:
|
|
234
|
+
# Ensure new parent directory exists (no-op for S3)
|
|
235
|
+
await file_service.ensure_directory(PathLib(target_file_path).parent)
|
|
236
|
+
|
|
237
|
+
# If old file exists, remove it via file_service (for cloud compatibility)
|
|
238
|
+
if await file_service.exists(entity.file_path):
|
|
239
|
+
await file_service.delete_file(entity.file_path)
|
|
240
|
+
else:
|
|
241
|
+
# Ensure directory exists for in-place update
|
|
242
|
+
await file_service.ensure_directory(PathLib(target_file_path).parent)
|
|
243
|
+
|
|
244
|
+
# Write content to target file
|
|
245
|
+
checksum = await file_service.write_file(target_file_path, data.content)
|
|
246
|
+
|
|
247
|
+
# Get file info
|
|
248
|
+
file_metadata = await file_service.get_file_metadata(target_file_path)
|
|
249
|
+
|
|
250
|
+
# Determine file details
|
|
251
|
+
file_name = PathLib(target_file_path).name
|
|
252
|
+
content_type = file_service.content_type(target_file_path)
|
|
253
|
+
entity_type = "canvas" if target_file_path.endswith(".canvas") else "file"
|
|
254
|
+
|
|
255
|
+
# Update entity using internal ID
|
|
256
|
+
updated_entity = await entity_repository.update(
|
|
257
|
+
entity.id,
|
|
258
|
+
{
|
|
259
|
+
"title": file_name,
|
|
260
|
+
"entity_type": entity_type,
|
|
261
|
+
"content_type": content_type,
|
|
262
|
+
"file_path": target_file_path,
|
|
263
|
+
"checksum": checksum,
|
|
264
|
+
"updated_at": file_metadata.modified_at,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Index the updated file for search
|
|
269
|
+
await search_service.index_entity(updated_entity) # pyright: ignore
|
|
270
|
+
|
|
271
|
+
# Return success response
|
|
272
|
+
return ResourceResponse(
|
|
273
|
+
entity_id=entity.id,
|
|
274
|
+
external_id=entity.external_id,
|
|
275
|
+
file_path=target_file_path,
|
|
276
|
+
checksum=checksum,
|
|
277
|
+
size=file_metadata.size,
|
|
278
|
+
created_at=file_metadata.created_at.timestamp(),
|
|
279
|
+
modified_at=file_metadata.modified_at.timestamp(),
|
|
280
|
+
)
|
|
281
|
+
except HTTPException:
|
|
282
|
+
# Re-raise HTTP exceptions without wrapping
|
|
283
|
+
raise
|
|
284
|
+
except Exception as e: # pragma: no cover
|
|
285
|
+
logger.error(f"Error updating resource {entity_id}: {e}")
|
|
286
|
+
raise HTTPException(status_code=500, detail=f"Failed to update resource: {str(e)}")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""V2 router for search operations.
|
|
2
|
+
|
|
3
|
+
This router uses external_id UUIDs for stable, API-friendly routing.
|
|
4
|
+
V1 uses string-based project names which are less efficient and less stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, BackgroundTasks, Path
|
|
8
|
+
|
|
9
|
+
from basic_memory.api.routers.utils import to_search_results
|
|
10
|
+
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
|
11
|
+
from basic_memory.deps import SearchServiceV2ExternalDep, EntityServiceV2ExternalDep
|
|
12
|
+
|
|
13
|
+
# Note: No prefix here - it's added during registration as /v2/{project_id}/search
|
|
14
|
+
router = APIRouter(tags=["search"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.post("/search/", response_model=SearchResponse)
|
|
18
|
+
async def search(
|
|
19
|
+
query: SearchQuery,
|
|
20
|
+
search_service: SearchServiceV2ExternalDep,
|
|
21
|
+
entity_service: EntityServiceV2ExternalDep,
|
|
22
|
+
project_id: str = Path(..., description="Project external UUID"),
|
|
23
|
+
page: int = 1,
|
|
24
|
+
page_size: int = 10,
|
|
25
|
+
):
|
|
26
|
+
"""Search across all knowledge and documents in a project.
|
|
27
|
+
|
|
28
|
+
V2 uses external_id UUIDs for stable API references.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project_id: Project external UUID from URL path
|
|
32
|
+
query: Search query parameters (text, filters, etc.)
|
|
33
|
+
search_service: Search service scoped to project
|
|
34
|
+
entity_service: Entity service scoped to project
|
|
35
|
+
page: Page number for pagination
|
|
36
|
+
page_size: Number of results per page
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SearchResponse with paginated search results
|
|
40
|
+
"""
|
|
41
|
+
limit = page_size
|
|
42
|
+
offset = (page - 1) * page_size
|
|
43
|
+
results = await search_service.search(query, limit=limit, offset=offset)
|
|
44
|
+
search_results = await to_search_results(entity_service, results)
|
|
45
|
+
return SearchResponse(
|
|
46
|
+
results=search_results,
|
|
47
|
+
current_page=page,
|
|
48
|
+
page_size=page_size,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/search/reindex")
|
|
53
|
+
async def reindex(
|
|
54
|
+
background_tasks: BackgroundTasks,
|
|
55
|
+
search_service: SearchServiceV2ExternalDep,
|
|
56
|
+
project_id: str = Path(..., description="Project external UUID"),
|
|
57
|
+
):
|
|
58
|
+
"""Recreate and populate the search index for a project.
|
|
59
|
+
|
|
60
|
+
This is a background operation that rebuilds the search index
|
|
61
|
+
from scratch. Useful after bulk updates or if the index becomes
|
|
62
|
+
corrupted.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
project_id: Project external UUID from URL path
|
|
66
|
+
background_tasks: FastAPI background tasks handler
|
|
67
|
+
search_service: Search service scoped to project
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Status message indicating reindex has been initiated
|
|
71
|
+
"""
|
|
72
|
+
await search_service.reindex_all(background_tasks=background_tasks)
|
|
73
|
+
return {"status": "ok", "message": "Reindex initiated"}
|
basic_memory/cli/app.py
CHANGED
|
@@ -1,20 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
# Suppress Logfire "not configured" warning - we only use Logfire in cloud/server contexts
|
|
2
|
+
import os
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
os.environ.setdefault("LOGFIRE_IGNORE_NO_CONFIG", "1")
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
6
|
+
# Remove loguru's default handler IMMEDIATELY, before any other imports.
|
|
7
|
+
# This prevents DEBUG logs from appearing on stdout during module-level
|
|
8
|
+
# initialization (e.g., template_loader.TemplateLoader() logs at DEBUG level).
|
|
9
|
+
from loguru import logger
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
logger.remove()
|
|
12
|
+
|
|
13
|
+
from typing import Optional # noqa: E402
|
|
14
|
+
|
|
15
|
+
import typer # noqa: E402
|
|
16
|
+
|
|
17
|
+
from basic_memory.cli.container import CliContainer, set_container # noqa: E402
|
|
18
|
+
from basic_memory.config import init_cli_logging # noqa: E402
|
|
19
|
+
from basic_memory.telemetry import show_notice_if_needed, track_app_started # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool) -> None:
|
|
23
|
+
"""Show version and exit."""
|
|
24
|
+
if value: # pragma: no cover
|
|
25
|
+
import basic_memory
|
|
26
|
+
|
|
27
|
+
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
|
|
28
|
+
raise typer.Exit()
|
|
10
29
|
|
|
11
|
-
asyncio.run(db.run_migrations(config))
|
|
12
30
|
|
|
13
31
|
app = typer.Typer(name="basic-memory")
|
|
14
32
|
|
|
15
|
-
import_app = typer.Typer()
|
|
16
|
-
app.add_typer(import_app, name="import")
|
|
17
33
|
|
|
34
|
+
@app.callback()
|
|
35
|
+
def app_callback(
|
|
36
|
+
ctx: typer.Context,
|
|
37
|
+
version: Optional[bool] = typer.Option(
|
|
38
|
+
None,
|
|
39
|
+
"--version",
|
|
40
|
+
"-v",
|
|
41
|
+
help="Show version and exit.",
|
|
42
|
+
callback=version_callback,
|
|
43
|
+
is_eager=True,
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Basic Memory - Local-first personal knowledge management."""
|
|
47
|
+
|
|
48
|
+
# Initialize logging for CLI (file only, no stdout)
|
|
49
|
+
init_cli_logging()
|
|
50
|
+
|
|
51
|
+
# --- Composition Root ---
|
|
52
|
+
# Create container and read config (single point of config access)
|
|
53
|
+
container = CliContainer.create()
|
|
54
|
+
set_container(container)
|
|
18
55
|
|
|
19
|
-
|
|
56
|
+
# Show telemetry notice and track CLI startup
|
|
57
|
+
# Skip for 'mcp' command - it handles its own telemetry in lifespan
|
|
58
|
+
# Skip for 'telemetry' command - avoid issues when user is managing telemetry
|
|
59
|
+
if ctx.invoked_subcommand not in {"mcp", "telemetry"}:
|
|
60
|
+
show_notice_if_needed()
|
|
61
|
+
track_app_started("cli")
|
|
62
|
+
|
|
63
|
+
# Run initialization for commands that don't use the API
|
|
64
|
+
# Skip for 'mcp' command - it has its own lifespan that handles initialization
|
|
65
|
+
# Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py
|
|
66
|
+
# Skip for 'reset' command - it manages its own database lifecycle
|
|
67
|
+
skip_init_commands = {"mcp", "status", "sync", "project", "tool", "reset"}
|
|
68
|
+
if (
|
|
69
|
+
not version
|
|
70
|
+
and ctx.invoked_subcommand is not None
|
|
71
|
+
and ctx.invoked_subcommand not in skip_init_commands
|
|
72
|
+
):
|
|
73
|
+
from basic_memory.services.initialization import ensure_initialization
|
|
74
|
+
|
|
75
|
+
ensure_initialization(container.config)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## import
|
|
79
|
+
# Register sub-command groups
|
|
80
|
+
import_app = typer.Typer(help="Import data from various sources")
|
|
81
|
+
app.add_typer(import_app, name="import")
|
|
82
|
+
|
|
83
|
+
claude_app = typer.Typer(help="Import Conversations from Claude JSON export.")
|
|
20
84
|
import_app.add_typer(claude_app, name="claude")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## cloud
|
|
88
|
+
|
|
89
|
+
cloud_app = typer.Typer(help="Access Basic Memory Cloud")
|
|
90
|
+
app.add_typer(cloud_app, name="cloud")
|