basic-memory 0.1.0__py3-none-any.whl → 0.1.2__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 +1 -1
- basic_memory/alembic/README +1 -0
- basic_memory/alembic/env.py +75 -0
- basic_memory/alembic/migrations.py +29 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/api/__init__.py +2 -1
- basic_memory/api/app.py +26 -24
- basic_memory/api/routers/knowledge_router.py +28 -26
- basic_memory/api/routers/memory_router.py +17 -11
- basic_memory/api/routers/search_router.py +6 -12
- basic_memory/cli/__init__.py +1 -1
- basic_memory/cli/app.py +0 -1
- basic_memory/cli/commands/__init__.py +3 -3
- basic_memory/cli/commands/db.py +25 -0
- basic_memory/cli/commands/import_memory_json.py +35 -31
- basic_memory/cli/commands/mcp.py +20 -0
- basic_memory/cli/commands/status.py +10 -6
- basic_memory/cli/commands/sync.py +5 -56
- basic_memory/cli/main.py +5 -39
- basic_memory/config.py +3 -3
- basic_memory/db.py +19 -21
- basic_memory/deps.py +3 -4
- basic_memory/file_utils.py +36 -35
- basic_memory/markdown/entity_parser.py +13 -30
- basic_memory/markdown/markdown_processor.py +7 -7
- basic_memory/markdown/plugins.py +109 -123
- basic_memory/markdown/schemas.py +7 -8
- basic_memory/markdown/utils.py +70 -121
- basic_memory/mcp/__init__.py +1 -1
- basic_memory/mcp/async_client.py +0 -2
- basic_memory/mcp/server.py +3 -27
- basic_memory/mcp/tools/__init__.py +5 -3
- basic_memory/mcp/tools/knowledge.py +2 -2
- basic_memory/mcp/tools/memory.py +8 -4
- basic_memory/mcp/tools/search.py +2 -1
- basic_memory/mcp/tools/utils.py +1 -1
- basic_memory/models/__init__.py +1 -2
- basic_memory/models/base.py +3 -3
- basic_memory/models/knowledge.py +23 -60
- basic_memory/models/search.py +1 -1
- basic_memory/repository/__init__.py +5 -3
- basic_memory/repository/entity_repository.py +34 -98
- basic_memory/repository/relation_repository.py +0 -7
- basic_memory/repository/repository.py +2 -39
- basic_memory/repository/search_repository.py +21 -24
- basic_memory/schemas/__init__.py +4 -4
- basic_memory/schemas/base.py +21 -62
- basic_memory/schemas/delete.py +2 -3
- basic_memory/schemas/discovery.py +4 -1
- basic_memory/schemas/memory.py +12 -13
- basic_memory/schemas/request.py +4 -23
- basic_memory/schemas/response.py +10 -9
- basic_memory/schemas/search.py +4 -7
- basic_memory/services/__init__.py +2 -7
- basic_memory/services/context_service.py +116 -110
- basic_memory/services/entity_service.py +25 -62
- basic_memory/services/exceptions.py +1 -0
- basic_memory/services/file_service.py +73 -109
- basic_memory/services/link_resolver.py +9 -9
- basic_memory/services/search_service.py +22 -15
- basic_memory/services/service.py +3 -24
- basic_memory/sync/__init__.py +2 -2
- basic_memory/sync/file_change_scanner.py +3 -7
- basic_memory/sync/sync_service.py +38 -38
- basic_memory/sync/utils.py +6 -38
- basic_memory/sync/watch_service.py +26 -5
- basic_memory/utils.py +42 -33
- {basic_memory-0.1.0.dist-info → basic_memory-0.1.2.dist-info}/METADATA +2 -7
- basic_memory-0.1.2.dist-info/RECORD +78 -0
- basic_memory/cli/commands/init.py +0 -38
- basic_memory/mcp/main.py +0 -21
- basic_memory/mcp/tools/ai_edit.py +0 -84
- basic_memory/services/database_service.py +0 -158
- basic_memory-0.1.0.dist-info/RECORD +0 -75
- {basic_memory-0.1.0.dist-info → basic_memory-0.1.2.dist-info}/WHEEL +0 -0
- {basic_memory-0.1.0.dist-info → basic_memory-0.1.2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.1.0.dist-info → basic_memory-0.1.2.dist-info}/licenses/LICENSE +0 -0
basic_memory/services/service.py
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
"""Base service class."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from typing import TypeVar, Generic, List, Sequence
|
|
3
|
+
from typing import TypeVar, Generic
|
|
5
4
|
|
|
6
5
|
from basic_memory.models import Base
|
|
7
|
-
from basic_memory.repository.repository import Repository
|
|
8
6
|
|
|
9
7
|
T = TypeVar("T", bound=Base)
|
|
10
|
-
|
|
8
|
+
|
|
11
9
|
|
|
12
10
|
class BaseService(Generic[T]):
|
|
13
11
|
"""Base service that takes a repository."""
|
|
14
12
|
|
|
15
|
-
def __init__(self, repository
|
|
13
|
+
def __init__(self, repository):
|
|
16
14
|
"""Initialize service with repository."""
|
|
17
15
|
self.repository = repository
|
|
18
|
-
|
|
19
|
-
async def add(self, model: T) -> T:
|
|
20
|
-
"""Add model to repository."""
|
|
21
|
-
return await self.repository.add(model)
|
|
22
|
-
|
|
23
|
-
async def add_all(self, models: List[T]) -> Sequence[T]:
|
|
24
|
-
"""Add a List of models to repository."""
|
|
25
|
-
return await self.repository.add_all(models)
|
|
26
|
-
|
|
27
|
-
async def get_modified_since(self, since: datetime) -> Sequence[T]:
|
|
28
|
-
"""Get all items modified since the given timestamp.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
since: Datetime to search from
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
Sequence of items modified since the timestamp
|
|
35
|
-
"""
|
|
36
|
-
return await self.repository.find_modified_since(since)
|
basic_memory/sync/__init__.py
CHANGED
|
@@ -69,11 +69,7 @@ class FileChangeScanner:
|
|
|
69
69
|
rel_path = str(path.relative_to(directory))
|
|
70
70
|
content = path.read_text()
|
|
71
71
|
checksum = await compute_checksum(content)
|
|
72
|
-
|
|
73
|
-
if checksum: # Only store valid checksums
|
|
74
|
-
result.files[rel_path] = checksum
|
|
75
|
-
else:
|
|
76
|
-
result.errors[rel_path] = "Failed to compute checksum"
|
|
72
|
+
result.files[rel_path] = checksum
|
|
77
73
|
|
|
78
74
|
except Exception as e:
|
|
79
75
|
rel_path = str(path.relative_to(directory))
|
|
@@ -134,7 +130,7 @@ class FileChangeScanner:
|
|
|
134
130
|
logger.debug(f" Moved: {len(report.moves)}")
|
|
135
131
|
logger.debug(f" Deleted: {len(report.deleted)}")
|
|
136
132
|
|
|
137
|
-
if scan_result.errors:
|
|
133
|
+
if scan_result.errors: # pragma: no cover
|
|
138
134
|
logger.warning("Files skipped due to errors:")
|
|
139
135
|
for file_path, error in scan_result.errors.items():
|
|
140
136
|
logger.warning(f" {file_path}: {error}")
|
|
@@ -151,7 +147,7 @@ class FileChangeScanner:
|
|
|
151
147
|
"""
|
|
152
148
|
return {
|
|
153
149
|
r.file_path: FileState(
|
|
154
|
-
file_path=r.file_path, permalink=r.permalink, checksum=r.checksum
|
|
150
|
+
file_path=r.file_path, permalink=r.permalink, checksum=r.checksum or ""
|
|
155
151
|
)
|
|
156
152
|
for r in db_records
|
|
157
153
|
}
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
|
7
8
|
|
|
8
9
|
from basic_memory import file_utils
|
|
9
10
|
from basic_memory.markdown import EntityParser, EntityMarkdown
|
|
@@ -58,9 +59,6 @@ class SyncService:
|
|
|
58
59
|
for permalink in permalinks:
|
|
59
60
|
await self.search_service.delete_by_permalink(permalink)
|
|
60
61
|
|
|
61
|
-
else:
|
|
62
|
-
logger.debug(f"No entity found to delete: {file_path}")
|
|
63
|
-
|
|
64
62
|
async def sync(self, directory: Path) -> SyncReport:
|
|
65
63
|
"""Sync knowledge files with database."""
|
|
66
64
|
changes = await self.scanner.find_knowledge_changes(directory)
|
|
@@ -76,69 +74,63 @@ class SyncService:
|
|
|
76
74
|
entity.id, {"file_path": new_path, "checksum": changes.checksums[new_path]}
|
|
77
75
|
)
|
|
78
76
|
# update search index
|
|
79
|
-
|
|
77
|
+
if updated:
|
|
78
|
+
await self.search_service.index_entity(updated)
|
|
80
79
|
|
|
81
80
|
# Handle deletions next
|
|
82
81
|
# remove rows from db for files no longer present
|
|
83
|
-
for
|
|
84
|
-
await self.handle_entity_deletion(
|
|
82
|
+
for path in changes.deleted:
|
|
83
|
+
await self.handle_entity_deletion(path)
|
|
85
84
|
|
|
86
85
|
# Parse files that need updating
|
|
87
86
|
parsed_entities: Dict[str, EntityMarkdown] = {}
|
|
88
87
|
|
|
89
|
-
for
|
|
90
|
-
entity_markdown = await self.entity_parser.parse_file(directory /
|
|
91
|
-
parsed_entities[
|
|
88
|
+
for path in [*changes.new, *changes.modified]:
|
|
89
|
+
entity_markdown = await self.entity_parser.parse_file(directory / path)
|
|
90
|
+
parsed_entities[path] = entity_markdown
|
|
92
91
|
|
|
93
92
|
# First pass: Create/update entities
|
|
94
93
|
# entities will have a null checksum to indicate they are not complete
|
|
95
|
-
for
|
|
96
|
-
|
|
94
|
+
for path, entity_markdown in parsed_entities.items():
|
|
97
95
|
# Get unique permalink and update markdown if needed
|
|
98
96
|
permalink = await self.entity_service.resolve_permalink(
|
|
99
|
-
|
|
100
|
-
markdown=entity_markdown
|
|
97
|
+
Path(path), markdown=entity_markdown
|
|
101
98
|
)
|
|
102
99
|
|
|
103
100
|
if permalink != entity_markdown.frontmatter.permalink:
|
|
104
101
|
# Add/update permalink in frontmatter
|
|
105
|
-
logger.info(f"Adding permalink '{permalink}' to file: {
|
|
102
|
+
logger.info(f"Adding permalink '{permalink}' to file: {path}")
|
|
106
103
|
|
|
107
104
|
# update markdown
|
|
108
105
|
entity_markdown.frontmatter.metadata["permalink"] = permalink
|
|
109
|
-
|
|
106
|
+
|
|
110
107
|
# update file frontmatter
|
|
111
108
|
updated_checksum = await file_utils.update_frontmatter(
|
|
112
|
-
directory /
|
|
113
|
-
{"permalink": permalink}
|
|
109
|
+
directory / path, {"permalink": permalink}
|
|
114
110
|
)
|
|
115
111
|
|
|
116
112
|
# Update checksum in changes report since file was modified
|
|
117
|
-
changes.checksums[
|
|
118
|
-
|
|
113
|
+
changes.checksums[path] = updated_checksum
|
|
114
|
+
|
|
119
115
|
# if the file is new, create an entity
|
|
120
|
-
if
|
|
116
|
+
if path in changes.new:
|
|
121
117
|
# Create entity with final permalink
|
|
122
|
-
logger.debug(f"Creating new entity_markdown: {
|
|
123
|
-
await self.entity_service.create_entity_from_markdown(
|
|
124
|
-
file_path, entity_markdown
|
|
125
|
-
)
|
|
118
|
+
logger.debug(f"Creating new entity_markdown: {path}")
|
|
119
|
+
await self.entity_service.create_entity_from_markdown(Path(path), entity_markdown)
|
|
126
120
|
# otherwise we need to update the entity and observations
|
|
127
121
|
else:
|
|
128
|
-
logger.debug(f"Updating entity_markdown: {
|
|
122
|
+
logger.debug(f"Updating entity_markdown: {path}")
|
|
129
123
|
await self.entity_service.update_entity_and_observations(
|
|
130
|
-
|
|
124
|
+
Path(path), entity_markdown
|
|
131
125
|
)
|
|
132
126
|
|
|
133
127
|
# Second pass
|
|
134
|
-
for
|
|
135
|
-
logger.debug(f"Updating relations for: {
|
|
128
|
+
for path, entity_markdown in parsed_entities.items():
|
|
129
|
+
logger.debug(f"Updating relations for: {path}")
|
|
136
130
|
|
|
137
131
|
# Process relations
|
|
138
|
-
checksum = changes.checksums[
|
|
139
|
-
entity = await self.entity_service.update_entity_relations(
|
|
140
|
-
file_path, entity_markdown
|
|
141
|
-
)
|
|
132
|
+
checksum = changes.checksums[path]
|
|
133
|
+
entity = await self.entity_service.update_entity_relations(Path(path), entity_markdown)
|
|
142
134
|
|
|
143
135
|
# add to search index
|
|
144
136
|
await self.search_service.index_entity(entity)
|
|
@@ -152,14 +144,22 @@ class SyncService:
|
|
|
152
144
|
target_entity = await self.entity_service.link_resolver.resolve_link(relation.to_name)
|
|
153
145
|
# check we found a link that is not the source
|
|
154
146
|
if target_entity and target_entity.id != relation.from_id:
|
|
155
|
-
logger.debug(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Resolved forward reference: {relation.to_name} -> {target_entity.permalink}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
await self.relation_repository.update(
|
|
153
|
+
relation.id,
|
|
154
|
+
{
|
|
155
|
+
"to_id": target_entity.id,
|
|
156
|
+
"to_name": target_entity.title, # Update to actual title
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
except IntegrityError:
|
|
160
|
+
logger.debug(f"Ignoring duplicate relation {relation}")
|
|
160
161
|
|
|
161
162
|
# update search index
|
|
162
163
|
await self.search_service.index_entity(target_entity)
|
|
163
164
|
|
|
164
|
-
|
|
165
165
|
return changes
|
basic_memory/sync/utils.py
CHANGED
|
@@ -2,44 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Set, Dict, Optional
|
|
5
|
-
from watchfiles import Change
|
|
6
|
-
from basic_memory.services.file_service import FileService
|
|
7
|
-
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
class FileChange:
|
|
11
|
-
"""A change to a file detected by the watch service.
|
|
12
|
-
|
|
13
|
-
Attributes:
|
|
14
|
-
change_type: Type of change (added, modified, deleted)
|
|
15
|
-
path: Path to the file
|
|
16
|
-
checksum: File checksum (None for deleted files)
|
|
17
|
-
"""
|
|
18
|
-
change_type: Change
|
|
19
|
-
path: str
|
|
20
|
-
checksum: Optional[str] = None
|
|
6
|
+
from watchfiles import Change
|
|
21
7
|
|
|
22
|
-
@classmethod
|
|
23
|
-
async def from_path(cls, path: str, change_type: Change, file_service: FileService) -> "FileChange":
|
|
24
|
-
"""Create FileChange from a path, computing checksum if file exists.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
path: Path to the file
|
|
28
|
-
change_type: Type of change detected
|
|
29
|
-
file_service: Service to read file and compute checksum
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
FileChange with computed checksum for non-deleted files
|
|
33
|
-
"""
|
|
34
|
-
file_path = file_service.path(path)
|
|
35
|
-
content, checksum = await file_service.read_file(file_path) if change_type != Change.deleted else (None, None)
|
|
36
|
-
return cls(path=file_path, change_type=change_type, checksum=checksum)
|
|
37
8
|
|
|
38
9
|
|
|
39
10
|
@dataclass
|
|
40
11
|
class SyncReport:
|
|
41
12
|
"""Report of file changes found compared to database state.
|
|
42
|
-
|
|
13
|
+
|
|
43
14
|
Attributes:
|
|
44
15
|
total: Total number of files in directory being synced
|
|
45
16
|
new: Files that exist on disk but not in database
|
|
@@ -48,19 +19,16 @@ class SyncReport:
|
|
|
48
19
|
moves: Files that have been moved from one location to another
|
|
49
20
|
checksums: Current checksums for files on disk
|
|
50
21
|
"""
|
|
22
|
+
|
|
51
23
|
total: int = 0
|
|
24
|
+
# We keep paths as strings in sets/dicts for easier serialization
|
|
52
25
|
new: Set[str] = field(default_factory=set)
|
|
53
26
|
modified: Set[str] = field(default_factory=set)
|
|
54
27
|
deleted: Set[str] = field(default_factory=set)
|
|
55
|
-
moves: Dict[str, str] = field(default_factory=dict) # old_path -> new_path
|
|
56
|
-
checksums: Dict[str, str] = field(default_factory=dict)
|
|
28
|
+
moves: Dict[str, str] = field(default_factory=dict) # old_path -> new_path
|
|
29
|
+
checksums: Dict[str, str] = field(default_factory=dict) # path -> checksum
|
|
57
30
|
|
|
58
31
|
@property
|
|
59
32
|
def total_changes(self) -> int:
|
|
60
33
|
"""Total number of changes."""
|
|
61
34
|
return len(self.new) + len(self.modified) + len(self.deleted) + len(self.moves)
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def syned_files(self) -> int:
|
|
65
|
-
"""Total number of files synced."""
|
|
66
|
-
return len(self.new) + len(self.modified) + len(self.moves)
|
|
@@ -8,7 +8,6 @@ from datetime import datetime
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import List, Optional
|
|
10
10
|
|
|
11
|
-
from rich import box
|
|
12
11
|
from rich.console import Console
|
|
13
12
|
from rich.live import Live
|
|
14
13
|
from rich.table import Table
|
|
@@ -84,7 +83,7 @@ class WatchService:
|
|
|
84
83
|
|
|
85
84
|
def generate_table(self) -> Table:
|
|
86
85
|
"""Generate status display table"""
|
|
87
|
-
table = Table(
|
|
86
|
+
table = Table()
|
|
88
87
|
|
|
89
88
|
# Add status row
|
|
90
89
|
table.add_column("Status", style="cyan")
|
|
@@ -131,13 +130,36 @@ class WatchService:
|
|
|
131
130
|
|
|
132
131
|
return table
|
|
133
132
|
|
|
134
|
-
async def run(self):
|
|
133
|
+
async def run(self, console_status: bool = False): # pragma: no cover
|
|
135
134
|
"""Watch for file changes and sync them"""
|
|
135
|
+
logger.info("Watching for sync changes")
|
|
136
136
|
self.state.running = True
|
|
137
137
|
self.state.start_time = datetime.now()
|
|
138
138
|
await self.write_status()
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
if console_status:
|
|
141
|
+
with Live(self.generate_table(), refresh_per_second=4, console=self.console) as live:
|
|
142
|
+
try:
|
|
143
|
+
async for changes in awatch(
|
|
144
|
+
self.config.home,
|
|
145
|
+
watch_filter=self.filter_changes,
|
|
146
|
+
debounce=self.config.sync_delay,
|
|
147
|
+
recursive=True,
|
|
148
|
+
):
|
|
149
|
+
# Process changes
|
|
150
|
+
await self.handle_changes(self.config.home)
|
|
151
|
+
# Update display
|
|
152
|
+
live.update(self.generate_table())
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self.state.record_error(str(e))
|
|
156
|
+
await self.write_status()
|
|
157
|
+
raise
|
|
158
|
+
finally:
|
|
159
|
+
self.state.running = False
|
|
160
|
+
await self.write_status()
|
|
161
|
+
|
|
162
|
+
else:
|
|
141
163
|
try:
|
|
142
164
|
async for changes in awatch(
|
|
143
165
|
self.config.home,
|
|
@@ -148,7 +170,6 @@ class WatchService:
|
|
|
148
170
|
# Process changes
|
|
149
171
|
await self.handle_changes(self.config.home)
|
|
150
172
|
# Update display
|
|
151
|
-
live.update(self.generate_table())
|
|
152
173
|
|
|
153
174
|
except Exception as e:
|
|
154
175
|
self.state.record_error(str(e))
|
basic_memory/utils.py
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
1
|
"""Utility functions for basic-memory."""
|
|
2
|
+
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
4
|
-
import
|
|
5
|
+
import sys
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import Optional, Union
|
|
6
8
|
|
|
9
|
+
from loguru import logger
|
|
7
10
|
from unidecode import unidecode
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
def sanitize_name(name: str) -> str:
|
|
11
|
-
"""
|
|
12
|
-
Sanitize a name for filesystem use:
|
|
13
|
-
- Convert to lowercase
|
|
14
|
-
- Replace spaces/punctuation with underscores
|
|
15
|
-
- Remove emojis and other special characters
|
|
16
|
-
- Collapse multiple underscores
|
|
17
|
-
- Trim leading/trailing underscores
|
|
18
|
-
"""
|
|
19
|
-
# Normalize unicode to compose characters where possible
|
|
20
|
-
name = unicodedata.normalize("NFKD", name)
|
|
21
|
-
# Remove emojis and other special characters, keep only letters, numbers, spaces
|
|
22
|
-
name = "".join(c for c in name if c.isalnum() or c.isspace())
|
|
23
|
-
# Replace spaces with underscores
|
|
24
|
-
name = name.replace(" ", "_")
|
|
25
|
-
# Remove newline
|
|
26
|
-
name = name.replace("\n", "")
|
|
27
|
-
# Convert to lowercase
|
|
28
|
-
name = name.lower()
|
|
29
|
-
# Collapse multiple underscores and trim
|
|
30
|
-
name = re.sub(r"_+", "_", name).strip("_")
|
|
31
|
-
|
|
32
|
-
return name
|
|
12
|
+
from basic_memory.config import config
|
|
33
13
|
|
|
34
14
|
|
|
35
|
-
def generate_permalink(file_path: Path
|
|
15
|
+
def generate_permalink(file_path: Union[Path, str]) -> str:
|
|
36
16
|
"""Generate a stable permalink from a file path.
|
|
37
17
|
|
|
38
18
|
Args:
|
|
@@ -50,8 +30,11 @@ def generate_permalink(file_path: Path | str) -> str:
|
|
|
50
30
|
>>> generate_permalink("design/unified_model_refactor.md")
|
|
51
31
|
'design/unified-model-refactor'
|
|
52
32
|
"""
|
|
33
|
+
# Convert Path to string if needed
|
|
34
|
+
path_str = str(file_path)
|
|
35
|
+
|
|
53
36
|
# Remove extension
|
|
54
|
-
base = os.path.splitext(
|
|
37
|
+
base = os.path.splitext(path_str)[0]
|
|
55
38
|
|
|
56
39
|
# Transliterate unicode to ascii
|
|
57
40
|
ascii_text = unidecode(base)
|
|
@@ -63,16 +46,42 @@ def generate_permalink(file_path: Path | str) -> str:
|
|
|
63
46
|
lower_text = ascii_text.lower()
|
|
64
47
|
|
|
65
48
|
# replace underscores with hyphens
|
|
66
|
-
text_with_hyphens = lower_text.replace(
|
|
49
|
+
text_with_hyphens = lower_text.replace("_", "-")
|
|
67
50
|
|
|
68
51
|
# Replace remaining invalid chars with hyphens
|
|
69
|
-
clean_text = re.sub(r
|
|
52
|
+
clean_text = re.sub(r"[^a-z0-9/\-]", "-", text_with_hyphens)
|
|
70
53
|
|
|
71
54
|
# Collapse multiple hyphens
|
|
72
|
-
clean_text = re.sub(r
|
|
55
|
+
clean_text = re.sub(r"-+", "-", clean_text)
|
|
73
56
|
|
|
74
57
|
# Clean each path segment
|
|
75
|
-
segments = clean_text.split(
|
|
76
|
-
clean_segments = [s.strip(
|
|
58
|
+
segments = clean_text.split("/")
|
|
59
|
+
clean_segments = [s.strip("-") for s in segments]
|
|
60
|
+
|
|
61
|
+
return "/".join(clean_segments)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def setup_logging(home_dir: Path = config.home, log_file: Optional[str] = None) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Configure logging for the application.
|
|
67
|
+
"""
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
# Remove default handler and any existing handlers
|
|
70
|
+
logger.remove()
|
|
71
|
+
|
|
72
|
+
# Add file handler
|
|
73
|
+
if log_file:
|
|
74
|
+
log_path = home_dir / log_file
|
|
75
|
+
logger.add(
|
|
76
|
+
str(log_path), # loguru expects a string path
|
|
77
|
+
level=config.log_level,
|
|
78
|
+
rotation="100 MB",
|
|
79
|
+
retention="10 days",
|
|
80
|
+
backtrace=True,
|
|
81
|
+
diagnose=True,
|
|
82
|
+
enqueue=True,
|
|
83
|
+
colorize=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Add stderr handler
|
|
87
|
+
logger.add(sys.stderr, level=config.log_level, backtrace=True, diagnose=True, colorize=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -23,17 +23,12 @@ Requires-Dist: pydantic[email,timezone]>=2.10.3
|
|
|
23
23
|
Requires-Dist: pyright>=1.1.390
|
|
24
24
|
Requires-Dist: python-frontmatter>=1.1.0
|
|
25
25
|
Requires-Dist: pyyaml>=6.0.1
|
|
26
|
+
Requires-Dist: qasync>=0.27.1
|
|
26
27
|
Requires-Dist: rich>=13.9.4
|
|
27
28
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
28
29
|
Requires-Dist: typer>=0.9.0
|
|
29
30
|
Requires-Dist: unidecode>=1.3.8
|
|
30
31
|
Requires-Dist: watchfiles>=1.0.4
|
|
31
|
-
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
33
|
-
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
34
|
-
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
|
|
35
|
-
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
36
|
-
Requires-Dist: ruff>=0.1.6; extra == 'dev'
|
|
37
32
|
Description-Content-Type: text/markdown
|
|
38
33
|
|
|
39
34
|
# Basic Memory
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
basic_memory/__init__.py,sha256=_ij75bUYM3LqRQYHrJ1kLnDuUyauuHilEBF96OFw9hA,122
|
|
2
|
+
basic_memory/config.py,sha256=PZA2qgwKACvKfRcM3H-BPB_8FYVhgZAwTmlKJ3ROfhU,1643
|
|
3
|
+
basic_memory/db.py,sha256=BFZCp4aJ7Xj9_ZCMz0rnSBuCy5xIMvvWjSImmuKzdWg,4605
|
|
4
|
+
basic_memory/deps.py,sha256=UzivBw6e6iYcU_8SQ8LNCmSsmFyHfjdzfWvnfNzqbRc,5375
|
|
5
|
+
basic_memory/file_utils.py,sha256=gp7RCFWaddFnELIyTc1E19Rk8jJsrKshG2n8ZZR-kKA,5751
|
|
6
|
+
basic_memory/utils.py,sha256=HiLorP5_YCQeNeTcDqvnkrwY7OBaFRS3i_hdV9iWKLs,2374
|
|
7
|
+
basic_memory/alembic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
|
|
8
|
+
basic_memory/alembic/env.py,sha256=XqJVQhS41ba7NCPmmaSZ09_tbSLnwsY2bcpJpqx_ZTc,2107
|
|
9
|
+
basic_memory/alembic/migrations.py,sha256=CIbkMHEKZ60aDUhFGSQjv8kDNM7sazfvEYHGGcy1DBk,858
|
|
10
|
+
basic_memory/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
11
|
+
basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py,sha256=lTbWlAnd1es7xU99DoJgfaRe1_Kte8TL98riqeKGV80,4363
|
|
12
|
+
basic_memory/api/__init__.py,sha256=wCpj-21j1D0KzKl9Ql6unLBVFY0K1uGp_FeSZRKtqpk,72
|
|
13
|
+
basic_memory/api/app.py,sha256=AEHcslN4SBq5Ni7q7wkG4jDH0-SwMWV2DeTdaUSQKns,2083
|
|
14
|
+
basic_memory/api/routers/__init__.py,sha256=iviQ1QVYobC8huUuyRhEjcA0BDjrOUm1lXHXhJkxP9A,239
|
|
15
|
+
basic_memory/api/routers/knowledge_router.py,sha256=cMLhRczOfSRnsZdyR0bSS8PENPRTu70dlwaV27O34bs,5705
|
|
16
|
+
basic_memory/api/routers/memory_router.py,sha256=pF0GzmWoxmjhtxZM8jCmfLwqjey_fmXER5vYbD8fsQw,4556
|
|
17
|
+
basic_memory/api/routers/resource_router.py,sha256=_Gp5HSJr-L-GUkQKbEP2bAZvCY8Smd-sBNWpGyqXS4c,1056
|
|
18
|
+
basic_memory/api/routers/search_router.py,sha256=dCRnBbp3r966U8UYwgAaxZBbg7yX7pC8QJqagdACUi0,1086
|
|
19
|
+
basic_memory/cli/__init__.py,sha256=arcKLAWRDhPD7x5t80MlviZeYzwHZ0GZigyy3NKVoGk,33
|
|
20
|
+
basic_memory/cli/app.py,sha256=hF4MgYCgFql4J6qi3lguqc6HQdP2gm6PpvtSxKBSjZc,34
|
|
21
|
+
basic_memory/cli/main.py,sha256=Vvpmh33MSZJftCENEjzJH3yBbxD4B40Pl6IBIumiVX4,505
|
|
22
|
+
basic_memory/cli/commands/__init__.py,sha256=OQGLaKTsOdPsp2INM_pHzmOlbVfdL0sytBNgvqTqCDY,159
|
|
23
|
+
basic_memory/cli/commands/db.py,sha256=I92CRufPskvHl9c90f5Eg7U7D0uIzLBiwngQuAh5cLk,772
|
|
24
|
+
basic_memory/cli/commands/import_memory_json.py,sha256=ZXSRHH_3GgJzmMLvDulakKIpzsKxrZIUmEuWgJmwMOE,5138
|
|
25
|
+
basic_memory/cli/commands/mcp.py,sha256=a0v54iFL01_eykODHuWIupTHCn-COm-WZGdSO5iinc0,563
|
|
26
|
+
basic_memory/cli/commands/status.py,sha256=aNpP8u-ECoVTiL5MIb-D2cXXLJtv6z2z8CMCh5nt2KY,5782
|
|
27
|
+
basic_memory/cli/commands/sync.py,sha256=sb6OGl9IVZLmGfHUm0-aexD365BRTaHJhpwqt0O5yxk,7035
|
|
28
|
+
basic_memory/markdown/__init__.py,sha256=DdzioCWtDnKaq05BHYLgL_78FawEHLpLXnp-kPSVfIc,501
|
|
29
|
+
basic_memory/markdown/entity_parser.py,sha256=sJk8TRUd9cAaIjATiJn7dBQRorrYngRbd7MRVfc0Oc4,3781
|
|
30
|
+
basic_memory/markdown/markdown_processor.py,sha256=mV3pYoDTaQMEl1tA5n_XztBvNlYyH2SzKs4vnKdAet4,4952
|
|
31
|
+
basic_memory/markdown/plugins.py,sha256=gtIzKRjoZsyvBqLpVNnrmzl_cbTZ5ZGn8kcuXxQjRko,6639
|
|
32
|
+
basic_memory/markdown/schemas.py,sha256=mzVEDUhH98kwETMknjkKw5H697vg_zUapsJkJVi17ho,1894
|
|
33
|
+
basic_memory/markdown/utils.py,sha256=ZtHa-dG--ZwFEUC3jfl04KZGhM_ZWo5b-8d8KpJ90gY,2758
|
|
34
|
+
basic_memory/mcp/__init__.py,sha256=dsDOhKqjYeIbCULbHIxfcItTbqudEuEg1Np86eq0GEQ,35
|
|
35
|
+
basic_memory/mcp/async_client.py,sha256=Eo345wANiBRSM4u3j_Vd6Ax4YtMg7qbWd9PIoFfj61I,236
|
|
36
|
+
basic_memory/mcp/server.py,sha256=L92Vit7llaKT9NlPZfxdp67C33niObmRH2QFyUhmnD0,355
|
|
37
|
+
basic_memory/mcp/tools/__init__.py,sha256=MHZmWw016N0qbtC3f186Jg1tPzh2g88_ZsCKJ0oyrrs,873
|
|
38
|
+
basic_memory/mcp/tools/knowledge.py,sha256=2U8YUKCizsAETHCC1mBVKMfCEef6tlc_pa2wOmA9mD4,2016
|
|
39
|
+
basic_memory/mcp/tools/memory.py,sha256=gl4MBm9l2lMOfu_xmUqjoZacWSIHOAYZiAm8z7oDuY8,5203
|
|
40
|
+
basic_memory/mcp/tools/notes.py,sha256=4GKnhDK53UkeZtpZENQ9id9XdemKxLzGwMQJeuX-Kok,3772
|
|
41
|
+
basic_memory/mcp/tools/search.py,sha256=tx6aIuB2FWmmrvzu3RHSQvszlk-zHcwrWhkLLHWjuZc,1105
|
|
42
|
+
basic_memory/mcp/tools/utils.py,sha256=icm-Xyqw3GxooGYkXqjEjoZvIGy_Z3CPw-uUYBxR_YQ,4831
|
|
43
|
+
basic_memory/models/__init__.py,sha256=Bf0xXV_ryndogvZDiVM_Wb6iV2fHUxYNGMZNWNcZi0s,307
|
|
44
|
+
basic_memory/models/base.py,sha256=4hAXJ8CE1RnjKhb23lPd-QM7G_FXIdTowMJ9bRixspU,225
|
|
45
|
+
basic_memory/models/knowledge.py,sha256=R05mLr2GXDfUcmPe2ja20wvzP818b4npnxL1PvQooEY,5921
|
|
46
|
+
basic_memory/models/search.py,sha256=IB-ySJUqlQq9FqLGfWnraIFcB_brWa9eBwsQP1rVTeI,1164
|
|
47
|
+
basic_memory/repository/__init__.py,sha256=TnscLXARq2iOgQZFvQoT9X1Bn9SB_7s1xw2fOqRs3Jg,252
|
|
48
|
+
basic_memory/repository/entity_repository.py,sha256=VFLymzJ1W6AZru_s1S3U6nlqSprBrVV5Toy0-qysIfw,3524
|
|
49
|
+
basic_memory/repository/observation_repository.py,sha256=BOcy4wARqCXu-thYyt7mPxt2A2C8TW0le3s_X9wrK6I,1701
|
|
50
|
+
basic_memory/repository/relation_repository.py,sha256=DwpTcn9z_1sZQcyMOUABz1k1VSwo_AU63x2zR7aerTk,2933
|
|
51
|
+
basic_memory/repository/repository.py,sha256=jUScHWOfcB2FajwVZ2Sbjtg-gSI2Y2rhiIaTULjvmn8,11321
|
|
52
|
+
basic_memory/repository/search_repository.py,sha256=OfocJZ7EWum33klFFvsLE7BEUnZPda1BNSwrbkRiXko,9233
|
|
53
|
+
basic_memory/schemas/__init__.py,sha256=eVxrtuPT7-9JIQ7UDx2J8t8xlS3u0iUkV_VLNbzvxo4,1575
|
|
54
|
+
basic_memory/schemas/base.py,sha256=epSauNNVZ2lRLATf-HIzqeberq4ZBTgxliNmjitAsWc,5538
|
|
55
|
+
basic_memory/schemas/delete.py,sha256=UAR2JK99WMj3gP-yoGWlHD3eZEkvlTSRf8QoYIE-Wfw,1180
|
|
56
|
+
basic_memory/schemas/discovery.py,sha256=6Y2tUiv9f06rFTsa8_wTH2haS2bhCfuQh0uW33hwdd8,876
|
|
57
|
+
basic_memory/schemas/memory.py,sha256=mqslazV0lQswtbNgYv_y2-KxmifIvRlg5I3IuTTMnO4,2882
|
|
58
|
+
basic_memory/schemas/request.py,sha256=rt_guNWrUMePJvDmsh1g1dc7IqEY6K6mGXMKx8tBCj8,1614
|
|
59
|
+
basic_memory/schemas/response.py,sha256=2su3YP-gkbw4MvgGtgZLHEuTp6RuVlK736KakaV7fP4,6273
|
|
60
|
+
basic_memory/schemas/search.py,sha256=pWBA1-xEQ3rH8vLIgrQT4oygq9MMwr0B7VCbFafVVOw,3278
|
|
61
|
+
basic_memory/services/__init__.py,sha256=oop6SKmzV4_NAYt9otGnupLGVCCKIVgxEcdRQWwh25I,197
|
|
62
|
+
basic_memory/services/context_service.py,sha256=Bu1wVl9q3FDGbGChrLqgFGQW95-W1OfjNqq6SGljqWg,9388
|
|
63
|
+
basic_memory/services/entity_service.py,sha256=bm_Z63_AJmXiRQkVYWwoB3PYLMW1t1xS3Nh0Nm9SwiI,11538
|
|
64
|
+
basic_memory/services/exceptions.py,sha256=VGlCLd4UD2w5NWKqC7QpG4jOM_hA7jKRRM-MqvEVMNk,288
|
|
65
|
+
basic_memory/services/file_service.py,sha256=r4JfPY1wyenAH0Y-iq7vGHPwT616ayUWoLnvA1NuzpA,5695
|
|
66
|
+
basic_memory/services/link_resolver.py,sha256=VdhoPAVa65T6LW7kSTLWts55zbnnN481fr7VLz3HaXE,4513
|
|
67
|
+
basic_memory/services/search_service.py,sha256=iB-BgFwInrJxTfYBerj68QORlMv46wYy2-ceQx61Dd8,7839
|
|
68
|
+
basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
|
|
69
|
+
basic_memory/sync/__init__.py,sha256=ko0xLQv1S5U7sAOmIP2XKl03akVPzoY-a9m3TFPcMh4,193
|
|
70
|
+
basic_memory/sync/file_change_scanner.py,sha256=4whJej6t9sxwUp1ox93efJ0bBHSnAr6STpk_PsKU6to,5784
|
|
71
|
+
basic_memory/sync/sync_service.py,sha256=nAOX4N90lbpRJeq5tRR_7PYptIoWwhXMUljE7yrneF4,7087
|
|
72
|
+
basic_memory/sync/utils.py,sha256=uc7VLK34HufKyKavGwTPGU-ARfoQr_jYbjs4fsmUvuo,1233
|
|
73
|
+
basic_memory/sync/watch_service.py,sha256=CtKBrP1imI3ZSEgJl7Ffi-JZ_oDGKrhiyGgs41h5QYI,7563
|
|
74
|
+
basic_memory-0.1.2.dist-info/METADATA,sha256=QW_mPiSlc0TltyO1DCB5B5MXxiv-g5JOnZt2Y3LtV3o,7539
|
|
75
|
+
basic_memory-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
76
|
+
basic_memory-0.1.2.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
|
|
77
|
+
basic_memory-0.1.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
78
|
+
basic_memory-0.1.2.dist-info/RECORD,,
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Initialize command for basic-memory CLI."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from loguru import logger
|
|
8
|
-
|
|
9
|
-
from basic_memory.cli.app import app
|
|
10
|
-
from basic_memory.db import engine_session_factory, DatabaseType
|
|
11
|
-
from basic_memory.config import config
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def _init(force: bool = False):
|
|
15
|
-
"""Initialize the database."""
|
|
16
|
-
db_path = config.database_path
|
|
17
|
-
|
|
18
|
-
if db_path.exists() and not force:
|
|
19
|
-
typer.echo(f"Database already exists at {db_path}. Use --force to reinitialize.")
|
|
20
|
-
raise typer.Exit(1)
|
|
21
|
-
|
|
22
|
-
# Create data directory if needed
|
|
23
|
-
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
async with engine_session_factory(db_path, db_type=DatabaseType.FILESYSTEM, init=True):
|
|
27
|
-
typer.echo(f"Initialized database at {db_path}")
|
|
28
|
-
except Exception as e:
|
|
29
|
-
logger.error(f"Error initializing database: {e}")
|
|
30
|
-
typer.echo(f"Error initializing database: {e}")
|
|
31
|
-
raise typer.Exit(1)
|
|
32
|
-
|
|
33
|
-
@app.command()
|
|
34
|
-
def init(
|
|
35
|
-
force: bool = typer.Option(False, "--force", "-f", help="Force reinitialization if database exists")
|
|
36
|
-
):
|
|
37
|
-
"""Initialize a new basic-memory database."""
|
|
38
|
-
asyncio.run(_init(force))
|
basic_memory/mcp/main.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Main MCP entrypoint for Basic Memory.
|
|
2
|
-
|
|
3
|
-
Creates and configures the shared MCP instance and handles server startup.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from loguru import logger
|
|
7
|
-
|
|
8
|
-
from basic_memory.config import config
|
|
9
|
-
|
|
10
|
-
# Import shared mcp instance
|
|
11
|
-
from basic_memory.mcp.server import mcp
|
|
12
|
-
|
|
13
|
-
# Import tools to register them
|
|
14
|
-
import basic_memory.mcp.tools # noqa: F401
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if __name__ == "__main__":
|
|
18
|
-
home_dir = config.home
|
|
19
|
-
logger.info("Starting Basic Memory MCP server")
|
|
20
|
-
logger.info(f"Home directory: {home_dir}")
|
|
21
|
-
mcp.run()
|