basic-memory 0.13.2__py3-none-any.whl → 0.13.4__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,7 +1,7 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
3
  # Package version - updated by release automation
4
- __version__ = "0.13.2"
4
+ __version__ = "0.13.4"
5
5
 
6
6
  # API version for FastAPI - independent of package version
7
7
  __api_version__ = "v0"
basic_memory/config.py CHANGED
@@ -4,7 +4,7 @@ import json
4
4
  import os
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
- from typing import Any, Dict, Literal, Optional, List
7
+ from typing import Any, Dict, Literal, Optional, List, Tuple
8
8
 
9
9
  from loguru import logger
10
10
  from pydantic import Field, field_validator
@@ -196,7 +196,8 @@ class ConfigManager:
196
196
 
197
197
  def add_project(self, name: str, path: str) -> ProjectConfig:
198
198
  """Add a new project to the configuration."""
199
- if name in self.config.projects: # pragma: no cover
199
+ project_name, _ = self.get_project(name)
200
+ if project_name: # pragma: no cover
200
201
  raise ValueError(f"Project '{name}' already exists")
201
202
 
202
203
  # Ensure the path exists
@@ -209,10 +210,12 @@ class ConfigManager:
209
210
 
210
211
  def remove_project(self, name: str) -> None:
211
212
  """Remove a project from the configuration."""
212
- if name not in self.config.projects: # pragma: no cover
213
+
214
+ project_name, path = self.get_project(name)
215
+ if not project_name: # pragma: no cover
213
216
  raise ValueError(f"Project '{name}' not found")
214
217
 
215
- if name == self.config.default_project: # pragma: no cover
218
+ if project_name == self.config.default_project: # pragma: no cover
216
219
  raise ValueError(f"Cannot remove the default project '{name}'")
217
220
 
218
221
  del self.config.projects[name]
@@ -220,12 +223,21 @@ class ConfigManager:
220
223
 
221
224
  def set_default_project(self, name: str) -> None:
222
225
  """Set the default project."""
223
- if name not in self.config.projects: # pragma: no cover
226
+ project_name, path = self.get_project(name)
227
+ if not project_name: # pragma: no cover
224
228
  raise ValueError(f"Project '{name}' not found")
225
229
 
226
230
  self.config.default_project = name
227
231
  self.save_config(self.config)
228
232
 
233
+ def get_project(self, name: str) -> Tuple[str, str] | Tuple[None, None]:
234
+ """Look up a project from the configuration by name or permalink"""
235
+ project_permalink = generate_permalink(name)
236
+ for name, path in app_config.projects.items():
237
+ if project_permalink == generate_permalink(name):
238
+ return name, path
239
+ return None, None
240
+
229
241
 
230
242
  def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
231
243
  """
@@ -256,11 +268,14 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
256
268
  # the config contains a dict[str,str] of project names and absolute paths
257
269
  assert actual_project_name is not None, "actual_project_name cannot be None"
258
270
 
259
- project_path = app_config.projects.get(actual_project_name)
260
- if not project_path: # pragma: no cover
261
- raise ValueError(f"Project '{actual_project_name}' not found")
271
+ project_permalink = generate_permalink(actual_project_name)
272
+
273
+ for name, path in app_config.projects.items():
274
+ if project_permalink == generate_permalink(name):
275
+ return ProjectConfig(name=name, home=Path(path))
262
276
 
263
- return ProjectConfig(name=actual_project_name, home=Path(project_path))
277
+ # otherwise raise error
278
+ raise ValueError(f"Project '{actual_project_name}' not found") # pragma: no cover
264
279
 
265
280
 
266
281
  # Create config manager
@@ -48,4 +48,4 @@ __all__ = [
48
48
  "sync_status",
49
49
  "view_note",
50
50
  "write_note",
51
- ]
51
+ ]
@@ -9,7 +9,6 @@ from textwrap import dedent
9
9
  from fastmcp import Context
10
10
  from loguru import logger
11
11
 
12
- from basic_memory.config import get_project_config
13
12
  from basic_memory.mcp.async_client import client
14
13
  from basic_memory.mcp.project_session import session, add_project_metadata
15
14
  from basic_memory.mcp.server import mcp
@@ -19,7 +18,7 @@ from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
19
18
  from basic_memory.utils import generate_permalink
20
19
 
21
20
 
22
- @mcp.tool()
21
+ @mcp.tool("list_memory_projects")
23
22
  async def list_projects(ctx: Context | None = None) -> str:
24
23
  """List all available projects with their status.
25
24
 
@@ -85,27 +84,38 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
85
84
  response = await call_get(client, "/projects/projects")
86
85
  project_list = ProjectList.model_validate(response.json())
87
86
 
88
- # Check if project exists
89
- project_exists = any(p.permalink == project_permalink for p in project_list.projects)
90
- if not project_exists:
87
+ # Find the project by name (case-insensitive) or permalink
88
+ target_project = None
89
+ for p in project_list.projects:
90
+ # Match by permalink (handles case-insensitive input)
91
+ if p.permalink == project_permalink:
92
+ target_project = p
93
+ break
94
+ # Also match by name comparison (case-insensitive)
95
+ if p.name.lower() == project_name.lower():
96
+ target_project = p
97
+ break
98
+
99
+ if not target_project:
91
100
  available_projects = [p.name for p in project_list.projects]
92
101
  return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
93
102
 
94
- # Switch to the project
95
- session.set_current_project(project_permalink)
103
+ # Switch to the project using the canonical name from database
104
+ canonical_name = target_project.name
105
+ session.set_current_project(canonical_name)
96
106
  current_project = session.get_current_project()
97
- project_config = get_project_config(current_project)
98
107
 
99
108
  # Get project info to show summary
100
109
  try:
110
+ current_project_permalink = generate_permalink(canonical_name)
101
111
  response = await call_get(
102
112
  client,
103
- f"{project_config.project_url}/project/info",
104
- params={"project_name": project_permalink},
113
+ f"/{current_project_permalink}/project/info",
114
+ params={"project_name": canonical_name},
105
115
  )
106
116
  project_info = ProjectInfoResponse.model_validate(response.json())
107
117
 
108
- result = f"✓ Switched to {project_permalink} project\n\n"
118
+ result = f"✓ Switched to {canonical_name} project\n\n"
109
119
  result += "Project Summary:\n"
110
120
  result += f"• {project_info.statistics.total_entities} entities\n"
111
121
  result += f"• {project_info.statistics.total_observations} observations\n"
@@ -113,11 +123,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
113
123
 
114
124
  except Exception as e:
115
125
  # If we can't get project info, still confirm the switch
116
- logger.warning(f"Could not get project info for {project_name}: {e}")
117
- result = f"✓ Switched to {project_name} project\n\n"
126
+ logger.warning(f"Could not get project info for {canonical_name}: {e}")
127
+ result = f"✓ Switched to {canonical_name} project\n\n"
118
128
  result += "Project summary unavailable.\n"
119
129
 
120
- return add_project_metadata(result, project_name)
130
+ return add_project_metadata(result, canonical_name)
121
131
 
122
132
  except Exception as e:
123
133
  logger.error(f"Error switching to project {project_name}: {e}")
@@ -165,13 +175,13 @@ async def get_current_project(ctx: Context | None = None) -> str:
165
175
  await ctx.info("Getting current project information")
166
176
 
167
177
  current_project = session.get_current_project()
168
- project_config = get_project_config(current_project)
169
178
  result = f"Current project: {current_project}\n\n"
170
179
 
171
- # get project stats
180
+ # get project stats (use permalink in URL path)
181
+ current_project_permalink = generate_permalink(current_project)
172
182
  response = await call_get(
173
183
  client,
174
- f"{project_config.project_url}/project/info",
184
+ f"/{current_project_permalink}/project/info",
175
185
  params={"project_name": current_project},
176
186
  )
177
187
  project_info = ProjectInfoResponse.model_validate(response.json())
@@ -185,9 +185,9 @@ class ProjectItem(BaseModel):
185
185
  name: str
186
186
  path: str
187
187
  is_default: bool = False
188
-
188
+
189
189
  @property
190
- def permalink(self) -> str: # pragma: no cover
190
+ def permalink(self) -> str: # pragma: no cover
191
191
  return generate_permalink(self.name)
192
192
 
193
193
 
@@ -64,8 +64,10 @@ class ProjectService:
64
64
  return await self.repository.find_all()
65
65
 
66
66
  async def get_project(self, name: str) -> Optional[Project]:
67
- """Get the file path for a project by name."""
68
- return await self.repository.get_by_name(name)
67
+ """Get the file path for a project by name or permalink."""
68
+ return await self.repository.get_by_name(name) or await self.repository.get_by_permalink(
69
+ name
70
+ )
69
71
 
70
72
  async def add_project(self, name: str, path: str, set_default: bool = False) -> None:
71
73
  """Add a new project to the configuration and database.
@@ -347,12 +349,15 @@ class ProjectService:
347
349
  # Use specified project or fall back to config project
348
350
  project_name = project_name or config.project
349
351
  # Get project path from configuration
350
- project_path = config_manager.projects.get(project_name)
351
- if not project_path: # pragma: no cover
352
+ name, project_path = config_manager.get_project(project_name)
353
+ if not name: # pragma: no cover
352
354
  raise ValueError(f"Project '{project_name}' not found in configuration")
353
355
 
356
+ assert project_path is not None
357
+ project_permalink = generate_permalink(project_name)
358
+
354
359
  # Get project from database to get project_id
355
- db_project = await self.repository.get_by_name(project_name)
360
+ db_project = await self.repository.get_by_permalink(project_permalink)
356
361
  if not db_project: # pragma: no cover
357
362
  raise ValueError(f"Project '{project_name}' not found in database")
358
363
 
@@ -367,7 +372,7 @@ class ProjectService:
367
372
 
368
373
  # Get enhanced project information from database
369
374
  db_projects = await self.repository.get_active_projects()
370
- db_projects_by_name = {p.name: p for p in db_projects}
375
+ db_projects_by_permalink = {p.permalink: p for p in db_projects}
371
376
 
372
377
  # Get default project info
373
378
  default_project = config_manager.default_project
@@ -375,7 +380,8 @@ class ProjectService:
375
380
  # Convert config projects to include database info
376
381
  enhanced_projects = {}
377
382
  for name, path in config_manager.projects.items():
378
- db_project = db_projects_by_name.get(name)
383
+ config_permalink = generate_permalink(name)
384
+ db_project = db_projects_by_permalink.get(config_permalink)
379
385
  enhanced_projects[name] = {
380
386
  "path": path,
381
387
  "active": db_project.is_active if db_project else True,
@@ -668,4 +674,4 @@ class ProjectService:
668
674
  database_size=db_size_readable,
669
675
  watch_status=watch_status,
670
676
  timestamp=datetime.now(),
671
- )
677
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basic-memory
3
- Version: 0.13.2
3
+ Version: 0.13.4
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,5 +1,5 @@
1
- basic_memory/__init__.py,sha256=s4xkAkIj-yvZFc_ImULxCYGhhrih4z5rAGrb5c81sqc,256
2
- basic_memory/config.py,sha256=lNpbn-b1k9nunQ-htciYQHC8XatRIhc_m6SN2Pbvp-E,11101
1
+ basic_memory/__init__.py,sha256=wtW6Z-HbBdts3U7R1VxrCpzX6uCcRhqCse_ax-93jAE,256
2
+ basic_memory/config.py,sha256=u19Pj3AnkPLQQyD4PRamzdinZgrNbO3VnUm_1Inx4Gk,11699
3
3
  basic_memory/db.py,sha256=X4-uyEZdJXVLfFDTpcNZxWzawRZXhDdKoEFWAGgE4Lk,6193
4
4
  basic_memory/deps.py,sha256=zXOhqXCoSVIa1iIcO8U6uUiofJn5eT4ycwJkH9I2kX4,12102
5
5
  basic_memory/file_utils.py,sha256=eaxTKLLEbTIy_Mb_Iv_Dmt4IXAJSrZGVi-Knrpyci3E,6700
@@ -72,14 +72,14 @@ basic_memory/mcp/prompts/sync_status.py,sha256=_5EqnCavY9BTsaxX2tPp-AgQZLt4bUrqQ
72
72
  basic_memory/mcp/prompts/utils.py,sha256=VacrbqwYtySpIlYIrKHo5s6jtoTMscYJqrFRH3zpC6Q,5431
73
73
  basic_memory/mcp/resources/ai_assistant_guide.md,sha256=qnYWDkYlb-JmKuOoZ5llmRas_t4dWDXB_i8LE277Lgs,14777
74
74
  basic_memory/mcp/resources/project_info.py,sha256=LcUkTx4iXBfU6Lp4TVch78OqLopbOy4ljyKnfr4VXso,1906
75
- basic_memory/mcp/tools/__init__.py,sha256=lCCOC0jElvL2v53WI_dxRs4qABq4Eo-YGm6j2XeZ6AQ,1591
75
+ basic_memory/mcp/tools/__init__.py,sha256=eVy_IS4yRfwf-90W1kpRjQ2vj0j2AVh8K8IurGW0DiU,1590
76
76
  basic_memory/mcp/tools/build_context.py,sha256=RbevfGVblSF901kAD2zc1CQ5z3tzfLC9XV_jcq35d_Y,4490
77
77
  basic_memory/mcp/tools/canvas.py,sha256=22F9G9gfPb-l8i1B5ra4Ja_h9zYY83rPY9mDA5C5gkY,3738
78
78
  basic_memory/mcp/tools/delete_note.py,sha256=tSyRc_VgBmLyVeenClwX1Sk--LKcGahAMzTX2mK2XIs,7346
79
79
  basic_memory/mcp/tools/edit_note.py,sha256=q4x-f7-j_l-wzm17-AVFT1_WGCo0Cq4lI3seYSe21aY,13570
80
80
  basic_memory/mcp/tools/list_directory.py,sha256=-FxDsCru5YD02M4qkQDAurEJWyRaC7YI4YR6zg0atR8,5236
81
81
  basic_memory/mcp/tools/move_note.py,sha256=esnbddG2OcmIgRNuQwx5OhlwZ1CWcOheg3hUobsEcq0,11320
82
- basic_memory/mcp/tools/project_management.py,sha256=aLkfgEL3RlRztzERQur283cIed4mv14eAP7kh6gzHpw,12293
82
+ basic_memory/mcp/tools/project_management.py,sha256=Y2pSrY8nWZnk_0SweMED5P84EfL4KVCLD_vyDB32_Qg,12755
83
83
  basic_memory/mcp/tools/read_content.py,sha256=4FTw13B8UjVVhR78NJB9HKeJb_nA6-BGT1WdGtekN5Q,8596
84
84
  basic_memory/mcp/tools/read_note.py,sha256=GdsJLkcDrCBnmNeM9BZRx9Xs2LUqH5ty_E471T9Kf1Y,7493
85
85
  basic_memory/mcp/tools/recent_activity.py,sha256=XVjNJAJnmxvzx9_Ls1A-QOd2yTR7pJlSTTuRxSivmN4,4833
@@ -107,7 +107,7 @@ basic_memory/schemas/delete.py,sha256=UAR2JK99WMj3gP-yoGWlHD3eZEkvlTSRf8QoYIE-Wf
107
107
  basic_memory/schemas/directory.py,sha256=F9_LrJqRqb_kO08GDKJzXLb2nhbYG2PdVUo5eDD_Kf4,881
108
108
  basic_memory/schemas/importer.py,sha256=FAh-RGxuhFW2rz3HFxwLzENJOiGgbTR2hUeXZZpM3OA,663
109
109
  basic_memory/schemas/memory.py,sha256=6YjEyJ9GJLC4VrFD0EnoRDTfg-Sf6g0D4bhL9rwNBi4,5816
110
- basic_memory/schemas/project_info.py,sha256=4yM51eGchS75ao2qyyVsk8_JyWRKJ0cJM3UzQF3G2ls,7050
110
+ basic_memory/schemas/project_info.py,sha256=fcNjUpe25_5uMmKy142ib3p5qEakzs1WJPLkgol5zyw,7047
111
111
  basic_memory/schemas/prompt.py,sha256=SpIVfZprQT8E5uP40j3CpBc2nHKflwOo3iZD7BFPIHE,3648
112
112
  basic_memory/schemas/request.py,sha256=Mv5EvrLZlFIiPr8dOjo_4QXvkseYhQI7cd_X2zDsxQM,3760
113
113
  basic_memory/schemas/response.py,sha256=lVYR31DTtSeFRddGWX_wQWnQgyiwX0LEpNJ4f4lKpTM,6440
@@ -121,7 +121,7 @@ basic_memory/services/file_service.py,sha256=jCrmnEkTQ4t9HF7L_M6BL7tdDqjjzty9hpT
121
121
  basic_memory/services/initialization.py,sha256=6ZeuTInPksyre4pjmiK_GXi5o_mJk3mfqGGH6apHxko,9271
122
122
  basic_memory/services/link_resolver.py,sha256=1-_VFsvqdT5rVBHe8Jrq63U59XQ0hxGezxY8c24Tiow,4594
123
123
  basic_memory/services/migration_service.py,sha256=pFJCSD7UgHLx1CHvtN4Df1CzDEp-CZ9Vqx4XYn1m1M0,6096
124
- basic_memory/services/project_service.py,sha256=Nz6N-2rk6DLLKBmtTBcPQATh_nzkLuD8LWWDAwgl6Oc,26875
124
+ basic_memory/services/project_service.py,sha256=YDZl_e7R36D6KcObpBeMqIiM05oh9nOIfZFIFgIRxbY,27151
125
125
  basic_memory/services/search_service.py,sha256=c5Ky0ufz7YPFgHhVzNRQ4OecF_JUrt7nALzpMjobW4M,12782
126
126
  basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
127
127
  basic_memory/services/sync_status_service.py,sha256=PRAnYrsNJY8EIlxaxCrDsY0TjySDdhktjta8ReQZyiY,6838
@@ -131,8 +131,8 @@ basic_memory/sync/sync_service.py,sha256=AxC5J1YTcPWTmA0HdzvOZBthi4-_LZ44kNF0KQo
131
131
  basic_memory/sync/watch_service.py,sha256=JAumrHUjV1lF9NtEK32jgg0myWBfLXotNXxONeIV9SM,15316
132
132
  basic_memory/templates/prompts/continue_conversation.hbs,sha256=begMFHOPN3aCm5sHz5PlKMLOfZ8hlpFxFJ-hgy0T9K4,3075
133
133
  basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
134
- basic_memory-0.13.2.dist-info/METADATA,sha256=kTZGjOSHLa84vzIf6BuhEpDxzos3c-MWFGi-VcgLe2o,15469
135
- basic_memory-0.13.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
136
- basic_memory-0.13.2.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
137
- basic_memory-0.13.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
138
- basic_memory-0.13.2.dist-info/RECORD,,
134
+ basic_memory-0.13.4.dist-info/METADATA,sha256=V-vcDG3dmeJf3KBCUW47H_SdLHD4vd7fRZlx3tnhztM,15469
135
+ basic_memory-0.13.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
136
+ basic_memory-0.13.4.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
137
+ basic_memory-0.13.4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
138
+ basic_memory-0.13.4.dist-info/RECORD,,