basic-memory 0.9.0__py3-none-any.whl → 0.10.1__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/api/routers/project_info_router.py +3 -4
- basic_memory/cli/commands/__init__.py +1 -2
- basic_memory/cli/commands/import_chatgpt.py +5 -6
- basic_memory/cli/commands/import_claude_conversations.py +5 -6
- basic_memory/cli/commands/import_claude_projects.py +5 -6
- basic_memory/cli/commands/project.py +168 -2
- basic_memory/cli/commands/tool.py +5 -1
- basic_memory/config.py +3 -4
- basic_memory/file_utils.py +3 -3
- basic_memory/markdown/entity_parser.py +13 -10
- basic_memory/markdown/markdown_processor.py +1 -1
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/mcp/prompts/ai_assistant_guide.py +3 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +413 -0
- basic_memory/mcp/tools/search.py +2 -2
- basic_memory/mcp/tools/write_note.py +17 -7
- basic_memory/services/entity_service.py +11 -1
- basic_memory/services/file_service.py +3 -5
- basic_memory/sync/watch_service.py +22 -12
- basic_memory/utils.py +27 -1
- basic_memory-0.10.1.dist-info/METADATA +398 -0
- {basic_memory-0.9.0.dist-info → basic_memory-0.10.1.dist-info}/RECORD +26 -26
- basic_memory/cli/commands/project_info.py +0 -167
- basic_memory-0.9.0.dist-info/METADATA +0 -736
- {basic_memory-0.9.0.dist-info → basic_memory-0.10.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.9.0.dist-info → basic_memory-0.10.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.9.0.dist-info → basic_memory-0.10.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
|
|
6
|
-
from fastapi import APIRouter
|
|
7
|
-
from sqlalchemy import text
|
|
8
|
-
|
|
9
6
|
from basic_memory.config import config, config_manager
|
|
10
7
|
from basic_memory.deps import (
|
|
11
8
|
ProjectInfoRepositoryDep,
|
|
@@ -18,6 +15,8 @@ from basic_memory.schemas import (
|
|
|
18
15
|
SystemStatus,
|
|
19
16
|
)
|
|
20
17
|
from basic_memory.sync.watch_service import WATCH_STATUS_JSON
|
|
18
|
+
from fastapi import APIRouter
|
|
19
|
+
from sqlalchemy import text
|
|
21
20
|
|
|
22
21
|
router = APIRouter(prefix="/stats", tags=["statistics"])
|
|
23
22
|
|
|
@@ -262,7 +261,7 @@ async def get_system_status() -> SystemStatus:
|
|
|
262
261
|
watch_status_path = config.home / ".basic-memory" / WATCH_STATUS_JSON
|
|
263
262
|
if watch_status_path.exists():
|
|
264
263
|
try:
|
|
265
|
-
watch_status = json.loads(watch_status_path.read_text())
|
|
264
|
+
watch_status = json.loads(watch_status_path.read_text(encoding="utf-8"))
|
|
266
265
|
except Exception: # pragma: no cover
|
|
267
266
|
pass
|
|
268
267
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""CLI commands for basic-memory."""
|
|
2
2
|
|
|
3
3
|
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
|
|
4
|
-
from . import import_claude_projects, import_chatgpt, tool, project
|
|
4
|
+
from . import import_claude_projects, import_chatgpt, tool, project
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
7
7
|
"status",
|
|
@@ -14,5 +14,4 @@ __all__ = [
|
|
|
14
14
|
"import_chatgpt",
|
|
15
15
|
"tool",
|
|
16
16
|
"project",
|
|
17
|
-
"project_info",
|
|
18
17
|
]
|
|
@@ -7,15 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Dict, Any, List, Annotated, Set, Optional
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
|
-
from loguru import logger
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
14
|
-
|
|
15
10
|
from basic_memory.cli.app import import_app
|
|
16
11
|
from basic_memory.config import config
|
|
17
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
18
13
|
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
19
18
|
|
|
20
19
|
console = Console()
|
|
21
20
|
|
|
@@ -167,7 +166,7 @@ async def process_chatgpt_json(
|
|
|
167
166
|
read_task = progress.add_task("Reading chat data...", total=None)
|
|
168
167
|
|
|
169
168
|
# Read conversations
|
|
170
|
-
conversations = json.loads(json_path.read_text())
|
|
169
|
+
conversations = json.loads(json_path.read_text(encoding="utf-8"))
|
|
171
170
|
progress.update(read_task, total=len(conversations))
|
|
172
171
|
|
|
173
172
|
# Process each conversation
|
|
@@ -7,15 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Dict, Any, List, Annotated
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
|
-
from loguru import logger
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
14
|
-
|
|
15
10
|
from basic_memory.cli.app import claude_app
|
|
16
11
|
from basic_memory.config import config
|
|
17
12
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
18
13
|
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
19
18
|
|
|
20
19
|
console = Console()
|
|
21
20
|
|
|
@@ -124,7 +123,7 @@ async def process_conversations_json(
|
|
|
124
123
|
read_task = progress.add_task("Reading chat data...", total=None)
|
|
125
124
|
|
|
126
125
|
# Read chat data - handle array of arrays format
|
|
127
|
-
data = json.loads(json_path.read_text())
|
|
126
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
128
127
|
conversations = [chat for chat in data]
|
|
129
128
|
progress.update(read_task, total=len(conversations))
|
|
130
129
|
|
|
@@ -6,15 +6,14 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Dict, Any, Annotated, Optional
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
|
-
from loguru import logger
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
13
|
-
|
|
14
9
|
from basic_memory.cli.app import claude_app
|
|
15
10
|
from basic_memory.config import config
|
|
16
11
|
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
|
17
12
|
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
18
17
|
|
|
19
18
|
console = Console()
|
|
20
19
|
|
|
@@ -103,7 +102,7 @@ async def process_projects_json(
|
|
|
103
102
|
read_task = progress.add_task("Reading project data...", total=None)
|
|
104
103
|
|
|
105
104
|
# Read project data
|
|
106
|
-
data = json.loads(json_path.read_text())
|
|
105
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
107
106
|
progress.update(read_task, total=len(data))
|
|
108
107
|
|
|
109
108
|
# Track import counts
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Command module for basic-memory project management."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -9,6 +10,12 @@ from rich.table import Table
|
|
|
9
10
|
|
|
10
11
|
from basic_memory.cli.app import app
|
|
11
12
|
from basic_memory.config import ConfigManager, config
|
|
13
|
+
from basic_memory.mcp.tools.project_info import project_info
|
|
14
|
+
import json
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.tree import Tree
|
|
12
19
|
|
|
13
20
|
console = Console()
|
|
14
21
|
|
|
@@ -92,12 +99,22 @@ def remove_project(
|
|
|
92
99
|
def set_default_project(
|
|
93
100
|
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
|
94
101
|
) -> None:
|
|
95
|
-
"""Set the default project."""
|
|
102
|
+
"""Set the default project and activate it for the current session."""
|
|
96
103
|
config_manager = ConfigManager()
|
|
97
104
|
|
|
98
105
|
try:
|
|
106
|
+
# Set the default project
|
|
99
107
|
config_manager.set_default_project(name)
|
|
100
|
-
|
|
108
|
+
|
|
109
|
+
# Also activate it for the current session by setting the environment variable
|
|
110
|
+
os.environ["BASIC_MEMORY_PROJECT"] = name
|
|
111
|
+
|
|
112
|
+
# Reload configuration to apply the change
|
|
113
|
+
from importlib import reload
|
|
114
|
+
from basic_memory import config as config_module
|
|
115
|
+
|
|
116
|
+
reload(config_module)
|
|
117
|
+
console.print(f"[green]Project '{name}' set as default and activated[/green]")
|
|
101
118
|
except ValueError as e: # pragma: no cover
|
|
102
119
|
console.print(f"[red]Error: {e}[/red]")
|
|
103
120
|
raise typer.Exit(1)
|
|
@@ -117,3 +134,152 @@ def show_current_project() -> None:
|
|
|
117
134
|
except ValueError: # pragma: no cover
|
|
118
135
|
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
|
|
119
136
|
console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@project_app.command("info")
|
|
140
|
+
def display_project_info(
|
|
141
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
142
|
+
):
|
|
143
|
+
"""Display detailed information and statistics about the current project."""
|
|
144
|
+
try:
|
|
145
|
+
# Get project info
|
|
146
|
+
info = asyncio.run(project_info())
|
|
147
|
+
|
|
148
|
+
if json_output:
|
|
149
|
+
# Convert to JSON and print
|
|
150
|
+
print(json.dumps(info.model_dump(), indent=2, default=str))
|
|
151
|
+
else:
|
|
152
|
+
# Create rich display
|
|
153
|
+
console = Console()
|
|
154
|
+
|
|
155
|
+
# Project configuration section
|
|
156
|
+
console.print(
|
|
157
|
+
Panel(
|
|
158
|
+
f"[bold]Project:[/bold] {info.project_name}\n"
|
|
159
|
+
f"[bold]Path:[/bold] {info.project_path}\n"
|
|
160
|
+
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
|
161
|
+
title="📊 Basic Memory Project Info",
|
|
162
|
+
expand=False,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Statistics section
|
|
167
|
+
stats_table = Table(title="📈 Statistics")
|
|
168
|
+
stats_table.add_column("Metric", style="cyan")
|
|
169
|
+
stats_table.add_column("Count", style="green")
|
|
170
|
+
|
|
171
|
+
stats_table.add_row("Entities", str(info.statistics.total_entities))
|
|
172
|
+
stats_table.add_row("Observations", str(info.statistics.total_observations))
|
|
173
|
+
stats_table.add_row("Relations", str(info.statistics.total_relations))
|
|
174
|
+
stats_table.add_row(
|
|
175
|
+
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
|
|
176
|
+
)
|
|
177
|
+
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
|
|
178
|
+
|
|
179
|
+
console.print(stats_table)
|
|
180
|
+
|
|
181
|
+
# Entity types
|
|
182
|
+
if info.statistics.entity_types:
|
|
183
|
+
entity_types_table = Table(title="📑 Entity Types")
|
|
184
|
+
entity_types_table.add_column("Type", style="blue")
|
|
185
|
+
entity_types_table.add_column("Count", style="green")
|
|
186
|
+
|
|
187
|
+
for entity_type, count in info.statistics.entity_types.items():
|
|
188
|
+
entity_types_table.add_row(entity_type, str(count))
|
|
189
|
+
|
|
190
|
+
console.print(entity_types_table)
|
|
191
|
+
|
|
192
|
+
# Most connected entities
|
|
193
|
+
if info.statistics.most_connected_entities:
|
|
194
|
+
connected_table = Table(title="🔗 Most Connected Entities")
|
|
195
|
+
connected_table.add_column("Title", style="blue")
|
|
196
|
+
connected_table.add_column("Permalink", style="cyan")
|
|
197
|
+
connected_table.add_column("Relations", style="green")
|
|
198
|
+
|
|
199
|
+
for entity in info.statistics.most_connected_entities:
|
|
200
|
+
connected_table.add_row(
|
|
201
|
+
entity["title"], entity["permalink"], str(entity["relation_count"])
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
console.print(connected_table)
|
|
205
|
+
|
|
206
|
+
# Recent activity
|
|
207
|
+
if info.activity.recently_updated:
|
|
208
|
+
recent_table = Table(title="🕒 Recent Activity")
|
|
209
|
+
recent_table.add_column("Title", style="blue")
|
|
210
|
+
recent_table.add_column("Type", style="cyan")
|
|
211
|
+
recent_table.add_column("Last Updated", style="green")
|
|
212
|
+
|
|
213
|
+
for entity in info.activity.recently_updated[:5]: # Show top 5
|
|
214
|
+
updated_at = (
|
|
215
|
+
datetime.fromisoformat(entity["updated_at"])
|
|
216
|
+
if isinstance(entity["updated_at"], str)
|
|
217
|
+
else entity["updated_at"]
|
|
218
|
+
)
|
|
219
|
+
recent_table.add_row(
|
|
220
|
+
entity["title"],
|
|
221
|
+
entity["entity_type"],
|
|
222
|
+
updated_at.strftime("%Y-%m-%d %H:%M"),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
console.print(recent_table)
|
|
226
|
+
|
|
227
|
+
# System status
|
|
228
|
+
system_tree = Tree("🖥️ System Status")
|
|
229
|
+
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
|
230
|
+
system_tree.add(
|
|
231
|
+
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Watch status
|
|
235
|
+
if info.system.watch_status: # pragma: no cover
|
|
236
|
+
watch_branch = system_tree.add("Watch Service")
|
|
237
|
+
running = info.system.watch_status.get("running", False)
|
|
238
|
+
status_color = "green" if running else "red"
|
|
239
|
+
watch_branch.add(
|
|
240
|
+
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if running:
|
|
244
|
+
start_time = (
|
|
245
|
+
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
|
246
|
+
if isinstance(info.system.watch_status.get("start_time"), str)
|
|
247
|
+
else info.system.watch_status.get("start_time")
|
|
248
|
+
)
|
|
249
|
+
watch_branch.add(
|
|
250
|
+
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
|
251
|
+
)
|
|
252
|
+
watch_branch.add(
|
|
253
|
+
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
|
254
|
+
)
|
|
255
|
+
watch_branch.add(
|
|
256
|
+
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
system_tree.add("[yellow]Watch service not running[/yellow]")
|
|
260
|
+
|
|
261
|
+
console.print(system_tree)
|
|
262
|
+
|
|
263
|
+
# Available projects
|
|
264
|
+
projects_table = Table(title="📁 Available Projects")
|
|
265
|
+
projects_table.add_column("Name", style="blue")
|
|
266
|
+
projects_table.add_column("Path", style="cyan")
|
|
267
|
+
projects_table.add_column("Default", style="green")
|
|
268
|
+
|
|
269
|
+
for name, path in info.available_projects.items():
|
|
270
|
+
is_default = name == info.default_project
|
|
271
|
+
projects_table.add_row(name, path, "✓" if is_default else "")
|
|
272
|
+
|
|
273
|
+
console.print(projects_table)
|
|
274
|
+
|
|
275
|
+
# Timestamp
|
|
276
|
+
current_time = (
|
|
277
|
+
datetime.fromisoformat(str(info.system.timestamp))
|
|
278
|
+
if isinstance(info.system.timestamp, str)
|
|
279
|
+
else info.system.timestamp
|
|
280
|
+
)
|
|
281
|
+
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
|
|
282
|
+
|
|
283
|
+
except Exception as e: # pragma: no cover
|
|
284
|
+
typer.echo(f"Error getting project info: {e}", err=True)
|
|
285
|
+
raise typer.Exit(1)
|
|
@@ -29,7 +29,7 @@ from basic_memory.schemas.memory import MemoryUrl
|
|
|
29
29
|
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
|
30
30
|
|
|
31
31
|
tool_app = typer.Typer()
|
|
32
|
-
app.add_typer(tool_app, name="tool", help="
|
|
32
|
+
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@tool_app.command()
|
|
@@ -103,6 +103,7 @@ def write_note(
|
|
|
103
103
|
|
|
104
104
|
@tool_app.command()
|
|
105
105
|
def read_note(identifier: str, page: int = 1, page_size: int = 10):
|
|
106
|
+
"""Read a markdown note from the knowledge base."""
|
|
106
107
|
try:
|
|
107
108
|
note = asyncio.run(mcp_read_note(identifier, page, page_size))
|
|
108
109
|
rprint(note)
|
|
@@ -122,6 +123,7 @@ def build_context(
|
|
|
122
123
|
page_size: int = 10,
|
|
123
124
|
max_related: int = 10,
|
|
124
125
|
):
|
|
126
|
+
"""Get context needed to continue a discussion."""
|
|
125
127
|
try:
|
|
126
128
|
context = asyncio.run(
|
|
127
129
|
mcp_build_context(
|
|
@@ -154,6 +156,7 @@ def recent_activity(
|
|
|
154
156
|
page_size: int = 10,
|
|
155
157
|
max_related: int = 10,
|
|
156
158
|
):
|
|
159
|
+
"""Get recent activity across the knowledge base."""
|
|
157
160
|
try:
|
|
158
161
|
context = asyncio.run(
|
|
159
162
|
mcp_recent_activity(
|
|
@@ -189,6 +192,7 @@ def search(
|
|
|
189
192
|
page: int = 1,
|
|
190
193
|
page_size: int = 10,
|
|
191
194
|
):
|
|
195
|
+
"""Search across all content in the knowledge base."""
|
|
192
196
|
if permalink and title: # pragma: no cover
|
|
193
197
|
print("Cannot search both permalink and title")
|
|
194
198
|
raise typer.Abort()
|
basic_memory/config.py
CHANGED
|
@@ -5,13 +5,12 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Dict, Literal, Optional
|
|
7
7
|
|
|
8
|
+
import basic_memory
|
|
9
|
+
from basic_memory.utils import setup_logging
|
|
8
10
|
from loguru import logger
|
|
9
11
|
from pydantic import Field, field_validator
|
|
10
12
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
13
|
|
|
12
|
-
import basic_memory
|
|
13
|
-
from basic_memory.utils import setup_logging
|
|
14
|
-
|
|
15
14
|
DATABASE_NAME = "memory.db"
|
|
16
15
|
DATA_DIR_NAME = ".basic-memory"
|
|
17
16
|
CONFIG_FILE_NAME = "config.json"
|
|
@@ -111,7 +110,7 @@ class ConfigManager:
|
|
|
111
110
|
"""Load configuration from file or create default."""
|
|
112
111
|
if self.config_file.exists():
|
|
113
112
|
try:
|
|
114
|
-
data = json.loads(self.config_file.read_text())
|
|
113
|
+
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
|
115
114
|
return BasicMemoryConfig(**data)
|
|
116
115
|
except Exception as e:
|
|
117
116
|
logger.error(f"Failed to load config: {e}")
|
basic_memory/file_utils.py
CHANGED
|
@@ -85,7 +85,7 @@ async def write_file_atomic(path: FilePath, content: str) -> None:
|
|
|
85
85
|
temp_path = path_obj.with_suffix(".tmp")
|
|
86
86
|
|
|
87
87
|
try:
|
|
88
|
-
temp_path.write_text(content)
|
|
88
|
+
temp_path.write_text(content, encoding="utf-8")
|
|
89
89
|
temp_path.replace(path_obj)
|
|
90
90
|
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
|
|
91
91
|
except Exception as e: # pragma: no cover
|
|
@@ -203,7 +203,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
|
|
203
203
|
path_obj = Path(path) if isinstance(path, str) else path
|
|
204
204
|
|
|
205
205
|
# Read current content
|
|
206
|
-
content = path_obj.read_text()
|
|
206
|
+
content = path_obj.read_text(encoding="utf-8")
|
|
207
207
|
|
|
208
208
|
# Parse current frontmatter
|
|
209
209
|
current_fm = {}
|
|
@@ -215,7 +215,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
|
|
215
215
|
new_fm = {**current_fm, **updates}
|
|
216
216
|
|
|
217
217
|
# Write new file with updated frontmatter
|
|
218
|
-
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
|
218
|
+
yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
|
|
219
219
|
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
|
220
220
|
|
|
221
221
|
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
|
|
@@ -4,21 +4,22 @@ Uses markdown-it with plugins to parse structured data from markdown content.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
-
from pathlib import Path
|
|
8
7
|
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
9
|
from typing import Any, Optional
|
|
10
|
-
import dateparser
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
import dateparser
|
|
13
12
|
import frontmatter
|
|
13
|
+
from markdown_it import MarkdownIt
|
|
14
14
|
|
|
15
15
|
from basic_memory.markdown.plugins import observation_plugin, relation_plugin
|
|
16
16
|
from basic_memory.markdown.schemas import (
|
|
17
|
-
EntityMarkdown,
|
|
18
17
|
EntityFrontmatter,
|
|
18
|
+
EntityMarkdown,
|
|
19
19
|
Observation,
|
|
20
20
|
Relation,
|
|
21
21
|
)
|
|
22
|
+
from basic_memory.utils import parse_tags
|
|
22
23
|
|
|
23
24
|
md = MarkdownIt().use(observation_plugin).use(relation_plugin)
|
|
24
25
|
|
|
@@ -56,11 +57,11 @@ def parse(content: str) -> EntityContent:
|
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def parse_tags(tags: Any) -> list[str]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
# def parse_tags(tags: Any) -> list[str]:
|
|
61
|
+
# """Parse tags into list of strings."""
|
|
62
|
+
# if isinstance(tags, (list, tuple)):
|
|
63
|
+
# return [str(t).strip() for t in tags if str(t).strip()]
|
|
64
|
+
# return [t.strip() for t in tags.split(",") if t.strip()]
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
class EntityParser:
|
|
@@ -101,7 +102,9 @@ class EntityParser:
|
|
|
101
102
|
metadata = post.metadata
|
|
102
103
|
metadata["title"] = post.metadata.get("title", absolute_path.name)
|
|
103
104
|
metadata["type"] = post.metadata.get("type", "note")
|
|
104
|
-
|
|
105
|
+
tags = parse_tags(post.metadata.get("tags", [])) # pyright: ignore
|
|
106
|
+
if tags:
|
|
107
|
+
metadata["tags"] = tags
|
|
105
108
|
|
|
106
109
|
# frontmatter
|
|
107
110
|
entity_frontmatter = EntityFrontmatter(
|
|
@@ -83,7 +83,7 @@ class MarkdownProcessor:
|
|
|
83
83
|
"""
|
|
84
84
|
# Dirty check if needed
|
|
85
85
|
if expected_checksum is not None:
|
|
86
|
-
current_content = path.read_text()
|
|
86
|
+
current_content = path.read_text(encoding="utf-8")
|
|
87
87
|
current_checksum = await file_utils.compute_checksum(current_content)
|
|
88
88
|
if current_checksum != expected_checksum:
|
|
89
89
|
raise DirtyFileError(f"File {path} has been modified")
|
basic_memory/markdown/schemas.py
CHANGED
|
@@ -42,7 +42,7 @@ class EntityFrontmatter(BaseModel):
|
|
|
42
42
|
|
|
43
43
|
@property
|
|
44
44
|
def tags(self) -> List[str]:
|
|
45
|
-
return self.metadata.get("tags") if self.metadata else
|
|
45
|
+
return self.metadata.get("tags") if self.metadata else None # pyright: ignore
|
|
46
46
|
|
|
47
47
|
@property
|
|
48
48
|
def title(self) -> str:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from loguru import logger
|
|
4
|
-
|
|
5
3
|
from basic_memory.mcp.server import mcp
|
|
4
|
+
from loguru import logger
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
@mcp.resource(
|
|
@@ -20,7 +19,7 @@ def ai_assistant_guide() -> str:
|
|
|
20
19
|
A focused guide on Basic Memory usage.
|
|
21
20
|
"""
|
|
22
21
|
logger.info("Loading AI assistant guide resource")
|
|
23
|
-
guide_doc = Path(__file__).parent.parent
|
|
24
|
-
content = guide_doc.read_text()
|
|
22
|
+
guide_doc = Path(__file__).parent.parent / "resources" / "ai_assistant_guide.md"
|
|
23
|
+
content = guide_doc.read_text(encoding="utf-8")
|
|
25
24
|
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
|
|
26
25
|
return content
|