basic-memory 0.1.1__py3-none-any.whl → 0.2.0__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.

Files changed (77) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/README +1 -0
  3. basic_memory/alembic/env.py +75 -0
  4. basic_memory/alembic/migrations.py +29 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  7. basic_memory/api/__init__.py +2 -1
  8. basic_memory/api/app.py +26 -24
  9. basic_memory/api/routers/knowledge_router.py +28 -26
  10. basic_memory/api/routers/memory_router.py +17 -11
  11. basic_memory/api/routers/search_router.py +6 -12
  12. basic_memory/cli/__init__.py +1 -1
  13. basic_memory/cli/app.py +0 -1
  14. basic_memory/cli/commands/__init__.py +3 -3
  15. basic_memory/cli/commands/db.py +25 -0
  16. basic_memory/cli/commands/import_memory_json.py +35 -31
  17. basic_memory/cli/commands/mcp.py +20 -0
  18. basic_memory/cli/commands/status.py +10 -6
  19. basic_memory/cli/commands/sync.py +5 -56
  20. basic_memory/cli/main.py +5 -38
  21. basic_memory/config.py +3 -3
  22. basic_memory/db.py +15 -22
  23. basic_memory/deps.py +3 -4
  24. basic_memory/file_utils.py +36 -35
  25. basic_memory/markdown/entity_parser.py +13 -30
  26. basic_memory/markdown/markdown_processor.py +7 -7
  27. basic_memory/markdown/plugins.py +109 -123
  28. basic_memory/markdown/schemas.py +7 -8
  29. basic_memory/markdown/utils.py +70 -121
  30. basic_memory/mcp/__init__.py +1 -1
  31. basic_memory/mcp/async_client.py +0 -2
  32. basic_memory/mcp/server.py +3 -27
  33. basic_memory/mcp/tools/__init__.py +5 -3
  34. basic_memory/mcp/tools/knowledge.py +2 -2
  35. basic_memory/mcp/tools/memory.py +8 -4
  36. basic_memory/mcp/tools/search.py +2 -1
  37. basic_memory/mcp/tools/utils.py +1 -1
  38. basic_memory/models/__init__.py +1 -2
  39. basic_memory/models/base.py +3 -3
  40. basic_memory/models/knowledge.py +23 -60
  41. basic_memory/models/search.py +1 -1
  42. basic_memory/repository/__init__.py +5 -3
  43. basic_memory/repository/entity_repository.py +34 -98
  44. basic_memory/repository/relation_repository.py +0 -7
  45. basic_memory/repository/repository.py +2 -39
  46. basic_memory/repository/search_repository.py +20 -25
  47. basic_memory/schemas/__init__.py +4 -4
  48. basic_memory/schemas/base.py +21 -62
  49. basic_memory/schemas/delete.py +2 -3
  50. basic_memory/schemas/discovery.py +4 -1
  51. basic_memory/schemas/memory.py +12 -13
  52. basic_memory/schemas/request.py +4 -23
  53. basic_memory/schemas/response.py +10 -9
  54. basic_memory/schemas/search.py +4 -7
  55. basic_memory/services/__init__.py +2 -7
  56. basic_memory/services/context_service.py +116 -110
  57. basic_memory/services/entity_service.py +25 -62
  58. basic_memory/services/exceptions.py +1 -0
  59. basic_memory/services/file_service.py +73 -109
  60. basic_memory/services/link_resolver.py +9 -9
  61. basic_memory/services/search_service.py +22 -15
  62. basic_memory/services/service.py +3 -24
  63. basic_memory/sync/__init__.py +2 -2
  64. basic_memory/sync/file_change_scanner.py +3 -7
  65. basic_memory/sync/sync_service.py +35 -40
  66. basic_memory/sync/utils.py +6 -38
  67. basic_memory/sync/watch_service.py +26 -5
  68. basic_memory/utils.py +42 -33
  69. {basic_memory-0.1.1.dist-info → basic_memory-0.2.0.dist-info}/METADATA +2 -7
  70. basic_memory-0.2.0.dist-info/RECORD +78 -0
  71. basic_memory/mcp/main.py +0 -21
  72. basic_memory/mcp/tools/ai_edit.py +0 -84
  73. basic_memory/services/database_service.py +0 -159
  74. basic_memory-0.1.1.dist-info/RECORD +0 -74
  75. {basic_memory-0.1.1.dist-info → basic_memory-0.2.0.dist-info}/WHEEL +0 -0
  76. {basic_memory-0.1.1.dist-info → basic_memory-0.2.0.dist-info}/entry_points.txt +0 -0
  77. {basic_memory-0.1.1.dist-info → basic_memory-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,36 +1,15 @@
1
1
  """Base service class."""
2
2
 
3
- from datetime import datetime
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
- R = TypeVar("R", bound=Repository)
8
+
11
9
 
12
10
  class BaseService(Generic[T]):
13
11
  """Base service that takes a repository."""
14
12
 
15
- def __init__(self, repository: R):
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)
@@ -1,5 +1,5 @@
1
1
  from .file_change_scanner import FileChangeScanner
2
2
  from .sync_service import SyncService
3
+ from .watch_service import WatchService
3
4
 
4
- __all__ = ["SyncService", "FileChangeScanner"]
5
-
5
+ __all__ = ["SyncService", "FileChangeScanner", "WatchService"]
@@ -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
  }
@@ -59,9 +59,6 @@ class SyncService:
59
59
  for permalink in permalinks:
60
60
  await self.search_service.delete_by_permalink(permalink)
61
61
 
62
- else:
63
- logger.debug(f"No entity found to delete: {file_path}")
64
-
65
62
  async def sync(self, directory: Path) -> SyncReport:
66
63
  """Sync knowledge files with database."""
67
64
  changes = await self.scanner.find_knowledge_changes(directory)
@@ -77,69 +74,63 @@ class SyncService:
77
74
  entity.id, {"file_path": new_path, "checksum": changes.checksums[new_path]}
78
75
  )
79
76
  # update search index
80
- await self.search_service.index_entity(updated)
77
+ if updated:
78
+ await self.search_service.index_entity(updated)
81
79
 
82
80
  # Handle deletions next
83
81
  # remove rows from db for files no longer present
84
- for file_path in changes.deleted:
85
- await self.handle_entity_deletion(file_path)
82
+ for path in changes.deleted:
83
+ await self.handle_entity_deletion(path)
86
84
 
87
85
  # Parse files that need updating
88
86
  parsed_entities: Dict[str, EntityMarkdown] = {}
89
87
 
90
- for file_path in [*changes.new, *changes.modified]:
91
- entity_markdown = await self.entity_parser.parse_file(directory / file_path)
92
- parsed_entities[file_path] = entity_markdown
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
93
91
 
94
92
  # First pass: Create/update entities
95
93
  # entities will have a null checksum to indicate they are not complete
96
- for file_path, entity_markdown in parsed_entities.items():
97
-
94
+ for path, entity_markdown in parsed_entities.items():
98
95
  # Get unique permalink and update markdown if needed
99
96
  permalink = await self.entity_service.resolve_permalink(
100
- file_path,
101
- markdown=entity_markdown
97
+ Path(path), markdown=entity_markdown
102
98
  )
103
99
 
104
100
  if permalink != entity_markdown.frontmatter.permalink:
105
101
  # Add/update permalink in frontmatter
106
- logger.info(f"Adding permalink '{permalink}' to file: {file_path}")
102
+ logger.info(f"Adding permalink '{permalink}' to file: {path}")
107
103
 
108
104
  # update markdown
109
105
  entity_markdown.frontmatter.metadata["permalink"] = permalink
110
-
106
+
111
107
  # update file frontmatter
112
108
  updated_checksum = await file_utils.update_frontmatter(
113
- directory / file_path,
114
- {"permalink": permalink}
109
+ directory / path, {"permalink": permalink}
115
110
  )
116
111
 
117
112
  # Update checksum in changes report since file was modified
118
- changes.checksums[file_path] = updated_checksum
119
-
113
+ changes.checksums[path] = updated_checksum
114
+
120
115
  # if the file is new, create an entity
121
- if file_path in changes.new:
116
+ if path in changes.new:
122
117
  # Create entity with final permalink
123
- logger.debug(f"Creating new entity_markdown: {file_path}")
124
- await self.entity_service.create_entity_from_markdown(
125
- file_path, entity_markdown
126
- )
118
+ logger.debug(f"Creating new entity_markdown: {path}")
119
+ await self.entity_service.create_entity_from_markdown(Path(path), entity_markdown)
127
120
  # otherwise we need to update the entity and observations
128
121
  else:
129
- logger.debug(f"Updating entity_markdown: {file_path}")
122
+ logger.debug(f"Updating entity_markdown: {path}")
130
123
  await self.entity_service.update_entity_and_observations(
131
- file_path, entity_markdown
124
+ Path(path), entity_markdown
132
125
  )
133
126
 
134
127
  # Second pass
135
- for file_path, entity_markdown in parsed_entities.items():
136
- logger.debug(f"Updating relations for: {file_path}")
128
+ for path, entity_markdown in parsed_entities.items():
129
+ logger.debug(f"Updating relations for: {path}")
137
130
 
138
131
  # Process relations
139
- checksum = changes.checksums[file_path]
140
- entity = await self.entity_service.update_entity_relations(
141
- file_path, entity_markdown
142
- )
132
+ checksum = changes.checksums[path]
133
+ entity = await self.entity_service.update_entity_relations(Path(path), entity_markdown)
143
134
 
144
135
  # add to search index
145
136
  await self.search_service.index_entity(entity)
@@ -153,18 +144,22 @@ class SyncService:
153
144
  target_entity = await self.entity_service.link_resolver.resolve_link(relation.to_name)
154
145
  # check we found a link that is not the source
155
146
  if target_entity and target_entity.id != relation.from_id:
156
- logger.debug(f"Resolved forward reference: {relation.to_name} -> {target_entity.permalink}")
147
+ logger.debug(
148
+ f"Resolved forward reference: {relation.to_name} -> {target_entity.permalink}"
149
+ )
157
150
 
158
151
  try:
159
- await self.relation_repository.update(relation.id, {
160
- "to_id": target_entity.id,
161
- "to_name": target_entity.title # Update to actual title
162
- })
163
- except IntegrityError as e:
164
- logger.info(f"Ignoring duplicate relation {relation}")
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}")
165
161
 
166
162
  # update search index
167
163
  await self.search_service.index_entity(target_entity)
168
164
 
169
-
170
165
  return changes
@@ -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
- @dataclass
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) # path -> checksum
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(title="Basic Memory Sync Status")
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
- with Live(self.generate_table(), refresh_per_second=4, console=self.console) as live:
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 unicodedata
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 | str) -> str:
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(file_path)[0]
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'[^a-z0-9/\-]', '-', text_with_hyphens)
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'-+', '-', clean_text)
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('-') for s in segments]
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
- return '/'.join(clean_segments)
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.1
3
+ Version: 0.2.0
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.2.0.dist-info/METADATA,sha256=54ldvapxyUcnEpifbzHUtP1py712UqQjxTQVcWHR_s0,7539
75
+ basic_memory-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
76
+ basic_memory-0.2.0.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
77
+ basic_memory-0.2.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
78
+ basic_memory-0.2.0.dist-info/RECORD,,
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()
@@ -1,84 +0,0 @@
1
- """Tool for AI-assisted file editing."""
2
-
3
- from pathlib import Path
4
- from typing import List, Dict, Any
5
-
6
- from basic_memory.mcp.server import mcp
7
-
8
-
9
- def _detect_indent(text: str, match_pos: int) -> int:
10
- """Get indentation level at a position in text."""
11
- # Find start of line containing the match
12
- line_start = text.rfind("\n", 0, match_pos)
13
- if line_start < 0:
14
- line_start = 0
15
- else:
16
- line_start += 1 # Skip newline char
17
-
18
- # Count leading spaces
19
- pos = line_start
20
- while pos < len(text) and text[pos].isspace():
21
- pos += 1
22
- return pos - line_start
23
-
24
-
25
- def _apply_indent(text: str, spaces: int) -> str:
26
- """Apply indentation to text."""
27
- prefix = " " * spaces
28
- return "\n".join(prefix + line if line.strip() else line for line in text.split("\n"))
29
-
30
-
31
- @mcp.tool()
32
- async def ai_edit(path: str, edits: List[Dict[str, Any]]) -> bool:
33
- """AI-assisted file editing tool.
34
-
35
- Args:
36
- path: Path to file to edit
37
- edits: List of edits to apply. Each edit is a dict with:
38
- oldText: Text to replace
39
- newText: New content
40
- options: Optional dict with:
41
- indent: Number of spaces to indent
42
- preserveIndentation: Keep existing indent (default: true)
43
-
44
- Returns:
45
- bool: True if edits were applied successfully
46
- """
47
- try:
48
- # Read file
49
- content = Path(path).read_text()
50
- original = content
51
- success = True
52
-
53
- # Apply each edit
54
- for edit in edits:
55
- old_text = edit["oldText"]
56
- new_text = edit["newText"]
57
- options = edit.get("options", {})
58
-
59
- # Find text to replace
60
- match_pos = content.find(old_text)
61
- if match_pos < 0:
62
- success = False
63
- continue
64
-
65
- # Handle indentation
66
- if not options.get("preserveIndentation", True):
67
- # Use existing indentation
68
- indent = _detect_indent(content, match_pos)
69
- new_text = _apply_indent(new_text, indent)
70
- elif "indent" in options:
71
- # Use specified indentation
72
- new_text = _apply_indent(new_text, options["indent"])
73
-
74
- # Apply the edit
75
- content = content.replace(old_text, new_text)
76
-
77
- # Write back if changed
78
- if content != original:
79
- Path(path).write_text(content)
80
- return success
81
-
82
- except Exception as e:
83
- print(f"Error applying edits: {e}")
84
- return False