mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c29dcc2b2 | |||
| 448210e552 | |||
| 3621bb7b4d | |||
| f80ac0ee72 | |||
| 23ddf1918c | |||
| 2aca19aa05 | |||
| 827f7cf3e3 | |||
| bd4f55158b | |||
| 5360005122 | |||
| 39f811f8b5 | |||
| 8e69c8b533 | |||
| 627a5c3c22 | |||
| cd88945b22 | |||
| cd8e372f0a | |||
| a589f8b894 | |||
| c2f4b632cf | |||
| 46d102cef1 |
+168
-40
@@ -1,56 +1,184 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.13.8 (2025-06-20)
|
||||
|
||||
### Features
|
||||
|
||||
- **Docker Container Support** - Complete Docker integration with volume mounting for Obsidian directories
|
||||
([`3269a2f`](https://github.com/basicmachines-co/basic-memory/commit/3269a2f33a7595f6d9e5207924062e2542f46759))
|
||||
- Docker Compose configuration for easy deployment
|
||||
- Volume mounting for persistent data and Obsidian integration
|
||||
- Comprehensive Docker documentation and setup guides
|
||||
- Streamlined container-based workflow
|
||||
## v0.14.2 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#151**: Fix reset command project configuration persistence issue
|
||||
([`af44941`](https://github.com/basicmachines-co/basic-memory/commit/af44941d5aa57b5ad7fcc6af4ed700f49bdb6d4d))
|
||||
- Reset command now properly clears project configuration from `~/.basic-memory/config.json`
|
||||
- Eliminates issue where projects would be recreated after database reset
|
||||
- Ensures clean slate when resetting Basic Memory installation
|
||||
- **#204**: Fix MCP Error with MCP-Hub integration
|
||||
([`3621bb7`](https://github.com/basicmachines-co/basic-memory/commit/3621bb7b4d6ac12d892b18e36bb8f7c9101c7b10))
|
||||
- Resolve compatibility issues with MCP-Hub
|
||||
- Improve error handling in project management tools
|
||||
- Ensure stable MCP tool integration across different environments
|
||||
|
||||
- **#148**: Resolve project state inconsistency between MCP and CLI
|
||||
([`35e4f73`](https://github.com/basicmachines-co/basic-memory/commit/35e4f73ae8a65501da4d48258ed702f957184c92))
|
||||
- Fix "Project not found" errors when switching default projects
|
||||
- MCP session now automatically refreshes when project configuration changes
|
||||
- Eliminates need to restart MCP server after project operations
|
||||
- Ensures consistent project state across CLI and MCP interfaces
|
||||
- **Modernize datetime handling and suppress SQLAlchemy warnings**
|
||||
([`f80ac0e`](https://github.com/basicmachines-co/basic-memory/commit/f80ac0e3e74b7a737a7fc7b956b5c1d61b0c67b8))
|
||||
- Replace deprecated `datetime.utcnow()` with timezone-aware alternatives
|
||||
- Suppress SQLAlchemy deprecation warnings for cleaner output
|
||||
- Improve future compatibility with Python datetime best practices
|
||||
|
||||
- **FastMCP Compatibility** - Resolve deprecation warnings for FastMCP integration
|
||||
([`7be001c`](https://github.com/basicmachines-co/basic-memory/commit/7be001ca6834b3344bb6160cbe537b36bcbaa579))
|
||||
- Update FastMCP usage patterns to eliminate deprecation warnings
|
||||
- Improve future compatibility with FastMCP library updates
|
||||
- Clean up entity repository and service layer code
|
||||
## v0.14.1 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#203**: Constrain fastmcp version to prevent breaking changes
|
||||
([`827f7cf`](https://github.com/basicmachines-co/basic-memory/commit/827f7cf86e7b84c56e7a43bb83f2e5d84a1ad8b8))
|
||||
- Pin fastmcp to compatible version range to avoid API breaking changes
|
||||
- Ensure stable MCP server functionality across updates
|
||||
- Improve dependency management for production deployments
|
||||
|
||||
- **#190**: Fix Problems with MCP integration
|
||||
([`bd4f551`](https://github.com/basicmachines-co/basic-memory/commit/bd4f551a5bb0b7b4d3a5b04de70e08987c6ab2f9))
|
||||
- Resolve MCP server initialization and communication issues
|
||||
- Improve error handling and recovery in MCP operations
|
||||
- Enhance stability for AI assistant integrations
|
||||
|
||||
### Features
|
||||
|
||||
- **Add Cursor IDE integration button** - One-click setup for Cursor IDE users
|
||||
([`5360005`](https://github.com/basicmachines-co/basic-memory/commit/536000512294d66090bf87abc8014f4dfc284310))
|
||||
- Direct installation button for Cursor IDE in README
|
||||
- Streamlined setup process for Cursor users
|
||||
- Enhanced developer experience for AI-powered coding
|
||||
|
||||
- **Add Homebrew installation instructions** - Official Homebrew tap support
|
||||
([`39f811f`](https://github.com/basicmachines-co/basic-memory/commit/39f811f8b57dd998445ae43537cd492c680b2e11))
|
||||
- Official Homebrew formula in basicmachines-co/basic-memory tap
|
||||
- Simplified installation process for macOS users
|
||||
- Package manager integration for easier dependency management
|
||||
|
||||
## v0.14.0 (2025-06-26)
|
||||
|
||||
### Features
|
||||
|
||||
- **Docker Container Registry Migration** - Switch from Docker Hub to GitHub Container Registry for better security and integration
|
||||
([`616c1f0`](https://github.com/basicmachines-co/basic-memory/commit/616c1f0710da59c7098a5f4843d4f017877ff7b2))
|
||||
- Automated Docker image publishing via GitHub Actions CI/CD pipeline
|
||||
- Enhanced container security with GitHub's integrated vulnerability scanning
|
||||
- Streamlined container deployment workflow for production environments
|
||||
|
||||
- **Enhanced Search Documentation** - Comprehensive search syntax examples for improved user experience
|
||||
([`a589f8b`](https://github.com/basicmachines-co/basic-memory/commit/a589f8b894e78cce01eb25656856cfea8785fbbf))
|
||||
- Detailed examples for Boolean search operators (AND, OR, NOT)
|
||||
- Advanced search patterns including phrase matching and field-specific queries
|
||||
- User-friendly documentation for complex search scenarios
|
||||
|
||||
- **Cross-Project File Management** - Intelligent move operations with project boundary detection
|
||||
([`db5ef7d`](https://github.com/basicmachines-co/basic-memory/commit/db5ef7d35cc0894309c7a57b5741c9dd978526d4))
|
||||
- Automatic detection of cross-project move attempts with helpful guidance
|
||||
- Clear error messages when attempting unsupported cross-project operations
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#184**: Preserve permalinks when editing notes without frontmatter permalinks
|
||||
([`c2f4b63`](https://github.com/basicmachines-co/basic-memory/commit/c2f4b632cf04921b1a3c2f0d43831b80c519cb31))
|
||||
- Fix permalink preservation during note editing operations
|
||||
- Ensure consistent permalink handling across different note formats
|
||||
- Maintain note identity and searchability during incremental edits
|
||||
|
||||
- **#183**: Implement project-specific sync status checks for MCP tools
|
||||
([`12b5152`](https://github.com/basicmachines-co/basic-memory/commit/12b51522bc953fca117fc5bc01fcb29c6ca7e13c))
|
||||
- Fix sync status reporting to correctly reflect current project state
|
||||
- Resolve inconsistencies where sync status showed global instead of project-specific information
|
||||
- Improve project isolation for sync operations and status reporting
|
||||
|
||||
- **#180**: Handle Boolean search syntax with hyphenated terms
|
||||
([`546e3cd`](https://github.com/basicmachines-co/basic-memory/commit/546e3cd8db98b74f746749d41887f8a213cd0b11))
|
||||
- Fix search parsing issues with hyphenated terms in Boolean queries
|
||||
- Improve search query tokenization for complex term structures
|
||||
- Enhanced search reliability for technical documentation and multi-word concepts
|
||||
|
||||
- **#174**: Respect BASIC_MEMORY_HOME environment variable in Docker containers
|
||||
([`9f1db23`](https://github.com/basicmachines-co/basic-memory/commit/9f1db23c78d4648e2c242ad1ee27eed85e3f3b5d))
|
||||
- Fix Docker container configuration to properly honor custom home directory settings
|
||||
- Improve containerized deployment flexibility with environment variable support
|
||||
- Ensure consistent behavior between local and containerized installations
|
||||
|
||||
- **#168**: Scope entity queries by project_id in upsert_entity method
|
||||
([`2a3adc1`](https://github.com/basicmachines-co/basic-memory/commit/2a3adc109a3e4d7ccd65cae4abf63d9bb2338326))
|
||||
- Fix entity isolation issues in multi-project setups
|
||||
- Prevent cross-project entity conflicts during database operations
|
||||
- Strengthen project boundary enforcement at the database level
|
||||
|
||||
- **#166**: Handle None from_entity in Context API RelationSummary
|
||||
([`8a065c3`](https://github.com/basicmachines-co/basic-memory/commit/8a065c32f4e41613207d29aafc952a56e3a52241))
|
||||
- Fix null pointer exceptions in relation processing
|
||||
- Improve error handling for incomplete relation data
|
||||
- Enhanced stability for knowledge graph traversal operations
|
||||
|
||||
- **#164**: Remove log level configuration from mcp_server.run()
|
||||
([`224e4bf`](https://github.com/basicmachines-co/basic-memory/commit/224e4bf9e4438c44a82ffc21bd1a282fe9087690))
|
||||
- Simplify MCP server startup by removing redundant log level settings
|
||||
- Fix potential logging configuration conflicts
|
||||
- Streamline server initialization process
|
||||
|
||||
- **#162**: Ensure permalinks are generated for entities with null permalinks during move operations
|
||||
([`f506507`](https://github.com/basicmachines-co/basic-memory/commit/f50650763dbd4322c132e4bdc959ce4bf074374b))
|
||||
- Fix move operations for entities without existing permalinks
|
||||
- Automatic permalink generation during file move operations
|
||||
- Maintain database consistency during file reorganization
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- **Comprehensive Integration Testing** - New test suites for critical user workflows
|
||||
- Full integration tests for database reset functionality
|
||||
- End-to-end project state synchronization testing
|
||||
- Real MCP client-server communication validation
|
||||
- Direct config file validation without complex mocking
|
||||
- **Comprehensive Test Coverage** - Extensive test suites for new features and edge cases
|
||||
- Enhanced test coverage for project-specific sync status functionality
|
||||
- Additional test scenarios for search syntax validation and edge cases
|
||||
- Integration tests for Docker CI workflow and container publishing
|
||||
- Comprehensive move operations testing with project boundary validation
|
||||
|
||||
- **Code Quality** - Enhanced error handling and validation
|
||||
- Improved project state management across system components
|
||||
- Better session refresh patterns for configuration changes
|
||||
- Streamlined Docker setup with reduced image size
|
||||
- **Docker CI/CD Pipeline** - Production-ready automated container publishing
|
||||
([`74847cc`](https://github.com/basicmachines-co/basic-memory/commit/74847cc3807b0c6ed511e0d83e0d560e9f07ec44))
|
||||
- Automated Docker image building and publishing on release
|
||||
- Multi-architecture container support for AMD64 and ARM64 platforms
|
||||
- Integrated security scanning and vulnerability assessments
|
||||
- Streamlined deployment pipeline for production environments
|
||||
|
||||
### Documentation
|
||||
- **Release Process Improvements** - Enhanced automation and quality gates
|
||||
([`a52ce1c`](https://github.com/basicmachines-co/basic-memory/commit/a52ce1c8605ec2cd450d1f909154172cbc30faa2))
|
||||
- Homebrew formula updates limited to stable releases only
|
||||
- Improved release automation with better quality control
|
||||
- Enhanced CI/CD pipeline reliability and error handling
|
||||
|
||||
- **Docker Integration Guide** - Complete documentation for container deployment
|
||||
- Step-by-step Docker Compose setup instructions
|
||||
- Volume mounting configuration for Obsidian workflows
|
||||
- Container-based development environment setup
|
||||
- **Code Quality Enhancements** - Improved error handling and validation
|
||||
- Better null safety in entity and relation processing
|
||||
- Enhanced project isolation validation throughout the codebase
|
||||
- Improved error messages and user guidance for edge cases
|
||||
- Strengthened database consistency guarantees across operations
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **GitHub Container Registry Integration** - Modern container infrastructure
|
||||
- Migration from Docker Hub to GitHub Container Registry (ghcr.io)
|
||||
- Improved security with integrated vulnerability scanning
|
||||
- Better integration with GitHub-based development workflow
|
||||
- Enhanced container versioning and artifact management
|
||||
|
||||
- **Enhanced CI/CD Workflows** - Robust automated testing and deployment
|
||||
- Automated Docker image publishing on releases
|
||||
- Comprehensive test coverage validation before deployment
|
||||
- Multi-platform container building and publishing
|
||||
- Integration with GitHub's security and monitoring tools
|
||||
|
||||
### Migration Guide
|
||||
|
||||
This release includes several behind-the-scenes improvements and fixes. All changes are backward compatible:
|
||||
|
||||
- **Docker Users**: Container images now served from `ghcr.io/basicmachines-co/basic-memory` instead of Docker Hub
|
||||
- **Search Users**: Enhanced search syntax handling - existing queries continue to work unchanged
|
||||
- **Multi-Project Users**: Improved project isolation - all existing projects remain fully functional
|
||||
- **All Users**: Enhanced stability and error handling - no breaking changes to existing workflows
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Latest stable release
|
||||
uv tool install basic-memory
|
||||
|
||||
# Update existing installation
|
||||
uv tool upgrade basic-memory
|
||||
|
||||
# Docker (new registry)
|
||||
docker pull ghcr.io/basicmachines-co/basic-memory:latest
|
||||
```
|
||||
|
||||
## v0.13.7 (2025-06-19)
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
|
||||
# Install with uv (recommended)
|
||||
uv tool install basic-memory
|
||||
|
||||
# or with Homebrew
|
||||
brew tap basicmachines-co/basic-memory
|
||||
brew install basic-memory
|
||||
|
||||
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
|
||||
# Add this to your config:
|
||||
{
|
||||
@@ -66,6 +70,13 @@ npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
|
||||
|
||||
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. Note: The Smithery installation uses their hosted MCP server, while your data remains stored locally as Markdown files.
|
||||
|
||||
### Add to Cursor
|
||||
|
||||
Once you have installed Basic Memory revisit this page for the 1-click installer for Cursor:
|
||||
|
||||
[](https://cursor.com/install-mcp?name=basic-memory&config=eyJjb21tYW5kIjoiL1VzZXJzL2RyZXcvLmxvY2FsL2Jpbi91dnggYmFzaWMtbWVtb3J5IG1jcCJ9)
|
||||
|
||||
|
||||
### Glama.ai
|
||||
|
||||
<a href="https://glama.ai/mcp/servers/o90kttu9ym">
|
||||
@@ -214,7 +225,7 @@ title: <Entity title>
|
||||
type: <The type of Entity> (e.g. note)
|
||||
permalink: <a uri slug>
|
||||
|
||||
- <optional metadata> (such as tags)
|
||||
- <optional metadata> (such as tags)
|
||||
```
|
||||
|
||||
### Observations
|
||||
|
||||
@@ -17,7 +17,7 @@ test: test-unit test-int
|
||||
|
||||
# Lint and fix code
|
||||
lint:
|
||||
ruff check . --fix
|
||||
uv run ruff check . --fix
|
||||
|
||||
# Type check code
|
||||
type-check:
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ dependencies = [
|
||||
"alembic>=1.14.1",
|
||||
"pillow>=11.1.0",
|
||||
"pybars3>=0.9.7",
|
||||
"fastmcp>=2.3.4",
|
||||
"fastmcp>=2.3.4,<2.10.0",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
@@ -123,4 +123,4 @@ omit = [
|
||||
]
|
||||
|
||||
[tool.logfire]
|
||||
ignore_no_config = true
|
||||
ignore_no_config = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.14.0b1"
|
||||
__version__ = "0.14.2"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -38,7 +38,9 @@ def entity_model_from_markdown(
|
||||
# Update basic fields
|
||||
model.title = markdown.frontmatter.title
|
||||
model.entity_type = markdown.frontmatter.type
|
||||
model.permalink = markdown.frontmatter.permalink
|
||||
# Only update permalink if it exists in frontmatter, otherwise preserve existing
|
||||
if markdown.frontmatter.permalink is not None:
|
||||
model.permalink = markdown.frontmatter.permalink
|
||||
model.file_path = str(file_path)
|
||||
model.content_type = "text/markdown"
|
||||
model.created_at = markdown.created
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""OAuth authentication provider for Basic Memory MCP server."""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, Optional
|
||||
|
||||
import jwt
|
||||
@@ -92,7 +92,7 @@ class BasicMemoryOAuthProvider(
|
||||
self.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
|
||||
code=auth_code,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
expires_at=(datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
@@ -119,7 +119,7 @@ class BasicMemoryOAuthProvider(
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check if expired
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
if datetime.now(timezone.utc).timestamp() > code.expires_at:
|
||||
del self.authorization_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
@@ -135,7 +135,7 @@ class BasicMemoryOAuthProvider(
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[access_token] = BasicMemoryAccessToken(
|
||||
token=access_token,
|
||||
@@ -187,7 +187,7 @@ class BasicMemoryOAuthProvider(
|
||||
new_refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store new tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[new_access_token] = BasicMemoryAccessToken(
|
||||
token=new_access_token,
|
||||
@@ -220,7 +220,7 @@ class BasicMemoryOAuthProvider(
|
||||
|
||||
if access_token:
|
||||
# Check if expired
|
||||
if access_token.expires_at and datetime.utcnow().timestamp() > access_token.expires_at:
|
||||
if access_token.expires_at and datetime.now(timezone.utc).timestamp() > access_token.expires_at:
|
||||
logger.debug("Token found in memory but expired, removing")
|
||||
del self.access_tokens[token]
|
||||
return None
|
||||
@@ -262,8 +262,8 @@ class BasicMemoryOAuthProvider(
|
||||
"iss": self.issuer_url,
|
||||
"sub": client_id,
|
||||
"aud": "basic-memory",
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"scopes": scopes,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import os
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import httpx
|
||||
@@ -123,7 +123,7 @@ class SupabaseOAuthProvider(
|
||||
self.pending_auth_codes[state] = SupabaseAuthorizationCode(
|
||||
code=state,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
expires_at=(datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
@@ -218,7 +218,7 @@ class SupabaseOAuthProvider(
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check expiration
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
if datetime.now(timezone.utc).timestamp() > code.expires_at:
|
||||
del self.pending_auth_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
@@ -453,8 +453,8 @@ class SupabaseOAuthProvider(
|
||||
"email": email,
|
||||
"scopes": scopes,
|
||||
"supabase_token": supabase_access_token[:10] + "...", # Reference only
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
"iat": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
# Use Supabase JWT secret if available
|
||||
|
||||
@@ -20,24 +20,24 @@ from basic_memory.mcp.tools.edit_note import edit_note
|
||||
from basic_memory.mcp.tools.move_note import move_note
|
||||
from basic_memory.mcp.tools.sync_status import sync_status
|
||||
from basic_memory.mcp.tools.project_management import (
|
||||
list_projects,
|
||||
list_memory_projects,
|
||||
switch_project,
|
||||
get_current_project,
|
||||
set_default_project,
|
||||
create_project,
|
||||
create_memory_project,
|
||||
delete_project,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"build_context",
|
||||
"canvas",
|
||||
"create_project",
|
||||
"create_memory_project",
|
||||
"delete_note",
|
||||
"delete_project",
|
||||
"edit_note",
|
||||
"get_current_project",
|
||||
"list_directory",
|
||||
"list_projects",
|
||||
"list_memory_projects",
|
||||
"move_note",
|
||||
"read_content",
|
||||
"read_note",
|
||||
|
||||
@@ -5,6 +5,7 @@ and manage project context during conversations.
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from loguru import logger
|
||||
@@ -19,7 +20,9 @@ from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@mcp.tool("list_memory_projects")
|
||||
async def list_projects(ctx: Context | None = None) -> str:
|
||||
async def list_memory_projects(
|
||||
ctx: Context | None = None, _compatibility: Optional[str] = None
|
||||
) -> str:
|
||||
"""List all available projects with their status.
|
||||
|
||||
Shows all Basic Memory projects that are available, indicating which one
|
||||
@@ -29,7 +32,7 @@ async def list_projects(ctx: Context | None = None) -> str:
|
||||
Formatted list of projects with status indicators
|
||||
|
||||
Example:
|
||||
list_projects()
|
||||
list_memory_projects()
|
||||
"""
|
||||
if ctx: # pragma: no cover
|
||||
await ctx.info("Listing all available projects")
|
||||
@@ -144,13 +147,13 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
Your session remains on the previous project.
|
||||
|
||||
## Troubleshooting:
|
||||
1. **Check available projects**: Use `list_projects()` to see valid project names
|
||||
1. **Check available projects**: Use `list_memory_projects()` to see valid project names
|
||||
2. **Verify spelling**: Ensure the project name is spelled correctly
|
||||
3. **Check permissions**: Verify you have access to the requested project
|
||||
4. **Try again**: The error might be temporary
|
||||
|
||||
## Available options:
|
||||
- See all projects: `list_projects()`
|
||||
- See all projects: `list_memory_projects()`
|
||||
- Stay on current project: `get_current_project()`
|
||||
- Try different project: `switch_project("correct-project-name")`
|
||||
|
||||
@@ -159,7 +162,9 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_current_project(ctx: Context | None = None) -> str:
|
||||
async def get_current_project(
|
||||
ctx: Context | None = None, _compatibility: Optional[str] = None
|
||||
) -> str:
|
||||
"""Show the currently active project and basic stats.
|
||||
|
||||
Displays which project is currently active and provides basic information
|
||||
@@ -231,7 +236,7 @@ async def set_default_project(project_name: str, ctx: Context | None = None) ->
|
||||
|
||||
|
||||
@mcp.tool("create_memory_project")
|
||||
async def create_project(
|
||||
async def create_memory_project(
|
||||
project_name: str, project_path: str, set_default: bool = False, ctx: Context | None = None
|
||||
) -> str:
|
||||
"""Create a new Basic Memory project.
|
||||
@@ -248,8 +253,8 @@ async def create_project(
|
||||
Confirmation message with project details
|
||||
|
||||
Example:
|
||||
create_project("my-research", "~/Documents/research")
|
||||
create_project("work-notes", "/home/user/work", set_default=True)
|
||||
create_memory_project("my-research", "~/Documents/research")
|
||||
create_memory_project("work-notes", "/home/user/work", set_default=True)
|
||||
"""
|
||||
if ctx: # pragma: no cover
|
||||
await ctx.info(f"Creating project: {project_name} at {project_path}")
|
||||
|
||||
@@ -45,13 +45,18 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
||||
- Boolean OR: `meeting OR discussion`
|
||||
- Boolean NOT: `project NOT archived`
|
||||
- Grouped: `(project OR planning) AND notes`
|
||||
- Exact phrases: `"weekly standup meeting"`
|
||||
- Content-specific: `tag:example` or `category:observation`
|
||||
|
||||
## Try again with:
|
||||
```
|
||||
search_notes("INSERT_CLEAN_QUERY_HERE")
|
||||
search_notes("{clean_query}")
|
||||
```
|
||||
|
||||
Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
|
||||
## Alternative search strategies:
|
||||
- Break into simpler terms: `search_notes("{" ".join(clean_query.split()[:2])}")`
|
||||
- Try different search types: `search_notes("{clean_query}", search_type="title")`
|
||||
- Use filtering: `search_notes("{clean_query}", types=["entity"])`
|
||||
""").strip()
|
||||
|
||||
# Project not found errors (check before general "not found")
|
||||
@@ -85,24 +90,39 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
||||
|
||||
No content found matching '{query}' in the current project.
|
||||
|
||||
## Suggestions to try:
|
||||
## Search strategy suggestions:
|
||||
1. **Broaden your search**: Try fewer or more general terms
|
||||
- Instead of: `{query}`
|
||||
- Try: `{simplified_query}`
|
||||
|
||||
2. **Check spelling**: Verify terms are spelled correctly
|
||||
3. **Try different search types**:
|
||||
- Text search: `search_notes("{query}", search_type="text")`
|
||||
- Title search: `search_notes("{query}", search_type="title")`
|
||||
- Permalink search: `search_notes("{query}", search_type="permalink")`
|
||||
2. **Check spelling and try variations**:
|
||||
- Verify terms are spelled correctly
|
||||
- Try synonyms or related terms
|
||||
|
||||
4. **Use boolean operators**:
|
||||
- Try OR search for broader results
|
||||
3. **Use different search approaches**:
|
||||
- **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
|
||||
- **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
|
||||
- **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
|
||||
|
||||
## Check what content exists:
|
||||
- Recent activity: `recent_activity(timeframe="7d")`
|
||||
- List files: `list_directory("/")`
|
||||
- Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
|
||||
4. **Try boolean operators for broader results**:
|
||||
- OR search: `search_notes("{" OR ".join(query.split()[:3])}")`
|
||||
- Remove restrictive terms: Focus on the most important keywords
|
||||
|
||||
5. **Use filtering to narrow scope**:
|
||||
- By content type: `search_notes("{query}", types=["entity"])`
|
||||
- By recent content: `search_notes("{query}", after_date="1 week")`
|
||||
- By entity type: `search_notes("{query}", entity_types=["observation"])`
|
||||
|
||||
6. **Try advanced search patterns**:
|
||||
- Tag search: `search_notes("tag:your-tag")`
|
||||
- Category search: `search_notes("category:observation")`
|
||||
- Pattern matching: `search_notes("*{query}*", search_type="permalink")`
|
||||
|
||||
## Explore what content exists:
|
||||
- **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
|
||||
- **List directories**: `list_directory("/")` - Browse all content
|
||||
- **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
|
||||
- **Check project**: `get_current_project()` - Verify you're in the right project
|
||||
""").strip()
|
||||
|
||||
# Server/API errors
|
||||
@@ -151,25 +171,36 @@ You don't have permission to search in the current project: {error_message}
|
||||
|
||||
Error searching for '{query}': {error_message}
|
||||
|
||||
## General troubleshooting:
|
||||
1. **Check your query**: Ensure it uses valid search syntax
|
||||
2. **Try simpler terms**: Use basic words without special characters
|
||||
## Troubleshooting steps:
|
||||
1. **Simplify your query**: Try basic words without special characters
|
||||
2. **Check search syntax**: Ensure boolean operators are correctly formatted
|
||||
3. **Verify project access**: Make sure you can access the current project
|
||||
4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
|
||||
4. **Test with simple search**: Try `search_notes("test")` to verify search is working
|
||||
|
||||
## Alternative approaches:
|
||||
- Browse files: `list_directory("/")`
|
||||
- Try different search type: `search_notes("{query}", search_type="title")`
|
||||
- Search with filters: `search_notes("{query}", types=["entity"])`
|
||||
## Alternative search approaches:
|
||||
- **Different search types**:
|
||||
- Title only: `search_notes("{query}", search_type="title")`
|
||||
- Permalink patterns: `search_notes("{query}*", search_type="permalink")`
|
||||
- **With filters**: `search_notes("{query}", types=["entity"])`
|
||||
- **Recent content**: `search_notes("{query}", after_date="1 week")`
|
||||
- **Boolean variations**: `search_notes("{" OR ".join(query.split()[:2])}")`
|
||||
|
||||
## Need help?
|
||||
- View recent changes: `recent_activity()`
|
||||
- List projects: `list_projects()`
|
||||
- Check current project: `get_current_project()`"""
|
||||
## Explore your content:
|
||||
- **Browse files**: `list_directory("/")` - See all available content
|
||||
- **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
|
||||
- **Project info**: `get_current_project()` - Verify current project
|
||||
- **All projects**: `list_projects()` - Switch to different project if needed
|
||||
|
||||
## Search syntax reference:
|
||||
- **Basic**: `keyword` or `multiple words`
|
||||
- **Boolean**: `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`
|
||||
- **Phrases**: `"exact phrase"`
|
||||
- **Grouping**: `(term1 OR term2) AND term3`
|
||||
- **Patterns**: `tag:example`, `category:observation`"""
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Search across all content in the knowledge base.",
|
||||
description="Search across all content in the knowledge base with advanced syntax support.",
|
||||
)
|
||||
async def search_notes(
|
||||
query: str,
|
||||
@@ -181,24 +212,60 @@ async def search_notes(
|
||||
after_date: Optional[str] = None,
|
||||
project: Optional[str] = None,
|
||||
) -> SearchResponse | str:
|
||||
"""Search across all content in the knowledge base.
|
||||
"""Search across all content in the knowledge base with comprehensive syntax support.
|
||||
|
||||
This tool searches the knowledge base using full-text search, pattern matching,
|
||||
or exact permalink lookup. It supports filtering by content type, entity type,
|
||||
and date.
|
||||
and date, with advanced boolean and phrase search capabilities.
|
||||
|
||||
## Search Syntax Examples
|
||||
|
||||
### Basic Searches
|
||||
- `search_notes("keyword")` - Find any content containing "keyword"
|
||||
- `search_notes("exact phrase")` - Search for exact phrase match
|
||||
|
||||
### Advanced Boolean Searches
|
||||
- `search_notes("term1 term2")` - Find content with both terms (implicit AND)
|
||||
- `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
|
||||
- `search_notes("term1 OR term2")` - Either term can be present
|
||||
- `search_notes("term1 NOT term2")` - Include term1 but exclude term2
|
||||
- `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
|
||||
|
||||
### Content-Specific Searches
|
||||
- `search_notes("tag:example")` - Search within specific tags (if supported by content)
|
||||
- `search_notes("category:observation")` - Filter by observation categories
|
||||
- `search_notes("author:username")` - Find content by author (if metadata available)
|
||||
|
||||
### Search Type Examples
|
||||
- `search_notes("Meeting", search_type="title")` - Search only in titles
|
||||
- `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
|
||||
- `search_notes("keyword", search_type="text")` - Full-text search (default)
|
||||
|
||||
### Filtering Options
|
||||
- `search_notes("query", types=["entity"])` - Search only entities
|
||||
- `search_notes("query", types=["note", "person"])` - Multiple content types
|
||||
- `search_notes("query", entity_types=["observation"])` - Filter by entity type
|
||||
- `search_notes("query", after_date="2024-01-01")` - Recent content only
|
||||
- `search_notes("query", after_date="1 week")` - Relative date filtering
|
||||
|
||||
### Advanced Pattern Examples
|
||||
- `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
|
||||
- `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
|
||||
- `search_notes("bug NOT fixed")` - Exclude resolved issues
|
||||
- `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
query: The search query string (supports boolean operators, phrases, patterns)
|
||||
page: The page number of results to return (default 1)
|
||||
page_size: The number of results to return per page (default 10)
|
||||
search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
|
||||
types: Optional list of note types to search (e.g., ["note", "person"])
|
||||
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
|
||||
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
||||
after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
|
||||
project: Optional project name to search in. If not provided, uses current active project.
|
||||
|
||||
Returns:
|
||||
SearchResponse with results and pagination info
|
||||
SearchResponse with results and pagination info, or helpful error guidance if search fails
|
||||
|
||||
Examples:
|
||||
# Basic text search
|
||||
@@ -216,16 +283,19 @@ async def search_notes(
|
||||
# Boolean search with grouping
|
||||
results = await search_notes("(project OR planning) AND notes")
|
||||
|
||||
# Exact phrase search
|
||||
results = await search_notes("\"weekly standup meeting\"")
|
||||
|
||||
# Search with type filter
|
||||
results = await search_notes(
|
||||
query="meeting notes",
|
||||
types=["entity"],
|
||||
)
|
||||
|
||||
# Search with entity type filter, e.g., note vs
|
||||
# Search with entity type filter
|
||||
results = await search_notes(
|
||||
query="meeting notes",
|
||||
types=["entity"],
|
||||
entity_types=["observation"],
|
||||
)
|
||||
|
||||
# Search for recent content
|
||||
@@ -242,6 +312,13 @@ async def search_notes(
|
||||
|
||||
# Search in specific project
|
||||
results = await search_notes("meeting notes", project="work-project")
|
||||
|
||||
# Complex search with multiple filters
|
||||
results = await search_notes(
|
||||
query="(bug OR issue) AND NOT resolved",
|
||||
types=["entity"],
|
||||
after_date="2024-01-01"
|
||||
)
|
||||
"""
|
||||
# Create a SearchQuery object based on the parameters
|
||||
search_query = SearchQuery()
|
||||
|
||||
@@ -131,7 +131,7 @@ class EntityResponse(SQLAlchemyModel):
|
||||
}
|
||||
"""
|
||||
|
||||
permalink: Permalink
|
||||
permalink: Optional[Permalink]
|
||||
title: str
|
||||
file_path: str
|
||||
entity_type: EntityType
|
||||
|
||||
@@ -173,6 +173,8 @@ def setup_logging(
|
||||
"httpx": logging.WARNING,
|
||||
# File watching logs
|
||||
"watchfiles.main": logging.WARNING,
|
||||
# SQLAlchemy deprecation warnings
|
||||
"sqlalchemy": logging.WARNING,
|
||||
}
|
||||
|
||||
# Set log levels for noisy loggers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for OAuth authentication provider."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from mcp.server.auth.provider import AuthorizationParams
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
from pydantic import AnyHttpUrl
|
||||
@@ -185,7 +185,7 @@ class TestBasicMemoryOAuthProvider:
|
||||
token=token_str,
|
||||
client_id=client.client_id,
|
||||
scopes=["read", "write"],
|
||||
expires_at=int((datetime.utcnow() + timedelta(hours=1)).timestamp()),
|
||||
expires_at=int((datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()),
|
||||
)
|
||||
provider.access_tokens[token_str] = access_token
|
||||
|
||||
@@ -226,7 +226,7 @@ class TestBasicMemoryOAuthProvider:
|
||||
provider.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
|
||||
code=auth_code,
|
||||
scopes=["read"],
|
||||
expires_at=(datetime.utcnow() - timedelta(minutes=1)).timestamp(),
|
||||
expires_at=(datetime.now(timezone.utc) - timedelta(minutes=1)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge="challenge",
|
||||
redirect_uri=AnyHttpUrl("http://localhost:3000/callback"),
|
||||
@@ -288,7 +288,7 @@ class TestBasicMemoryOAuthProvider:
|
||||
token=expired_token_str,
|
||||
client_id="test-client",
|
||||
scopes=["read"],
|
||||
expires_at=int((datetime.utcnow() - timedelta(minutes=1)).timestamp()), # Expired
|
||||
expires_at=int((datetime.now(timezone.utc) - timedelta(minutes=1)).timestamp()), # Expired
|
||||
)
|
||||
provider.access_tokens[expired_token_str] = expired_access_token
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestMCPServer:
|
||||
# Missing SUPABASE_ANON_KEY
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_vars):
|
||||
with patch.dict(os.environ, env_vars, clear=True):
|
||||
with pytest.raises(ValueError, match="SUPABASE_URL and SUPABASE_ANON_KEY must be set"):
|
||||
create_auth_config()
|
||||
|
||||
|
||||
@@ -359,3 +359,42 @@ async def test_edit_note_find_replace_empty_find_text(client):
|
||||
assert isinstance(result, str)
|
||||
assert "# Edit Failed" in result
|
||||
# Should contain helpful guidance about the error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_note_preserves_permalink_when_frontmatter_missing(client):
|
||||
"""Test that editing a note preserves the permalink when frontmatter doesn't contain one.
|
||||
|
||||
This is a regression test for issue #170 where edit_note would fail with a validation error
|
||||
because the permalink was being set to None when the markdown file didn't have a permalink
|
||||
in its frontmatter.
|
||||
"""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
title="Test Note",
|
||||
folder="test",
|
||||
content="# Test Note\nOriginal content here.",
|
||||
)
|
||||
|
||||
# Verify the note was created with a permalink
|
||||
first_result = await edit_note.fn(
|
||||
identifier="test/test-note",
|
||||
operation="append",
|
||||
content="\nFirst edit.",
|
||||
)
|
||||
|
||||
assert isinstance(first_result, str)
|
||||
assert "permalink: test/test-note" in first_result
|
||||
|
||||
# Perform another edit - this should preserve the permalink even if the
|
||||
# file doesn't have a permalink in its frontmatter
|
||||
second_result = await edit_note.fn(
|
||||
identifier="test/test-note",
|
||||
operation="append",
|
||||
content="\nSecond edit.",
|
||||
)
|
||||
|
||||
assert isinstance(second_result, str)
|
||||
assert "Edited note (append)" in second_result
|
||||
assert "permalink: test/test-note" in second_result
|
||||
# The edit should succeed without validation errors
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import patch
|
||||
|
||||
from basic_memory.mcp.tools import write_note
|
||||
from basic_memory.mcp.tools.search import search_notes, _format_search_error_response
|
||||
from basic_memory.schemas.search import SearchResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -23,9 +24,14 @@ async def test_search_text(client):
|
||||
# Search for it
|
||||
response = await search_notes.fn(query="searchable")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify SearchResponse
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -43,9 +49,14 @@ async def test_search_title(client):
|
||||
# Search for it
|
||||
response = await search_notes.fn(query="Search Note", search_type="title")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, str):
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
else:
|
||||
# Success case - verify SearchResponse
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -63,9 +74,14 @@ async def test_search_permalink(client):
|
||||
# Search for it
|
||||
response = await search_notes.fn(query="test/test-search-note", search_type="permalink")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify SearchResponse
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -83,9 +99,14 @@ async def test_search_permalink_match(client):
|
||||
# Search for it
|
||||
response = await search_notes.fn(query="test/test-search-*", search_type="permalink")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify SearchResponse
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -103,9 +124,14 @@ async def test_search_pagination(client):
|
||||
# Search for it
|
||||
response = await search_notes.fn(query="searchable", page=1, page_size=1)
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) == 1
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify SearchResponse
|
||||
assert len(response.results) == 1
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -121,8 +147,13 @@ async def test_search_with_type_filter(client):
|
||||
# Search with type filter
|
||||
response = await search_notes.fn(query="type", types=["note"])
|
||||
|
||||
# Verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -138,8 +169,13 @@ async def test_search_with_entity_type_filter(client):
|
||||
# Search with entity type filter
|
||||
response = await search_notes.fn(query="type", entity_types=["entity"])
|
||||
|
||||
# Verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -156,8 +192,13 @@ async def test_search_with_date_filter(client):
|
||||
one_hour_ago = datetime.now() - timedelta(hours=1)
|
||||
response = await search_notes.fn(query="recent", after_date=one_hour_ago.isoformat())
|
||||
|
||||
# Verify we get results within timeframe
|
||||
assert len(response.results) > 0
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
# Success case - verify we get results within timeframe
|
||||
assert len(response.results) > 0
|
||||
else:
|
||||
# If search failed and returned error message, test should fail with informative message
|
||||
pytest.fail(f"Search failed with error: {response}")
|
||||
|
||||
|
||||
class TestSearchErrorFormatting:
|
||||
@@ -212,7 +253,7 @@ class TestSearchErrorFormatting:
|
||||
|
||||
assert "# Search Failed" in result
|
||||
assert "Error searching for 'test query': unknown error" in result
|
||||
assert "General troubleshooting" in result
|
||||
assert "## Troubleshooting steps:" in result
|
||||
|
||||
|
||||
class TestSearchToolErrorHandling:
|
||||
|
||||
@@ -551,12 +551,14 @@ class TestSearchTermPreparation:
|
||||
def test_boolean_query_empty_parts_coverage(self, search_repository):
|
||||
"""Test Boolean query parsing with empty parts (line 143 coverage)."""
|
||||
# Create queries that will result in empty parts after splitting
|
||||
result1 = search_repository._prepare_boolean_query("hello AND AND world") # Double operator
|
||||
result1 = search_repository._prepare_boolean_query(
|
||||
"hello AND AND world"
|
||||
) # Double operator
|
||||
assert "hello" in result1 and "world" in result1
|
||||
|
||||
|
||||
result2 = search_repository._prepare_boolean_query(" OR test") # Leading operator
|
||||
assert "test" in result2
|
||||
|
||||
|
||||
result3 = search_repository._prepare_boolean_query("test OR ") # Trailing operator
|
||||
assert "test" in result3
|
||||
|
||||
@@ -566,19 +568,19 @@ class TestSearchTermPreparation:
|
||||
result = search_repository._prepare_parenthetical_term('(say "hello" world)')
|
||||
# Should escape quotes by doubling them
|
||||
assert '""hello""' in result
|
||||
|
||||
|
||||
# Test term with single quotes
|
||||
result2 = search_repository._prepare_parenthetical_term('(it\'s working)')
|
||||
result2 = search_repository._prepare_parenthetical_term("(it's working)")
|
||||
assert "it's working" in result2
|
||||
|
||||
def test_needs_quoting_empty_input(self, search_repository):
|
||||
"""Test _needs_quoting with empty inputs (line 207 coverage)."""
|
||||
# Test empty string
|
||||
assert not search_repository._needs_quoting("")
|
||||
|
||||
|
||||
# Test whitespace-only string
|
||||
assert not search_repository._needs_quoting(" ")
|
||||
|
||||
|
||||
# Test None-like cases
|
||||
assert not search_repository._needs_quoting("\t")
|
||||
|
||||
@@ -587,11 +589,11 @@ class TestSearchTermPreparation:
|
||||
# Test empty string
|
||||
result1 = search_repository._prepare_single_term("")
|
||||
assert result1 == ""
|
||||
|
||||
|
||||
# Test whitespace-only string
|
||||
result2 = search_repository._prepare_single_term(" ")
|
||||
assert result2 == " " # Should return as-is
|
||||
|
||||
|
||||
# Test string that becomes empty after strip
|
||||
result3 = search_repository._prepare_single_term("\t\n")
|
||||
assert result3 == "\t\n" # Should return original
|
||||
|
||||
@@ -127,6 +127,36 @@ def test_entity_out_from_attributes():
|
||||
assert len(entity.relations) == 1
|
||||
|
||||
|
||||
def test_entity_response_with_none_permalink():
|
||||
"""Test EntityResponse can handle None permalink (fixes issue #170).
|
||||
|
||||
This test ensures that EntityResponse properly validates when the permalink
|
||||
field is None, which can occur when markdown files don't have explicit
|
||||
permalinks in their frontmatter during edit operations.
|
||||
"""
|
||||
# Simulate database model attributes with None permalink
|
||||
db_data = {
|
||||
"title": "Test Entity",
|
||||
"permalink": None, # This should not cause validation errors
|
||||
"file_path": "test/test-entity.md",
|
||||
"entity_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"observations": [],
|
||||
"relations": [],
|
||||
"created_at": "2023-01-01T00:00:00",
|
||||
"updated_at": "2023-01-01T00:00:00",
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError
|
||||
entity = EntityResponse.model_validate(db_data)
|
||||
assert entity.permalink is None
|
||||
assert entity.title == "Test Entity"
|
||||
assert entity.file_path == "test/test-entity.md"
|
||||
assert entity.entity_type == "note"
|
||||
assert len(entity.observations) == 0
|
||||
assert len(entity.relations) == 0
|
||||
|
||||
|
||||
def test_search_nodes_input():
|
||||
"""Test SearchNodesInput validation."""
|
||||
search = SearchNodesRequest.model_validate({"query": "test query"})
|
||||
|
||||
@@ -121,7 +121,7 @@ requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.14.1" },
|
||||
{ name = "dateparser", specifier = ">=1.2.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" },
|
||||
{ name = "fastmcp", specifier = ">=2.3.4" },
|
||||
{ name = "fastmcp", specifier = ">=2.3.4,<2.10.0" },
|
||||
{ name = "greenlet", specifier = ">=3.1.1" },
|
||||
{ name = "icecream", specifier = ">=2.1.3" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
|
||||
Reference in New Issue
Block a user