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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
- __version__ = "0.10.0"
3
+ __version__ = "0.10.1"
@@ -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, project_info
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
- console.print(f"[green]Project '{name}' set as default[/green]")
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="Direct access to MCP tools via CLI")
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
- from markdown_it import MarkdownIt
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
- """Parse tags into list of strings."""
61
- if isinstance(tags, (list, tuple)):
62
- return [str(t).strip() for t in tags if str(t).strip()]
63
- return [t.strip() for t in tags.split(",") if t.strip()]
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
- metadata["tags"] = parse_tags(post.metadata.get("tags", []))
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(
@@ -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 [] # pyright: ignore
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
@@ -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 basic-memory, including documents and entities",
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 basic-memory.
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 Optional, List
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: Optional[List[str]] = None,
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]] and uses [[Core Utils]]`
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: Optional list of tags to categorize the note
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 tags]} if tags else None
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 tags:
109
- summary.append(f"\n## Tags\n- {', '.join(tags)}")
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(post, sort_keys=False)
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
@@ -351,4 +351,4 @@ class WatchService:
351
351
  duration_ms=duration_ms,
352
352
  )
353
353
 
354
- await self.write_status()
354
+ await self.write_status()
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.0
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
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
38
38
  [![Tests](https://github.com/basicmachines-co/basic-memory/workflows/Tests/badge.svg)](https://github.com/basicmachines-co/basic-memory/actions)
39
39
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
40
+ ![](https://badge.mcpx.dev?type=server 'MCP Server')
41
+ ![](https://badge.mcpx.dev?type=dev 'MCP Dev')
40
42
  [![smithery badge](https://smithery.ai/badge/@basicmachines-co/basic-memory)](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=YT8crmOfNy-XKuzs53zmYlk7WGRbsDxeU_X1c_UEqAE,123
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=EPumxT4HkprZG9BzWZnym3teOjn24Vm3lgncQADN5ew,3764
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=aBihwtUFs0MpsxCed74A5fYoxFmJA4NOVXPRrVY4lnw,427
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=vEPv34Grn8DGwP8Ig9xcFWeVW0cro_Qyfq2TU22pcQQ,4103
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=DjoB4slhmzfCjIgom1-xBP6sRl_gy7PrhGtQk3rNAqM,8855
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=LnjG_wg38LVN8JndsZJV2UVGPIaoIV5sGs94iQ9PL6k,3781
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=mzVEDUhH98kwETMknjkKw5H697vg_zUapsJkJVi17ho,1894
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=rfp3xPmTsbSMlv_NI91BwGhYXtkn6T9ISvtEWru5kM8,855
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=0PcLCpXe73X72jSudVLVMO8TQwEjnB6F1V9jCtjf2ZE,2999
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=CdUdFitmuDQl8z36IqrbSB0hSEdw20k_ZIXnpV_KDSc,4374
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=p4yP-VngdtfCqbvygQ968tGQVOJ1nFzN3XRyXenEcRM,12432
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=ZcpMfbNpMsKVjKJQ8BaF3CXKpTycOF9HddTsRlefVEQ,13467
95
- basic_memory-0.10.0.dist-info/METADATA,sha256=g4VfqLtcp4NiydgNNfZYwQyJ4kL96jWv8_0GfHbjmEs,12850
96
- basic_memory-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- basic_memory-0.10.0.dist-info/entry_points.txt,sha256=IDQa_VmVTzmvMrpnjhEfM0S3F--XsVGEj3MpdJfuo-Q,59
98
- basic_memory-0.10.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
99
- basic_memory-0.10.0.dist-info/RECORD,,
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)