basic-memory 0.4.1__py3-none-any.whl → 0.4.3__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.4.3"
@@ -1,34 +1,118 @@
1
1
  """Routes for getting entity content."""
2
2
 
3
+ import tempfile
3
4
  from pathlib import Path
4
5
 
5
- from fastapi import APIRouter, HTTPException
6
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
6
7
  from fastapi.responses import FileResponse
7
8
  from loguru import logger
8
9
 
9
- from basic_memory.deps import ProjectConfigDep, LinkResolverDep
10
+ from basic_memory.deps import (
11
+ ProjectConfigDep,
12
+ LinkResolverDep,
13
+ SearchServiceDep,
14
+ EntityServiceDep,
15
+ FileServiceDep,
16
+ )
17
+ from basic_memory.repository.search_repository import SearchIndexRow
18
+ from basic_memory.schemas.memory import normalize_memory_url
19
+ from basic_memory.schemas.search import SearchQuery, SearchItemType
10
20
 
11
21
  router = APIRouter(prefix="/resource", tags=["resources"])
12
22
 
13
23
 
24
+ def get_entity_ids(item: SearchIndexRow) -> list[int]:
25
+ match item.type:
26
+ case SearchItemType.ENTITY:
27
+ return [item.id]
28
+ case SearchItemType.OBSERVATION:
29
+ return [item.entity_id] # pyright: ignore [reportReturnType]
30
+ case SearchItemType.RELATION:
31
+ from_entity = item.from_id
32
+ to_entity = item.to_id # pyright: ignore [reportReturnType]
33
+ return [from_entity, to_entity] if to_entity else [from_entity] # pyright: ignore [reportReturnType]
34
+ case _:
35
+ raise ValueError(f"Unexpected type: {item.type}")
36
+
37
+
14
38
  @router.get("/{identifier:path}")
15
39
  async def get_resource_content(
16
40
  config: ProjectConfigDep,
17
41
  link_resolver: LinkResolverDep,
42
+ search_service: SearchServiceDep,
43
+ entity_service: EntityServiceDep,
44
+ file_service: FileServiceDep,
45
+ background_tasks: BackgroundTasks,
18
46
  identifier: str,
19
47
  ) -> FileResponse:
20
48
  """Get resource content by identifier: name or permalink."""
21
- logger.debug(f"Getting content for permalink: {identifier}")
49
+ logger.debug(f"Getting content for: {identifier}")
22
50
 
23
- # Find entity by permalink
51
+ # Find single entity by permalink
24
52
  entity = await link_resolver.resolve_link(identifier)
25
- if not entity:
26
- raise HTTPException(status_code=404, detail=f"Entity not found: {identifier}")
27
-
28
- file_path = Path(f"{config.home}/{entity.file_path}")
29
- if not file_path.exists():
30
- raise HTTPException(
31
- status_code=404,
32
- detail=f"File not found: {file_path}",
53
+ results = [entity] if entity else []
54
+
55
+ # search using the identifier as a permalink
56
+ if not results:
57
+ # if the identifier contains a wildcard, use GLOB search
58
+ query = (
59
+ SearchQuery(permalink_match=identifier)
60
+ if "*" in identifier
61
+ else SearchQuery(permalink=identifier)
33
62
  )
34
- return FileResponse(path=file_path)
63
+ search_results = await search_service.search(query)
64
+ if not search_results:
65
+ raise HTTPException(status_code=404, detail=f"Resource not found: {identifier}")
66
+
67
+ # get the entities related to the search results
68
+ entity_ids = [id for result in search_results for id in get_entity_ids(result)]
69
+ results = await entity_service.get_entities_by_id(entity_ids)
70
+
71
+ # return single response
72
+ if len(results) == 1:
73
+ entity = results[0]
74
+ file_path = Path(f"{config.home}/{entity.file_path}")
75
+ if not file_path.exists():
76
+ raise HTTPException(
77
+ status_code=404,
78
+ detail=f"File not found: {file_path}",
79
+ )
80
+ return FileResponse(path=file_path)
81
+
82
+ # for multiple files, initialize a temporary file for writing the results
83
+ with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".md") as tmp_file:
84
+ temp_file_path = tmp_file.name
85
+
86
+ for result in results:
87
+ # Read content for each entity
88
+ content = await file_service.read_entity_content(result)
89
+ memory_url = normalize_memory_url(result.permalink)
90
+ modified_date = result.updated_at.isoformat()
91
+ assert result.checksum
92
+ checksum = result.checksum[:8]
93
+
94
+ # Prepare the delimited content
95
+ response_content = f"--- {memory_url} {modified_date} {checksum}\n"
96
+ response_content += f"\n{content}\n"
97
+ response_content += "\n"
98
+
99
+ # Write content directly to the temporary file in append mode
100
+ tmp_file.write(response_content)
101
+
102
+ # Ensure all content is written to disk
103
+ tmp_file.flush()
104
+
105
+ # Schedule the temporary file to be deleted after the response
106
+ background_tasks.add_task(cleanup_temp_file, temp_file_path)
107
+
108
+ # Return the file response
109
+ return FileResponse(path=temp_file_path)
110
+
111
+
112
+ def cleanup_temp_file(file_path: str):
113
+ """Delete the temporary file."""
114
+ try:
115
+ Path(file_path).unlink() # Deletes the file
116
+ logger.debug(f"Temporary file deleted: {file_path}")
117
+ except Exception as e: # pragma: no cover
118
+ logger.error(f"Error deleting temporary file {file_path}: {e}")
@@ -69,7 +69,11 @@ def traverse_messages(
69
69
 
70
70
 
71
71
  def format_chat_markdown(
72
- title: str, mapping: Dict[str, Any], root_id: Optional[str], created_at: float, modified_at: float
72
+ title: str,
73
+ mapping: Dict[str, Any],
74
+ root_id: Optional[str],
75
+ created_at: float,
76
+ modified_at: float,
73
77
  ) -> str:
74
78
  """Format chat as clean markdown."""
75
79
 
basic_memory/db.py CHANGED
@@ -137,8 +137,18 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
137
137
  # Get the absolute path to the alembic directory relative to this file
138
138
  alembic_dir = Path(__file__).parent / "alembic"
139
139
  config = Config()
140
+
141
+ # Set required Alembic config options programmatically
140
142
  config.set_main_option("script_location", str(alembic_dir))
141
- config.set_main_option("sqlalchemy.url", "driver://user:pass@localhost/dbname")
143
+ config.set_main_option(
144
+ "file_template",
145
+ "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s",
146
+ )
147
+ config.set_main_option("timezone", "UTC")
148
+ config.set_main_option("revision_environment", "false")
149
+ config.set_main_option(
150
+ "sqlalchemy.url", DatabaseType.get_db_url(app_config.database_path, database_type)
151
+ )
142
152
 
143
153
  command.upgrade(config, "head")
144
154
  logger.info("Migrations completed successfully")
@@ -147,4 +157,4 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
147
157
  await SearchRepository(session_maker).init_search_index()
148
158
  except Exception as e: # pragma: no cover
149
159
  logger.error(f"Error running migrations: {e}")
150
- raise
160
+ raise
@@ -28,9 +28,29 @@ async def write_note(
28
28
  ) -> EntityResponse | str:
29
29
  """Write a markdown note to the knowledge base.
30
30
 
31
+ The content can include semantic observations and relations using markdown syntax.
32
+ Relations can be specified either explicitly or through inline wiki-style links:
33
+
34
+ Observations format:
35
+ `- [category] Observation text #tag1 #tag2 (optional context)`
36
+
37
+ Examples:
38
+ `- [design] Files are the source of truth #architecture (All state comes from files)`
39
+ `- [tech] Using SQLite for storage #implementation`
40
+ `- [note] Need to add error handling #todo`
41
+
42
+ Relations format:
43
+ - Explicit: `- relation_type [[Entity]] (optional context)`
44
+ - Inline: Any `[[Entity]]` reference creates a relation
45
+
46
+ Examples:
47
+ `- depends_on [[Content Parser]] (Need for semantic extraction)`
48
+ `- implements [[Search Spec]] (Initial implementation)`
49
+ `- This feature extends [[Base Design]] and uses [[Core Utils]]`
50
+
31
51
  Args:
32
52
  title: The title of the note
33
- content: Markdown content for the note
53
+ content: Markdown content for the note, can include observations and relations
34
54
  folder: the folder where the file should be saved
35
55
  tags: Optional list of tags to categorize the note
36
56
  verbose: If True, returns full EntityResponse with semantic info
@@ -40,19 +60,32 @@ async def write_note(
40
60
  If verbose=True: EntityResponse with full semantic details
41
61
 
42
62
  Examples:
43
- # Create a simple note
63
+ # Note with both explicit and inline relations
44
64
  write_note(
45
- tile="Meeting Notes: Project Planning.md",
46
- content="# Key Points\\n\\n- Discussed timeline\\n- Set priorities"
47
- folder="notes"
65
+ title="Search Implementation",
66
+ content="# Search Component\\n\\n"
67
+ "Implementation of the search feature, building on [[Core Search]].\\n\\n"
68
+ "## Observations\\n"
69
+ "- [tech] Using FTS5 for full-text search #implementation\\n"
70
+ "- [design] Need pagination support #todo\\n\\n"
71
+ "## Relations\\n"
72
+ "- implements [[Search Spec]]\\n"
73
+ "- depends_on [[Database Schema]]",
74
+ folder="docs/components"
48
75
  )
49
76
 
50
- # Create note with tags
77
+ # Note with tags
51
78
  write_note(
52
- title="Security Review",
53
- content="# Findings\\n\\n1. Updated auth flow\\n2. Added rate limiting",
54
- folder="security",
55
- tags=["security", "development"]
79
+ title="Error Handling Design",
80
+ content="# Error Handling\\n\\n"
81
+ "This design builds on [[Reliability Design]].\\n\\n"
82
+ "## Approach\\n"
83
+ "- [design] Use error codes #architecture\\n"
84
+ "- [tech] Implement retry logic #implementation\\n\\n"
85
+ "## Relations\\n"
86
+ "- extends [[Base Error Handling]]",
87
+ folder="docs/design",
88
+ tags=["architecture", "reliability"]
56
89
  )
57
90
  """
58
91
  logger.info(f"Writing note folder:'{folder}' title: '{title}'")
@@ -76,26 +109,58 @@ async def write_note(
76
109
  return result if verbose else result.permalink
77
110
 
78
111
 
79
- @mcp.tool(description="Read a note's content by its title or permalink")
112
+ @mcp.tool(description="Read note content by title, permalink, relation, or pattern")
80
113
  async def read_note(identifier: str) -> str:
81
- """Get the markdown content of a note.
82
- Uses the resource router to return the actual file content.
114
+ """Get note content in unified diff format.
115
+
116
+ The content is returned in a unified diff inspired format:
117
+ ```
118
+ --- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
119
+ <document content>
120
+ ```
121
+
122
+ Multiple documents (from relations or pattern matches) are separated by
123
+ additional headers.
83
124
 
84
125
  Args:
85
- identifier: Note title or permalink
126
+ identifier: Can be one of:
127
+ - Note title ("Project Planning")
128
+ - Note permalink ("docs/example")
129
+ - Relation path ("docs/example/depends-on/other-doc")
130
+ - Pattern match ("docs/*-architecture")
86
131
 
87
132
  Returns:
88
- The note's markdown content
133
+ Document content in unified diff format. For single documents, returns
134
+ just that document's content. For relations or pattern matches, returns
135
+ multiple documents separated by unified diff headers.
89
136
 
90
137
  Examples:
91
- # Read by title
92
- read_note("Meeting Notes: Project Planning")
138
+ # Single document
139
+ content = await read_note("Project Planning")
93
140
 
94
141
  # Read by permalink
95
- read_note("notes/project-planning")
142
+ content = await read_note("docs/architecture/file-first")
143
+
144
+ # Follow relation
145
+ content = await read_note("docs/architecture/depends-on/docs/content-parser")
146
+
147
+ # Pattern matching
148
+ content = await read_note("docs/*-architecture") # All architecture docs
149
+ content = await read_note("docs/*/implements/*") # Find implementations
150
+
151
+ Output format:
152
+ ```
153
+ --- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
154
+ <first document content>
155
+
156
+ --- memory://docs/other 2025-01-30T15:45:22 a1b2c3d4
157
+ <second document content>
158
+ ```
96
159
 
97
- Raises:
98
- ValueError: If the note cannot be found
160
+ The headers include:
161
+ - Full memory:// URI for the document
162
+ - Last modified timestamp
163
+ - Content checksum
99
164
  """
100
165
  logger.info(f"Reading note {identifier}")
101
166
  url = memory_url_path(identifier)
@@ -68,24 +68,40 @@ class SearchRepository:
68
68
 
69
69
  async def init_search_index(self):
70
70
  """Create or recreate the search index."""
71
-
72
71
  logger.info("Initializing search index")
73
- async with db.scoped_session(self.session_maker) as session:
74
- await session.execute(CREATE_SEARCH_INDEX)
75
- await session.commit()
76
-
77
- def _quote_search_term(self, term: str) -> str:
78
- """Add quotes if term contains special characters.
79
- For FTS5, special characters and phrases need to be quoted to be treated as a single token.
72
+ try:
73
+ async with db.scoped_session(self.session_maker) as session:
74
+ await session.execute(CREATE_SEARCH_INDEX)
75
+ await session.commit()
76
+ except Exception as e: # pragma: no cover
77
+ logger.error(f"Error initializing search index: {e}")
78
+ raise e
79
+
80
+ def _prepare_search_term(self, term: str, is_prefix: bool = True) -> str:
81
+ """Prepare a search term for FTS5 query.
82
+
83
+ Args:
84
+ term: The search term to prepare
85
+ is_prefix: Whether to add prefix search capability (* suffix)
86
+
87
+ For FTS5:
88
+ - Special characters and phrases need to be quoted
89
+ - Terms with spaces or special chars need quotes
80
90
  """
81
- # List of special characters that need quoting
82
- special_chars = ["/", "*", "-", ".", " ", "(", ")", "[", "]", '"', "'"]
91
+ if "*" in term:
92
+ return term
93
+
94
+ # List of special characters that need quoting (excluding *)
95
+ special_chars = ["/", "-", ".", " ", "(", ")", "[", "]", '"', "'"]
83
96
 
84
97
  # Check if term contains any special characters
85
- if any(c in term for c in special_chars):
86
- # If the term already contains quotes, escape them
98
+ needs_quotes = any(c in term for c in special_chars)
99
+
100
+ if needs_quotes:
101
+ # If the term already contains quotes, escape them and add a wildcard
87
102
  term = term.replace('"', '""')
88
- return f'"{term}"'
103
+ term = f'"{term}"*'
104
+
89
105
  return term
90
106
 
91
107
  async def search(
@@ -106,14 +122,14 @@ class SearchRepository:
106
122
 
107
123
  # Handle text search for title and content
108
124
  if search_text:
109
- search_text = self._quote_search_term(search_text.lower().strip())
110
- params["text"] = f"{search_text}*"
125
+ search_text = self._prepare_search_term(search_text.strip())
126
+ params["text"] = search_text
111
127
  conditions.append("(title MATCH :text OR content MATCH :text)")
112
128
 
113
129
  # Handle title match search
114
130
  if title:
115
- title_text = self._quote_search_term(title.lower().strip())
116
- params["text"] = f"{title_text}*"
131
+ title_text = self._prepare_search_term(title.strip())
132
+ params["text"] = title_text
117
133
  conditions.append("title MATCH :text")
118
134
 
119
135
  # Handle permalink exact search
@@ -123,8 +139,15 @@ class SearchRepository:
123
139
 
124
140
  # Handle permalink match search, supports *
125
141
  if permalink_match:
126
- params["permalink"] = self._quote_search_term(permalink_match)
127
- conditions.append("permalink MATCH :permalink")
142
+ # Clean and prepare permalink for FTS5 GLOB match
143
+ permalink_text = self._prepare_search_term(
144
+ permalink_match.lower().strip(), is_prefix=False
145
+ )
146
+ params["permalink"] = permalink_text
147
+ if "*" in permalink_match:
148
+ conditions.append("permalink GLOB :permalink")
149
+ else:
150
+ conditions.append("permalink MATCH :permalink")
128
151
 
129
152
  # Handle type filter
130
153
  if types:
@@ -173,7 +196,7 @@ class SearchRepository:
173
196
  LIMIT :limit
174
197
  """
175
198
 
176
- # logger.debug(f"Search {sql} params: {params}")
199
+ logger.debug(f"Search {sql} params: {params}")
177
200
  async with db.scoped_session(self.session_maker) as session:
178
201
  result = await session.execute(text(sql), params)
179
202
  rows = result.fetchall()
@@ -199,8 +222,11 @@ class SearchRepository:
199
222
  for row in rows
200
223
  ]
201
224
 
202
- # for r in results:
203
- # logger.debug(f"Search result: type:{r.type} title: {r.title} permalink: {r.permalink} score: {r.score}")
225
+ logger.debug(f"Found {len(results)} search results")
226
+ for r in results:
227
+ logger.debug(
228
+ f"Search result: type:{r.type} title: {r.title} permalink: {r.permalink} score: {r.score}"
229
+ )
204
230
 
205
231
  return results
206
232
 
@@ -233,7 +259,7 @@ class SearchRepository:
233
259
  """),
234
260
  search_index_row.to_insert(),
235
261
  )
236
- logger.debug(f"indexed permalink {search_index_row.permalink}")
262
+ logger.debug(f"indexed row {search_index_row}")
237
263
  await session.commit()
238
264
 
239
265
  async def delete_by_permalink(self, permalink: str):
@@ -11,6 +11,7 @@ Key Features:
11
11
  4. Bulk operations return all affected items
12
12
  """
13
13
 
14
+ from datetime import datetime
14
15
  from typing import List, Optional, Dict
15
16
 
16
17
  from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
@@ -43,6 +44,8 @@ class ObservationResponse(Observation, SQLAlchemyModel):
43
44
  }
44
45
  """
45
46
 
47
+ permalink: Permalink
48
+
46
49
 
47
50
  class RelationResponse(Relation, SQLAlchemyModel):
48
51
  """Response schema for relation operations.
@@ -59,6 +62,8 @@ class RelationResponse(Relation, SQLAlchemyModel):
59
62
  }
60
63
  """
61
64
 
65
+ permalink: Permalink
66
+
62
67
  from_id: Permalink = Field(
63
68
  # use the permalink from the associated Entity
64
69
  # or the from_id value
@@ -131,9 +136,12 @@ class EntityResponse(SQLAlchemyModel):
131
136
  file_path: str
132
137
  entity_type: EntityType
133
138
  entity_metadata: Optional[Dict] = None
139
+ checksum: Optional[str] = None
134
140
  content_type: ContentType
135
141
  observations: List[ObservationResponse] = []
136
142
  relations: List[RelationResponse] = []
143
+ created_at: datetime
144
+ updated_at: datetime
137
145
 
138
146
 
139
147
  class EntityListResponse(SQLAlchemyModel):
@@ -185,6 +185,11 @@ class EntityService(BaseService[EntityModel]):
185
185
  raise EntityNotFoundError(f"Entity not found: {permalink}")
186
186
  return db_entity
187
187
 
188
+ async def get_entities_by_id(self, ids: List[int]) -> Sequence[EntityModel]:
189
+ """Get specific entities and their relationships."""
190
+ logger.debug(f"Getting entities: {ids}")
191
+ return await self.repository.find_by_ids(ids)
192
+
188
193
  async def get_entities_by_permalinks(self, permalinks: List[str]) -> Sequence[EntityModel]:
189
194
  """Get specific nodes and their relationships."""
190
195
  logger.debug(f"Getting entities permalinks: {permalinks}")
@@ -16,9 +16,10 @@ class LinkResolver:
16
16
 
17
17
  Uses a combination of exact matching and search-based resolution:
18
18
  1. Try exact permalink match (fastest)
19
- 2. Try exact title match
20
- 3. Fall back to search for fuzzy matching
21
- 4. Generate new permalink if no match found
19
+ 2. Try permalink pattern match (for wildcards)
20
+ 3. Try exact title match
21
+ 4. Fall back to search for fuzzy matching
22
+ 5. Generate new permalink if no match found
22
23
  """
23
24
 
24
25
  def __init__(self, entity_repository: EntityRepository, search_service: SearchService):
@@ -45,8 +46,8 @@ class LinkResolver:
45
46
  logger.debug(f"Found title match: {entity.title}")
46
47
  return entity
47
48
 
48
- if use_search:
49
- # 3. Fall back to search for fuzzy matching on title if specified
49
+ if use_search and "*" not in clean_text:
50
+ # 3. Fall back to search for fuzzy matching on title
50
51
  results = await self.search_service.search(
51
52
  query=SearchQuery(title=clean_text, types=[SearchItemType.ENTITY]),
52
53
  )
@@ -59,7 +60,7 @@ class LinkResolver:
59
60
  )
60
61
  return await self.entity_repository.get_by_permalink(best_match.permalink)
61
62
 
62
- # if we couldn't find anything then return None
63
+ # if we couldn't find anything then return None
63
64
  return None
64
65
 
65
66
  def _normalize_link_text(self, link_text: str) -> Tuple[str, Optional[str]]:
@@ -87,7 +88,7 @@ class LinkResolver:
87
88
 
88
89
  return text, alias
89
90
 
90
- def _select_best_match(self, search_text: str, results: List[SearchIndexRow]) -> Entity:
91
+ def _select_best_match(self, search_text: str, results: List[SearchIndexRow]) -> SearchIndexRow:
91
92
  """Select best match from search results.
92
93
 
93
94
  Uses multiple criteria:
@@ -75,7 +75,7 @@ class SearchService:
75
75
  else None
76
76
  )
77
77
 
78
- # permalink search
78
+ # search
79
79
  results = await self.repository.search(
80
80
  search_text=query.text,
81
81
  permalink=query.permalink,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-memory
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
@@ -1,6 +1,6 @@
1
- basic_memory/__init__.py,sha256=2cRKgLU6pxU7UGIk0ISBR4M8QSEQY1sWg26LKqHmX1Y,122
1
+ basic_memory/__init__.py,sha256=4zJaih3yN9F8I3EouZZVetyCv1cVWunEjLvDM38EAy8,122
2
2
  basic_memory/config.py,sha256=PZA2qgwKACvKfRcM3H-BPB_8FYVhgZAwTmlKJ3ROfhU,1643
3
- basic_memory/db.py,sha256=kFCPOzf4bUiW7E_7E6JEHcrVaQuUgdHXkNnh6-Fk_Kg,4891
3
+ basic_memory/db.py,sha256=IK_gz8Uiwcgxe8TjarW7kpl8cVVNtEnR0lm1LemgZ8I,5283
4
4
  basic_memory/deps.py,sha256=UzivBw6e6iYcU_8SQ8LNCmSsmFyHfjdzfWvnfNzqbRc,5375
5
5
  basic_memory/file_utils.py,sha256=gp7RCFWaddFnELIyTc1E19Rk8jJsrKshG2n8ZZR-kKA,5751
6
6
  basic_memory/utils.py,sha256=HiLorP5_YCQeNeTcDqvnkrwY7OBaFRS3i_hdV9iWKLs,2374
@@ -14,14 +14,14 @@ basic_memory/api/app.py,sha256=3ddcWTxVjMy_5SUq89kROhMwosZqcr67Q5evOlSR9GE,1389
14
14
  basic_memory/api/routers/__init__.py,sha256=iviQ1QVYobC8huUuyRhEjcA0BDjrOUm1lXHXhJkxP9A,239
15
15
  basic_memory/api/routers/knowledge_router.py,sha256=cMLhRczOfSRnsZdyR0bSS8PENPRTu70dlwaV27O34bs,5705
16
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
17
+ basic_memory/api/routers/resource_router.py,sha256=adFrZdTIHfYF3UjB-2LqwkAvgwLgJa9V5KfUsEUAbqc,4340
18
18
  basic_memory/api/routers/search_router.py,sha256=dCRnBbp3r966U8UYwgAaxZBbg7yX7pC8QJqagdACUi0,1086
19
19
  basic_memory/cli/__init__.py,sha256=arcKLAWRDhPD7x5t80MlviZeYzwHZ0GZigyy3NKVoGk,33
20
20
  basic_memory/cli/app.py,sha256=NG6gs_UzyXBiQLHbiZRZlew3nb7G7i_8gwPh1383EnA,450
21
21
  basic_memory/cli/main.py,sha256=_x9Tvjv5Xl26Bhn6dO2A2-5yu5ckiLiPZr0yFeDYB2w,611
22
22
  basic_memory/cli/commands/__init__.py,sha256=OQGLaKTsOdPsp2INM_pHzmOlbVfdL0sytBNgvqTqCDY,159
23
23
  basic_memory/cli/commands/db.py,sha256=XW2ujzas5j2Gf01NOPQI89L4NK-21GksO_OIekKxv6c,770
24
- basic_memory/cli/commands/import_chatgpt.py,sha256=zzp0I3vu5zekYlvBf7YzPTfNq9SumULwwL-Ky5rEjA4,8133
24
+ basic_memory/cli/commands/import_chatgpt.py,sha256=Jnqj_kswM9S-qauPCHqLiMIQMvY4PXULHZSiqVJ_veQ,8150
25
25
  basic_memory/cli/commands/import_claude_conversations.py,sha256=Ba97fH5yfW642yrkxay3YkyDdgIYCeru-MUIZfEGblo,6812
26
26
  basic_memory/cli/commands/import_claude_projects.py,sha256=euht03ydbI6c5IO_VeArlk9YUYMXNZGXekaa7uG8i7g,6635
27
27
  basic_memory/cli/commands/import_memory_json.py,sha256=zqpU4eCzQXx04aRsigddJAyhvklmTgSAzeRTuEdNw0c,5194
@@ -40,7 +40,7 @@ basic_memory/mcp/server.py,sha256=L92Vit7llaKT9NlPZfxdp67C33niObmRH2QFyUhmnD0,35
40
40
  basic_memory/mcp/tools/__init__.py,sha256=MHZmWw016N0qbtC3f186Jg1tPzh2g88_ZsCKJ0oyrrs,873
41
41
  basic_memory/mcp/tools/knowledge.py,sha256=2U8YUKCizsAETHCC1mBVKMfCEef6tlc_pa2wOmA9mD4,2016
42
42
  basic_memory/mcp/tools/memory.py,sha256=gl4MBm9l2lMOfu_xmUqjoZacWSIHOAYZiAm8z7oDuY8,5203
43
- basic_memory/mcp/tools/notes.py,sha256=pe7n0f0_nrkjnq6E4PCr7L8oOvzMnQgthfJNy9Vr3DE,3905
43
+ basic_memory/mcp/tools/notes.py,sha256=KRZfldeUBVxIzdlhHZuPJc11EOUKU4PaAFZRV2gldWo,6775
44
44
  basic_memory/mcp/tools/search.py,sha256=tx6aIuB2FWmmrvzu3RHSQvszlk-zHcwrWhkLLHWjuZc,1105
45
45
  basic_memory/mcp/tools/utils.py,sha256=icm-Xyqw3GxooGYkXqjEjoZvIGy_Z3CPw-uUYBxR_YQ,4831
46
46
  basic_memory/models/__init__.py,sha256=Bf0xXV_ryndogvZDiVM_Wb6iV2fHUxYNGMZNWNcZi0s,307
@@ -52,30 +52,30 @@ basic_memory/repository/entity_repository.py,sha256=VFLymzJ1W6AZru_s1S3U6nlqSprB
52
52
  basic_memory/repository/observation_repository.py,sha256=BOcy4wARqCXu-thYyt7mPxt2A2C8TW0le3s_X9wrK6I,1701
53
53
  basic_memory/repository/relation_repository.py,sha256=DwpTcn9z_1sZQcyMOUABz1k1VSwo_AU63x2zR7aerTk,2933
54
54
  basic_memory/repository/repository.py,sha256=jUScHWOfcB2FajwVZ2Sbjtg-gSI2Y2rhiIaTULjvmn8,11321
55
- basic_memory/repository/search_repository.py,sha256=OfocJZ7EWum33klFFvsLE7BEUnZPda1BNSwrbkRiXko,9233
55
+ basic_memory/repository/search_repository.py,sha256=NcOGMkPvwo3VlbsmeQWDsA2f1eTzX5cyl-7M2b-ADYs,10036
56
56
  basic_memory/schemas/__init__.py,sha256=eVxrtuPT7-9JIQ7UDx2J8t8xlS3u0iUkV_VLNbzvxo4,1575
57
57
  basic_memory/schemas/base.py,sha256=epSauNNVZ2lRLATf-HIzqeberq4ZBTgxliNmjitAsWc,5538
58
58
  basic_memory/schemas/delete.py,sha256=UAR2JK99WMj3gP-yoGWlHD3eZEkvlTSRf8QoYIE-Wfw,1180
59
59
  basic_memory/schemas/discovery.py,sha256=6Y2tUiv9f06rFTsa8_wTH2haS2bhCfuQh0uW33hwdd8,876
60
60
  basic_memory/schemas/memory.py,sha256=mqslazV0lQswtbNgYv_y2-KxmifIvRlg5I3IuTTMnO4,2882
61
61
  basic_memory/schemas/request.py,sha256=rt_guNWrUMePJvDmsh1g1dc7IqEY6K6mGXMKx8tBCj8,1614
62
- basic_memory/schemas/response.py,sha256=2su3YP-gkbw4MvgGtgZLHEuTp6RuVlK736KakaV7fP4,6273
62
+ basic_memory/schemas/response.py,sha256=lVYR31DTtSeFRddGWX_wQWnQgyiwX0LEpNJ4f4lKpTM,6440
63
63
  basic_memory/schemas/search.py,sha256=pWBA1-xEQ3rH8vLIgrQT4oygq9MMwr0B7VCbFafVVOw,3278
64
64
  basic_memory/services/__init__.py,sha256=oop6SKmzV4_NAYt9otGnupLGVCCKIVgxEcdRQWwh25I,197
65
65
  basic_memory/services/context_service.py,sha256=Bu1wVl9q3FDGbGChrLqgFGQW95-W1OfjNqq6SGljqWg,9388
66
- basic_memory/services/entity_service.py,sha256=bm_Z63_AJmXiRQkVYWwoB3PYLMW1t1xS3Nh0Nm9SwiI,11538
66
+ basic_memory/services/entity_service.py,sha256=CoN1HVrjlT2JDaG3tfs6NixkZgJ4xbaEjABkv8hyGJ4,11784
67
67
  basic_memory/services/exceptions.py,sha256=VGlCLd4UD2w5NWKqC7QpG4jOM_hA7jKRRM-MqvEVMNk,288
68
68
  basic_memory/services/file_service.py,sha256=r4JfPY1wyenAH0Y-iq7vGHPwT616ayUWoLnvA1NuzpA,5695
69
- basic_memory/services/link_resolver.py,sha256=VdhoPAVa65T6LW7kSTLWts55zbnnN481fr7VLz3HaXE,4513
70
- basic_memory/services/search_service.py,sha256=iB-BgFwInrJxTfYBerj68QORlMv46wYy2-ceQx61Dd8,7839
69
+ basic_memory/services/link_resolver.py,sha256=GmUPTViW5JplQo4yJNaX18OGInqMitrxUeR4LQqUABA,4581
70
+ basic_memory/services/search_service.py,sha256=VDtRYYCHojzG2-yQbi_8ncs5YdoyHM7k_QHdA-0g6oI,7829
71
71
  basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
72
72
  basic_memory/sync/__init__.py,sha256=ko0xLQv1S5U7sAOmIP2XKl03akVPzoY-a9m3TFPcMh4,193
73
73
  basic_memory/sync/file_change_scanner.py,sha256=4whJej6t9sxwUp1ox93efJ0bBHSnAr6STpk_PsKU6to,5784
74
74
  basic_memory/sync/sync_service.py,sha256=nAOX4N90lbpRJeq5tRR_7PYptIoWwhXMUljE7yrneF4,7087
75
75
  basic_memory/sync/utils.py,sha256=wz1Fe7Mb_M5N9vYRQnDKGODiMGcj5MEK16KVJ3eoQ9g,1191
76
76
  basic_memory/sync/watch_service.py,sha256=CtKBrP1imI3ZSEgJl7Ffi-JZ_oDGKrhiyGgs41h5QYI,7563
77
- basic_memory-0.4.1.dist-info/METADATA,sha256=y13i0kzJZW19UFRHvatQcxkOwHkAqaLT0x0HmunnFwM,10791
78
- basic_memory-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
79
- basic_memory-0.4.1.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
80
- basic_memory-0.4.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
81
- basic_memory-0.4.1.dist-info/RECORD,,
77
+ basic_memory-0.4.3.dist-info/METADATA,sha256=HMO_cAbhq2It1uGyslBIh0cdgXiO1UmQGASjyl0UelM,10791
78
+ basic_memory-0.4.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
79
+ basic_memory-0.4.3.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
80
+ basic_memory-0.4.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
81
+ basic_memory-0.4.3.dist-info/RECORD,,