Skip to content

Commit 0b20801

Browse files
phernandezclaude
andauthored
feat: add directory support to move_note and delete_note tools (#518)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8730067 commit 0b20801

63 files changed

Lines changed: 2148 additions & 585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,13 @@ See SPEC-16 for full context manager refactor details.
248248
- Basic Memory exposes these MCP tools to LLMs:
249249

250250
**Content Management:**
251-
- `write_note(title, content, folder, tags)` - Create/update markdown notes with semantic observations and relations
251+
- `write_note(title, content, directory, tags)` - Create/update markdown notes with semantic observations and relations
252252
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph awareness
253253
- `read_content(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
254254
- `view_note(identifier, page, page_size)` - View notes as formatted artifacts for better readability
255255
- `edit_note(identifier, operation, content)` - Edit notes incrementally (append, prepend, find/replace, replace_section)
256-
- `move_note(identifier, destination_path)` - Move notes to new locations, updating database and maintaining links
257-
- `delete_note(identifier)` - Delete notes from the knowledge base
256+
- `move_note(identifier, destination_path, is_directory)` - Move notes or directories to new locations, updating database and maintaining links
257+
- `delete_note(identifier, is_directory)` - Delete notes or directories from the knowledge base
258258

259259
**Knowledge Graph Navigation:**
260260
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation continuity
@@ -270,7 +270,7 @@ See SPEC-16 for full context manager refactor details.
270270
- `delete_project(project_name)` - Delete a project from configuration
271271

272272
**Visualization:**
273-
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
273+
- `canvas(nodes, edges, title, directory)` - Generate Obsidian canvas files for knowledge graph visualization
274274

275275
**ChatGPT-Compatible Tools:**
276276
- `search(query)` - Search across knowledge base (OpenAI actions compatible)

src/basic_memory/api/routers/importer_router.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
async def import_chatgpt(
2828
importer: ChatGPTImporterDep,
2929
file: UploadFile,
30-
folder: str = Form("conversations"),
30+
directory: str = Form("conversations"),
3131
) -> ChatImportResult:
3232
"""Import conversations from ChatGPT JSON export.
3333
3434
Args:
3535
file: The ChatGPT conversations.json file.
36-
folder: The folder to place the files in.
36+
directory: The directory to place the files in.
3737
markdown_processor: MarkdownProcessor instance.
3838
3939
Returns:
@@ -42,20 +42,20 @@ async def import_chatgpt(
4242
Raises:
4343
HTTPException: If import fails.
4444
"""
45-
return await import_file(importer, file, folder)
45+
return await import_file(importer, file, directory)
4646

4747

4848
@router.post("/claude/conversations", response_model=ChatImportResult)
4949
async def import_claude_conversations(
5050
importer: ClaudeConversationsImporterDep,
5151
file: UploadFile,
52-
folder: str = Form("conversations"),
52+
directory: str = Form("conversations"),
5353
) -> ChatImportResult:
5454
"""Import conversations from Claude conversations.json export.
5555
5656
Args:
5757
file: The Claude conversations.json file.
58-
folder: The folder to place the files in.
58+
directory: The directory to place the files in.
5959
markdown_processor: MarkdownProcessor instance.
6060
6161
Returns:
@@ -64,20 +64,20 @@ async def import_claude_conversations(
6464
Raises:
6565
HTTPException: If import fails.
6666
"""
67-
return await import_file(importer, file, folder)
67+
return await import_file(importer, file, directory)
6868

6969

7070
@router.post("/claude/projects", response_model=ProjectImportResult)
7171
async def import_claude_projects(
7272
importer: ClaudeProjectsImporterDep,
7373
file: UploadFile,
74-
folder: str = Form("projects"),
74+
directory: str = Form("projects"),
7575
) -> ProjectImportResult:
7676
"""Import projects from Claude projects.json export.
7777
7878
Args:
7979
file: The Claude projects.json file.
80-
base_folder: The base folder to place the files in.
80+
directory: The directory to place the files in.
8181
markdown_processor: MarkdownProcessor instance.
8282
8383
Returns:
@@ -86,20 +86,20 @@ async def import_claude_projects(
8686
Raises:
8787
HTTPException: If import fails.
8888
"""
89-
return await import_file(importer, file, folder)
89+
return await import_file(importer, file, directory)
9090

9191

9292
@router.post("/memory-json", response_model=EntityImportResult)
9393
async def import_memory_json(
9494
importer: MemoryJsonImporterDep,
9595
file: UploadFile,
96-
folder: str = Form("conversations"),
96+
directory: str = Form("conversations"),
9797
) -> EntityImportResult:
9898
"""Import entities and relations from a memory.json file.
9999
100100
Args:
101101
file: The memory.json file.
102-
destination_folder: Optional destination folder within the project.
102+
directory: Optional destination directory within the project.
103103
markdown_processor: MarkdownProcessor instance.
104104
105105
Returns:
@@ -116,7 +116,7 @@ async def import_memory_json(
116116
json_data = json.loads(line)
117117
file_data.append(json_data)
118118

119-
result = await importer.import_data(file_data, folder)
119+
result = await importer.import_data(file_data, directory)
120120
if not result.success: # pragma: no cover
121121
raise HTTPException(
122122
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

src/basic_memory/api/routers/knowledge_router.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
DeleteEntitiesResponse,
3030
DeleteEntitiesRequest,
3131
)
32-
from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
32+
from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest, MoveDirectoryRequest
33+
from basic_memory.schemas.response import DirectoryMoveResult
3334
from basic_memory.schemas.base import Permalink, Entity
3435

3536
router = APIRouter(
@@ -231,6 +232,50 @@ async def move_entity(
231232
raise HTTPException(status_code=400, detail=str(e))
232233

233234

235+
@router.post("/move-directory")
236+
async def move_directory(
237+
data: MoveDirectoryRequest,
238+
background_tasks: BackgroundTasks,
239+
entity_service: EntityServiceDep,
240+
project_config: ProjectConfigDep,
241+
app_config: AppConfigDep,
242+
search_service: SearchServiceDep,
243+
) -> DirectoryMoveResult:
244+
"""Move all entities in a directory to a new location.
245+
246+
This endpoint moves all files within a source directory to a destination
247+
directory, updating database records and optionally updating permalinks.
248+
"""
249+
logger.info(
250+
f"API request: endpoint='move_directory', source='{data.source_directory}', destination='{data.destination_directory}'"
251+
)
252+
253+
try:
254+
# Move the directory using the service
255+
result = await entity_service.move_directory(
256+
source_directory=data.source_directory,
257+
destination_directory=data.destination_directory,
258+
project_config=project_config,
259+
app_config=app_config,
260+
)
261+
262+
# Reindex moved entities
263+
for file_path in result.moved_files:
264+
entity = await entity_service.link_resolver.resolve_link(file_path)
265+
if entity:
266+
await search_service.index_entity(entity, background_tasks=background_tasks)
267+
268+
logger.info(
269+
f"API response: endpoint='move_directory', "
270+
f"total={result.total_files}, success={result.successful_moves}, failed={result.failed_moves}"
271+
)
272+
return result
273+
274+
except Exception as e:
275+
logger.error(f"Error moving directory: {e}")
276+
raise HTTPException(status_code=400, detail=str(e))
277+
278+
234279
## Read endpoints
235280

236281

src/basic_memory/api/v2/routers/importer_router.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ async def import_chatgpt(
3232
importer: ChatGPTImporterV2ExternalDep,
3333
file: UploadFile,
3434
project_id: str = Path(..., description="Project external UUID"),
35-
folder: str = Form("conversations"),
35+
directory: str = Form("conversations"),
3636
) -> ChatImportResult:
3737
"""Import conversations from ChatGPT JSON export.
3838
3939
Args:
4040
project_id: Project external UUID from URL path
4141
file: The ChatGPT conversations.json file.
42-
folder: The folder to place the files in.
42+
directory: The directory to place the files in.
4343
importer: ChatGPT importer instance.
4444
4545
Returns:
@@ -49,22 +49,22 @@ async def import_chatgpt(
4949
HTTPException: If import fails.
5050
"""
5151
logger.info(f"V2 Importing ChatGPT conversations for project {project_id}")
52-
return await import_file(importer, file, folder)
52+
return await import_file(importer, file, directory)
5353

5454

5555
@router.post("/claude/conversations", response_model=ChatImportResult)
5656
async def import_claude_conversations(
5757
importer: ClaudeConversationsImporterV2ExternalDep,
5858
file: UploadFile,
5959
project_id: str = Path(..., description="Project external UUID"),
60-
folder: str = Form("conversations"),
60+
directory: str = Form("conversations"),
6161
) -> ChatImportResult:
6262
"""Import conversations from Claude conversations.json export.
6363
6464
Args:
6565
project_id: Project external UUID from URL path
6666
file: The Claude conversations.json file.
67-
folder: The folder to place the files in.
67+
directory: The directory to place the files in.
6868
importer: Claude conversations importer instance.
6969
7070
Returns:
@@ -74,22 +74,22 @@ async def import_claude_conversations(
7474
HTTPException: If import fails.
7575
"""
7676
logger.info(f"V2 Importing Claude conversations for project {project_id}")
77-
return await import_file(importer, file, folder)
77+
return await import_file(importer, file, directory)
7878

7979

8080
@router.post("/claude/projects", response_model=ProjectImportResult)
8181
async def import_claude_projects(
8282
importer: ClaudeProjectsImporterV2ExternalDep,
8383
file: UploadFile,
8484
project_id: str = Path(..., description="Project external UUID"),
85-
folder: str = Form("projects"),
85+
directory: str = Form("projects"),
8686
) -> ProjectImportResult:
8787
"""Import projects from Claude projects.json export.
8888
8989
Args:
9090
project_id: Project external UUID from URL path
9191
file: The Claude projects.json file.
92-
folder: The base folder to place the files in.
92+
directory: The base directory to place the files in.
9393
importer: Claude projects importer instance.
9494
9595
Returns:
@@ -99,22 +99,22 @@ async def import_claude_projects(
9999
HTTPException: If import fails.
100100
"""
101101
logger.info(f"V2 Importing Claude projects for project {project_id}")
102-
return await import_file(importer, file, folder)
102+
return await import_file(importer, file, directory)
103103

104104

105105
@router.post("/memory-json", response_model=EntityImportResult)
106106
async def import_memory_json(
107107
importer: MemoryJsonImporterV2ExternalDep,
108108
file: UploadFile,
109109
project_id: str = Path(..., description="Project external UUID"),
110-
folder: str = Form("conversations"),
110+
directory: str = Form("conversations"),
111111
) -> EntityImportResult:
112112
"""Import entities and relations from a memory.json file.
113113
114114
Args:
115115
project_id: Project external UUID from URL path
116116
file: The memory.json file.
117-
folder: Optional destination folder within the project.
117+
directory: Optional destination directory within the project.
118118
importer: Memory JSON importer instance.
119119
120120
Returns:
@@ -132,7 +132,7 @@ async def import_memory_json(
132132
json_data = json.loads(line)
133133
file_data.append(json_data)
134134

135-
result = await importer.import_data(file_data, folder)
135+
result = await importer.import_data(file_data, directory)
136136
if not result.success: # pragma: no cover
137137
raise HTTPException(
138138
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -147,13 +147,13 @@ async def import_memory_json(
147147
return result
148148

149149

150-
async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
150+
async def import_file(importer: Importer, file: UploadFile, destination_directory: str):
151151
"""Helper function to import a file using an importer instance.
152152
153153
Args:
154154
importer: The importer instance to use
155155
file: The file to import
156-
destination_folder: Destination folder for imported content
156+
destination_directory: Destination directory for imported content
157157
158158
Returns:
159159
Import result from the importer
@@ -164,7 +164,7 @@ async def import_file(importer: Importer, file: UploadFile, destination_folder:
164164
try:
165165
# Process file
166166
json_data = json.load(file.file)
167-
result = await importer.import_data(json_data, destination_folder)
167+
result = await importer.import_data(json_data, destination_directory)
168168
if not result.success: # pragma: no cover
169169
raise HTTPException(
170170
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

0 commit comments

Comments
 (0)