Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0cc559426 | |||
| 7460a938df | |||
| 43fa5762a8 | |||
| fb1350b294 | |||
| 7585a29c96 | |||
| 752c78c379 | |||
| a4a3b1b689 | |||
| 6361574a20 | |||
| 24a1d6195d | |||
| a0cf62375d | |||
| 473f70c949 | |||
| 2c29dcc2b2 | |||
| 448210e552 | |||
| 3621bb7b4d | |||
| f80ac0ee72 | |||
| 23ddf1918c | |||
| 2aca19aa05 | |||
| 827f7cf3e3 | |||
| bd4f55158b | |||
| 5360005122 | |||
| 39f811f8b5 | |||
| 8e69c8b533 | |||
| 627a5c3c22 | |||
| cd88945b22 | |||
| cd8e372f0a | |||
| a589f8b894 | |||
| c2f4b632cf | |||
| 46d102cef1 | |||
| 8e4dc026ce | |||
| 7af8e198c2 | |||
| 12b51522bc | |||
| ac9e148bcc | |||
| 546e3cd8db | |||
| de4737cc22 | |||
| 77eefeb252 | |||
| e5923a0378 | |||
| 1bf348259b | |||
| 224e4bf9e4 | |||
| 9f1db23c78 | |||
| db5ef7d35c | |||
| f50650763d | |||
| 8a065c32f4 | |||
| 2a3adc109a | |||
| a52ce1c860 | |||
| 616c1f0710 | |||
| 74847cc380 | |||
| d3b6c85184 | |||
| af44941d5a | |||
| 35e4f73ae8 | |||
| 7be001ca68 | |||
| 3269a2f33a | |||
| b8191d090f | |||
| 2ce8a8e4b0 | |||
| f8099cd004 | |||
| 688e0b0971 | |||
| ed09ea4ec7 | |||
| c85d9f74d7 | |||
| 84d2aaf641 | |||
| 7789864493 | |||
| c6215fd819 | |||
| b4c26a6133 | |||
| 3fdce683d7 | |||
| 782cb2df28 | |||
| 56c875f137 | |||
| 5049de7e2d | |||
| 49011768f7 | |||
| bc3557f000 | |||
| 611f5cd305 | |||
| 4ea392d284 | |||
| d491757980 | |||
| 7a69ca2c36 | |||
| 70a6ce3411 | |||
| 5b69fd65cd |
@@ -1,190 +0,0 @@
|
||||
# /project:check-health - Project Health Assessment
|
||||
|
||||
Comprehensive health check of the Basic Memory project including code quality, test coverage, dependencies, and documentation.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/project:check-health
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert DevOps engineer for the Basic Memory project. When the user runs `/project:check-health`, execute the following comprehensive assessment:
|
||||
|
||||
### Step 1: Git Repository Health
|
||||
1. **Repository Status**
|
||||
```bash
|
||||
git status
|
||||
git log --oneline -5
|
||||
git branch -vv
|
||||
```
|
||||
- Check working directory status
|
||||
- Verify branch alignment with remote
|
||||
- Check recent commit activity
|
||||
|
||||
2. **Branch Analysis**
|
||||
- Verify on main branch
|
||||
- Check if ahead/behind remote
|
||||
- Identify any untracked files
|
||||
|
||||
### Step 2: Code Quality Assessment
|
||||
1. **Linting and Formatting**
|
||||
```bash
|
||||
uv run ruff check .
|
||||
uv run pyright
|
||||
```
|
||||
- Count linting issues by severity
|
||||
- Check type annotation coverage
|
||||
- Verify code formatting compliance
|
||||
|
||||
2. **Test Suite Health**
|
||||
```bash
|
||||
uv run pytest --collect-only -q
|
||||
uv run pytest --co -q | wc -l
|
||||
```
|
||||
- Count total tests
|
||||
- Check for test discovery issues
|
||||
- Verify test structure integrity
|
||||
|
||||
### Step 3: Dependency Analysis
|
||||
1. **Dependency Health**
|
||||
```bash
|
||||
uv tree
|
||||
uv lock --dry-run
|
||||
```
|
||||
- Check for dependency conflicts
|
||||
- Identify outdated dependencies
|
||||
- Verify lock file consistency
|
||||
|
||||
2. **Security Scan**
|
||||
```bash
|
||||
uv run pip-audit --desc
|
||||
```
|
||||
- Scan for known vulnerabilities
|
||||
- Check dependency licenses
|
||||
- Identify security advisories
|
||||
|
||||
### Step 4: Performance Metrics
|
||||
1. **Test Performance**
|
||||
```bash
|
||||
uv run pytest --durations=10
|
||||
```
|
||||
- Identify slowest tests
|
||||
- Check overall test execution time
|
||||
- Monitor test suite growth
|
||||
|
||||
2. **Build Performance**
|
||||
```bash
|
||||
time uv run python -c "import basic_memory"
|
||||
```
|
||||
- Check import time
|
||||
- Validate package installation
|
||||
- Monitor startup performance
|
||||
|
||||
### Step 5: Documentation Health
|
||||
1. **Documentation Coverage**
|
||||
- Check README.md currency
|
||||
- Verify CLI documentation
|
||||
- Validate MCP tool documentation
|
||||
- Check changelog completeness
|
||||
|
||||
2. **API Documentation**
|
||||
- Verify docstring coverage
|
||||
- Check type annotation completeness
|
||||
- Validate example code
|
||||
|
||||
### Step 6: Project Metrics
|
||||
1. **Code Statistics**
|
||||
```bash
|
||||
find src -name "*.py" | xargs wc -l
|
||||
find tests -name "*.py" | xargs wc -l
|
||||
```
|
||||
- Lines of code trends
|
||||
- Test-to-code ratio
|
||||
- File organization metrics
|
||||
|
||||
## Health Report Format
|
||||
|
||||
Generate comprehensive health dashboard:
|
||||
|
||||
```
|
||||
🏥 Basic Memory Project Health Report
|
||||
|
||||
📊 OVERALL HEALTH: 🟢 EXCELLENT (92/100)
|
||||
|
||||
🗂️ GIT REPOSITORY
|
||||
✅ Clean working directory
|
||||
✅ Up to date with origin/main
|
||||
✅ Recent commit activity (5 commits this week)
|
||||
|
||||
🔍 CODE QUALITY
|
||||
✅ Linting: 0 errors, 2 warnings
|
||||
✅ Type checking: 100% coverage
|
||||
✅ Formatting: Compliant
|
||||
⚠️ Complex functions: 3 need refactoring
|
||||
|
||||
🧪 TEST SUITE
|
||||
✅ Total tests: 744
|
||||
✅ Test discovery: All tests found
|
||||
✅ Coverage: 98.2%
|
||||
⚡ Performance: 45.2s (good)
|
||||
|
||||
📦 DEPENDENCIES
|
||||
✅ Dependencies: Up to date
|
||||
✅ Security: No vulnerabilities
|
||||
✅ Conflicts: None detected
|
||||
⚠️ Outdated: 2 minor updates available
|
||||
|
||||
📖 DOCUMENTATION
|
||||
✅ README: Current
|
||||
✅ API docs: 95% coverage
|
||||
⚠️ CLI reference: Needs update
|
||||
✅ Changelog: Complete
|
||||
|
||||
📈 METRICS
|
||||
├── Source code: 15,432 lines
|
||||
├── Test code: 8,967 lines
|
||||
├── Test ratio: 58% (excellent)
|
||||
└── Complexity: Low (maintainable)
|
||||
|
||||
🎯 RECOMMENDATIONS:
|
||||
1. Update CLI documentation
|
||||
2. Refactor 3 complex functions
|
||||
3. Update minor dependencies
|
||||
4. Consider splitting large test files
|
||||
|
||||
🏆 PROJECT STATUS: Ready for v0.13.0 release!
|
||||
```
|
||||
|
||||
## Health Scoring
|
||||
|
||||
### Excellent (90-100)
|
||||
- All quality gates pass
|
||||
- High test coverage (>95%)
|
||||
- No security issues
|
||||
- Documentation current
|
||||
|
||||
### Good (75-89)
|
||||
- Minor issues present
|
||||
- Good test coverage (>90%)
|
||||
- No critical security issues
|
||||
- Most documentation current
|
||||
|
||||
### Needs Attention (60-74)
|
||||
- Several quality issues
|
||||
- Adequate test coverage (>80%)
|
||||
- Minor security concerns
|
||||
- Documentation gaps
|
||||
|
||||
### Critical (<60)
|
||||
- Major quality problems
|
||||
- Low test coverage (<80%)
|
||||
- Security vulnerabilities
|
||||
- Significant documentation issues
|
||||
|
||||
## Context
|
||||
- Provides comprehensive project overview
|
||||
- Identifies potential issues before they become problems
|
||||
- Tracks project health trends over time
|
||||
- Helps prioritize maintenance tasks
|
||||
- Supports release readiness decisions
|
||||
@@ -1,62 +0,0 @@
|
||||
# Basic Memory Custom Commands
|
||||
|
||||
This directory contains custom Claude Code slash commands for the Basic Memory project.
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Release Management (`/project:release:*`)
|
||||
- `/project:release:beta` - Create beta releases with automated quality checks
|
||||
- `/project:release:release` - Create stable releases with comprehensive validation
|
||||
- `/project:release:release-check` - Pre-flight validation without making changes
|
||||
- `/project:release:changelog` - Generate changelog entries from commits
|
||||
|
||||
### Development (`/project:*`)
|
||||
- `/project:test-coverage` - Run tests with detailed coverage analysis
|
||||
- `/project:test-live` - Live testing suite using real Basic Memory installation
|
||||
- `/project:lint-fix` - Run comprehensive linting with auto-fix
|
||||
- `/project:check-health` - Comprehensive project health assessment
|
||||
|
||||
## Command Structure
|
||||
|
||||
Commands are organized by functionality:
|
||||
```
|
||||
.claude/commands/
|
||||
├── release/ # Release management commands
|
||||
│ ├── beta.md # /project:release:beta
|
||||
│ ├── release.md # /project:release:release
|
||||
│ ├── release-check.md # /project:release:release-check
|
||||
│ └── changelog.md # /project:release:changelog
|
||||
├── test-coverage.md # /project:test-coverage
|
||||
├── test-live.md # /project:test-live
|
||||
├── lint-fix.md # /project:lint-fix
|
||||
├── check-health.md # /project:check-health
|
||||
└── commands.md # This overview file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Commands are invoked using the `/project:` prefix:
|
||||
- `/project:release:beta v0.13.0b4`
|
||||
- `/project:test-coverage mcp`
|
||||
- `/project:test-live core`
|
||||
- `/project:release:release-check`
|
||||
- `/project:check-health`
|
||||
|
||||
## Implementation
|
||||
|
||||
Each command is implemented as a Markdown file containing structured prompts that:
|
||||
1. Validate preconditions
|
||||
2. Execute steps in the correct order
|
||||
3. Handle errors gracefully
|
||||
4. Provide clear status updates
|
||||
5. Return actionable results
|
||||
|
||||
## Tooling Integration
|
||||
|
||||
Commands leverage existing project tooling:
|
||||
- `just check` - Quality checks
|
||||
- `just test` - Test suite
|
||||
- `just update-deps` - Dependency updates
|
||||
- `uv` - Package management
|
||||
- `git` - Version control
|
||||
- GitHub Actions - CI/CD pipeline
|
||||
@@ -1,145 +0,0 @@
|
||||
# /project:lint-fix - Comprehensive Code Quality Fix
|
||||
|
||||
Run comprehensive linting and auto-fix code quality issues across the codebase.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/project:lint-fix
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert code quality engineer for the Basic Memory project. When the user runs `/project:lint-fix`, execute the following steps:
|
||||
|
||||
### Step 1: Pre-flight Check
|
||||
1. **Verify Clean Working Directory**
|
||||
```bash
|
||||
git status --porcelain
|
||||
```
|
||||
- Check for uncommitted changes
|
||||
- Warn if working directory is not clean
|
||||
- Suggest stashing changes if needed
|
||||
|
||||
### Step 2: Import Organization
|
||||
1. **Fix Import Order and Cleanup**
|
||||
```bash
|
||||
uv run ruff check --select I --fix .
|
||||
```
|
||||
- Sort imports by category (standard, third-party, local)
|
||||
- Remove unused imports
|
||||
- Fix import spacing and organization
|
||||
|
||||
### Step 3: Code Formatting
|
||||
1. **Apply Consistent Formatting**
|
||||
```bash
|
||||
uv run ruff format .
|
||||
```
|
||||
- Format code according to project style
|
||||
- Fix line length issues (100 chars max)
|
||||
- Standardize quotes and spacing
|
||||
|
||||
### Step 4: Linting with Auto-fix
|
||||
1. **Fix Linting Issues**
|
||||
```bash
|
||||
uv run ruff check --fix .
|
||||
```
|
||||
- Auto-fix safe linting issues
|
||||
- Report any remaining manual fixes needed
|
||||
- Focus on code quality and best practices
|
||||
|
||||
### Step 5: Type Checking
|
||||
1. **Validate Type Annotations**
|
||||
```bash
|
||||
uv run pyright
|
||||
```
|
||||
- Check for type errors
|
||||
- Report any missing type annotations
|
||||
- Validate type compatibility
|
||||
|
||||
### Step 6: Report Generation
|
||||
Generate comprehensive quality report:
|
||||
|
||||
```
|
||||
🔧 Code Quality Fix Report
|
||||
|
||||
✅ FIXES APPLIED:
|
||||
├── Import organization: 12 files updated
|
||||
├── Code formatting: 8 files reformatted
|
||||
├── Auto-fixable lint issues: 23 issues resolved
|
||||
└── Total files processed: 156
|
||||
|
||||
⚠️ MANUAL ATTENTION NEEDED:
|
||||
├── Type annotations missing in entity_service.py:45
|
||||
├── Complex function needs refactoring in sync_service.py:123
|
||||
└── Unused variable in test_utils.py:67
|
||||
|
||||
🎯 QUALITY SCORE: 96.2% (excellent)
|
||||
|
||||
📁 Run `git diff` to review all changes
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues
|
||||
- **Merge Conflicts**: Provide resolution guidance
|
||||
- **Syntax Errors**: Point to specific files and lines
|
||||
- **Type Errors**: Suggest specific fixes
|
||||
- **Import Errors**: Check for missing dependencies
|
||||
|
||||
### Recovery Steps
|
||||
- If auto-fixes introduce issues, provide rollback instructions
|
||||
- If type checking fails, suggest incremental fixes
|
||||
- If tests break, provide debugging guidance
|
||||
|
||||
## Quality Gates
|
||||
|
||||
### Must Pass
|
||||
- [ ] All auto-fixable lint issues resolved
|
||||
- [ ] Code formatting consistent
|
||||
- [ ] No syntax errors
|
||||
- [ ] Import organization clean
|
||||
|
||||
### Should Pass (Warnings)
|
||||
- [ ] No type checking errors
|
||||
- [ ] No complex function warnings
|
||||
- [ ] No unused variables/imports
|
||||
- [ ] Consistent naming conventions
|
||||
|
||||
## Output Examples
|
||||
|
||||
### Successful Fix
|
||||
```
|
||||
🎉 CODE QUALITY IMPROVED!
|
||||
|
||||
✅ All auto-fixes applied successfully
|
||||
📏 Code formatting: 100% compliant
|
||||
🔍 Linting: No issues found
|
||||
🏷️ Type checking: All passed
|
||||
|
||||
Ready for commit! Use:
|
||||
git add -A && git commit -m "style: fix code quality issues"
|
||||
```
|
||||
|
||||
### Issues Requiring Attention
|
||||
```
|
||||
⚠️ PARTIAL SUCCESS - MANUAL FIXES NEEDED
|
||||
|
||||
✅ Auto-fixes applied: 45 issues
|
||||
❌ Manual fixes needed: 3 issues
|
||||
|
||||
Priority fixes:
|
||||
1. Fix type annotation in services/entity_service.py:142
|
||||
2. Simplify complex function in sync/sync_service.py:67
|
||||
3. Remove unused import in tests/conftest.py:12
|
||||
|
||||
Run these commands:
|
||||
# Fix specific file
|
||||
uv run pyright src/basic_memory/services/entity_service.py
|
||||
```
|
||||
|
||||
## Context
|
||||
- Uses ruff for fast Python linting and formatting
|
||||
- Uses pyright for type checking
|
||||
- Follows project code style guidelines (100 char line length)
|
||||
- Maintains backward compatibility
|
||||
- Integrates with existing pre-commit hooks
|
||||
@@ -8,7 +8,7 @@ Analyze commits and generate formatted changelog entry for a version.
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `version` (required): Version like `v0.13.0` or `v0.13.0b4`
|
||||
- `version` (required): Version like `v0.14.0` or `v0.14.0b1`
|
||||
- `type` (optional): `beta`, `rc`, or `stable` (default: `stable`)
|
||||
|
||||
## Implementation
|
||||
@@ -59,8 +59,9 @@ You are an expert technical writer for the Basic Memory project. When the user r
|
||||
### Step 3: Generate Changelog Entry
|
||||
Create formatted entry following existing CHANGELOG.md style:
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## v0.13.0 (2025-06-03)
|
||||
## <version> (<date>)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -128,6 +129,8 @@ Create formatted entry following existing CHANGELOG.md style:
|
||||
## Output Format
|
||||
|
||||
### For Beta Releases
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## v0.13.0b4 (2025-06-03)
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ You are an expert release manager for the Basic Memory project. When the user ru
|
||||
3. Verify we're on the `main` branch
|
||||
4. Confirm no existing tag with this version
|
||||
|
||||
#### Documentation Validation
|
||||
1. **Changelog Check**
|
||||
- CHANGELOG.md contains entry for target version
|
||||
- Entry includes all major features and fixes
|
||||
- Breaking changes are documented
|
||||
|
||||
### Step 2: Use Justfile Automation
|
||||
Execute the automated release process:
|
||||
```bash
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# /test-coverage - Run Tests with Coverage Analysis
|
||||
|
||||
Execute test suite with comprehensive coverage reporting and analysis.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/test-coverage [pattern]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `pattern` (optional): Test pattern to run specific tests (e.g., `test_mcp`, `*integration*`)
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert QA engineer for the Basic Memory project. When the user runs `/test-coverage`, execute the following steps:
|
||||
|
||||
### Step 1: Test Execution
|
||||
1. **Run Tests with Coverage**
|
||||
```bash
|
||||
# Full test suite
|
||||
uv run pytest --cov=basic_memory --cov-report=html --cov-report=term -v
|
||||
|
||||
# Or with pattern if provided
|
||||
uv run pytest tests/*{pattern}* --cov=basic_memory --cov-report=html --cov-report=term -v
|
||||
```
|
||||
|
||||
2. **Generate Coverage Reports**
|
||||
- Terminal summary with percentages
|
||||
- HTML report for detailed analysis
|
||||
- Identify files below coverage threshold
|
||||
|
||||
### Step 2: Coverage Analysis
|
||||
1. **Summary Statistics**
|
||||
- Overall coverage percentage
|
||||
- Number of files with 100% coverage
|
||||
- Files below 95% threshold
|
||||
- Total lines covered/missed
|
||||
|
||||
2. **Detailed Breakdown**
|
||||
- Coverage by module/package
|
||||
- Identify untested code paths
|
||||
- Find missing edge case tests
|
||||
|
||||
### Step 3: Report Generation
|
||||
Generate comprehensive coverage report:
|
||||
|
||||
```
|
||||
🧪 Test Coverage Report
|
||||
|
||||
📊 OVERALL COVERAGE: 98.2% (target: 95%+)
|
||||
|
||||
✅ EXCELLENT COVERAGE (>95%):
|
||||
├── basic_memory/mcp/: 99.1%
|
||||
├── basic_memory/services/: 98.8%
|
||||
├── basic_memory/repository/: 97.9%
|
||||
└── basic_memory/api/: 96.2%
|
||||
|
||||
⚠️ NEEDS ATTENTION (<95%):
|
||||
├── basic_memory/sync/: 94.1% (missing 12 lines)
|
||||
└── basic_memory/importers/: 91.8% (missing 23 lines)
|
||||
|
||||
🎯 SPECIFIC GAPS:
|
||||
├── sync_service.py:142-145 (error handling)
|
||||
├── importer_base.py:67-70 (edge case)
|
||||
└── file_utils.py:89 (exception path)
|
||||
|
||||
📁 HTML Report: htmlcov/index.html
|
||||
🚀 Run `open htmlcov/index.html` to view detailed report
|
||||
```
|
||||
|
||||
### Step 4: Actionable Recommendations
|
||||
1. **Coverage Improvements**
|
||||
- Suggest specific tests to add
|
||||
- Identify edge cases to cover
|
||||
- Recommend integration tests
|
||||
|
||||
2. **Quality Insights**
|
||||
- Highlight well-tested modules
|
||||
- Point out testing patterns to follow
|
||||
- Suggest refactoring for testability
|
||||
|
||||
## Advanced Analysis
|
||||
|
||||
### Performance Metrics
|
||||
- Test execution time by module
|
||||
- Slowest tests identification
|
||||
- Coverage collection overhead
|
||||
|
||||
### Integration Coverage
|
||||
- MCP tool integration tests
|
||||
- API endpoint coverage
|
||||
- Database operation coverage
|
||||
- File system operation coverage
|
||||
|
||||
## Output Examples
|
||||
|
||||
### Full Coverage Success
|
||||
```
|
||||
🎉 EXCELLENT COVERAGE!
|
||||
|
||||
📊 Coverage: 98.7% (744 tests passed)
|
||||
✅ All modules above 95% threshold
|
||||
🏆 23 files with 100% coverage
|
||||
⚡ Tests completed in 45.2s
|
||||
|
||||
Ready for release! 🚀
|
||||
```
|
||||
|
||||
### Coverage Issues Found
|
||||
```
|
||||
⚠️ COVERAGE GAPS DETECTED
|
||||
|
||||
📊 Coverage: 92.1% (below 95% target)
|
||||
❌ 3 modules need attention
|
||||
🔍 43 uncovered lines found
|
||||
|
||||
Priority fixes:
|
||||
1. Add tests for error handling in sync_service.py
|
||||
2. Cover edge cases in importer_base.py
|
||||
3. Test exception paths in file_utils.py
|
||||
|
||||
Run specific tests:
|
||||
uv run pytest tests/sync/ -v
|
||||
```
|
||||
|
||||
## Context
|
||||
- Uses pytest with coverage plugin
|
||||
- Generates both terminal and HTML reports
|
||||
- Focuses on actionable improvement suggestions
|
||||
- Integrates with existing test infrastructure
|
||||
- Helps maintain high code quality standards
|
||||
@@ -1,6 +1,7 @@
|
||||
# /project:test-live - Live Basic Memory Testing Suite
|
||||
|
||||
Execute comprehensive real-world testing of Basic Memory using the installed version, following the methodology in TESTING.md. All test results are recorded as notes in a dedicated test project.
|
||||
Execute comprehensive real-world testing of Basic Memory using the installed version.
|
||||
All test results are recorded as notes in a dedicated test project.
|
||||
|
||||
## Usage
|
||||
```
|
||||
@@ -8,12 +9,45 @@ Execute comprehensive real-world testing of Basic Memory using the installed ver
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `phase` (optional): Specific test phase to run (`core`, `features`, `edge`, `workflows`, `stress`, or `all`)
|
||||
- `phase` (optional): Specific test phase to run (`recent`, `core`, `features`, `edge`, `workflows`, `stress`, or `all`)
|
||||
- `recent` - Focus on recent changes and new features (recommended for regular testing)
|
||||
- `core` - Essential tools only (Tier 1: write_note, read_note, search_notes, edit_note, list_projects, switch_project)
|
||||
- `features` - Core + important workflows (Tier 1 + Tier 2)
|
||||
- `all` - Comprehensive testing of all tools and scenarios
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert QA engineer conducting live testing of Basic Memory.
|
||||
When the user runs `/project:test-live`, execute comprehensive testing following the TESTING.md methodology:
|
||||
When the user runs `/project:test-live`, execute comprehensive test plan:
|
||||
|
||||
## Tool Testing Priority
|
||||
|
||||
### **Tier 1: Critical Core (Always Test)**
|
||||
1. **write_note** - Foundation of all knowledge creation
|
||||
2. **read_note** - Primary knowledge retrieval mechanism
|
||||
3. **search_notes** - Essential for finding information
|
||||
4. **edit_note** - Core content modification capability
|
||||
5. **list_memory_projects** - Project discovery and status
|
||||
6. **switch_project** - Context switching for multi-project workflows
|
||||
|
||||
### **Tier 2: Important Workflows (Usually Test)**
|
||||
7. **recent_activity** - Understanding what's changed
|
||||
8. **build_context** - Conversation continuity via memory:// URLs
|
||||
9. **create_memory_project** - Essential for project setup
|
||||
10. **move_note** - Knowledge organization
|
||||
11. **sync_status** - Understanding system state
|
||||
|
||||
### **Tier 3: Enhanced Functionality (Sometimes Test)**
|
||||
12. **view_note** - Claude Desktop artifact display
|
||||
13. **read_content** - Raw content access
|
||||
14. **delete_note** - Content removal
|
||||
15. **list_directory** - File system exploration
|
||||
16. **set_default_project** - Configuration
|
||||
17. **delete_project** - Administrative cleanup
|
||||
|
||||
### **Tier 4: Specialized (Rarely Test)**
|
||||
18. **canvas** - Obsidian visualization (specialized use case)
|
||||
19. **MCP Prompts** - Enhanced UX tools (ai_assistant_guide, continue_conversation)
|
||||
|
||||
### Pre-Test Setup
|
||||
|
||||
@@ -22,7 +56,13 @@ When the user runs `/project:test-live`, execute comprehensive testing following
|
||||
- Check version and confirm it's the expected release
|
||||
- Test MCP connection and tool availability
|
||||
|
||||
2. **Test Project Creation**
|
||||
2. **Recent Changes Analysis** (if phase includes 'recent' or 'all')
|
||||
- Run `git log --oneline -20` to examine recent commits
|
||||
- Identify new features, bug fixes, and enhancements
|
||||
- Generate targeted test scenarios for recent changes
|
||||
- Prioritize regression testing for recently fixed issues
|
||||
|
||||
3. **Test Project Creation**
|
||||
|
||||
Run the bash `date` command to get the current date/time.
|
||||
|
||||
@@ -34,83 +74,158 @@ Run the bash `date` command to get the current date/time.
|
||||
|
||||
Make sure to switch to the newly created project with the `switch_project()` tool.
|
||||
|
||||
3. **Baseline Documentation**
|
||||
4. **Baseline Documentation**
|
||||
Create initial test session note with:
|
||||
- Test environment details
|
||||
- Version being tested
|
||||
- Recent changes identified (if applicable)
|
||||
- Test objectives and scope
|
||||
- Start timestamp
|
||||
|
||||
### Phase 1: Core Functionality Validation
|
||||
### Phase 0: Recent Changes Validation (if 'recent' or 'all' phase)
|
||||
|
||||
Test all fundamental MCP tools systematically:
|
||||
Based on recent commit analysis, create targeted test scenarios:
|
||||
|
||||
**write_note Tests:**
|
||||
- Basic note creation with various content types
|
||||
- Frontmatter handling (tags, custom fields)
|
||||
- Special characters in titles and content
|
||||
- Unicode and emoji support
|
||||
- Empty notes and minimal content
|
||||
**Recent Changes Test Protocol:**
|
||||
1. **Feature Addition Tests** - For each new feature identified:
|
||||
- Test basic functionality
|
||||
- Test integration with existing tools
|
||||
- Verify documentation accuracy
|
||||
- Test edge cases and error handling
|
||||
|
||||
**read_note Tests:**
|
||||
- Read by title, permalink, memory:// URLs
|
||||
- Non-existent notes (error handling)
|
||||
- Notes with complex formatting
|
||||
- Performance with large notes
|
||||
2. **Bug Fix Regression Tests** - For each recent fix:
|
||||
- Recreate the original problem scenario
|
||||
- Verify the fix works as expected
|
||||
- Test related functionality isn't broken
|
||||
- Document the verification in test notes
|
||||
|
||||
**view_note Tests:**
|
||||
- View notes as formatted artifacts (Claude Desktop)
|
||||
- Title extraction from frontmatter and headings
|
||||
- Unicode and emoji content in artifacts
|
||||
- Error handling for non-existent notes
|
||||
- Artifact display quality and readability
|
||||
3. **Performance/Enhancement Validation** - For optimizations:
|
||||
- Establish baseline timing
|
||||
- Compare with expected improvements
|
||||
- Test under various load conditions
|
||||
- Document performance observations
|
||||
|
||||
**search_notes Tests:**
|
||||
- Simple text queries
|
||||
- Tag-based searches
|
||||
- Boolean operators and complex queries
|
||||
- Empty/no results scenarios
|
||||
- Performance with growing knowledge base
|
||||
**Example Recent Changes (Update based on actual git log):**
|
||||
- Watch Service Restart (#156): Test project creation → file modification → automatic restart
|
||||
- Cross-Project Moves (#161): Test move_note with cross-project detection
|
||||
- Docker Environment Support (#174): Test BASIC_MEMORY_HOME behavior
|
||||
- MCP Server Logging (#164): Verify log level configurations
|
||||
|
||||
**Recent Activity Tests:**
|
||||
- Various timeframes ("today", "1 week", "1d")
|
||||
- Type filtering (if available)
|
||||
- Empty project scenarios
|
||||
- Performance with many recent changes
|
||||
### Phase 1: Core Functionality Validation (Tier 1 Tools)
|
||||
|
||||
**Context Building Tests:**
|
||||
- Different depth levels (1, 2, 3+)
|
||||
- Various timeframes
|
||||
- Relation traversal accuracy
|
||||
- Performance with complex graphs
|
||||
Test essential MCP tools that form the foundation of Basic Memory:
|
||||
|
||||
### Phase 2: v0.13.0 Feature Deep Dive
|
||||
**1. write_note Tests (Critical):**
|
||||
- ✅ Basic note creation with frontmatter
|
||||
- ✅ Special characters and Unicode in titles
|
||||
- ✅ Various content types (lists, headings, code blocks)
|
||||
- ✅ Empty notes and minimal content edge cases
|
||||
- ⚠️ Error handling for invalid parameters
|
||||
|
||||
**Project Management:**
|
||||
- Create multiple projects dynamically
|
||||
- Switch between projects mid-conversation
|
||||
- Cross-project operations
|
||||
- Project discovery and status
|
||||
- Default project behavior
|
||||
- Invalid project handling
|
||||
**2. read_note Tests (Critical):**
|
||||
- ✅ Read by title, permalink, memory:// URLs
|
||||
- ✅ Non-existent notes (error handling)
|
||||
- ✅ Notes with complex markdown formatting
|
||||
- ⚠️ Performance with large notes (>10MB)
|
||||
|
||||
**Advanced Note Editing:**
|
||||
- `edit_note` with append operations
|
||||
- Prepend operations
|
||||
- Find/replace with validation
|
||||
- Section replacement under headers
|
||||
- Error scenarios (invalid operations)
|
||||
- Frontmatter preservation
|
||||
**3. search_notes Tests (Critical):**
|
||||
- ✅ Simple text queries across content
|
||||
- ✅ Tag-based searches with multiple tags
|
||||
- ✅ Boolean operators (AND, OR, NOT)
|
||||
- ✅ Empty/no results scenarios
|
||||
- ⚠️ Performance with 100+ notes
|
||||
|
||||
**File Management:**
|
||||
- `move_note` within same project
|
||||
- Move between projects
|
||||
- Automatic folder creation
|
||||
- Special characters in paths
|
||||
- Database consistency validation
|
||||
- Search index updates after moves
|
||||
**4. edit_note Tests (Critical):**
|
||||
- ✅ Append operations preserving frontmatter
|
||||
- ✅ Prepend operations
|
||||
- ✅ Find/replace with validation
|
||||
- ✅ Section replacement under headers
|
||||
- ⚠️ Error scenarios (invalid operations)
|
||||
|
||||
### Phase 3: Edge Case Exploration
|
||||
**5. list_memory_projects Tests (Critical):**
|
||||
- ✅ Display all projects with status indicators
|
||||
- ✅ Current and default project identification
|
||||
- ✅ Empty project list handling
|
||||
- ✅ Project metadata accuracy
|
||||
|
||||
**6. switch_project Tests (Critical):**
|
||||
- ✅ Switch between existing projects
|
||||
- ✅ Context preservation during switch
|
||||
- ⚠️ Invalid project name handling
|
||||
- ✅ Confirmation of successful switch
|
||||
|
||||
### Phase 2: Important Workflows (Tier 2 Tools)
|
||||
|
||||
**7. recent_activity Tests (Important):**
|
||||
- ✅ Various timeframes ("today", "1 week", "1d")
|
||||
- ✅ Type filtering capabilities
|
||||
- ✅ Empty project scenarios
|
||||
- ⚠️ Performance with many recent changes
|
||||
|
||||
**8. build_context Tests (Important):**
|
||||
- ✅ Different depth levels (1, 2, 3+)
|
||||
- ✅ Various timeframes for context
|
||||
- ✅ memory:// URL navigation
|
||||
- ⚠️ Performance with complex relation graphs
|
||||
|
||||
**9. create_memory_project Tests (Important):**
|
||||
- ✅ Create projects dynamically
|
||||
- ✅ Set default during creation
|
||||
- ✅ Path validation and creation
|
||||
- ⚠️ Invalid paths and names
|
||||
- ✅ Integration with existing projects
|
||||
|
||||
**10. move_note Tests (Important):**
|
||||
- ✅ Move within same project
|
||||
- ✅ Cross-project moves with detection (#161)
|
||||
- ✅ Automatic folder creation
|
||||
- ✅ Database consistency validation
|
||||
- ⚠️ Special characters in paths
|
||||
|
||||
**11. sync_status Tests (Important):**
|
||||
- ✅ Background operation monitoring
|
||||
- ✅ File synchronization status
|
||||
- ✅ Project sync state reporting
|
||||
- ⚠️ Error state handling
|
||||
|
||||
### Phase 3: Enhanced Functionality (Tier 3 Tools)
|
||||
|
||||
**12. view_note Tests (Enhanced):**
|
||||
- ✅ Claude Desktop artifact display
|
||||
- ✅ Title extraction from frontmatter
|
||||
- ✅ Unicode and emoji content rendering
|
||||
- ⚠️ Error handling for non-existent notes
|
||||
|
||||
**13. read_content Tests (Enhanced):**
|
||||
- ✅ Raw file content access
|
||||
- ✅ Binary file handling
|
||||
- ✅ Image file reading
|
||||
- ⚠️ Large file performance
|
||||
|
||||
**14. delete_note Tests (Enhanced):**
|
||||
- ✅ Single note deletion
|
||||
- ✅ Database consistency after deletion
|
||||
- ⚠️ Non-existent note handling
|
||||
- ✅ Confirmation of successful deletion
|
||||
|
||||
**15. list_directory Tests (Enhanced):**
|
||||
- ✅ Directory content listing
|
||||
- ✅ Depth control and filtering
|
||||
- ✅ File name globbing
|
||||
- ⚠️ Empty directory handling
|
||||
|
||||
**16. set_default_project Tests (Enhanced):**
|
||||
- ✅ Change default project
|
||||
- ✅ Configuration persistence
|
||||
- ⚠️ Invalid project handling
|
||||
|
||||
**17. delete_project Tests (Enhanced):**
|
||||
- ✅ Project removal from config
|
||||
- ✅ Database cleanup
|
||||
- ⚠️ Default project protection
|
||||
- ⚠️ Non-existent project handling
|
||||
|
||||
### Phase 4: Edge Case Exploration
|
||||
|
||||
**Boundary Testing:**
|
||||
- Very long titles and content (stress limits)
|
||||
@@ -134,7 +249,7 @@ Test all fundamental MCP tools systematically:
|
||||
- Rapid successive operations
|
||||
- Memory usage monitoring
|
||||
|
||||
### Phase 4: Real-World Workflow Scenarios
|
||||
### Phase 5: Real-World Workflow Scenarios
|
||||
|
||||
**Meeting Notes Pipeline:**
|
||||
1. Create meeting notes with action items
|
||||
@@ -164,7 +279,35 @@ Test all fundamental MCP tools systematically:
|
||||
4. Update content with edit operations
|
||||
5. Validate knowledge graph integrity
|
||||
|
||||
### Phase 5: Creative Stress Testing
|
||||
### Phase 6: Specialized Tools Testing (Tier 4)
|
||||
|
||||
**18. canvas Tests (Specialized):**
|
||||
- ✅ JSON Canvas generation
|
||||
- ✅ Node and edge creation
|
||||
- ✅ Obsidian compatibility
|
||||
- ⚠️ Complex graph handling
|
||||
|
||||
**19. MCP Prompts Tests (Specialized):**
|
||||
- ✅ ai_assistant_guide output
|
||||
- ✅ continue_conversation functionality
|
||||
- ✅ Formatted search results
|
||||
- ✅ Enhanced activity reports
|
||||
|
||||
### Phase 7: Integration & File Watching Tests
|
||||
|
||||
**File System Integration:**
|
||||
- ✅ Watch service behavior with file changes
|
||||
- ✅ Project creation → watch restart (#156)
|
||||
- ✅ Multi-project synchronization
|
||||
- ⚠️ MCP→API→DB→File stack validation
|
||||
|
||||
**Real Integration Testing:**
|
||||
- ✅ End-to-end file watching vs manual operations
|
||||
- ✅ Cross-session persistence
|
||||
- ✅ Database consistency across operations
|
||||
- ⚠️ Performance under real file system changes
|
||||
|
||||
### Phase 8: Creative Stress Testing
|
||||
|
||||
**Creative Exploration:**
|
||||
- Rapid project creation/switching patterns
|
||||
@@ -180,6 +323,26 @@ Test all fundamental MCP tools systematically:
|
||||
- Complex boolean search expressions
|
||||
- Resource constraint testing
|
||||
|
||||
## Test Execution Guidelines
|
||||
|
||||
### Quick Testing (core/features phases)
|
||||
- Focus on Tier 1 tools (core) or Tier 1+2 (features)
|
||||
- Test essential functionality and common edge cases
|
||||
- Record critical issues immediately
|
||||
- Complete in 15-20 minutes
|
||||
|
||||
### Comprehensive Testing (all phase)
|
||||
- Cover all tiers systematically
|
||||
- Include specialized tools and stress testing
|
||||
- Document performance baselines
|
||||
- Complete in 45-60 minutes
|
||||
|
||||
### Recent Changes Focus (recent phase)
|
||||
- Analyze git log for recent commits
|
||||
- Generate targeted test scenarios
|
||||
- Focus on regression testing for fixes
|
||||
- Validate new features thoroughly
|
||||
|
||||
## Test Observation Format
|
||||
|
||||
Record ALL observations immediately as Basic Memory notes:
|
||||
@@ -202,14 +365,14 @@ permalink: test-session-[phase]-[timestamp]
|
||||
## Test Results
|
||||
|
||||
### ✅ Successful Operations
|
||||
- [timestamp] write_note: Created note with emoji title 📝 #functionality
|
||||
- [timestamp] search_notes: Boolean query returned 23 results in 0.4s #performance
|
||||
- [timestamp] edit_note: Append operation preserved frontmatter #reliability
|
||||
- [timestamp] ✅ write_note: Created note with emoji title 📝 #tier1 #functionality
|
||||
- [timestamp] ✅ search_notes: Boolean query returned 23 results in 0.4s #tier1 #performance
|
||||
- [timestamp] ✅ edit_note: Append operation preserved frontmatter #tier1 #reliability
|
||||
|
||||
### ⚠️ Issues Discovered
|
||||
- [timestamp] move_note: Slow with deep folder paths (2.1s) #performance
|
||||
- [timestamp] search_notes: Unicode query returned unexpected results #bug
|
||||
- [timestamp] project switch: Context lost for memory:// URLs #issue
|
||||
- [timestamp] ⚠️ move_note: Slow with deep folder paths (2.1s) #tier2 #performance
|
||||
- [timestamp] 🚨 search_notes: Unicode query returned unexpected results #tier1 #bug #critical
|
||||
- [timestamp] ⚠️ build_context: Context lost for memory:// URLs #tier2 #issue
|
||||
|
||||
### 🚀 Enhancements Identified
|
||||
- edit_note could benefit from preview mode #ux-improvement
|
||||
@@ -353,24 +516,44 @@ For each error discovered:
|
||||
- Create comprehensive summary report
|
||||
- Generate development recommendations
|
||||
|
||||
## Testing Success Criteria
|
||||
|
||||
### Core Testing (Tier 1) - Must Pass
|
||||
- All 6 critical tools function correctly
|
||||
- No critical bugs in essential workflows
|
||||
- Acceptable performance for basic operations
|
||||
- Error handling works as expected
|
||||
|
||||
### Feature Testing (Tier 1+2) - Should Pass
|
||||
- All 11 core + important tools function
|
||||
- Workflow scenarios complete successfully
|
||||
- Performance meets baseline expectations
|
||||
- Integration points work correctly
|
||||
|
||||
### Comprehensive Testing (All Tiers) - Complete Coverage
|
||||
- All tools tested across all scenarios
|
||||
- Edge cases and stress testing completed
|
||||
- Performance baselines established
|
||||
- Full documentation of issues and enhancements
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
**System Validation:**
|
||||
- v0.13.0 feature verification in real usage
|
||||
- Edge case discovery beyond unit tests
|
||||
- Feature verification prioritized by tier importance
|
||||
- Recent changes validated for regression
|
||||
- Performance baseline establishment
|
||||
- Bug identification with reproduction cases
|
||||
- Bug identification with severity assessment
|
||||
|
||||
**Knowledge Base Creation:**
|
||||
- Comprehensive testing documentation
|
||||
- Prioritized testing documentation
|
||||
- Real usage examples for user guides
|
||||
- Edge case scenarios for future testing
|
||||
- Recent changes validation records
|
||||
- Performance insights for optimization
|
||||
|
||||
**Development Insights:**
|
||||
- Prioritized bug fix list
|
||||
- Tier-based bug priority list
|
||||
- Recent changes impact assessment
|
||||
- Enhancement ideas from real usage
|
||||
- Architecture validation results
|
||||
- User experience improvement areas
|
||||
|
||||
## Post-Test Deliverables
|
||||
@@ -402,9 +585,11 @@ For each error discovered:
|
||||
- Add performance benchmarks and targets
|
||||
|
||||
## Context
|
||||
- Uses installed basic-memory version (not development)
|
||||
- Uses real installed basic-memory version
|
||||
- Tests complete MCP→API→DB→File stack
|
||||
- Creates living documentation in Basic Memory itself
|
||||
- Follows integration over isolation philosophy
|
||||
- Prioritizes testing by tool importance and usage frequency
|
||||
- Adapts to recent development changes dynamically
|
||||
- Focuses on real usage patterns over checklist validation
|
||||
- Generates actionable insights for development team
|
||||
- Generates actionable insights prioritized by impact
|
||||
@@ -0,0 +1,60 @@
|
||||
# Git files
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Development files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing files
|
||||
tests/
|
||||
test-int/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# Virtual environments (uv creates these during build)
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
|
||||
# CI/CD files
|
||||
.github/
|
||||
|
||||
# Documentation (keep README.md and pyproject.toml)
|
||||
docs/
|
||||
CHANGELOG.md
|
||||
CLAUDE.md
|
||||
CONTRIBUTING.md
|
||||
|
||||
# Example files not needed for runtime
|
||||
examples/
|
||||
|
||||
# Local development files
|
||||
.basic-memory/
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.log
|
||||
@@ -1,55 +0,0 @@
|
||||
# OAuth Configuration for Basic Memory MCP Server
|
||||
# Copy this file to .env and update the values
|
||||
|
||||
# Enable OAuth authentication
|
||||
FASTMCP_AUTH_ENABLED=true
|
||||
|
||||
# OAuth provider type: basic, github, google, or supabase
|
||||
# - basic: Built-in OAuth provider with in-memory storage
|
||||
# - github: Integrate with GitHub OAuth
|
||||
# - google: Integrate with Google OAuth
|
||||
# - supabase: Integrate with Supabase Auth (recommended for production)
|
||||
FASTMCP_AUTH_PROVIDER=basic
|
||||
|
||||
# OAuth issuer URL (your MCP server URL)
|
||||
FASTMCP_AUTH_ISSUER_URL=http://localhost:8000
|
||||
|
||||
# Documentation URL for OAuth endpoints
|
||||
FASTMCP_AUTH_DOCS_URL=http://localhost:8000/docs/oauth
|
||||
|
||||
# Required scopes (comma-separated)
|
||||
# Examples: read,write,admin
|
||||
FASTMCP_AUTH_REQUIRED_SCOPES=read,write
|
||||
|
||||
# Secret key for JWT tokens (auto-generated if not set)
|
||||
# FASTMCP_AUTH_SECRET_KEY=your-secret-key-here
|
||||
|
||||
# Enable client registration endpoint
|
||||
FASTMCP_AUTH_CLIENT_REGISTRATION_ENABLED=true
|
||||
|
||||
# Enable token revocation endpoint
|
||||
FASTMCP_AUTH_REVOCATION_ENABLED=true
|
||||
|
||||
# Default scopes for new clients
|
||||
FASTMCP_AUTH_DEFAULT_SCOPES=read
|
||||
|
||||
# Valid scopes that can be requested
|
||||
FASTMCP_AUTH_VALID_SCOPES=read,write,admin
|
||||
|
||||
# Client secret expiry in seconds (optional)
|
||||
# FASTMCP_AUTH_CLIENT_SECRET_EXPIRY=86400
|
||||
|
||||
# GitHub OAuth settings (if using github provider)
|
||||
# GITHUB_CLIENT_ID=your-github-client-id
|
||||
# GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
|
||||
# Google OAuth settings (if using google provider)
|
||||
# GOOGLE_CLIENT_ID=your-google-client-id
|
||||
# GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
# Supabase settings (if using supabase provider)
|
||||
# SUPABASE_URL=https://your-project.supabase.co
|
||||
# SUPABASE_ANON_KEY=your-anon-key
|
||||
# SUPABASE_SERVICE_KEY=your-service-key # Optional, for admin operations
|
||||
# SUPABASE_JWT_SECRET=your-jwt-secret # Optional, for token validation
|
||||
# SUPABASE_ALLOWED_CLIENTS=client1,client2 # Comma-separated list of allowed client IDs
|
||||
@@ -111,4 +111,4 @@ jobs:
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_tools: Bash(uv run pytest),Bash(uv run ruff check . --fix),Bash(uv run ruff format .),Bash(uv run pyright),Bash(just test),Bash(just lint),Bash(just format),Bash(just type-check),Bash(just check),Read,Write,Edit,MultiEdit,Glob,Grep,LS
|
||||
allowed_tools: Bash(uv run pytest),Bash(uv run ruff check . --fix),Bash(uv run ruff format .),Bash(uv run pyright),Bash(just test),Bash(just lint),Bash(just format),Bash(just type-check),Bash(just check),Read,Write,Edit,MultiEdit,Glob,Grep,LS, mcp__web_search
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Trigger on version tags like v1.0.0, v0.13.0, etc.
|
||||
workflow_dispatch: # Allow manual triggering for testing
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: basicmachines-co/basic-memory
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -51,4 +51,35 @@ jobs:
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
homebrew:
|
||||
name: Update Homebrew Formula
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
# Only run for stable releases (not dev, beta, or rc versions)
|
||||
if: ${{ !contains(github.ref_name, 'dev') && !contains(github.ref_name, 'b') && !contains(github.ref_name, 'rc') }}
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Update Homebrew formula
|
||||
uses: mislav/bump-homebrew-formula-action@v3
|
||||
with:
|
||||
# Formula name in homebrew-basic-memory repo
|
||||
formula-name: basic-memory
|
||||
# The tap repository
|
||||
homebrew-tap: basicmachines-co/homebrew-basic-memory
|
||||
# Base branch of the tap repository
|
||||
base-branch: main
|
||||
# Download URL will be automatically constructed from the tag
|
||||
download-url: https://github.com/basicmachines-co/basic-memory/archive/refs/tags/${{ github.ref_name }}.tar.gz
|
||||
# Commit message for the formula update
|
||||
commit-message: |
|
||||
{{formulaName}} {{version}}
|
||||
|
||||
Created by https://github.com/basicmachines-co/basic-memory/actions/runs/${{ github.run_id }}
|
||||
env:
|
||||
# Personal Access Token with repo scope for homebrew-basic-memory repo
|
||||
COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"/Users/phernandez/dev/basicmachines/basic-memory",
|
||||
"run",
|
||||
"src/basic_memory/cli/main.py",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# OAuth Quick Start
|
||||
|
||||
Basic Memory supports OAuth authentication for secure access control. For detailed documentation, see [OAuth Authentication Guide](docs/OAuth%20Authentication%20Guide.md).
|
||||
|
||||
## Quick Test with MCP Inspector
|
||||
|
||||
```bash
|
||||
# 1. Set a consistent secret key
|
||||
export FASTMCP_AUTH_SECRET_KEY="test-secret-key"
|
||||
|
||||
# 2. Start server with OAuth
|
||||
FASTMCP_AUTH_ENABLED=true basic-memory mcp --transport streamable-http
|
||||
|
||||
# 3. In another terminal, get a test token
|
||||
export FASTMCP_AUTH_SECRET_KEY="test-secret-key" # Same key!
|
||||
basic-memory auth test-auth
|
||||
|
||||
# 4. Copy the access token and use in MCP Inspector:
|
||||
# - Server URL: http://localhost:8000/mcp
|
||||
# - Transport: streamable-http
|
||||
# - Custom Headers:
|
||||
# Authorization: Bearer YOUR_ACCESS_TOKEN
|
||||
# Accept: application/json, text/event-stream
|
||||
```
|
||||
|
||||
## OAuth Endpoints
|
||||
|
||||
- `GET /authorize` - Authorization endpoint
|
||||
- `POST /token` - Token exchange endpoint
|
||||
- `GET /.well-known/oauth-authorization-server` - OAuth metadata
|
||||
|
||||
## Common Issues
|
||||
|
||||
1. **401 Unauthorized**: Make sure you're using the same secret key for both server and client
|
||||
2. **404 Not Found**: Use `/authorize` not `/auth/authorize`
|
||||
3. **Token Invalid**: Tokens don't persist across server restarts with basic provider
|
||||
|
||||
## Documentation
|
||||
|
||||
- [OAuth Authentication Guide](docs/OAuth%20Authentication%20Guide.md) - Complete setup guide
|
||||
- [Supabase OAuth Setup](docs/Supabase%20OAuth%20Setup.md) - Production deployment
|
||||
- [External OAuth Providers](docs/External%20OAuth%20Providers.md) - GitHub, Google integration
|
||||
@@ -1,5 +1,334 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.14.2 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#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
|
||||
|
||||
- **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
|
||||
|
||||
## 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 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
|
||||
|
||||
- **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
|
||||
|
||||
- **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
|
||||
|
||||
- **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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Homebrew Integration** - Automatic Homebrew formula updates
|
||||
- **Documentation** - Add git sign-off reminder to development guide
|
||||
|
||||
## v0.13.6 (2025-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Custom Entity Types** - Support for custom entity types in write_note
|
||||
([`7789864`](https://github.com/basicmachines-co/basic-memory/commit/77898644933589c2da9bdd60571d54137a5309ed))
|
||||
- Fixed `entity_type` parameter for `write_note` MCP tool to respect value passed in
|
||||
- Frontmatter `type` field automatically respected when no explicit parameter provided
|
||||
- Maintains backward compatibility with default "note" type
|
||||
|
||||
- **#139**: Fix "UNIQUE constraint failed: entity.permalink" database error
|
||||
([`c6215fd`](https://github.com/basicmachines-co/basic-memory/commit/c6215fd819f9564ead91cf3a950f855241446096))
|
||||
- Implement SQLAlchemy UPSERT strategy to handle permalink conflicts gracefully
|
||||
- Eliminates crashes when creating notes with existing titles in same folders
|
||||
- Seamlessly updates existing entities instead of failing with constraint errors
|
||||
|
||||
- **Database Migration Performance** - Eliminate redundant migration initialization
|
||||
([`84d2aaf`](https://github.com/basicmachines-co/basic-memory/commit/84d2aaf6414dd083af4b0df73f6c8139b63468f6))
|
||||
- Fix duplicate migration calls that slowed system startup
|
||||
- Improve performance with multiple projects (tested with 28+ projects)
|
||||
- Add migration deduplication safeguards with comprehensive test coverage
|
||||
|
||||
- **User Experience** - Correct spelling error in continue_conversation prompt
|
||||
([`b4c26a6`](https://github.com/basicmachines-co/basic-memory/commit/b4c26a613379e6f2ba655efe3d7d8d40c27999e5))
|
||||
- Fix "Chose a folder" → "Choose a folder" in MCP prompt instructions
|
||||
- Improve grammar and clarity in user-facing prompt text
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Website Updates** - Add new website and community links to README
|
||||
([`3fdce68`](https://github.com/basicmachines-co/basic-memory/commit/3fdce683d7ad8b6f4855d7138d5ff2136d4c07bc))
|
||||
|
||||
- **Project Documentation** - Update README.md and CLAUDE.md with latest project information
|
||||
([`782cb2d`](https://github.com/basicmachines-co/basic-memory/commit/782cb2df28803482d209135a054e67cc32d7363e))
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- **Comprehensive Test Coverage** - Add extensive test suites for new features
|
||||
- Custom entity type validation with 8 new test scenarios
|
||||
- UPSERT behavior testing with edge case coverage
|
||||
- Migration deduplication testing with 6 test scenarios
|
||||
- Database constraint handling validation
|
||||
|
||||
- **Code Quality** - Enhanced error handling and validation
|
||||
- Improved SQLAlchemy patterns with modern UPSERT operations
|
||||
- Better conflict resolution strategies for entity management
|
||||
- Strengthened database consistency guarantees
|
||||
|
||||
### Performance
|
||||
|
||||
- **Database Operations** - Faster startup and improved scalability
|
||||
- Reduced migration overhead for multi-project setups
|
||||
- Optimized conflict resolution for entity creation
|
||||
- Enhanced performance with growing knowledge bases
|
||||
|
||||
### Migration Guide
|
||||
|
||||
This release includes automatic database improvements. No manual migration required:
|
||||
|
||||
- Existing notes and entity types continue working unchanged
|
||||
- New `entity_type` parameter is optional and backward compatible
|
||||
- Database performance improvements apply automatically
|
||||
- All existing MCP tool behavior preserved
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Latest stable release
|
||||
uv tool install basic-memory
|
||||
|
||||
# Update existing installation
|
||||
uv tool upgrade basic-memory
|
||||
```
|
||||
|
||||
## v0.13.5 (2025-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **MCP Tools**: Renamed `create_project` tool to `create_memory_project` for namespace isolation
|
||||
- **Namespace**: Continued namespace isolation effort to prevent conflicts with other MCP servers
|
||||
|
||||
### Changes
|
||||
|
||||
- Tool functionality remains identical - only the name changed from `create_project` to `create_memory_project`
|
||||
- All integration tests updated to use the new tool name
|
||||
- Completes namespace isolation for project management tools alongside `list_memory_projects`
|
||||
|
||||
## v0.13.4 (2025-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **MCP Tools**: Renamed `list_projects` tool to `list_memory_projects` to avoid naming conflicts with other MCP servers
|
||||
- **Namespace**: Improved tool naming specificity for better MCP server integration and isolation
|
||||
|
||||
### Changes
|
||||
|
||||
- Tool functionality remains identical - only the name changed from `list_projects` to `list_memory_projects`
|
||||
- All integration tests updated to use the new tool name
|
||||
- Better namespace isolation for Basic Memory MCP tools
|
||||
|
||||
## v0.13.3 (2025-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Projects**: Fixed case-insensitive project switching where switching succeeded but subsequent operations failed due to session state inconsistency
|
||||
- **Config**: Enhanced config manager with case-insensitive project lookup using permalink-based matching
|
||||
- **MCP Tools**: Updated project management tools to store canonical project names from database instead of user input
|
||||
- **API**: Improved project service to handle both name and permalink lookups consistently
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- Added comprehensive case-insensitive project switching test coverage with 5 new integration test scenarios
|
||||
- Fixed permalink generation inconsistencies where different case inputs could generate different permalinks
|
||||
- Enhanced project URL construction to use permalinks consistently across all API calls
|
||||
- Improved error handling and session state management for project operations
|
||||
|
||||
### Changes
|
||||
|
||||
- Project switching now preserves canonical project names from database in session state
|
||||
- All project operations use permalink-based lookups for case-insensitive matching
|
||||
- Enhanced test coverage ensures reliable case-insensitive project operations
|
||||
|
||||
## v0.13.2 (2025-06-11)
|
||||
|
||||
### Features
|
||||
|
||||
- **Release Management**: Added automated release management system with version control in `__init__.py`
|
||||
- **Automation**: Implemented justfile targets for `release` and `beta` commands with comprehensive quality gates
|
||||
- **CI/CD**: Enhanced release process with automatic version updates, git tagging, and GitHub release creation
|
||||
|
||||
### Development Experience
|
||||
|
||||
- Added `.claude/commands/release/` directory with automation documentation
|
||||
- Implemented release validation including lint, type-check, and test execution
|
||||
- Streamlined release workflow from manual process to single-command automation
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- Updated package version management to use actual version numbers instead of dynamic versioning
|
||||
- Added release process documentation and command references
|
||||
- Enhanced justfile with comprehensive release automation targets
|
||||
|
||||
## v0.13.1 (2025-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -97,15 +97,26 @@ See the [README.md](README.md) file for a project overview.
|
||||
|
||||
**Content Management:**
|
||||
- `write_note(title, content, folder, tags)` - Create/update markdown notes with semantic observations and relations
|
||||
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph
|
||||
awareness
|
||||
- `read_file(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
|
||||
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph awareness
|
||||
- `edit_note(identifier, operation, content)` - Edit notes incrementally (append, prepend, find/replace, section replace)
|
||||
- `move_note(identifier, destination_path)` - Move notes with database consistency and search reindexing
|
||||
- `view_note(identifier)` - Display notes as formatted artifacts for better readability in Claude Desktop
|
||||
- `read_content(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
|
||||
- `delete_note(identifier)` - Delete notes from knowledge base
|
||||
|
||||
**Project Management:**
|
||||
- `list_memory_projects()` - List all available projects with status indicators
|
||||
- `switch_project(project_name)` - Switch to different project context during conversations
|
||||
- `get_current_project()` - Show currently active project with statistics
|
||||
- `create_memory_project(name, path, set_default)` - Create new Basic Memory projects
|
||||
- `delete_project(name)` - Delete projects from configuration and database
|
||||
- `set_default_project(name)` - Set default project in config
|
||||
- `sync_status()` - Check file synchronization status and background operations
|
||||
|
||||
**Knowledge Graph Navigation:**
|
||||
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation
|
||||
continuity
|
||||
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "
|
||||
1d", "1 week")
|
||||
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation continuity
|
||||
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "1d", "1 week")
|
||||
- `list_directory(dir_name, depth, file_name_glob)` - List directory contents with filtering and depth control
|
||||
|
||||
**Search & Discovery:**
|
||||
- `search_notes(query, page, page_size)` - Full-text search across all content with filtering options
|
||||
@@ -212,13 +223,35 @@ Basic Memory uses `uv-dynamic-versioning` for automatic version management based
|
||||
- Users install with: `pip install basic-memory --pre`
|
||||
- Use for milestone testing before stable release
|
||||
|
||||
#### Stable Releases (Manual)
|
||||
#### Stable Releases (Automated)
|
||||
- Use the automated release system: `just release v0.13.0`
|
||||
- Includes comprehensive quality checks (lint, format, type-check, tests)
|
||||
- Automatically updates version in `__init__.py`
|
||||
- Creates git tag and pushes to GitHub
|
||||
- Triggers GitHub Actions workflow for:
|
||||
- PyPI publication
|
||||
- Homebrew formula update (requires HOMEBREW_TOKEN secret)
|
||||
|
||||
**Manual method (legacy):**
|
||||
- Create version tag: `git tag v0.13.0 && git push origin v0.13.0`
|
||||
- Automatically builds, creates GitHub release, and publishes to PyPI
|
||||
- Users install with: `pip install basic-memory`
|
||||
|
||||
#### Homebrew Formula Updates
|
||||
- Automatically triggered after successful PyPI release for **stable releases only**
|
||||
- **Stable releases** (e.g., v0.13.7) automatically update the main `basic-memory` formula
|
||||
- **Pre-releases** (dev/beta/rc) are NOT automatically updated - users must specify version manually
|
||||
- Updates formula in `basicmachines-co/homebrew-basic-memory` repo
|
||||
- Requires `HOMEBREW_TOKEN` secret in GitHub repository settings:
|
||||
- Create a fine-grained Personal Access Token with `Contents: Read and Write` and `Actions: Read` scopes on `basicmachines-co/homebrew-basic-memory`
|
||||
- Add as repository secret named `HOMEBREW_TOKEN` in `basicmachines-co/basic-memory`
|
||||
- Formula updates include new version URL and SHA256 checksum
|
||||
|
||||
### For Development
|
||||
- No manual version bumping required
|
||||
- Versions automatically derived from git tags
|
||||
- `pyproject.toml` uses `dynamic = ["version"]`
|
||||
- `__init__.py` dynamically reads version from package metadata
|
||||
- **Automated releases**: Use `just release v0.13.x` for stable releases and `just beta v0.13.0b1` for beta releases
|
||||
- **Quality gates**: All releases require passing lint, format, type-check, and test suites
|
||||
- **Version management**: Versions automatically derived from git tags via `uv-dynamic-versioning`
|
||||
- **Configuration**: `pyproject.toml` uses `dynamic = ["version"]`
|
||||
- **Release automation**: `__init__.py` updated automatically during release process
|
||||
- **CI/CD**: GitHub Actions handles building and PyPI publication
|
||||
|
||||
## Development Notes
|
||||
- make sure you sign off on commits
|
||||
|
||||
@@ -27,7 +27,12 @@ project and how to get started as a developer.
|
||||
|
||||
> **Note**: Basic Memory uses [just](https://just.systems) as a modern command runner. Install with `brew install just` or `cargo install just`.
|
||||
|
||||
3. **Run the Tests**:
|
||||
3. **Activate the Virtual Environment**
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
4. **Run the Tests**:
|
||||
```bash
|
||||
# Run all tests
|
||||
just test
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
# Copy uv from official image
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Copy the project into the image
|
||||
ADD . /app
|
||||
|
||||
# Sync the project into a new environment, asserting the lockfile is up to date
|
||||
WORKDIR /app
|
||||
RUN uv sync --locked
|
||||
|
||||
# Copy the project files
|
||||
COPY . .
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Install pip and build dependencies
|
||||
RUN pip install --upgrade pip \
|
||||
&& pip install . --no-cache-dir --ignore-installed
|
||||
# Set default data directory and add venv to PATH
|
||||
ENV BASIC_MEMORY_HOME=/app/data \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Expose port if necessary (e.g., uv might use a port, but MCP over stdio so not needed here)
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Use the basic-memory entrypoint to run the MCP server
|
||||
CMD ["basic-memory", "mcp"]
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD basic-memory --version || exit 1
|
||||
|
||||
# Use the basic-memory entrypoint to run the MCP server with default SSE transport
|
||||
CMD ["basic-memory", "mcp", "--transport", "sse", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -13,8 +13,11 @@ Basic Memory lets you build persistent knowledge through natural conversations w
|
||||
Claude, while keeping everything in simple Markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
||||
enable any compatible LLM to read and write to your local knowledge base.
|
||||
|
||||
- Website: https://basicmachines.co
|
||||
- Website: https://basicmemory.com
|
||||
- Company: https://basicmachines.co
|
||||
- Documentation: https://memory.basicmachines.co
|
||||
- Discord: https://discord.gg/tyvKNccgqN
|
||||
- YouTube: https://www.youtube.com/@basicmachines-co
|
||||
|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
@@ -30,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:
|
||||
{
|
||||
@@ -61,8 +68,14 @@ Memory for Claude Desktop:
|
||||
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. The
|
||||
Smithery server hosts the MCP server component, while your data remains stored locally as Markdown files.
|
||||
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
|
||||
|
||||
@@ -153,7 +166,8 @@ The note embeds semantic content and links to other topics via simple Markdown f
|
||||
|
||||
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
|
||||
|
||||
- Realtime sync is enabled by default with the v0.12.0 version
|
||||
- Realtime sync is enabled by default starting with v0.12.0
|
||||
- Project switching during conversations is supported starting with v0.13.0
|
||||
|
||||
4. In a chat with the LLM, you can reference a topic:
|
||||
|
||||
@@ -211,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
|
||||
@@ -351,10 +365,20 @@ Basic Memory will sync the files in your project in real time if you make manual
|
||||
```
|
||||
write_note(title, content, folder, tags) - Create or update notes
|
||||
read_note(identifier, page, page_size) - Read notes by title or permalink
|
||||
edit_note(identifier, operation, content) - Edit notes incrementally (append, prepend, find/replace)
|
||||
move_note(identifier, destination_path) - Move notes with database consistency
|
||||
view_note(identifier) - Display notes as formatted artifacts for better readability
|
||||
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
|
||||
search_notes(query, page, page_size) - Search across your knowledge base
|
||||
recent_activity(type, depth, timeframe) - Find recently updated information
|
||||
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
||||
list_memory_projects() - List all available projects with status
|
||||
switch_project(project_name) - Switch to different project context
|
||||
get_current_project() - Show current project and statistics
|
||||
create_memory_project(name, path, set_default) - Create new projects
|
||||
delete_project(name) - Delete projects from configuration
|
||||
set_default_project(name) - Set default project
|
||||
sync_status() - Check file synchronization status
|
||||
```
|
||||
|
||||
5. Example prompts to try:
|
||||
@@ -365,6 +389,10 @@ canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
||||
"Create a canvas visualization of my project components"
|
||||
"Read my notes on the authentication system"
|
||||
"What have I been working on in the past week?"
|
||||
"Switch to my work-notes project"
|
||||
"List all my available projects"
|
||||
"Edit my coffee brewing note to add a new technique"
|
||||
"Move my old meeting notes to the archive folder"
|
||||
```
|
||||
|
||||
## Futher info
|
||||
@@ -394,6 +422,31 @@ Development versions are automatically published on every commit to main with ve
|
||||
pip install basic-memory --pre --force-reinstall
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Run Basic Memory in a container with volume mounting for your Obsidian vault:
|
||||
|
||||
```bash
|
||||
# Clone and start with Docker Compose
|
||||
git clone https://github.com/basicmachines-co/basic-memory.git
|
||||
cd basic-memory
|
||||
|
||||
# Edit docker-compose.yml to point to your Obsidian vault
|
||||
# Then start the container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Or use Docker directly:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name basic-memory-server \
|
||||
-v /path/to/your/obsidian-vault:/data/knowledge:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
ghcr.io/basicmachines-co/basic-memory:latest
|
||||
```
|
||||
|
||||
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Docker Compose configuration for Basic Memory
|
||||
# See docs/Docker.md for detailed setup instructions
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
basic-memory:
|
||||
# Use pre-built image (recommended for most users)
|
||||
image: ghcr.io/basicmachines-co/basic-memory:latest
|
||||
|
||||
# Uncomment to build locally instead:
|
||||
# build: .
|
||||
|
||||
container_name: basic-memory-server
|
||||
|
||||
# Volume mounts for knowledge directories and persistent data
|
||||
volumes:
|
||||
|
||||
# Persistent storage for configuration and database
|
||||
- basic-memory-config:/root/.basic-memory:rw
|
||||
|
||||
# Mount your knowledge directory (required)
|
||||
# Change './knowledge' to your actual Obsidian vault or knowledge directory
|
||||
- ./knowledge:/app/data:rw
|
||||
|
||||
# OPTIONAL: Mount additional knowledge directories for multiple projects
|
||||
# - ./work-notes:/app/data/work:rw
|
||||
# - ./personal-notes:/app/data/personal:rw
|
||||
|
||||
# You can edit the project config manually in the mounted config volume
|
||||
# The default project will be configured to use /app/data
|
||||
environment:
|
||||
# Project configuration
|
||||
- BASIC_MEMORY_DEFAULT_PROJECT=main
|
||||
|
||||
# Enable real-time file synchronization (recommended for Docker)
|
||||
- BASIC_MEMORY_SYNC_CHANGES=true
|
||||
|
||||
# Logging configuration
|
||||
- BASIC_MEMORY_LOG_LEVEL=INFO
|
||||
|
||||
# Sync delay in milliseconds (adjust for performance vs responsiveness)
|
||||
- BASIC_MEMORY_SYNC_DELAY=1000
|
||||
|
||||
# Port exposure for HTTP transport (only needed if not using STDIO)
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
# Command with SSE transport (configurable via environment variables above)
|
||||
# IMPORTANT: The SSE and streamable-http endpoints are not secured
|
||||
command: ["basic-memory", "mcp", "--transport", "sse", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
# Container management
|
||||
restart: unless-stopped
|
||||
|
||||
# Health monitoring
|
||||
healthcheck:
|
||||
test: ["CMD", "basic-memory", "--version"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Optional: Resource limits
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# memory: 512M
|
||||
# cpus: '0.5'
|
||||
# reservations:
|
||||
# memory: 256M
|
||||
# cpus: '0.25'
|
||||
|
||||
volumes:
|
||||
# Named volume for persistent configuration and database
|
||||
# This ensures your configuration and knowledge graph persist across container restarts
|
||||
basic-memory-config:
|
||||
driver: local
|
||||
|
||||
# Network configuration (optional)
|
||||
# networks:
|
||||
# basic-memory-net:
|
||||
# driver: bridge
|
||||
@@ -1,414 +0,0 @@
|
||||
---
|
||||
title: CLI Reference
|
||||
type: note
|
||||
permalink: docs/cli-reference
|
||||
---
|
||||
|
||||
# CLI Reference
|
||||
|
||||
Basic Memory provides command line tools for managing your knowledge base. This reference covers the available commands and their options.
|
||||
|
||||
## Core Commands
|
||||
|
||||
### auth (New in v0.13.0)
|
||||
|
||||
Manage OAuth authentication for secure remote access:
|
||||
|
||||
```bash
|
||||
# Test authentication setup
|
||||
basic-memory auth test-auth
|
||||
|
||||
# Register OAuth client
|
||||
basic-memory auth register-client
|
||||
```
|
||||
|
||||
Supports multiple authentication providers:
|
||||
- **Basic Provider**: For development and testing
|
||||
- **Supabase Provider**: For production deployments
|
||||
- **External Providers**: GitHub, Google integration framework
|
||||
|
||||
See [[OAuth Authentication Guide]] for complete setup instructions.
|
||||
|
||||
### sync
|
||||
|
||||
Keeps files and the knowledge graph in sync:
|
||||
|
||||
```bash
|
||||
# Basic sync
|
||||
basic-memory sync
|
||||
|
||||
# Watch for changes
|
||||
basic-memory sync --watch
|
||||
|
||||
# Show detailed sync information
|
||||
basic-memory sync --verbose
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--watch`: Continuously monitor for changes
|
||||
- `--verbose`: Show detailed output
|
||||
|
||||
**Note**:
|
||||
|
||||
As of the v0.12.0 release syncing will occur in real time when the mcp process starts.
|
||||
- The real time sync means that it is no longer necessary to run the `basic-memory sync --watch` process in a a terminal to sync changes to the db (so the AI can see them). This will be done automatically.
|
||||
|
||||
This behavior can be changed via the config. The config file for Basic Memory is in the home directory under `.basic-memory/config.json`.
|
||||
|
||||
To change the properties, set the following values:
|
||||
```
|
||||
~/.basic-memory/config.json
|
||||
{
|
||||
"sync_changes": false,
|
||||
}
|
||||
```
|
||||
|
||||
Thanks for using Basic Memory!
|
||||
### import (Enhanced in v0.13.0)
|
||||
|
||||
Imports external knowledge sources with support for project targeting:
|
||||
|
||||
```bash
|
||||
# Claude conversations
|
||||
basic-memory import claude conversations
|
||||
|
||||
# Claude projects
|
||||
basic-memory import claude projects
|
||||
|
||||
# ChatGPT history
|
||||
basic-memory import chatgpt
|
||||
|
||||
# Memory JSON format
|
||||
basic-memory import memory-json /path/to/memory.json
|
||||
|
||||
# Import to specific project (v0.13.0)
|
||||
basic-memory --project=work import claude conversations
|
||||
```
|
||||
|
||||
**New in v0.13.0:**
|
||||
- **Project Targeting**: Import directly to specific projects
|
||||
- **Real-time Sync**: Imported content available immediately
|
||||
- **Unified Database**: All imports stored in centralized database
|
||||
|
||||
> **Note**: Changes sync automatically - no manual sync required in v0.13.0.
|
||||
### status
|
||||
|
||||
Shows system status information:
|
||||
|
||||
```bash
|
||||
# Basic status check
|
||||
basic-memory status
|
||||
|
||||
# Detailed status
|
||||
basic-memory status --verbose
|
||||
|
||||
# JSON output
|
||||
basic-memory status --json
|
||||
```
|
||||
|
||||
|
||||
### project (Enhanced in v0.13.0)
|
||||
|
||||
Manage multiple projects with the new unified database architecture. Projects can now be switched instantly during conversations without restart.
|
||||
|
||||
```bash
|
||||
# List all configured projects with status
|
||||
basic-memory project list
|
||||
|
||||
# Create a new project
|
||||
basic-memory project create work ~/work-basic-memory
|
||||
|
||||
# Set the default project
|
||||
basic-memory project set-default work
|
||||
|
||||
# Delete a project (doesn't delete files)
|
||||
basic-memory project delete personal
|
||||
|
||||
# Show detailed project statistics
|
||||
basic-memory project info
|
||||
```
|
||||
|
||||
**New in v0.13.0:**
|
||||
- **Unified Database**: All projects share a single database for better performance
|
||||
- **Instant Switching**: Switch projects during conversations without restart
|
||||
- **Enhanced Commands**: Updated project commands with better status information
|
||||
- **Project Statistics**: Detailed info about entities, observations, and relations
|
||||
|
||||
#### Using Projects in Commands
|
||||
|
||||
All commands support the `--project` flag to specify which project to use:
|
||||
|
||||
```bash
|
||||
# Sync a specific project
|
||||
basic-memory --project=work sync
|
||||
|
||||
# Run MCP server for a specific project
|
||||
basic-memory --project=personal mcp
|
||||
```
|
||||
|
||||
You can also set the `BASIC_MEMORY_PROJECT` environment variable:
|
||||
|
||||
```bash
|
||||
BASIC_MEMORY_PROJECT=work basic-memory sync
|
||||
```
|
||||
|
||||
### tool (Enhanced in v0.13.0)
|
||||
|
||||
Direct access to MCP tools via CLI with new editing and file management capabilities:
|
||||
|
||||
```bash
|
||||
# Create notes
|
||||
basic-memory tool write-note --title "My Note" --content "Content here"
|
||||
|
||||
# Edit notes incrementally (v0.13.0)
|
||||
echo "New content" | basic-memory tool edit-note --title "My Note" --operation append
|
||||
|
||||
# Move notes (v0.13.0)
|
||||
basic-memory tool move-note --identifier "My Note" --destination "archive/my-note.md"
|
||||
|
||||
# Search notes
|
||||
basic-memory tool search-notes --query "authentication"
|
||||
|
||||
# Project management (v0.13.0)
|
||||
basic-memory tool list-projects
|
||||
basic-memory tool switch-project --project-name "work"
|
||||
```
|
||||
|
||||
**New in v0.13.0:**
|
||||
- **edit-note**: Incremental editing (append, prepend, find/replace, section replace)
|
||||
- **move-note**: File management with database consistency
|
||||
- **Project tools**: list-projects, switch-project, get-current-project
|
||||
- **Cross-project operations**: Use `--project` flag with any tool
|
||||
|
||||
### help
|
||||
|
||||
The full list of commands and help for each can be viewed with the `--help` argument.
|
||||
|
||||
```
|
||||
✗ basic-memory --help
|
||||
|
||||
Usage: basic-memory [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Basic Memory - Local-first personal knowledge management system.
|
||||
|
||||
╭─ Options ─────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ --project -p TEXT Specify which project to use │
|
||||
│ [env var: BASIC_MEMORY_PROJECT] │
|
||||
│ [default: None] │
|
||||
│ --version -V Show version information and exit. │
|
||||
│ --install-completion Install completion for the current shell. │
|
||||
│ --show-completion Show completion for the current shell, to copy it or │
|
||||
│ customize the installation. │
|
||||
│ --help Show this message and exit. │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─ Commands ────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ auth OAuth authentication management (v0.13.0) │
|
||||
│ sync Sync knowledge files with the database │
|
||||
│ status Show sync status between files and database │
|
||||
│ reset Reset database (drop all tables and recreate) │
|
||||
│ mcp Run the MCP server for Claude Desktop integration │
|
||||
│ import Import data from various sources │
|
||||
│ tool Direct access to MCP tools via CLI │
|
||||
│ project Manage multiple Basic Memory projects │
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
|
||||
```bash
|
||||
# Install Basic Memory
|
||||
uv install basic-memory
|
||||
|
||||
# First sync
|
||||
basic-memory sync
|
||||
|
||||
# Start watching mode
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
> **Important**: You need to install Basic Memory via `uv` or `pip` to use the command line tools, see [[Getting Started with Basic Memory#Installation]].
|
||||
|
||||
## Regular Usage
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
basic-memory status
|
||||
|
||||
# Import new content
|
||||
basic-memory import claude conversations
|
||||
|
||||
# Sync changes
|
||||
basic-memory sync
|
||||
|
||||
# Sync changes continuously
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
## Maintenance Tasks
|
||||
|
||||
```bash
|
||||
# Check system status in detail
|
||||
basic-memory status --verbose
|
||||
|
||||
# Full resync of all files
|
||||
basic-memory sync
|
||||
|
||||
# Import updates to specific folder
|
||||
basic-memory import claude conversations --folder new
|
||||
```
|
||||
|
||||
|
||||
## Using stdin with Basic Memory's `write_note` Tool
|
||||
|
||||
The `write-note` tool supports reading content from standard input (stdin), allowing for more flexible workflows when creating or updating notes in your Basic Memory knowledge base.
|
||||
|
||||
### Use Cases
|
||||
|
||||
This feature is particularly useful for:
|
||||
|
||||
1. **Piping output from other commands** directly into Basic Memory notes
|
||||
2. **Creating notes with multi-line content** without having to escape quotes or special characters
|
||||
3. **Integrating with AI assistants** like Claude Code that can generate content and pipe it to Basic Memory
|
||||
4. **Processing text data** from files or other sources
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### Method 1: Using a Pipe
|
||||
|
||||
You can pipe content from another command into `write_note`:
|
||||
|
||||
```bash
|
||||
# Pipe output of a command into a new note
|
||||
echo "# My Note\n\nThis is a test note" | basic-memory tool write-note --title "Test Note" --folder "notes"
|
||||
|
||||
# Pipe output of a file into a new note
|
||||
cat README.md | basic-memory tool write-note --title "Project README" --folder "documentation"
|
||||
|
||||
# Process text through other tools before saving as a note
|
||||
cat data.txt | grep "important" | basic-memory tool write-note --title "Important Data" --folder "data"
|
||||
```
|
||||
|
||||
#### Method 2: Using Heredoc Syntax
|
||||
|
||||
For multi-line content, you can use heredoc syntax:
|
||||
|
||||
```bash
|
||||
# Create a note with heredoc
|
||||
cat << EOF | basic-memory tool write_note --title "Project Ideas" --folder "projects"
|
||||
# Project Ideas for Q2
|
||||
|
||||
## AI Integration
|
||||
- Improve recommendation engine
|
||||
- Add semantic search to product catalog
|
||||
|
||||
## Infrastructure
|
||||
- Migrate to Kubernetes
|
||||
- Implement CI/CD pipeline
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Method 3: Input Redirection
|
||||
|
||||
You can redirect input from a file:
|
||||
|
||||
```bash
|
||||
# Create a note from file content
|
||||
basic-memory tool write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
|
||||
```
|
||||
|
||||
## Integration with Claude Code
|
||||
|
||||
This feature works well with Claude Code in the terminal:
|
||||
|
||||
### cli
|
||||
|
||||
In a Claude Code session, let Claude know he can use the basic-memory tools, then he can execute them via the cli:
|
||||
|
||||
```
|
||||
⏺ Bash(echo "# Test Note from Claude\n\nThis is a test note created by Claude to test the stdin functionality." | basic-memory tool write-note --title "Claude Test Note" --folder "test" --tags "test" --tags "claude")…
|
||||
⎿ # Created test/Claude Test Note.md (23e00eec)
|
||||
permalink: test/claude-test-note
|
||||
|
||||
## Tags
|
||||
- test, claude
|
||||
|
||||
```
|
||||
|
||||
### MCP
|
||||
|
||||
Claude code can also now use mcp tools, so it can use any of the basic-memory tool natively. To install basic-memory in Claude Code:
|
||||
|
||||
Run
|
||||
```
|
||||
claude mcp add basic-memory basic-memory mcp
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
➜ ~ claude mcp add basic-memory basic-memory mcp
|
||||
Added stdio MCP server basic-memory with command: basic-memory mcp to project config
|
||||
➜ ~ claude mcp list
|
||||
basic-memory: basic-memory mcp
|
||||
```
|
||||
|
||||
You can then use the `/mcp` command in the REPL:
|
||||
|
||||
```
|
||||
/mcp
|
||||
⎿ MCP Server Status
|
||||
|
||||
• basic-memory: connected
|
||||
```
|
||||
|
||||
## Version Management (New in v0.13.0)
|
||||
|
||||
Basic Memory v0.13.0 introduces automatic version management and multiple installation options:
|
||||
|
||||
```bash
|
||||
# Stable releases
|
||||
pip install basic-memory
|
||||
|
||||
# Beta/pre-releases
|
||||
pip install basic-memory --pre
|
||||
|
||||
# Latest development builds (auto-published)
|
||||
pip install basic-memory --pre --force-reinstall
|
||||
|
||||
# Check current version
|
||||
basic-memory --version
|
||||
```
|
||||
|
||||
**Version Types:**
|
||||
- **Stable**: `0.13.0` (manual git tags)
|
||||
- **Beta**: `0.13.0b1` (manual git tags)
|
||||
- **Development**: `0.12.4.dev26+468a22f` (automatic from commits)
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### Sync Conflicts
|
||||
|
||||
If you encounter a file changed during sync error:
|
||||
1. Check the file referenced in the error message
|
||||
2. Resolve any conflicts manually
|
||||
3. Run sync again
|
||||
|
||||
### Import Errors
|
||||
|
||||
If import fails:
|
||||
1. Check that the source file is in the correct format
|
||||
2. Verify permissions on the target directory
|
||||
3. Use --verbose flag for detailed error information
|
||||
|
||||
### Status Issues
|
||||
|
||||
If status shows problems:
|
||||
1. Note any unresolved relations or warnings
|
||||
2. Run a full sync to attempt automatic resolution
|
||||
3. Check file permissions if database access errors occur
|
||||
|
||||
|
||||
## Relations
|
||||
- used_by [[Getting Started with Basic Memory]] (Installation instructions)
|
||||
- complements [[User Guide]] (How to use Basic Memory)
|
||||
- relates_to [[Introduction to Basic Memory]] (System overview)
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: Canvas Visualizations
|
||||
type: note
|
||||
permalink: docs/canvas
|
||||
tags:
|
||||
- visualization
|
||||
- mapping
|
||||
- obsidian
|
||||
---
|
||||
|
||||
# Canvas Visualizations
|
||||
|
||||
Basic Memory can create visual knowledge maps using Obsidian's Canvas feature. These visualizations help you understand relationships between concepts, map out processes, and visualize your knowledge structure.
|
||||
|
||||
## Creating Canvas Visualizations
|
||||
|
||||
Ask Claude to create a visualization by describing what you want to map:
|
||||
|
||||
```
|
||||
You: "Create a canvas visualization of my project components and their relationships."
|
||||
|
||||
You: "Make a concept map showing the main themes from our discussion about climate change."
|
||||
|
||||
You: "Can you make a canvas diagram of the perfect pour over method?"
|
||||
```
|
||||
|
||||
![[Canvas.png]]
|
||||
|
||||
## Types of Visualizations
|
||||
|
||||
Basic Memory can create several types of visual maps:
|
||||
|
||||
### Document Maps
|
||||
Visualize connections between your notes and documents
|
||||
|
||||
### Concept Maps
|
||||
Create visual representations of ideas and their relationships
|
||||
|
||||
### Process Diagrams
|
||||
Map workflows, sequences, and procedures
|
||||
|
||||
### Thematic Analysis
|
||||
Organize ideas around central themes
|
||||
|
||||
### Relationship Networks
|
||||
Show how different entities relate to each other
|
||||
|
||||
## Visualization Sources
|
||||
|
||||
Claude can create visualizations based on:
|
||||
|
||||
### Documents in Your Knowledge Base
|
||||
```
|
||||
You: "Create a canvas showing the connections between my project planning documents"
|
||||
```
|
||||
|
||||
### Conversation Content
|
||||
```
|
||||
You: "Make a canvas visualization of the main points we just discussed"
|
||||
```
|
||||
|
||||
### Search Results
|
||||
```
|
||||
You: "Find all my notes about psychology and create a visual map of the concepts"
|
||||
```
|
||||
|
||||
### Themes and Relationships
|
||||
```
|
||||
You: "Create a visual map showing how different philosophical schools relate to each other"
|
||||
```
|
||||
|
||||
## Visualization Workflow
|
||||
|
||||
1. **Request a visualization** by describing what you want to see
|
||||
2. **Claude creates the canvas file** in your Basic Memory directory
|
||||
3. **Open the file in Obsidian** to view the visualization
|
||||
4. **Refine the visualization** by asking Claude for adjustments:
|
||||
```
|
||||
You: "Could you reorganize the canvas to group related components together?"
|
||||
|
||||
You: "Please add more detail about the connection between these two concepts."
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
Behind the scenes, Claude:
|
||||
|
||||
1. Creates a `.canvas` file in JSON format
|
||||
2. Adds nodes for each concept or document
|
||||
3. Creates edges to represent relationships
|
||||
4. Sets positions for visual clarity
|
||||
5. Includes any relevant metadata
|
||||
|
||||
The resulting file is fully compatible with Obsidian's Canvas feature and can be edited directly in Obsidian.
|
||||
|
||||
## Tips for Effective Visualizations
|
||||
|
||||
- **Be specific** about what you want to visualize
|
||||
- **Specify the level of detail** you need
|
||||
- **Mention the visualization type** you want (concept map, process flow, etc.)
|
||||
- **Start simple** and ask for refinements
|
||||
- **Provide context** about what documents or concepts to include
|
||||
|
||||
## Relations
|
||||
- enhances [[Obsidian Integration]] (Using Basic Memory with Obsidian)
|
||||
- visualizes [[Knowledge Format]] (The structure of your knowledge)
|
||||
- complements [[User Guide]] (Ways to use Basic Memory)
|
||||
@@ -1,335 +0,0 @@
|
||||
# Claude.ai Integration Guide
|
||||
|
||||
This guide explains how to connect Basic Memory to Claude.ai, enabling Claude to read and write to your personal knowledge base.
|
||||
|
||||
## Overview
|
||||
|
||||
When connected to Claude.ai, Basic Memory provides:
|
||||
- Persistent memory across conversations
|
||||
- Knowledge graph navigation
|
||||
- Note-taking and search capabilities
|
||||
- File organization and management
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Basic Memory MCP server with OAuth enabled
|
||||
2. Public HTTPS URL (or tunneling service for testing)
|
||||
3. Claude.ai account (Free, Pro, or Enterprise)
|
||||
|
||||
## Quick Start (Testing)
|
||||
|
||||
### 1. Start MCP Server with OAuth
|
||||
|
||||
```bash
|
||||
# Enable OAuth with basic provider
|
||||
export FASTMCP_AUTH_ENABLED=true
|
||||
export FASTMCP_AUTH_PROVIDER=basic
|
||||
|
||||
# Start server on all interfaces
|
||||
basic-memory mcp --transport streamable-http --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 2. Make Server Accessible
|
||||
|
||||
For testing, use ngrok:
|
||||
|
||||
```bash
|
||||
# Install ngrok
|
||||
brew install ngrok # macOS
|
||||
# or download from https://ngrok.com
|
||||
|
||||
# Create tunnel
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
Note the HTTPS URL (e.g., `https://abc123.ngrok.io`)
|
||||
|
||||
### 3. Register OAuth Client
|
||||
|
||||
```bash
|
||||
# Register a client for Claude
|
||||
basic-memory auth register-client --client-id claude-ai
|
||||
|
||||
# Save the credentials!
|
||||
# Client ID: claude-ai
|
||||
# Client Secret: xxx...
|
||||
```
|
||||
|
||||
### 4. Connect in Claude.ai
|
||||
|
||||
1. Go to Claude.ai → Settings → Integrations
|
||||
2. Click "Add More"
|
||||
3. Enter your server URL: `https://abc123.ngrok.io/mcp`
|
||||
4. Click "Connect"
|
||||
5. Authorize the connection
|
||||
|
||||
### 5. Use in Conversations
|
||||
|
||||
- Click the tools icon (🔧) in the chat
|
||||
- Select "Basic Memory"
|
||||
- Try commands like:
|
||||
- "Create a note about our meeting"
|
||||
- "Search for project ideas"
|
||||
- "Show recent notes"
|
||||
|
||||
## Production Setup
|
||||
|
||||
### 1. Deploy with Supabase Auth
|
||||
|
||||
```bash
|
||||
# .env file
|
||||
FASTMCP_AUTH_ENABLED=true
|
||||
FASTMCP_AUTH_PROVIDER=supabase
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_KEY=your-service-key
|
||||
```
|
||||
|
||||
### 2. Deploy to Cloud
|
||||
|
||||
Options for deployment:
|
||||
|
||||
#### Vercel
|
||||
```json
|
||||
// vercel.json
|
||||
{
|
||||
"functions": {
|
||||
"api/mcp.py": {
|
||||
"runtime": "python3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Railway
|
||||
```bash
|
||||
# Install Railway CLI
|
||||
brew install railway
|
||||
|
||||
# Deploy
|
||||
railway init
|
||||
railway up
|
||||
```
|
||||
|
||||
#### Docker
|
||||
```dockerfile
|
||||
FROM python:3.12
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install -e .
|
||||
CMD ["basic-memory", "mcp", "--transport", "streamable-http"]
|
||||
```
|
||||
|
||||
### 3. Configure for Organization
|
||||
|
||||
For Claude.ai Enterprise:
|
||||
|
||||
1. **Admin Setup**:
|
||||
- Go to Organizational Settings
|
||||
- Navigate to Integrations
|
||||
- Add MCP server URL for all users
|
||||
- Configure allowed scopes
|
||||
|
||||
2. **User Permissions**:
|
||||
- Users connect individually
|
||||
- Each user has their own auth token
|
||||
- Scopes determine access level
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Use HTTPS
|
||||
- Required for OAuth
|
||||
- Encrypt all data in transit
|
||||
- Use proper SSL certificates
|
||||
|
||||
### 2. Implement Scopes
|
||||
```bash
|
||||
# Configure required scopes
|
||||
FASTMCP_AUTH_REQUIRED_SCOPES=read,write
|
||||
|
||||
# User-specific scopes
|
||||
read: Can search and read notes
|
||||
write: Can create and update notes
|
||||
admin: Can manage all data
|
||||
```
|
||||
|
||||
### 3. Token Security
|
||||
- Short-lived access tokens (1 hour)
|
||||
- Refresh token rotation
|
||||
- Secure token storage
|
||||
|
||||
### 4. Rate Limiting
|
||||
```python
|
||||
# In your MCP server
|
||||
from fastapi import HTTPException
|
||||
from slowapi import Limiter
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
@app.get("/mcp")
|
||||
@limiter.limit("100/minute")
|
||||
async def mcp_endpoint():
|
||||
# Handle MCP requests
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Custom Tools
|
||||
|
||||
Create specialized tools for Claude:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def analyze_notes(topic: str) -> str:
|
||||
"""Analyze all notes on a specific topic."""
|
||||
# Search and analyze implementation
|
||||
return analysis
|
||||
```
|
||||
|
||||
### 2. Context Preservation
|
||||
|
||||
Use memory:// URLs to maintain context:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def continue_conversation(memory_url: str) -> str:
|
||||
"""Continue from a previous conversation."""
|
||||
context = await build_context(memory_url)
|
||||
return context
|
||||
```
|
||||
|
||||
### 3. Multi-User Support
|
||||
|
||||
With Supabase, each user has isolated data:
|
||||
|
||||
```sql
|
||||
-- Row-level security
|
||||
CREATE POLICY "Users see own notes"
|
||||
ON notes FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
1. **"Failed to connect"**
|
||||
- Verify server is running
|
||||
- Check HTTPS is working
|
||||
- Confirm OAuth is enabled
|
||||
|
||||
2. **"Authorization failed"**
|
||||
- Check client credentials
|
||||
- Verify redirect URLs
|
||||
- Review OAuth logs
|
||||
|
||||
3. **"No tools available"**
|
||||
- Ensure MCP tools are registered
|
||||
- Check required scopes
|
||||
- Verify transport type
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable detailed logging:
|
||||
|
||||
```bash
|
||||
# Server side
|
||||
export FASTMCP_LOG_LEVEL=DEBUG
|
||||
export LOGURU_LEVEL=DEBUG
|
||||
|
||||
# Check logs
|
||||
tail -f logs/mcp.log
|
||||
```
|
||||
|
||||
### Test Connection
|
||||
|
||||
```bash
|
||||
# Test OAuth flow
|
||||
curl https://your-server.com/mcp/.well-known/oauth-authorization-server
|
||||
|
||||
# Should return OAuth metadata
|
||||
{
|
||||
"issuer": "https://your-server.com",
|
||||
"authorization_endpoint": "https://your-server.com/auth/authorize",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Regular Backups**
|
||||
- Export your knowledge base
|
||||
- Use version control
|
||||
- Multiple storage locations
|
||||
|
||||
2. **Access Control**
|
||||
- Principle of least privilege
|
||||
- Regular token rotation
|
||||
- Audit access logs
|
||||
|
||||
3. **Performance**
|
||||
- Index frequently searched fields
|
||||
- Optimize large knowledge bases
|
||||
- Use caching where appropriate
|
||||
|
||||
4. **User Experience**
|
||||
- Clear tool descriptions
|
||||
- Helpful error messages
|
||||
- Quick response times
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating Notes
|
||||
|
||||
```
|
||||
User: Create a note about the meeting with the product team
|
||||
|
||||
Claude: I'll create a note about your meeting with the product team.
|
||||
|
||||
[Uses write_note tool]
|
||||
|
||||
Note created: "Meeting with Product Team - 2024-01-15"
|
||||
Location: Work/Meetings/
|
||||
|
||||
I've documented the meeting notes. The note includes the date, attendees, and key discussion points.
|
||||
```
|
||||
|
||||
### Searching Knowledge
|
||||
|
||||
```
|
||||
User: What did we discuss about the API redesign?
|
||||
|
||||
Claude: Let me search for information about the API redesign.
|
||||
|
||||
[Uses search_notes tool]
|
||||
|
||||
I found 3 relevant notes about the API redesign:
|
||||
|
||||
1. "API Redesign Proposal" (2024-01-10)
|
||||
- RESTful architecture
|
||||
- Version 2.0 specifications
|
||||
- Migration timeline
|
||||
|
||||
2. "Technical Review: API Changes" (2024-01-12)
|
||||
- Breaking changes documented
|
||||
- Backwards compatibility plan
|
||||
|
||||
3. "Meeting: API Implementation" (2024-01-14)
|
||||
- Team assignments
|
||||
- Q1 deliverables
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Set up production deployment
|
||||
2. Configure organizational access
|
||||
3. Create custom tools for your workflow
|
||||
4. Implement advanced security features
|
||||
5. Monitor usage and performance
|
||||
|
||||
## Resources
|
||||
|
||||
- [Basic Memory Documentation](../README.md)
|
||||
- [OAuth Setup Guide](OAuth%20Authentication.md)
|
||||
- [MCP Specification](https://modelcontextprotocol.io)
|
||||
- [Claude.ai Help Center](https://support.anthropic.com)
|
||||
@@ -0,0 +1,334 @@
|
||||
# Docker Setup Guide
|
||||
|
||||
Basic Memory can be run in Docker containers to provide a consistent, isolated environment for your knowledge management
|
||||
system. This is particularly useful for integrating with existing Dockerized MCP servers or for deployment scenarios.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Using Pre-built Images (Recommended)
|
||||
|
||||
Basic Memory provides pre-built Docker images on GitHub Container Registry that are automatically updated with each release.
|
||||
|
||||
1. **Use the official image directly:**
|
||||
```bash
|
||||
docker run -d \
|
||||
--name basic-memory-server \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/your/obsidian-vault:/app/data:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
ghcr.io/basicmachines-co/basic-memory:latest
|
||||
```
|
||||
|
||||
2. **Or use Docker Compose with the pre-built image:**
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
basic-memory:
|
||||
image: ghcr.io/basicmachines-co/basic-memory:latest
|
||||
container_name: basic-memory-server
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- /path/to/your/obsidian-vault:/app/data:rw
|
||||
- basic-memory-config:/root/.basic-memory:rw
|
||||
environment:
|
||||
- BASIC_MEMORY_DEFAULT_PROJECT=main
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Option 2: Using Docker Compose (Building Locally)
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/basicmachines-co/basic-memory.git
|
||||
cd basic-memory
|
||||
```
|
||||
|
||||
2. **Update the docker-compose.yml:**
|
||||
Edit the volume mount to point to your Obsidian vault:
|
||||
```yaml
|
||||
volumes:
|
||||
# Change './obsidian-vault' to your actual directory path
|
||||
- /path/to/your/obsidian-vault:/app/data:rw
|
||||
```
|
||||
|
||||
3. **Start the container:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Option 3: Using Docker CLI
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t basic-memory .
|
||||
|
||||
# Run with volume mounting
|
||||
docker run -d \
|
||||
--name basic-memory-server \
|
||||
-v /path/to/your/obsidian-vault:/app/data:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
-e BASIC_MEMORY_DEFAULT_PROJECT=main \
|
||||
basic-memory
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Volume Mounts
|
||||
|
||||
Basic Memory requires several volume mounts for proper operation:
|
||||
|
||||
1. **Knowledge Directory** (Required):
|
||||
```yaml
|
||||
- /path/to/your/obsidian-vault:/app/data:rw
|
||||
```
|
||||
Mount your Obsidian vault or knowledge base directory.
|
||||
|
||||
2. **Configuration and Database** (Recommended):
|
||||
```yaml
|
||||
- basic-memory-config:/root/.basic-memory:rw
|
||||
```
|
||||
Persistent storage for configuration and SQLite database.
|
||||
|
||||
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json after Basic Memory starts.
|
||||
|
||||
3. **Multiple Projects** (Optional):
|
||||
```yaml
|
||||
- /path/to/project1:/app/data/project1:rw
|
||||
- /path/to/project2:/app/data/project2:rw
|
||||
```
|
||||
|
||||
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json
|
||||
|
||||
## CLI Commands via Docker
|
||||
|
||||
You can run Basic Memory CLI commands inside the container using `docker exec`:
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
docker exec basic-memory-server basic-memory status
|
||||
|
||||
# Sync files
|
||||
docker exec basic-memory-server basic-memory sync
|
||||
|
||||
# Show help
|
||||
docker exec basic-memory-server basic-memory --help
|
||||
```
|
||||
|
||||
### Managing Projects with Volume Mounts
|
||||
|
||||
When using Docker volumes, you'll need to configure projects to point to your mounted directories:
|
||||
|
||||
1. **Check current configuration:**
|
||||
```bash
|
||||
docker exec basic-memory-server cat /root/.basic-memory/config.json
|
||||
```
|
||||
|
||||
2. **Add a project for your mounted volume:**
|
||||
```bash
|
||||
# If you mounted /path/to/your/vault to /app/data
|
||||
docker exec basic-memory-server basic-memory project create my-vault /app/data
|
||||
|
||||
# Set it as default
|
||||
docker exec basic-memory-server basic-memory project set-default my-vault
|
||||
```
|
||||
|
||||
3. **Sync the new project:**
|
||||
```bash
|
||||
docker exec basic-memory-server basic-memory sync
|
||||
```
|
||||
|
||||
### Example: Setting up an Obsidian Vault
|
||||
|
||||
If you mounted your Obsidian vault like this in docker-compose.yml:
|
||||
```yaml
|
||||
volumes:
|
||||
- /Users/yourname/Documents/ObsidianVault:/app/data:rw
|
||||
```
|
||||
|
||||
Then configure it:
|
||||
```bash
|
||||
# Create project pointing to mounted vault
|
||||
docker exec basic-memory-server basic-memory project create obsidian /app/data
|
||||
|
||||
# Set as default
|
||||
docker exec basic-memory-server basic-memory project set-default obsidian
|
||||
|
||||
# Sync to index all files
|
||||
docker exec basic-memory-server basic-memory sync
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure Basic Memory using environment variables:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
|
||||
# Default project
|
||||
- BASIC_MEMORY_DEFAULT_PROJECT=main
|
||||
|
||||
# Enable real-time sync
|
||||
- BASIC_MEMORY_SYNC_CHANGES=true
|
||||
|
||||
# Logging level
|
||||
- BASIC_MEMORY_LOG_LEVEL=INFO
|
||||
|
||||
# Sync delay in milliseconds
|
||||
- BASIC_MEMORY_SYNC_DELAY=1000
|
||||
```
|
||||
|
||||
## File Permissions
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
Ensure your knowledge directories have proper permissions:
|
||||
|
||||
```bash
|
||||
# Make directories readable/writable
|
||||
chmod -R 755 /path/to/your/obsidian-vault
|
||||
|
||||
# If using specific user/group
|
||||
chown -R $USER:$USER /path/to/your/obsidian-vault
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
When using Docker Desktop on Windows, ensure the directories are shared:
|
||||
|
||||
1. Open Docker Desktop
|
||||
2. Go to Settings → Resources → File Sharing
|
||||
3. Add your knowledge directory path
|
||||
4. Apply & Restart
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **File Watching Not Working:**
|
||||
- Ensure volume mounts are read-write (`:rw`)
|
||||
- Check directory permissions
|
||||
- On Linux, may need to increase inotify limits:
|
||||
```bash
|
||||
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
2. **Configuration Not Persisting:**
|
||||
- Use named volumes for `/root/.basic-memory`
|
||||
- Check volume mount permissions
|
||||
|
||||
3. **Network Connectivity:**
|
||||
- For HTTP transport, ensure port 8000 is exposed
|
||||
- Check firewall settings
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Run with debug logging:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- BASIC_MEMORY_LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```bash
|
||||
docker-compose logs -f basic-memory
|
||||
```
|
||||
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Docker Security:**
|
||||
The container runs as root for simplicity. For production, consider additional security measures.
|
||||
|
||||
2. **Volume Permissions:**
|
||||
Ensure mounted directories have appropriate permissions and don't expose sensitive data.
|
||||
|
||||
3. **Network Security:**
|
||||
If using HTTP transport, consider using reverse proxy with SSL/TLS and authentication if the endpoint is available on
|
||||
a network.
|
||||
|
||||
4. **IMPORTANT:** The HTTP endpoints have no authorization. They should not be exposed on a public network.
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Claude Desktop with Docker
|
||||
|
||||
The recommended way to connect Claude Desktop to the containerized Basic Memory is using `mcp-proxy`, which converts the HTTP transport to STDIO that Claude Desktop expects:
|
||||
|
||||
1. **Start the Docker container:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Configure Claude Desktop** to use mcp-proxy:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-proxy",
|
||||
"http://localhost:8000/mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
For Docker-specific issues:
|
||||
|
||||
1. Check the [troubleshooting section](#troubleshooting) above
|
||||
2. Review container logs: `docker-compose logs basic-memory`
|
||||
3. Verify volume mounts: `docker inspect basic-memory-server`
|
||||
4. Test file permissions: `docker exec basic-memory-server ls -la /root`
|
||||
|
||||
For general Basic Memory support, see the main [README](../README.md)
|
||||
and [documentation](https://memory.basicmachines.co/).
|
||||
|
||||
## GitHub Container Registry Images
|
||||
|
||||
### Available Images
|
||||
|
||||
Pre-built Docker images are available on GitHub Container Registry at [`ghcr.io/basicmachines-co/basic-memory`](https://github.com/basicmachines-co/basic-memory/pkgs/container/basic-memory).
|
||||
|
||||
**Supported architectures:**
|
||||
- `linux/amd64` (Intel/AMD x64)
|
||||
- `linux/arm64` (ARM64, including Apple Silicon)
|
||||
|
||||
**Available tags:**
|
||||
- `latest` - Latest stable release
|
||||
- `v0.13.8`, `v0.13.7`, etc. - Specific version tags
|
||||
- `v0.13`, `v0.12`, etc. - Major.minor tags
|
||||
|
||||
### Automated Builds
|
||||
|
||||
Docker images are automatically built and published when new releases are tagged:
|
||||
|
||||
1. **Release Process:** When a git tag matching `v*` (e.g., `v0.13.8`) is pushed, the CI workflow automatically:
|
||||
- Builds multi-platform Docker images
|
||||
- Pushes to GitHub Container Registry with appropriate tags
|
||||
- Uses native GitHub integration for seamless publishing
|
||||
|
||||
2. **CI/CD Pipeline:** The Docker workflow includes:
|
||||
- Multi-platform builds (AMD64 and ARM64)
|
||||
- Layer caching for faster builds
|
||||
- Automatic tagging with semantic versioning
|
||||
- Security scanning and optimization
|
||||
|
||||
### Setup Requirements (For Maintainers)
|
||||
|
||||
GitHub Container Registry integration is automatic for this repository:
|
||||
|
||||
1. **No external setup required** - GHCR is natively integrated with GitHub
|
||||
2. **Automatic permissions** - Uses `GITHUB_TOKEN` with `packages: write` permission
|
||||
3. **Public by default** - Images are automatically public for public repositories
|
||||
|
||||
The Docker CI workflow (`.github/workflows/docker.yml`) handles everything automatically when version tags are pushed.
|
||||
@@ -1,355 +0,0 @@
|
||||
---
|
||||
title: Getting Started with Basic Memory
|
||||
type: note
|
||||
permalink: docs/getting-started
|
||||
---
|
||||
|
||||
# Getting Started with Basic Memory
|
||||
|
||||
This guide will help you install Basic Memory, configure it with Claude Desktop, and create your first knowledge notes
|
||||
through conversations.
|
||||
|
||||
Basic Memory uses the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) to connect with LLMs.
|
||||
It can be used with any service that supports the MCP, but Claude Desktop works especially well.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The easiest way to install basic memory is via `uv`. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
### 1. Install Basic Memory
|
||||
|
||||
**v0.13.0 offers multiple installation options:**
|
||||
|
||||
```bash
|
||||
# Stable release (recommended)
|
||||
uv tool install basic-memory
|
||||
# or: pip install basic-memory
|
||||
|
||||
# Beta releases (new features, testing)
|
||||
pip install basic-memory --pre
|
||||
|
||||
# Development builds (latest changes)
|
||||
pip install basic-memory --pre --force-reinstall
|
||||
```
|
||||
|
||||
**Version Information:**
|
||||
- **Stable**: Latest tested release (e.g., `0.13.0`)
|
||||
- **Beta**: Pre-release versions (e.g., `0.13.0b1`)
|
||||
- **Development**: Auto-published from git commits (e.g., `0.12.4.dev26+468a22f`)
|
||||
|
||||
> **Important**: You need to install Basic Memory using one of the commands above to use the command line tools.
|
||||
|
||||
Using `uv tool install` will install the basic-memory package in a standalone virtual environment. See the [UV docs](https://docs.astral.sh/uv/concepts/tools/) for more info.
|
||||
|
||||
### 2. Configure Claude Desktop
|
||||
|
||||
Edit your Claude Desktop config, located at `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Restart Claude Desktop**. You should see Basic Memory tools available in the "tools" menu in Claude Desktop (the little hammer icon in the bottom-right corner of the chat interface). Click it to view available tools.
|
||||
#### Fix Path to uv
|
||||
|
||||
If you get an error that says `ENOENT` , this most likely means Claude Desktop could not find your `uv` installation. Make sure that you have `uv` installed per the instructions above, then:
|
||||
|
||||
**Step 1: Find the absolute path to uvx**
|
||||
|
||||
Open Terminal and run:
|
||||
|
||||
```bash
|
||||
which uvx
|
||||
```
|
||||
|
||||
This will show you the full path (e.g., `/Users/yourusername/.cargo/bin/uvx`).
|
||||
|
||||
**Step 2: Edit Claude Desktop Configuration**
|
||||
|
||||
Edit the Claude Desktop config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "/absolute/path/to/uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `/absolute/path/to/uvx` with the actual path you found in Step 1.
|
||||
|
||||
**Step 3: Restart Claude Desktop**
|
||||
|
||||
Close and reopen Claude Desktop for the changes to take effect.
|
||||
|
||||
### 3. Sync changes in real time
|
||||
|
||||
> **Note**: The service will sync changes from your project directory in real time so they available for the AI assistant.
|
||||
|
||||
To disable realtime sync, you can update the config. See [[CLI Reference#sync]].
|
||||
### 4. Staying Updated
|
||||
|
||||
To update Basic Memory when new versions are released:
|
||||
|
||||
```bash
|
||||
# Update stable release
|
||||
uv tool upgrade basic-memory
|
||||
# or: pip install --upgrade basic-memory
|
||||
|
||||
# Update to latest beta (v0.13.0)
|
||||
pip install --upgrade basic-memory --pre
|
||||
|
||||
# Get latest development build
|
||||
pip install --upgrade basic-memory --pre --force-reinstall
|
||||
```
|
||||
|
||||
**v0.13.0 Update Benefits:**
|
||||
- **Fluid project switching** during conversations
|
||||
- **Advanced note editing** capabilities
|
||||
- **Smart file management** with move operations
|
||||
- **Enhanced search** with frontmatter tag support
|
||||
|
||||
> **Note**: After updating, restart Claude Desktop for changes to take effect. No sync restart needed in v0.13.0.
|
||||
|
||||
### 5. Multi-Project Setup (Enhanced in v0.13.0)
|
||||
|
||||
By default, Basic Memory creates a project in `~/basic-memory`. v0.13.0 introduces **fluid project management** - switch between projects instantly during conversations.
|
||||
|
||||
```
|
||||
# Create a new project
|
||||
basic-memory project create work ~/work-basic-memory
|
||||
|
||||
# Set the default project
|
||||
basic-memory project set-default work
|
||||
|
||||
# List all projects with status
|
||||
basic-memory project list
|
||||
|
||||
# Get detailed project information
|
||||
basic-memory project info
|
||||
```
|
||||
|
||||
**New in v0.13.0:**
|
||||
- **Instant switching**: Change projects during conversations without restart
|
||||
- **Unified database**: All projects in single `~/.basic-memory/memory.db`
|
||||
- **Better performance**: Optimized queries and reduced file I/O
|
||||
- **Session context**: Maintains active project throughout conversations
|
||||
|
||||
## Troubleshooting Installation
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Claude Says "No Basic Memory Tools Available"
|
||||
|
||||
If Claude cannot find Basic Memory tools:
|
||||
|
||||
1. **Check absolute paths**: Ensure you're using complete absolute paths to uvx in the Claude Desktop configuration
|
||||
2. **Verify installation**: Run `basic-memory --version` in Terminal to confirm Basic Memory is installed
|
||||
3. **Restart applications**: Restart both Terminal and Claude Desktop after making configuration changes
|
||||
4. **Check sync status**: You can view the sync status by running `basic-memory status
|
||||
.
|
||||
#### Permission Issues
|
||||
|
||||
If you encounter permission errors:
|
||||
|
||||
1. Check that Basic Memory has access to create files in your home directory
|
||||
2. Ensure Claude Desktop has permission to execute the uvx command
|
||||
|
||||
## Creating Your First Knowledge Note
|
||||
|
||||
1. **Open Claude Desktop** and start a new conversation.
|
||||
|
||||
2. **Have a natural conversation** about any topic:
|
||||
```
|
||||
You: "Let's talk about coffee brewing methods I've been experimenting with."
|
||||
Claude: "I'd be happy to discuss coffee brewing methods..."
|
||||
You: "I've found that pour over gives more flavor clarity than French press..."
|
||||
```
|
||||
|
||||
3. **Ask Claude to create a note**:
|
||||
```
|
||||
You: "Could you create a note summarizing what we've discussed about coffee brewing?"
|
||||
```
|
||||
|
||||
4. **Confirm note creation**:
|
||||
Claude will confirm when the note has been created and where it's stored.
|
||||
|
||||
5. **View the created file** in your `~/basic-memory` directory using any text editor or Obsidian.
|
||||
The file structure will look similar to:
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
permalink: coffee-brewing-methods
|
||||
tags: [coffee, brewing, equipment] # v0.13.0: Now searchable!
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
## Observations
|
||||
- [method] Pour over provides more clarity...
|
||||
- [technique] Water temperature at 205°F...
|
||||
|
||||
## Relations
|
||||
- relates_to [[Other Coffee Topics]]
|
||||
```
|
||||
|
||||
**v0.13.0 Improvements:**
|
||||
- **Real-time sync**: Changes appear immediately, no background sync needed
|
||||
- **Searchable tags**: Frontmatter tags are now indexed for search
|
||||
- **Better file organization**: Enhanced file management capabilities
|
||||
|
||||
## Using Special Prompts
|
||||
|
||||
Basic Memory includes special prompts that help you start conversations with context from your knowledge base:
|
||||
|
||||
### Continue Conversation
|
||||
|
||||
To resume a previous topic:
|
||||
|
||||
```
|
||||
You: "Let's continue our conversation about coffee brewing."
|
||||
```
|
||||
|
||||
This prompt triggers Claude to:
|
||||
|
||||
1. Search your knowledge base for relevant content about coffee brewing
|
||||
2. Build context from these documents
|
||||
3. Resume the conversation with full awareness of previous discussions
|
||||
|
||||
### Recent Activity
|
||||
|
||||
To see what you've been working on:
|
||||
|
||||
```
|
||||
You: "What have we been discussing recently?"
|
||||
```
|
||||
|
||||
This prompt causes Claude to:
|
||||
|
||||
1. Retrieve documents modified in the recent past
|
||||
2. Summarize the topics and main points
|
||||
3. Offer to continue any of those discussions
|
||||
|
||||
### Search
|
||||
|
||||
To find specific information:
|
||||
|
||||
```
|
||||
You: "Find information about pour over coffee methods."
|
||||
```
|
||||
|
||||
Claude will:
|
||||
|
||||
1. Search your knowledge base for relevant documents
|
||||
2. Summarize the key findings
|
||||
3. Offer to explore specific documents in more detail
|
||||
|
||||
See [[User Guide#Using Special Prompts]] for further information.
|
||||
|
||||
## Using Your Knowledge Base
|
||||
|
||||
### Referencing Knowledge
|
||||
|
||||
In future conversations, reference your existing knowledge:
|
||||
|
||||
```
|
||||
You: "What water temperature did we decide was optimal for coffee brewing?"
|
||||
```
|
||||
|
||||
Or directly reference notes using memory:// URLs:
|
||||
|
||||
```
|
||||
You: "Take a look at memory://coffee-brewing-methods and let's discuss how to improve my technique."
|
||||
```
|
||||
|
||||
### Building On Previous Knowledge (Enhanced in v0.13.0)
|
||||
|
||||
Basic Memory enables continuous knowledge building:
|
||||
|
||||
1. **Reference previous discussions** in new conversations
|
||||
2. **Edit notes incrementally** without rewriting entire documents
|
||||
3. **Move and organize notes** as your knowledge base grows
|
||||
4. **Switch between projects** instantly during conversations
|
||||
5. **Search by tags** to find related content quickly
|
||||
6. **Create connections** between related topics
|
||||
7. **Follow relationships** to build comprehensive context
|
||||
|
||||
### v0.13.0 Workflow Examples
|
||||
|
||||
**Incremental Editing:**
|
||||
```
|
||||
You: "Add a section about espresso to my coffee brewing notes"
|
||||
Claude: [Uses edit_note to append new section]
|
||||
```
|
||||
|
||||
**File Organization:**
|
||||
```
|
||||
You: "Move my old meeting notes to an archive folder"
|
||||
Claude: [Uses move_note with database consistency]
|
||||
```
|
||||
|
||||
**Project Switching:**
|
||||
```
|
||||
You: "Switch to my work project and show recent activity"
|
||||
Claude: [Switches projects and shows work-specific content]
|
||||
```
|
||||
|
||||
## Importing Existing Conversations
|
||||
|
||||
Import your existing AI conversations:
|
||||
|
||||
```bash
|
||||
# From Claude
|
||||
basic-memory import claude conversations
|
||||
|
||||
# From ChatGPT
|
||||
basic-memory import chatgpt
|
||||
```
|
||||
|
||||
After importing, changes sync automatically in real-time. You can see project statistics by running `basic-memory project info`.
|
||||
|
||||
## Quick Tips
|
||||
|
||||
### General Usage
|
||||
- Basic Memory syncs changes in real-time (no manual sync needed)
|
||||
- Use special prompts (Continue Conversation, Recent Activity, Search) to start contextual discussions
|
||||
- Build connections between notes for a richer knowledge graph
|
||||
- Use direct `memory://` URLs with permalinks for precise context
|
||||
- Review and edit AI-generated notes for accuracy
|
||||
|
||||
### v0.13.0 Features
|
||||
- **Switch projects instantly**: "Switch to my work project" - no restart needed
|
||||
- **Edit notes incrementally**: "Add a section about..." instead of rewriting
|
||||
- **Organize with moves**: "Move this to my archive folder" with database consistency
|
||||
- **Search by tags**: Frontmatter tags are now searchable
|
||||
- **Try beta builds**: `pip install basic-memory --pre` for latest features
|
||||
|
||||
## Next Steps
|
||||
|
||||
After getting started, explore these areas:
|
||||
|
||||
1. **Read the [[User Guide]]** for comprehensive usage instructions
|
||||
2. **Understand the [[Knowledge Format]]** to learn how knowledge is structured
|
||||
3. **Set up [[Obsidian Integration]]** for visual knowledge navigation
|
||||
4. **Learn about [[Canvas]]** visualizations for mapping concepts
|
||||
5. **Review the [[CLI Reference]]** for command line tools
|
||||
6. **Explore [[OAuth Authentication Guide]]** for secure remote access (v0.13.0)
|
||||
7. **Set up multiple projects** for different knowledge areas (v0.13.0)
|
||||
@@ -1,207 +0,0 @@
|
||||
---
|
||||
title: Knowledge Format
|
||||
type: note
|
||||
permalink: docs/knowledge-format
|
||||
tags:
|
||||
- architecture
|
||||
- patterns
|
||||
- knowledge
|
||||
- design
|
||||
---
|
||||
|
||||
# Knowledge Format
|
||||
|
||||
Basic Memory uses standard Markdown with simple semantic patterns to create a knowledge graph. This document details the file structure and patterns used to organize knowledge.
|
||||
|
||||
## File-First Architecture
|
||||
|
||||
All knowledge in Basic Memory is stored in plain text Markdown files:
|
||||
|
||||
- Files are the source of truth for all knowledge
|
||||
- Changes to files automatically update the knowledge graph
|
||||
- You maintain complete ownership and control
|
||||
- Files work with git and other version control systems
|
||||
- Knowledge persists independently of any AI conversation
|
||||
|
||||
## Core Document Structure
|
||||
|
||||
Every document uses this basic structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Document Title
|
||||
type: note
|
||||
tags: [tag1, tag2]
|
||||
permalink: custom-path
|
||||
---
|
||||
|
||||
# Document Title
|
||||
|
||||
Regular markdown content...
|
||||
|
||||
## Observations
|
||||
- [category] Content with #tags (optional context)
|
||||
|
||||
## Relations
|
||||
- relation_type [[Other Document]] (optional context)
|
||||
```
|
||||
|
||||
### Frontmatter
|
||||
|
||||
The YAML frontmatter at the top of each file defines essential metadata:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Document Title # Used for linking and references
|
||||
type: note # Document type
|
||||
tags: [tag1, tag2] # For organization and searching
|
||||
permalink: custom-link # Optional custom URL path
|
||||
---
|
||||
```
|
||||
|
||||
The title is particularly important as it's used to create links between documents.
|
||||
|
||||
### Observations
|
||||
|
||||
Observations are facts or statements about a topic:
|
||||
|
||||
```markdown
|
||||
## Observations
|
||||
- [tech] Uses SQLite for storage #database
|
||||
- [design] Follows local-first architecture #architecture
|
||||
- [decision] Selected bcrypt for passwords #security (Based on audit)
|
||||
```
|
||||
|
||||
Each observation contains:
|
||||
- **Category** in [brackets] - classifies the information type
|
||||
- **Content text** - the main information
|
||||
- Optional **#tags** - additional categorization
|
||||
- Optional **(context)** - supporting details
|
||||
|
||||
Common categories include:
|
||||
- `[tech]`: Technical details
|
||||
- `[design]`: Architecture decisions
|
||||
- `[feature]`: User capabilities
|
||||
- `[decision]`: Choices that were made
|
||||
- `[principle]`: Fundamental concepts
|
||||
- `[method]`: Approaches or techniques
|
||||
- `[preference]`: Personal opinions
|
||||
|
||||
### Relations
|
||||
|
||||
Relations connect documents to form the knowledge graph:
|
||||
|
||||
```markdown
|
||||
## Relations
|
||||
- implements [[Search Design]]
|
||||
- depends_on [[Database Schema]]
|
||||
- relates_to [[User Interface]]
|
||||
```
|
||||
|
||||
You can also create inline references:
|
||||
```markdown
|
||||
This builds on [[Core Design]] and uses [[Utility Functions]].
|
||||
```
|
||||
|
||||
Common relation types include:
|
||||
- `implements`: Implementation of a specification
|
||||
- `depends_on`: Required dependency
|
||||
- `relates_to`: General connection
|
||||
- `inspired_by`: Source of ideas
|
||||
- `extends`: Enhancement
|
||||
- `part_of`: Component relationship
|
||||
- `contains`: Hierarchical relationship
|
||||
- `pairs_with`: Complementary relationship
|
||||
|
||||
## Knowledge Graph
|
||||
|
||||
Basic Memory automatically builds a knowledge graph from your document connections:
|
||||
|
||||
- Each document becomes a node in the graph
|
||||
- Relations create edges between nodes
|
||||
- Relation types add semantic meaning to connections
|
||||
- Forward references can link to documents that don't exist yet
|
||||
|
||||
This graph enables rich context building and navigation across your knowledge base.
|
||||
|
||||
## Permalinks and memory:// URLs
|
||||
|
||||
Every document in Basic Memory has a unique permalink that serves as its stable identifier:
|
||||
|
||||
### How Permalinks Work
|
||||
|
||||
- **Automatically assigned**: The system generates a permalink for each document
|
||||
- **Based on title**: By default, derived from the document title
|
||||
- **Always unique**: If conflicts exist, the system adds a suffix to ensure uniqueness
|
||||
- **Stable reference**: Remains the same even if the file moves in the directory structure
|
||||
- **Used in memory:// URLs**: Forms the basis of the memory:// addressing scheme
|
||||
|
||||
You can specify a custom permalink in the frontmatter:
|
||||
```yaml
|
||||
---
|
||||
title: Authentication Approaches
|
||||
permalink: auth-approaches-2024
|
||||
---
|
||||
```
|
||||
|
||||
If not specified, one will be generated automatically from the title, if the note has has a frontmatter section.
|
||||
|
||||
By default a notes' permalink value will not change if the file is moved. It's a **stable** identifier :). But if you'd rather permalinks are always updated when a file moves, you can set the config setting in the global config.
|
||||
|
||||
The config file for Basic Memory is in the home directory under `.basic-memory/config.json`.
|
||||
|
||||
To change the behavior, set the following value:
|
||||
```
|
||||
~/.basic-memory/config.json
|
||||
{
|
||||
"update_permalinks_on_move": true
|
||||
}
|
||||
```
|
||||
|
||||
### Using memory:// URLs
|
||||
|
||||
The memory:// URL scheme provides a reliable way to reference knowledge:
|
||||
|
||||
```
|
||||
memory://auth-approaches-2024 # Direct access by permalink
|
||||
memory://Authentication Approaches # Access by title (automatically resolves)
|
||||
memory://project/auth-approaches # Access by path
|
||||
```
|
||||
|
||||
Memory URLs support pattern matching for more powerful queries:
|
||||
|
||||
```
|
||||
memory://auth* # All documents with permalinks starting with "auth"
|
||||
memory://*/approaches # All documents with permalinks ending with "approaches"
|
||||
memory://project/*/requirements # All requirements documents in the project folder
|
||||
memory://docs/search/implements/* # Follow all implements relations from search docs
|
||||
```
|
||||
|
||||
This addressing scheme ensures content remains accessible even as your knowledge base evolves and files are reorganized.
|
||||
|
||||
## File Organization
|
||||
|
||||
Organize files in any structure that suits your needs:
|
||||
|
||||
```
|
||||
docs/
|
||||
architecture/
|
||||
design.md
|
||||
patterns.md
|
||||
features/
|
||||
search.md
|
||||
auth.md
|
||||
```
|
||||
|
||||
You can:
|
||||
- Group by topic in folders
|
||||
- Use a flat structure with descriptive filenames
|
||||
- Tag files for easier discovery
|
||||
- Add custom metadata in frontmatter
|
||||
|
||||
The system will build the semantic knowledge graph regardless of how you organize your files.
|
||||
|
||||
## Relations
|
||||
- implemented_by [[User Guide]] (How to work with this format)
|
||||
- relates_to [[Getting Started with Basic Memory]] (Setup instructions)
|
||||
- explained_in [[Introduction to Basic Memory]] (Overview of the system)
|
||||
@@ -1,259 +0,0 @@
|
||||
# OAuth Authentication Guide
|
||||
|
||||
Basic Memory MCP server supports OAuth 2.1 authentication for secure access control. This guide covers setup, testing, and production deployment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Enable OAuth
|
||||
|
||||
```bash
|
||||
# Set environment variable
|
||||
export FASTMCP_AUTH_ENABLED=true
|
||||
|
||||
# Or use .env file
|
||||
echo "FASTMCP_AUTH_ENABLED=true" >> .env
|
||||
```
|
||||
|
||||
### 2. Start the Server
|
||||
|
||||
```bash
|
||||
basic-memory mcp --transport streamable-http
|
||||
```
|
||||
|
||||
### 3. Test with MCP Inspector
|
||||
|
||||
Since the basic auth provider uses in-memory storage with per-instance secret keys, you'll need to use a consistent approach:
|
||||
|
||||
#### Option A: Use Environment Variable for Secret Key
|
||||
|
||||
```bash
|
||||
# Set a fixed secret key for testing
|
||||
export FASTMCP_AUTH_SECRET_KEY="your-test-secret-key"
|
||||
|
||||
# Start the server
|
||||
FASTMCP_AUTH_ENABLED=true basic-memory mcp --transport streamable-http
|
||||
|
||||
# In another terminal, register a client
|
||||
basic-memory auth register-client --client-id=test-client
|
||||
|
||||
# Get a token using the same secret key
|
||||
basic-memory auth test-auth
|
||||
```
|
||||
|
||||
#### Option B: Use the Built-in Test Endpoint
|
||||
|
||||
```bash
|
||||
# Start server with OAuth
|
||||
FASTMCP_AUTH_ENABLED=true basic-memory mcp --transport streamable-http
|
||||
|
||||
# Register a client and get token in one step
|
||||
curl -X POST http://localhost:8000/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"client_metadata": {"client_name": "Test Client"}}'
|
||||
|
||||
# Use the returned client_id and client_secret
|
||||
curl -X POST http://localhost:8000/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"
|
||||
```
|
||||
|
||||
### 4. Configure MCP Inspector
|
||||
|
||||
1. Open MCP Inspector
|
||||
2. Configure:
|
||||
- Server URL: `http://localhost:8000/mcp/` (note the trailing slash!)
|
||||
- Transport: `streamable-http`
|
||||
- Custom Headers:
|
||||
```
|
||||
Authorization: Bearer YOUR_ACCESS_TOKEN
|
||||
Accept: application/json, text/event-stream
|
||||
```
|
||||
|
||||
## OAuth Endpoints
|
||||
|
||||
The server provides these OAuth endpoints automatically:
|
||||
|
||||
- `GET /authorize` - Authorization endpoint
|
||||
- `POST /token` - Token exchange endpoint
|
||||
- `GET /.well-known/oauth-authorization-server` - OAuth metadata
|
||||
- `POST /register` - Client registration (if enabled)
|
||||
- `POST /revoke` - Token revocation (if enabled)
|
||||
|
||||
## OAuth Flow
|
||||
|
||||
### Standard Authorization Code Flow
|
||||
|
||||
1. **Get Authorization Code**:
|
||||
```bash
|
||||
curl "http://localhost:8000/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8000/callback&response_type=code&code_challenge=YOUR_CHALLENGE&code_challenge_method=S256"
|
||||
```
|
||||
|
||||
2. **Exchange Code for Token**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=authorization_code&code=AUTH_CODE&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&code_verifier=YOUR_VERIFIER"
|
||||
```
|
||||
|
||||
3. **Use Access Token**:
|
||||
```bash
|
||||
curl http://localhost:8000/mcp \
|
||||
-H "Authorization: Bearer ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Using Supabase Auth
|
||||
|
||||
For production, use Supabase for persistent auth storage:
|
||||
|
||||
```bash
|
||||
# Configure environment
|
||||
FASTMCP_AUTH_ENABLED=true
|
||||
FASTMCP_AUTH_PROVIDER=supabase
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_KEY=your-service-key
|
||||
|
||||
# Start server
|
||||
basic-memory mcp --transport streamable-http --host 0.0.0.0
|
||||
```
|
||||
|
||||
### Security Requirements
|
||||
|
||||
1. **HTTPS Required**: OAuth requires HTTPS in production (localhost exception for testing)
|
||||
2. **PKCE Support**: Claude.ai requires PKCE for authorization
|
||||
3. **Token Expiration**: Access tokens expire after 1 hour
|
||||
4. **Scopes**: Supported scopes are `read`, `write`, and `admin`
|
||||
|
||||
## Connecting from Claude.ai
|
||||
|
||||
1. **Deploy with HTTPS**:
|
||||
```bash
|
||||
# Use ngrok for testing
|
||||
ngrok http 8000
|
||||
|
||||
# Or deploy to cloud provider
|
||||
```
|
||||
|
||||
2. **Configure in Claude.ai**:
|
||||
- Go to Settings → Integrations
|
||||
- Click "Add More"
|
||||
- Enter: `https://your-server.com/mcp`
|
||||
- Click "Connect"
|
||||
- Authorize in the popup window
|
||||
|
||||
## Debugging
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **401 Unauthorized**:
|
||||
- Check token is valid and not expired
|
||||
- Verify secret key consistency
|
||||
- Ensure bearer token format: `Authorization: Bearer TOKEN`
|
||||
|
||||
2. **404 on Auth Endpoints**:
|
||||
- Endpoints are at root, not under `/auth`
|
||||
- Use `/authorize` not `/auth/authorize`
|
||||
|
||||
3. **Token Validation Fails**:
|
||||
- Basic provider uses in-memory storage
|
||||
- Tokens don't persist across server restarts
|
||||
- Use same secret key for testing
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check OAuth metadata
|
||||
curl http://localhost:8000/.well-known/oauth-authorization-server
|
||||
|
||||
# Enable debug logging
|
||||
export FASTMCP_LOG_LEVEL=DEBUG
|
||||
|
||||
# Test token directly
|
||||
curl http://localhost:8000/mcp \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-v
|
||||
```
|
||||
|
||||
## Provider Options
|
||||
|
||||
- **basic**: In-memory storage (development only)
|
||||
- **supabase**: Recommended for production
|
||||
- **github**: GitHub OAuth integration
|
||||
- **google**: Google OAuth integration
|
||||
|
||||
## Example Test Script
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import asyncio
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
async def test_oauth_flow():
|
||||
"""Test the full OAuth flow"""
|
||||
client_id = "test-client"
|
||||
client_secret = "test-secret"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# 1. Get authorization code
|
||||
auth_response = await client.get(
|
||||
"http://localhost:8000/authorize",
|
||||
params={
|
||||
"client_id": client_id,
|
||||
"redirect_uri": "http://localhost:8000/callback",
|
||||
"response_type": "code",
|
||||
"code_challenge": "test-challenge",
|
||||
"code_challenge_method": "S256",
|
||||
"state": "test-state"
|
||||
}
|
||||
)
|
||||
|
||||
# Extract code from redirect URL
|
||||
redirect_url = auth_response.headers.get("Location")
|
||||
parsed = urlparse(redirect_url)
|
||||
code = parse_qs(parsed.query)["code"][0]
|
||||
|
||||
# 2. Exchange for token
|
||||
token_response = await client.post(
|
||||
"http://localhost:8000/token",
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code_verifier": "test-verifier",
|
||||
"redirect_uri": "http://localhost:8000/callback"
|
||||
}
|
||||
)
|
||||
|
||||
tokens = token_response.json()
|
||||
print(f"Access token: {tokens['access_token']}")
|
||||
|
||||
# 3. Test MCP endpoint
|
||||
mcp_response = await client.post(
|
||||
"http://localhost:8000/mcp",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"method": "initialize", "params": {}}
|
||||
)
|
||||
|
||||
print(f"MCP Response: {mcp_response.status_code}")
|
||||
|
||||
asyncio.run(test_oauth_flow())
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `FASTMCP_AUTH_ENABLED` | Enable OAuth authentication | `false` |
|
||||
| `FASTMCP_AUTH_PROVIDER` | OAuth provider type | `basic` |
|
||||
| `FASTMCP_AUTH_SECRET_KEY` | JWT signing key (basic provider) | Random |
|
||||
| `FASTMCP_AUTH_ISSUER_URL` | OAuth issuer URL | `http://localhost:8000` |
|
||||
| `FASTMCP_AUTH_REQUIRED_SCOPES` | Required scopes (comma-separated) | `read,write` |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Supabase OAuth Setup](./Supabase%20OAuth%20Setup.md) - Production auth setup
|
||||
- [External OAuth Providers](./External%20OAuth%20Providers.md) - GitHub, Google integration
|
||||
- [MCP OAuth Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) - Official spec
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
title: Obsidian Integration
|
||||
type: note
|
||||
permalink: docs/obsidian-integration
|
||||
---
|
||||
|
||||
# Obsidian Integration
|
||||
|
||||
Basic Memory integrates seamlessly with [Obsidian](https://obsidian.md), providing powerful visualization and navigation capabilities for your knowledge graph.
|
||||
|
||||
## Setup
|
||||
|
||||
### Creating an Obsidian Vault
|
||||
|
||||
1. Download and install [Obsidian](https://obsidian.md)
|
||||
2. Create a new vault
|
||||
3. Point it to your Basic Memory directory (~/basic-memory by default)
|
||||
4. Enable core plugins like Graph View, Backlinks, and Tags
|
||||
|
||||
## Visualization Features
|
||||
|
||||
### Graph View
|
||||
|
||||
Obsidian's Graph View provides a visual representation of your knowledge network:
|
||||
|
||||
- Each document appears as a node
|
||||
- Relations appear as connections between nodes
|
||||
- Colors can be customized to distinguish types
|
||||
- Filters let you focus on specific aspects
|
||||
- Local graphs show connections for individual documents
|
||||
|
||||
### Backlinks
|
||||
|
||||
Obsidian automatically tracks references between documents:
|
||||
|
||||
- View all documents that reference the current one
|
||||
- See the exact context of each reference
|
||||
- Navigate easily through connections
|
||||
- Track how concepts relate to each other
|
||||
|
||||
### Tag Explorer
|
||||
|
||||
Use tags to organize and filter content:
|
||||
|
||||
- View all tags in your knowledge base
|
||||
- See how many documents use each tag
|
||||
- Filter documents by tag combinations
|
||||
- Create hierarchical tag structures
|
||||
|
||||
## Knowledge Elements
|
||||
|
||||
Basic Memory's knowledge format works natively with Obsidian:
|
||||
|
||||
### Wiki Links
|
||||
|
||||
```markdown
|
||||
## Relations
|
||||
- implements [[Search Design]]
|
||||
- depends_on [[Database Schema]]
|
||||
```
|
||||
|
||||
These display as clickable links in Obsidian and appear in the graph view.
|
||||
|
||||
### Observations with Tags
|
||||
|
||||
```markdown
|
||||
## Observations
|
||||
- [tech] Using SQLite #database
|
||||
- [design] Local-first #architecture
|
||||
```
|
||||
|
||||
Tags become searchable and filterable in Obsidian's tag pane.
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Document Title
|
||||
type: note
|
||||
tags: [search, design]
|
||||
---
|
||||
```
|
||||
|
||||
Frontmatter provides metadata for Obsidian to use in search and filtering.
|
||||
|
||||
## Canvas Integration
|
||||
|
||||
Basic Memory can create [Obsidian Canvas](https://obsidian.md/canvas) files:
|
||||
|
||||
1. Ask Claude to create a visualization:
|
||||
```
|
||||
You: "Create a canvas showing the structure of our project components."
|
||||
```
|
||||
|
||||
2. Claude generates a .canvas file in your knowledge base
|
||||
|
||||
3. Open the file in Obsidian to view and edit the visual representation
|
||||
|
||||
4. Canvas files maintain references to your documents
|
||||
|
||||
## Recommended Plugins
|
||||
|
||||
These Obsidian plugins work especially well with Basic Memory:
|
||||
|
||||
- **Dataview**: Query your knowledge base programmatically
|
||||
- **Kanban**: Organize tasks from knowledge files
|
||||
- **Calendar**: View and navigate temporal knowledge
|
||||
- **Templates**: Create consistent knowledge structures
|
||||
|
||||
## Workflow Suggestions
|
||||
|
||||
### Daily Notes
|
||||
|
||||
```markdown
|
||||
# 2024-01-21
|
||||
|
||||
## Progress
|
||||
- Updated [[Search Design]]
|
||||
- Fixed [[Bug Report 123]]
|
||||
|
||||
## Notes
|
||||
- [idea] Better indexing #enhancement
|
||||
- [todo] Update docs #documentation
|
||||
|
||||
## Links
|
||||
- relates_to [[Current Sprint]]
|
||||
- updates [[Project Status]]
|
||||
```
|
||||
|
||||
### Project Tracking
|
||||
|
||||
```markdown
|
||||
# Current Sprint
|
||||
|
||||
## Tasks
|
||||
- [ ] Update [[Search]]
|
||||
- [ ] Fix [[Auth Bug]]
|
||||
|
||||
## Tags
|
||||
#sprint #planning #current
|
||||
```
|
||||
|
||||
## Relations
|
||||
- enhances [[Introduction to Basic Memory]] (Overview of system)
|
||||
- relates_to [[Canvas]] (Visual knowledge mapping)
|
||||
- complements [[User Guide]] (Using Basic Memory)
|
||||
@@ -1,311 +0,0 @@
|
||||
# Supabase OAuth Setup for Basic Memory
|
||||
|
||||
This guide explains how to set up Supabase as the OAuth provider for Basic Memory MCP server in production.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Supabase project (create one at [supabase.com](https://supabase.com))
|
||||
2. Basic Memory MCP server deployed
|
||||
3. Environment variables configuration
|
||||
|
||||
## Overview
|
||||
|
||||
The Supabase OAuth provider offers:
|
||||
- Production-ready authentication with persistent storage
|
||||
- User management through Supabase Auth
|
||||
- JWT token validation
|
||||
- Integration with Supabase's security features
|
||||
- Support for social logins (GitHub, Google, etc.)
|
||||
|
||||
## Setup Steps
|
||||
|
||||
### 1. Get Supabase Credentials
|
||||
|
||||
From your Supabase project dashboard:
|
||||
|
||||
1. Go to Settings > API
|
||||
2. Copy these values:
|
||||
- `Project URL` → `SUPABASE_URL`
|
||||
- `anon public` key → `SUPABASE_ANON_KEY`
|
||||
- `service_role` key → `SUPABASE_SERVICE_KEY` (keep this secret!)
|
||||
- JWT secret → `SUPABASE_JWT_SECRET` (under Settings > API > JWT Settings)
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```bash
|
||||
# Enable OAuth
|
||||
FASTMCP_AUTH_ENABLED=true
|
||||
FASTMCP_AUTH_PROVIDER=supabase
|
||||
|
||||
# Your MCP server URL
|
||||
FASTMCP_AUTH_ISSUER_URL=https://your-mcp-server.com
|
||||
|
||||
# Supabase configuration
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_KEY=your-service-key
|
||||
SUPABASE_JWT_SECRET=your-jwt-secret
|
||||
|
||||
# Allowed OAuth clients (comma-separated)
|
||||
SUPABASE_ALLOWED_CLIENTS=web-app,mobile-app,cli-tool
|
||||
|
||||
# Required scopes
|
||||
FASTMCP_AUTH_REQUIRED_SCOPES=read,write
|
||||
```
|
||||
|
||||
### 3. Create OAuth Clients Table (Optional)
|
||||
|
||||
For production, create a table to store OAuth clients in Supabase:
|
||||
|
||||
```sql
|
||||
CREATE TABLE oauth_clients (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
client_id TEXT UNIQUE NOT NULL,
|
||||
client_secret TEXT NOT NULL,
|
||||
name TEXT,
|
||||
redirect_uris TEXT[],
|
||||
allowed_scopes TEXT[],
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create an index for faster lookups
|
||||
CREATE INDEX idx_oauth_clients_client_id ON oauth_clients(client_id);
|
||||
|
||||
-- RLS policies
|
||||
ALTER TABLE oauth_clients ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Only service role can manage clients
|
||||
CREATE POLICY "Service role can manage clients" ON oauth_clients
|
||||
FOR ALL USING (auth.jwt()->>'role' = 'service_role');
|
||||
```
|
||||
|
||||
### 4. Set Up Auth Flow
|
||||
|
||||
The Supabase OAuth provider handles the following flow:
|
||||
|
||||
1. **Client Authorization Request**
|
||||
```
|
||||
GET /authorize?client_id=web-app&redirect_uri=https://app.com/callback
|
||||
```
|
||||
|
||||
2. **Redirect to Supabase Auth**
|
||||
- User authenticates with Supabase (email/password, magic link, or social login)
|
||||
- Supabase redirects back to your MCP server
|
||||
|
||||
3. **Token Exchange**
|
||||
```
|
||||
POST /token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&code=xxx&client_id=web-app
|
||||
```
|
||||
|
||||
4. **Access Protected Resources**
|
||||
```
|
||||
GET /mcp
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
### 5. Enable Social Logins (Optional)
|
||||
|
||||
In Supabase dashboard:
|
||||
|
||||
1. Go to Authentication > Providers
|
||||
2. Enable desired providers (GitHub, Google, etc.)
|
||||
3. Configure OAuth apps for each provider
|
||||
4. Users can now log in via social providers
|
||||
|
||||
### 6. User Management
|
||||
|
||||
Supabase provides:
|
||||
- User registration and login
|
||||
- Password reset flows
|
||||
- Email verification
|
||||
- User metadata storage
|
||||
- Admin APIs for user management
|
||||
|
||||
Access user data in your MCP tools:
|
||||
|
||||
```python
|
||||
# In your MCP tool
|
||||
async def get_user_info(ctx: Context):
|
||||
# The token is already validated by the OAuth middleware
|
||||
user_id = ctx.auth.user_id
|
||||
email = ctx.auth.email
|
||||
|
||||
# Use Supabase client to get more user data if needed
|
||||
user = await supabase.auth.admin.get_user_by_id(user_id)
|
||||
return user
|
||||
```
|
||||
|
||||
### 7. Production Deployment
|
||||
|
||||
1. **Environment Security**
|
||||
- Never expose `SUPABASE_SERVICE_KEY`
|
||||
- Use environment variables, not hardcoded values
|
||||
- Rotate keys periodically
|
||||
|
||||
2. **HTTPS Required**
|
||||
- Always use HTTPS in production
|
||||
- Configure proper SSL certificates
|
||||
|
||||
3. **Rate Limiting**
|
||||
- Implement rate limiting for auth endpoints
|
||||
- Use Supabase's built-in rate limiting
|
||||
|
||||
4. **Monitoring**
|
||||
- Monitor auth logs in Supabase dashboard
|
||||
- Set up alerts for suspicious activity
|
||||
|
||||
## Testing
|
||||
|
||||
### Local Development
|
||||
|
||||
For local testing with Supabase:
|
||||
|
||||
```bash
|
||||
# Start MCP server with Supabase auth
|
||||
FASTMCP_AUTH_ENABLED=true \
|
||||
FASTMCP_AUTH_PROVIDER=supabase \
|
||||
SUPABASE_URL=http://localhost:54321 \
|
||||
SUPABASE_ANON_KEY=your-local-anon-key \
|
||||
bm mcp --transport streamable-http
|
||||
```
|
||||
|
||||
### Test Authentication Flow
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import asyncio
|
||||
|
||||
async def test_supabase_auth():
|
||||
# 1. Register/login with Supabase directly
|
||||
supabase_url = "https://your-project.supabase.co"
|
||||
|
||||
# 2. Get MCP authorization URL
|
||||
response = await httpx.get(
|
||||
"http://localhost:8000/authorize",
|
||||
params={
|
||||
"client_id": "web-app",
|
||||
"redirect_uri": "http://localhost:3000/callback",
|
||||
"response_type": "code",
|
||||
}
|
||||
)
|
||||
|
||||
# 3. User logs in via Supabase
|
||||
# 4. Exchange code for MCP tokens
|
||||
# 5. Access protected resources
|
||||
|
||||
asyncio.run(test_supabase_auth())
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom User Metadata
|
||||
|
||||
Store additional user data in Supabase:
|
||||
|
||||
```sql
|
||||
-- Add custom fields to auth.users
|
||||
ALTER TABLE auth.users
|
||||
ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}';
|
||||
|
||||
-- Or create a separate profiles table
|
||||
CREATE TABLE profiles (
|
||||
id UUID REFERENCES auth.users PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### Row Level Security (RLS)
|
||||
|
||||
Protect user data with RLS:
|
||||
|
||||
```sql
|
||||
-- Users can only access their own data
|
||||
CREATE POLICY "Users can view own profile" ON profiles
|
||||
FOR SELECT USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can update own profile" ON profiles
|
||||
FOR UPDATE USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
### Custom Claims
|
||||
|
||||
Add custom claims to JWT tokens:
|
||||
|
||||
```sql
|
||||
-- Function to add custom claims
|
||||
CREATE OR REPLACE FUNCTION custom_jwt_claims()
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN json_build_object(
|
||||
'user_role', current_setting('request.jwt.claims')::json->>'user_role',
|
||||
'permissions', current_setting('request.jwt.claims')::json->>'permissions'
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Invalid JWT Secret**
|
||||
- Ensure `SUPABASE_JWT_SECRET` matches your Supabase project
|
||||
- Check Settings > API > JWT Settings in Supabase dashboard
|
||||
|
||||
2. **CORS Errors**
|
||||
- Configure CORS in your MCP server
|
||||
- Add allowed origins in Supabase dashboard
|
||||
|
||||
3. **Token Validation Fails**
|
||||
- Verify tokens are being passed correctly
|
||||
- Check token expiration times
|
||||
- Ensure scopes match requirements
|
||||
|
||||
4. **User Not Found**
|
||||
- Confirm user exists in Supabase Auth
|
||||
- Check if email is verified (if required)
|
||||
- Verify client permissions
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
export FASTMCP_LOG_LEVEL=DEBUG
|
||||
export SUPABASE_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Secure Keys**: Never commit secrets to version control
|
||||
2. **Least Privilege**: Use minimal required scopes
|
||||
3. **Token Rotation**: Implement refresh token rotation
|
||||
4. **Audit Logs**: Monitor authentication events
|
||||
5. **Rate Limiting**: Protect against brute force attacks
|
||||
6. **HTTPS Only**: Always use encrypted connections
|
||||
|
||||
## Migration from Basic Auth
|
||||
|
||||
To migrate from the basic auth provider:
|
||||
|
||||
1. Export existing user data
|
||||
2. Import users into Supabase Auth
|
||||
3. Update client applications to use new auth flow
|
||||
4. Gradually transition users to Supabase login
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Set up email templates in Supabase
|
||||
- Configure password policies
|
||||
- Implement MFA (multi-factor authentication)
|
||||
- Add social login providers
|
||||
- Create admin dashboard for user management
|
||||
@@ -1,243 +0,0 @@
|
||||
---
|
||||
title: Technical Information
|
||||
type: note
|
||||
permalink: docs/technical-information
|
||||
---
|
||||
|
||||
# Technical Information
|
||||
|
||||
This document provides technical details about Basic Memory's implementation, licensing, and integration with the Model Context Protocol (MCP).
|
||||
|
||||
## Architecture
|
||||
|
||||
Basic Memory consists of:
|
||||
|
||||
1. **Core Knowledge Engine**: Parses and indexes Markdown files
|
||||
2. **SQLite Database**: Provides fast querying and search
|
||||
3. **MCP Server**: Implements the Model Context Protocol
|
||||
4. **CLI Tools**: Command-line utilities for management
|
||||
5. **Sync Service**: Monitors file changes and updates the database
|
||||
|
||||
The system follows a file-first architecture where all knowledge is represented in standard Markdown files and the database serves as a secondary index.
|
||||
|
||||
## Model Context Protocol (MCP)
|
||||
|
||||
Basic Memory implements the [Model Context Protocol](https://github.com/modelcontextprotocol/spec), an open standard for enabling AI models to access external tools:
|
||||
|
||||
- **Standardized Interface**: Common protocol for tool integration
|
||||
- **Tool Registration**: Basic Memory registers as a tool provider
|
||||
- **Asynchronous Communication**: Enables efficient interaction with AI models
|
||||
- **Standardized Schema**: Structured data exchange format
|
||||
|
||||
Integration with Claude Desktop uses the MCP to grant Claude access to your knowledge base through a set of specialized tools that search, read, and write knowledge.
|
||||
|
||||
## Licensing
|
||||
|
||||
Basic Memory is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.en.html):
|
||||
|
||||
- **Free Software**: You can use, study, share, and modify the software
|
||||
- **Copyleft**: Derivative works must be distributed under the same license
|
||||
- **Network Use**: Network users must be able to receive the source code
|
||||
- **Commercial Use**: Allowed, subject to license requirements
|
||||
|
||||
The AGPL license ensures Basic Memory remains open source while protecting against proprietary forks.
|
||||
|
||||
## Source Code
|
||||
|
||||
Basic Memory is developed as an open-source project:
|
||||
|
||||
- **GitHub Repository**: [https://github.com/basicmachines-co/basic-memory](https://github.com/basicmachines-co/basic-memory)
|
||||
- **Issue Tracker**: Report bugs and request features on GitHub
|
||||
- **Contributions**: Pull requests are welcome following the contributing guidelines
|
||||
- **Documentation**: Source for this documentation is also available in the repository
|
||||
|
||||
## Data Storage and Privacy
|
||||
|
||||
Basic Memory is designed with privacy as a core principle:
|
||||
|
||||
- **Local-First**: All data remains on your local machine
|
||||
- **No Cloud Dependency**: No remote servers or accounts required
|
||||
- **Telemetry**: Optional and disabled by default
|
||||
- **Standard Formats**: All data is stored in standard file formats you control
|
||||
|
||||
## Implementation Details
|
||||
|
||||
Knowledge in Basic Memory is organized as a semantic graph:
|
||||
|
||||
1. **Entities** - Distinct concepts represented by Markdown documents
|
||||
2. **Observations** - Categorized facts and information about entities
|
||||
3. **Relations** - Connections between entities that form the knowledge graph
|
||||
|
||||
This structure emerges from simple text patterns in standard Markdown:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
type: note
|
||||
permalink: coffee/coffee-brewing-methods
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#brewing'
|
||||
- '#methods'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
An exploration of different coffee brewing techniques, their characteristics, and how they affect flavor extraction.
|
||||
|
||||
## Overview
|
||||
|
||||
Coffee brewing is both an art and a science. Different brewing methods extract different compounds from coffee beans,
|
||||
resulting in unique flavor profiles, body, and mouthfeel. The key variables in any brewing method are:
|
||||
|
||||
- Grind size
|
||||
- Water temperature
|
||||
- Brew time
|
||||
- Coffee-to-water ratio
|
||||
- Agitation/turbulence
|
||||
|
||||
## Observations
|
||||
|
||||
- [principle] Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds
|
||||
#extraction
|
||||
- [method] Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity
|
||||
|
||||
## Relations
|
||||
|
||||
- requires [[Proper Grinding Technique]]
|
||||
- affects [[Flavor Extraction]]
|
||||
```
|
||||
|
||||
Becomes
|
||||
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"permalink": "coffee/coffee-brewing-methods",
|
||||
"title": "Coffee Brewing Methods",
|
||||
"file_path": "Coffee Notes/Coffee Brewing Methods.md",
|
||||
"entity_type": "note",
|
||||
"entity_metadata": {
|
||||
"title": "Coffee Brewing Methods",
|
||||
"type": "note",
|
||||
"permalink": "coffee/coffee-brewing-methods",
|
||||
"tags": "['#coffee', '#brewing', '#methods', '#demo']"
|
||||
},
|
||||
"checksum": "bfa32a0f23fa124b53f0694c344d2788b0ce50bd090b55b6d738401d2a349e4c",
|
||||
"content_type": "text/markdown",
|
||||
"observations": [
|
||||
{
|
||||
"category": "principle",
|
||||
"content": "Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds #extraction",
|
||||
"tags": [
|
||||
"extraction"
|
||||
],
|
||||
"permalink": "coffee/coffee-brewing-methods/observations/principle/coffee-extraction-follows-a-predictable-pattern-acids-extract-first-then-sugars-then-bitter-compounds-extraction"
|
||||
},
|
||||
{
|
||||
"category": "method",
|
||||
"content": "Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity",
|
||||
"tags": [
|
||||
"clarity"
|
||||
],
|
||||
"permalink": "coffee/coffee-brewing-methods/observations/method/pour-over-methods-generally-produce-cleaner-brighter-cups-with-more-distinct-flavor-notes-clarity"
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"from_id": "coffee/coffee-bean-origins",
|
||||
"to_id": "coffee/coffee-brewing-methods",
|
||||
"relation_type": "pairs_with",
|
||||
"permalink": "coffee/coffee-bean-origins/pairs-with/coffee/coffee-brewing-methods",
|
||||
"to_name": "Coffee Brewing Methods"
|
||||
},
|
||||
{
|
||||
"from_id": "coffee/flavor-extraction",
|
||||
"to_id": "coffee/coffee-brewing-methods",
|
||||
"relation_type": "affected_by",
|
||||
"permalink": "coffee/flavor-extraction/affected-by/coffee/coffee-brewing-methods",
|
||||
"to_name": "Coffee Brewing Methods"
|
||||
}
|
||||
],
|
||||
"created_at": "2025-03-06T14:01:23.445071",
|
||||
"updated_at": "2025-03-06T13:34:48.563606"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Basic Memory understands how to build context via its semantic graph.
|
||||
|
||||
### Entity Model
|
||||
|
||||
Basic Memory's core data model consists of:
|
||||
|
||||
- **Entities**: Documents in your knowledge base
|
||||
- **Observations**: Facts or statements about entities
|
||||
- **Relations**: Connections between entities
|
||||
- **Tags**: Additional categorization for entities and observations
|
||||
|
||||
The system parses Markdown files to extract this structured information while preserving the human-readable format.
|
||||
|
||||
### Files as Source of Truth
|
||||
|
||||
Plain Markdown files store all knowledge, making it accessible with any text editor and easy to version with git.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User((User)) <--> |Conversation| Claude["Claude or other LLM"]
|
||||
Claude <-->|API Calls| BMCP["Basic Memory MCP Server"]
|
||||
|
||||
subgraph "Local Storage"
|
||||
KnowledgeFiles["Markdown Files - Source of Truth"]
|
||||
KnowledgeIndex[(Knowledge Graph SQLite Index)]
|
||||
end
|
||||
|
||||
BMCP <-->|"write_note() read_note()"| KnowledgeFiles
|
||||
BMCP <-->|"search_notes() build_context()"| KnowledgeIndex
|
||||
KnowledgeFiles <-.->|Sync Process| KnowledgeIndex
|
||||
KnowledgeFiles <-->|Direct Editing| Editors((Text Editors & Git))
|
||||
|
||||
User -.->|"Complete control, Privacy preserved"| KnowledgeFiles
|
||||
|
||||
|
||||
class Claude primary
|
||||
class BMCP secondary
|
||||
class KnowledgeFiles tertiary
|
||||
class KnowledgeIndex quaternary
|
||||
class User,Editors user`;
|
||||
```
|
||||
|
||||
### Sqlite Database
|
||||
|
||||
A local SQLite database maintains the knowledge graph topology for fast queries and semantic traversal without cloud dependencies. It contains:
|
||||
- db tables for the knowledge graph schema
|
||||
- a search index table enabling full text search across the knowledge base
|
||||
|
||||
|
||||
### Sync Process
|
||||
|
||||
The sync process:
|
||||
|
||||
1. Detects changes to files in the knowledge directory
|
||||
2. Parses modified files to extract structured data
|
||||
3. Updates the SQLite database with changes
|
||||
4. Resolves forward references when new entities are created
|
||||
5. Updates the search index for fast querying
|
||||
|
||||
### Search Engine
|
||||
|
||||
The search functionality:
|
||||
|
||||
1. Uses a combination of full-text search and semantic matching
|
||||
2. Indexes observations, relations, and content
|
||||
3. Supports wildcards and pattern matching in memory:// URLs
|
||||
4. Traverses the knowledge graph to follow relationships
|
||||
5. Ranks results by relevance to the query
|
||||
|
||||
## Relations
|
||||
- relates_to [[Welcome to Basic memory]] (Overview)
|
||||
- relates_to [[CLI Reference]] (Command line tools)
|
||||
- implements [[Knowledge Format]] (File structure and format)
|
||||
@@ -1,657 +0,0 @@
|
||||
---
|
||||
title: User Guide
|
||||
type: note
|
||||
permalink: docs/user-guide
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
This guide explains how to effectively use Basic Memory in your daily workflow, from creating knowledge through
|
||||
conversations to building a rich semantic network.
|
||||
|
||||
## Basic Memory Workflow
|
||||
|
||||
Using Basic Memory follows a natural cycle:
|
||||
|
||||
1. **Have conversations** with AI assistants like Claude
|
||||
2. **Capture knowledge** in Markdown files
|
||||
3. **Build connections** between pieces of knowledge
|
||||
4. **Reference your knowledge** in future conversations
|
||||
5. **Edit files directly** when needed
|
||||
6. **Sync changes** automatically
|
||||
|
||||
## Creating Knowledge
|
||||
|
||||
### Through Conversations
|
||||
|
||||
To create knowledge during conversations with Claude:
|
||||
|
||||
```
|
||||
You: We've covered several authentication approaches. Could you create a note summarizing what we've discussed?
|
||||
|
||||
Claude: I'll create a note summarizing our authentication discussion.
|
||||
```
|
||||
|
||||
This creates a Markdown file in your `~/basic-memory` directory with semantic markup.
|
||||
|
||||
### Direct File Creation
|
||||
|
||||
You can create files directly:
|
||||
|
||||
1. Create a new Markdown file in your `~/basic-memory` directory
|
||||
2. Add frontmatter with title, type, and optional tags
|
||||
3. Structure content with observations and relations
|
||||
4. Save the file
|
||||
5. Run `basic-memory sync` if not in watch mode
|
||||
|
||||
## Using Special Prompts
|
||||
|
||||
Basic Memory includes several special prompts that help you leverage your knowledge base more effectively. In apps like
|
||||
Claude Desktop, these prompts trigger specific tools to search and analyze your knowledge base.
|
||||
|
||||
### Continue Conversation
|
||||
|
||||
When you want to pick up where you left off on a topic:
|
||||
|
||||
```
|
||||
You: Let's continue our conversation about authentication systems.
|
||||
```
|
||||
|
||||
Behind the scenes:
|
||||
|
||||
- Claude searches your knowledge base for content about "authentication systems"
|
||||
- It retrieves relevant documents and their relations
|
||||
- It analyzes the context to understand where you left off
|
||||
- It builds a comprehensive picture of what you've previously discussed
|
||||
- It can then resume the conversation with all that context
|
||||
|
||||
This is particularly useful when:
|
||||
|
||||
- Starting a new session days or weeks after your last discussion
|
||||
- Switching between multiple ongoing projects
|
||||
- Building on previous work without repeating yourself
|
||||
|
||||
### Recent Activity
|
||||
|
||||
To get an overview of what you've been working on:
|
||||
|
||||
```
|
||||
You: What have we been discussing recently?
|
||||
```
|
||||
|
||||
Behind the scenes:
|
||||
|
||||
- Claude retrieves documents modified recently
|
||||
- It analyzes patterns and themes
|
||||
- It summarizes the key topics and changes
|
||||
- It offers to continue working on any of those topics
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Coming back after a break
|
||||
- Getting a quick reminder of ongoing projects
|
||||
- Deciding what to work on next
|
||||
|
||||
### Search
|
||||
|
||||
To find specific information in your knowledge base:
|
||||
|
||||
```
|
||||
You: Find information about JWT authentication in my notes.
|
||||
```
|
||||
|
||||
Behind the scenes:
|
||||
|
||||
- Claude performs a semantic search for "JWT authentication"
|
||||
- It retrieves and ranks the most relevant documents
|
||||
- It summarizes the key findings
|
||||
- It offers to explore specific areas in more detail
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Finding specific information quickly
|
||||
- Exploring what you know about a topic
|
||||
- Starting work on an existing topic
|
||||
|
||||
### Example
|
||||
|
||||
Choose "Continue Conversation"
|
||||
![[prompt 1.png|500]]
|
||||
|
||||
Enter a topic
|
||||
![[prompt2.png|500]]
|
||||
|
||||
Give instructions
|
||||
![[prompt3.png|500]]
|
||||
|
||||
Claude Desktop lets you send a prompt to provide context. You can use this at the beginning of a chat to preload context
|
||||
without needing to copy paste all the time. By using one of the supplied prompts, Basic Memory will search the knowledge
|
||||
base and give the AI instructions for how to build context.
|
||||
|
||||
Choose "Continue Conversation":
|
||||
|
||||
![[prompt 1.png|500]]
|
||||
|
||||
Enter a topic:
|
||||
|
||||
![[prompt2.png|500]]
|
||||
|
||||
Give optional additional instructions:
|
||||
|
||||
![[prompt3.png|500]]
|
||||
|
||||
Claude can build context from the supplied topic. This works independently of Claude Project information. All the
|
||||
context comes from your local knowledge base.
|
||||
|
||||
![[prompt4.png|500]]
|
||||
|
||||
## Searching Your Knowledge Base
|
||||
|
||||
Basic Memory provides multiple ways to search and explore your knowledge base:
|
||||
|
||||
### Natural Language Search
|
||||
|
||||
The simplest way to search is to ask Claude directly:
|
||||
|
||||
```
|
||||
You: What do I know about authentication methods?
|
||||
```
|
||||
|
||||
Claude will search your knowledge base semantically and return relevant information.
|
||||
|
||||
### Search Prompt
|
||||
|
||||
Use the dedicated search prompt for more focused searches:
|
||||
|
||||
```
|
||||
You: Search for "JWT authentication"
|
||||
```
|
||||
|
||||
This triggers a specialized search that returns precise results with document titles, relevant excerpts, and offers to
|
||||
explore specific documents.
|
||||
|
||||
### Boolean Search
|
||||
|
||||
For more precise searches, use boolean operators to refine your queries:
|
||||
|
||||
```
|
||||
You: Search for "authentication AND OAuth NOT basic"
|
||||
```
|
||||
|
||||
Basic Memory supports standard boolean operators:
|
||||
|
||||
- **AND**: Find documents containing both terms
|
||||
```
|
||||
You: Search for "python AND flask"
|
||||
```
|
||||
This finds documents containing both "python" and "flask"
|
||||
|
||||
- **OR**: Find documents containing either term
|
||||
```
|
||||
You: Search for "python OR javascript"
|
||||
```
|
||||
This finds documents containing either "python" or "javascript"
|
||||
|
||||
- **NOT**: Exclude documents containing specific terms
|
||||
```
|
||||
You: Search for "python NOT django"
|
||||
```
|
||||
This finds documents containing "python" but excludes those containing "django"
|
||||
|
||||
- **Grouping with parentheses**: Control operator precedence
|
||||
```
|
||||
You: Search for "(python OR javascript) AND web"
|
||||
```
|
||||
This finds documents about web development that mention either Python or JavaScript
|
||||
|
||||
Boolean search is particularly useful for:
|
||||
|
||||
- Narrowing down results in large knowledge bases
|
||||
- Finding specific combinations of concepts
|
||||
- Excluding irrelevant content from search results
|
||||
- Creating complex queries for precise information retrieval
|
||||
|
||||
### Memory URL Pattern Matching
|
||||
|
||||
For advanced searches, use memory:// URL patterns with wildcards:
|
||||
|
||||
```
|
||||
You: Look at memory://auth* and summarize all authentication approaches.
|
||||
```
|
||||
|
||||
Pattern matching supports:
|
||||
|
||||
- **Wildcards**: `memory://auth*` matches all permalinks starting with "auth"
|
||||
- **Path patterns**: `memory://project/*/auth` matches auth documents in any project subfolder
|
||||
- **Relation traversal**: `memory://auth-system/implements/*` finds all documents that implement the auth system
|
||||
|
||||
### Combining Search with Context Building
|
||||
|
||||
The most powerful searches build comprehensive context by following relationships:
|
||||
|
||||
```
|
||||
You: Search for JWT authentication and then follow all implementation relations.
|
||||
```
|
||||
|
||||
This builds a complete picture by:
|
||||
|
||||
1. Finding documents about JWT authentication
|
||||
2. Following implementation relationships from those documents
|
||||
3. Building a complete picture of how JWT is implemented across your system
|
||||
|
||||
### Search Best Practices
|
||||
|
||||
For effective searching:
|
||||
|
||||
1. **Be specific** with search terms and phrases
|
||||
2. **Use boolean operators** to refine searches and find precise information
|
||||
3. **Use technical terms** when searching for technical content
|
||||
4. **Follow up** on search results by asking for more details about specific documents
|
||||
5. **Combine approaches** by starting with search and then using memory:// URLs for precision
|
||||
6. **Use relation traversal** to explore connected concepts after finding initial documents
|
||||
|
||||
## Referencing Knowledge
|
||||
|
||||
### Using memory:// URLs
|
||||
|
||||
Reference specific knowledge directly:
|
||||
|
||||
```
|
||||
You: Please look at memory://authentication-approaches and suggest which approach would be best for our mobile app.
|
||||
```
|
||||
|
||||
### Natural Language References
|
||||
|
||||
Reference knowledge conversationally:
|
||||
|
||||
```
|
||||
You: What did we decide about authentication for the project?
|
||||
```
|
||||
|
||||
### Advanced References
|
||||
|
||||
Follow connections across your knowledge graph:
|
||||
|
||||
```
|
||||
You: Look at memory://project-architecture and check related documents to give me a complete picture.
|
||||
```
|
||||
|
||||
## Working with Files
|
||||
|
||||
### File Location and Organization
|
||||
|
||||
By default, Basic Memory stores files in `~/basic-memory`:
|
||||
|
||||
- Browse this directory in your file explorer
|
||||
- Organize files into subfolders
|
||||
- Use git for version control
|
||||
|
||||
### File Format
|
||||
|
||||
Each knowledge file follows this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Authentication Approaches
|
||||
type: note
|
||||
tags: [security, architecture]
|
||||
permalink: authentication-approaches
|
||||
---
|
||||
|
||||
# Authentication Approaches
|
||||
|
||||
A comparison of authentication methods.
|
||||
|
||||
## Observations
|
||||
|
||||
- [approach] JWT provides stateless authentication #security
|
||||
- [limitation] Session tokens require server-side storage #infrastructure
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- affects [[User Login Flow]]
|
||||
```
|
||||
|
||||
### Editing Files
|
||||
|
||||
Modify files in any text editor:
|
||||
|
||||
1. Open the file in your preferred editor
|
||||
2. Make changes to content, observations, or relations
|
||||
3. Save the file
|
||||
4. Basic Memory detects changes automatically when running in watch mode
|
||||
|
||||
## Building a Knowledge Graph
|
||||
|
||||
The value of Basic Memory comes from connections between pieces of knowledge.
|
||||
|
||||
### Creating Relations
|
||||
|
||||
When creating or editing notes, build connections:
|
||||
|
||||
```markdown
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- depends_on [[User Authentication]]
|
||||
```
|
||||
|
||||
Relations can be:
|
||||
|
||||
- Hierarchical (part_of, contains)
|
||||
- Directional (implements, depends_on)
|
||||
- Associative (relates_to, similar_to)
|
||||
- Temporal (precedes, follows)
|
||||
|
||||
Relations are also created via regular wiki-link style links within the body text.
|
||||
|
||||
### Forward References
|
||||
|
||||
Reference documents that don't exist yet:
|
||||
|
||||
```markdown
|
||||
- will_impact [[Future Feature]]
|
||||
```
|
||||
|
||||
These references resolve automatically when you create the referenced document.
|
||||
|
||||
## Conversation Continuity
|
||||
|
||||
Basic Memory maintains context across different conversations.
|
||||
|
||||
### Starting New Sessions with Context
|
||||
|
||||
When starting a new conversation with Claude, you can:
|
||||
|
||||
1. **Use special prompts** like "Continue conversation about..." or "What were we working on?"
|
||||
2. **Reference specific documents** with memory:// URLs
|
||||
3. **Ask about recent work** with "What have we been discussing recently?"
|
||||
4. **Search for specific topics** with "Find information about..."
|
||||
|
||||
### Long-Term Projects
|
||||
|
||||
Maintain context for complex projects over time:
|
||||
|
||||
1. **Document key decisions** as you make them
|
||||
2. **Create relationships** between project components
|
||||
3. **Reference past decisions** when implementing features
|
||||
4. **Update documentation** as the project evolves
|
||||
|
||||
### Tips for Effective Continuity
|
||||
|
||||
1. **Be specific about topics** when continuing a conversation
|
||||
2. **Reference documents directly** with memory:// URLs for precision
|
||||
3. **Create summary notes** after important discussions
|
||||
4. **Update existing notes** rather than creating duplicates
|
||||
5. **Build robust connections** between related topics
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Note Editing (New in v0.13.0)
|
||||
|
||||
**Edit notes incrementally without rewriting entire documents:**
|
||||
|
||||
```
|
||||
💬 "Add a new section about deployment to my API documentation"
|
||||
🤖 [Uses edit_note to append new section]
|
||||
|
||||
💬 "Update the date at the top of my meeting notes"
|
||||
🤖 [Uses edit_note to prepend new timestamp]
|
||||
|
||||
💬 "Replace the implementation section in my design doc"
|
||||
🤖 [Uses edit_note to replace specific section]
|
||||
```
|
||||
|
||||
Available editing operations:
|
||||
- **Append**: Add content to end of notes
|
||||
- **Prepend**: Add content to beginning of notes
|
||||
- **Replace Section**: Replace content under specific headers
|
||||
- **Find & Replace**: Simple text replacements with validation
|
||||
|
||||
### File Management (New in v0.13.0)
|
||||
|
||||
**Move and organize notes with full database consistency:**
|
||||
|
||||
```
|
||||
💬 "Move my old meeting notes to the archive folder"
|
||||
🤖 [Uses move_note with automatic folder creation and database updates]
|
||||
|
||||
💬 "Reorganize my project files into a better structure"
|
||||
🤖 [Moves files while maintaining search indexes and links]
|
||||
```
|
||||
|
||||
Move operations include:
|
||||
- **Database Consistency**: Updates file paths, permalinks, and checksums
|
||||
- **Search Reindexing**: Maintains search functionality after moves
|
||||
- **Folder Creation**: Automatically creates destination directories
|
||||
- **Project Isolation**: Moves are contained within the current project
|
||||
- **Rollback Protection**: Ensures data integrity during failed operations
|
||||
|
||||
### Enhanced Search (New in v0.13.0)
|
||||
|
||||
**Frontmatter tags are now searchable:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
tags: [coffee, brewing, equipment]
|
||||
---
|
||||
```
|
||||
|
||||
Now searchable by: "coffee", "brewing", "equipment", or "Coffee Brewing Methods"
|
||||
|
||||
### Importing External Knowledge
|
||||
|
||||
Import existing conversations:
|
||||
|
||||
```bash
|
||||
# From Claude
|
||||
basic-memory import claude conversations
|
||||
|
||||
# From ChatGPT
|
||||
basic-memory import chatgpt
|
||||
|
||||
# Target specific projects (v0.13.0)
|
||||
basic-memory --project=work import claude conversations
|
||||
```
|
||||
|
||||
After importing, changes sync automatically in real-time.
|
||||
|
||||
### Obsidian Integration
|
||||
|
||||
Use with [Obsidian](https://obsidian.md):
|
||||
|
||||
1. Point Obsidian to your `~/basic-memory` directory
|
||||
2. Use Obsidian's graph view to visualize your knowledge network
|
||||
3. All changes sync back to Basic Memory
|
||||
|
||||
### Canvas Visualizations
|
||||
|
||||
Create visual knowledge maps:
|
||||
|
||||
```
|
||||
You: Could you create a canvas visualization of our project components?
|
||||
```
|
||||
|
||||
This generates an Obsidian canvas file showing the relationships between concepts.
|
||||
|
||||
### Advanced Memory URI Patterns
|
||||
|
||||
Use wildcards and patterns:
|
||||
|
||||
```
|
||||
You: Review memory://project/*/requirements to summarize all project requirements.
|
||||
```
|
||||
|
||||
## Command Line Interface
|
||||
|
||||
### Sync Commands
|
||||
|
||||
```bash
|
||||
# One-time sync
|
||||
basic-memory sync
|
||||
|
||||
# Watch for changes
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
### Status and Information
|
||||
|
||||
```bash
|
||||
# Check system status
|
||||
basic-memory status
|
||||
|
||||
# View CLI help
|
||||
basic-memory --help
|
||||
```
|
||||
|
||||
### Import Commands
|
||||
|
||||
```bash
|
||||
# Import from Claude
|
||||
basic-memory import claude conversations
|
||||
|
||||
# Import from ChatGPT
|
||||
basic-memory import chatgpt
|
||||
```
|
||||
|
||||
## Multiple Projects (v0.13.0)
|
||||
|
||||
Basic Memory v0.13.0 introduces **fluid project management** - the ability to switch between projects instantly during conversations without restart. This allows you to maintain separate knowledge graphs for different purposes while seamlessly switching between them.
|
||||
|
||||
### Instant Project Switching (New in v0.13.0)
|
||||
|
||||
**Switch projects during conversations:**
|
||||
|
||||
```
|
||||
💬 "What projects do I have?"
|
||||
🤖 Available projects:
|
||||
• main (current, default)
|
||||
• work-notes
|
||||
• personal-journal
|
||||
• code-snippets
|
||||
|
||||
💬 "Switch to work-notes"
|
||||
🤖 ✓ Switched to work-notes project
|
||||
|
||||
Project Summary:
|
||||
• 47 entities
|
||||
• 125 observations
|
||||
• 23 relations
|
||||
|
||||
💬 "What did I work on yesterday?"
|
||||
🤖 [Shows recent activity from work-notes project]
|
||||
```
|
||||
|
||||
### Project-Specific Operations (New in v0.13.0)
|
||||
|
||||
Some MCP tools support optional project parameters for targeting specific projects:
|
||||
|
||||
```
|
||||
💬 "Create a note about this meeting in my personal-notes project"
|
||||
🤖 [Creates note in personal-notes project]
|
||||
|
||||
💬 "Switch to my work project"
|
||||
🤖 [Switches project context, then all operations work within that project]
|
||||
```
|
||||
|
||||
**Note**: Operations like search, move, and edit work within the currently active project. To work with content in different projects, switch to that project first or use the project parameter where supported.
|
||||
|
||||
### Managing Projects
|
||||
|
||||
```bash
|
||||
# List all configured projects
|
||||
basic-memory project list
|
||||
|
||||
# Add a new project
|
||||
basic-memory project create work ~/work-basic-memory
|
||||
|
||||
# Set the default project
|
||||
basic-memory project set-default work
|
||||
|
||||
# Remove a project (doesn't delete files)
|
||||
basic-memory project delete personal
|
||||
|
||||
# Show current project statistics
|
||||
basic-memory project info
|
||||
```
|
||||
|
||||
### Using Projects in Commands
|
||||
|
||||
All commands support the `--project` flag to specify which project to use:
|
||||
|
||||
```bash
|
||||
# Sync a specific project
|
||||
basic-memory --project=work sync
|
||||
|
||||
# Run MCP server for a specific project
|
||||
basic-memory --project=personal mcp
|
||||
```
|
||||
|
||||
You can also set the `BASIC_MEMORY_PROJECT` environment variable:
|
||||
|
||||
```bash
|
||||
BASIC_MEMORY_PROJECT=work basic-memory sync
|
||||
```
|
||||
|
||||
### Unified Database Architecture (New in v0.13.0)
|
||||
|
||||
Basic Memory v0.13.0 uses a unified database architecture:
|
||||
|
||||
- **Single Database**: All projects share `~/.basic-memory/memory.db`
|
||||
- **Project Isolation**: Proper data separation with project context
|
||||
- **Better Performance**: Optimized queries and reduced file I/O
|
||||
- **Easier Backup**: Single database file contains all project data
|
||||
- **Session Context**: Maintains active project throughout conversations
|
||||
|
||||
## Workflow Tips
|
||||
|
||||
### General Workflow
|
||||
1. **Project Organization**: Use multiple projects to separate different areas (work, personal, research)
|
||||
2. **Session Context**: Switch projects during conversations without restart (v0.13.0)
|
||||
3. **Real-time Sync**: Changes sync automatically - no need to run watch mode
|
||||
4. **Review Content**: Edit AI-created content for accuracy
|
||||
5. **Build Connections**: Create rich relationships between related ideas
|
||||
6. **Use Special Prompts**: Start conversations with context from your knowledge base
|
||||
|
||||
### v0.13.0 Workflow Enhancements
|
||||
7. **Incremental Editing**: Use edit_note for small changes instead of rewriting entire documents
|
||||
8. **File Organization**: Move and reorganize notes as your knowledge base grows
|
||||
9. **Project-Specific Creation**: Create notes in specific projects using project parameters
|
||||
10. **Search Tags**: Use frontmatter tags to improve content discoverability
|
||||
11. **Project Statistics**: Monitor project growth and activity with project info commands
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sync Issues
|
||||
|
||||
If changes aren't showing up:
|
||||
|
||||
1. Run `basic-memory status` to check system state
|
||||
2. Try a manual sync with `basic-memory sync`
|
||||
|
||||
### Missing Content
|
||||
|
||||
If content isn't found:
|
||||
|
||||
1. Check the exact path and permalink
|
||||
2. Try searching with more general terms
|
||||
3. Verify the file exists in your knowledge base
|
||||
|
||||
### Relation Problems
|
||||
|
||||
If relations aren't working:
|
||||
|
||||
1. Ensure exact title matching in [[WikiLinks]]
|
||||
2. Check for typos in relation types
|
||||
3. Verify both documents exist
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Knowledge Format]] (How knowledge is structured)
|
||||
- relates_to [[Getting Started with Basic Memory]] (Setup and first steps)
|
||||
- relates_to [[Canvas]] (Creating visual knowledge maps)
|
||||
- relates_to [[CLI Reference]] (Command line tools)
|
||||
- enhanced_in_v0.13.0 [[OAuth Authentication Guide]] (Production authentication)
|
||||
- enhanced_in_v0.13.0 [[Project Management]] (Multi-project workflows)
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
title: Introduction to Basic Memory
|
||||
type: docs
|
||||
permalink: docs/introduction
|
||||
tags:
|
||||
- documentation
|
||||
- index
|
||||
- overview
|
||||
---
|
||||
|
||||
# BASIC MEMORY
|
||||
|
||||
Basic Memory is a knowledge management system that allows you to build a persistent semantic graph from conversations
|
||||
with AI assistants. All knowledge is stored in standard Markdown files on your computer, giving you full control and
|
||||
ownership of your data.
|
||||
|
||||
Basic Memory connects you and AI assistants through shared knowledge:
|
||||
|
||||
1. **Captures knowledge** from natural conversations with AI assistants
|
||||
2. **Structures information** using simple semantic patterns in Markdown
|
||||
3. **Enables knowledge reuse** across different conversations and sessions
|
||||
4. **Maintains persistence** through local files you control completely
|
||||
|
||||
Both you and AI assistants like Claude can read from and write to the same knowledge base, creating a continuous
|
||||
learning environment where each conversation builds upon previous ones.
|
||||
|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
- AI assistants can load context from local files in a new conversation
|
||||
- Notes are saved locally as Markdown files in real time
|
||||
- No project knowledge or special prompting required
|
||||
|
||||
![[Claude-Obsidian-Demo.mp4]]
|
||||
|
||||
Basic Memory uses:
|
||||
|
||||
- **Files as the source of truth** - Everything is stored in plain Markdown files
|
||||
- **Git-compatible storage** - All knowledge can be versioned, branched, and merged
|
||||
- **Local SQLite database** - For fast indexing and searching only (not primary storage)
|
||||
- **Model Context Protocol (MCP)** - For seamless AI assistant integration
|
||||
|
||||
Basic Memory gives you complete control over your knowledge:
|
||||
|
||||
- **Local-first storage** - All knowledge lives on your computer
|
||||
- **Standard file formats** - Plain Markdown compatible with any editor
|
||||
- **Directory organization** - Knowledge stored in `~/basic-memory` by default
|
||||
- **Version control ready** - Use git for history, branching, and collaboration
|
||||
- **Edit anywhere** - Modify files with any text editor or Obsidian
|
||||
|
||||
Changes to files automatically sync with the knowledge graph, and AI assistants can see your edits in conversations.
|
||||
|
||||
## Documentation Map
|
||||
|
||||
Continue exploring Basic Memory with these guides:
|
||||
|
||||
- Installation and setup [[Getting Started with Basic Memory]]
|
||||
- Comprehensive usage instructions [[User Guide]]
|
||||
- Detailed explanation of knowledge structure [[Knowledge Format]]
|
||||
- Obsidian integration guide [[Obsidian Integration]]
|
||||
- Canvas visualization guide [[Canvas]]
|
||||
- Command line tool reference [[CLI Reference]]
|
||||
- Reference for AI assistants using Basic Memory [[AI Assistant Guide]]
|
||||
- Technical implementation details [[Technical Information]]
|
||||
|
||||
## Next Steps
|
||||
|
||||
Start with the [[Getting Started with Basic Memory]] guide to install Basic Memory and configure it with your AI
|
||||
assistant.
|
||||
|
Before Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 908 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,241 @@
|
||||
# Character Handling and Conflict Resolution
|
||||
|
||||
Basic Memory handles various character encoding scenarios and file naming conventions to provide consistent permalink generation and conflict resolution. This document explains how the system works and how to resolve common character-related issues.
|
||||
|
||||
## Overview
|
||||
|
||||
Basic Memory uses a sophisticated system to generate permalinks from file paths while maintaining consistency across different operating systems and character encodings. The system normalizes file paths and generates unique permalinks to prevent conflicts.
|
||||
|
||||
## Character Normalization Rules
|
||||
|
||||
### 1. Permalink Generation
|
||||
|
||||
When Basic Memory processes a file path, it applies these normalization rules:
|
||||
|
||||
```
|
||||
Original: "Finance/My Investment Strategy.md"
|
||||
Permalink: "finance/my-investment-strategy"
|
||||
```
|
||||
|
||||
**Transformation process:**
|
||||
1. Remove file extension (`.md`)
|
||||
2. Convert to lowercase (case-insensitive)
|
||||
3. Replace spaces with hyphens
|
||||
4. Replace underscores with hyphens
|
||||
5. Handle international characters (transliteration for Latin, preservation for non-Latin)
|
||||
6. Convert camelCase to kebab-case
|
||||
|
||||
### 2. International Character Support
|
||||
|
||||
**Latin characters with diacritics** are transliterated:
|
||||
- `ø` → `o` (Søren → soren)
|
||||
- `ü` → `u` (Müller → muller)
|
||||
- `é` → `e` (Café → cafe)
|
||||
- `ñ` → `n` (Niño → nino)
|
||||
|
||||
**Non-Latin characters** are preserved:
|
||||
- Chinese: `中文/测试文档.md` → `中文/测试文档`
|
||||
- Japanese: `日本語/文書.md` → `日本語/文書`
|
||||
|
||||
## Common Conflict Scenarios
|
||||
|
||||
### 1. Hyphen vs Space Conflicts
|
||||
|
||||
**Problem:** Files with existing hyphens conflict with generated permalinks from spaces.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
|
||||
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug" (CONFLICT!)
|
||||
```
|
||||
|
||||
**Resolution:** The system automatically resolves this by adding suffixes:
|
||||
```
|
||||
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
|
||||
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug-1"
|
||||
```
|
||||
|
||||
**Best Practice:** Choose consistent naming conventions within your project.
|
||||
|
||||
### 2. Case Sensitivity Conflicts
|
||||
|
||||
**Problem:** Different case variations that normalize to the same permalink.
|
||||
|
||||
**Example on macOS:**
|
||||
```
|
||||
Directory: Finance/investment.md
|
||||
Directory: finance/investment.md (different on filesystem, same permalink)
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory detects case conflicts and prevents them during sync operations with helpful error messages.
|
||||
|
||||
**Best Practice:** Use consistent casing for directory and file names.
|
||||
|
||||
### 3. Character Encoding Conflicts
|
||||
|
||||
**Problem:** Different Unicode normalizations of the same logical character.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
File 1: "café.md" (é as single character)
|
||||
File 2: "café.md" (e + combining accent)
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory normalizes Unicode characters using NFD normalization to detect these conflicts.
|
||||
|
||||
### 4. Forward Slash Conflicts
|
||||
|
||||
**Problem:** Forward slashes in frontmatter or file names interpreted as path separators.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
---
|
||||
permalink: finance/investment/strategy
|
||||
---
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory validates frontmatter permalinks and warns about path separator conflicts.
|
||||
|
||||
## Error Messages and Troubleshooting
|
||||
|
||||
### "UNIQUE constraint failed: entity.file_path, entity.project_id"
|
||||
|
||||
**Cause:** Two entities trying to use the same file path within a project.
|
||||
|
||||
**Common scenarios:**
|
||||
1. File move operation where destination is already occupied
|
||||
2. Case sensitivity differences on macOS
|
||||
3. Character encoding conflicts
|
||||
4. Concurrent file operations
|
||||
|
||||
**Resolution steps:**
|
||||
1. Check for duplicate file names with different cases
|
||||
2. Look for files with similar names but different character encodings
|
||||
3. Rename conflicting files to have unique names
|
||||
4. Run sync again after resolving conflicts
|
||||
|
||||
### "File path conflict detected during move"
|
||||
|
||||
**Cause:** Enhanced conflict detection preventing potential database integrity violations.
|
||||
|
||||
**What this means:** The system detected that moving a file would create a conflict before attempting the database operation.
|
||||
|
||||
**Resolution:** Follow the specific guidance in the error message, which will indicate the type of conflict detected.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. File Naming Conventions
|
||||
|
||||
**Recommended patterns:**
|
||||
- Use consistent casing (prefer lowercase)
|
||||
- Use hyphens instead of spaces for multi-word files
|
||||
- Avoid special characters that could conflict with path separators
|
||||
- Be consistent with directory structure casing
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
✅ Good:
|
||||
- finance/investment-strategy.md
|
||||
- projects/basic-memory-features.md
|
||||
- docs/api-reference.md
|
||||
|
||||
❌ Problematic:
|
||||
- Finance/Investment Strategy.md (mixed case, spaces)
|
||||
- finance/Investment Strategy.md (inconsistent case)
|
||||
- docs/API/Reference.md (mixed case directories)
|
||||
```
|
||||
|
||||
### 2. Permalink Management
|
||||
|
||||
**Custom permalinks in frontmatter:**
|
||||
```yaml
|
||||
---
|
||||
type: knowledge
|
||||
permalink: custom-permalink-name
|
||||
---
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
- Use lowercase permalinks
|
||||
- Use hyphens for word separation
|
||||
- Avoid path separators unless creating sub-paths
|
||||
- Ensure uniqueness within your project
|
||||
|
||||
### 3. Directory Structure
|
||||
|
||||
**Consistent casing:**
|
||||
```
|
||||
✅ Good:
|
||||
finance/
|
||||
investment-strategies.md
|
||||
portfolio-management.md
|
||||
|
||||
❌ Problematic:
|
||||
Finance/ (capital F)
|
||||
investment-strategies.md
|
||||
finance/ (lowercase f)
|
||||
portfolio-management.md
|
||||
```
|
||||
|
||||
## Migration and Cleanup
|
||||
|
||||
### Identifying Conflicts
|
||||
|
||||
Use Basic Memory's built-in conflict detection:
|
||||
|
||||
```bash
|
||||
# Sync will report conflicts
|
||||
basic-memory sync
|
||||
|
||||
# Check sync status for warnings
|
||||
basic-memory status
|
||||
```
|
||||
|
||||
### Resolving Existing Conflicts
|
||||
|
||||
1. **Identify conflicting files** from sync error messages
|
||||
2. **Choose consistent naming convention** for your project
|
||||
3. **Rename files** to follow the convention
|
||||
4. **Re-run sync** to verify resolution
|
||||
|
||||
### Bulk Renaming Strategy
|
||||
|
||||
For projects with many conflicts:
|
||||
|
||||
1. **Backup your project** before making changes
|
||||
2. **Standardize on lowercase** file and directory names
|
||||
3. **Replace spaces with hyphens** in file names
|
||||
4. **Use consistent character encoding** (UTF-8)
|
||||
5. **Test sync after each batch** of changes
|
||||
|
||||
## System Enhancements
|
||||
|
||||
### Recent Improvements (v0.13+)
|
||||
|
||||
1. **Enhanced conflict detection** before database operations
|
||||
2. **Improved error messages** with specific resolution guidance
|
||||
3. **Character normalization utilities** for consistent handling
|
||||
4. **File swap detection** for complex move scenarios
|
||||
5. **Proactive conflict warnings** during permalink resolution
|
||||
|
||||
### Monitoring and Logging
|
||||
|
||||
The system now provides detailed logging for conflict resolution:
|
||||
|
||||
```
|
||||
DEBUG: Detected potential file path conflicts for 'Finance/Investment.md': ['finance/investment.md']
|
||||
WARNING: File path conflict detected during move: entity_id=123 trying to move from 'old.md' to 'new.md'
|
||||
```
|
||||
|
||||
These logs help identify and resolve conflicts before they cause sync failures.
|
||||
|
||||
## Support and Resources
|
||||
|
||||
If you encounter character-related conflicts not covered in this guide:
|
||||
|
||||
1. **Check the logs** for specific conflict details
|
||||
2. **Review error messages** for resolution guidance
|
||||
3. **Report issues** with examples of the conflicting files
|
||||
4. **Consider the file naming best practices** outlined above
|
||||
|
||||
The Basic Memory system is designed to handle most character conflicts automatically while providing clear guidance for manual resolution when needed.
|
||||
@@ -1,5 +0,0 @@
|
||||
var analyticsScript = document.createElement('script');
|
||||
analyticsScript.defer = true;
|
||||
analyticsScript.setAttribute('data-website-id', '8d51086e-5c67-401e-97b0-b24706a6d4f3');
|
||||
analyticsScript.src = 'https://cloud.umami.is/script.js';
|
||||
document.head.appendChild(analyticsScript);
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"promptDelete": false
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,3 +0,0 @@
|
||||
[
|
||||
"optimize-canvas-connections"
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": true,
|
||||
"sync": true,
|
||||
"webviewer": false
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// main.ts
|
||||
var main_exports = {};
|
||||
__export(main_exports, {
|
||||
default: () => OptimizeCanvasConnectionsPlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian = require("obsidian");
|
||||
var OptimizeCanvasConnectionsPlugin = class extends import_obsidian.Plugin {
|
||||
async onload() {
|
||||
this.addCommand({
|
||||
id: "optimize-preserve-axes-selection",
|
||||
name: "Optimize selection (preserve axes)",
|
||||
checkCallback: (checking) => {
|
||||
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
|
||||
if ((canvasView == null ? void 0 : canvasView.getViewType()) == "canvas") {
|
||||
if (!checking) {
|
||||
this.optimize("preserve-axes");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "optimize-shortest-path-selection",
|
||||
name: "Optimize selection (shortest path)",
|
||||
checkCallback: (checking) => {
|
||||
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
|
||||
if ((canvasView == null ? void 0 : canvasView.getViewType()) == "canvas") {
|
||||
if (!checking) {
|
||||
this.optimize("shortest-path");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
onunload() {
|
||||
}
|
||||
async optimize(option) {
|
||||
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
|
||||
const canvas = canvasView == null ? void 0 : canvasView.canvas;
|
||||
const currentSelection = canvas == null ? void 0 : canvas.selection;
|
||||
let selectedIDs = new Array();
|
||||
currentSelection.forEach(function(selection) {
|
||||
selectedIDs.push(selection.id);
|
||||
});
|
||||
let applyToAll = false;
|
||||
if (selectedIDs.length == 0) {
|
||||
applyToAll = true;
|
||||
}
|
||||
for (let [edgeKey, edge] of canvas["edges"]) {
|
||||
let fromNode = edge["from"]["node"];
|
||||
let toNode = edge["to"]["node"];
|
||||
let fromPossibilities = [edge["from"]["side"]];
|
||||
if (applyToAll || selectedIDs.includes(fromNode["id"])) {
|
||||
switch (option) {
|
||||
case "shortest-path":
|
||||
fromPossibilities = ["top", "bottom", "left", "right"];
|
||||
break;
|
||||
case "preserve-axes":
|
||||
switch (edge["from"]["side"]) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
fromPossibilities = ["top", "bottom"];
|
||||
break;
|
||||
case "left":
|
||||
case "right":
|
||||
fromPossibilities = ["left", "right"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let toPossibilities = [edge["to"]["side"]];
|
||||
if (applyToAll || selectedIDs.includes(toNode["id"])) {
|
||||
switch (option) {
|
||||
case "shortest-path":
|
||||
toPossibilities = ["top", "bottom", "left", "right"];
|
||||
break;
|
||||
case "preserve-axes":
|
||||
switch (edge["to"]["side"]) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
toPossibilities = ["top", "bottom"];
|
||||
break;
|
||||
case "left":
|
||||
case "right":
|
||||
toPossibilities = ["left", "right"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let distances = [];
|
||||
for (const fromSide of fromPossibilities) {
|
||||
let fromPoint = { "x": 0, "y": 0 };
|
||||
if (fromSide == "top") {
|
||||
fromPoint = { "x": fromNode["x"] + fromNode["width"] / 2, "y": fromNode["y"] };
|
||||
} else if (fromSide == "bottom") {
|
||||
fromPoint = { "x": fromNode["x"] + fromNode["width"] / 2, "y": fromNode["y"] + fromNode["height"] };
|
||||
} else if (fromSide == "left") {
|
||||
fromPoint = { "x": fromNode["x"], "y": fromNode["y"] + fromNode["height"] / 2 };
|
||||
} else if (fromSide == "right") {
|
||||
fromPoint = { "x": fromNode["x"] + fromNode["width"], "y": fromNode["y"] + fromNode["height"] / 2 };
|
||||
}
|
||||
for (const toSide of toPossibilities) {
|
||||
let toPoint = { "x": 0, "y": 0 };
|
||||
if (toSide == "top") {
|
||||
toPoint = { "x": toNode["x"] + toNode["width"] / 2, "y": toNode["y"] };
|
||||
} else if (toSide == "bottom") {
|
||||
toPoint = { "x": toNode["x"] + toNode["width"] / 2, "y": toNode["y"] + toNode["height"] };
|
||||
} else if (toSide == "left") {
|
||||
toPoint = { "x": toNode["x"], "y": toNode["y"] + toNode["height"] / 2 };
|
||||
} else if (toSide == "right") {
|
||||
toPoint = { "x": toNode["x"] + toNode["width"], "y": toNode["y"] + toNode["height"] / 2 };
|
||||
}
|
||||
distances.push({
|
||||
"fromSide": fromSide,
|
||||
"toSide": toSide,
|
||||
"distance": (toPoint.x - fromPoint.x) ** 2 + (toPoint.y - fromPoint.y) ** 2
|
||||
});
|
||||
}
|
||||
}
|
||||
distances = distances.sort(function(a, b) {
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
edge["from"]["side"] = distances[0]["fromSide"];
|
||||
edge["to"]["side"] = distances[0]["toSide"];
|
||||
edge.render();
|
||||
}
|
||||
canvas.requestSave();
|
||||
}
|
||||
};
|
||||
|
||||
/* nosourcemap */
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "optimize-canvas-connections",
|
||||
"name": "Optimize Canvas Connections",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.1.9",
|
||||
"description": "An Obsidian plugin that declutters a canvas by reconnecting notes using their nearest edges.",
|
||||
"author": "Félix Chénier",
|
||||
"authorUrl": "https://felixchenier.uqam.ca",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"siteId": null,
|
||||
"host": null,
|
||||
"included": [],
|
||||
"excluded": []
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
title: Brewing Equipment
|
||||
type: note
|
||||
permalink: coffee/brewing-equipment
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#equipment'
|
||||
- '#gear'
|
||||
- '#brewing'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Brewing Equipment
|
||||
|
||||
Essential tools and equipment for brewing coffee, their characteristics, and how they affect the brewing process.
|
||||
|
||||
## Overview
|
||||
|
||||
The equipment used to brew coffee plays a crucial role in determining the final cup quality. From grinders to brewers to kettles, each piece of equipment contributes to different aspects of the brewing process.
|
||||
|
||||
## Observations
|
||||
|
||||
- [principle] Equipment quality often has a bigger impact on consistency than on absolute quality potential #quality
|
||||
- [principle] Good grind consistency is the most important technical factor in extraction quality #grind
|
||||
- [investment] A good burr grinder is often the most important investment for improving home coffee #gear
|
||||
- [technique] Equipment maintenance and cleaning significantly impact flavor consistency over time #maintenance
|
||||
|
||||
## Grinders
|
||||
|
||||
- [equipment] Burr grinders crush beans between two abrasive surfaces for more consistent particle size #grinders
|
||||
- [equipment] Blade grinders chop beans unevenly, leading to inconsistent extraction #grinders
|
||||
- [equipment] Flat burr grinders produce very consistent particle size but generate more heat #burrs
|
||||
- [equipment] Conical burr grinders create slightly less uniform grounds but with less heat and noise #burrs
|
||||
- [feature] Grind adjustment mechanisms range from stepped to stepless for different precision levels #adjustment
|
||||
- [feature] Retention (grounds trapped in grinder) affects dose consistency and freshness #retention
|
||||
- [price] Hand grinders offer excellent value, with models like Timemore C2 and 1Zpresso JX providing excellent results around $100-150 #budget
|
||||
- [price] Entry-level electric burr grinders like Baratza Encore start around $170 but provide significant improvement over blade grinders #value
|
||||
|
||||
## Brewers
|
||||
|
||||
### Pour Over Brewers
|
||||
- [equipment] Hario V60 uses a conical design with spiral ridges to control flow rate #pourover
|
||||
- [equipment] Kalita Wave has a flat bottom with three small holes for more consistent extraction #pourover
|
||||
- [equipment] Chemex combines brewer and server with thick proprietary filters for ultra-clean cup #pourover
|
||||
- [material] Ceramic brewers retain heat better than plastic but are more fragile #materials
|
||||
- [material] Glass brewers provide neutral flavor but less heat retention #materials
|
||||
- [material] Plastic brewers are inexpensive, durable, and surprisingly good for heat retention #materials
|
||||
|
||||
### Immersion Brewers
|
||||
- [equipment] French Press uses a metal mesh to separate grounds, allowing oils and fine particles to pass #immersion
|
||||
- [equipment] AeroPress uses pressure and paper filter for clean, versatile brewing #immersion
|
||||
- [equipment] Clever Dripper combines immersion and drip methods with a valve mechanism #hybrid
|
||||
- [material] Glass French presses look elegant but break easily and have poor heat retention #materials
|
||||
- [material] Stainless steel or ceramic French presses offer better durability and heat retention #materials
|
||||
|
||||
### Pressure Brewers
|
||||
- [equipment] Espresso machines use 9 bars of pressure, requiring significant investment for good results #espresso
|
||||
- [equipment] Moka pot uses steam pressure for strong, concentrated coffee at affordable price #moka
|
||||
- [equipment] Manual lever machines like Flair or Robot provide espresso-style coffee with manual control #manual_espresso
|
||||
|
||||
## Kettles
|
||||
|
||||
- [equipment] Gooseneck kettles provide precision pouring control essential for pour over methods #kettles
|
||||
- [feature] Variable temperature kettles allow precise temperature control for different roast levels #temp_control
|
||||
- [feature] Flow restrictors can help beginners maintain consistent pour rates #pour_control
|
||||
- [material] Electric kettles offer convenience and temperature stability #convenience
|
||||
- [material] Stovetop kettles may be more durable but offer less temperature control #durability
|
||||
|
||||
## Accessories
|
||||
|
||||
- [equipment] Coffee scale with 0.1g precision helps maintain consistent ratios #measurement
|
||||
- [equipment] Timer ensures consistent extraction times #consistency
|
||||
- [equipment] Quality filters significantly impact flavor clarity and body #filters
|
||||
- [equipment] Storage containers with one-way valves help preserve bean freshness #storage
|
||||
- [equipment] Blind shaker or dosing cup reduces grinder mess and improves workflow #workflow
|
||||
|
||||
## Relations
|
||||
|
||||
- improves [[Coffee Brewing Methods]]
|
||||
- affects [[Flavor Extraction]]
|
||||
- requires [[Proper Maintenance]]
|
||||
- enhances [[Home Coffee Setup]]
|
||||
- part_of [[Coffee Knowledge Base]]
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
title: Coffee Bean Origins
|
||||
type: note
|
||||
permalink: coffee/coffee-bean-origins
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#origins'
|
||||
- '#beans'
|
||||
- '#regions'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Coffee Bean Origins
|
||||
|
||||
An exploration of coffee-growing regions around the world and how geography, climate, and processing methods affect flavor profiles.
|
||||
|
||||
## Overview
|
||||
|
||||
Coffee beans are grown in various regions around the world, primarily in what's known as the "Coffee Belt" - the area between the Tropics of Cancer and Capricorn. The flavor characteristics of coffee beans are influenced by:
|
||||
|
||||
- Geographic region and climate
|
||||
- Altitude
|
||||
- Soil composition
|
||||
- Variety of coffee plant
|
||||
- Processing method
|
||||
- Harvest and sorting practices
|
||||
|
||||
## Observations
|
||||
|
||||
- [principle] Higher altitude generally produces harder, denser beans with more complex acidity #altitude
|
||||
- [region] Ethiopian beans often feature bright, fruity notes with floral aromatics #ethiopia
|
||||
- [region] Colombian coffee typically offers balanced acidity with caramel sweetness and nutty undertones #colombia
|
||||
- [region] Guatemalan coffee presents complex acidity with chocolate notes and sometimes spice characteristics #guatemala
|
||||
- [region] Brazilian coffee tends toward nutty, chocolate notes with lower acidity and fuller body #brazil
|
||||
- [region] Kenyan coffee is known for bright, wine-like acidity and berry or citrus notes #kenya
|
||||
- [processing] Natural (dry) processing tends to create fruitier, more fermented flavors #processing
|
||||
- [processing] Washed (wet) processing generally results in cleaner, brighter cups with more clarity #processing
|
||||
- [processing] Honey processing creates a middle ground with some fruity notes while maintaining clarity #processing
|
||||
- [factor] Shade-grown coffee typically develops more slowly, resulting in more complex flavors #cultivation
|
||||
- [factor] Soil volcanic soil often imparts distinctive mineral characteristics to coffee #terroir
|
||||
- [variety] Gesha/Geisha variety is known for exceptional floral and tea-like qualities #varieties
|
||||
- [variety] Bourbon varieties often feature sweet, complex cup profiles #varieties
|
||||
- [variety] Robusta beans have higher caffeine content but generally less complex flavor than Arabica #varieties
|
||||
|
||||
## Major Growing Regions
|
||||
|
||||
- [africa] Ethiopian coffees: Yirgacheffe, Sidamo, Harrar regions each with distinctive profiles #ethiopia
|
||||
- [africa] Kenyan coffees: Often categorized by grade (AA, AB, etc.) based on bean size #kenya
|
||||
- [americas] Colombian regions: Huila, Nariño, Antioquia each with unique characteristics #colombia
|
||||
- [americas] Central American producers: Guatemala, Costa Rica, Panama known for balanced profiles #central_america
|
||||
- [americas] Brazilian regions: Cerrado, Sul de Minas, Mogiana with varying profiles #brazil
|
||||
- [asia] Indonesian islands: Sumatra, Java, Sulawesi producing earthy, full-bodied coffees #indonesia
|
||||
- [asia] Vietnamese coffee: World's largest Robusta producer, often used in blends and commercial coffee #vietnam
|
||||
|
||||
## Processing Methods
|
||||
|
||||
- [natural] Beans dried inside the fruit, creating fruity, fermented notes and heavier body #processing
|
||||
- [washed] Fruit removed before drying, resulting in cleaner cup with more pronounced acidity #processing
|
||||
- [honey] Some fruit mucilage left on during drying, creates balanced sweetness and body #processing
|
||||
- [wet-hulled] Unique to Indonesia, creates earthy, herbal, low-acid profiles #processing
|
||||
- [experimental] Anaerobic fermentation, wine-yeast inoculation, and other newer methods #innovation
|
||||
|
||||
## Tasting Notes by Region
|
||||
|
||||
- [ethiopia] Blueberry, jasmine, bergamot, stone fruit, citrus #flavor_notes
|
||||
- [kenya] Blackcurrant, tomato, tropical fruit, wine-like acidity #flavor_notes
|
||||
- [colombia] Caramel, nuts, red apple, chocolate, balanced acidity #flavor_notes
|
||||
- [guatemala] Chocolate, spice, green apple, balanced #flavor_notes
|
||||
- [brazil] Nuts, chocolate, low acidity, full body #flavor_notes
|
||||
- [indonesia] Earthy, herbal, spice, cedar, full body, low acidity #flavor_notes
|
||||
|
||||
## Relations
|
||||
|
||||
- influences [[Flavor Extraction]]
|
||||
- pairs_with [[Coffee Brewing Methods]]
|
||||
- affects [[Tasting Notes]]
|
||||
- relates_to [[Specialty Coffee]]
|
||||
- part_of [[Coffee Knowledge Base]]
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
type: note
|
||||
permalink: coffee/coffee-brewing-methods
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#brewing'
|
||||
- '#methods'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
An exploration of different coffee brewing techniques, their characteristics, and how they affect flavor extraction.
|
||||
|
||||
## Overview
|
||||
|
||||
Coffee brewing is both an art and a science. Different brewing methods extract different compounds from coffee beans, resulting in unique flavor profiles, body, and mouthfeel. The key variables in any brewing method are:
|
||||
|
||||
- Grind size
|
||||
- Water temperature
|
||||
- Brew time
|
||||
- Coffee-to-water ratio
|
||||
- Agitation/turbulence
|
||||
|
||||
## Observations
|
||||
|
||||
- [principle] Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds #extraction
|
||||
- [method] Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity
|
||||
- [method] Immersion methods like French press create fuller body and more rounded flavors #body
|
||||
- [technique] Water at 195-205°F (90-96°C) extracts optimal flavor compounds for most brewing methods #temperature
|
||||
- [technique] Grind size directly correlates with ideal extraction time (finer = shorter, coarser = longer) #grind
|
||||
- [preference] Medium-light roasts often showcase more origin characteristics in pour over methods #roast
|
||||
- [equipment] Burr grinders produce more consistent particle size than blade grinders, resulting in more even extraction #gear
|
||||
- [ratio] 1:15 to 1:17 coffee-to-water ratio (by weight) works well for most brew methods #brewing
|
||||
- [science] Different brewing temperatures extract different chemical compounds from the beans #chemistry
|
||||
- [technique] Bloom phase (pre-infusion with small amount of water) allows CO2 to escape and improves extraction #bloom
|
||||
|
||||
## Pour Over Methods
|
||||
|
||||
- [method] V60 produces very clean cup with excellent clarity of flavor #pourover
|
||||
- [method] Chemex uses thicker filter paper, resulting in even cleaner cup with fewer oils #pourover
|
||||
- [method] Kalita Wave provides more consistent extraction due to flat bottom design #pourover
|
||||
- [technique] Concentric circular pouring pattern ensures even saturation of grounds #technique
|
||||
- [timing] Most pour over methods complete in 2:30-3:30 total brew time #brewing
|
||||
|
||||
## Immersion Methods
|
||||
|
||||
- [method] French Press creates full-bodied cup with rich mouthfeel due to metal filter allowing oils to pass #immersion
|
||||
- [method] AeroPress is versatile, capable of producing both espresso-like and filter-style coffee #immersion
|
||||
- [method] Cold brew uses time instead of heat to extract, resulting in lower acidity #immersion
|
||||
- [technique] French press ideal steep time is 4-5 minutes before plunging #timing
|
||||
- [technique] AeroPress inverted method prevents dripping during extraction phase #technique
|
||||
|
||||
## Pressure Methods
|
||||
|
||||
- [method] Espresso uses 9 bars of pressure to force water through finely ground coffee #pressure
|
||||
- [method] Moka pot uses steam pressure to push water through grounds, creating strong, concentrated coffee #pressure
|
||||
- [technique] Espresso requires very fine grind, almost powder-like consistency #grind
|
||||
- [timing] Espresso shots typically extract in 25-30 seconds #timing
|
||||
- [principle] Pressure methods can extract compounds that aren't soluble in regular brewing methods #extraction
|
||||
|
||||
## Relations
|
||||
|
||||
- requires [[Proper Grinding Technique]]
|
||||
- affects [[Flavor Extraction]]
|
||||
- pairs_with [[Coffee Bean Origins]]
|
||||
- uses [[Brewing Equipment]]
|
||||
- influences [[Tasting Notes]]
|
||||
- part_of [[Coffee Knowledge Base]]
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
title: Coffee Flavor Map
|
||||
type: note
|
||||
permalink: coffee/coffee-flavor-map
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#visualization'
|
||||
- '#canvas'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Coffee Flavor Map
|
||||
|
||||
A visual mapping of coffee flavor attributes, brewing methods, and their relationships. This note describes a canvas visualization that could be generated to demonstrate Basic Memory's visualization capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The Coffee Flavor Map provides a visual representation of how different brewing methods, coffee origins, and equipment choices affect flavor outcomes. This canvas visualization helps users understand the complex relationships in coffee brewing and tasting.
|
||||
|
||||
## Canvas Visualization Elements
|
||||
|
||||
### Core Nodes
|
||||
- **Flavor Attributes**: Acidity, Sweetness, Body, Clarity, Bitterness, Complexity
|
||||
- **Brewing Methods**: Pour Over, French Press, AeroPress, Espresso, Moka Pot, Cold Brew
|
||||
- **Origin Regions**: Ethiopia, Kenya, Colombia, Brazil, Guatemala, Indonesia
|
||||
- **Equipment Elements**: Grinder Quality, Water Temperature, Brewing Device, Filter Type
|
||||
|
||||
### Node Connections
|
||||
- Lines connecting brewing methods to their typical flavor outcomes
|
||||
- Arrows showing how equipment choices affect extraction variables
|
||||
- Connections between origins and their characteristic flavor profiles
|
||||
- Highlighting of optimal brewing methods for different origins
|
||||
|
||||
### Visual Organization
|
||||
- Flavor outcomes in the center
|
||||
- Brewing methods on the left side
|
||||
- Origins on the right side
|
||||
- Equipment variables at the bottom
|
||||
- Color coding by category (methods, origins, equipment, flavors)
|
||||
|
||||
## Using This Visualization
|
||||
|
||||
### For Coffee Exploration
|
||||
- Identify which brewing methods might highlight the characteristics you prefer
|
||||
- See which origins naturally pair well with your preferred brewing method
|
||||
- Understand how equipment changes can modify flavor outcomes
|
||||
- Visualize the complex interplay between all coffee variables
|
||||
|
||||
### As a Basic Memory Demo
|
||||
- Demonstrates Canvas visualization capabilities
|
||||
- Shows how relations can be visually mapped
|
||||
- Illustrates complex knowledge organization
|
||||
- Provides an intuitive way to navigate coffee knowledge
|
||||
|
||||
## How To Generate This Canvas
|
||||
|
||||
In a conversation with Claude, you could request:
|
||||
|
||||
```
|
||||
Please create a canvas visualization mapping the relationships between coffee brewing methods, origins, and flavor outcomes. Show how different equipment and techniques influence extraction and resulting flavor profiles.
|
||||
```
|
||||
|
||||
This would generate a `.canvas` file in your Basic Memory directory that could be opened with Obsidian for an interactive visualization of these coffee relationships.
|
||||
|
||||
## Example Visualization Snippets
|
||||
|
||||
### Pour Over Method Node
|
||||
- Connected to: High Clarity, Bright Acidity, Medium Body
|
||||
- Best pairs with: Ethiopian and Kenyan beans
|
||||
- Equipment dependencies: Gooseneck Kettle, Paper Filter, Burr Grinder
|
||||
|
||||
### Ethiopian Coffee Node
|
||||
- Characteristic flavors: Floral, Fruity, Bright
|
||||
- Best brewing methods: Pour Over, AeroPress
|
||||
- Challenging with: French Press (loses clarity of delicate notes)
|
||||
|
||||
### Grind Size Node
|
||||
- Affects: Extraction Rate, Flavor Balance
|
||||
- Fine grind increases: Extraction Speed, Surface Area
|
||||
- Coarse grind increases: Flow Rate, Reduces Bitter Compounds
|
||||
|
||||
## Relations
|
||||
|
||||
- visualizes [[Coffee Knowledge Base]]
|
||||
- relates_to [[Coffee Brewing Methods]]
|
||||
- relates_to [[Coffee Bean Origins]]
|
||||
- relates_to [[Flavor Extraction]]
|
||||
- relates_to [[Tasting Notes]]
|
||||
- demonstrates [[Canvas]]
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: Coffee Knowledge Base
|
||||
type: note
|
||||
permalink: coffee/coffee-knowledge-base
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#index'
|
||||
- '#demo'
|
||||
- '#knowledge'
|
||||
---
|
||||
|
||||
# Coffee Knowledge Base
|
||||
|
||||
A comprehensive collection of coffee knowledge, from bean origins to brewing methods to tasting notes. This knowledge base demonstrates Basic Memory's ability to organize and connect information in a meaningful way.
|
||||
|
||||
## Overview
|
||||
|
||||
This Coffee Knowledge Base captures key information about coffee, structured with semantic observations and relations that connect different aspects of coffee knowledge. It serves as both a useful reference for coffee enthusiasts and a demonstration of how Basic Memory organizes information.
|
||||
|
||||
## Key Topics
|
||||
|
||||
### Core Coffee Knowledge
|
||||
|
||||
- [[Coffee Brewing Methods]] - Different techniques for preparing coffee
|
||||
- [[Coffee Bean Origins]] - Where coffee comes from and how region affects flavor
|
||||
- [[Brewing Equipment]] - Tools and devices used to prepare coffee
|
||||
- [[Flavor Extraction]] - The science of dissolving flavor compounds from coffee
|
||||
- [[Tasting Notes]] - How to taste and describe coffee flavors
|
||||
|
||||
### Brewing Techniques
|
||||
|
||||
- Proper grinding is fundamental to good extraction
|
||||
- Water quality significantly impacts flavor
|
||||
- Different brewing methods highlight different characteristics
|
||||
- Time, temperature, and grind size are the key variables to control
|
||||
- Freshness of beans dramatically affects quality
|
||||
|
||||
### Coffee Preferences
|
||||
|
||||
- Light roasts preserve more origin characteristics and acidity
|
||||
- Dark roasts emphasize body and chocolatey/roasted flavors
|
||||
- Pour over methods highlight clarity and distinct flavor notes
|
||||
- Immersion methods create fuller body and rounded flavor
|
||||
- Personal preference matters more than "correctness"
|
||||
|
||||
## Using This Knowledge Base
|
||||
|
||||
### For Learning
|
||||
|
||||
Use this knowledge base to:
|
||||
- Understand coffee fundamentals
|
||||
- Explore connections between brewing methods and flavor outcomes
|
||||
- Learn how different origins produce distinct flavor profiles
|
||||
- Discover how equipment affects the brewing process
|
||||
- Develop a vocabulary for describing coffee experiences
|
||||
|
||||
### As a Demo
|
||||
|
||||
This knowledge base demonstrates:
|
||||
- Semantic knowledge organization with categories and relations
|
||||
- Building connections between related concepts
|
||||
- Creating a navigable knowledge graph
|
||||
- Structuring information in a way both humans and AI assistants can understand
|
||||
- How Basic Memory enables persistent knowledge across conversations
|
||||
|
||||
## Relations
|
||||
|
||||
- contains [[Coffee Brewing Methods]]
|
||||
- contains [[Coffee Bean Origins]]
|
||||
- contains [[Brewing Equipment]]
|
||||
- contains [[Flavor Extraction]]
|
||||
- contains [[Tasting Notes]]
|
||||
- demonstrates [[Basic Memory Capabilities]]
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Flavor Extraction
|
||||
type: note
|
||||
permalink: coffee/flavor-extraction
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#extraction'
|
||||
- '#brewing'
|
||||
- '#science'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Flavor Extraction
|
||||
|
||||
Understanding the science of coffee extraction, how different compounds dissolve at different rates, and how to control extraction to achieve desired flavor profiles.
|
||||
|
||||
## Overview
|
||||
|
||||
Coffee extraction is the process of dissolving flavor compounds from ground coffee into water. The science of extraction is key to producing a balanced, flavorful cup. Extraction is affected by numerous variables including grind size, water temperature, contact time, agitation, and pressure.
|
||||
|
||||
## Observations
|
||||
|
||||
- [science] Coffee contains over 1,000 aroma compounds and hundreds of flavor compounds #chemistry
|
||||
- [principle] Extraction occurs in a predictable sequence: acids → sugars → bitter compounds #extraction_order
|
||||
- [principle] Under-extraction results in sour, bright, thin coffee lacking sweetness and body #under_extraction
|
||||
- [principle] Over-extraction results in bitter, hollow, astringent flavors #over_extraction
|
||||
- [principle] The goal is typically balanced extraction (18-22% of coffee solubles dissolved) #balanced_extraction
|
||||
- [technique] Finer grind size increases extraction rate due to greater surface area #grind_size
|
||||
- [technique] Higher water temperature increases extraction rate and solubility of compounds #temperature
|
||||
- [technique] Longer contact time allows more complete extraction #brew_time
|
||||
- [technique] Agitation (stirring, turbulence) increases extraction rate by preventing saturation zones #agitation
|
||||
- [technique] Pressure (as in espresso) can extract compounds that aren't water-soluble at atmospheric pressure #pressure
|
||||
|
||||
## Factors Affecting Extraction
|
||||
|
||||
- [factor] Grind size: Finer = faster extraction, coarser = slower extraction #grind
|
||||
- [factor] Water temperature: Higher = faster extraction, lower = slower extraction #temperature
|
||||
- [factor] Contact time: Longer = more extraction, shorter = less extraction #time
|
||||
- [factor] Agitation: More = faster extraction, less = slower extraction #agitation
|
||||
- [factor] Coffee-to-water ratio: More coffee = lower extraction percentage #ratio
|
||||
- [factor] Water quality: Mineral content affects extraction of different compounds #water
|
||||
- [factor] Roast level: Darker roasts extract more easily than lighter roasts #roast
|
||||
- [factor] Bean density: Denser beans (typically high-altitude) require more effort to extract #density
|
||||
- [factor] Freshness: Freshly roasted coffee extracts differently than aged coffee #freshness
|
||||
- [factor] Brewing method: Different methods extract different compounds at different rates #method
|
||||
|
||||
## Signs of Extraction Levels
|
||||
|
||||
- [under] Sour, bright, lack of sweetness, thin body, quick finish #flavor
|
||||
- [under] Typically from: too coarse grind, too cool water, too short brew time #causes
|
||||
- [balanced] Sweet, bright but not sour, rich but not bitter, pleasing finish #flavor
|
||||
- [balanced] Achieved through proper ratio of variables for given coffee #technique
|
||||
- [over] Bitter, hollow, astringent, dry finish, sometimes papery #flavor
|
||||
- [over] Typically from: too fine grind, too hot water, too long brew time #causes
|
||||
|
||||
## Measuring Extraction
|
||||
|
||||
- [method] Total Dissolved Solids (TDS) meters measure concentration of coffee solution #measurement
|
||||
- [method] Extraction yield = percentage of coffee grounds dissolved in the final brew #calculation
|
||||
- [preference] Specialty coffee typically targets 18-22% extraction yield #standards
|
||||
- [preference] Some specialty light roasts may taste best at higher extraction percentages #speciality
|
||||
|
||||
## Controlling Extraction
|
||||
|
||||
- [technique] Adjust grind size as primary extraction control #basics
|
||||
- [technique] Use water temperature to fine-tune extraction #fine_tuning
|
||||
- [technique] Modify pour technique to control agitation level #technique
|
||||
- [technique] Adjust coffee-to-water ratio to balance strength and extraction #ratio
|
||||
- [technique] Pre-infusion (blooming) helps achieve even extraction #blooming
|
||||
- [technique] Pulse pouring creates different extraction dynamics than continuous pour #pour_technique
|
||||
|
||||
## Relations
|
||||
|
||||
- affected_by [[Coffee Brewing Methods]]
|
||||
- influenced_by [[Coffee Bean Origins]]
|
||||
- enhanced_by [[Brewing Equipment]]
|
||||
- determines [[Tasting Notes]]
|
||||
- requires [[Water Quality]]
|
||||
- part_of [[Coffee Knowledge Base]]
|
||||
@@ -1,161 +0,0 @@
|
||||
{
|
||||
"nodes":[
|
||||
{
|
||||
"id":"node-5",
|
||||
"type":"text",
|
||||
"text":"## Main Pour Phase\n- Use concentric circles from center outward\n- Maintain steady, controlled flow rate\n- Avoid pouring directly on filter walls\n- Keep water level consistent\n- Pulse pour in 2-3 stages (or continuous pour)\n- Total brew time target: 2:30-3:30",
|
||||
"position":{"x":450,"y":200},
|
||||
"x":530,
|
||||
"y":-100,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"1"
|
||||
},
|
||||
{
|
||||
"id":"node-8",
|
||||
"type":"text",
|
||||
"text":"## Drawdown\n- Allow water to fully drain\n- Flat bed indicates even extraction\n- Total brew time should be ~2:30-3:30\n- Remove filter promptly after brewing",
|
||||
"position":{"x":450,"y":700},
|
||||
"x":540,
|
||||
"y":375,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"1"
|
||||
},
|
||||
{
|
||||
"id":"node-6",
|
||||
"type":"text",
|
||||
"text":"## Pour Pattern\n\nConcentric circles ensure even saturation of coffee grounds. Begin at the center and work outward, avoiding filter edges. Pour height of 1-2 inches above coffee bed.",
|
||||
"position":{"x":250,"y":450},
|
||||
"x":960,
|
||||
"y":25,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"5"
|
||||
},
|
||||
{
|
||||
"id":"node-12",
|
||||
"type":"text",
|
||||
"text":"## Tasting Notes\n\n- Balanced extraction: sweet, bright, complex\n- Under-extraction: sour, lacking sweetness\n- Over-extraction: bitter, astringent, hollow\n\nTake notes on each brew to track improvements and preferences.",
|
||||
"position":{"x":-250,"y":700},
|
||||
"x":1020,
|
||||
"y":420,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"6"
|
||||
},
|
||||
{
|
||||
"id":"node-9",
|
||||
"type":"text",
|
||||
"text":"## Troubleshooting\n\n- Too sour/weak: Grind finer, water hotter, increase brew time\n- Too bitter/strong: Grind coarser, water cooler, decrease brew time\n- Uneven extraction: Improve pour technique, better grinder\n- Channeling: More careful pouring, better bloom\n- Slow drawdown: Coarser grind, less agitation\n- Fast drawdown: Finer grind, more careful pouring",
|
||||
"position":{"x":100,"y":700},
|
||||
"x":30,
|
||||
"y":570,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"6"
|
||||
},
|
||||
{
|
||||
"id":"node-3",
|
||||
"type":"text",
|
||||
"text":"## Preparation\n- Heat water to 195-205°F (90-96°C)\n- Measure coffee (1:15 to 1:17 ratio)\n- Medium-fine grind (sea salt consistency)\n- Rinse filter with hot water\n- Discard rinse water\n- Add ground coffee to filter\n- Level coffee bed",
|
||||
"position":{"x":-250,"y":200},
|
||||
"x":30,
|
||||
"y":-500,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"3"
|
||||
},
|
||||
{
|
||||
"id":"node-1",
|
||||
"type":"text",
|
||||
"text":"# Perfect Pour Over Method\n\nA systematic approach to brewing exceptional pour over coffee by controlling key variables and following proper technique.",
|
||||
"position":{"x":0,"y":0},
|
||||
"x":-580,
|
||||
"y":-760,
|
||||
"width":400,
|
||||
"height":120,
|
||||
"color":"4"
|
||||
},
|
||||
{
|
||||
"id":"node-10",
|
||||
"type":"text",
|
||||
"text":"## Grinding Parameters\n\n- V60: Medium-fine (sea salt)\n- Chemex: Medium (slightly coarser than V60)\n- Kalita Wave: Medium (between V60 and Chemex)\n\nConsistent particle size is critical; use quality burr grinder.",
|
||||
"position":{"x":-250,"y":450},
|
||||
"x":30,
|
||||
"y":-910,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"5"
|
||||
},
|
||||
{
|
||||
"id":"node-2",
|
||||
"type":"text",
|
||||
"text":"## Equipment Setup\n- Clean V60/Chemex/Kalita Wave\n- Paper filter (rinsed)\n- Server/mug\n- Scale with timer\n- Gooseneck kettle\n- Burr grinder\n- Fresh coffee beans",
|
||||
"position":{"x":-600,"y":200},
|
||||
"x":-530,
|
||||
"y":-500,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"3"
|
||||
},
|
||||
{
|
||||
"id":"node-4",
|
||||
"type":"text",
|
||||
"text":"## The Bloom\n- Start timer\n- Pour 2-3x coffee weight water\n- Ensure all grounds are saturated\n- Gentle stir or swirl if needed\n- Allow 30-45 seconds for degassing\n- Look for bubbling and dome formation",
|
||||
"position":{"x":100,"y":200},
|
||||
"x":530,
|
||||
"y":-500,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"1"
|
||||
},
|
||||
{
|
||||
"id":"node-13",
|
||||
"type":"text",
|
||||
"text":"## Coffee-to-Water Ratio\n\n- Standard: 1:15 to 1:17 (coffee:water)\n- Stronger cup: 1:15 (67g/L)\n- Medium cup: 1:16 (62.5g/L)\n- Lighter cup: 1:17 (58.8g/L)\n\nExample: For 300ml water, use ~18-20g coffee",
|
||||
"position":{"x":-600,"y":700},
|
||||
"x":30,
|
||||
"y":-100,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"6"
|
||||
},
|
||||
{
|
||||
"id":"node-7",
|
||||
"type":"text",
|
||||
"text":"## Brew Time Guideline\n\n- Bloom: 30-45 seconds\n- First pour: 1:00-1:15\n- Second pour: 1:45-2:00\n- Final pour: 2:15-2:30\n- Drawdown complete: 2:45-3:30\n\nAdjust for taste: shorter for lighter, longer for stronger",
|
||||
"position":{"x":600,"y":450},
|
||||
"x":-80,
|
||||
"y":220,
|
||||
"width":300,
|
||||
"height":200,
|
||||
"color":"5"
|
||||
},
|
||||
{
|
||||
"id":"node-11",
|
||||
"type":"text",
|
||||
"text":"## Water Quality\n\n- Clean, filtered water\n- No strong odors or flavors\n- Ideal TDS: 75-150 ppm\n- Ideal pH: 7.0-7.5\n- Avoid distilled water (lacks minerals)\n- Avoid hard water (scaling issues)",
|
||||
"position":{"x":-600,"y":450},
|
||||
"x":-780,
|
||||
"y":-125,
|
||||
"width":300,
|
||||
"height":150,
|
||||
"color":"5"
|
||||
}
|
||||
],
|
||||
"edges":[
|
||||
{"id":"edge-1","fromNode":"node-1","fromSide":"bottom","toNode":"node-2","toSide":"top","label":"Step 1"},
|
||||
{"id":"edge-2","fromNode":"node-2","fromSide":"right","toNode":"node-3","toSide":"left","label":"Step 2"},
|
||||
{"id":"edge-3","fromNode":"node-3","fromSide":"right","toNode":"node-4","toSide":"left","label":"Step 3"},
|
||||
{"id":"edge-4","fromNode":"node-4","fromSide":"bottom","toNode":"node-5","toSide":"top","label":"Step 4"},
|
||||
{"id":"edge-5","fromNode":"node-5","fromSide":"bottom","toNode":"node-8","toSide":"top","label":"Step 5"},
|
||||
{"id":"edge-6","fromNode":"node-5","fromSide":"left","toNode":"node-7","toSide":"right","label":"Timing"},
|
||||
{"id":"edge-7","fromNode":"node-5","fromSide":"right","toNode":"node-6","toSide":"left","label":"Technique"},
|
||||
{"id":"edge-8","fromNode":"node-8","fromSide":"left","toNode":"node-9","toSide":"right","label":"if problems"},
|
||||
{"id":"edge-9","fromNode":"node-3","fromSide":"top","toNode":"node-10","toSide":"bottom","label":"Grinding details"},
|
||||
{"id":"edge-10","fromNode":"node-2","fromSide":"bottom","toNode":"node-11","toSide":"right","label":"Water details"},
|
||||
{"id":"edge-11","fromNode":"node-8","fromSide":"right","toNode":"node-12","toSide":"left","label":"Evaluate"},
|
||||
{"id":"edge-12","fromNode":"node-3","fromSide":"bottom","toNode":"node-13","toSide":"top","label":"Ratio details"}
|
||||
]
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
---
|
||||
title: Tasting Notes
|
||||
type: note
|
||||
permalink: coffee/tasting-notes
|
||||
tags:
|
||||
- '#coffee'
|
||||
- '#tasting'
|
||||
- '#flavor'
|
||||
- '#cupping'
|
||||
- '#demo'
|
||||
---
|
||||
|
||||
# Tasting Notes
|
||||
|
||||
How to taste and evaluate coffee, identify flavor characteristics, and develop a personal coffee palate.
|
||||
|
||||
## Overview
|
||||
|
||||
Coffee tasting, or "cupping" in professional contexts, is the practice of observing the tastes and aromas of brewed coffee. Developing a coffee palate helps identify preferences, communicate about coffee experiences, and better understand how brewing variables affect the cup.
|
||||
|
||||
## Observations
|
||||
|
||||
- [principle] Flavor perception includes taste, aroma, mouthfeel, and retronasal perception #sensory
|
||||
- [principle] Our taste buds can only perceive sweet, sour, salty, bitter, and umami #taste
|
||||
- [principle] Most of what we call "flavor" is actually aroma detected retronasally #aroma
|
||||
- [technique] Professional coffee tasting (cupping) uses a standardized protocol for consistency #cupping
|
||||
- [technique] Slurping coffee aerates it and spreads it across all taste receptors #technique
|
||||
- [technique] Allowing coffee to cool reveals different flavor notes at different temperatures #temperature
|
||||
|
||||
## Coffee Flavor Wheel
|
||||
|
||||
- [tool] The SCA Coffee Flavor Wheel provides a standardized vocabulary for describing coffee #flavor_wheel
|
||||
- [category] Primary categories include: Fruity, Floral, Sweet, Nutty/Cocoa, Spice, Roasted, Other #categories
|
||||
- [subcategory] Fruity breaks down into: Berry, Dried Fruit, Citrus Fruit, Stone Fruit, Tropical Fruit, etc. #fruit_notes
|
||||
- [subcategory] Floral includes: Floral, Black Tea, Chamomile, Rose, Jasmine, etc. #floral_notes
|
||||
- [subcategory] Sweet includes: Brown Sugar, Molasses, Honey, Maple Syrup, Vanilla, etc. #sweet_notes
|
||||
- [subcategory] Nutty/Cocoa includes: Nut, Cocoa, Dark Chocolate, Chocolate, etc. #nutty_notes
|
||||
- [subcategory] Spice includes: Brown Spice, Pepper, Anise, Nutmeg, Cinnamon, etc. #spice_notes
|
||||
|
||||
## Basic Tasting Components
|
||||
|
||||
- [component] Acidity: The bright, tangy quality (not sourness from under-extraction) #acidity
|
||||
- [component] Sweetness: The pleasant, sugary quality balancing other elements #sweetness
|
||||
- [component] Body: The physical mouthfeel and weight of the coffee #body
|
||||
- [component] Finish/Aftertaste: The flavor that lingers after swallowing #finish
|
||||
- [component] Balance: How well all elements work together #balance
|
||||
- [component] Complexity: The range and layers of distinct flavors #complexity
|
||||
- [component] Cleanliness: Absence of defects or off-flavors #cleanliness
|
||||
|
||||
## Common Flavor Notes by Origin
|
||||
|
||||
- [ethiopia] Blueberry, jasmine, bergamot, lemon, tea-like #flavor_notes
|
||||
- [kenya] Blackcurrant, grapefruit, tomato-like acidity, winey #flavor_notes
|
||||
- [colombia] Caramel, red apple, nuts, chocolate, balanced acidity #flavor_notes
|
||||
- [guatemala] Chocolate, spice, apple, medium acidity #flavor_notes
|
||||
- [brazil] Nuts, chocolate, low-to-medium acidity, full body #flavor_notes
|
||||
- [indonesia] Earthy, herbal, spice, cedar, full body, low acidity #flavor_notes
|
||||
- [costa_rica] Clean, bright, citrus, balanced, light chocolate #flavor_notes
|
||||
|
||||
## Developing Your Palate
|
||||
|
||||
- [technique] Taste coffees side-by-side to identify differences #comparison
|
||||
- [technique] Try describing flavors before looking at roaster's notes #blind_tasting
|
||||
- [technique] Keep a coffee journal with detailed notes about each coffee #journaling
|
||||
- [technique] Explore different processing methods of the same origin #processing
|
||||
- [technique] Try the same coffee brewed with different methods #brewing_comparison
|
||||
- [technique] Use reference flavors (actual fruits, chocolates, etc.) to calibrate your palate #calibration
|
||||
|
||||
## Personal Coffee Experiences
|
||||
|
||||
- [experience] Ethiopian Yirgacheffe prepared as pour over: intense blueberry, jasmine aromatics, tea-like body
|
||||
- [experience] Sumatra Mandheling in French press: earthy, cedar, herbal, tobacco, full body
|
||||
- [experience] Panama Gesha as pour over: intense floral notes, jasmine, bergamot, delicate body
|
||||
- [experience] Brazil Cerrado as espresso: nutty, chocolate, caramel, low acidity, great crema
|
||||
- [experience] Kenya AA as pour over: bright blackcurrant, tomato-like acidity, winey finish
|
||||
|
||||
## Relations
|
||||
|
||||
- determined_by [[Flavor Extraction]]
|
||||
- influenced_by [[Coffee Bean Origins]]
|
||||
- varies_with [[Coffee Brewing Methods]]
|
||||
- enhanced_by [[Proper Grinding Technique]]
|
||||
- documented_in [[Coffee Journal]]
|
||||
- part_of [[Coffee Knowledge Base]]
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
title: Test Note Creation - Basic Functionality
|
||||
type: note
|
||||
permalink: testing/test-note-creation-basic-functionality
|
||||
tags:
|
||||
- '["testing"'
|
||||
- '"core-functionality"'
|
||||
- '"note-creation"]'
|
||||
---
|
||||
|
||||
---
|
||||
title: Test Note Creation - Basic Functionality
|
||||
tags: [testing, core-functionality, note-creation, edited]
|
||||
test_status: active
|
||||
last_edited: 2025-06-01
|
||||
---
|
||||
|
||||
# Test Note Creation - Basic Functionality
|
||||
|
||||
## Test Status: COMPREHENSIVE TESTING IN PROGRESS
|
||||
Testing basic note creation with various content types and structures.
|
||||
|
||||
## Content Types Tested
|
||||
- Plain text content ✓
|
||||
- Markdown formatting **bold**, *italic*
|
||||
- Lists:
|
||||
- Bullet points
|
||||
- Numbered items
|
||||
- Code blocks: `inline code`
|
||||
|
||||
```python
|
||||
# Block code
|
||||
def test_function():
|
||||
return "Hello, Basic Memory!"
|
||||
```
|
||||
|
||||
## Special Characters
|
||||
- Unicode: café, naïve, résumé
|
||||
- Emojis: 🚀 🔬 📝
|
||||
- Symbols: @#$%^&*()
|
||||
|
||||
## Frontmatter Testing
|
||||
This note should have proper frontmatter parsing.
|
||||
|
||||
## Relations to Test
|
||||
- connects_to [[Another Test Note]]
|
||||
- validates [[Core Functionality Tests]]
|
||||
|
||||
## Observations
|
||||
- [success] Note creation initiated
|
||||
- [test] Content variety included
|
||||
- [validation] Special characters included
|
||||
|
||||
|
||||
## Edit Test Results
|
||||
- [success] Note reading via title lookup ✓
|
||||
- [success] Search functionality returns relevant results ✓
|
||||
- [success] Special characters (unicode, emojis) preserved ✓
|
||||
- [test] Now testing append edit operation ✓
|
||||
|
||||
## Performance Notes
|
||||
- Note creation: Instantaneous
|
||||
- Note reading: Fast response
|
||||
- Search: Good relevance scoring
|
||||
|
||||
## Next Tests
|
||||
- Edit operations (append, prepend, find_replace)
|
||||
- Move operations
|
||||
- Cross-project functionality
|
||||
@@ -3,6 +3,9 @@
|
||||
# Install dependencies
|
||||
install:
|
||||
pip install -e ".[dev]"
|
||||
uv sync
|
||||
@echo ""
|
||||
@echo "💡 Remember to activate the virtual environment by running: source .venv/bin/activate"
|
||||
|
||||
# Run unit tests in parallel
|
||||
test-unit:
|
||||
@@ -17,7 +20,7 @@ test: test-unit test-int
|
||||
|
||||
# Lint and fix code
|
||||
lint:
|
||||
ruff check . --fix
|
||||
uv run ruff check . --fix
|
||||
|
||||
# Type check code
|
||||
type-check:
|
||||
@@ -179,4 +182,4 @@ beta version:
|
||||
|
||||
# List all available recipes
|
||||
default:
|
||||
@just --list
|
||||
@just --list
|
||||
|
||||
@@ -30,7 +30,7 @@ dependencies = [
|
||||
"alembic>=1.14.1",
|
||||
"pillow>=11.1.0",
|
||||
"pybars3>=0.9.7",
|
||||
"fastmcp>=2.3.4",
|
||||
"fastmcp==2.10.2",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
@@ -52,7 +52,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["src", "tests"]
|
||||
addopts = "--cov=basic_memory --cov-report term-missing -ra -q"
|
||||
addopts = "--cov=basic_memory --cov-report term-missing"
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "strict"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
@@ -123,4 +123,4 @@ omit = [
|
||||
]
|
||||
|
||||
[tool.logfire]
|
||||
ignore_no_config = true
|
||||
ignore_no_config = true
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Welcome to Basic Memory installer"
|
||||
|
||||
# 1. Install uv if not present
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Installing uv package manager..."
|
||||
curl -LsSf https://github.com/astral-sh/uv/releases/download/0.1.23/uv-installer.sh | sh
|
||||
fi
|
||||
|
||||
# 2. Configure Claude Desktop
|
||||
echo "Configuring Claude Desktop..."
|
||||
CONFIG_FILE="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
||||
|
||||
# Create config directory if it doesn't exist
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
|
||||
# If config file doesn't exist, create it with initial structure
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo '{"mcpServers": {}}' > "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Add/update the basic-memory config using jq
|
||||
jq '.mcpServers."basic-memory" = {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory"]
|
||||
}' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
|
||||
|
||||
echo "Installation complete! Basic Memory is now available in Claude Desktop."
|
||||
echo "Please restart Claude Desktop for changes to take effect."
|
||||
|
||||
echo -e "\nQuick Start:"
|
||||
echo "1. You can run sync directly using: uvx basic-memory sync"
|
||||
echo "2. Optionally, install globally with: uv pip install basic-memory"
|
||||
echo -e "\nBuilt with ♥️ by Basic Machines."
|
||||
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.13.2"
|
||||
__version__ = "0.14.3"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -8,17 +8,19 @@ from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from basic_memory.models import Base
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
|
||||
os.environ["BASIC_MEMORY_ENV"] = "test"
|
||||
|
||||
from basic_memory.config import app_config
|
||||
# Import after setting environment variable # noqa: E402
|
||||
from basic_memory.models import Base # noqa: E402
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Set the SQLAlchemy URL from our app config
|
||||
sqlalchemy_url = f"sqlite:///{app_config.database_path}"
|
||||
config.set_main_option("sqlalchemy.url", sqlalchemy_url)
|
||||
|
||||
@@ -20,15 +20,18 @@ from basic_memory.api.routers import (
|
||||
search,
|
||||
prompt_router,
|
||||
)
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.services.initialization import initialize_app, initialize_file_sync
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI): # pragma: no cover
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Initialize app and database
|
||||
logger.info("Starting Basic Memory API")
|
||||
print(f"fastapi {app_config.projects}")
|
||||
await initialize_app(app_config)
|
||||
|
||||
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.deps import SyncServiceDep, ProjectRepositoryDep
|
||||
|
||||
router = APIRouter(prefix="/management", tags=["management"])
|
||||
@@ -41,6 +41,8 @@ async def start_watch_service(
|
||||
# Watch service is already running
|
||||
return WatchStatusResponse(running=True)
|
||||
|
||||
app_config = ConfigManager().config
|
||||
|
||||
# Create and start a new watch service
|
||||
logger.info("Starting watch service via management API")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Router for project management."""
|
||||
|
||||
import os
|
||||
from fastapi import APIRouter, HTTPException, Path, Body
|
||||
from typing import Optional
|
||||
|
||||
@@ -32,40 +33,47 @@ async def get_project_info(
|
||||
@project_router.patch("/{name}", response_model=ProjectStatusResponse)
|
||||
async def update_project(
|
||||
project_service: ProjectServiceDep,
|
||||
project_name: str = Path(..., description="Name of the project to update"),
|
||||
path: Optional[str] = Body(None, description="New path for the project"),
|
||||
name: str = Path(..., description="Name of the project to update"),
|
||||
path: Optional[str] = Body(None, description="New absolute path for the project"),
|
||||
is_active: Optional[bool] = Body(None, description="Status of the project (active/inactive)"),
|
||||
) -> ProjectStatusResponse:
|
||||
"""Update a project's information in configuration and database.
|
||||
|
||||
Args:
|
||||
project_name: The name of the project to update
|
||||
path: Optional new path for the project
|
||||
name: The name of the project to update
|
||||
path: Optional new absolute path for the project
|
||||
is_active: Optional status update for the project
|
||||
|
||||
Returns:
|
||||
Response confirming the project was updated
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
try:
|
||||
# Validate that path is absolute if provided
|
||||
if path and not os.path.isabs(path):
|
||||
raise HTTPException(status_code=400, detail="Path must be absolute")
|
||||
|
||||
# Get original project info for the response
|
||||
old_project_info = ProjectItem(
|
||||
name=project_name,
|
||||
path=project_service.projects.get(project_name, ""),
|
||||
name=name,
|
||||
path=project_service.projects.get(name, ""),
|
||||
)
|
||||
|
||||
await project_service.update_project(project_name, updated_path=path, is_active=is_active)
|
||||
if path:
|
||||
await project_service.move_project(name, path)
|
||||
elif is_active is not None:
|
||||
await project_service.update_project(name, is_active=is_active)
|
||||
|
||||
# Get updated project info
|
||||
updated_path = path if path else project_service.projects.get(project_name, "")
|
||||
updated_path = path if path else project_service.projects.get(name, "")
|
||||
|
||||
return ProjectStatusResponse(
|
||||
message=f"Project '{project_name}' updated successfully",
|
||||
message=f"Project '{name}' updated successfully",
|
||||
status="success",
|
||||
default=(project_name == project_service.default_project),
|
||||
default=(name == project_service.default_project),
|
||||
old_project=old_project_info,
|
||||
new_project=ProjectItem(name=project_name, path=updated_path),
|
||||
new_project=ProjectItem(name=name, path=updated_path),
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ async def to_graph_context(
|
||||
file_path=item.file_path,
|
||||
permalink=item.permalink, # pyright: ignore
|
||||
relation_type=item.relation_type, # pyright: ignore
|
||||
from_entity=from_entity.title, # pyright: ignore
|
||||
from_entity=from_entity.title if from_entity else None,
|
||||
to_entity=to_entity.title if to_entity else None,
|
||||
created_at=item.created_at,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.config import get_project_config, ConfigManager
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ def version_callback(value: bool) -> None:
|
||||
"""Show version and exit."""
|
||||
if value: # pragma: no cover
|
||||
import basic_memory
|
||||
from basic_memory.config import config
|
||||
|
||||
config = get_project_config()
|
||||
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
|
||||
typer.echo(f"Current project: {config.project}")
|
||||
typer.echo(f"Project path: {config.home}")
|
||||
@@ -44,9 +44,9 @@ def app_callback(
|
||||
|
||||
# Run initialization for every command unless --version was specified
|
||||
if not version and ctx.invoked_subcommand is not None:
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.services.initialization import ensure_initialization
|
||||
|
||||
app_config = ConfigManager().config
|
||||
ensure_initialization(app_config)
|
||||
|
||||
# Initialize MCP session with the specified project or default
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""CLI commands for basic-memory."""
|
||||
|
||||
from . import auth, status, sync, db, import_memory_json, mcp, import_claude_conversations
|
||||
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
|
||||
from . import import_claude_projects, import_chatgpt, tool, project
|
||||
|
||||
__all__ = [
|
||||
"auth",
|
||||
"status",
|
||||
"sync",
|
||||
"db",
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""OAuth management commands."""
|
||||
|
||||
import typer
|
||||
from typing import Optional
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
|
||||
|
||||
auth_app = typer.Typer(help="OAuth client management commands")
|
||||
app.add_typer(auth_app, name="auth")
|
||||
|
||||
|
||||
@auth_app.command()
|
||||
def register_client(
|
||||
client_id: Optional[str] = typer.Option(
|
||||
None, help="Client ID (auto-generated if not provided)"
|
||||
),
|
||||
client_secret: Optional[str] = typer.Option(
|
||||
None, help="Client secret (auto-generated if not provided)"
|
||||
),
|
||||
issuer_url: str = typer.Option("http://localhost:8000", help="OAuth issuer URL"),
|
||||
):
|
||||
"""Register a new OAuth client for Basic Memory MCP server."""
|
||||
|
||||
# Create provider instance
|
||||
provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
# Create client info with required redirect_uris
|
||||
client_info = OAuthClientInformationFull(
|
||||
client_id=client_id or "", # Provider will generate if empty
|
||||
client_secret=client_secret or "", # Provider will generate if empty
|
||||
redirect_uris=[AnyHttpUrl("http://localhost:8000/callback")], # Default redirect URI
|
||||
client_name="Basic Memory OAuth Client",
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
)
|
||||
|
||||
# Register the client
|
||||
import asyncio
|
||||
|
||||
asyncio.run(provider.register_client(client_info))
|
||||
|
||||
typer.echo("Client registered successfully!")
|
||||
typer.echo(f"Client ID: {client_info.client_id}")
|
||||
typer.echo(f"Client Secret: {client_info.client_secret}")
|
||||
typer.echo("\nSave these credentials securely - the client secret cannot be retrieved later.")
|
||||
|
||||
|
||||
@auth_app.command()
|
||||
def test_auth(
|
||||
issuer_url: str = typer.Option("http://localhost:8000", help="OAuth issuer URL"),
|
||||
):
|
||||
"""Test OAuth authentication flow.
|
||||
|
||||
IMPORTANT: Use the same FASTMCP_AUTH_SECRET_KEY environment variable
|
||||
as your MCP server for tokens to validate correctly.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import secrets
|
||||
from mcp.server.auth.provider import AuthorizationParams
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
async def test_flow():
|
||||
# Create provider with same secret key as server
|
||||
provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
# Register a test client
|
||||
client_info = OAuthClientInformationFull(
|
||||
client_id=secrets.token_urlsafe(16),
|
||||
client_secret=secrets.token_urlsafe(32),
|
||||
redirect_uris=[AnyHttpUrl("http://localhost:8000/callback")],
|
||||
client_name="Test OAuth Client",
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
)
|
||||
await provider.register_client(client_info)
|
||||
typer.echo(f"Registered test client: {client_info.client_id}")
|
||||
|
||||
# Get the client
|
||||
client = await provider.get_client(client_info.client_id)
|
||||
if not client:
|
||||
typer.echo("Error: Client not found after registration", err=True)
|
||||
return
|
||||
|
||||
# Create authorization request
|
||||
auth_params = AuthorizationParams(
|
||||
state="test-state",
|
||||
scopes=["read", "write"],
|
||||
code_challenge="test-challenge",
|
||||
redirect_uri=AnyHttpUrl("http://localhost:8000/callback"),
|
||||
redirect_uri_provided_explicitly=True,
|
||||
)
|
||||
|
||||
# Get authorization URL
|
||||
auth_url = await provider.authorize(client, auth_params)
|
||||
typer.echo(f"Authorization URL: {auth_url}")
|
||||
|
||||
# Extract auth code from URL
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
parsed = urlparse(auth_url)
|
||||
params = parse_qs(parsed.query)
|
||||
auth_code = params.get("code", [None])[0]
|
||||
|
||||
if not auth_code:
|
||||
typer.echo("Error: No authorization code in URL", err=True)
|
||||
return
|
||||
|
||||
# Load the authorization code
|
||||
code_obj = await provider.load_authorization_code(client, auth_code)
|
||||
if not code_obj:
|
||||
typer.echo("Error: Invalid authorization code", err=True)
|
||||
return
|
||||
|
||||
# Exchange for tokens
|
||||
token = await provider.exchange_authorization_code(client, code_obj)
|
||||
typer.echo(f"Access token: {token.access_token}")
|
||||
typer.echo(f"Refresh token: {token.refresh_token}")
|
||||
typer.echo(f"Expires in: {token.expires_in} seconds")
|
||||
|
||||
# Validate access token
|
||||
access_token_obj = await provider.load_access_token(token.access_token)
|
||||
if access_token_obj:
|
||||
typer.echo("Access token validated successfully!")
|
||||
typer.echo(f"Client ID: {access_token_obj.client_id}")
|
||||
typer.echo(f"Scopes: {access_token_obj.scopes}")
|
||||
else:
|
||||
typer.echo("Error: Invalid access token", err=True)
|
||||
|
||||
asyncio.run(test_flow())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auth_app()
|
||||
@@ -7,7 +7,7 @@ from loguru import logger
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager, BasicMemoryConfig, save_basic_memory_config
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -17,6 +17,8 @@ def reset(
|
||||
"""Reset database (drop all tables and recreate)."""
|
||||
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
||||
logger.info("Resetting database...")
|
||||
config_manager = ConfigManager()
|
||||
app_config = config_manager.config
|
||||
# Get database path
|
||||
db_path = app_config.app_database_path
|
||||
|
||||
@@ -25,6 +27,11 @@ def reset(
|
||||
db_path.unlink()
|
||||
logger.info(f"Database file deleted: {db_path}")
|
||||
|
||||
# Reset project configuration
|
||||
config = BasicMemoryConfig()
|
||||
save_basic_memory_config(config_manager.config_file, config)
|
||||
logger.info("Project configuration reset to default")
|
||||
|
||||
# Create a new empty database
|
||||
asyncio.run(db.run_migrations(app_config))
|
||||
logger.info("Database reset complete")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers import ChatGPTImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -49,7 +50,7 @@ def import_chatgpt(
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
config = get_project_config()
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -42,6 +43,7 @@ def import_claude(
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
config = get_project_config()
|
||||
try:
|
||||
if not conversations_json.exists():
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -41,6 +42,7 @@ def import_projects(
|
||||
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
config = get_project_config()
|
||||
try:
|
||||
if not projects_json.exists():
|
||||
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -46,6 +47,7 @@ def memory_json(
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
config = get_project_config()
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
@@ -74,7 +76,8 @@ def memory_json(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {result.entities} entities\n"
|
||||
f"Added {result.relations} relations",
|
||||
f"Added {result.relations} relations\n"
|
||||
f"Skipped {result.skipped_entities} entities\n",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import typer
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
# Import mcp instance
|
||||
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
||||
@@ -34,26 +35,13 @@ def mcp(
|
||||
- sse: Server-Sent Events (for compatibility with existing clients)
|
||||
"""
|
||||
|
||||
# Check if OAuth is enabled
|
||||
import os
|
||||
|
||||
auth_enabled = os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true"
|
||||
if auth_enabled:
|
||||
logger.info("OAuth authentication is ENABLED")
|
||||
logger.info(f"Issuer URL: {os.getenv('FASTMCP_AUTH_ISSUER_URL', 'http://localhost:8000')}")
|
||||
if os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES"):
|
||||
logger.info(f"Required scopes: {os.getenv('FASTMCP_AUTH_REQUIRED_SCOPES')}")
|
||||
else:
|
||||
logger.info("OAuth authentication is DISABLED")
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.services.initialization import initialize_file_sync
|
||||
|
||||
# Start the MCP server with the specified transport
|
||||
|
||||
# Use unified thread-based sync approach for both transports
|
||||
import threading
|
||||
|
||||
app_config = ConfigManager().config
|
||||
|
||||
def run_file_sync():
|
||||
"""Run file sync in a separate thread with its own event loop."""
|
||||
loop = asyncio.new_event_loop()
|
||||
@@ -85,4 +73,5 @@ def mcp(
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level="INFO",
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from basic_memory.mcp.tools.utils import call_post
|
||||
from basic_memory.schemas.project_info import ProjectStatusResponse
|
||||
from basic_memory.mcp.tools.utils import call_delete
|
||||
from basic_memory.mcp.tools.utils import call_put
|
||||
from basic_memory.mcp.tools.utils import call_patch
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
console = Console()
|
||||
@@ -120,7 +121,7 @@ def set_default_project(
|
||||
try:
|
||||
project_name = generate_permalink(name)
|
||||
|
||||
response = asyncio.run(call_put(client, f"projects/{project_name}/default"))
|
||||
response = asyncio.run(call_put(client, f"/projects/{project_name}/default"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
@@ -128,12 +129,8 @@ def set_default_project(
|
||||
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Reload configuration to apply the change
|
||||
from importlib import reload
|
||||
from basic_memory import config as config_module
|
||||
|
||||
reload(config_module)
|
||||
|
||||
# The API call above should have updated both config and MCP session
|
||||
# No need for manual reload - the project service handles this automatically
|
||||
console.print("[green]Project activated for current session[/green]")
|
||||
|
||||
|
||||
@@ -152,6 +149,46 @@ def synchronize_projects() -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@project_app.command("move")
|
||||
def move_project(
|
||||
name: str = typer.Argument(..., help="Name of the project to move"),
|
||||
new_path: str = typer.Argument(..., help="New absolute path for the project"),
|
||||
) -> None:
|
||||
"""Move a project to a new location."""
|
||||
# Resolve to absolute path
|
||||
resolved_path = os.path.abspath(os.path.expanduser(new_path))
|
||||
|
||||
try:
|
||||
data = {"path": resolved_path}
|
||||
project_name = generate_permalink(name)
|
||||
|
||||
current_project = session.get_current_project()
|
||||
response = asyncio.run(
|
||||
call_patch(client, f"/{current_project}/project/{project_name}", json=data)
|
||||
)
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
|
||||
# Show important file movement reminder
|
||||
console.print() # Empty line for spacing
|
||||
console.print(
|
||||
Panel(
|
||||
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
|
||||
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
|
||||
f"[cyan]{resolved_path}[/cyan]\n\n"
|
||||
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
|
||||
title="⚠️ Manual File Movement Required",
|
||||
border_style="yellow",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error moving project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@project_app.command("info")
|
||||
def display_project_info(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
||||
|
||||
@@ -12,7 +12,7 @@ from rich.tree import Tree
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.cli.commands.sync import get_sync_service
|
||||
from basic_memory.config import config, app_config
|
||||
from basic_memory.config import ConfigManager, get_project_config
|
||||
from basic_memory.repository import ProjectRepository
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
|
||||
@@ -126,6 +126,9 @@ async def run_status(verbose: bool = False): # pragma: no cover
|
||||
"""Check sync status of files vs database."""
|
||||
# Check knowledge/ directory
|
||||
|
||||
app_config = ConfigManager().config
|
||||
config = get_project_config()
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.tree import Tree
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import ConfigManager, get_project_config
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.models import Project
|
||||
@@ -29,7 +29,6 @@ from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import SyncService
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
from basic_memory.config import app_config
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -42,6 +41,8 @@ class ValidationIssue:
|
||||
|
||||
async def get_sync_service(project: Project) -> SyncService: # pragma: no cover
|
||||
"""Get sync service instance with all dependencies."""
|
||||
|
||||
app_config = ConfigManager().config
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
@@ -96,6 +97,7 @@ def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[V
|
||||
|
||||
def display_sync_summary(knowledge: SyncReport):
|
||||
"""Display a one-line summary of sync changes."""
|
||||
config = get_project_config()
|
||||
total_changes = knowledge.total
|
||||
project_name = config.project
|
||||
|
||||
@@ -124,6 +126,7 @@ def display_sync_summary(knowledge: SyncReport):
|
||||
|
||||
def display_detailed_sync_results(knowledge: SyncReport):
|
||||
"""Display detailed sync results with trees."""
|
||||
config = get_project_config()
|
||||
project_name = config.project
|
||||
|
||||
if knowledge.total == 0:
|
||||
@@ -158,6 +161,9 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
||||
|
||||
async def run_sync(verbose: bool = False):
|
||||
"""Run sync operation."""
|
||||
app_config = ConfigManager().config
|
||||
config = get_project_config()
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
@@ -212,6 +218,8 @@ def sync(
|
||||
),
|
||||
) -> None:
|
||||
"""Sync knowledge files with the database."""
|
||||
config = get_project_config()
|
||||
|
||||
try:
|
||||
# Show which project we're syncing
|
||||
typer.echo(f"Syncing project: {config.project}")
|
||||
|
||||
@@ -4,7 +4,6 @@ from basic_memory.cli.app import app # pragma: no cover
|
||||
|
||||
# Register commands
|
||||
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
||||
auth,
|
||||
db,
|
||||
import_chatgpt,
|
||||
import_claude_conversations,
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Literal, Optional, List
|
||||
from typing import Any, Dict, Literal, Optional, List, Tuple
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import Field, field_validator
|
||||
@@ -45,7 +45,9 @@ class BasicMemoryConfig(BaseSettings):
|
||||
env: Environment = Field(default="dev", description="Environment name")
|
||||
|
||||
projects: Dict[str, str] = Field(
|
||||
default_factory=lambda: {"main": str(Path.home() / "basic-memory")},
|
||||
default_factory=lambda: {
|
||||
"main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
|
||||
},
|
||||
description="Mapping of project names to their filesystem paths",
|
||||
)
|
||||
default_project: str = Field(
|
||||
@@ -72,6 +74,12 @@ class BasicMemoryConfig(BaseSettings):
|
||||
description="Whether to sync changes in real time. default (True)",
|
||||
)
|
||||
|
||||
# API connection configuration
|
||||
api_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="URL of remote Basic Memory API. If set, MCP will connect to this API instead of using local ASGI transport.",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BASIC_MEMORY_",
|
||||
extra="ignore",
|
||||
@@ -92,7 +100,9 @@ class BasicMemoryConfig(BaseSettings):
|
||||
"""Ensure configuration is valid after initialization."""
|
||||
# Ensure main project exists
|
||||
if "main" not in self.projects: # pragma: no cover
|
||||
self.projects["main"] = str(Path.home() / "basic-memory")
|
||||
self.projects["main"] = str(
|
||||
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
|
||||
)
|
||||
|
||||
# Ensure default project is valid
|
||||
if self.default_project not in self.projects: # pragma: no cover
|
||||
@@ -120,6 +130,7 @@ class BasicMemoryConfig(BaseSettings):
|
||||
"""
|
||||
|
||||
# Load the app-level database path from the global config
|
||||
config_manager = ConfigManager()
|
||||
config = config_manager.load_config() # pragma: no cover
|
||||
return config.app_database_path # pragma: no cover
|
||||
|
||||
@@ -158,20 +169,21 @@ class ConfigManager:
|
||||
# Ensure config directory exists
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load or create configuration
|
||||
self.config = self.load_config()
|
||||
@property
|
||||
def config(self) -> BasicMemoryConfig:
|
||||
"""Get configuration, loading it lazily if needed."""
|
||||
return self.load_config()
|
||||
|
||||
def load_config(self) -> BasicMemoryConfig:
|
||||
"""Load configuration from file or create default."""
|
||||
|
||||
if self.config_file.exists():
|
||||
try:
|
||||
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
||||
return BasicMemoryConfig(**data)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
config = BasicMemoryConfig()
|
||||
self.save_config(config)
|
||||
return config
|
||||
logger.exception(f"Failed to load config: {e}")
|
||||
raise e
|
||||
else:
|
||||
config = BasicMemoryConfig()
|
||||
self.save_config(config)
|
||||
@@ -179,10 +191,7 @@ class ConfigManager:
|
||||
|
||||
def save_config(self, config: BasicMemoryConfig) -> None:
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
self.config_file.write_text(json.dumps(config.model_dump(), indent=2))
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
save_basic_memory_config(self.config_file, config)
|
||||
|
||||
@property
|
||||
def projects(self) -> Dict[str, str]:
|
||||
@@ -196,35 +205,54 @@ class ConfigManager:
|
||||
|
||||
def add_project(self, name: str, path: str) -> ProjectConfig:
|
||||
"""Add a new project to the configuration."""
|
||||
if name in self.config.projects: # pragma: no cover
|
||||
project_name, _ = self.get_project(name)
|
||||
if project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' already exists")
|
||||
|
||||
# Ensure the path exists
|
||||
project_path = Path(path)
|
||||
project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
||||
|
||||
self.config.projects[name] = str(project_path)
|
||||
self.save_config(self.config)
|
||||
# Load config, modify it, and save it
|
||||
config = self.load_config()
|
||||
config.projects[name] = str(project_path)
|
||||
self.save_config(config)
|
||||
return ProjectConfig(name=name, home=project_path)
|
||||
|
||||
def remove_project(self, name: str) -> None:
|
||||
"""Remove a project from the configuration."""
|
||||
if name not in self.config.projects: # pragma: no cover
|
||||
|
||||
project_name, path = self.get_project(name)
|
||||
if not project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' not found")
|
||||
|
||||
if name == self.config.default_project: # pragma: no cover
|
||||
# Load config, check, modify, and save
|
||||
config = self.load_config()
|
||||
if project_name == config.default_project: # pragma: no cover
|
||||
raise ValueError(f"Cannot remove the default project '{name}'")
|
||||
|
||||
del self.config.projects[name]
|
||||
self.save_config(self.config)
|
||||
del config.projects[name]
|
||||
self.save_config(config)
|
||||
|
||||
def set_default_project(self, name: str) -> None:
|
||||
"""Set the default project."""
|
||||
if name not in self.config.projects: # pragma: no cover
|
||||
project_name, path = self.get_project(name)
|
||||
if not project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' not found")
|
||||
|
||||
self.config.default_project = name
|
||||
self.save_config(self.config)
|
||||
# Load config, modify, and save
|
||||
config = self.load_config()
|
||||
config.default_project = name
|
||||
self.save_config(config)
|
||||
|
||||
def get_project(self, name: str) -> Tuple[str, str] | Tuple[None, None]:
|
||||
"""Look up a project from the configuration by name or permalink"""
|
||||
project_permalink = generate_permalink(name)
|
||||
app_config = self.config
|
||||
for project_name, path in app_config.projects.items():
|
||||
if project_permalink == generate_permalink(project_name):
|
||||
return project_name, path
|
||||
return None, None
|
||||
|
||||
|
||||
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
@@ -236,7 +264,7 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
actual_project_name = None
|
||||
|
||||
# load the config from file
|
||||
global app_config
|
||||
config_manager = ConfigManager()
|
||||
app_config = config_manager.load_config()
|
||||
|
||||
# Get project name from environment variable
|
||||
@@ -256,21 +284,22 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
# the config contains a dict[str,str] of project names and absolute paths
|
||||
assert actual_project_name is not None, "actual_project_name cannot be None"
|
||||
|
||||
project_path = app_config.projects.get(actual_project_name)
|
||||
if not project_path: # pragma: no cover
|
||||
raise ValueError(f"Project '{actual_project_name}' not found")
|
||||
project_permalink = generate_permalink(actual_project_name)
|
||||
|
||||
return ProjectConfig(name=actual_project_name, home=Path(project_path))
|
||||
for name, path in app_config.projects.items():
|
||||
if project_permalink == generate_permalink(name):
|
||||
return ProjectConfig(name=name, home=Path(path))
|
||||
|
||||
# otherwise raise error
|
||||
raise ValueError(f"Project '{actual_project_name}' not found") # pragma: no cover
|
||||
|
||||
|
||||
# Create config manager
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Export the app-level config
|
||||
app_config: BasicMemoryConfig = config_manager.config
|
||||
|
||||
# Load project config for the default project (backward compatibility)
|
||||
config: ProjectConfig = get_project_config()
|
||||
def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None:
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
file_path.write_text(json.dumps(config.model_dump(), indent=2))
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
|
||||
|
||||
def update_current_project(project_name: str) -> None:
|
||||
@@ -322,12 +351,17 @@ def setup_basic_memory_logging(): # pragma: no cover
|
||||
# print("Skipping duplicate logging setup")
|
||||
return
|
||||
|
||||
# Check for console logging environment variable
|
||||
console_logging = os.getenv("BASIC_MEMORY_CONSOLE_LOGGING", "false").lower() == "true"
|
||||
|
||||
config_manager = ConfigManager()
|
||||
config = get_project_config()
|
||||
setup_logging(
|
||||
env=config_manager.config.env,
|
||||
home_dir=user_home, # Use user home for logs
|
||||
log_level=config_manager.load_config().log_level,
|
||||
log_level=config_manager.config.log_level,
|
||||
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
||||
console=False,
|
||||
console=console_logging,
|
||||
)
|
||||
|
||||
logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
@@ -23,6 +23,7 @@ from basic_memory.repository.search_repository import SearchRepository
|
||||
# Module level state
|
||||
_engine: Optional[AsyncEngine] = None
|
||||
_session_maker: Optional[async_sessionmaker[AsyncSession]] = None
|
||||
_migrations_completed: bool = False
|
||||
|
||||
|
||||
class DatabaseType(Enum):
|
||||
@@ -72,18 +73,32 @@ async def scoped_session(
|
||||
await factory.remove()
|
||||
|
||||
|
||||
def _create_engine_and_session(
|
||||
db_path: Path, db_type: DatabaseType = DatabaseType.FILESYSTEM
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
||||
"""Internal helper to create engine and session maker."""
|
||||
db_url = DatabaseType.get_db_url(db_path, db_type)
|
||||
logger.debug(f"Creating engine for db_url: {db_url}")
|
||||
engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
||||
session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
return engine, session_maker
|
||||
|
||||
|
||||
async def get_or_create_db(
|
||||
db_path: Path,
|
||||
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
||||
ensure_migrations: bool = True,
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
||||
"""Get or create database engine and session maker."""
|
||||
global _engine, _session_maker
|
||||
|
||||
if _engine is None:
|
||||
db_url = DatabaseType.get_db_url(db_path, db_type)
|
||||
logger.debug(f"Creating engine for db_url: {db_url}")
|
||||
_engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
_engine, _session_maker = _create_engine_and_session(db_path, db_type)
|
||||
|
||||
# Run migrations automatically unless explicitly disabled
|
||||
if ensure_migrations:
|
||||
app_config = ConfigManager().config
|
||||
await run_migrations(app_config, db_type)
|
||||
|
||||
# These checks should never fail since we just created the engine and session maker
|
||||
# if they were None, but we'll check anyway for the type checker
|
||||
@@ -100,12 +115,13 @@ async def get_or_create_db(
|
||||
|
||||
async def shutdown_db() -> None: # pragma: no cover
|
||||
"""Clean up database connections."""
|
||||
global _engine, _session_maker
|
||||
global _engine, _session_maker, _migrations_completed
|
||||
|
||||
if _engine:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
_migrations_completed = False
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -119,7 +135,7 @@ async def engine_session_factory(
|
||||
for each test. For production use, use get_or_create_db() instead.
|
||||
"""
|
||||
|
||||
global _engine, _session_maker
|
||||
global _engine, _session_maker, _migrations_completed
|
||||
|
||||
db_url = DatabaseType.get_db_url(db_path, db_type)
|
||||
logger.debug(f"Creating engine for db_url: {db_url}")
|
||||
@@ -143,12 +159,20 @@ async def engine_session_factory(
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
_migrations_completed = False
|
||||
|
||||
|
||||
async def run_migrations(
|
||||
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
|
||||
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM, force: bool = False
|
||||
): # pragma: no cover
|
||||
"""Run any pending alembic migrations."""
|
||||
global _migrations_completed
|
||||
|
||||
# Skip if migrations already completed unless forced
|
||||
if _migrations_completed and not force:
|
||||
logger.debug("Migrations already completed in this session, skipping")
|
||||
return
|
||||
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
# Get the absolute path to the alembic directory relative to this file
|
||||
@@ -170,11 +194,18 @@ async def run_migrations(
|
||||
command.upgrade(config, "head")
|
||||
logger.info("Migrations completed successfully")
|
||||
|
||||
_, session_maker = await get_or_create_db(app_config.database_path, database_type)
|
||||
# Get session maker - ensure we don't trigger recursive migration calls
|
||||
if _session_maker is None:
|
||||
_, session_maker = _create_engine_and_session(app_config.database_path, database_type)
|
||||
else:
|
||||
session_maker = _session_maker
|
||||
|
||||
# initialize the search Index schema
|
||||
# the project_id is not used for init_search_index, so we pass a dummy value
|
||||
await SearchRepository(session_maker, 1).init_search_index()
|
||||
|
||||
# Mark migrations as completed
|
||||
_migrations_completed = True
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Error running migrations: {e}")
|
||||
raise
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
import pathlib
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig, ConfigManager
|
||||
from basic_memory.importers import (
|
||||
ChatGPTImporter,
|
||||
ClaudeConversationsImporter,
|
||||
@@ -33,10 +33,10 @@ from basic_memory.services.file_service import FileService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import SyncService
|
||||
from basic_memory.config import app_config
|
||||
|
||||
|
||||
def get_app_config() -> BasicMemoryConfig: # pragma: no cover
|
||||
app_config = ConfigManager().config
|
||||
return app_config
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
|
||||
|
||||
|
||||
async def get_sync_service(
|
||||
app_config: AppConfigDep,
|
||||
entity_service: EntityServiceDep,
|
||||
entity_parser: EntityParserDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
|
||||
@@ -193,7 +193,7 @@ class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
def _traverse_messages(
|
||||
self, mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
|
||||
) -> List[Dict[str, Any]]: # pragma: no cover
|
||||
"""Traverse message tree and return messages in order.
|
||||
"""Traverse message tree iteratively to handle deep conversations.
|
||||
|
||||
Args:
|
||||
mapping: Message mapping.
|
||||
@@ -204,19 +204,29 @@ class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
List of message data.
|
||||
"""
|
||||
messages = []
|
||||
node = mapping.get(root_id) if root_id else None
|
||||
if not root_id:
|
||||
return messages
|
||||
|
||||
while node:
|
||||
# Use iterative approach with stack to avoid recursion depth issues
|
||||
stack = [root_id]
|
||||
|
||||
while stack:
|
||||
node_id = stack.pop()
|
||||
if not node_id:
|
||||
continue
|
||||
|
||||
node = mapping.get(node_id)
|
||||
if not node:
|
||||
continue
|
||||
|
||||
# Process current node if it has a message and hasn't been seen
|
||||
if node["id"] not in seen and node.get("message"):
|
||||
seen.add(node["id"])
|
||||
messages.append(node["message"])
|
||||
|
||||
# Follow children
|
||||
# Add children to stack in reverse order to maintain conversation flow
|
||||
children = node.get("children", [])
|
||||
for child_id in children:
|
||||
child_msgs = self._traverse_messages(mapping, child_id, seen)
|
||||
messages.extend(child_msgs)
|
||||
|
||||
break # Don't follow siblings
|
||||
for child_id in reversed(children):
|
||||
stack.append(child_id)
|
||||
|
||||
return messages
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.schemas.importer import EntityImportResult
|
||||
@@ -27,10 +27,12 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
Returns:
|
||||
EntityImportResult containing statistics and status of the import.
|
||||
"""
|
||||
config = get_project_config()
|
||||
try:
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
skipped_entities: int = 0
|
||||
|
||||
# Ensure the base path exists
|
||||
base_path = config.home # pragma: no cover
|
||||
@@ -41,7 +43,13 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
for line in source_data:
|
||||
data = line
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
# Handle different possible name keys
|
||||
entity_name = data.get("name") or data.get("entityName") or data.get("id")
|
||||
if not entity_name:
|
||||
logger.warning(f"Entity missing name field: {data}")
|
||||
skipped_entities += 1
|
||||
continue
|
||||
entities[entity_name] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data.get("from") or data.get("from_id")
|
||||
@@ -57,25 +65,31 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
# Second pass - create and write entities
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
# Get entity type with fallback
|
||||
entity_type = entity_data.get("entityType") or entity_data.get("type") or "entity"
|
||||
|
||||
# Ensure entity type directory exists
|
||||
entity_type_dir = base_path / entity_data["entityType"]
|
||||
entity_type_dir = base_path / entity_type
|
||||
entity_type_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get observations with fallback to empty list
|
||||
observations = entity_data.get("observations", [])
|
||||
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"type": entity_type,
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}",
|
||||
"permalink": f"{entity_type}/{name}",
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[Observation(content=obs) for obs in entity_data["observations"]],
|
||||
observations=[Observation(content=obs) for obs in observations],
|
||||
relations=entity_relations.get(name, []),
|
||||
)
|
||||
|
||||
# Write entity file
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
file_path = base_path / f"{entity_type}/{name}.md"
|
||||
await self.write_entity(entity, file_path)
|
||||
entities_created += 1
|
||||
|
||||
@@ -86,6 +100,7 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
success=True,
|
||||
entities=entities_created,
|
||||
relations=relations_count,
|
||||
skipped_entities=skipped_entities,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
|
||||
@@ -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,8 +1,28 @@
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
|
||||
def create_client() -> AsyncClient:
|
||||
"""Create an HTTP client based on configuration.
|
||||
|
||||
Returns:
|
||||
AsyncClient configured for either local ASGI or remote HTTP transport
|
||||
"""
|
||||
config_manager = ConfigManager()
|
||||
config = config_manager.load_config()
|
||||
|
||||
if config.api_url:
|
||||
# Use HTTP transport for remote API
|
||||
logger.info(f"Creating HTTP client for remote Basic Memory API: {config.api_url}")
|
||||
return AsyncClient(base_url=config.api_url)
|
||||
else:
|
||||
# Use ASGI transport for local API
|
||||
logger.debug("Creating ASGI client for local Basic Memory API")
|
||||
return AsyncClient(transport=ASGITransport(app=fastapi_app), base_url="http://test")
|
||||
|
||||
BASE_URL = "http://test"
|
||||
|
||||
# Create shared async client
|
||||
client = AsyncClient(transport=ASGITransport(app=fastapi_app), base_url=BASE_URL)
|
||||
client = create_client()
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
"""OAuth authentication provider for Basic Memory MCP server."""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional
|
||||
|
||||
import jwt
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class BasicMemoryAuthorizationCode(AuthorizationCode):
|
||||
"""Extended authorization code with additional metadata."""
|
||||
|
||||
issuer_state: Optional[str] = None
|
||||
|
||||
|
||||
class BasicMemoryRefreshToken(RefreshToken):
|
||||
"""Extended refresh token with additional metadata."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BasicMemoryAccessToken(AccessToken):
|
||||
"""Extended access token with additional metadata."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BasicMemoryOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
BasicMemoryAuthorizationCode, BasicMemoryRefreshToken, BasicMemoryAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider for Basic Memory MCP server.
|
||||
|
||||
This is a simple in-memory implementation that can be extended
|
||||
to integrate with external OAuth providers or use persistent storage.
|
||||
"""
|
||||
|
||||
def __init__(self, issuer_url: str = "http://localhost:8000", secret_key: Optional[str] = None):
|
||||
self.issuer_url = issuer_url
|
||||
# Use environment variable for secret key if available, otherwise generate
|
||||
import os
|
||||
|
||||
self.secret_key = (
|
||||
secret_key or os.getenv("FASTMCP_AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
|
||||
)
|
||||
|
||||
# In-memory storage - in production, use a proper database
|
||||
self.clients: Dict[str, OAuthClientInformationFull] = {}
|
||||
self.authorization_codes: Dict[str, BasicMemoryAuthorizationCode] = {}
|
||||
self.refresh_tokens: Dict[str, BasicMemoryRefreshToken] = {}
|
||||
self.access_tokens: Dict[str, BasicMemoryAccessToken] = {}
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client by ID."""
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client."""
|
||||
# Generate client ID if not provided
|
||||
if not client_info.client_id:
|
||||
client_info.client_id = secrets.token_urlsafe(16)
|
||||
|
||||
# Generate client secret if not provided
|
||||
if not client_info.client_secret:
|
||||
client_info.client_secret = secrets.token_urlsafe(32)
|
||||
|
||||
self.clients[client_info.client_id] = client_info
|
||||
logger.info(f"Registered OAuth client: {client_info.client_id}")
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create an authorization URL for the OAuth flow.
|
||||
|
||||
For basic-memory, we'll implement a simple authorization flow.
|
||||
In production, this might redirect to an external provider.
|
||||
"""
|
||||
# Generate authorization code
|
||||
auth_code = secrets.token_urlsafe(32)
|
||||
|
||||
# Store authorization code with metadata
|
||||
self.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
|
||||
code=auth_code,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
issuer_state=params.state,
|
||||
)
|
||||
|
||||
# In a real implementation, we'd redirect to an authorization page
|
||||
# For now, we'll just return the redirect URL with the code
|
||||
redirect_uri = str(params.redirect_uri)
|
||||
separator = "&" if "?" in redirect_uri else "?"
|
||||
|
||||
auth_url = f"{redirect_uri}{separator}code={auth_code}"
|
||||
if params.state:
|
||||
auth_url += f"&state={params.state}"
|
||||
|
||||
return auth_url
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[BasicMemoryAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.authorization_codes.get(authorization_code)
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check if expired
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
del self.authorization_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: BasicMemoryAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange an authorization code for tokens."""
|
||||
# Generate tokens
|
||||
access_token = self._generate_access_token(client.client_id, authorization_code.scopes)
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[access_token] = BasicMemoryAccessToken(
|
||||
token=access_token,
|
||||
client_id=client.client_id,
|
||||
scopes=authorization_code.scopes,
|
||||
expires_at=int(expires_at),
|
||||
)
|
||||
|
||||
self.refresh_tokens[refresh_token] = BasicMemoryRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=authorization_code.scopes,
|
||||
)
|
||||
|
||||
# Remove used authorization code
|
||||
del self.authorization_codes[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=3600, # 1 hour
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[BasicMemoryRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token = self.refresh_tokens.get(refresh_token)
|
||||
|
||||
if token and token.client_id == client.client_id:
|
||||
return token
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: BasicMemoryRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange a refresh token for new tokens."""
|
||||
# Use requested scopes or original scopes
|
||||
token_scopes = scopes if scopes else refresh_token.scopes
|
||||
|
||||
# Generate new tokens
|
||||
new_access_token = self._generate_access_token(client.client_id, token_scopes)
|
||||
new_refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store new tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[new_access_token] = BasicMemoryAccessToken(
|
||||
token=new_access_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_scopes,
|
||||
expires_at=int(expires_at),
|
||||
)
|
||||
|
||||
self.refresh_tokens[new_refresh_token] = BasicMemoryRefreshToken(
|
||||
token=new_refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_scopes,
|
||||
)
|
||||
|
||||
# Remove old tokens
|
||||
del self.refresh_tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=3600, # 1 hour
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(token_scopes) if token_scopes else None,
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[BasicMemoryAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
logger.debug("Loading access token, checking in-memory store first")
|
||||
access_token = self.access_tokens.get(token)
|
||||
|
||||
if access_token:
|
||||
# Check if expired
|
||||
if access_token.expires_at and datetime.utcnow().timestamp() > access_token.expires_at:
|
||||
logger.debug("Token found in memory but expired, removing")
|
||||
del self.access_tokens[token]
|
||||
return None
|
||||
logger.debug("Token found in memory and valid")
|
||||
return access_token
|
||||
|
||||
# Try to decode as JWT
|
||||
logger.debug("Token not in memory, attempting JWT decode with secret key")
|
||||
try:
|
||||
# Decode with audience verification - PyJWT expects the audience to match
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self.secret_key,
|
||||
algorithms=["HS256"],
|
||||
audience="basic-memory", # Expecting this audience
|
||||
issuer=self.issuer_url, # And this issuer
|
||||
)
|
||||
logger.debug(f"JWT decoded successfully: {payload}")
|
||||
return BasicMemoryAccessToken(
|
||||
token=token,
|
||||
client_id=payload.get("sub", ""),
|
||||
scopes=payload.get("scopes", []),
|
||||
expires_at=payload.get("exp"),
|
||||
)
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.error(f"JWT decode failed: {e}")
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: BasicMemoryAccessToken | BasicMemoryRefreshToken) -> None:
|
||||
"""Revoke an access or refresh token."""
|
||||
if isinstance(token, BasicMemoryAccessToken):
|
||||
self.access_tokens.pop(token.token, None)
|
||||
else:
|
||||
self.refresh_tokens.pop(token.token, None)
|
||||
|
||||
def _generate_access_token(self, client_id: str, scopes: list[str]) -> str:
|
||||
"""Generate a JWT access token."""
|
||||
payload = {
|
||||
"iss": self.issuer_url,
|
||||
"sub": client_id,
|
||||
"aud": "basic-memory",
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
"scopes": scopes,
|
||||
}
|
||||
|
||||
return jwt.encode(payload, self.secret_key, algorithm="HS256")
|
||||
@@ -1,321 +0,0 @@
|
||||
"""External OAuth provider integration for Basic Memory MCP server."""
|
||||
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
construct_redirect_uri,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalAuthorizationCode(AuthorizationCode):
|
||||
"""Authorization code with external provider metadata."""
|
||||
|
||||
external_code: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalRefreshToken(RefreshToken):
|
||||
"""Refresh token with external provider metadata."""
|
||||
|
||||
external_token: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalAccessToken(AccessToken):
|
||||
"""Access token with external provider metadata."""
|
||||
|
||||
external_token: Optional[str] = None
|
||||
|
||||
|
||||
class ExternalOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
ExternalAuthorizationCode, ExternalRefreshToken, ExternalAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider that delegates to external OAuth providers.
|
||||
|
||||
This provider can integrate with services like:
|
||||
- GitHub OAuth
|
||||
- Google OAuth
|
||||
- Auth0
|
||||
- Okta
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
issuer_url: str,
|
||||
external_provider: str,
|
||||
external_client_id: str,
|
||||
external_client_secret: str,
|
||||
external_authorize_url: str,
|
||||
external_token_url: str,
|
||||
external_userinfo_url: Optional[str] = None,
|
||||
):
|
||||
self.issuer_url = issuer_url
|
||||
self.external_provider = external_provider
|
||||
self.external_client_id = external_client_id
|
||||
self.external_client_secret = external_client_secret
|
||||
self.external_authorize_url = external_authorize_url
|
||||
self.external_token_url = external_token_url
|
||||
self.external_userinfo_url = external_userinfo_url
|
||||
|
||||
# In-memory storage - in production, use a database
|
||||
self.clients: Dict[str, OAuthClientInformationFull] = {}
|
||||
self.codes: Dict[str, ExternalAuthorizationCode] = {}
|
||||
self.tokens: Dict[str, Any] = {}
|
||||
|
||||
self.http_client = httpx.AsyncClient()
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client by ID."""
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client."""
|
||||
self.clients[client_info.client_id] = client_info
|
||||
logger.info(f"Registered external OAuth client: {client_info.client_id}")
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create authorization URL redirecting to external provider."""
|
||||
# Store authorization request
|
||||
import secrets
|
||||
|
||||
state = secrets.token_urlsafe(32)
|
||||
|
||||
self.codes[state] = ExternalAuthorizationCode(
|
||||
code=state,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=0, # Will be set by external provider
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
state=params.state,
|
||||
)
|
||||
|
||||
# Build external provider URL
|
||||
external_params = {
|
||||
"client_id": self.external_client_id,
|
||||
"redirect_uri": f"{self.issuer_url}/callback",
|
||||
"response_type": "code",
|
||||
"state": state,
|
||||
"scope": " ".join(params.scopes or []),
|
||||
}
|
||||
|
||||
return construct_redirect_uri(self.external_authorize_url, **external_params)
|
||||
|
||||
async def handle_callback(self, code: str, state: str) -> str:
|
||||
"""Handle callback from external provider."""
|
||||
# Get original authorization request
|
||||
auth_code = self.codes.get(state)
|
||||
if not auth_code:
|
||||
raise ValueError("Invalid state parameter")
|
||||
|
||||
# Exchange code with external provider
|
||||
token_data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": f"{self.issuer_url}/callback",
|
||||
"client_id": self.external_client_id,
|
||||
"client_secret": self.external_client_secret,
|
||||
}
|
||||
|
||||
response = await self.http_client.post(
|
||||
self.external_token_url,
|
||||
data=token_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
external_tokens = response.json()
|
||||
|
||||
# Store external tokens
|
||||
import secrets
|
||||
|
||||
internal_code = secrets.token_urlsafe(32)
|
||||
|
||||
self.codes[internal_code] = ExternalAuthorizationCode(
|
||||
code=internal_code,
|
||||
scopes=auth_code.scopes,
|
||||
expires_at=0,
|
||||
client_id=auth_code.client_id,
|
||||
code_challenge=auth_code.code_challenge,
|
||||
redirect_uri=auth_code.redirect_uri,
|
||||
redirect_uri_provided_explicitly=auth_code.redirect_uri_provided_explicitly,
|
||||
external_code=code,
|
||||
state=auth_code.state,
|
||||
)
|
||||
|
||||
self.tokens[internal_code] = external_tokens
|
||||
|
||||
# Redirect to original client
|
||||
return construct_redirect_uri(
|
||||
str(auth_code.redirect_uri),
|
||||
code=internal_code,
|
||||
state=auth_code.state,
|
||||
)
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[ExternalAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.codes.get(authorization_code)
|
||||
if code and code.client_id == client.client_id:
|
||||
return code
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: ExternalAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange authorization code for tokens."""
|
||||
# Get stored external tokens
|
||||
external_tokens = self.tokens.get(authorization_code.code)
|
||||
if not external_tokens:
|
||||
raise ValueError("No tokens found for authorization code")
|
||||
|
||||
# Map external tokens to MCP tokens
|
||||
access_token = external_tokens.get("access_token")
|
||||
refresh_token = external_tokens.get("refresh_token")
|
||||
expires_in = external_tokens.get("expires_in", 3600)
|
||||
|
||||
# Store the mapping
|
||||
self.tokens[access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": access_token,
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
if refresh_token:
|
||||
self.tokens[refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": refresh_token,
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Clean up authorization code
|
||||
del self.codes[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=expires_in,
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[ExternalRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token_info = self.tokens.get(refresh_token)
|
||||
if token_info and token_info["client_id"] == client.client_id:
|
||||
return ExternalRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_info["scopes"],
|
||||
external_token=token_info.get("external_token"),
|
||||
)
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: ExternalRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange refresh token for new tokens."""
|
||||
# Exchange with external provider
|
||||
token_data = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token.external_token or refresh_token.token,
|
||||
"client_id": self.external_client_id,
|
||||
"client_secret": self.external_client_secret,
|
||||
}
|
||||
|
||||
response = await self.http_client.post(
|
||||
self.external_token_url,
|
||||
data=token_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
external_tokens = response.json()
|
||||
|
||||
# Update stored tokens
|
||||
new_access_token = external_tokens.get("access_token")
|
||||
new_refresh_token = external_tokens.get("refresh_token", refresh_token.token)
|
||||
expires_in = external_tokens.get("expires_in", 3600)
|
||||
|
||||
self.tokens[new_access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": new_access_token,
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
if new_refresh_token != refresh_token.token:
|
||||
self.tokens[new_refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": new_refresh_token,
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
del self.tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=expires_in,
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(scopes or refresh_token.scopes),
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[ExternalAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
token_info = self.tokens.get(token)
|
||||
if token_info:
|
||||
return ExternalAccessToken(
|
||||
token=token,
|
||||
client_id=token_info["client_id"],
|
||||
scopes=token_info["scopes"],
|
||||
external_token=token_info.get("external_token"),
|
||||
)
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: ExternalAccessToken | ExternalRefreshToken) -> None:
|
||||
"""Revoke a token."""
|
||||
self.tokens.pop(token.token, None)
|
||||
|
||||
|
||||
def create_github_provider() -> ExternalOAuthProvider:
|
||||
"""Create an OAuth provider for GitHub integration."""
|
||||
return ExternalOAuthProvider(
|
||||
issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
|
||||
external_provider="github",
|
||||
external_client_id=os.getenv("GITHUB_CLIENT_ID", ""),
|
||||
external_client_secret=os.getenv("GITHUB_CLIENT_SECRET", ""),
|
||||
external_authorize_url="https://github.com/login/oauth/authorize",
|
||||
external_token_url="https://github.com/login/oauth/access_token",
|
||||
external_userinfo_url="https://api.github.com/user",
|
||||
)
|
||||
|
||||
|
||||
def create_google_provider() -> ExternalOAuthProvider:
|
||||
"""Create an OAuth provider for Google integration."""
|
||||
return ExternalOAuthProvider(
|
||||
issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
|
||||
external_provider="google",
|
||||
external_client_id=os.getenv("GOOGLE_CLIENT_ID", ""),
|
||||
external_client_secret=os.getenv("GOOGLE_CLIENT_SECRET", ""),
|
||||
external_authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
||||
external_token_url="https://oauth2.googleapis.com/token",
|
||||
external_userinfo_url="https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
)
|
||||
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import ProjectConfig, get_project_config
|
||||
from basic_memory.config import ProjectConfig, get_project_config, ConfigManager
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,7 +23,7 @@ class ProjectSession:
|
||||
current_project: Optional[str] = None
|
||||
default_project: Optional[str] = None
|
||||
|
||||
def initialize(self, default_project: str) -> None:
|
||||
def initialize(self, default_project: str) -> "ProjectSession":
|
||||
"""Set the default project from config on startup.
|
||||
|
||||
Args:
|
||||
@@ -32,6 +32,7 @@ class ProjectSession:
|
||||
self.default_project = default_project
|
||||
self.current_project = default_project
|
||||
logger.info(f"Initialized project session with default project: {default_project}")
|
||||
return self
|
||||
|
||||
def get_current_project(self) -> str:
|
||||
"""Get the currently active project name.
|
||||
@@ -64,6 +65,21 @@ class ProjectSession:
|
||||
self.current_project = self.default_project # pragma: no cover
|
||||
logger.info(f"Reset project context to default: {self.default_project}") # pragma: no cover
|
||||
|
||||
def refresh_from_config(self) -> None:
|
||||
"""Refresh session state from current configuration.
|
||||
|
||||
This method reloads the default project from config and reinitializes
|
||||
the session. This should be called when the default project is changed
|
||||
via CLI or API to ensure MCP session stays in sync.
|
||||
"""
|
||||
# Reload config to get latest default project
|
||||
current_config = ConfigManager().config
|
||||
new_default = current_config.default_project
|
||||
|
||||
# Reinitialize with new default
|
||||
self.initialize(new_default)
|
||||
logger.info(f"Refreshed project session from config, new default: {new_default}")
|
||||
|
||||
|
||||
# Global session instance
|
||||
session = ProjectSession()
|
||||
@@ -87,7 +103,8 @@ def get_active_project(project_override: Optional[str] = None) -> ProjectConfig:
|
||||
return project
|
||||
|
||||
current_project = session.get_current_project()
|
||||
return get_project_config(current_project)
|
||||
active_project = get_project_config(current_project)
|
||||
return active_project
|
||||
|
||||
|
||||
def add_project_metadata(result: str, project_name: str) -> str:
|
||||
|
||||
@@ -10,12 +10,10 @@ from basic_memory.mcp.prompts import continue_conversation
|
||||
from basic_memory.mcp.prompts import recent_activity
|
||||
from basic_memory.mcp.prompts import search
|
||||
from basic_memory.mcp.prompts import ai_assistant_guide
|
||||
from basic_memory.mcp.prompts import sync_status
|
||||
|
||||
__all__ = [
|
||||
"ai_assistant_guide",
|
||||
"continue_conversation",
|
||||
"recent_activity",
|
||||
"search",
|
||||
"sync_status",
|
||||
]
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
"""Sync status prompt for Basic Memory MCP server."""
|
||||
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@mcp.prompt(
|
||||
description="""Get sync status with recommendations for AI assistants.
|
||||
|
||||
This prompt provides both current sync status and guidance on how
|
||||
AI assistants should respond when sync operations are in progress or completed.
|
||||
""",
|
||||
)
|
||||
async def sync_status_prompt() -> str:
|
||||
"""Get sync status with AI assistant guidance.
|
||||
|
||||
This prompt provides detailed sync status information along with
|
||||
recommendations for how AI assistants should handle different sync states.
|
||||
|
||||
Returns:
|
||||
Formatted sync status with AI assistant guidance
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
from basic_memory.services.migration_service import migration_manager
|
||||
|
||||
state = migration_manager.state
|
||||
|
||||
# Build status report
|
||||
lines = [
|
||||
"# Basic Memory Sync Status",
|
||||
"",
|
||||
f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
|
||||
f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
|
||||
"",
|
||||
]
|
||||
|
||||
if migration_manager.is_ready:
|
||||
lines.extend(
|
||||
[
|
||||
"✅ **All sync operations completed** - System is fully operational",
|
||||
"",
|
||||
"All Basic Memory tools are available and functioning normally.",
|
||||
"File indexing is complete and knowledge graphs are up to date.",
|
||||
"You can proceed with any knowledge management tasks.",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.append(f"**Status Message**: {state.message}")
|
||||
|
||||
if state.status.value == "in_progress":
|
||||
if state.projects_total > 0:
|
||||
progress = f" ({state.projects_migrated}/{state.projects_total})"
|
||||
lines.append(f"**Progress**: {progress}")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
|
||||
"",
|
||||
"**Impact**: Some tools may show status messages instead of normal responses",
|
||||
"until sync completes (usually 1-3 minutes).",
|
||||
]
|
||||
)
|
||||
|
||||
elif state.status.value == "failed":
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"❌ **Sync failed**: {state.error or 'Unknown error'}",
|
||||
"",
|
||||
"**Impact**: System may have limited functionality until issue is resolved.",
|
||||
]
|
||||
)
|
||||
|
||||
# Add AI assistant recommendations
|
||||
if not migration_manager.is_ready:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## AI Assistant Recommendations",
|
||||
"",
|
||||
"**When sync is in progress:**",
|
||||
"- Inform the user about the background file processing",
|
||||
"- Suggest using `sync_status()` tool to check progress",
|
||||
"- Explain that tools will work normally once sync completes",
|
||||
"- Avoid creating complex workflows until sync is done",
|
||||
"",
|
||||
"**What to tell users:**",
|
||||
"- 'Basic Memory is processing your files and building knowledge graphs'",
|
||||
"- 'This usually takes 1-3 minutes depending on your content size'",
|
||||
"- 'You can check progress anytime with the sync_status tool'",
|
||||
"- 'Full functionality will be available once processing completes'",
|
||||
"",
|
||||
"**User-friendly language:**",
|
||||
"- Say 'processing files' instead of 'migration' or 'sync'",
|
||||
"- Say 'building knowledge graphs' instead of 'indexing'",
|
||||
"- Say 'setting up your knowledge base' instead of 'running migrations'",
|
||||
]
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
return f"""# Sync Status - Error
|
||||
|
||||
❌ **Unable to check sync status**: {str(e)}
|
||||
|
||||
## AI Assistant Recommendations
|
||||
|
||||
**When status is unavailable:**
|
||||
- Assume the system is likely working normally
|
||||
- Try proceeding with normal operations
|
||||
- If users report issues, suggest checking logs or restarting
|
||||
- Use user-friendly language about 'setting up the knowledge base'
|
||||
"""
|
||||
@@ -7,25 +7,10 @@ from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncIterator, Optional, Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.logging import configure_logging as mcp_configure_logging
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.services.initialization import initialize_app
|
||||
from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
|
||||
from basic_memory.mcp.project_session import session
|
||||
from basic_memory.mcp.external_auth_provider import (
|
||||
create_github_provider,
|
||||
create_google_provider,
|
||||
)
|
||||
from basic_memory.mcp.supabase_auth_provider import SupabaseOAuthProvider
|
||||
|
||||
# mcp console logging
|
||||
mcp_configure_logging(level="ERROR")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,7 +21,11 @@ class AppContext:
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
|
||||
"""Manage application lifecycle with type-safe context"""
|
||||
""" """
|
||||
# defer import so tests can monkeypatch
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Initialize on startup (now returns migration_manager)
|
||||
migration_manager = await initialize_app(app_config)
|
||||
|
||||
@@ -50,61 +39,8 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma:
|
||||
pass
|
||||
|
||||
|
||||
# OAuth configuration function
|
||||
def create_auth_config() -> tuple[AuthSettings | None, Any | None]:
|
||||
"""Create OAuth configuration if enabled."""
|
||||
# Check if OAuth is enabled via environment variable
|
||||
import os
|
||||
|
||||
if os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true":
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
# Configure OAuth settings
|
||||
issuer_url = os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000")
|
||||
required_scopes = os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES", "read,write")
|
||||
docs_url = os.getenv("FASTMCP_AUTH_DOCS_URL") or "http://localhost:8000/docs/oauth"
|
||||
|
||||
auth_settings = AuthSettings(
|
||||
issuer_url=AnyHttpUrl(issuer_url),
|
||||
service_documentation_url=AnyHttpUrl(docs_url),
|
||||
required_scopes=required_scopes.split(",") if required_scopes else ["read", "write"],
|
||||
)
|
||||
|
||||
# Create OAuth provider based on type
|
||||
provider_type = os.getenv("FASTMCP_AUTH_PROVIDER", "basic").lower()
|
||||
|
||||
if provider_type == "github":
|
||||
auth_provider = create_github_provider()
|
||||
elif provider_type == "google":
|
||||
auth_provider = create_google_provider()
|
||||
elif provider_type == "supabase":
|
||||
supabase_url = os.getenv("SUPABASE_URL")
|
||||
supabase_anon_key = os.getenv("SUPABASE_ANON_KEY")
|
||||
supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY")
|
||||
|
||||
if not supabase_url or not supabase_anon_key:
|
||||
raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set for Supabase auth")
|
||||
|
||||
auth_provider = SupabaseOAuthProvider(
|
||||
supabase_url=supabase_url,
|
||||
supabase_anon_key=supabase_anon_key,
|
||||
supabase_service_key=supabase_service_key,
|
||||
issuer_url=issuer_url,
|
||||
)
|
||||
else: # default to "basic"
|
||||
auth_provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
return auth_settings, auth_provider
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
# Create auth configuration
|
||||
auth_settings, auth_provider = create_auth_config()
|
||||
|
||||
# Create the shared server instance
|
||||
# Create the shared server instance with custom Stytch auth
|
||||
mcp = FastMCP(
|
||||
name="Basic Memory",
|
||||
log_level="DEBUG",
|
||||
auth=auth_provider,
|
||||
lifespan=app_lifespan,
|
||||
)
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
"""Supabase OAuth provider for Basic Memory MCP server."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
from loguru import logger
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
TokenError,
|
||||
AuthorizeError,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseAuthorizationCode(AuthorizationCode):
|
||||
"""Authorization code with Supabase metadata."""
|
||||
|
||||
user_id: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseRefreshToken(RefreshToken):
|
||||
"""Refresh token with Supabase metadata."""
|
||||
|
||||
supabase_refresh_token: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseAccessToken(AccessToken):
|
||||
"""Access token with Supabase metadata."""
|
||||
|
||||
supabase_access_token: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class SupabaseOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
SupabaseAuthorizationCode, SupabaseRefreshToken, SupabaseAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider that integrates with Supabase Auth.
|
||||
|
||||
This provider uses Supabase as the authentication backend while
|
||||
maintaining compatibility with MCP's OAuth requirements.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
supabase_url: str,
|
||||
supabase_anon_key: str,
|
||||
supabase_service_key: Optional[str] = None,
|
||||
issuer_url: str = "http://localhost:8000",
|
||||
):
|
||||
self.supabase_url = supabase_url.rstrip("/")
|
||||
self.supabase_anon_key = supabase_anon_key
|
||||
self.supabase_service_key = supabase_service_key or supabase_anon_key
|
||||
self.issuer_url = issuer_url
|
||||
|
||||
# HTTP client for Supabase API calls
|
||||
self.http_client = httpx.AsyncClient()
|
||||
|
||||
# Temporary storage for auth flows (in production, use Supabase DB)
|
||||
self.pending_auth_codes: Dict[str, SupabaseAuthorizationCode] = {}
|
||||
self.mcp_to_supabase_tokens: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client from Supabase.
|
||||
|
||||
In production, this would query a clients table in Supabase.
|
||||
"""
|
||||
# For now, we'll validate against a configured list of allowed clients
|
||||
# In production, query Supabase DB for client info
|
||||
allowed_clients = os.getenv("SUPABASE_ALLOWED_CLIENTS", "").split(",")
|
||||
|
||||
if client_id in allowed_clients:
|
||||
return OAuthClientInformationFull(
|
||||
client_id=client_id,
|
||||
client_secret="", # Supabase handles secrets
|
||||
redirect_uris=[], # Supabase handles redirect URIs
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client in Supabase.
|
||||
|
||||
In production, this would insert into a clients table.
|
||||
"""
|
||||
# For development, we just log the registration
|
||||
logger.info(f"Would register client {client_info.client_id} in Supabase")
|
||||
|
||||
# In production:
|
||||
# await self.supabase.table('oauth_clients').insert({
|
||||
# 'client_id': client_info.client_id,
|
||||
# 'client_secret': client_info.client_secret,
|
||||
# 'metadata': client_info.client_metadata,
|
||||
# }).execute()
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create authorization URL redirecting to Supabase Auth.
|
||||
|
||||
This initiates the OAuth flow with Supabase as the identity provider.
|
||||
"""
|
||||
# Generate state for this auth request
|
||||
state = secrets.token_urlsafe(32)
|
||||
|
||||
# Store the authorization request
|
||||
self.pending_auth_codes[state] = SupabaseAuthorizationCode(
|
||||
code=state,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
)
|
||||
|
||||
# Build Supabase auth URL
|
||||
auth_params = {
|
||||
"redirect_to": f"{self.issuer_url}/auth/callback",
|
||||
"scopes": " ".join(params.scopes or ["openid", "email"]),
|
||||
"state": state,
|
||||
}
|
||||
|
||||
# Use Supabase's OAuth endpoint
|
||||
auth_url = f"{self.supabase_url}/auth/v1/authorize"
|
||||
query_string = "&".join(f"{k}={v}" for k, v in auth_params.items())
|
||||
|
||||
return f"{auth_url}?{query_string}"
|
||||
|
||||
async def handle_supabase_callback(self, code: str, state: str) -> str:
|
||||
"""Handle callback from Supabase after user authentication."""
|
||||
# Get the original auth request
|
||||
auth_request = self.pending_auth_codes.get(state)
|
||||
if not auth_request:
|
||||
raise AuthorizeError(
|
||||
error="invalid_request",
|
||||
error_description="Invalid state parameter",
|
||||
)
|
||||
|
||||
# Exchange code with Supabase for tokens
|
||||
token_response = await self.http_client.post(
|
||||
f"{self.supabase_url}/auth/v1/token",
|
||||
json={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": f"{self.issuer_url}/auth/callback",
|
||||
},
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {self.supabase_anon_key}",
|
||||
},
|
||||
)
|
||||
|
||||
if not token_response.is_success:
|
||||
raise AuthorizeError(
|
||||
error="server_error",
|
||||
error_description="Failed to exchange code with Supabase",
|
||||
)
|
||||
|
||||
supabase_tokens = token_response.json()
|
||||
|
||||
# Get user info from Supabase
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {supabase_tokens['access_token']}",
|
||||
},
|
||||
)
|
||||
|
||||
user_data = user_response.json() if user_response.is_success else {}
|
||||
|
||||
# Generate MCP authorization code
|
||||
mcp_code = secrets.token_urlsafe(32)
|
||||
|
||||
# Update auth request with user info
|
||||
auth_request.code = mcp_code
|
||||
auth_request.user_id = user_data.get("id")
|
||||
auth_request.email = user_data.get("email")
|
||||
|
||||
# Store mapping
|
||||
self.pending_auth_codes[mcp_code] = auth_request
|
||||
self.mcp_to_supabase_tokens[mcp_code] = {
|
||||
"supabase_tokens": supabase_tokens,
|
||||
"user": user_data,
|
||||
}
|
||||
|
||||
# Clean up old state
|
||||
del self.pending_auth_codes[state]
|
||||
|
||||
# Redirect back to client
|
||||
redirect_uri = str(auth_request.redirect_uri)
|
||||
separator = "&" if "?" in redirect_uri else "?"
|
||||
|
||||
return f"{redirect_uri}{separator}code={mcp_code}&state={state}"
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[SupabaseAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.pending_auth_codes.get(authorization_code)
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check expiration
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
del self.pending_auth_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: SupabaseAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange authorization code for tokens."""
|
||||
# Get stored Supabase tokens
|
||||
token_data = self.mcp_to_supabase_tokens.get(authorization_code.code)
|
||||
if not token_data:
|
||||
raise TokenError(error="invalid_grant", error_description="Invalid authorization code")
|
||||
|
||||
supabase_tokens = token_data["supabase_tokens"]
|
||||
user = token_data["user"]
|
||||
|
||||
# Generate MCP tokens that wrap Supabase tokens
|
||||
access_token = self._generate_mcp_token(
|
||||
client_id=client.client_id,
|
||||
user_id=user.get("id", ""),
|
||||
email=user.get("email", ""),
|
||||
scopes=authorization_code.scopes,
|
||||
supabase_access_token=supabase_tokens["access_token"],
|
||||
)
|
||||
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store the token mapping
|
||||
self.mcp_to_supabase_tokens[access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user.get("id"),
|
||||
"email": user.get("email"),
|
||||
"supabase_access_token": supabase_tokens["access_token"],
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Store refresh token mapping
|
||||
self.mcp_to_supabase_tokens[refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user.get("id"),
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Clean up authorization code
|
||||
del self.pending_auth_codes[authorization_code.code]
|
||||
del self.mcp_to_supabase_tokens[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=supabase_tokens.get("expires_in", 3600),
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[SupabaseRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token_data = self.mcp_to_supabase_tokens.get(refresh_token)
|
||||
|
||||
if token_data and token_data["client_id"] == client.client_id:
|
||||
return SupabaseRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_data["scopes"],
|
||||
supabase_refresh_token=token_data["supabase_refresh_token"],
|
||||
user_id=token_data.get("user_id"),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: SupabaseRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange refresh token for new tokens using Supabase."""
|
||||
# Refresh with Supabase
|
||||
token_response = await self.http_client.post(
|
||||
f"{self.supabase_url}/auth/v1/token",
|
||||
json={
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token.supabase_refresh_token,
|
||||
},
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {self.supabase_anon_key}",
|
||||
},
|
||||
)
|
||||
|
||||
if not token_response.is_success:
|
||||
raise TokenError(
|
||||
error="invalid_grant",
|
||||
error_description="Failed to refresh with Supabase",
|
||||
)
|
||||
|
||||
supabase_tokens = token_response.json()
|
||||
|
||||
# Get updated user info
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {supabase_tokens['access_token']}",
|
||||
},
|
||||
)
|
||||
|
||||
user_data = user_response.json() if user_response.is_success else {}
|
||||
|
||||
# Generate new MCP tokens
|
||||
new_access_token = self._generate_mcp_token(
|
||||
client_id=client.client_id,
|
||||
user_id=user_data.get("id", ""),
|
||||
email=user_data.get("email", ""),
|
||||
scopes=scopes or refresh_token.scopes,
|
||||
supabase_access_token=supabase_tokens["access_token"],
|
||||
)
|
||||
|
||||
new_refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Update token mappings
|
||||
self.mcp_to_supabase_tokens[new_access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user_data.get("id"),
|
||||
"email": user_data.get("email"),
|
||||
"supabase_access_token": supabase_tokens["access_token"],
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
self.mcp_to_supabase_tokens[new_refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user_data.get("id"),
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
# Clean up old tokens
|
||||
del self.mcp_to_supabase_tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=supabase_tokens.get("expires_in", 3600),
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(scopes or refresh_token.scopes),
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[SupabaseAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
# First check our mapping
|
||||
token_data = self.mcp_to_supabase_tokens.get(token)
|
||||
if token_data:
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id=token_data["client_id"],
|
||||
scopes=token_data["scopes"],
|
||||
supabase_access_token=token_data.get("supabase_access_token"),
|
||||
user_id=token_data.get("user_id"),
|
||||
email=token_data.get("email"),
|
||||
)
|
||||
|
||||
# Try to decode as JWT
|
||||
try:
|
||||
# Verify with Supabase's JWT secret
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
os.getenv("SUPABASE_JWT_SECRET", ""),
|
||||
algorithms=["HS256"],
|
||||
audience="authenticated",
|
||||
)
|
||||
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id=payload.get("client_id", ""),
|
||||
scopes=payload.get("scopes", []),
|
||||
user_id=payload.get("sub"),
|
||||
email=payload.get("email"),
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
pass
|
||||
|
||||
# Validate with Supabase
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {token}",
|
||||
},
|
||||
)
|
||||
|
||||
if user_response.is_success:
|
||||
user_data = user_response.json()
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id="", # Unknown client for direct Supabase tokens
|
||||
scopes=[],
|
||||
supabase_access_token=token,
|
||||
user_id=user_data.get("id"),
|
||||
email=user_data.get("email"),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: SupabaseAccessToken | SupabaseRefreshToken) -> None:
|
||||
"""Revoke a token."""
|
||||
# Remove from our mapping
|
||||
self.mcp_to_supabase_tokens.pop(token.token, None)
|
||||
|
||||
# In production, also revoke in Supabase:
|
||||
# await self.supabase.auth.admin.sign_out(token.user_id)
|
||||
|
||||
def _generate_mcp_token(
|
||||
self,
|
||||
client_id: str,
|
||||
user_id: str,
|
||||
email: str,
|
||||
scopes: list[str],
|
||||
supabase_access_token: str,
|
||||
) -> str:
|
||||
"""Generate an MCP token that wraps Supabase authentication."""
|
||||
payload = {
|
||||
"iss": self.issuer_url,
|
||||
"sub": user_id,
|
||||
"client_id": client_id,
|
||||
"email": email,
|
||||
"scopes": scopes,
|
||||
"supabase_token": supabase_access_token[:10] + "...", # Reference only
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
}
|
||||
|
||||
# Use Supabase JWT secret if available
|
||||
secret = os.getenv("SUPABASE_JWT_SECRET", secrets.token_urlsafe(32))
|
||||
|
||||
return jwt.encode(payload, secret, algorithm="HS256")
|
||||
@@ -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",
|
||||
|
||||
@@ -82,10 +82,15 @@ async def build_context(
|
||||
logger.info(f"Building context from {url}")
|
||||
# URL is already validated and normalized by MemoryUrl type annotation
|
||||
|
||||
# Get the active project first to check project-specific sync status
|
||||
active_project = get_active_project(project)
|
||||
|
||||
# Check migration status and wait briefly if needed
|
||||
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
||||
|
||||
migration_status = await wait_for_migration_or_return_status(timeout=5.0)
|
||||
migration_status = await wait_for_migration_or_return_status(
|
||||
timeout=5.0, project_name=active_project.name
|
||||
)
|
||||
if migration_status: # pragma: no cover
|
||||
# Return a proper GraphContext with status message
|
||||
from basic_memory.schemas.memory import MemoryMetadata
|
||||
@@ -102,8 +107,6 @@ async def build_context(
|
||||
uri=migration_status, # Include status in metadata
|
||||
),
|
||||
)
|
||||
|
||||
active_project = get_active_project(project)
|
||||
project_url = active_project.project_url
|
||||
|
||||
response = await call_get(
|
||||
|
||||
@@ -7,9 +7,145 @@ from loguru import logger
|
||||
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.utils import call_post
|
||||
from basic_memory.mcp.tools.utils import call_post, call_get
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.schemas import EntityResponse
|
||||
from basic_memory.schemas.project_info import ProjectList
|
||||
from basic_memory.utils import validate_project_path
|
||||
|
||||
|
||||
async def _detect_cross_project_move_attempt(
|
||||
identifier: str, destination_path: str, current_project: str
|
||||
) -> Optional[str]:
|
||||
"""Detect potential cross-project move attempts and return guidance.
|
||||
|
||||
Args:
|
||||
identifier: The note identifier being moved
|
||||
destination_path: The destination path
|
||||
current_project: The current active project
|
||||
|
||||
Returns:
|
||||
Error message with guidance if cross-project move is detected, None otherwise
|
||||
"""
|
||||
try:
|
||||
# Get list of all available projects to check against
|
||||
response = await call_get(client, "/projects/projects")
|
||||
project_list = ProjectList.model_validate(response.json())
|
||||
project_names = [p.name.lower() for p in project_list.projects]
|
||||
|
||||
# Check if destination path contains any project names
|
||||
dest_lower = destination_path.lower()
|
||||
path_parts = dest_lower.split("/")
|
||||
|
||||
# Look for project names in the destination path
|
||||
for part in path_parts:
|
||||
if part in project_names and part != current_project.lower():
|
||||
# Found a different project name in the path
|
||||
matching_project = next(
|
||||
p.name for p in project_list.projects if p.name.lower() == part
|
||||
)
|
||||
return _format_cross_project_error_response(
|
||||
identifier, destination_path, current_project, matching_project
|
||||
)
|
||||
|
||||
# No other cross-project patterns detected
|
||||
|
||||
except Exception as e:
|
||||
# If we can't detect, don't interfere with normal error handling
|
||||
logger.debug(f"Could not check for cross-project move: {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _format_cross_project_error_response(
|
||||
identifier: str, destination_path: str, current_project: str, target_project: str
|
||||
) -> str:
|
||||
"""Format error response for detected cross-project move attempts."""
|
||||
return dedent(f"""
|
||||
# Move Failed - Cross-Project Move Not Supported
|
||||
|
||||
Cannot move '{identifier}' to '{destination_path}' because it appears to reference a different project ('{target_project}').
|
||||
|
||||
**Current project:** {current_project}
|
||||
**Target project:** {target_project}
|
||||
|
||||
## Cross-project moves are not supported directly
|
||||
|
||||
Notes can only be moved within the same project. To move content between projects, use this workflow:
|
||||
|
||||
### Recommended approach:
|
||||
```
|
||||
# 1. Read the note content from current project
|
||||
read_note("{identifier}")
|
||||
|
||||
# 2. Switch to the target project
|
||||
switch_project("{target_project}")
|
||||
|
||||
# 3. Create the note in the target project
|
||||
write_note("Note Title", "content from step 1", "target-folder")
|
||||
|
||||
# 4. Switch back to original project (optional)
|
||||
switch_project("{current_project}")
|
||||
|
||||
# 5. Delete the original note if desired
|
||||
delete_note("{identifier}")
|
||||
```
|
||||
|
||||
### Alternative: Stay in current project
|
||||
If you want to move the note within the **{current_project}** project only:
|
||||
```
|
||||
move_note("{identifier}", "new-folder/new-name.md")
|
||||
```
|
||||
|
||||
## Available projects:
|
||||
Use `list_projects()` to see all available projects and `switch_project("project-name")` to change projects.
|
||||
""").strip()
|
||||
|
||||
|
||||
def _format_potential_cross_project_guidance(
|
||||
identifier: str, destination_path: str, current_project: str, available_projects: list[str]
|
||||
) -> str:
|
||||
"""Format guidance for potentially cross-project moves."""
|
||||
other_projects = ", ".join(available_projects[:3]) # Show first 3 projects
|
||||
if len(available_projects) > 3:
|
||||
other_projects += f" (and {len(available_projects) - 3} others)"
|
||||
|
||||
return dedent(f"""
|
||||
# Move Failed - Check Project Context
|
||||
|
||||
Cannot move '{identifier}' to '{destination_path}' within the current project '{current_project}'.
|
||||
|
||||
## If you intended to move within the current project:
|
||||
The destination path should be relative to the project root:
|
||||
```
|
||||
move_note("{identifier}", "folder/filename.md")
|
||||
```
|
||||
|
||||
## If you intended to move to a different project:
|
||||
Cross-project moves require switching projects first. Available projects: {other_projects}
|
||||
|
||||
### To move to another project:
|
||||
```
|
||||
# 1. Read the content
|
||||
read_note("{identifier}")
|
||||
|
||||
# 2. Switch to target project
|
||||
switch_project("target-project-name")
|
||||
|
||||
# 3. Create note in target project
|
||||
write_note("Title", "content", "folder")
|
||||
|
||||
# 4. Switch back and delete original if desired
|
||||
switch_project("{current_project}")
|
||||
delete_note("{identifier}")
|
||||
```
|
||||
|
||||
### To see all projects:
|
||||
```
|
||||
list_projects()
|
||||
```
|
||||
""").strip()
|
||||
|
||||
|
||||
def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
|
||||
@@ -258,6 +394,36 @@ async def move_note(
|
||||
active_project = get_active_project(project)
|
||||
project_url = active_project.project_url
|
||||
|
||||
# Validate destination path to prevent path traversal attacks
|
||||
project_path = active_project.home
|
||||
if not validate_project_path(destination_path, project_path):
|
||||
logger.warning(
|
||||
"Attempted path traversal attack blocked",
|
||||
destination_path=destination_path,
|
||||
project=active_project.name,
|
||||
)
|
||||
return f"""# Move Failed - Security Validation Error
|
||||
|
||||
The destination path '{destination_path}' is not allowed - paths must stay within project boundaries.
|
||||
|
||||
## Valid path examples:
|
||||
- `notes/my-file.md`
|
||||
- `projects/2025/meeting-notes.md`
|
||||
- `archive/old-notes.md`
|
||||
|
||||
## Try again with a safe path:
|
||||
```
|
||||
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
||||
```"""
|
||||
|
||||
# Check for potential cross-project move attempts
|
||||
cross_project_error = await _detect_cross_project_move_attempt(
|
||||
identifier, destination_path, active_project.name
|
||||
)
|
||||
if cross_project_error:
|
||||
logger.info(f"Detected cross-project move attempt: {identifier} -> {destination_path}")
|
||||
return cross_project_error
|
||||
|
||||
try:
|
||||
# Prepare move request
|
||||
move_data = {
|
||||
|
||||
@@ -5,11 +5,11 @@ and manage project context during conversations.
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.project_session import session, add_project_metadata
|
||||
from basic_memory.mcp.server import mcp
|
||||
@@ -19,8 +19,10 @@ from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_projects(ctx: Context | None = None) -> str:
|
||||
@mcp.tool("list_memory_projects")
|
||||
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
|
||||
@@ -30,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")
|
||||
@@ -85,27 +87,38 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
response = await call_get(client, "/projects/projects")
|
||||
project_list = ProjectList.model_validate(response.json())
|
||||
|
||||
# Check if project exists
|
||||
project_exists = any(p.permalink == project_permalink for p in project_list.projects)
|
||||
if not project_exists:
|
||||
# Find the project by name (case-insensitive) or permalink
|
||||
target_project = None
|
||||
for p in project_list.projects:
|
||||
# Match by permalink (handles case-insensitive input)
|
||||
if p.permalink == project_permalink:
|
||||
target_project = p
|
||||
break
|
||||
# Also match by name comparison (case-insensitive)
|
||||
if p.name.lower() == project_name.lower():
|
||||
target_project = p
|
||||
break
|
||||
|
||||
if not target_project:
|
||||
available_projects = [p.name for p in project_list.projects]
|
||||
return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
||||
|
||||
# Switch to the project
|
||||
session.set_current_project(project_permalink)
|
||||
# Switch to the project using the canonical name from database
|
||||
canonical_name = target_project.name
|
||||
session.set_current_project(canonical_name)
|
||||
current_project = session.get_current_project()
|
||||
project_config = get_project_config(current_project)
|
||||
|
||||
# Get project info to show summary
|
||||
try:
|
||||
current_project_permalink = generate_permalink(canonical_name)
|
||||
response = await call_get(
|
||||
client,
|
||||
f"{project_config.project_url}/project/info",
|
||||
params={"project_name": project_permalink},
|
||||
f"/{current_project_permalink}/project/info",
|
||||
params={"project_name": canonical_name},
|
||||
)
|
||||
project_info = ProjectInfoResponse.model_validate(response.json())
|
||||
|
||||
result = f"✓ Switched to {project_permalink} project\n\n"
|
||||
result = f"✓ Switched to {canonical_name} project\n\n"
|
||||
result += "Project Summary:\n"
|
||||
result += f"• {project_info.statistics.total_entities} entities\n"
|
||||
result += f"• {project_info.statistics.total_observations} observations\n"
|
||||
@@ -113,11 +126,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
|
||||
except Exception as e:
|
||||
# If we can't get project info, still confirm the switch
|
||||
logger.warning(f"Could not get project info for {project_name}: {e}")
|
||||
result = f"✓ Switched to {project_name} project\n\n"
|
||||
logger.warning(f"Could not get project info for {canonical_name}: {e}")
|
||||
result = f"✓ Switched to {canonical_name} project\n\n"
|
||||
result += "Project summary unavailable.\n"
|
||||
|
||||
return add_project_metadata(result, project_name)
|
||||
return add_project_metadata(result, canonical_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error switching to project {project_name}: {e}")
|
||||
@@ -134,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")`
|
||||
|
||||
@@ -149,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
|
||||
@@ -165,13 +180,13 @@ async def get_current_project(ctx: Context | None = None) -> str:
|
||||
await ctx.info("Getting current project information")
|
||||
|
||||
current_project = session.get_current_project()
|
||||
project_config = get_project_config(current_project)
|
||||
result = f"Current project: {current_project}\n\n"
|
||||
|
||||
# get project stats
|
||||
# get project stats (use permalink in URL path)
|
||||
current_project_permalink = generate_permalink(current_project)
|
||||
response = await call_get(
|
||||
client,
|
||||
f"{project_config.project_url}/project/info",
|
||||
f"/{current_project_permalink}/project/info",
|
||||
params={"project_name": current_project},
|
||||
)
|
||||
project_info = ProjectInfoResponse.model_validate(response.json())
|
||||
@@ -220,8 +235,8 @@ async def set_default_project(project_name: str, ctx: Context | None = None) ->
|
||||
return add_project_metadata(result, session.get_current_project())
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def create_project(
|
||||
@mcp.tool("create_memory_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.
|
||||
@@ -238,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}")
|
||||
@@ -331,4 +346,4 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
|
||||
result += "Re-add the project to access its content again.\n"
|
||||
|
||||
return add_project_metadata(result, session.get_current_project())
|
||||
return add_project_metadata(result, session.get_current_project())
|
||||
|
||||