basic-memory 0.10.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/cli/commands/__init__.py +1 -2
- basic_memory/cli/commands/project.py +168 -2
- basic_memory/cli/commands/tool.py +5 -1
- basic_memory/markdown/entity_parser.py +13 -10
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/mcp/prompts/ai_assistant_guide.py +1 -3
- 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/sync/watch_service.py +1 -1
- basic_memory/utils.py +27 -1
- {basic_memory-0.10.0.dist-info → basic_memory-0.10.1.dist-info}/METADATA +13 -1
- {basic_memory-0.10.0.dist-info → basic_memory-0.10.1.dist-info}/RECORD +18 -18
- basic_memory/cli/commands/project_info.py +0 -167
- {basic_memory-0.10.0.dist-info → basic_memory-0.10.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.10.0.dist-info → basic_memory-0.10.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.10.0.dist-info → basic_memory-0.10.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -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
|
]
|
|
@@ -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()
|
|
@@ -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(
|
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:
|
|
@@ -19,9 +19,7 @@ def ai_assistant_guide() -> str:
|
|
|
19
19
|
A focused guide on Basic Memory usage.
|
|
20
20
|
"""
|
|
21
21
|
logger.info("Loading AI assistant guide resource")
|
|
22
|
-
guide_doc = (
|
|
23
|
-
Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
|
|
24
|
-
)
|
|
22
|
+
guide_doc = Path(__file__).parent.parent / "resources" / "ai_assistant_guide.md"
|
|
25
23
|
content = guide_doc.read_text(encoding="utf-8")
|
|
26
24
|
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
|
|
27
25
|
return content
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# AI Assistant Guide for Basic Memory
|
|
2
|
+
|
|
3
|
+
This guide helps AIs use Basic Memory tools effectively when working with users. It covers reading, writing, and
|
|
4
|
+
navigating knowledge through the Model Context Protocol (MCP).
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Basic Memory allows you and users to record context in local Markdown files, building a rich knowledge base through
|
|
9
|
+
natural conversations. The system automatically creates a semantic knowledge graph from simple text patterns.
|
|
10
|
+
|
|
11
|
+
- **Local-First**: All data is stored in plain text files on the user's computer
|
|
12
|
+
- **Real-Time**: Users see content updates immediately
|
|
13
|
+
- **Bi-Directional**: Both you and users can read and edit notes
|
|
14
|
+
- **Semantic**: Simple patterns create a structured knowledge graph
|
|
15
|
+
- **Persistent**: Knowledge persists across sessions and conversations
|
|
16
|
+
|
|
17
|
+
## The Importance of the Knowledge Graph
|
|
18
|
+
|
|
19
|
+
**Basic Memory's value comes from connections between notes, not just the notes themselves.**
|
|
20
|
+
|
|
21
|
+
When writing notes, your primary goal should be creating a rich, interconnected knowledge graph:
|
|
22
|
+
|
|
23
|
+
1. **Increase Semantic Density**: Add multiple observations and relations to each note
|
|
24
|
+
2. **Use Accurate References**: Aim to reference existing entities by their exact titles
|
|
25
|
+
3. **Create Forward References**: Feel free to reference entities that don't exist yet - Basic Memory will resolve these
|
|
26
|
+
when they're created later
|
|
27
|
+
4. **Create Bidirectional Links**: When appropriate, connect entities from both directions
|
|
28
|
+
5. **Use Meaningful Categories**: Add semantic context with appropriate observation categories
|
|
29
|
+
6. **Choose Precise Relations**: Use specific relation types that convey meaning
|
|
30
|
+
|
|
31
|
+
Remember: A knowledge graph with 10 heavily connected notes is more valuable than 20 isolated notes. Your job is to help
|
|
32
|
+
build these connections!
|
|
33
|
+
|
|
34
|
+
## Core Tools Reference
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Writing knowledge - THE MOST IMPORTANT TOOL!
|
|
38
|
+
response = await write_note(
|
|
39
|
+
title="Search Design", # Required: Note title
|
|
40
|
+
content="# Search Design\n...", # Required: Note content
|
|
41
|
+
folder="specs", # Optional: Folder to save in
|
|
42
|
+
tags=["search", "design"], # Optional: Tags for categorization
|
|
43
|
+
verbose=True # Optional: Get parsing details
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Reading knowledge
|
|
47
|
+
content = await read_note("Search Design") # By title
|
|
48
|
+
content = await read_note("specs/search-design") # By path
|
|
49
|
+
content = await read_note("memory://specs/search") # By memory URL
|
|
50
|
+
|
|
51
|
+
# Searching for knowledge
|
|
52
|
+
results = await search(
|
|
53
|
+
query="authentication system", # Text to search for
|
|
54
|
+
page=1, # Optional: Pagination
|
|
55
|
+
page_size=10 # Optional: Results per page
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Building context from the knowledge graph
|
|
59
|
+
context = await build_context(
|
|
60
|
+
url="memory://specs/search", # Starting point
|
|
61
|
+
depth=2, # Optional: How many hops to follow
|
|
62
|
+
timeframe="1 month" # Optional: Recent timeframe
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Checking recent changes
|
|
66
|
+
activity = await recent_activity(
|
|
67
|
+
type="all", # Optional: Entity types to include
|
|
68
|
+
depth=1, # Optional: Related items to include
|
|
69
|
+
timeframe="1 week" # Optional: Time window
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Creating a knowledge visualization
|
|
73
|
+
canvas_result = await canvas(
|
|
74
|
+
nodes=[{"id": "note1", "label": "Search Design"}], # Nodes to display
|
|
75
|
+
edges=[{"from": "note1", "to": "note2"}], # Connections
|
|
76
|
+
title="Project Overview", # Canvas title
|
|
77
|
+
folder="diagrams" # Storage location
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## memory:// URLs Explained
|
|
82
|
+
|
|
83
|
+
Basic Memory uses a special URL format to reference entities in the knowledge graph:
|
|
84
|
+
|
|
85
|
+
- `memory://title` - Reference by title
|
|
86
|
+
- `memory://folder/title` - Reference by folder and title
|
|
87
|
+
- `memory://permalink` - Reference by permalink
|
|
88
|
+
- `memory://path/relation_type/*` - Follow all relations of a specific type
|
|
89
|
+
- `memory://path/*/target` - Find all entities with relations to target
|
|
90
|
+
|
|
91
|
+
## Semantic Markdown Format
|
|
92
|
+
|
|
93
|
+
Knowledge is encoded in standard markdown using simple patterns:
|
|
94
|
+
|
|
95
|
+
**Observations** - Facts about an entity:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
- [category] This is an observation #tag1 #tag2 (optional context)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Relations** - Links between entities:
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
- relation_type [[Target Entity]] (optional context)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Common Categories & Relation Types:**
|
|
108
|
+
|
|
109
|
+
- Categories: `[idea]`, `[decision]`, `[question]`, `[fact]`, `[requirement]`, `[technique]`, `[recipe]`, `[preference]`
|
|
110
|
+
- Relations: `relates_to`, `implements`, `requires`, `extends`, `part_of`, `pairs_with`, `inspired_by`,
|
|
111
|
+
`originated_from`
|
|
112
|
+
|
|
113
|
+
## When to Record Context
|
|
114
|
+
|
|
115
|
+
**Always consider recording context when**:
|
|
116
|
+
|
|
117
|
+
1. Users make decisions or reach conclusions
|
|
118
|
+
2. Important information emerges during conversation
|
|
119
|
+
3. Multiple related topics are discussed
|
|
120
|
+
4. The conversation contains information that might be useful later
|
|
121
|
+
5. Plans, tasks, or action items are mentioned
|
|
122
|
+
|
|
123
|
+
**Protocol for recording context**:
|
|
124
|
+
|
|
125
|
+
1. Identify valuable information in the conversation
|
|
126
|
+
2. Ask the user: "Would you like me to record our discussion about [topic] in Basic Memory?"
|
|
127
|
+
3. If they agree, use `write_note` to capture the information
|
|
128
|
+
4. If they decline, continue without recording
|
|
129
|
+
5. Let the user know when information has been recorded: "I've saved our discussion about [topic] to Basic Memory."
|
|
130
|
+
|
|
131
|
+
## Understanding User Interactions
|
|
132
|
+
|
|
133
|
+
Users will interact with Basic Memory in patterns like:
|
|
134
|
+
|
|
135
|
+
1. **Creating knowledge**:
|
|
136
|
+
```
|
|
137
|
+
Human: "Let's write up what we discussed about search."
|
|
138
|
+
|
|
139
|
+
You: I'll create a note capturing our discussion about the search functionality.
|
|
140
|
+
[Use write_note() to record the conversation details]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
2. **Referencing existing knowledge**:
|
|
144
|
+
```
|
|
145
|
+
Human: "Take a look at memory://specs/search"
|
|
146
|
+
|
|
147
|
+
You: I'll examine that information.
|
|
148
|
+
[Use build_context() to gather related information]
|
|
149
|
+
[Then read_note() to access specific content]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
3. **Finding information**:
|
|
153
|
+
```
|
|
154
|
+
Human: "What were our decisions about auth?"
|
|
155
|
+
|
|
156
|
+
You: Let me find that information for you.
|
|
157
|
+
[Use search() to find relevant notes]
|
|
158
|
+
[Then build_context() to understand connections]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Key Things to Remember
|
|
162
|
+
|
|
163
|
+
1. **Files are Truth**
|
|
164
|
+
- All knowledge lives in local files on the user's computer
|
|
165
|
+
- Users can edit files outside your interaction
|
|
166
|
+
- Changes need to be synced by the user (usually automatic)
|
|
167
|
+
- Always verify information is current with `recent_activity()`
|
|
168
|
+
|
|
169
|
+
2. **Building Context Effectively**
|
|
170
|
+
- Start with specific entities
|
|
171
|
+
- Follow meaningful relations
|
|
172
|
+
- Check recent changes
|
|
173
|
+
- Build context incrementally
|
|
174
|
+
- Combine related information
|
|
175
|
+
|
|
176
|
+
3. **Writing Knowledge Wisely**
|
|
177
|
+
- Using the same title+folder will overwrite existing notes
|
|
178
|
+
- Structure content with clear headings and sections
|
|
179
|
+
- Use semantic markup for observations and relations
|
|
180
|
+
- Keep files organized in logical folders
|
|
181
|
+
|
|
182
|
+
## Common Knowledge Patterns
|
|
183
|
+
|
|
184
|
+
### Capturing Decisions
|
|
185
|
+
|
|
186
|
+
```markdown
|
|
187
|
+
# Coffee Brewing Methods
|
|
188
|
+
|
|
189
|
+
## Context
|
|
190
|
+
|
|
191
|
+
I've experimented with various brewing methods including French press, pour over, and espresso.
|
|
192
|
+
|
|
193
|
+
## Decision
|
|
194
|
+
|
|
195
|
+
Pour over is my preferred method for light to medium roasts because it highlights subtle flavors and offers more control
|
|
196
|
+
over the extraction.
|
|
197
|
+
|
|
198
|
+
## Observations
|
|
199
|
+
|
|
200
|
+
- [technique] Blooming the coffee grounds for 30 seconds improves extraction #brewing
|
|
201
|
+
- [preference] Water temperature between 195-205°F works best #temperature
|
|
202
|
+
- [equipment] Gooseneck kettle provides better control of water flow #tools
|
|
203
|
+
|
|
204
|
+
## Relations
|
|
205
|
+
|
|
206
|
+
- pairs_with [[Light Roast Beans]]
|
|
207
|
+
- contrasts_with [[French Press Method]]
|
|
208
|
+
- requires [[Proper Grinding Technique]]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Recording Project Structure
|
|
212
|
+
|
|
213
|
+
```markdown
|
|
214
|
+
# Garden Planning
|
|
215
|
+
|
|
216
|
+
## Overview
|
|
217
|
+
|
|
218
|
+
This document outlines the garden layout and planting strategy for this season.
|
|
219
|
+
|
|
220
|
+
## Observations
|
|
221
|
+
|
|
222
|
+
- [structure] Raised beds in south corner for sun exposure #layout
|
|
223
|
+
- [structure] Drip irrigation system installed for efficiency #watering
|
|
224
|
+
- [pattern] Companion planting used to deter pests naturally #technique
|
|
225
|
+
|
|
226
|
+
## Relations
|
|
227
|
+
|
|
228
|
+
- contains [[Vegetable Section]]
|
|
229
|
+
- contains [[Herb Garden]]
|
|
230
|
+
- implements [[Organic Gardening Principles]]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Technical Discussions
|
|
234
|
+
|
|
235
|
+
```markdown
|
|
236
|
+
# Recipe Improvement Discussion
|
|
237
|
+
|
|
238
|
+
## Key Points
|
|
239
|
+
|
|
240
|
+
Discussed strategies for improving the chocolate chip cookie recipe.
|
|
241
|
+
|
|
242
|
+
## Observations
|
|
243
|
+
|
|
244
|
+
- [issue] Cookies spread too thin when baked at 350°F #texture
|
|
245
|
+
- [solution] Chilling dough for 24 hours improves flavor and reduces spreading #technique
|
|
246
|
+
- [decision] Will use brown butter instead of regular butter #flavor
|
|
247
|
+
|
|
248
|
+
## Relations
|
|
249
|
+
|
|
250
|
+
- improves [[Basic Cookie Recipe]]
|
|
251
|
+
- inspired_by [[Bakery-Style Cookies]]
|
|
252
|
+
- pairs_with [[Homemade Ice Cream]]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Creating Effective Relations
|
|
256
|
+
|
|
257
|
+
When creating relations, you can:
|
|
258
|
+
|
|
259
|
+
1. Reference existing entities by their exact title
|
|
260
|
+
2. Create forward references to entities that don't exist yet
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# Example workflow for creating notes with effective relations
|
|
264
|
+
async def create_note_with_effective_relations():
|
|
265
|
+
# Search for existing entities to reference
|
|
266
|
+
search_results = await search("travel")
|
|
267
|
+
existing_entities = [result.title for result in search_results.primary_results]
|
|
268
|
+
|
|
269
|
+
# Check if specific entities exist
|
|
270
|
+
packing_tips_exists = "Packing Tips" in existing_entities
|
|
271
|
+
japan_travel_exists = "Japan Travel Guide" in existing_entities
|
|
272
|
+
|
|
273
|
+
# Prepare relations section - include both existing and forward references
|
|
274
|
+
relations_section = "## Relations\n"
|
|
275
|
+
|
|
276
|
+
# Existing reference - exact match to known entity
|
|
277
|
+
if packing_tips_exists:
|
|
278
|
+
relations_section += "- references [[Packing Tips]]\n"
|
|
279
|
+
else:
|
|
280
|
+
# Forward reference - will be linked when that entity is created later
|
|
281
|
+
relations_section += "- references [[Packing Tips]]\n"
|
|
282
|
+
|
|
283
|
+
# Another possible reference
|
|
284
|
+
if japan_travel_exists:
|
|
285
|
+
relations_section += "- part_of [[Japan Travel Guide]]\n"
|
|
286
|
+
|
|
287
|
+
# You can also check recently modified notes to reference them
|
|
288
|
+
recent = await recent_activity(timeframe="1 week")
|
|
289
|
+
recent_titles = [item.title for item in recent.primary_results]
|
|
290
|
+
|
|
291
|
+
if "Transportation Options" in recent_titles:
|
|
292
|
+
relations_section += "- relates_to [[Transportation Options]]\n"
|
|
293
|
+
|
|
294
|
+
# Always include meaningful forward references, even if they don't exist yet
|
|
295
|
+
relations_section += "- located_in [[Tokyo]]\n"
|
|
296
|
+
relations_section += "- visited_during [[Spring 2023 Trip]]\n"
|
|
297
|
+
|
|
298
|
+
# Now create the note with both verified and forward relations
|
|
299
|
+
content = f"""# Tokyo Neighborhood Guide
|
|
300
|
+
|
|
301
|
+
## Overview
|
|
302
|
+
Details about different Tokyo neighborhoods and their unique characteristics.
|
|
303
|
+
|
|
304
|
+
## Observations
|
|
305
|
+
- [area] Shibuya is a busy shopping district #shopping
|
|
306
|
+
- [transportation] Yamanote Line connects major neighborhoods #transit
|
|
307
|
+
- [recommendation] Visit Shimokitazawa for vintage shopping #unique
|
|
308
|
+
- [tip] Get a Suica card for easy train travel #convenience
|
|
309
|
+
|
|
310
|
+
{relations_section}
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
result = await write_note(
|
|
314
|
+
title="Tokyo Neighborhood Guide",
|
|
315
|
+
content=content,
|
|
316
|
+
verbose=True
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# You can check which relations were resolved and which are forward references
|
|
320
|
+
if result and 'relations' in result:
|
|
321
|
+
resolved = [r['to_name'] for r in result['relations'] if r.get('target_id')]
|
|
322
|
+
forward_refs = [r['to_name'] for r in result['relations'] if not r.get('target_id')]
|
|
323
|
+
|
|
324
|
+
print(f"Resolved relations: {resolved}")
|
|
325
|
+
print(f"Forward references that will be resolved later: {forward_refs}")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Error Handling
|
|
329
|
+
|
|
330
|
+
Common issues to watch for:
|
|
331
|
+
|
|
332
|
+
1. **Missing Content**
|
|
333
|
+
```python
|
|
334
|
+
try:
|
|
335
|
+
content = await read_note("Document")
|
|
336
|
+
except:
|
|
337
|
+
# Try search instead
|
|
338
|
+
results = await search("Document")
|
|
339
|
+
if results and results.primary_results:
|
|
340
|
+
# Found something similar
|
|
341
|
+
content = await read_note(results.primary_results[0].permalink)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
2. **Forward References (Unresolved Relations)**
|
|
345
|
+
```python
|
|
346
|
+
response = await write_note(..., verbose=True)
|
|
347
|
+
# Check for forward references (unresolved relations)
|
|
348
|
+
forward_refs = []
|
|
349
|
+
for relation in response.get('relations', []):
|
|
350
|
+
if not relation.get('target_id'):
|
|
351
|
+
forward_refs.append(relation.get('to_name'))
|
|
352
|
+
|
|
353
|
+
if forward_refs:
|
|
354
|
+
# This is a feature, not an error! Inform the user about forward references
|
|
355
|
+
print(f"Note created with forward references to: {forward_refs}")
|
|
356
|
+
print("These will be automatically linked when those notes are created.")
|
|
357
|
+
|
|
358
|
+
# Optionally suggest creating those entities now
|
|
359
|
+
print("Would you like me to create any of these notes now to complete the connections?")
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
3. **Sync Issues**
|
|
363
|
+
```python
|
|
364
|
+
# If information seems outdated
|
|
365
|
+
activity = await recent_activity(timeframe="1 hour")
|
|
366
|
+
if not activity or not activity.primary_results:
|
|
367
|
+
print("It seems there haven't been recent updates. You might need to run 'basic-memory sync'.")
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Best Practices
|
|
371
|
+
|
|
372
|
+
1. **Proactively Record Context**
|
|
373
|
+
- Offer to capture important discussions
|
|
374
|
+
- Record decisions, rationales, and conclusions
|
|
375
|
+
- Link to related topics
|
|
376
|
+
- Ask for permission first: "Would you like me to save our discussion about [topic]?"
|
|
377
|
+
- Confirm when complete: "I've saved our discussion to Basic Memory"
|
|
378
|
+
|
|
379
|
+
2. **Create a Rich Semantic Graph**
|
|
380
|
+
- **Add meaningful observations**: Include at least 3-5 categorized observations in each note
|
|
381
|
+
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
|
|
382
|
+
- **Use existing entities**: Before creating a new relation, search for existing entities
|
|
383
|
+
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
|
|
384
|
+
- **Check accuracy**: Use `search()` or `recent_activity()` to confirm entity titles
|
|
385
|
+
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
|
|
386
|
+
of "relates_to")
|
|
387
|
+
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|
|
388
|
+
|
|
389
|
+
3. **Structure Content Thoughtfully**
|
|
390
|
+
- Use clear, descriptive titles
|
|
391
|
+
- Organize with logical sections (Context, Decision, Implementation, etc.)
|
|
392
|
+
- Include relevant context and background
|
|
393
|
+
- Add semantic observations with appropriate categories
|
|
394
|
+
- Use a consistent format for similar types of notes
|
|
395
|
+
- Balance detail with conciseness
|
|
396
|
+
|
|
397
|
+
4. **Navigate Knowledge Effectively**
|
|
398
|
+
- Start with specific searches
|
|
399
|
+
- Follow relation paths
|
|
400
|
+
- Combine information from multiple sources
|
|
401
|
+
- Verify information is current
|
|
402
|
+
- Build a complete picture before responding
|
|
403
|
+
|
|
404
|
+
5. **Help Users Maintain Their Knowledge**
|
|
405
|
+
- Suggest organizing related topics
|
|
406
|
+
- Identify potential duplicates
|
|
407
|
+
- Recommend adding relations between topics
|
|
408
|
+
- Offer to create summaries of scattered information
|
|
409
|
+
- Suggest potential missing relations: "I notice this might relate to [topic], would you like me to add that
|
|
410
|
+
connection?"
|
|
411
|
+
|
|
412
|
+
Built with ♥️ b
|
|
413
|
+
y Basic Machines
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -9,10 +9,10 @@ from basic_memory.mcp.async_client import client
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@mcp.tool(
|
|
12
|
-
description="Search across all content in
|
|
12
|
+
description="Search across all content in the knowledge base.",
|
|
13
13
|
)
|
|
14
14
|
async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
|
|
15
|
-
"""Search across all content in
|
|
15
|
+
"""Search across all content in the knowledge base.
|
|
16
16
|
|
|
17
17
|
This tool searches the knowledge base using full-text search, pattern matching,
|
|
18
18
|
or exact permalink lookup. It supports filtering by content type, entity type,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Write note tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import List, Union
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
@@ -9,6 +9,13 @@ from basic_memory.mcp.server import mcp
|
|
|
9
9
|
from basic_memory.mcp.tools.utils import call_put
|
|
10
10
|
from basic_memory.schemas import EntityResponse
|
|
11
11
|
from basic_memory.schemas.base import Entity
|
|
12
|
+
from basic_memory.utils import parse_tags
|
|
13
|
+
|
|
14
|
+
# Define TagType as a Union that can accept either a string or a list of strings or None
|
|
15
|
+
TagType = Union[List[str], str, None]
|
|
16
|
+
|
|
17
|
+
# Define TagType as a Union that can accept either a string or a list of strings or None
|
|
18
|
+
TagType = Union[List[str], str, None]
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
@mcp.tool(
|
|
@@ -18,7 +25,7 @@ async def write_note(
|
|
|
18
25
|
title: str,
|
|
19
26
|
content: str,
|
|
20
27
|
folder: str,
|
|
21
|
-
tags
|
|
28
|
+
tags=None, # Remove type hint completely to avoid schema issues
|
|
22
29
|
) -> str:
|
|
23
30
|
"""Write a markdown note to the knowledge base.
|
|
24
31
|
|
|
@@ -40,13 +47,14 @@ async def write_note(
|
|
|
40
47
|
Examples:
|
|
41
48
|
`- depends_on [[Content Parser]] (Need for semantic extraction)`
|
|
42
49
|
`- implements [[Search Spec]] (Initial implementation)`
|
|
43
|
-
`- This feature extends [[Base Design]]
|
|
50
|
+
`- This feature extends [[Base Design]] andst uses [[Core Utils]]`
|
|
44
51
|
|
|
45
52
|
Args:
|
|
46
53
|
title: The title of the note
|
|
47
54
|
content: Markdown content for the note, can include observations and relations
|
|
48
55
|
folder: the folder where the file should be saved
|
|
49
|
-
tags:
|
|
56
|
+
tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
|
|
57
|
+
Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
|
|
50
58
|
|
|
51
59
|
Returns:
|
|
52
60
|
A markdown formatted summary of the semantic content, including:
|
|
@@ -58,8 +66,10 @@ async def write_note(
|
|
|
58
66
|
"""
|
|
59
67
|
logger.info("MCP tool call", tool="write_note", folder=folder, title=title, tags=tags)
|
|
60
68
|
|
|
69
|
+
# Process tags using the helper function
|
|
70
|
+
tag_list = parse_tags(tags)
|
|
61
71
|
# Create the entity request
|
|
62
|
-
metadata = {"tags": [f"#{tag}" for tag in
|
|
72
|
+
metadata = {"tags": [f"#{tag}" for tag in tag_list]} if tag_list else None
|
|
63
73
|
entity = Entity(
|
|
64
74
|
title=title,
|
|
65
75
|
folder=folder,
|
|
@@ -105,8 +115,8 @@ async def write_note(
|
|
|
105
115
|
summary.append(f"- Unresolved: {unresolved}")
|
|
106
116
|
summary.append("\nUnresolved relations will be retried on next sync.")
|
|
107
117
|
|
|
108
|
-
if
|
|
109
|
-
summary.append(f"\n## Tags\n- {', '.join(
|
|
118
|
+
if tag_list:
|
|
119
|
+
summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
|
|
110
120
|
|
|
111
121
|
# Log the response with structured data
|
|
112
122
|
logger.info(
|
|
@@ -141,10 +141,20 @@ class EntityService(BaseService[EntityModel]):
|
|
|
141
141
|
# Convert file path string to Path
|
|
142
142
|
file_path = Path(entity.file_path)
|
|
143
143
|
|
|
144
|
+
# Read existing frontmatter from the file if it exists
|
|
145
|
+
existing_markdown = await self.entity_parser.parse_file(file_path)
|
|
146
|
+
|
|
147
|
+
# Create post with new content from schema
|
|
144
148
|
post = await schema_to_markdown(schema)
|
|
145
149
|
|
|
150
|
+
# Merge new metadata with existing metadata
|
|
151
|
+
existing_markdown.frontmatter.metadata.update(post.metadata)
|
|
152
|
+
|
|
153
|
+
# Create a new post with merged metadata
|
|
154
|
+
merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata)
|
|
155
|
+
|
|
146
156
|
# write file
|
|
147
|
-
final_content = frontmatter.dumps(
|
|
157
|
+
final_content = frontmatter.dumps(merged_post, sort_keys=False)
|
|
148
158
|
checksum = await self.file_service.write_file(file_path, final_content)
|
|
149
159
|
|
|
150
160
|
# parse entity from file
|
basic_memory/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
import re
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Optional, Protocol, Union, runtime_checkable
|
|
9
|
+
from typing import Optional, Protocol, Union, runtime_checkable, List
|
|
10
10
|
|
|
11
11
|
from loguru import logger
|
|
12
12
|
from unidecode import unidecode
|
|
@@ -128,3 +128,29 @@ def setup_logging(
|
|
|
128
128
|
# Set log levels for noisy loggers
|
|
129
129
|
for logger_name, level in noisy_loggers.items():
|
|
130
130
|
logging.getLogger(logger_name).setLevel(level)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
|
134
|
+
"""Parse tags from various input formats into a consistent list.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
tags: Can be a list of strings, a comma-separated string, or None
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A list of tag strings, or an empty list if no tags
|
|
141
|
+
"""
|
|
142
|
+
if tags is None:
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
if isinstance(tags, list):
|
|
146
|
+
return tags
|
|
147
|
+
|
|
148
|
+
if isinstance(tags, str):
|
|
149
|
+
return [tag.strip() for tag in tags.split(",") if tag.strip()]
|
|
150
|
+
|
|
151
|
+
# For any other type, try to convert to string and parse
|
|
152
|
+
try: # pragma: no cover
|
|
153
|
+
return parse_tags(str(tags))
|
|
154
|
+
except (ValueError, TypeError): # pragma: no cover
|
|
155
|
+
logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
|
|
156
|
+
return []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.1
|
|
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
|
|
@@ -37,6 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
[](https://www.python.org/downloads/)
|
|
38
38
|
[](https://github.com/basicmachines-co/basic-memory/actions)
|
|
39
39
|
[](https://github.com/astral-sh/ruff)
|
|
40
|
+

|
|
41
|
+

|
|
40
42
|
[](https://smithery.ai/server/@basicmachines-co/basic-memory)
|
|
41
43
|
|
|
42
44
|
# Basic Memory
|
|
@@ -383,4 +385,14 @@ AGPL-3.0
|
|
|
383
385
|
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
|
|
384
386
|
and submitting PRs.
|
|
385
387
|
|
|
388
|
+
## Star History
|
|
389
|
+
|
|
390
|
+
<a href="https://www.star-history.com/#basicmachines-co/basic-memory&Date">
|
|
391
|
+
<picture>
|
|
392
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date&theme=dark" />
|
|
393
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
|
394
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
|
395
|
+
</picture>
|
|
396
|
+
</a>
|
|
397
|
+
|
|
386
398
|
Built with ♥️ by Basic Machines
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
basic_memory/__init__.py,sha256=
|
|
1
|
+
basic_memory/__init__.py,sha256=1fDmXe64nAOzDOpG-Ekxh0NJVffTMyGSW3sxqA1XBvY,123
|
|
2
2
|
basic_memory/config.py,sha256=PHUrzwjJSHab691mzShz5uZ-uuJyXcznnXA8kjD-wJU,7659
|
|
3
3
|
basic_memory/db.py,sha256=UDWBr52u7oBT4aXputhAG_Prmsv5og00sYVzPmaylhk,6026
|
|
4
4
|
basic_memory/deps.py,sha256=yI6RL_5-8LXw7ywSJ_84BXAczDtv2h9GFLw-E9XDJFg,5770
|
|
5
5
|
basic_memory/file_utils.py,sha256=csvij8o_j14A-rr8NTDeH6pUaI4DdBqNAWJIVc5r4A0,6658
|
|
6
|
-
basic_memory/utils.py,sha256=
|
|
6
|
+
basic_memory/utils.py,sha256=0EfSx_PiTtFkWIDN2wic9nTPIH35B4fddT-GWi8aVsk,4553
|
|
7
7
|
basic_memory/alembic/alembic.ini,sha256=IEZsnF8CbbZnkwBr67LzKKNobHuzTaQNUvM8Psop5xc,3733
|
|
8
8
|
basic_memory/alembic/env.py,sha256=GyQpEpQu84flqAdelxR0-H9nbkHrVoCboYGfmltBDoA,2737
|
|
9
9
|
basic_memory/alembic/migrations.py,sha256=lriHPXDdBLSNXEW3QTpU0SJKuVd1V-8NrVkpN3qfsUQ,718
|
|
@@ -23,34 +23,34 @@ basic_memory/api/routers/search_router.py,sha256=R_a5OF5_8rCjmoOMhmw3M4VLCy6I1KL
|
|
|
23
23
|
basic_memory/cli/__init__.py,sha256=arcKLAWRDhPD7x5t80MlviZeYzwHZ0GZigyy3NKVoGk,33
|
|
24
24
|
basic_memory/cli/app.py,sha256=J4mkWnxbevOYmJwwRMx344olGOxoXq0o4RNG6DMQLKE,1804
|
|
25
25
|
basic_memory/cli/main.py,sha256=9uwxOUc4mDeTeZCEWyJh7X5PzPXG1fva2veV2OPbFtg,1442
|
|
26
|
-
basic_memory/cli/commands/__init__.py,sha256=
|
|
26
|
+
basic_memory/cli/commands/__init__.py,sha256=3oojcC-Y-4RPqff9vtwWziT_T4uvBVicL0pSHNilVkU,393
|
|
27
27
|
basic_memory/cli/commands/db.py,sha256=UL3JXGJrLzKZ-uRwgk6p0kbRznBy5x7keirvweVGNvY,754
|
|
28
28
|
basic_memory/cli/commands/import_chatgpt.py,sha256=M4_oUN9o_BaW5jpKQu2pTEybivB5ccVolhdZzmhLOsI,8162
|
|
29
29
|
basic_memory/cli/commands/import_claude_conversations.py,sha256=D_4-0xFKkZka7xFvvW8OkgjLv3TFqsC_VuB2Z-Y3avU,6827
|
|
30
30
|
basic_memory/cli/commands/import_claude_projects.py,sha256=KzUuf3wrlvJlqTWCzoLRrNxD3OYNteRXaTFj5IB1FA8,6649
|
|
31
31
|
basic_memory/cli/commands/import_memory_json.py,sha256=zqpU4eCzQXx04aRsigddJAyhvklmTgSAzeRTuEdNw0c,5194
|
|
32
32
|
basic_memory/cli/commands/mcp.py,sha256=ue_zDA8w0zZZToHLvu56s8hWkalgZsC64CfTyXX6z2I,715
|
|
33
|
-
basic_memory/cli/commands/project.py,sha256=
|
|
34
|
-
basic_memory/cli/commands/project_info.py,sha256=2XFe0eONsJ-FOmiOO6faYAS9AgX7Dmj4HNeTTrUr0ZE,7099
|
|
33
|
+
basic_memory/cli/commands/project.py,sha256=BSjdz07xDM3R4CUXggv1qhrWLJsEgvGFir6aOUzdr2Q,11330
|
|
35
34
|
basic_memory/cli/commands/status.py,sha256=nbs3myxaNtehEEJ4BBngPuKs-vqZTHNCCb0bTgDsE-s,5277
|
|
36
35
|
basic_memory/cli/commands/sync.py,sha256=JxGJA6b7Qksz0ZKonHfB3s--7qzY7eWeLogPX8tR1pY,8351
|
|
37
|
-
basic_memory/cli/commands/tool.py,sha256=
|
|
36
|
+
basic_memory/cli/commands/tool.py,sha256=JG_G4vG_gWLM4PkDXwl89vYwOLSeGDrTb2h0dswQCkE,9075
|
|
38
37
|
basic_memory/markdown/__init__.py,sha256=DdzioCWtDnKaq05BHYLgL_78FawEHLpLXnp-kPSVfIc,501
|
|
39
|
-
basic_memory/markdown/entity_parser.py,sha256=
|
|
38
|
+
basic_memory/markdown/entity_parser.py,sha256=Pc6hTm9TB5SRW_ghf7WD-UMjEgOPyc8j8tgCtxEWfLQ,3893
|
|
40
39
|
basic_memory/markdown/markdown_processor.py,sha256=S5ny69zu2dlqO7tWJoLrpLSzg8emQIDq7Du7olpJUsk,4968
|
|
41
40
|
basic_memory/markdown/plugins.py,sha256=gtIzKRjoZsyvBqLpVNnrmzl_cbTZ5ZGn8kcuXxQjRko,6639
|
|
42
|
-
basic_memory/markdown/schemas.py,sha256=
|
|
41
|
+
basic_memory/markdown/schemas.py,sha256=eyxYCr1hVyWmImcle0asE5It_DD6ARkqaBZYu1KK5n4,1896
|
|
43
42
|
basic_memory/markdown/utils.py,sha256=zlHlUtrnXaUCnsaPNJzR0wlhg2kB1YbXx0DMvs-snJM,2973
|
|
44
43
|
basic_memory/mcp/__init__.py,sha256=dsDOhKqjYeIbCULbHIxfcItTbqudEuEg1Np86eq0GEQ,35
|
|
45
44
|
basic_memory/mcp/async_client.py,sha256=Eo345wANiBRSM4u3j_Vd6Ax4YtMg7qbWd9PIoFfj61I,236
|
|
46
45
|
basic_memory/mcp/main.py,sha256=0kbcyf1PxRC1bLnHv2zzParfJ6cOq7Am9ScF9UoI50U,703
|
|
47
46
|
basic_memory/mcp/server.py,sha256=VGv0uWma6JGkT6Y_GESYGhGMYfPavkhEKlCNza8bvtY,287
|
|
48
47
|
basic_memory/mcp/prompts/__init__.py,sha256=-Bl9Dgj2TD9PULjzggPqXuvPEjWCRy7S9Yg03h2-U7A,615
|
|
49
|
-
basic_memory/mcp/prompts/ai_assistant_guide.py,sha256=
|
|
48
|
+
basic_memory/mcp/prompts/ai_assistant_guide.py,sha256=8TI5xObiRVcwv6w9by1xQHlX0whvyE7-LGsiqDMRTFg,821
|
|
50
49
|
basic_memory/mcp/prompts/continue_conversation.py,sha256=zb_3cOaO7NMFuStBkJDlMstQZqz1RCOYl6txwaHYM_Q,4424
|
|
51
50
|
basic_memory/mcp/prompts/recent_activity.py,sha256=7607MWiGJWY0vPurhVII17LxLZlXY_zmH3xH9LfT6SY,2793
|
|
52
51
|
basic_memory/mcp/prompts/search.py,sha256=nCz5wPxTszJLNQJ1CE7CIhnamy08EpGLQjoBMlXRRNc,6283
|
|
53
52
|
basic_memory/mcp/prompts/utils.py,sha256=u_bG8DMtMMERvGPJfA3gbl5VAs0xmkuK8ZJBkY8xyV8,5371
|
|
53
|
+
basic_memory/mcp/resources/ai_assistant_guide.md,sha256=EIMQlESWX1pZ19Ia3jMuodW44aCM8u8l78vQlwvPwfs,14747
|
|
54
54
|
basic_memory/mcp/tools/__init__.py,sha256=mp8BiY-2YY5zzGBAIbf9hMCQM6uhDtst3eq1ApR2p2s,870
|
|
55
55
|
basic_memory/mcp/tools/build_context.py,sha256=8xYRPpeYCEU8F9Dv_ctvbunZ8ciKwmFu9i8Pdv5vYfI,2891
|
|
56
56
|
basic_memory/mcp/tools/canvas.py,sha256=fHC90eshnSSmROTBV-tBB-FSuXSpYVj_BcDrc96pWi0,2993
|
|
@@ -59,9 +59,9 @@ basic_memory/mcp/tools/project_info.py,sha256=pyoHpOMhjMIvZFku2iEIpXc2XDtbnNeb-O
|
|
|
59
59
|
basic_memory/mcp/tools/read_content.py,sha256=PKnvLzNmHfzoIxRKXNaYW5P5q0d1azVwG9juPXPYeQo,8148
|
|
60
60
|
basic_memory/mcp/tools/read_note.py,sha256=pM6FUxMdDxxCNxhnDrkrVqIJouIRPbUqSHsL3BVgiy8,6469
|
|
61
61
|
basic_memory/mcp/tools/recent_activity.py,sha256=S0LgIk9RaeYzIsi2FIHs0KK7R1K-LJy3QaSokGlY9ew,3501
|
|
62
|
-
basic_memory/mcp/tools/search.py,sha256=
|
|
62
|
+
basic_memory/mcp/tools/search.py,sha256=zyIcPC2z9485FFcJCnWi7Rv-RIG_BTRo8jS7d03rB3s,2978
|
|
63
63
|
basic_memory/mcp/tools/utils.py,sha256=tOWklfSlDcoAJCRBmxkCVwkTY_TDBa5vOGxzU8J5eiQ,13636
|
|
64
|
-
basic_memory/mcp/tools/write_note.py,sha256=
|
|
64
|
+
basic_memory/mcp/tools/write_note.py,sha256=Z2z92pHb74-uLzahY2Cz5Kj8kGikS4VSWPZ1I1rGy1U,4942
|
|
65
65
|
basic_memory/models/__init__.py,sha256=Bf0xXV_ryndogvZDiVM_Wb6iV2fHUxYNGMZNWNcZi0s,307
|
|
66
66
|
basic_memory/models/base.py,sha256=4hAXJ8CE1RnjKhb23lPd-QM7G_FXIdTowMJ9bRixspU,225
|
|
67
67
|
basic_memory/models/knowledge.py,sha256=lbKd8VOOVPqXtIhNMY30bIokoQutFjLpHwLD5At90MY,6644
|
|
@@ -83,7 +83,7 @@ basic_memory/schemas/response.py,sha256=lVYR31DTtSeFRddGWX_wQWnQgyiwX0LEpNJ4f4lK
|
|
|
83
83
|
basic_memory/schemas/search.py,sha256=mfPHo8lzZ8BMLzmID-0g_0pWHhBIBNIvy4c8KYHFuvQ,3655
|
|
84
84
|
basic_memory/services/__init__.py,sha256=oop6SKmzV4_NAYt9otGnupLGVCCKIVgxEcdRQWwh25I,197
|
|
85
85
|
basic_memory/services/context_service.py,sha256=fhJNHQoTEeIC9ZmZ49CXcNF2aVBghVnmo6LtdSDcAas,9708
|
|
86
|
-
basic_memory/services/entity_service.py,sha256=
|
|
86
|
+
basic_memory/services/entity_service.py,sha256=MlJLpZbr67KpSEZXi1yBJSkA_a7_9Ysa-aZnyEi6b7I,12896
|
|
87
87
|
basic_memory/services/exceptions.py,sha256=VGlCLd4UD2w5NWKqC7QpG4jOM_hA7jKRRM-MqvEVMNk,288
|
|
88
88
|
basic_memory/services/file_service.py,sha256=7uLVkcFMFj20srdch8c6u9T6nO5X4wHgcXdL81pGV88,9935
|
|
89
89
|
basic_memory/services/link_resolver.py,sha256=yWqqKqJtGU_93xy25y6Us4xRTNijrBLz76Nvm_zFEOI,3326
|
|
@@ -91,9 +91,9 @@ basic_memory/services/search_service.py,sha256=t3d5jhABs5bXwtOu7_AvRCpVd8RRd2j6G
|
|
|
91
91
|
basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
|
|
92
92
|
basic_memory/sync/__init__.py,sha256=CVHguYH457h2u2xoM8KvOilJC71XJlZ-qUh8lHcjYj4,156
|
|
93
93
|
basic_memory/sync/sync_service.py,sha256=YirSOgk0PyqPJoHXVUzAxhNKdd2pebP8sFeXeAYmGjM,21957
|
|
94
|
-
basic_memory/sync/watch_service.py,sha256=
|
|
95
|
-
basic_memory-0.10.
|
|
96
|
-
basic_memory-0.10.
|
|
97
|
-
basic_memory-0.10.
|
|
98
|
-
basic_memory-0.10.
|
|
99
|
-
basic_memory-0.10.
|
|
94
|
+
basic_memory/sync/watch_service.py,sha256=IliisNN8HMKLnBaF95wGHpA5WqRWujprWJVxOnAQ2yc,13468
|
|
95
|
+
basic_memory-0.10.1.dist-info/METADATA,sha256=B0EeeYGFTpEe0VMSaoPT-w-mGIPPB-iNt7uMDaEYERQ,13483
|
|
96
|
+
basic_memory-0.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
97
|
+
basic_memory-0.10.1.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
|
|
98
|
+
basic_memory-0.10.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
99
|
+
basic_memory-0.10.1.dist-info/RECORD,,
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"""CLI command for project info status."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
from rich.console import Console
|
|
9
|
-
from rich.table import Table
|
|
10
|
-
from rich.panel import Panel
|
|
11
|
-
from rich.tree import Tree
|
|
12
|
-
|
|
13
|
-
from basic_memory.cli.app import app
|
|
14
|
-
from basic_memory.mcp.tools.project_info import project_info
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
info_app = typer.Typer()
|
|
18
|
-
app.add_typer(info_app, name="info", help="Get information about your Basic Memory project")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@info_app.command("stats")
|
|
22
|
-
def display_project_info(
|
|
23
|
-
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
24
|
-
):
|
|
25
|
-
"""Display detailed information and statistics about the current project."""
|
|
26
|
-
try:
|
|
27
|
-
# Get project info
|
|
28
|
-
info = asyncio.run(project_info())
|
|
29
|
-
|
|
30
|
-
if json_output:
|
|
31
|
-
# Convert to JSON and print
|
|
32
|
-
print(json.dumps(info.model_dump(), indent=2, default=str))
|
|
33
|
-
else:
|
|
34
|
-
# Create rich display
|
|
35
|
-
console = Console()
|
|
36
|
-
|
|
37
|
-
# Project configuration section
|
|
38
|
-
console.print(
|
|
39
|
-
Panel(
|
|
40
|
-
f"[bold]Project:[/bold] {info.project_name}\n"
|
|
41
|
-
f"[bold]Path:[/bold] {info.project_path}\n"
|
|
42
|
-
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
|
43
|
-
title="📊 Basic Memory Project Info",
|
|
44
|
-
expand=False,
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
# Statistics section
|
|
49
|
-
stats_table = Table(title="📈 Statistics")
|
|
50
|
-
stats_table.add_column("Metric", style="cyan")
|
|
51
|
-
stats_table.add_column("Count", style="green")
|
|
52
|
-
|
|
53
|
-
stats_table.add_row("Entities", str(info.statistics.total_entities))
|
|
54
|
-
stats_table.add_row("Observations", str(info.statistics.total_observations))
|
|
55
|
-
stats_table.add_row("Relations", str(info.statistics.total_relations))
|
|
56
|
-
stats_table.add_row(
|
|
57
|
-
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
|
|
58
|
-
)
|
|
59
|
-
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
|
|
60
|
-
|
|
61
|
-
console.print(stats_table)
|
|
62
|
-
|
|
63
|
-
# Entity types
|
|
64
|
-
if info.statistics.entity_types:
|
|
65
|
-
entity_types_table = Table(title="📑 Entity Types")
|
|
66
|
-
entity_types_table.add_column("Type", style="blue")
|
|
67
|
-
entity_types_table.add_column("Count", style="green")
|
|
68
|
-
|
|
69
|
-
for entity_type, count in info.statistics.entity_types.items():
|
|
70
|
-
entity_types_table.add_row(entity_type, str(count))
|
|
71
|
-
|
|
72
|
-
console.print(entity_types_table)
|
|
73
|
-
|
|
74
|
-
# Most connected entities
|
|
75
|
-
if info.statistics.most_connected_entities:
|
|
76
|
-
connected_table = Table(title="🔗 Most Connected Entities")
|
|
77
|
-
connected_table.add_column("Title", style="blue")
|
|
78
|
-
connected_table.add_column("Permalink", style="cyan")
|
|
79
|
-
connected_table.add_column("Relations", style="green")
|
|
80
|
-
|
|
81
|
-
for entity in info.statistics.most_connected_entities:
|
|
82
|
-
connected_table.add_row(
|
|
83
|
-
entity["title"], entity["permalink"], str(entity["relation_count"])
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
console.print(connected_table)
|
|
87
|
-
|
|
88
|
-
# Recent activity
|
|
89
|
-
if info.activity.recently_updated:
|
|
90
|
-
recent_table = Table(title="🕒 Recent Activity")
|
|
91
|
-
recent_table.add_column("Title", style="blue")
|
|
92
|
-
recent_table.add_column("Type", style="cyan")
|
|
93
|
-
recent_table.add_column("Last Updated", style="green")
|
|
94
|
-
|
|
95
|
-
for entity in info.activity.recently_updated[:5]: # Show top 5
|
|
96
|
-
updated_at = (
|
|
97
|
-
datetime.fromisoformat(entity["updated_at"])
|
|
98
|
-
if isinstance(entity["updated_at"], str)
|
|
99
|
-
else entity["updated_at"]
|
|
100
|
-
)
|
|
101
|
-
recent_table.add_row(
|
|
102
|
-
entity["title"],
|
|
103
|
-
entity["entity_type"],
|
|
104
|
-
updated_at.strftime("%Y-%m-%d %H:%M"),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
console.print(recent_table)
|
|
108
|
-
|
|
109
|
-
# System status
|
|
110
|
-
system_tree = Tree("🖥️ System Status")
|
|
111
|
-
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
|
112
|
-
system_tree.add(
|
|
113
|
-
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
# Watch status
|
|
117
|
-
if info.system.watch_status: # pragma: no cover
|
|
118
|
-
watch_branch = system_tree.add("Watch Service")
|
|
119
|
-
running = info.system.watch_status.get("running", False)
|
|
120
|
-
status_color = "green" if running else "red"
|
|
121
|
-
watch_branch.add(
|
|
122
|
-
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if running:
|
|
126
|
-
start_time = (
|
|
127
|
-
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
|
128
|
-
if isinstance(info.system.watch_status.get("start_time"), str)
|
|
129
|
-
else info.system.watch_status.get("start_time")
|
|
130
|
-
)
|
|
131
|
-
watch_branch.add(
|
|
132
|
-
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
|
133
|
-
)
|
|
134
|
-
watch_branch.add(
|
|
135
|
-
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
|
136
|
-
)
|
|
137
|
-
watch_branch.add(
|
|
138
|
-
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'}]"
|
|
139
|
-
)
|
|
140
|
-
else:
|
|
141
|
-
system_tree.add("[yellow]Watch service not running[/yellow]")
|
|
142
|
-
|
|
143
|
-
console.print(system_tree)
|
|
144
|
-
|
|
145
|
-
# Available projects
|
|
146
|
-
projects_table = Table(title="📁 Available Projects")
|
|
147
|
-
projects_table.add_column("Name", style="blue")
|
|
148
|
-
projects_table.add_column("Path", style="cyan")
|
|
149
|
-
projects_table.add_column("Default", style="green")
|
|
150
|
-
|
|
151
|
-
for name, path in info.available_projects.items():
|
|
152
|
-
is_default = name == info.default_project
|
|
153
|
-
projects_table.add_row(name, path, "✓" if is_default else "")
|
|
154
|
-
|
|
155
|
-
console.print(projects_table)
|
|
156
|
-
|
|
157
|
-
# Timestamp
|
|
158
|
-
current_time = (
|
|
159
|
-
datetime.fromisoformat(str(info.system.timestamp))
|
|
160
|
-
if isinstance(info.system.timestamp, str)
|
|
161
|
-
else info.system.timestamp
|
|
162
|
-
)
|
|
163
|
-
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
|
|
164
|
-
|
|
165
|
-
except Exception as e: # pragma: no cover
|
|
166
|
-
typer.echo(f"Error getting project info: {e}", err=True)
|
|
167
|
-
raise typer.Exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|