Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6329d05
feat: Implement API v2 with ID-based endpoints and v1 deprecation (Ph…
jope-bm Nov 19, 2025
f799ffb
fix: Complete v2 API project ID implementation
jope-bm Nov 20, 2025
d683a0e
feat: Add v2 project management endpoints
jope-bm Nov 20, 2025
8b339e4
test: Add comprehensive tests for v2 API endpoints
jope-bm Nov 21, 2025
d6d238c
feat: Add PostgreSQL database backend support (#439)
phernandez Nov 20, 2025
86ef63c
fix: Use create_search_repository factory for v2 API
jope-bm Nov 21, 2025
95862c0
fix: Normalize path comparison in v2 project router test for Windows
jope-bm Nov 21, 2025
23ecfbd
feat: Add v2 memory, search, and resource routers with tests
jope-bm Nov 21, 2025
fdb7da3
fix: Update v2 memory router tests - replace 'entities' with 'results'
jope-bm Nov 21, 2025
82c2ba3
fix: Fix v2 API test failures and add numeric ID support
jope-bm Nov 21, 2025
3f90e87
security: Add path traversal protection to v2 resource router
jope-bm Nov 21, 2025
a3800bc
security: Add path traversal protection to v1 resource router
jope-bm Nov 21, 2025
8b65d31
revert: Remove v1 resource router security changes
jope-bm Nov 21, 2025
3de403c
refactor: Remove deprecation middleware and metrics
jope-bm Nov 21, 2025
659b67e
refactor: Remove unused deprecation middleware files
jope-bm Nov 21, 2025
39719be
try to make sure claude always signs commits
jope-bm Nov 21, 2025
de78e23
fix: Handle Windows line endings and paths in tests
jope-bm Nov 21, 2025
c1e4b4e
Merge branch 'main' into feature/api-v2-migration
jope-bm Nov 21, 2025
897f43e
fix: Export all v2 routers from v2 module
jope-bm Nov 21, 2025
02482a7
fix: Use EntityResponseV2 in v2 knowledge router endpoints
jope-bm Nov 22, 2025
bb64d87
test: Update v2 knowledge router tests to verify id field is returned
jope-bm Nov 22, 2025
1a2d073
feat: Fix v2 API path structure and add directory endpoints
jope-bm Nov 24, 2025
94c56af
feat: Redesign v2 resource endpoints to use entity IDs
jope-bm Nov 24, 2025
8ae3e90
fix: Handle missing 'name' field in Claude conversations import
jope-bm Nov 24, 2025
e1174f3
feat: Add v2 prompt endpoints for continue-conversation and search
jope-bm Nov 24, 2025
887da3f
feat: Add v2 import endpoints for ChatGPT, Claude, and memory JSON
jope-bm Nov 24, 2025
231c795
feat: Add database IDs to all V2 API entity responses
jope-bm Nov 26, 2025
2bdcd74
fix: Correct V2 move endpoint to use entity ID in URL path
jope-bm Nov 26, 2025
7ef7d7f
Merge branch 'main' into feature/api-v2-migration
phernandez Nov 27, 2025
b16b9d9
Merge branch 'main' into feature/api-v2-migration
phernandez Nov 27, 2025
0f34994
update from main, fix search_index for postgres
phernandez Nov 27, 2025
c19b96f
Merge branch 'feature/api-v2-migration' of github.com:basicmachines-c…
phernandez Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ ENV/

# claude action
claude-output
**/.claude/settings.local.json
**/.claude/settings.local.json
.mcp.json
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,6 @@ With GitHub integration, the development workflow includes:
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
3. **Branch management** - Claude can create feature branches for implementations
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
5. **Code Commits**: ALWAYS sign off commits with `git commit -s`

This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.
25 changes: 22 additions & 3 deletions src/basic_memory/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
search,
prompt_router,
)
from basic_memory.api.v2.routers import (
knowledge_router as v2_knowledge,
project_router as v2_project,
memory_router as v2_memory,
search_router as v2_search,
resource_router as v2_resource,
directory_router as v2_directory,
prompt_router as v2_prompt,
importer_router as v2_importer,
)
from basic_memory.config import ConfigManager
from basic_memory.services.initialization import initialize_file_sync, initialize_app

Expand Down Expand Up @@ -66,8 +76,7 @@ async def lifespan(app: FastAPI): # pragma: no cover
lifespan=lifespan,
)


# Include routers
# Include v1 routers
app.include_router(knowledge.router, prefix="/{project}")
app.include_router(memory.router, prefix="/{project}")
app.include_router(resource.router, prefix="/{project}")
Expand All @@ -77,7 +86,17 @@ async def lifespan(app: FastAPI): # pragma: no cover
app.include_router(prompt_router.router, prefix="/{project}")
app.include_router(importer_router.router, prefix="/{project}")

# Project resource router works accross projects
# Include v2 routers (ID-based paths)
app.include_router(v2_knowledge, prefix="/v2/projects/{project_id}")
app.include_router(v2_memory, prefix="/v2/projects/{project_id}")
app.include_router(v2_search, prefix="/v2/projects/{project_id}")
app.include_router(v2_resource, prefix="/v2/projects/{project_id}")
app.include_router(v2_directory, prefix="/v2/projects/{project_id}")
app.include_router(v2_prompt, prefix="/v2/projects/{project_id}")
app.include_router(v2_importer, prefix="/v2/projects/{project_id}")
app.include_router(v2_project, prefix="/v2")

# Project resource router works across projects
app.include_router(project.project_resource_router)
app.include_router(management.router)

Expand Down
15 changes: 13 additions & 2 deletions src/basic_memory/api/routers/knowledge_router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""Router for knowledge graph operations."""
"""Router for knowledge graph operations.

⚠️ DEPRECATED: This v1 API is deprecated and will be removed on June 30, 2026.
Please migrate to /v2/{project}/knowledge endpoints which use entity IDs instead
of path-based identifiers for improved performance and stability.

Migration guide: See docs/migration/v1-to-v2.md
"""

from typing import Annotated

Expand All @@ -25,7 +32,11 @@
from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
from basic_memory.schemas.base import Permalink, Entity

router = APIRouter(prefix="/knowledge", tags=["knowledge"])
router = APIRouter(
prefix="/knowledge",
tags=["knowledge"],
deprecated=True, # Marks entire router as deprecated in OpenAPI docs
)


async def resolve_relations_background(sync_service, entity_id: int, entity_permalink: str) -> None:
Expand Down
58 changes: 50 additions & 8 deletions src/basic_memory/api/routers/project_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def get_project(
) # pragma: no cover

return ProjectItem(
id=found_project.id,
name=found_project.name,
path=normalize_project_path(found_project.path),
is_default=found_project.is_default or False,
Expand Down Expand Up @@ -80,9 +81,17 @@ async def update_project(
raise HTTPException(status_code=400, detail="Path must be absolute")

# Get original project info for the response
old_project = await project_service.get_project(name)
if not old_project:
raise HTTPException(
status_code=400, detail=f"Project '{name}' not found in configuration"
)

old_project_info = ProjectItem(
name=name,
path=project_service.projects.get(name, ""),
id=old_project.id,
name=old_project.name,
path=old_project.path,
is_default=old_project.is_default or False,
)

if path:
Expand All @@ -91,14 +100,21 @@ async def update_project(
await project_service.update_project(name, is_active=is_active)

# Get updated project info
updated_path = path if path else project_service.projects.get(name, "")
updated_project = await project_service.get_project(name)
if not updated_project:
raise HTTPException(status_code=404, detail=f"Project '{name}' not found after update")

return ProjectStatusResponse(
message=f"Project '{name}' updated successfully",
status="success",
default=(name == project_service.default_project),
old_project=old_project_info,
new_project=ProjectItem(name=name, path=updated_path),
new_project=ProjectItem(
id=updated_project.id,
name=updated_project.name,
path=updated_project.path,
is_default=updated_project.is_default or False,
),
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
Expand Down Expand Up @@ -186,6 +202,7 @@ async def list_projects(

project_items = [
ProjectItem(
id=project.id,
name=project.name,
path=normalize_project_path(project.path),
is_default=project.is_default or False,
Expand Down Expand Up @@ -232,6 +249,7 @@ async def add_project(
status="success",
default=existing_project.is_default or False,
new_project=ProjectItem(
id=existing_project.id,
name=existing_project.name,
path=existing_project.path,
is_default=existing_project.is_default or False,
Expand All @@ -250,12 +268,20 @@ async def add_project(
project_data.name, project_data.path, set_default=project_data.set_default
)

# Fetch the newly created project to get its ID
new_project = await project_service.get_project(project_data.name)
if not new_project:
raise HTTPException(status_code=500, detail="Failed to retrieve newly created project")

return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
message=f"Project '{project_data.name}' added successfully",
status="success",
default=project_data.set_default,
new_project=ProjectItem(
name=project_data.name, path=project_data.path, is_default=project_data.set_default
id=new_project.id,
name=new_project.name,
path=new_project.path,
is_default=new_project.is_default or False,
),
)
except ValueError as e: # pragma: no cover
Expand Down Expand Up @@ -306,7 +332,12 @@ async def remove_project(
message=f"Project '{name}' removed successfully",
status="success",
default=False,
old_project=ProjectItem(name=old_project.name, path=old_project.path),
old_project=ProjectItem(
id=old_project.id,
name=old_project.name,
path=old_project.path,
is_default=old_project.is_default or False,
),
new_project=None,
)
except ValueError as e: # pragma: no cover
Expand Down Expand Up @@ -349,8 +380,14 @@ async def set_default_project(
message=f"Project '{name}' set as default successfully",
status="success",
default=True,
old_project=ProjectItem(name=default_name, path=default_project.path),
old_project=ProjectItem(
id=default_project.id,
name=default_name,
path=default_project.path,
is_default=False,
),
new_project=ProjectItem(
id=new_default_project.id,
name=name,
path=new_default_project.path,
is_default=True,
Expand Down Expand Up @@ -378,7 +415,12 @@ async def get_default_project(
status_code=404, detail=f"Default Project: '{default_name}' does not exist"
)

return ProjectItem(name=default_project.name, path=default_project.path, is_default=True)
return ProjectItem(
id=default_project.id,
name=default_project.name,
path=default_project.path,
is_default=True,
)


# Synchronize projects between config and database
Expand Down
25 changes: 25 additions & 0 deletions src/basic_memory/api/routers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
match item.type:
case SearchItemType.ENTITY:
return EntitySummary(
entity_id=item.id,
title=item.title, # pyright: ignore
permalink=item.permalink,
content=item.content,
Expand All @@ -37,6 +38,8 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
)
case SearchItemType.OBSERVATION:
return ObservationSummary(
observation_id=item.id,
entity_id=item.entity_id, # pyright: ignore
title=item.title, # pyright: ignore
file_path=item.file_path,
category=item.category, # pyright: ignore
Expand All @@ -48,12 +51,16 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
from_entity = await entity_repository.find_by_id(item.from_id) # pyright: ignore
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
return RelationSummary(
relation_id=item.id,
entity_id=item.entity_id, # pyright: ignore
title=item.title, # pyright: ignore
file_path=item.file_path,
permalink=item.permalink, # pyright: ignore
relation_type=item.relation_type, # pyright: ignore
from_entity=from_entity.title if from_entity else None,
from_entity_id=item.from_id, # pyright: ignore
to_entity=to_entity.title if to_entity else None,
to_entity_id=item.to_id,
created_at=item.created_at,
)
case _: # pragma: no cover
Expand Down Expand Up @@ -111,6 +118,21 @@ async def to_search_results(entity_service: EntityService, results: List[SearchI
search_results = []
for r in results:
entities = await entity_service.get_entities_by_id([r.entity_id, r.from_id, r.to_id]) # pyright: ignore

# Determine which IDs to set based on type
entity_id = None
observation_id = None
relation_id = None

if r.type == SearchItemType.ENTITY:
entity_id = r.id
elif r.type == SearchItemType.OBSERVATION:
observation_id = r.id
entity_id = r.entity_id # Parent entity
elif r.type == SearchItemType.RELATION:
relation_id = r.id
entity_id = r.entity_id # Parent entity

search_results.append(
SearchResult(
title=r.title, # pyright: ignore
Expand All @@ -121,6 +143,9 @@ async def to_search_results(entity_service: EntityService, results: List[SearchI
content=r.content,
file_path=r.file_path,
metadata=r.metadata,
entity_id=entity_id,
observation_id=observation_id,
relation_id=relation_id,
category=r.category,
from_entity=entities[0].permalink if entities else None,
to_entity=entities[1].permalink if len(entities) > 1 else None,
Expand Down
35 changes: 35 additions & 0 deletions src/basic_memory/api/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""API v2 module - ID-based entity references.

Version 2 of the Basic Memory API uses integer entity IDs as the primary
identifier for improved performance and stability.

Key changes from v1:
- Entity lookups use integer IDs instead of paths/permalinks
- Direct database queries instead of cascading resolution
- Stable references that don't change with file moves
- Better caching support

All v2 routers are registered with the /v2 prefix.
"""

from basic_memory.api.v2.routers import (
knowledge_router,
memory_router,
project_router,
resource_router,
search_router,
directory_router,
prompt_router,
importer_router,
)

__all__ = [
"knowledge_router",
"memory_router",
"project_router",
"resource_router",
"search_router",
"directory_router",
"prompt_router",
"importer_router",
]
21 changes: 21 additions & 0 deletions src/basic_memory/api/v2/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""V2 API routers."""

from basic_memory.api.v2.routers.knowledge_router import router as knowledge_router
from basic_memory.api.v2.routers.project_router import router as project_router
from basic_memory.api.v2.routers.memory_router import router as memory_router
from basic_memory.api.v2.routers.search_router import router as search_router
from basic_memory.api.v2.routers.resource_router import router as resource_router
from basic_memory.api.v2.routers.directory_router import router as directory_router
from basic_memory.api.v2.routers.prompt_router import router as prompt_router
from basic_memory.api.v2.routers.importer_router import router as importer_router

__all__ = [
"knowledge_router",
"project_router",
"memory_router",
"search_router",
"resource_router",
"directory_router",
"prompt_router",
"importer_router",
]
Loading
Loading