mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
221 Commits
v0.7.0
...
dxt-packaging
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a7206ad9a | |||
| 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 | |||
| 85a178a6b8 | |||
| e4b32d7bc9 | |||
| 9590b934cf | |||
| 735f239f9b | |||
| 3ee30e1f36 | |||
| ac401ea254 | |||
| fb2fd62ed9 | |||
| 126d1655e6 | |||
| 2abf626c46 | |||
| ba8e3d112d | |||
| 7108a7baf1 | |||
| 35884ef3a7 | |||
| 040be05a81 | |||
| c141d7d1e6 | |||
| b73aeb5ed8 | |||
| 9a0e0bd82d | |||
| 117fa44ecf | |||
| 69d7610d47 | |||
| f608cd13f1 | |||
| 2162ad57fe | |||
| dd6ca80716 | |||
| ae3eeb0cc1 | |||
| 602c55fe90 | |||
| 91bfe2dc92 | |||
| a3cae1064d | |||
| c5c70cb0f4 | |||
| 80ec860a1c | |||
| f64d5b2152 | |||
| 69a625acd1 | |||
| 53c29a37ca | |||
| d8c13bf1d3 | |||
| 3f70f5ed42 | |||
| 569a3de80b | |||
| ac08a8d024 | |||
| c13d4b1511 | |||
| 5b85d33a99 | |||
| e2bd3142dc | |||
| 40b7de9168 | |||
| 455a280707 | |||
| 634486107b | |||
| 23d34289fd | |||
| 5ec4087f7c | |||
| 71941c35dd | |||
| 020957cd76 | |||
| b3af6aa685 | |||
| 9437a5f83b | |||
| 3c1cc346df | |||
| 81616ab42e | |||
| 321471f29d | |||
| 73ea91fe0d | |||
| 03d4e97b90 | |||
| 98622a7a47 | |||
| 54dfa08aba | |||
| 9433065a57 | |||
| 2934176331 | |||
| ac89eb47cb | |||
| 78a3412bcf | |||
| fa314b5d2b | |||
| d2ac62a27c | |||
| 7ce7a29f35 | |||
| 3f4d9e4d87 | |||
| 00c8633cfc | |||
| 617e60bda4 | |||
| 6c19c9edf5 | |||
| 9bff1f732e | |||
| 248214cb11 | |||
| 40ea28b0bf | |||
| 43cbb7b38c | |||
| 7930ddb291 | |||
| 1844e58210 | |||
| 7fbb5ebfc5 | |||
| 0e5a465b10 | |||
| 9d581cee13 | |||
| f58852954b | |||
| edd426b99f | |||
| 6b4a421315 | |||
| 4632405446 | |||
| 3a6d5ff58d | |||
| 03de85ec32 | |||
| 084c91ecce | |||
| 3d45227b72 | |||
| 069c0a21c6 | |||
| b27827671d | |||
| 0743ade5fc | |||
| f1c95709cb | |||
| 3c68b7d5dd | |||
| 3d0077990f | |||
| 731b502d36 | |||
| 2a881b1425 | |||
| 681af5d450 | |||
| b35037eb86 | |||
| b667bca5a2 | |||
| e716946b44 | |||
| 46c4fd2164 | |||
| 9c791259a0 | |||
| cc2cae72c1 | |||
| 0a1fdd8f15 | |||
| 78f234b180 | |||
| cbe72be10a | |||
| 0ade6b0603 | |||
| 390ff9d31c | |||
| 3ed74b4a4d | |||
| dfaf0fea9c | |||
| b26afa927f | |||
| 3806943f8c | |||
| 3bffb2e190 | |||
| 9fe34dcddf | |||
| eb5e4ec6bd | |||
| 6b110b28dd | |||
| 88d193e0aa | |||
| fa9175f049 | |||
| 7c787a413c | |||
| 0fa1adf3b5 | |||
| cbe488fbd2 | |||
| 99ff8f60c2 | |||
| 06ee2852a0 | |||
| 939bab1a24 | |||
| 67736487fb | |||
| 2ac620f98b | |||
| 4cbda7a9d4 | |||
| 3070ade69a | |||
| 9af913da4f | |||
| 7b7bec9557 | |||
| fea2f40d1b | |||
| eb1e7b6088 | |||
| 9203730ba7 | |||
| 1a473c02e9 | |||
| b6f5d8a545 | |||
| addeeb1e7f | |||
| 85f9099b0a | |||
| 82b6f3d60e | |||
| 8a7432949b | |||
| 6a4bd54646 | |||
| da97353cfc | |||
| 2e9d673e54 | |||
| c4732a47b3 | |||
| 2d5176e800 | |||
| 9bb8a020c3 | |||
| 31cca6f913 | |||
| d2bd75a949 | |||
| 90d5754180 | |||
| e6496df595 | |||
| 41868fd34c | |||
| a3072702b9 | |||
| 182ec78355 | |||
| f8078cdd46 | |||
| 4bcbeacdd2 | |||
| 093dab5f03 | |||
| 0d7b0b3d7e | |||
| 93cc6379eb | |||
| 37a01b806d | |||
| a573f78317 | |||
| ab36e7e560 | |||
| 959b278687 | |||
| 04b387c742 | |||
| 0c59fd5e83 | |||
| 2ed920aa50 | |||
| bd20185492 | |||
| f731a23e49 | |||
| 07a9415451 | |||
| 20d0375ffa | |||
| eb4a55a5e7 | |||
| 51b4eb32c5 | |||
| 74adae506e | |||
| 94394f0bfe | |||
| 9e3f71cb87 | |||
| f4b703e57f |
@@ -0,0 +1,95 @@
|
||||
# /beta - Create Beta Release
|
||||
|
||||
Create a new beta release using the automated justfile target with quality checks and tagging.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/beta <version>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `version` (required): Beta version like `v0.13.2b1` or `v0.13.2rc1`
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert release manager for the Basic Memory project. When the user runs `/beta`, execute the following steps:
|
||||
|
||||
### Step 1: Pre-flight Validation
|
||||
1. Verify version format matches `v\d+\.\d+\.\d+(b\d+|rc\d+)` pattern
|
||||
2. Check current git status for uncommitted changes
|
||||
3. Verify we're on the `main` branch
|
||||
4. Confirm no existing tag with this version
|
||||
|
||||
### Step 2: Use Justfile Automation
|
||||
Execute the automated beta release process:
|
||||
```bash
|
||||
just beta <version>
|
||||
```
|
||||
|
||||
The justfile target handles:
|
||||
- ✅ Beta version format validation (supports b1, b2, rc1, etc.)
|
||||
- ✅ Git status and branch checks
|
||||
- ✅ Quality checks (`just check` - lint, format, type-check, tests)
|
||||
- ✅ Version update in `src/basic_memory/__init__.py`
|
||||
- ✅ Automatic commit with proper message
|
||||
- ✅ Tag creation and pushing to GitHub
|
||||
- ✅ Beta release workflow trigger
|
||||
|
||||
### Step 3: Monitor Beta Release
|
||||
1. Check GitHub Actions workflow starts successfully
|
||||
2. Monitor workflow at: https://github.com/basicmachines-co/basic-memory/actions
|
||||
3. Verify PyPI pre-release publication
|
||||
4. Test beta installation: `uv tool install basic-memory --pre`
|
||||
|
||||
### Step 4: Beta Testing Instructions
|
||||
Provide users with beta testing instructions:
|
||||
|
||||
```bash
|
||||
# Install/upgrade to beta
|
||||
uv tool install basic-memory --pre
|
||||
|
||||
# Or upgrade existing installation
|
||||
uv tool upgrade basic-memory --prerelease=allow
|
||||
```
|
||||
|
||||
## Version Guidelines
|
||||
- **First beta**: `v0.13.2b1`
|
||||
- **Subsequent betas**: `v0.13.2b2`, `v0.13.2b3`, etc.
|
||||
- **Release candidates**: `v0.13.2rc1`, `v0.13.2rc2`, etc.
|
||||
- **Final release**: `v0.13.2` (use `/release` command)
|
||||
|
||||
## Error Handling
|
||||
- If `just beta` fails, examine the error output for specific issues
|
||||
- If quality checks fail, fix issues and retry
|
||||
- If version format is invalid, correct and retry
|
||||
- If tag already exists, increment version number
|
||||
|
||||
## Success Output
|
||||
```
|
||||
✅ Beta Release v0.13.2b1 Created Successfully!
|
||||
|
||||
🏷️ Tag: v0.13.2b1
|
||||
🚀 GitHub Actions: Running
|
||||
📦 PyPI: Will be available in ~5 minutes as pre-release
|
||||
|
||||
Install/test with:
|
||||
uv tool install basic-memory --pre
|
||||
|
||||
Monitor release: https://github.com/basicmachines-co/basic-memory/actions
|
||||
```
|
||||
|
||||
## Beta Testing Workflow
|
||||
1. **Create beta**: Use `/beta v0.13.2b1`
|
||||
2. **Test features**: Install and validate new functionality
|
||||
3. **Fix issues**: Address bugs found during testing
|
||||
4. **Iterate**: Create `v0.13.2b2` if needed
|
||||
5. **Release candidate**: Create `v0.13.2rc1` when stable
|
||||
6. **Final release**: Use `/release v0.13.2` when ready
|
||||
|
||||
## Context
|
||||
- Beta releases are pre-releases for testing new features
|
||||
- Automatically published to PyPI with pre-release flag
|
||||
- Uses the automated justfile target for consistency
|
||||
- Version is automatically updated in `__init__.py`
|
||||
- Ideal for validating changes before stable release
|
||||
- Supports both beta (b1, b2) and release candidate (rc1, rc2) versions
|
||||
@@ -0,0 +1,160 @@
|
||||
# /changelog - Generate or Update Changelog Entry
|
||||
|
||||
Analyze commits and generate formatted changelog entry for a version.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/changelog <version> [type]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `version` (required): Version like `v0.14.0` or `v0.14.0b1`
|
||||
- `type` (optional): `beta`, `rc`, or `stable` (default: `stable`)
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert technical writer for the Basic Memory project. When the user runs `/changelog`, execute the following steps:
|
||||
|
||||
### Step 1: Version Analysis
|
||||
1. **Determine Commit Range**
|
||||
```bash
|
||||
# Find last release tag
|
||||
git tag -l "v*" --sort=-version:refname | grep -v "b\|rc" | head -1
|
||||
|
||||
# Get commits since last release
|
||||
git log --oneline ${last_tag}..HEAD
|
||||
```
|
||||
|
||||
2. **Parse Conventional Commits**
|
||||
- Extract feat: (features)
|
||||
- Extract fix: (bug fixes)
|
||||
- Extract BREAKING CHANGE: (breaking changes)
|
||||
- Extract chore:, docs:, test: (other improvements)
|
||||
|
||||
### Step 2: Categorize Changes
|
||||
1. **Features (feat:)**
|
||||
- New MCP tools
|
||||
- New CLI commands
|
||||
- New API endpoints
|
||||
- Major functionality additions
|
||||
|
||||
2. **Bug Fixes (fix:)**
|
||||
- User-facing bug fixes
|
||||
- Critical issues resolved
|
||||
- Performance improvements
|
||||
- Security fixes
|
||||
|
||||
3. **Technical Improvements**
|
||||
- Test coverage improvements
|
||||
- Code quality enhancements
|
||||
- Dependency updates
|
||||
- Documentation updates
|
||||
|
||||
4. **Breaking Changes**
|
||||
- API changes
|
||||
- Configuration changes
|
||||
- Behavior changes
|
||||
- Migration requirements
|
||||
|
||||
### Step 3: Generate Changelog Entry
|
||||
Create formatted entry following existing CHANGELOG.md style:
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## <version> (<date>)
|
||||
|
||||
### Features
|
||||
|
||||
- **Multi-Project Management System** - Switch between projects instantly during conversations
|
||||
([`993e88a`](https://github.com/basicmachines-co/basic-memory/commit/993e88a))
|
||||
- Instant project switching with session context
|
||||
- Project-specific operations and isolation
|
||||
- Project discovery and management tools
|
||||
|
||||
- **Advanced Note Editing** - Incremental editing with append, prepend, find/replace, and section operations
|
||||
([`6fc3904`](https://github.com/basicmachines-co/basic-memory/commit/6fc3904))
|
||||
- `edit_note` tool with multiple operation types
|
||||
- Smart frontmatter-aware editing
|
||||
- Validation and error handling
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#118**: Fix YAML tag formatting to follow standard specification
|
||||
([`2dc7e27`](https://github.com/basicmachines-co/basic-memory/commit/2dc7e27))
|
||||
|
||||
- **#110**: Make --project flag work consistently across CLI commands
|
||||
([`02dd91a`](https://github.com/basicmachines-co/basic-memory/commit/02dd91a))
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- **Comprehensive Testing** - 100% test coverage with integration testing
|
||||
([`468a22f`](https://github.com/basicmachines-co/basic-memory/commit/468a22f))
|
||||
- MCP integration test suite
|
||||
- End-to-end testing framework
|
||||
- Performance and edge case validation
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Database Migration**: Automatic migration from per-project to unified database.
|
||||
Data will be re-index from the filesystem, resulting in no data loss.
|
||||
- **Configuration Changes**: Projects now synced between config.json and database
|
||||
- **Full Backward Compatibility**: All existing setups continue to work seamlessly
|
||||
```
|
||||
|
||||
### Step 4: Integration
|
||||
1. **Update CHANGELOG.md**
|
||||
- Insert new entry at top
|
||||
- Maintain consistent formatting
|
||||
- Include commit links and issue references
|
||||
|
||||
2. **Validation**
|
||||
- Check all major changes are captured
|
||||
- Verify commit links work
|
||||
- Ensure issue numbers are correct
|
||||
|
||||
## Smart Analysis Features
|
||||
|
||||
### Automatic Classification
|
||||
- Detect feature additions from file changes
|
||||
- Identify bug fixes from commit messages
|
||||
- Find breaking changes from code analysis
|
||||
- Extract issue numbers from commit messages
|
||||
|
||||
### Content Enhancement
|
||||
- Add context for technical changes
|
||||
- Include migration guidance for breaking changes
|
||||
- Suggest installation/upgrade instructions
|
||||
- Link to relevant documentation
|
||||
|
||||
## Output Format
|
||||
|
||||
### For Beta Releases
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## v0.13.0b4 (2025-06-03)
|
||||
|
||||
### Beta Changes Since v0.13.0b3
|
||||
|
||||
- Fix FastMCP API compatibility issues
|
||||
- Update dependencies to latest versions
|
||||
- Resolve setuptools import error
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
uv tool install basic-memory --prerelease=allow
|
||||
```
|
||||
|
||||
### Known Issues
|
||||
- [List any known issues for beta testing]
|
||||
```
|
||||
|
||||
### For Stable Releases
|
||||
Full changelog with complete feature list, organized by impact and category.
|
||||
|
||||
## Context
|
||||
- Follows existing CHANGELOG.md format and style
|
||||
- Uses conventional commit standards
|
||||
- Includes GitHub commit links for traceability
|
||||
- Focuses on user-facing changes and value
|
||||
- Maintains consistency with previous entries
|
||||
@@ -0,0 +1,131 @@
|
||||
# /release-check - Pre-flight Release Validation
|
||||
|
||||
Comprehensive pre-flight check for release readiness without making any changes.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/release-check [version]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `version` (optional): Version to validate like `v0.13.0`. If not provided, determines from context.
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert QA engineer for the Basic Memory project. When the user runs `/release-check`, execute the following validation steps:
|
||||
|
||||
### Step 1: Environment Validation
|
||||
1. **Git Status Check**
|
||||
- Verify working directory is clean
|
||||
- Confirm on `main` branch
|
||||
- Check if ahead/behind origin
|
||||
|
||||
2. **Version Validation**
|
||||
- Validate version format if provided
|
||||
- Check for existing tags with same version
|
||||
- Verify version increments properly from last release
|
||||
|
||||
### Step 2: Code Quality Gates
|
||||
1. **Test Suite Validation**
|
||||
```bash
|
||||
just test
|
||||
```
|
||||
- All tests must pass
|
||||
- Check test coverage (target: 95%+)
|
||||
- Validate no skipped critical tests
|
||||
|
||||
2. **Code Quality Checks**
|
||||
```bash
|
||||
just lint
|
||||
just type-check
|
||||
```
|
||||
- No linting errors
|
||||
- No type checking errors
|
||||
- Code formatting is consistent
|
||||
|
||||
### Step 3: Documentation Validation
|
||||
1. **Changelog Check**
|
||||
- CHANGELOG.md contains entry for target version
|
||||
- Entry includes all major features and fixes
|
||||
- Breaking changes are documented
|
||||
|
||||
2. **Documentation Currency**
|
||||
- README.md reflects current functionality
|
||||
- CLI reference is up to date
|
||||
- MCP tools are documented
|
||||
|
||||
### Step 4: Dependency Validation
|
||||
1. **Security Scan**
|
||||
- No known vulnerabilities in dependencies
|
||||
- All dependencies are at appropriate versions
|
||||
- No conflicting dependency versions
|
||||
|
||||
2. **Build Validation**
|
||||
- Package builds successfully
|
||||
- All required files are included
|
||||
- No missing dependencies
|
||||
|
||||
### Step 5: Issue Tracking Validation
|
||||
1. **GitHub Issues Check**
|
||||
- No critical open issues blocking release
|
||||
- All milestone issues are resolved
|
||||
- High-priority bugs are fixed
|
||||
|
||||
2. **Testing Coverage**
|
||||
- Integration tests pass
|
||||
- MCP tool tests pass
|
||||
- Cross-platform compatibility verified
|
||||
|
||||
## Report Format
|
||||
|
||||
Generate a comprehensive report:
|
||||
|
||||
```
|
||||
🔍 Release Readiness Check for v0.13.0
|
||||
|
||||
✅ PASSED CHECKS:
|
||||
├── Git status clean
|
||||
├── On main branch
|
||||
├── All tests passing (744/744)
|
||||
├── Test coverage: 98.2%
|
||||
├── Type checking passed
|
||||
├── Linting passed
|
||||
├── CHANGELOG.md updated
|
||||
└── No critical issues open
|
||||
|
||||
⚠️ WARNINGS:
|
||||
├── 2 medium-priority issues still open
|
||||
└── Documentation could be updated
|
||||
|
||||
❌ BLOCKING ISSUES:
|
||||
└── None found
|
||||
|
||||
🎯 RELEASE READINESS: ✅ READY
|
||||
|
||||
Recommended next steps:
|
||||
1. Address warnings if desired
|
||||
2. Run `/release v0.13.0` when ready
|
||||
```
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
### Must Pass (Blocking)
|
||||
- [ ] All tests pass
|
||||
- [ ] No type errors
|
||||
- [ ] No linting errors
|
||||
- [ ] Working directory clean
|
||||
- [ ] On main branch
|
||||
- [ ] CHANGELOG.md has version entry
|
||||
- [ ] No critical open issues
|
||||
|
||||
### Should Pass (Warnings)
|
||||
- [ ] Test coverage >95%
|
||||
- [ ] No medium-priority open issues
|
||||
- [ ] Documentation up to date
|
||||
- [ ] No dependency vulnerabilities
|
||||
|
||||
## Context
|
||||
- This is a read-only validation - makes no changes
|
||||
- Provides confidence before running actual release
|
||||
- Helps identify issues early in release process
|
||||
- Can be run multiple times safely
|
||||
@@ -0,0 +1,92 @@
|
||||
# /release - Create Stable Release
|
||||
|
||||
Create a stable release using the automated justfile target with comprehensive validation.
|
||||
|
||||
## Usage
|
||||
```
|
||||
/release <version>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `version` (required): Release version like `v0.13.2`
|
||||
|
||||
## Implementation
|
||||
|
||||
You are an expert release manager for the Basic Memory project. When the user runs `/release`, execute the following steps:
|
||||
|
||||
### Step 1: Pre-flight Validation
|
||||
1. Verify version format matches `v\d+\.\d+\.\d+` pattern
|
||||
2. Check current git status for uncommitted changes
|
||||
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
|
||||
just release <version>
|
||||
```
|
||||
|
||||
The justfile target handles:
|
||||
- ✅ Version format validation
|
||||
- ✅ Git status and branch checks
|
||||
- ✅ Quality checks (`just check` - lint, format, type-check, tests)
|
||||
- ✅ Version update in `src/basic_memory/__init__.py`
|
||||
- ✅ Automatic commit with proper message
|
||||
- ✅ Tag creation and pushing to GitHub
|
||||
- ✅ Release workflow trigger
|
||||
|
||||
### Step 3: Monitor Release Process
|
||||
1. Check that GitHub Actions workflow starts successfully
|
||||
2. Monitor workflow completion at: https://github.com/basicmachines-co/basic-memory/actions
|
||||
3. Verify PyPI publication
|
||||
4. Test installation: `uv tool install basic-memory`
|
||||
|
||||
### Step 4: Post-Release Validation
|
||||
1. Verify GitHub release is created automatically
|
||||
2. Check PyPI publication
|
||||
3. Validate release assets
|
||||
4. Update any post-release documentation
|
||||
|
||||
## Pre-conditions Check
|
||||
Before starting, verify:
|
||||
- [ ] All beta testing is complete
|
||||
- [ ] Critical bugs are fixed
|
||||
- [ ] Breaking changes are documented
|
||||
- [ ] CHANGELOG.md is updated (if needed)
|
||||
- [ ] Version number follows semantic versioning
|
||||
|
||||
## Error Handling
|
||||
- If `just release` fails, examine the error output for specific issues
|
||||
- If quality checks fail, fix issues and retry
|
||||
- If changelog entry missing, update CHANGELOG.md and commit before retrying
|
||||
- If GitHub Actions fail, check workflow logs for debugging
|
||||
|
||||
## Success Output
|
||||
```
|
||||
🎉 Stable Release v0.13.2 Created Successfully!
|
||||
|
||||
🏷️ Tag: v0.13.2
|
||||
📋 GitHub Release: https://github.com/basicmachines-co/basic-memory/releases/tag/v0.13.2
|
||||
📦 PyPI: https://pypi.org/project/basic-memory/0.13.2/
|
||||
🚀 GitHub Actions: Completed
|
||||
|
||||
Install with:
|
||||
uv tool install basic-memory
|
||||
|
||||
Users can now upgrade:
|
||||
uv tool upgrade basic-memory
|
||||
```
|
||||
|
||||
## Context
|
||||
- This creates production releases used by end users
|
||||
- Must pass all quality gates before proceeding
|
||||
- Uses the automated justfile target for consistency
|
||||
- Version is automatically updated in `__init__.py`
|
||||
- Triggers automated GitHub release with changelog
|
||||
- Leverages uv-dynamic-versioning for package version management
|
||||
@@ -0,0 +1,595 @@
|
||||
# /project:test-live - Live Basic Memory Testing Suite
|
||||
|
||||
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
|
||||
```
|
||||
/project:test-live [phase]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `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 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
|
||||
|
||||
1. **Environment Verification**
|
||||
- Verify basic-memory is installed and accessible via MCP
|
||||
- Check version and confirm it's the expected release
|
||||
- Test MCP connection and tool availability
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
Create project: "basic-memory-testing-[timestamp]"
|
||||
Location: ~/basic-memory-testing-[timestamp]
|
||||
Purpose: Record all test observations and results
|
||||
```
|
||||
|
||||
Make sure to switch to the newly created project with the `switch_project()` tool.
|
||||
|
||||
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 0: Recent Changes Validation (if 'recent' or 'all' phase)
|
||||
|
||||
Based on recent commit analysis, create targeted test scenarios:
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
|
||||
3. **Performance/Enhancement Validation** - For optimizations:
|
||||
- Establish baseline timing
|
||||
- Compare with expected improvements
|
||||
- Test under various load conditions
|
||||
- Document performance observations
|
||||
|
||||
**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
|
||||
|
||||
### Phase 1: Core Functionality Validation (Tier 1 Tools)
|
||||
|
||||
Test essential MCP tools that form the foundation of Basic Memory:
|
||||
|
||||
**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
|
||||
|
||||
**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)
|
||||
|
||||
**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
|
||||
|
||||
**4. edit_note Tests (Critical):**
|
||||
- ✅ Append operations preserving frontmatter
|
||||
- ✅ Prepend operations
|
||||
- ✅ Find/replace with validation
|
||||
- ✅ Section replacement under headers
|
||||
- ⚠️ Error scenarios (invalid operations)
|
||||
|
||||
**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)
|
||||
- Empty projects and notes
|
||||
- Unicode, emojis, special symbols
|
||||
- Deeply nested folder structures
|
||||
- Circular relations and self-references
|
||||
- Maximum relation depths
|
||||
|
||||
**Error Scenarios:**
|
||||
- Invalid memory:// URLs
|
||||
- Missing files referenced in database
|
||||
- Invalid project names and paths
|
||||
- Malformed note structures
|
||||
- Concurrent operation conflicts
|
||||
|
||||
**Performance Testing:**
|
||||
- Create 100+ notes rapidly
|
||||
- Complex search queries
|
||||
- Deep relation chains (5+ levels)
|
||||
- Rapid successive operations
|
||||
- Memory usage monitoring
|
||||
|
||||
### Phase 5: Real-World Workflow Scenarios
|
||||
|
||||
**Meeting Notes Pipeline:**
|
||||
1. Create meeting notes with action items
|
||||
2. Extract action items using edit_note
|
||||
3. Build relations to project documents
|
||||
4. Update progress incrementally
|
||||
5. Search and track completion
|
||||
|
||||
**Research Knowledge Building:**
|
||||
1. Create research topic hierarchy
|
||||
2. Build complex relation networks
|
||||
3. Add incremental findings over time
|
||||
4. Search for connections and patterns
|
||||
5. Reorganize as knowledge evolves
|
||||
|
||||
**Multi-Project Workflow:**
|
||||
1. Technical documentation project
|
||||
2. Personal recipe collection project
|
||||
3. Learning/course notes project
|
||||
4. Switch contexts during conversation
|
||||
5. Cross-reference related concepts
|
||||
|
||||
**Content Evolution:**
|
||||
1. Start with basic notes
|
||||
2. Enhance with relations and observations
|
||||
3. Reorganize file structure using moves
|
||||
4. Update content with edit operations
|
||||
5. Validate knowledge graph integrity
|
||||
|
||||
### 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
|
||||
- Unusual but valid markdown structures
|
||||
- Creative observation categories
|
||||
- Novel relation types and patterns
|
||||
- Unexpected tool combinations
|
||||
|
||||
**Stress Scenarios:**
|
||||
- Bulk operations (many notes quickly)
|
||||
- Complex nested moves and edits
|
||||
- Deep context building
|
||||
- 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:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Test Session [Phase] YYYY-MM-DD HH:MM
|
||||
tags: [testing, v0.13.0, live-testing, [phase]]
|
||||
permalink: test-session-[phase]-[timestamp]
|
||||
---
|
||||
|
||||
# Test Session [Phase] - [Date/Time]
|
||||
|
||||
## Environment
|
||||
- Basic Memory version: [version]
|
||||
- MCP connection: [status]
|
||||
- Test project: [name]
|
||||
- Phase focus: [description]
|
||||
|
||||
## Test Results
|
||||
|
||||
### ✅ Successful Operations
|
||||
- [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) #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
|
||||
- search_notes needs fuzzy matching for typos #feature-idea
|
||||
- move_note could auto-suggest folder creation #usability
|
||||
|
||||
### 📊 Performance Metrics
|
||||
- Average write_note time: 0.3s
|
||||
- Search with 100+ notes: 0.6s
|
||||
- Project switch overhead: 0.1s
|
||||
- Memory usage: [observed levels]
|
||||
|
||||
## Relations
|
||||
- tests [[Basic Memory v0.13.0]]
|
||||
- part_of [[Live Testing Suite]]
|
||||
- found_issues [[Bug Report: Unicode Search]]
|
||||
- discovered [[Performance Optimization Opportunities]]
|
||||
```
|
||||
|
||||
## Quality Assessment Areas
|
||||
|
||||
**User Experience & Usability:**
|
||||
- Tool instruction clarity and examples
|
||||
- Error message actionability
|
||||
- Response time acceptability
|
||||
- Tool consistency and discoverability
|
||||
- Learning curve and intuitiveness
|
||||
|
||||
**System Behavior:**
|
||||
- Context preservation across operations
|
||||
- memory:// URL navigation reliability
|
||||
- Multi-step workflow cohesion
|
||||
- Edge case graceful handling
|
||||
- Recovery from user errors
|
||||
|
||||
**Documentation Alignment:**
|
||||
- Tool output clarity and helpfulness
|
||||
- Behavior vs. documentation accuracy
|
||||
- Example validity and usefulness
|
||||
- Real-world vs. documented workflows
|
||||
|
||||
**Mental Model Validation:**
|
||||
- Natural user expectation alignment
|
||||
- Surprising behavior identification
|
||||
- Mistake recovery ease
|
||||
- Knowledge graph concept naturalness
|
||||
|
||||
**Performance & Reliability:**
|
||||
- Operation completion times
|
||||
- Consistency across sessions
|
||||
- Scaling behavior with growth
|
||||
- Unexpected slowness identification
|
||||
|
||||
## Error Documentation Protocol
|
||||
|
||||
For each error discovered:
|
||||
|
||||
1. **Immediate Recording**
|
||||
- Create dedicated error note
|
||||
- Include exact reproduction steps
|
||||
- Capture error messages verbatim
|
||||
- Note system state when error occurred
|
||||
|
||||
2. **Error Note Format**
|
||||
```markdown
|
||||
---
|
||||
title: Bug Report - [Short Description]
|
||||
tags: [bug, testing, v0.13.0, [severity]]
|
||||
---
|
||||
|
||||
# Bug Report: [Description]
|
||||
|
||||
## Reproduction Steps
|
||||
1. [Exact steps to reproduce]
|
||||
2. [Include all parameters used]
|
||||
3. [Note any special conditions]
|
||||
|
||||
## Expected Behavior
|
||||
[What should have happened]
|
||||
|
||||
## Actual Behavior
|
||||
[What actually happened]
|
||||
|
||||
## Error Messages
|
||||
```
|
||||
[Exact error text]
|
||||
```
|
||||
|
||||
## Environment
|
||||
- Version: [version]
|
||||
- Project: [name]
|
||||
- Timestamp: [when]
|
||||
|
||||
## Severity
|
||||
- [ ] Critical (blocks major functionality)
|
||||
- [ ] High (impacts user experience)
|
||||
- [ ] Medium (workaround available)
|
||||
- [ ] Low (minor inconvenience)
|
||||
|
||||
## Relations
|
||||
- discovered_during [[Test Session [Phase]]]
|
||||
- affects [[Feature Name]]
|
||||
```
|
||||
|
||||
## Success Metrics Tracking
|
||||
|
||||
**Quantitative Measures:**
|
||||
- Test scenario completion rate
|
||||
- Bug discovery count with severity
|
||||
- Performance benchmark establishment
|
||||
- Tool coverage completeness
|
||||
|
||||
**Qualitative Measures:**
|
||||
- Conversation flow naturalness
|
||||
- Knowledge graph quality
|
||||
- User experience insights
|
||||
- System reliability assessment
|
||||
|
||||
## Test Execution Flow
|
||||
|
||||
1. **Setup Phase** (5 minutes)
|
||||
- Verify environment and create test project
|
||||
- Record baseline system state
|
||||
- Establish performance benchmarks
|
||||
|
||||
2. **Core Testing** (15-20 minutes per phase)
|
||||
- Execute test scenarios systematically
|
||||
- Record observations immediately
|
||||
- Note timestamps for performance tracking
|
||||
- Explore variations when interesting behaviors occur
|
||||
|
||||
3. **Documentation** (5 minutes per phase)
|
||||
- Create phase summary note
|
||||
- Link related test observations
|
||||
- Update running issues list
|
||||
- Record enhancement ideas
|
||||
|
||||
4. **Analysis Phase** (10 minutes)
|
||||
- Review all observations across phases
|
||||
- Identify patterns and trends
|
||||
- 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:**
|
||||
- Feature verification prioritized by tier importance
|
||||
- Recent changes validated for regression
|
||||
- Performance baseline establishment
|
||||
- Bug identification with severity assessment
|
||||
|
||||
**Knowledge Base Creation:**
|
||||
- Prioritized testing documentation
|
||||
- Real usage examples for user guides
|
||||
- Recent changes validation records
|
||||
- Performance insights for optimization
|
||||
|
||||
**Development Insights:**
|
||||
- Tier-based bug priority list
|
||||
- Recent changes impact assessment
|
||||
- Enhancement ideas from real usage
|
||||
- User experience improvement areas
|
||||
|
||||
## Post-Test Deliverables
|
||||
|
||||
1. **Test Summary Note**
|
||||
- Overall results and findings
|
||||
- Critical issues requiring immediate attention
|
||||
- Enhancement opportunities discovered
|
||||
- System readiness assessment
|
||||
|
||||
2. **Bug Report Collection**
|
||||
- All discovered issues with reproduction steps
|
||||
- Severity and impact assessments
|
||||
- Suggested fixes where applicable
|
||||
|
||||
3. **Performance Baseline**
|
||||
- Timing data for all operations
|
||||
- Scaling behavior observations
|
||||
- Resource usage patterns
|
||||
|
||||
4. **UX Improvement Recommendations**
|
||||
- Usability enhancement suggestions
|
||||
- Documentation improvement areas
|
||||
- Tool design optimization ideas
|
||||
|
||||
5. **Updated TESTING.md**
|
||||
- Incorporate new test scenarios discovered
|
||||
- Update based on real execution experience
|
||||
- Add performance benchmarks and targets
|
||||
|
||||
## Context
|
||||
- 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 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
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Development files
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.ruff_cache/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Testing
|
||||
test*/
|
||||
tests/
|
||||
*.test.js
|
||||
*.test.py
|
||||
pytest.ini
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
.tox/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Claude
|
||||
.claude
|
||||
|
||||
# Git files
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation build
|
||||
docs/_build/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# DXT package output
|
||||
*.dxt
|
||||
dxt-bundle/
|
||||
|
||||
|
||||
# Lock files
|
||||
uv.lock
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
justfile
|
||||
smithery.yaml
|
||||
|
||||
# Project files
|
||||
.python.version
|
||||
CHANGELOG.md
|
||||
CITATION.cff
|
||||
CLA.md
|
||||
CLAUDE.md
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
README.md
|
||||
SECURITY.md
|
||||
docs/
|
||||
llms-install.md
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Basic Memory
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Steps To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1. Install version '...'
|
||||
2. Run command '...'
|
||||
3. Use tool/feature '...'
|
||||
4. See error
|
||||
|
||||
## Expected Behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Actual Behavior
|
||||
What actually happened, including error messages and output.
|
||||
|
||||
## Environment
|
||||
- OS: [e.g. macOS 14.2, Ubuntu 22.04]
|
||||
- Python version: [e.g. 3.12.1]
|
||||
- Basic Memory version: [e.g. 0.1.0]
|
||||
- Installation method: [e.g. pip, uv, source]
|
||||
- Claude Desktop version (if applicable):
|
||||
|
||||
## Additional Context
|
||||
- Configuration files (if relevant)
|
||||
- Logs or screenshots
|
||||
- Any special configuration or environment variables
|
||||
|
||||
## Possible Solution
|
||||
If you have any ideas on what might be causing the issue or how to fix it, please share them here.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Basic Memory Discussions
|
||||
url: https://github.com/basicmachines-co/basic-memory/discussions
|
||||
about: For questions, ideas, or more open-ended discussions
|
||||
- name: Documentation
|
||||
url: https://github.com/basicmachines-co/basic-memory#readme
|
||||
about: Please check the documentation first before reporting an issue
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Documentation improvement
|
||||
about: Suggest improvements or report issues with documentation
|
||||
title: '[DOCS] '
|
||||
labels: documentation
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Documentation Issue
|
||||
Describe what's missing, unclear, or incorrect in the current documentation.
|
||||
|
||||
## Location
|
||||
Where is the problematic documentation? (URL, file path, or section)
|
||||
|
||||
## Suggested Improvement
|
||||
How would you improve this documentation? Please be as specific as possible.
|
||||
|
||||
## Additional Context
|
||||
Any additional information or screenshots that might help explain the issue or improvement.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Basic Memory
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Feature Description
|
||||
A clear and concise description of the feature you'd like to see implemented.
|
||||
|
||||
## Problem This Feature Solves
|
||||
Describe the problem or limitation you're experiencing that this feature would address.
|
||||
|
||||
## Proposed Solution
|
||||
Describe how you envision this feature working. Include:
|
||||
- User workflow
|
||||
- Interface design (if applicable)
|
||||
- Technical approach (if you have ideas)
|
||||
|
||||
## Alternative Solutions
|
||||
Have you considered any alternative solutions or workarounds?
|
||||
|
||||
## Additional Context
|
||||
Add any other context, screenshots, or examples about the feature request here.
|
||||
|
||||
## Impact
|
||||
How would this feature benefit you and other users of Basic Memory?
|
||||
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check user permissions
|
||||
id: check_membership
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let actor;
|
||||
if (context.eventName === 'issue_comment') {
|
||||
actor = context.payload.comment.user.login;
|
||||
} else if (context.eventName === 'pull_request_review_comment') {
|
||||
actor = context.payload.comment.user.login;
|
||||
} else if (context.eventName === 'pull_request_review') {
|
||||
actor = context.payload.review.user.login;
|
||||
} else if (context.eventName === 'issues') {
|
||||
actor = context.payload.issue.user.login;
|
||||
}
|
||||
|
||||
console.log(`Checking permissions for user: ${actor}`);
|
||||
|
||||
// List of explicitly allowed users (organization members)
|
||||
const allowedUsers = [
|
||||
'phernandez',
|
||||
'groksrc',
|
||||
'nellins',
|
||||
'bm-claudeai'
|
||||
];
|
||||
|
||||
if (allowedUsers.includes(actor)) {
|
||||
console.log(`User ${actor} is in the allowed list`);
|
||||
core.setOutput('is_member', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Check if user has repository permissions
|
||||
try {
|
||||
const collaboration = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: actor
|
||||
});
|
||||
|
||||
const permission = collaboration.data.permission;
|
||||
console.log(`User ${actor} has permission level: ${permission}`);
|
||||
|
||||
// Allow if user has push access or higher (write, maintain, admin)
|
||||
const allowed = ['write', 'maintain', 'admin'].includes(permission);
|
||||
|
||||
core.setOutput('is_member', allowed);
|
||||
|
||||
if (!allowed) {
|
||||
core.notice(`User ${actor} does not have sufficient repository permissions (has: ${permission})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error checking permissions: ${error.message}`);
|
||||
|
||||
// Final fallback: Check if user is a public member of the organization
|
||||
try {
|
||||
const membership = await github.rest.orgs.getMembershipForUser({
|
||||
org: 'basicmachines-co',
|
||||
username: actor
|
||||
});
|
||||
|
||||
const allowed = membership.data.state === 'active';
|
||||
core.setOutput('is_member', allowed);
|
||||
|
||||
if (!allowed) {
|
||||
core.notice(`User ${actor} is not a public member of basicmachines-co organization`);
|
||||
}
|
||||
} catch (membershipError) {
|
||||
console.log(`Error checking organization membership: ${membershipError.message}`);
|
||||
core.setOutput('is_member', false);
|
||||
core.notice(`User ${actor} does not have access to this repository`);
|
||||
}
|
||||
}
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check_membership.outputs.is_member == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
if: steps.check_membership.outputs.is_member == 'true'
|
||||
id: claude
|
||||
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, mcp__web_search
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Dev Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
dev-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
uv venv
|
||||
uv sync
|
||||
uv build
|
||||
|
||||
- name: Check if this is a dev version
|
||||
id: check_version
|
||||
run: |
|
||||
VERSION=$(uv run python -c "import basic_memory; print(basic_memory.__version__)")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
if [[ "$VERSION" == *"dev"* ]]; then
|
||||
echo "is_dev=true" >> $GITHUB_OUTPUT
|
||||
echo "Dev version detected: $VERSION"
|
||||
else
|
||||
echo "is_dev=false" >> $GITHUB_OUTPUT
|
||||
echo "Release version detected: $VERSION, skipping dev release"
|
||||
fi
|
||||
|
||||
- name: Publish dev version to PyPI
|
||||
if: steps.check_version.outputs.is_dev == 'true'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
skip-existing: true # Don't fail if version already exists
|
||||
@@ -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
|
||||
|
||||
@@ -1,96 +1,85 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: 'Type of version bump (major, minor, patch)'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Trigger on version tags like v1.0.0, v0.13.0, etc.
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency: release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
outputs:
|
||||
released: ${{ steps.release.outputs.released }}
|
||||
tag: ${{ steps.release.outputs.tag }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Python Semantic Release
|
||||
id: release
|
||||
uses: python-semantic-release/python-semantic-release@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
if: steps.release.outputs.released == 'true'
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to GitHub Release Assets
|
||||
uses: python-semantic-release/publish-action@v9.8.9
|
||||
if: steps.release.outputs.released == 'true'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ steps.release.outputs.tag }}
|
||||
|
||||
build-macos:
|
||||
needs: release
|
||||
if: needs.release.outputs.released == 'true'
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.tag }}
|
||||
|
||||
- name: Set up Python "3.12"
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install librsvg
|
||||
run: brew install librsvg
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Create virtual env
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
uv venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync
|
||||
uv build
|
||||
|
||||
- name: Build macOS installer
|
||||
- name: Verify build succeeded
|
||||
run: |
|
||||
make installer-mac
|
||||
xattr -dr com.apple.quarantine "installer/build/Basic Memory Installer.app"
|
||||
# Verify that build artifacts exist
|
||||
ls -la dist/
|
||||
echo "Build completed successfully"
|
||||
|
||||
- name: Zip macOS installer
|
||||
run: |
|
||||
cd installer/build
|
||||
zip -ry "Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip" "Basic Memory Installer.app"
|
||||
|
||||
- name: Upload macOS installer
|
||||
uses: softprops/action-gh-release@v1
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: installer/build/Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip
|
||||
tag_name: ${{ needs.release.outputs.tag }}
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
generate_release_notes: true
|
||||
tag_name: ${{ github.ref_name }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
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 }}
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ on:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
# pull_request_target runs on the BASE of the PR, not the merge result.
|
||||
# It has write permissions and access to secrets.
|
||||
# It's useful for PRs from forks or automated PRs but requires careful use for security reasons.
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
|
||||
pull_request_target:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -29,6 +35,10 @@ jobs:
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Install just
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -39,9 +49,9 @@ jobs:
|
||||
|
||||
- name: Run type checks
|
||||
run: |
|
||||
uv run make type-check
|
||||
just type-check
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
uv pip install pytest pytest-cov
|
||||
uv run make test
|
||||
just test
|
||||
|
||||
+15
-1
@@ -42,4 +42,18 @@ ENV/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
/.coverage.*
|
||||
.coverage.*
|
||||
|
||||
# obsidian docs:
|
||||
/docs/.obsidian/
|
||||
/examples/.obsidian/
|
||||
/examples/.basic-memory/
|
||||
|
||||
|
||||
# claude action
|
||||
claude-output
|
||||
**/.claude/settings.local.json
|
||||
|
||||
# DXT package files
|
||||
basic-memory.dxt
|
||||
dxt-bundle/
|
||||
+940
-1
@@ -1,5 +1,944 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 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
|
||||
|
||||
- **CLI**: Fixed `basic-memory project` project management commands that were not working in v0.13.0 (#129)
|
||||
- **Projects**: Resolved case sensitivity issues when switching between projects that caused "Project not found" errors (#127)
|
||||
- **API**: Standardized CLI project command endpoints and improved error handling
|
||||
- **Core**: Implemented consistent project name handling using permalinks to avoid case-related conflicts
|
||||
|
||||
### Changes
|
||||
|
||||
- Renamed `basic-memory project sync` command to `basic-memory project sync-config` for clarity
|
||||
- Improved project switching reliability across different case variations
|
||||
- Removed redundant server status messages from CLI error outputs
|
||||
|
||||
## v0.13.0 (2025-06-11)
|
||||
|
||||
### Overview
|
||||
|
||||
Basic Memory v0.13.0 is a **major release** that transforms Basic Memory into a true multi-project knowledge management system. This release introduces fluid project switching, advanced note editing capabilities, robust file management, and production-ready OAuth authentication - all while maintaining full backward compatibility.
|
||||
|
||||
**What's New for Users:**
|
||||
- 🎯 **Switch between projects instantly** during conversations with Claude
|
||||
- ✏️ **Edit notes incrementally** without rewriting entire documents
|
||||
- 📁 **Move and organize notes** with full database consistency
|
||||
- 📖 **View notes as formatted artifacts** for better readability in Claude Desktop
|
||||
- 🔍 **Search frontmatter tags** to discover content more easily
|
||||
- 🔐 **OAuth authentication** for secure remote access
|
||||
- ⚡ **Development builds** automatically published for beta testing
|
||||
|
||||
**Key v0.13.0 Accomplishments:**
|
||||
- ✅ **Complete Project Management System** - Project switching and project-specific operations
|
||||
- ✅ **Advanced Note Editing** - Incremental editing with append, prepend, find/replace, and section operations
|
||||
- ✅ **View Notes as Artifacts in Claude Desktop/Web** - Use the view_note tool to view a note as an artifact
|
||||
- ✅ **File Management System** - Full move operations with database consistency and rollback protection
|
||||
- ✅ **Enhanced Search Capabilities** - Frontmatter tags now searchable, improved content discoverability
|
||||
- ✅ **Unified Database Architecture** - Single app-level database for better performance and project management
|
||||
|
||||
### Major Features
|
||||
|
||||
#### 1. Multiple Project Management
|
||||
|
||||
**Switch between projects instantly 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]
|
||||
```
|
||||
|
||||
**Key Capabilities:**
|
||||
- **Instant Project Switching**: Change project context mid-conversation without restart
|
||||
- **Project-Specific Operations**: Operations work within the currently active project context
|
||||
- **Project Discovery**: List all available projects with status indicators
|
||||
- **Session Context**: Maintains active project throughout conversation
|
||||
- **Backward Compatibility**: Existing single-project setups continue to work seamlessly
|
||||
|
||||
#### 2. Advanced Note Editing
|
||||
|
||||
**Edit notes incrementally without rewriting entire documents:**
|
||||
|
||||
```python
|
||||
# Append new sections to existing notes
|
||||
edit_note("project-planning", "append", "\n## New Requirements\n- Feature X\n- Feature Y")
|
||||
|
||||
# Prepend timestamps to meeting notes
|
||||
edit_note("meeting-notes", "prepend", "## 2025-05-27 Update\n- Progress update...")
|
||||
|
||||
# Replace specific sections under headers
|
||||
edit_note("api-spec", "replace_section", "New implementation details", section="## Implementation")
|
||||
|
||||
# Find and replace with validation
|
||||
edit_note("config", "find_replace", "v0.13.0", find_text="v0.12.0", expected_replacements=2)
|
||||
```
|
||||
|
||||
**Key Capabilities:**
|
||||
- **Append Operations**: Add content to end of notes (most common use case)
|
||||
- **Prepend Operations**: Add content to beginning of notes
|
||||
- **Section Replacement**: Replace content under specific markdown headers
|
||||
- **Find & Replace**: Simple text replacements with occurrence counting
|
||||
- **Smart Error Handling**: Helpful guidance when operations fail
|
||||
- **Project Context**: Works within the active project with session awareness
|
||||
|
||||
#### 3. Smart File Management
|
||||
|
||||
**Move and organize notes:**
|
||||
|
||||
```python
|
||||
# Simple moves with automatic folder creation
|
||||
move_note("my-note", "work/projects/my-note.md")
|
||||
|
||||
# Organize within the active project
|
||||
move_note("shared-doc", "archive/old-docs/shared-doc.md")
|
||||
|
||||
# Rename operations
|
||||
move_note("old-name", "same-folder/new-name.md")
|
||||
```
|
||||
|
||||
**Key Capabilities:**
|
||||
- **Database Consistency**: Updates file paths, permalinks, and checksums automatically
|
||||
- **Search Reindexing**: Maintains search functionality after moves
|
||||
- **Folder Creation**: Automatically creates destination directories
|
||||
- **Project Isolation**: Operates within the currently active project
|
||||
- **Link Preservation**: Maintains internal links and references
|
||||
|
||||
#### 4. Enhanced Search & Discovery
|
||||
|
||||
**Find content more easily with improved search capabilities:**
|
||||
|
||||
- **Frontmatter Tag Search**: Tags from YAML frontmatter are now indexed and searchable
|
||||
- **Improved Content Discovery**: Search across titles, content, tags, and metadata
|
||||
- **Project-Scoped Search**: Search within the currently active project
|
||||
- **Better Search Quality**: Enhanced FTS5 indexing with tag content inclusion
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
tags: [coffee, brewing, equipment]
|
||||
---
|
||||
```
|
||||
Now searchable by: "coffee", "brewing", "equipment", or "Coffee Brewing Methods"
|
||||
|
||||
#### 5. Unified Database Architecture
|
||||
|
||||
**Single app-level database for better performance and project management:**
|
||||
|
||||
- **Migration from Per-Project DBs**: Moved from multiple SQLite files to single app database
|
||||
- **Project Isolation**: Proper data separation with project_id foreign keys
|
||||
- **Better Performance**: Optimized queries and reduced file I/O
|
||||
|
||||
### Complete MCP Tool Suite
|
||||
|
||||
#### New Project Management Tools
|
||||
- **`list_projects()`** - Discover and list all available projects with status
|
||||
- **`switch_project(project_name)`** - Change active project context during conversations
|
||||
- **`get_current_project()`** - Show currently active project with statistics
|
||||
- **`set_default_project(project_name)`** - Update default project configuration
|
||||
- **`sync_status()`** - Check file synchronization status and background operations
|
||||
|
||||
#### New Note Operations Tools
|
||||
- **`edit_note()`** - Incremental note editing (append, prepend, find/replace, section replace)
|
||||
- **`move_note()`** - Move notes with database consistency and search reindexing
|
||||
- **`view_note()`** - Display notes as formatted artifacts for better readability in Claude Desktop
|
||||
|
||||
#### Enhanced Existing Tools
|
||||
All existing tools now support:
|
||||
- **Session context awareness** (operates within the currently active project)
|
||||
- **Enhanced error messages** with project context metadata
|
||||
- **Improved response formatting** with project information footers
|
||||
- **Project isolation** ensures operations stay within the correct project boundaries
|
||||
|
||||
|
||||
### User Experience Improvements
|
||||
|
||||
#### Installation Options
|
||||
|
||||
**Multiple ways to install and test Basic Memory:**
|
||||
|
||||
```bash
|
||||
# Stable release
|
||||
uv tool install basic-memory
|
||||
|
||||
# Beta/pre-releases
|
||||
uv tool install basic-memory --pre
|
||||
```
|
||||
|
||||
|
||||
#### Bug Fixes & Quality Improvements
|
||||
|
||||
**Major issues resolved in v0.13.0:**
|
||||
|
||||
- **#118**: Fixed YAML tag formatting to follow standard specification
|
||||
- **#110**: Fixed `--project` flag consistency across all CLI commands
|
||||
- **#107**: Fixed write_note update failures with existing notes
|
||||
- **#93**: Fixed custom permalink handling in frontmatter
|
||||
- **#52**: Enhanced search capabilities with frontmatter tag indexing
|
||||
- **FTS5 Search**: Fixed special character handling in search queries
|
||||
- **Error Handling**: Improved error messages and validation across all tools
|
||||
|
||||
### Breaking Changes & Migration
|
||||
|
||||
#### For Existing Users
|
||||
|
||||
**Automatic Migration**: First run will automatically migrate existing data to the new unified database structure. No manual action required.
|
||||
|
||||
**What Changes:**
|
||||
- Database location: Moved to `~/.basic-memory/memory.db` (unified across projects)
|
||||
- Configuration: Projects defined in `~/.basic-memory/config.json` are synced with database
|
||||
|
||||
**What Stays the Same:**
|
||||
- All existing notes and data remain unchanged
|
||||
- Default project behavior maintained for single-project users
|
||||
- All existing MCP tools continue to work without modification
|
||||
|
||||
### Documentation & Resources
|
||||
|
||||
#### New Documentation
|
||||
- [Project Management Guide](docs/Project%20Management.md) - Multi-project workflows
|
||||
- [Note Editing Guide](docs/Note%20Editing.md) - Advanced editing techniques
|
||||
|
||||
#### Updated Documentation
|
||||
- [README.md](README.md) - Installation options and beta build instructions
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Release process and version management
|
||||
- [CLAUDE.md](CLAUDE.md) - Development workflow and CI/CD documentation
|
||||
- [Claude.ai Integration](docs/Claude.ai%20Integration.md) - Updated MCP tool examples
|
||||
|
||||
#### Quick Start Examples
|
||||
|
||||
**Project Switching:**
|
||||
```
|
||||
💬 "Switch to my work project and show recent activity"
|
||||
🤖 [Calls switch_project("work") then recent_activity()]
|
||||
```
|
||||
|
||||
**Note Editing:**
|
||||
```
|
||||
💬 "Add a section about deployment to my API docs"
|
||||
🤖 [Calls edit_note("api-docs", "append", "## Deployment\n...")]
|
||||
```
|
||||
|
||||
**File Organization:**
|
||||
```
|
||||
💬 "Move my old meeting notes to the archive folder"
|
||||
🤖 [Calls move_note("meeting-notes", "archive/old-meetings.md")]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## v0.12.3 (2025-04-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add extra logic for permalink generation with mixed Latin unicode and Chinese characters
|
||||
([`73ea91f`](https://github.com/basicmachines-co/basic-memory/commit/73ea91fe0d1f7ab89b99a1b691d59fe608b7fcbb))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Modify recent_activity args to be strings instead of enums
|
||||
([`3c1cc34`](https://github.com/basicmachines-co/basic-memory/commit/3c1cc346df519e703fae6412d43a92c7232c6226))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.12.2 (2025-04-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Utf8 for all file reads/write/open instead of default platform encoding
|
||||
([#91](https://github.com/basicmachines-co/basic-memory/pull/91),
|
||||
[`2934176`](https://github.com/basicmachines-co/basic-memory/commit/29341763318408ea8f1e954a41046c4185f836c6))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.12.1 (2025-04-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Run migrations and sync when starting mcp
|
||||
([#88](https://github.com/basicmachines-co/basic-memory/pull/88),
|
||||
[`78a3412`](https://github.com/basicmachines-co/basic-memory/commit/78a3412bcff83b46e78e26f8b9fce42ed9e05991))
|
||||
|
||||
|
||||
## v0.12.0 (2025-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [bug] `#` character accumulation in markdown frontmatter tags prop
|
||||
([#79](https://github.com/basicmachines-co/basic-memory/pull/79),
|
||||
[`6c19c9e`](https://github.com/basicmachines-co/basic-memory/commit/6c19c9edf5131054ba201a109b37f15c83ef150c))
|
||||
|
||||
- [bug] Cursor has errors calling search tool
|
||||
([#78](https://github.com/basicmachines-co/basic-memory/pull/78),
|
||||
[`9d581ce`](https://github.com/basicmachines-co/basic-memory/commit/9d581cee133f9dde4a0a85118868227390c84161))
|
||||
|
||||
- [bug] Some notes never exit "modified" status
|
||||
([#77](https://github.com/basicmachines-co/basic-memory/pull/77),
|
||||
[`7930ddb`](https://github.com/basicmachines-co/basic-memory/commit/7930ddb2919057be30ceac8c4c19da6aaa1d3e92))
|
||||
|
||||
- [bug] write_note Tool Fails to Update Existing Files in Some Situations.
|
||||
([#80](https://github.com/basicmachines-co/basic-memory/pull/80),
|
||||
[`9bff1f7`](https://github.com/basicmachines-co/basic-memory/commit/9bff1f732e71bc60f88b5c2ce3db5a2aa60b8e28))
|
||||
|
||||
- Set default mcp log level to ERROR
|
||||
([#81](https://github.com/basicmachines-co/basic-memory/pull/81),
|
||||
[`248214c`](https://github.com/basicmachines-co/basic-memory/commit/248214cb114a269ca60ff6398e382f9e2495ad8e))
|
||||
|
||||
- Write_note preserves frontmatter fields in content
|
||||
([#84](https://github.com/basicmachines-co/basic-memory/pull/84),
|
||||
[`3f4d9e4`](https://github.com/basicmachines-co/basic-memory/commit/3f4d9e4d872ebc0ed719c61b24d803c14a9db5e6))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add VS Code instructions to README
|
||||
([#76](https://github.com/basicmachines-co/basic-memory/pull/76),
|
||||
[`43cbb7b`](https://github.com/basicmachines-co/basic-memory/commit/43cbb7b38cc0482ac0a41b6759320e3588186e43))
|
||||
|
||||
- Updated basicmachines.co links to be https
|
||||
([#69](https://github.com/basicmachines-co/basic-memory/pull/69),
|
||||
[`40ea28b`](https://github.com/basicmachines-co/basic-memory/commit/40ea28b0bfc60012924a69ecb76511daa4c7d133))
|
||||
|
||||
### Features
|
||||
|
||||
- Add watch to mcp process ([#83](https://github.com/basicmachines-co/basic-memory/pull/83),
|
||||
[`00c8633`](https://github.com/basicmachines-co/basic-memory/commit/00c8633cfcee75ff640ff8fe81dafeb956281a94))
|
||||
|
||||
- Permalink enhancements ([#82](https://github.com/basicmachines-co/basic-memory/pull/82),
|
||||
[`617e60b`](https://github.com/basicmachines-co/basic-memory/commit/617e60bda4a590678a5f551f10a73e7b47e3b13e))
|
||||
|
||||
- Avoiding "useless permalink values" for files without metadata - Enable permalinks to be updated
|
||||
on move via config setting
|
||||
|
||||
|
||||
## v0.11.0 (2025-03-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Just delete db for reset db instead of using migrations.
|
||||
([#65](https://github.com/basicmachines-co/basic-memory/pull/65),
|
||||
[`0743ade`](https://github.com/basicmachines-co/basic-memory/commit/0743ade5fc07440f95ecfd816ba7e4cfd74bca12))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Make logs for each process - mcp, sync, cli
|
||||
([#64](https://github.com/basicmachines-co/basic-memory/pull/64),
|
||||
[`f1c9570`](https://github.com/basicmachines-co/basic-memory/commit/f1c95709cbffb1b88292547b0b8f29fcca22d186))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update broken "Multiple Projects" link in README.md
|
||||
([#55](https://github.com/basicmachines-co/basic-memory/pull/55),
|
||||
[`3c68b7d`](https://github.com/basicmachines-co/basic-memory/commit/3c68b7d5dd689322205c67637dca7d188111ee6b))
|
||||
|
||||
### Features
|
||||
|
||||
- Add bm command alias for basic-memory
|
||||
([#67](https://github.com/basicmachines-co/basic-memory/pull/67),
|
||||
[`069c0a2`](https://github.com/basicmachines-co/basic-memory/commit/069c0a21c630784e1bf47d2b7de5d6d1f6fadd7a))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Rename search tool to search_notes
|
||||
([#66](https://github.com/basicmachines-co/basic-memory/pull/66),
|
||||
[`b278276`](https://github.com/basicmachines-co/basic-memory/commit/b27827671dc010be3e261b8b221aca6b7f836661))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.10.1 (2025-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make set_default_project also activate project for current session to fix #37
|
||||
([`cbe72be`](https://github.com/basicmachines-co/basic-memory/commit/cbe72be10a646c0b03931bb39aff9285feae47f9))
|
||||
|
||||
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
|
||||
for future invocations (persistent change) 2. Activate the project for the current session
|
||||
(immediate change)
|
||||
|
||||
Added tests to verify this behavior, which resolves issue #37 where the project name and path
|
||||
weren't changing properly when the default project was changed.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Make set_default_project also activate project for current session to fix #37
|
||||
([`46c4fd2`](https://github.com/basicmachines-co/basic-memory/commit/46c4fd21645b109af59eb2a0201c7bd849b34a49))
|
||||
|
||||
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
|
||||
for future invocations (persistent change) 2. Activate the project for the current session
|
||||
(immediate change)
|
||||
|
||||
Added tests to verify this behavior, which resolves issue #37 where the project name and path
|
||||
weren't changing properly when the default project was changed.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Move ai_assistant_guide.md into package resources to fix #39
|
||||
([`390ff9d`](https://github.com/basicmachines-co/basic-memory/commit/390ff9d31ccee85bef732e8140b5eeecd7ee176f))
|
||||
|
||||
This change relocates the AI assistant guide from the static directory into the package resources
|
||||
directory, ensuring it gets properly included in the distribution package and is accessible when
|
||||
installed via pip/uv.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Move ai_assistant_guide.md into package resources to fix #39
|
||||
([`cc2cae7`](https://github.com/basicmachines-co/basic-memory/commit/cc2cae72c14b380f78ffeb67c2261e4dbee45faf))
|
||||
|
||||
This change relocates the AI assistant guide from the static directory into the package resources
|
||||
directory, ensuring it gets properly included in the distribution package and is accessible when
|
||||
installed via pip/uv.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Preserve custom frontmatter fields when updating notes
|
||||
([`78f234b`](https://github.com/basicmachines-co/basic-memory/commit/78f234b1806b578a0a833e8ee4184015b7369a97))
|
||||
|
||||
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
|
||||
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
|
||||
notes are updated through the write_note MCP tool.
|
||||
|
||||
Added test case that verifies this behavior by creating a note with custom frontmatter and then
|
||||
updating it.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Preserve custom frontmatter fields when updating notes
|
||||
([`e716946`](https://github.com/basicmachines-co/basic-memory/commit/e716946b4408d017eca4be720956d5a210b4e6b1))
|
||||
|
||||
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
|
||||
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
|
||||
notes are updated through the write_note MCP tool.
|
||||
|
||||
Added test case that verifies this behavior by creating a note with custom frontmatter and then
|
||||
updating it.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Chores
|
||||
|
||||
- Remove duplicate code in entity_service.py from bad merge
|
||||
([`681af5d`](https://github.com/basicmachines-co/basic-memory/commit/681af5d4505dadc40b4086630f739d76bac9201d))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add help docs to mcp cli tools
|
||||
([`731b502`](https://github.com/basicmachines-co/basic-memory/commit/731b502d36cec253d114403d73b48fab3c47786e))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Add mcp badge, update cli reference, llms-install.md
|
||||
([`b26afa9`](https://github.com/basicmachines-co/basic-memory/commit/b26afa927f98021246cd8b64858e57333595ea90))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Update CLAUDE.md ([#33](https://github.com/basicmachines-co/basic-memory/pull/33),
|
||||
[`dfaf0fe`](https://github.com/basicmachines-co/basic-memory/commit/dfaf0fea9cf5b97d169d51a6276ec70162c21a7e))
|
||||
|
||||
fix spelling in CLAUDE.md: enviroment -> environment Signed-off-by: Ikko Eltociear Ashimine
|
||||
<eltociear@gmail.com>
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Move project stats into projct subcommand
|
||||
([`2a881b1`](https://github.com/basicmachines-co/basic-memory/commit/2a881b1425c73947f037fbe7ac5539c015b62526))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.10.0 (2025-03-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`da97353`](https://github.com/basicmachines-co/basic-memory/commit/da97353cfc3acc1ceb0eca22ac6af326f77dc199))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`c4732a4`](https://github.com/basicmachines-co/basic-memory/commit/c4732a47b37dd2e404139fb283b65556c81ce7c9))
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`2e9d673`](https://github.com/basicmachines-co/basic-memory/commit/2e9d673e54ad6a63a971db64f01fc2f4e59c2e69))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Don't sync *.tmp files on watch ([#31](https://github.com/basicmachines-co/basic-memory/pull/31),
|
||||
[`6b110b2`](https://github.com/basicmachines-co/basic-memory/commit/6b110b28dd8ba705ebfc0bcb41faf2cb993da2c3))
|
||||
|
||||
Fixes #30
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Drop search_index table on db reindex
|
||||
([`31cca6f`](https://github.com/basicmachines-co/basic-memory/commit/31cca6f913849a0ab8fc944803533e3072e9ef88))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Improve utf-8 support for file reading/writing
|
||||
([#32](https://github.com/basicmachines-co/basic-memory/pull/32),
|
||||
[`eb5e4ec`](https://github.com/basicmachines-co/basic-memory/commit/eb5e4ec6bd4d2fe757087be030d867f4ca1d38ba))
|
||||
|
||||
fixes #29
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Chores
|
||||
|
||||
- Remove logfire
|
||||
([`9bb8a02`](https://github.com/basicmachines-co/basic-memory/commit/9bb8a020c3425a02cb3a88f6f02adcd281bccee2))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add glama badge. Fix typos in README.md
|
||||
([#28](https://github.com/basicmachines-co/basic-memory/pull/28),
|
||||
[`9af913d`](https://github.com/basicmachines-co/basic-memory/commit/9af913da4fba7bb4908caa3f15f2db2aa03777ec))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Update CLAUDE.md with GitHub integration capabilities
|
||||
([#25](https://github.com/basicmachines-co/basic-memory/pull/25),
|
||||
[`fea2f40`](https://github.com/basicmachines-co/basic-memory/commit/fea2f40d1b54d0c533e6d7ee7ce1aa7b83ad9a47))
|
||||
|
||||
This PR updates the CLAUDE.md file to document the GitHub integration capabilities that enable
|
||||
Claude to participate directly in the development workflow.
|
||||
|
||||
### Features
|
||||
|
||||
- Add Smithery integration for easier installation
|
||||
([#24](https://github.com/basicmachines-co/basic-memory/pull/24),
|
||||
[`eb1e7b6`](https://github.com/basicmachines-co/basic-memory/commit/eb1e7b6088b0b3dead9c104ee44174b2baebf417))
|
||||
|
||||
This PR adds support for deploying Basic Memory on the Smithery platform.
|
||||
|
||||
Signed-off-by: bm-claudeai <claude@basicmachines.co>
|
||||
|
||||
|
||||
## v0.9.0 (2025-03-07)
|
||||
|
||||
### Chores
|
||||
|
||||
- Pre beta prep ([#20](https://github.com/basicmachines-co/basic-memory/pull/20),
|
||||
[`6a4bd54`](https://github.com/basicmachines-co/basic-memory/commit/6a4bd546466a45107007b5000276b6c9bb62ef27))
|
||||
|
||||
fix: drop search_index table on db reindex
|
||||
|
||||
fix: ai_resource_guide.md path
|
||||
|
||||
chore: remove logfire
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update README.md and CLAUDE.md
|
||||
([`182ec78`](https://github.com/basicmachines-co/basic-memory/commit/182ec7835567fc246798d9b4ad121b2f85bc6ade))
|
||||
|
||||
### Features
|
||||
|
||||
- Add project_info tool ([#19](https://github.com/basicmachines-co/basic-memory/pull/19),
|
||||
[`d2bd75a`](https://github.com/basicmachines-co/basic-memory/commit/d2bd75a949cc4323cb376ac2f6cb39f47c78c428))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Beta work ([#17](https://github.com/basicmachines-co/basic-memory/pull/17),
|
||||
[`e6496df`](https://github.com/basicmachines-co/basic-memory/commit/e6496df595f3cafde6cc836384ee8c60886057a5))
|
||||
|
||||
feat: Add multiple projects support
|
||||
|
||||
feat: enhanced read_note for when initial result is not found
|
||||
|
||||
fix: merge frontmatter when updating note
|
||||
|
||||
fix: handle directory removed on sync watch
|
||||
|
||||
- Implement boolean search ([#18](https://github.com/basicmachines-co/basic-memory/pull/18),
|
||||
[`90d5754`](https://github.com/basicmachines-co/basic-memory/commit/90d5754180beaf4acd4be38f2438712555640b49))
|
||||
|
||||
|
||||
## v0.8.0 (2025-02-28)
|
||||
|
||||
### Chores
|
||||
|
||||
- Formatting
|
||||
([`93cc637`](https://github.com/basicmachines-co/basic-memory/commit/93cc6379ebb9ecc6a1652feeeecbf47fc992d478))
|
||||
|
||||
- Refactor logging setup
|
||||
([`f4b703e`](https://github.com/basicmachines-co/basic-memory/commit/f4b703e57f0ddf686de6840ff346b8be2be499ad))
|
||||
|
||||
### Features
|
||||
|
||||
- Add enhanced prompts and resources
|
||||
([#15](https://github.com/basicmachines-co/basic-memory/pull/15),
|
||||
[`093dab5`](https://github.com/basicmachines-co/basic-memory/commit/093dab5f03cf7b090a9f4003c55507859bf355b0))
|
||||
|
||||
## Summary - Add comprehensive documentation to all MCP prompt modules - Enhance search prompt with
|
||||
detailed contextual output formatting - Implement consistent logging and docstring patterns across
|
||||
prompt utilities - Fix type checking in prompt modules
|
||||
|
||||
## Prompts Added/Enhanced - `search.py`: New formatted output with relevance scores, excerpts, and
|
||||
next steps - `recent_activity.py`: Enhanced with better metadata handling and documentation -
|
||||
`continue_conversation.py`: Improved context management
|
||||
|
||||
## Resources Added/Enhanced - `ai_assistant_guide`: Resource with description to give to LLM to
|
||||
understand how to use the tools
|
||||
|
||||
## Technical improvements - Added detailed docstrings to all prompt modules explaining their purpose
|
||||
and usage - Enhanced the search prompt with rich contextual output that helps LLMs understand
|
||||
results - Created a consistent pattern for formatting output across prompts - Improved error
|
||||
handling in metadata extraction - Standardized import organization and naming conventions - Fixed
|
||||
various type checking issues across the codebase
|
||||
|
||||
This PR is part of our ongoing effort to improve the MCP's interaction quality with LLMs, making the
|
||||
system more helpful and intuitive for AI assistants to navigate knowledge bases.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
---------
|
||||
|
||||
Co-authored-by: phernandez <phernandez@basicmachines.co>
|
||||
|
||||
- Add new `canvas` tool to create json canvas files in obsidian.
|
||||
([#14](https://github.com/basicmachines-co/basic-memory/pull/14),
|
||||
[`0d7b0b3`](https://github.com/basicmachines-co/basic-memory/commit/0d7b0b3d7ede7555450ddc9728951d4b1edbbb80))
|
||||
|
||||
Add new `canvas` tool to create json canvas files in obsidian.
|
||||
|
||||
---------
|
||||
|
||||
Co-authored-by: phernandez <phernandez@basicmachines.co>
|
||||
|
||||
- Incremental sync on watch ([#13](https://github.com/basicmachines-co/basic-memory/pull/13),
|
||||
[`37a01b8`](https://github.com/basicmachines-co/basic-memory/commit/37a01b806d0758029d34a862e76d44c7e5d538a5))
|
||||
|
||||
- incremental sync on watch - sync non-markdown files in knowledge base - experimental
|
||||
`read_resource` tool for reading non-markdown files in raw form (pdf, image)
|
||||
|
||||
|
||||
## v0.7.0 (2025-02-19)
|
||||
|
||||
@@ -371,4 +1310,4 @@ Co-authored-by: phernandez <phernandez@basicmachines.co>
|
||||
### Chores
|
||||
|
||||
- Remove basic-foundation src ref in pyproject.toml
|
||||
([`29fce8b`](https://github.com/basicmachines-co/basic-memory/commit/29fce8b0b922d54d7799bf2534107ee6cfb961b8))
|
||||
([`29fce8b`](https://github.com/basicmachines-co/basic-memory/commit/29fce8b0b922d54d7799bf2534107ee6cfb961b8))
|
||||
@@ -0,0 +1,34 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
https://developercertificate.org/
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
@@ -0,0 +1,257 @@
|
||||
# CLAUDE.md - Basic Memory Project Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
Basic Memory is a local-first knowledge management system built on the Model Context Protocol (MCP). It enables
|
||||
bidirectional communication between LLMs (like Claude) and markdown files, creating a personal knowledge graph that can
|
||||
be traversed using links between documents.
|
||||
|
||||
## CODEBASE DEVELOPMENT
|
||||
|
||||
### Project information
|
||||
|
||||
See the [README.md](README.md) file for a project overview.
|
||||
|
||||
### Build and Test Commands
|
||||
|
||||
- Install: `just install` or `pip install -e ".[dev]"`
|
||||
- Run tests: `uv run pytest -p pytest_mock -v` or `just test`
|
||||
- Single test: `pytest tests/path/to/test_file.py::test_function_name`
|
||||
- Lint: `just lint` or `ruff check . --fix`
|
||||
- Type check: `just type-check` or `uv run pyright`
|
||||
- Format: `just format` or `uv run ruff format .`
|
||||
- Run all code checks: `just check` (runs lint, format, type-check, test)
|
||||
- Create db migration: `just migration "Your migration message"`
|
||||
- Run development MCP Inspector: `just run-inspector`
|
||||
|
||||
### Code Style Guidelines
|
||||
|
||||
- Line length: 100 characters max
|
||||
- Python 3.12+ with full type annotations
|
||||
- Format with ruff (consistent styling)
|
||||
- Import order: standard lib, third-party, local imports
|
||||
- Naming: snake_case for functions/variables, PascalCase for classes
|
||||
- Prefer async patterns with SQLAlchemy 2.0
|
||||
- Use Pydantic v2 for data validation and schemas
|
||||
- CLI uses Typer for command structure
|
||||
- API uses FastAPI for endpoints
|
||||
- Follow the repository pattern for data access
|
||||
- Tools communicate to api routers via the httpx ASGI client (in process)
|
||||
- avoid using "private" functions in modules or classes (prepended with _)
|
||||
|
||||
### Codebase Architecture
|
||||
|
||||
- `/alembic` - Alembic db migrations
|
||||
- `/api` - FastAPI implementation of REST endpoints
|
||||
- `/cli` - Typer command-line interface
|
||||
- `/markdown` - Markdown parsing and processing
|
||||
- `/mcp` - Model Context Protocol server implementation
|
||||
- `/models` - SQLAlchemy ORM models
|
||||
- `/repository` - Data access layer
|
||||
- `/schemas` - Pydantic models for validation
|
||||
- `/services` - Business logic layer
|
||||
- `/sync` - File synchronization services
|
||||
|
||||
### Development Notes
|
||||
|
||||
- MCP tools are defined in src/basic_memory/mcp/tools/
|
||||
- MCP prompts are defined in src/basic_memory/mcp/prompts/
|
||||
- MCP tools should be atomic, composable operations
|
||||
- Use `textwrap.dedent()` for multi-line string formatting in prompts and tools
|
||||
- MCP Prompts are used to invoke tools and format content with instructions for an LLM
|
||||
- Schema changes require Alembic migrations
|
||||
- SQLite is used for indexing and full text search, files are source of truth
|
||||
- Testing uses pytest with asyncio support (strict mode)
|
||||
- Test database uses in-memory SQLite
|
||||
- Avoid creating mocks in tests in most circumstances.
|
||||
- Each test runs in a standalone environment with in memory SQLite and tmp_file directory
|
||||
- Do not use mocks in tests if possible. Tests run with an in memory sqlite db, so they are not needed. See fixtures in conftest.py
|
||||
|
||||
## BASIC MEMORY PRODUCT USAGE
|
||||
|
||||
### Knowledge Structure
|
||||
|
||||
- Entity: Any concept, document, or idea represented as a markdown file
|
||||
- Observation: A categorized fact about an entity (`- [category] content`)
|
||||
- Relation: A directional link between entities (`- relation_type [[Target]]`)
|
||||
- Frontmatter: YAML metadata at the top of markdown files
|
||||
- Knowledge representation follows precise markdown format:
|
||||
- Observations with [category] prefixes
|
||||
- Relations with WikiLinks [[Entity]]
|
||||
- Frontmatter with metadata
|
||||
|
||||
### Basic Memory Commands
|
||||
|
||||
- Sync knowledge: `basic-memory sync` or `basic-memory sync --watch`
|
||||
- Import from Claude: `basic-memory import claude conversations`
|
||||
- Import from ChatGPT: `basic-memory import chatgpt`
|
||||
- Import from Memory JSON: `basic-memory import memory-json`
|
||||
- Check sync status: `basic-memory status`
|
||||
- Tool access: `basic-memory tools` (provides CLI access to MCP tools)
|
||||
- Guide: `basic-memory tools basic-memory-guide`
|
||||
- Continue: `basic-memory tools continue-conversation --topic="search"`
|
||||
|
||||
### MCP Capabilities
|
||||
|
||||
- Basic Memory exposes these MCP tools to LLMs:
|
||||
|
||||
**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
|
||||
- `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")
|
||||
- `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
|
||||
|
||||
**Visualization:**
|
||||
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
|
||||
|
||||
- MCP Prompts for better AI interaction:
|
||||
- `ai_assistant_guide()` - Guidance on effectively using Basic Memory tools for AI assistants
|
||||
- `continue_conversation(topic, timeframe)` - Continue previous conversations with relevant historical context
|
||||
- `search_notes(query, after_date)` - Search with detailed, formatted results for better context understanding
|
||||
- `recent_activity(timeframe)` - View recently changed items with formatted output
|
||||
- `json_canvas_spec()` - Full JSON Canvas specification for Obsidian visualization
|
||||
|
||||
## AI-Human Collaborative Development
|
||||
|
||||
Basic Memory emerged from and enables a new kind of development process that combines human and AI capabilities. Instead
|
||||
of using AI just for code generation, we've developed a true collaborative workflow:
|
||||
|
||||
1. AI (LLM) writes initial implementation based on specifications and context
|
||||
2. Human reviews, runs tests, and commits code with any necessary adjustments
|
||||
3. Knowledge persists across conversations using Basic Memory's knowledge graph
|
||||
4. Development continues seamlessly across different AI sessions with consistent context
|
||||
5. Results improve through iterative collaboration and shared understanding
|
||||
|
||||
This approach has allowed us to tackle more complex challenges and build a more robust system than either humans or AI
|
||||
could achieve independently.
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
Basic Memory uses Claude directly into the development workflow through GitHub:
|
||||
|
||||
### GitHub MCP Tools
|
||||
|
||||
Using the GitHub Model Context Protocol server, Claude can:
|
||||
|
||||
- **Repository Management**:
|
||||
- View repository files and structure
|
||||
- Read file contents
|
||||
- Create new branches
|
||||
- Create and update files
|
||||
|
||||
- **Issue Management**:
|
||||
- Create new issues
|
||||
- Comment on existing issues
|
||||
- Close and update issues
|
||||
- Search across issues
|
||||
|
||||
- **Pull Request Workflow**:
|
||||
- Create pull requests
|
||||
- Review code changes
|
||||
- Add comments to PRs
|
||||
|
||||
This integration enables Claude to participate as a full team member in the development process, not just as a code
|
||||
generation tool. Claude's GitHub account ([bm-claudeai](https://github.com/bm-claudeai)) is a member of the Basic
|
||||
Machines organization with direct contributor access to the codebase.
|
||||
|
||||
### Collaborative Development Process
|
||||
|
||||
With GitHub integration, the development workflow includes:
|
||||
|
||||
1. **Direct code review** - Claude can analyze PRs and provide detailed feedback
|
||||
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
|
||||
3. **Branch management** - Claude can create feature branches for implementations
|
||||
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
|
||||
|
||||
With this integration, the AI assistant is a full-fledged team member rather than just a tool for generating code
|
||||
snippets.
|
||||
|
||||
|
||||
### Basic Memory Pro
|
||||
|
||||
Basic Memory Pro is a desktop GUI application that wraps the basic-memory CLI/MCP tools:
|
||||
|
||||
- Built with Tauri (Rust), React (TypeScript), and a Python FastAPI sidecar
|
||||
- Provides visual knowledge graph exploration and project management
|
||||
- Uses the same core codebase but adds a desktop-friendly interface
|
||||
- Project configuration is shared between CLI and Pro versions
|
||||
- Multiple project support with visual switching interface
|
||||
|
||||
local repo: /Users/phernandez/dev/basicmachines/basic-memory-pro
|
||||
github: https://github.com/basicmachines-co/basic-memory-pro
|
||||
|
||||
## Release and Version Management
|
||||
|
||||
Basic Memory uses `uv-dynamic-versioning` for automatic version management based on git tags:
|
||||
|
||||
### Version Types
|
||||
- **Development versions**: Automatically generated from commits (e.g., `0.12.4.dev26+468a22f`)
|
||||
- **Beta releases**: Created by tagging with beta suffixes (e.g., `v0.13.0b1`, `v0.13.0rc1`)
|
||||
- **Stable releases**: Created by tagging with version numbers (e.g., `v0.13.0`)
|
||||
|
||||
### Release Workflows
|
||||
|
||||
#### Development Builds (Automatic)
|
||||
- Triggered on every push to `main` branch
|
||||
- Publishes dev versions like `0.12.4.dev26+468a22f` to PyPI
|
||||
- Allows continuous testing of latest changes
|
||||
- Users install with: `pip install basic-memory --pre --force-reinstall`
|
||||
|
||||
#### Beta/RC Releases (Manual)
|
||||
- Create beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
|
||||
- Automatically builds and publishes to PyPI as pre-release
|
||||
- Users install with: `pip install basic-memory --pre`
|
||||
- Use for milestone testing before stable release
|
||||
|
||||
#### 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`
|
||||
|
||||
#### 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
|
||||
- **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
|
||||
+190
-8
@@ -1,17 +1,199 @@
|
||||
# Contributing to Basic Memory
|
||||
|
||||
Thank you for considering contributing to Basic Memory! Your help is greatly appreciated to improve this project.
|
||||
Thank you for considering contributing to Basic Memory! This document outlines the process for contributing to the
|
||||
project and how to get started as a developer.
|
||||
|
||||
## How to Contribute
|
||||
## Getting Started
|
||||
|
||||
1. **Fork the Repo**: Fork the repository and clone your copy.
|
||||
1. **Create a Branch**: Create a new branch for your feature or fix.
|
||||
1. **Test Your Changes**: Ensure tests pass locally and include new tests when necessary to ensure 100& test coverage.
|
||||
1. **Format Your Code**: Run `make format` to ensure code is formatted appropriately.
|
||||
4. **Submit a PR**: Submit a pull request with a detailed description of your changes.
|
||||
### Development Environment
|
||||
|
||||
Thank You!
|
||||
1. **Clone the Repository**:
|
||||
```bash
|
||||
git clone https://github.com/basicmachines-co/basic-memory.git
|
||||
cd basic-memory
|
||||
```
|
||||
|
||||
2. **Install Dependencies**:
|
||||
```bash
|
||||
# Using just (recommended)
|
||||
just install
|
||||
|
||||
# Or using uv
|
||||
uv install -e ".[dev]"
|
||||
|
||||
# Or using pip
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
> **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**:
|
||||
```bash
|
||||
# Run all tests
|
||||
just test
|
||||
# or
|
||||
uv run pytest -p pytest_mock -v
|
||||
|
||||
# Run a specific test
|
||||
pytest tests/path/to/test_file.py::test_function_name
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Fork the Repo**: Fork the repository on GitHub and clone your copy.
|
||||
2. **Create a Branch**: Create a new branch for your feature or fix.
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/issue-you-are-fixing
|
||||
```
|
||||
3. **Make Your Changes**: Implement your changes with appropriate test coverage.
|
||||
4. **Check Code Quality**:
|
||||
```bash
|
||||
# Run all checks at once
|
||||
just check
|
||||
|
||||
# Or run individual checks
|
||||
just lint # Run linting
|
||||
just format # Format code
|
||||
just type-check # Type checking
|
||||
```
|
||||
5. **Test Your Changes**: Ensure all tests pass locally and maintain 100% test coverage.
|
||||
```bash
|
||||
just test
|
||||
```
|
||||
6. **Submit a PR**: Submit a pull request with a detailed description of your changes.
|
||||
|
||||
## LLM-Assisted Development
|
||||
|
||||
This project is designed for collaborative development between humans and LLMs (Large Language Models):
|
||||
|
||||
1. **CLAUDE.md**: The repository includes a `CLAUDE.md` file that serves as a project guide for both humans and LLMs.
|
||||
This file contains:
|
||||
- Key project information and architectural overview
|
||||
- Development commands and workflows
|
||||
- Code style guidelines
|
||||
- Documentation standards
|
||||
|
||||
2. **AI-Human Collaborative Workflow**:
|
||||
- We encourage using LLMs like Claude for code generation, reviews, and documentation
|
||||
- When possible, save context in markdown files that can be referenced later
|
||||
- This enables seamless knowledge transfer between different development sessions
|
||||
- Claude can help with implementation details while you focus on architecture and design
|
||||
|
||||
3. **Adding to CLAUDE.md**:
|
||||
- If you discover useful project information or common commands, consider adding them to CLAUDE.md
|
||||
- This helps all contributors (human and AI) maintain consistent knowledge of the project
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Create a Pull Request**: Open a PR against the `main` branch with a clear title and description.
|
||||
2. **Sign the Developer Certificate of Origin (DCO)**: All contributions require signing our DCO, which certifies that
|
||||
you have the right to submit your contributions. This will be automatically checked by our CLA assistant when you
|
||||
create a PR.
|
||||
3. **PR Description**: Include:
|
||||
- What the PR changes
|
||||
- Why the change is needed
|
||||
- How you tested the changes
|
||||
- Any related issues (use "Fixes #123" to automatically close issues)
|
||||
4. **Code Review**: Wait for code review and address any feedback.
|
||||
5. **CI Checks**: Ensure all CI checks pass.
|
||||
6. **Merge**: Once approved, a maintainer will merge your PR.
|
||||
|
||||
## Developer Certificate of Origin
|
||||
|
||||
By contributing to this project, you agree to the [Developer Certificate of Origin (DCO)](CLA.md). This means you
|
||||
certify that:
|
||||
|
||||
- You have the right to submit your contributions
|
||||
- You're not knowingly submitting code with patent or copyright issues
|
||||
- Your contributions are provided under the project's license (AGPL-3.0)
|
||||
|
||||
This is a lightweight alternative to a Contributor License Agreement and helps ensure that all contributions can be
|
||||
properly incorporated into the project and potentially used in commercial applications.
|
||||
|
||||
### Signing Your Commits
|
||||
|
||||
Sign your commit:
|
||||
|
||||
**Using the `-s` or `--signoff` flag**:
|
||||
|
||||
```bash
|
||||
git commit -s -m "Your commit message"
|
||||
```
|
||||
|
||||
This adds a `Signed-off-by` line to your commit message, certifying that you adhere to the DCO.
|
||||
|
||||
The sign-off certifies that you have the right to submit your contribution under the project's license and verifies your
|
||||
agreement to the DCO.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- **Python Version**: Python 3.12+ with full type annotations
|
||||
- **Line Length**: 100 characters maximum
|
||||
- **Formatting**: Use ruff for consistent styling
|
||||
- **Import Order**: Standard lib, third-party, local imports
|
||||
- **Naming**: Use snake_case for functions/variables, PascalCase for classes
|
||||
- **Documentation**: Add docstrings to public functions, classes, and methods
|
||||
- **Type Annotations**: Use type hints for all functions and methods
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- **Coverage Target**: We aim for 100% test coverage for all code
|
||||
- **Test Framework**: Use pytest for unit and integration tests
|
||||
- **Mocking**: Use pytest-mock for mocking dependencies only when necessary
|
||||
- **Edge Cases**: Test both normal operation and edge cases
|
||||
- **Database Testing**: Use in-memory SQLite for testing database operations
|
||||
- **Fixtures**: Use async pytest fixtures for setup and teardown
|
||||
|
||||
## Release Process
|
||||
|
||||
Basic Memory uses automatic versioning based on git tags with `uv-dynamic-versioning`. Here's how releases work:
|
||||
|
||||
### Version Management
|
||||
- **Development versions**: Automatically generated from git commits (e.g., `0.12.4.dev26+468a22f`)
|
||||
- **Beta releases**: Created by tagging with beta suffixes (e.g., `git tag v0.13.0b1`)
|
||||
- **Stable releases**: Created by tagging with version numbers (e.g., `git tag v0.13.0`)
|
||||
|
||||
### Release Workflows
|
||||
|
||||
#### Development Builds
|
||||
- Automatically published to PyPI on every commit to `main`
|
||||
- Version format: `0.12.4.dev26+468a22f` (base version + dev + commit count + hash)
|
||||
- Users install with: `pip install basic-memory --pre --force-reinstall`
|
||||
|
||||
#### Beta Releases
|
||||
1. Create and push a beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
|
||||
2. GitHub Actions automatically builds and publishes to PyPI
|
||||
3. Users install with: `pip install basic-memory --pre`
|
||||
|
||||
#### Stable Releases
|
||||
1. Create and push a version tag: `git tag v0.13.0 && git push origin v0.13.0`
|
||||
2. GitHub Actions automatically:
|
||||
- Builds the package with version `0.13.0`
|
||||
- Creates GitHub release with auto-generated notes
|
||||
- Publishes to PyPI
|
||||
3. Users install with: `pip install basic-memory`
|
||||
|
||||
### For Contributors
|
||||
- No manual version bumping required
|
||||
- Versions are automatically derived from git tags
|
||||
- Focus on code changes, not version management
|
||||
|
||||
## Creating Issues
|
||||
|
||||
If you're planning to work on something, please create an issue first to discuss the approach. Include:
|
||||
|
||||
- A clear title and description
|
||||
- Steps to reproduce if reporting a bug
|
||||
- Expected behavior vs. actual behavior
|
||||
- Any relevant logs or screenshots
|
||||
- Your proposed solution, if you have one
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
All contributors must follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Thank You!
|
||||
|
||||
Your contributions help make Basic Memory better. We appreciate your time and effort!
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Set default data directory and add venv to PATH
|
||||
ENV BASIC_MEMORY_HOME=/app/data \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# 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"]
|
||||
@@ -1,45 +0,0 @@
|
||||
.PHONY: install test lint clean format type-check installer-mac installer-win check
|
||||
|
||||
install:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
test:
|
||||
uv run pytest -p pytest_mock -v
|
||||
|
||||
lint:
|
||||
ruff check . --fix
|
||||
|
||||
type-check:
|
||||
uv run pyright
|
||||
|
||||
clean:
|
||||
find . -type f -name '*.pyc' -delete
|
||||
find . -type d -name '__pycache__' -exec rm -r {} +
|
||||
rm -rf installer/build/
|
||||
rm -rf installer/dist/
|
||||
rm -f rw.*.dmg
|
||||
rm -rf dist
|
||||
rm -rf installer/build
|
||||
rm -rf installer/dist
|
||||
rm -f .coverage.*
|
||||
|
||||
format:
|
||||
uv run ruff format .
|
||||
|
||||
# run inspector tool
|
||||
run-dev:
|
||||
uv run mcp dev src/basic_memory/mcp/main.py
|
||||
|
||||
# Build app installer
|
||||
installer-mac:
|
||||
cd installer && chmod +x make_icons.sh && ./make_icons.sh
|
||||
cd installer && uv run python setup.py bdist_mac
|
||||
|
||||
installer-win:
|
||||
cd installer && uv run python setup.py bdist_win32
|
||||
|
||||
|
||||
update-deps:
|
||||
uv lock f--upgrade
|
||||
|
||||
check: lint format type-check test
|
||||
@@ -1,344 +1,460 @@
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://badge.fury.io/py/basic-memory)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://github.com/basicmachines-co/basic-memory/actions)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||

|
||||
[](https://smithery.ai/server/@basicmachines-co/basic-memory)
|
||||
|
||||
# Basic Memory
|
||||
|
||||
Basic Memory lets you build persistent knowledge through natural conversations with Large Language Models (LLMs) like
|
||||
Claude, while keeping everything in simple markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
||||
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.
|
||||
|
||||
## What is Basic Memory?
|
||||
- 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
|
||||
|
||||
Most people use LLMs like calculators - paste in some text, expect to get an answer back, repeat. Each conversation
|
||||
starts fresh,
|
||||
and any knowledge or context is lost. Some try to work around this by:
|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
- Saving chat histories (but they're hard to reference)
|
||||
- Copying and pasting previous conversations (messy and repetitive)
|
||||
- Using RAG systems to query documents (complex and often cloud-based)
|
||||
- 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
|
||||
|
||||
Basic Memory takes a different approach by letting both humans and LLMs read and write knowledge naturally using
|
||||
standard markdown files. This means:
|
||||
https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
|
||||
|
||||
- Your knowledge stays in files you control
|
||||
- Both you and the LLM can read and write notes
|
||||
- Context persists across conversations
|
||||
- Context stays local and user controlled
|
||||
|
||||
## How It Works in Practice
|
||||
|
||||
Let's say you're working on a new project and want to capture design decisions. Here's how it works:
|
||||
|
||||
1. Start by chatting normally:
|
||||
|
||||
```markdown
|
||||
We need to design a new auth system, some key features:
|
||||
|
||||
- local first, don't delegate users to third party system
|
||||
- support multiple platforms via jwt
|
||||
- want to keep it simple but secure
|
||||
```
|
||||
|
||||
... continue conversation.
|
||||
|
||||
2. Ask Claude to help structure this knowledge:
|
||||
|
||||
```
|
||||
"Lets write a note about the auth system design."
|
||||
```
|
||||
|
||||
Claude creates a new markdown file on your system (which you can see instantly in Obsidian or your editor):
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Auth System Design
|
||||
permalink: auth-system-design
|
||||
tags
|
||||
- design
|
||||
- auth
|
||||
---
|
||||
|
||||
# Auth System Design
|
||||
|
||||
## Observations
|
||||
|
||||
- [requirement] Local-first authentication without third party delegation
|
||||
- [tech] JWT-based auth for cross-platform support
|
||||
- [principle] Balance simplicity with security
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- relates_to [[Platform Support]]
|
||||
- referenced_by [[JWT Implementation]]
|
||||
```
|
||||
|
||||
The note embeds semantic content (Observations) and links to other topics (Relations) via simple markdown formatting.
|
||||
|
||||
3. You can edit this file directly in your editor in real time:
|
||||
|
||||
```markdown
|
||||
# Auth System Design
|
||||
|
||||
## Observations
|
||||
|
||||
- [requirement] Local-first authentication without third party delegation
|
||||
- [tech] JWT-based auth for cross-platform support
|
||||
- [principle] Balance simplicity with security
|
||||
- [decision] Will use bcrypt for password hashing # Added by you
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- relates_to [[Platform Support]]
|
||||
- referenced_by [[JWT Implementation]]
|
||||
- blocks [[User Service]] # Added by you
|
||||
```
|
||||
|
||||
4. In a new chat with Claude, you can reference this knowledge:
|
||||
|
||||
```
|
||||
"Claude, look at memory://auth-system-design for context about our auth system"
|
||||
```
|
||||
|
||||
Claude can now build rich context from the knowledge graph. For example:
|
||||
|
||||
```
|
||||
Following relation 'implements [[Security Requirements]]':
|
||||
- Found authentication best practices
|
||||
- OWASP guidelines for JWT
|
||||
- Rate limiting requirements
|
||||
|
||||
Following relation 'relates_to [[Platform Support]]':
|
||||
- Mobile auth requirements
|
||||
- Browser security considerations
|
||||
- JWT storage strategies
|
||||
```
|
||||
|
||||
Each related document can lead to more context, building a rich semantic understanding of your knowledge base. All of
|
||||
this context comes from standard markdown files that both humans and LLMs can read and write.
|
||||
|
||||
Everything stays in local markdown files that you can:
|
||||
|
||||
- Edit in any text editor
|
||||
- Version via git
|
||||
- Back up normally
|
||||
- Share when you want to
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Under the hood, Basic Memory:
|
||||
|
||||
1. Stores everything in markdown files
|
||||
2. Uses a SQLite database just for searching and indexing
|
||||
3. Extracts semantic meaning from simple markdown patterns
|
||||
4. Maintains a local knowledge graph from file content
|
||||
|
||||
The file format is just markdown with some simple markup:
|
||||
|
||||
Frontmatter
|
||||
|
||||
- title
|
||||
- type
|
||||
- permalink
|
||||
- optional metadata
|
||||
|
||||
Observations
|
||||
|
||||
- facts about a topic
|
||||
|
||||
```markdown
|
||||
- [category] content #tag (optional context)
|
||||
```
|
||||
|
||||
Relations
|
||||
|
||||
- links to other topics
|
||||
|
||||
```markdown
|
||||
- relation_type [[WikiLink]] (optional context)
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Note tile
|
||||
type: note
|
||||
permalink: unique/stable/id # Added automatically
|
||||
tags
|
||||
- tag1
|
||||
- tag2
|
||||
---
|
||||
|
||||
# Note Title
|
||||
|
||||
Regular markdown content...
|
||||
|
||||
## Observations
|
||||
|
||||
- [category] Structured knowledge #tag (optional context)
|
||||
- [idea] Another observation
|
||||
|
||||
## Relations
|
||||
|
||||
- links_to [[Other Note]]
|
||||
- implements [[Some Spec]]
|
||||
```
|
||||
|
||||
Basic Memory will parse the markdown and derive the semantic relationships in the content. When you run
|
||||
`basic-memory sync`:
|
||||
|
||||
1. New and changed files are detected
|
||||
2. Markdown patterns become semantic knowledge:
|
||||
|
||||
- `[tech]` becomes a categorized observation
|
||||
- `[[WikiLink]]` creates a relation in the knowledge graph
|
||||
- Tags and metadata are indexed for search
|
||||
|
||||
3. A SQLite database maintains these relationships for fast querying
|
||||
4. Claude and other MCP-compatible LLMs can access this knowledge via memory:// URLs
|
||||
|
||||
This creates a two-way flow where:
|
||||
|
||||
- Humans write and edit markdown files
|
||||
- LLMs read and write through the MCP protocol
|
||||
- Sync keeps everything consistent
|
||||
- All knowledge stays in local files.
|
||||
|
||||
## Using with Claude
|
||||
|
||||
Basic Memory works with the Claude desktop app (https://claude.ai/):
|
||||
|
||||
1. Install Basic Memory locally:
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory"
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
# Now in Claude Desktop, you can:
|
||||
# - Write notes with "Create a note about coffee brewing methods"
|
||||
# - Read notes with "What do I know about pour over coffee?"
|
||||
# - Search with "Find information about Ethiopian beans"
|
||||
|
||||
```
|
||||
|
||||
You can view shared context via files in `~/basic-memory` (default directory location).
|
||||
|
||||
### Alternative Installation via Smithery
|
||||
|
||||
You can use [Smithery](https://smithery.ai/server/@basicmachines-co/basic-memory) to automatically configure Basic
|
||||
Memory for Claude Desktop:
|
||||
|
||||
```bash
|
||||
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
|
||||
```
|
||||
|
||||
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. Note: The Smithery installation uses their hosted MCP server, while your data remains stored locally as Markdown files.
|
||||
|
||||
### Glama.ai
|
||||
|
||||
<a href="https://glama.ai/mcp/servers/o90kttu9ym">
|
||||
<img width="380" height="200" src="https://glama.ai/mcp/servers/o90kttu9ym/badge" alt="basic-memory MCP server" />
|
||||
</a>
|
||||
|
||||
## Why Basic Memory?
|
||||
|
||||
Most LLM interactions are ephemeral - you ask a question, get an answer, and everything is forgotten. Each conversation
|
||||
starts fresh, without the context or knowledge from previous ones. Current workarounds have limitations:
|
||||
|
||||
- Chat histories capture conversations but aren't structured knowledge
|
||||
- RAG systems can query documents but don't let LLMs write back
|
||||
- Vector databases require complex setups and often live in the cloud
|
||||
- Knowledge graphs typically need specialized tools to maintain
|
||||
|
||||
Basic Memory addresses these problems with a simple approach: structured Markdown files that both humans and LLMs can
|
||||
read
|
||||
and write to. The key advantages:
|
||||
|
||||
- **Local-first:** All knowledge stays in files you control
|
||||
- **Bi-directional:** Both you and the LLM read and write to the same files
|
||||
- **Structured yet simple:** Uses familiar Markdown with semantic patterns
|
||||
- **Traversable knowledge graph:** LLMs can follow links between topics
|
||||
- **Standard formats:** Works with existing editors like Obsidian
|
||||
- **Lightweight infrastructure:** Just local files indexed in a local SQLite database
|
||||
|
||||
With Basic Memory, you can:
|
||||
|
||||
- Have conversations that build on previous knowledge
|
||||
- Create structured notes during natural conversations
|
||||
- Have conversations with LLMs that remember what you've discussed before
|
||||
- Navigate your knowledge graph semantically
|
||||
- Keep everything local and under your control
|
||||
- Use familiar tools like Obsidian to view and edit notes
|
||||
- Build a personal knowledge base that grows over time
|
||||
|
||||
## How It Works in Practice
|
||||
|
||||
Let's say you're exploring coffee brewing methods and want to capture your knowledge. Here's how it works:
|
||||
|
||||
1. Start by chatting normally:
|
||||
|
||||
```
|
||||
I've been experimenting with different coffee brewing methods. Key things I've learned:
|
||||
|
||||
- Pour over gives more clarity in flavor than French press
|
||||
- Water temperature is critical - around 205°F seems best
|
||||
- Freshly ground beans make a huge difference
|
||||
```
|
||||
|
||||
... continue conversation.
|
||||
|
||||
2. Ask the LLM to help structure this knowledge:
|
||||
|
||||
```
|
||||
"Let's write a note about coffee brewing methods."
|
||||
```
|
||||
|
||||
LLM creates a new Markdown file on your system (which you can see instantly in Obsidian or your editor):
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
permalink: coffee-brewing-methods
|
||||
tags:
|
||||
- coffee
|
||||
- brewing
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
## Observations
|
||||
|
||||
- [method] Pour over provides more clarity and highlights subtle flavors
|
||||
- [technique] Water temperature at 205°F (96°C) extracts optimal compounds
|
||||
- [principle] Freshly ground beans preserve aromatics and flavor
|
||||
|
||||
## Relations
|
||||
|
||||
- relates_to [[Coffee Bean Origins]]
|
||||
- requires [[Proper Grinding Technique]]
|
||||
- affects [[Flavor Extraction]]
|
||||
```
|
||||
|
||||
The note embeds semantic content and links to other topics via simple Markdown formatting.
|
||||
|
||||
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 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:
|
||||
|
||||
```
|
||||
Look at `coffee-brewing-methods` for context about pour over coffee
|
||||
```
|
||||
|
||||
The LLM can now build rich context from the knowledge graph. For example:
|
||||
|
||||
```
|
||||
Following relation 'relates_to [[Coffee Bean Origins]]':
|
||||
- Found information about Ethiopian Yirgacheffe
|
||||
- Notes on Colombian beans' nutty profile
|
||||
- Altitude effects on bean characteristics
|
||||
|
||||
Following relation 'requires [[Proper Grinding Technique]]':
|
||||
- Burr vs. blade grinder comparisons
|
||||
- Grind size recommendations for different methods
|
||||
- Impact of consistent particle size on extraction
|
||||
```
|
||||
|
||||
Each related document can lead to more context, building a rich semantic understanding of your knowledge base.
|
||||
|
||||
This creates a two-way flow where:
|
||||
|
||||
- Humans write and edit Markdown files
|
||||
- LLMs read and write through the MCP protocol
|
||||
- Sync keeps everything consistent
|
||||
- All knowledge stays in local files.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Under the hood, Basic Memory:
|
||||
|
||||
1. Stores everything in Markdown files
|
||||
2. Uses a SQLite database for searching and indexing
|
||||
3. Extracts semantic meaning from simple Markdown patterns
|
||||
- Files become `Entity` objects
|
||||
- Each `Entity` can have `Observations`, or facts associated with it
|
||||
- `Relations` connect entities together to form the knowledge graph
|
||||
4. Maintains the local knowledge graph derived from the files
|
||||
5. Provides bidirectional synchronization between files and the knowledge graph
|
||||
6. Implements the Model Context Protocol (MCP) for AI integration
|
||||
7. Exposes tools that let AI assistants traverse and manipulate the knowledge graph
|
||||
8. Uses memory:// URLs to reference entities across tools and conversations
|
||||
|
||||
The file format is just Markdown with some simple markup:
|
||||
|
||||
Each Markdown file has:
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```markdown
|
||||
title: <Entity title>
|
||||
type: <The type of Entity> (e.g. note)
|
||||
permalink: <a uri slug>
|
||||
|
||||
- <optional metadata> (such as tags)
|
||||
```
|
||||
|
||||
### Observations
|
||||
|
||||
Observations are facts about a topic.
|
||||
They can be added by creating a Markdown list with a special format that can reference a `category`, `tags` using a
|
||||
"#" character, and an optional `context`.
|
||||
|
||||
Observation Markdown format:
|
||||
|
||||
```markdown
|
||||
- [category] content #tag (optional context)
|
||||
```
|
||||
|
||||
Examples of observations:
|
||||
|
||||
```markdown
|
||||
- [method] Pour over extracts more floral notes than French press
|
||||
- [tip] Grind size should be medium-fine for pour over #brewing
|
||||
- [preference] Ethiopian beans have bright, fruity flavors (especially from Yirgacheffe)
|
||||
- [fact] Lighter roasts generally contain more caffeine than dark roasts
|
||||
- [experiment] Tried 1:15 coffee-to-water ratio with good results
|
||||
- [resource] James Hoffman's V60 technique on YouTube is excellent
|
||||
- [question] Does water temperature affect extraction of different compounds differently?
|
||||
- [note] My favorite local shop uses a 30-second bloom time
|
||||
```
|
||||
|
||||
### Relations
|
||||
|
||||
Relations are links to other topics. They define how entities connect in the knowledge graph.
|
||||
|
||||
Markdown format:
|
||||
|
||||
```markdown
|
||||
- relation_type [[WikiLink]] (optional context)
|
||||
```
|
||||
|
||||
Examples of relations:
|
||||
|
||||
```markdown
|
||||
- pairs_well_with [[Chocolate Desserts]]
|
||||
- grown_in [[Ethiopia]]
|
||||
- contrasts_with [[Tea Brewing Methods]]
|
||||
- requires [[Burr Grinder]]
|
||||
- improves_with [[Fresh Beans]]
|
||||
- relates_to [[Morning Routine]]
|
||||
- inspired_by [[Japanese Coffee Culture]]
|
||||
- documented_in [[Coffee Journal]]
|
||||
```
|
||||
|
||||
## Using with VS Code
|
||||
For one-click installation, click one of the install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D&quality=insiders)
|
||||
|
||||
You can use Basic Memory with VS Code to easily retrieve and store information while coding. Click the installation buttons above for one-click setup, or follow the manual installation instructions below.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Add to Claude Desktop:
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
```
|
||||
Basic Memory is available with these tools:
|
||||
- write_note() for creating/updating notes
|
||||
- read_note() for loading notes
|
||||
- build_context() to load notes via memory:// URLs
|
||||
- recent_activity() to find recently updated information
|
||||
- search() to search infomation in the knowledge base
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Install via uv
|
||||
## Using with Claude Desktop
|
||||
|
||||
Basic Memory is built using the MCP (Model Context Protocol) and works with the Claude desktop app (https://claude.ai/):
|
||||
|
||||
1. Configure Claude Desktop to use Basic Memory:
|
||||
|
||||
Edit your MCP configuration file (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
for OS X):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a specific project (see [Multiple Projects](docs/User%20Guide.md#multiple-projects)), update your
|
||||
Claude Desktop
|
||||
config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"--project",
|
||||
"your-project-name",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Sync your knowledge:
|
||||
|
||||
Basic Memory will sync the files in your project in real time if you make manual edits.
|
||||
|
||||
3. In Claude Desktop, the LLM can now use these tools:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
"Create a note about our project architecture decisions"
|
||||
"Find information about JWT authentication in my notes"
|
||||
"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
|
||||
|
||||
See the [Documentation](https://memory.basicmachines.co/) for more info, including:
|
||||
|
||||
- [Complete User Guide](https://memory.basicmachines.co/docs/user-guide)
|
||||
- [CLI tools](https://memory.basicmachines.co/docs/cli-reference)
|
||||
- [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
|
||||
- [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Stable Release
|
||||
```bash
|
||||
pip install basic-memory
|
||||
```
|
||||
|
||||
### Beta/Pre-releases
|
||||
```bash
|
||||
pip install basic-memory --pre
|
||||
```
|
||||
|
||||
### Development Builds
|
||||
Development versions are automatically published on every commit to main with versions like `0.12.4.dev26+468a22f`:
|
||||
```bash
|
||||
pip install basic-memory --pre --force-reinstall
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Run Basic Memory in a container with volume mounting for your Obsidian vault:
|
||||
|
||||
```bash
|
||||
uv add basic-memory
|
||||
# Clone and start with Docker Compose
|
||||
git clone https://github.com/basicmachines-co/basic-memory.git
|
||||
cd basic-memory
|
||||
|
||||
# sync local knowledge updates
|
||||
basic-memory sync
|
||||
|
||||
# run realtime sync process
|
||||
basic-memory sync --watch
|
||||
# Edit docker-compose.yml to point to your Obsidian vault
|
||||
# Then start the container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Basic Memory is built on some key ideas:
|
||||
|
||||
- Your knowledge should stay in files you control
|
||||
- Both humans and AI should use natural formats
|
||||
- Simple text patterns can capture rich meaning
|
||||
- Local-first doesn't mean feature-poor
|
||||
|
||||
## Importing data
|
||||
|
||||
Basic memory has cli commands to import data from several formats into Markdown files
|
||||
|
||||
### Claude.ai
|
||||
|
||||
First, request an export of your data from your Claude account. The data will be emailed to you in several files,
|
||||
including
|
||||
`conversations.json` and `projects.json`.
|
||||
|
||||
Import Claude.ai conversation data
|
||||
|
||||
Or use Docker directly:
|
||||
```bash
|
||||
basic-memory import claude conversations
|
||||
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
|
||||
```
|
||||
|
||||
The conversations will be turned into Markdown files and placed in the "conversations" folder by default (this can be
|
||||
changed with the --folder arg).
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
Importing chats from conversations.json...writing to .../basic-memory
|
||||
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 307 conversations │
|
||||
│ Containing 7769 messages │
|
||||
╰────────────────────────────╯
|
||||
```
|
||||
|
||||
Next, you can run the `sync` command to import the data into basic-memory
|
||||
|
||||
```bash
|
||||
basic-memory sync
|
||||
```
|
||||
|
||||
You can also import project data from Claude.ai
|
||||
|
||||
```bash
|
||||
➜ basic-memory import claude projects
|
||||
Importing projects from projects.json...writing to .../basic-memory/projects
|
||||
Reading project data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 101 project documents │
|
||||
│ Imported 32 prompt templates │
|
||||
╰────────────────────────────────╯
|
||||
|
||||
Run 'basic-memory sync' to index the new files.
|
||||
```
|
||||
|
||||
### Chat Gpt
|
||||
|
||||
```bash
|
||||
➜ basic-memory import chatgpt
|
||||
Importing chats from conversations.json...writing to .../basic-memory/conversations
|
||||
|
||||
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 198 conversations │
|
||||
│ Containing 11777 messages │
|
||||
╰────────────────────────────╯
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Memory json
|
||||
|
||||
```bash
|
||||
➜ basic-memory import memory-json
|
||||
Importing from memory.json...writing to .../basic-memory
|
||||
Reading memory.json... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
Creating entities... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭──────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Created 126 entities │
|
||||
│ Added 252 relations │
|
||||
╰──────────────────────╯
|
||||
```
|
||||
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
AGPL-3.0
|
||||
|
||||
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
|
||||
and submitting PRs.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#basicmachines-co/basic-memory&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
Built with ♥️ by Basic Machines
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
If you find a vulnerability, please contact hello@basicmachines.co
|
||||
-1419
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,431 @@
|
||||
---
|
||||
title: AI Assistant Guide
|
||||
type: note
|
||||
permalink: docs/ai-assistant-guide
|
||||
---
|
||||
> Note: This is an optional document that can be copy/pasted into the project knowledge for an LLM to provide a full description of how it can work with Basic Memory. It is provided as a helpful resource. The tools contain extensive usage description prompts with enable the LLM to understand them.
|
||||
|
||||
You can [download](https://github.com/basicmachines-co/basic-memory/blob/main/docs/AI%20Assistant%20Guide.md) the contents of this file from GitHub
|
||||
# AI Assistant Guide for Basic Memory
|
||||
|
||||
This guide helps you, the AI assistant, use Basic Memory tools effectively when working with users. It covers reading, writing, and navigating knowledge through the Model Context Protocol (MCP).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Essential Tools:**
|
||||
- `write_note()` - Create/update notes (primary tool)
|
||||
- `read_note()` - Read existing content
|
||||
- `search_notes()` - Find information
|
||||
- `edit_note()` - Modify existing notes incrementally (v0.13.0)
|
||||
- `move_note()` - Organize files with database consistency (v0.13.0)
|
||||
|
||||
**Project Management (v0.13.0):**
|
||||
- `list_projects()` - Show available projects
|
||||
- `switch_project()` - Change active project
|
||||
- `get_current_project()` - Current project info
|
||||
|
||||
**Key Principles:**
|
||||
1. **Build connections** - Rich knowledge graphs > isolated notes
|
||||
2. **Ask permission** - "Would you like me to record this?"
|
||||
3. **Use exact titles** - For accurate `[[WikiLinks]]`
|
||||
4. **Leverage v0.13.0** - Edit incrementally, organize proactively, switch projects contextually
|
||||
|
||||
## Overview
|
||||
|
||||
Basic Memory allows you and users to record context in local Markdown files, building a rich knowledge base through natural conversations. The system automatically creates a semantic knowledge graph from simple text patterns.
|
||||
|
||||
- **Local-First**: All data is stored in plain text files on the user's computer
|
||||
- **Real-Time**: Users see content updates immediately
|
||||
- **Bi-Directional**: Both you and users can read and edit notes
|
||||
- **Semantic**: Simple patterns create a structured knowledge graph
|
||||
- **Persistent**: Knowledge persists across sessions and conversations
|
||||
|
||||
## The Importance of the Knowledge Graph
|
||||
|
||||
Basic Memory's value comes from connections between notes, not just the notes themselves. When writing notes, your primary goal should be creating a rich, interconnected knowledge graph.
|
||||
|
||||
When creating content, focus on:
|
||||
|
||||
1. **Increasing Semantic Density**: Add multiple observations and relations to each note
|
||||
2. **Using Accurate References**: Aim to reference existing entities by their exact titles
|
||||
3. **Creating Forward References**: Feel free to reference entities that don't exist yet - Basic Memory will resolve these when they're created later
|
||||
4. **Creating Bidirectional Links**: When appropriate, connect entities from both directions
|
||||
5. **Using Meaningful Categories**: Add semantic context with appropriate observation categories
|
||||
6. **Choosing Precise Relations**: Use specific relation types that convey meaning
|
||||
|
||||
Remember that a knowledge graph with 10 heavily connected notes is more valuable than 20 isolated notes. Your job is to help build these connections.
|
||||
|
||||
## Core Tools Reference
|
||||
|
||||
### Essential Content Management
|
||||
|
||||
**Writing knowledge** (most important tool):
|
||||
```
|
||||
write_note(
|
||||
title="Search Design",
|
||||
content="# Search Design\n...",
|
||||
folder="specs", # Optional
|
||||
tags=["search", "design"], # v0.13.0: now searchable!
|
||||
project="work-notes" # v0.13.0: target specific project
|
||||
)
|
||||
```
|
||||
|
||||
**Reading knowledge:**
|
||||
```
|
||||
read_note("Search Design") # By title
|
||||
read_note("specs/search-design") # By path
|
||||
read_note("memory://specs/search") # By memory URL
|
||||
```
|
||||
|
||||
**Viewing notes as formatted artifacts (Claude Desktop):**
|
||||
```
|
||||
view_note("Search Design") # Creates readable artifact
|
||||
view_note("specs/search-design") # By permalink
|
||||
view_note("memory://specs/search") # By memory URL
|
||||
```
|
||||
|
||||
**Incremental editing** (v0.13.0):
|
||||
```
|
||||
edit_note(
|
||||
identifier="Search Design", # Must be EXACT title/permalink (strict matching)
|
||||
operation="append", # append, prepend, find_replace, replace_section
|
||||
content="\n## New Section\nContent here..."
|
||||
)
|
||||
```
|
||||
**⚠️ Important:** `edit_note` requires exact identifiers (no fuzzy matching). Use `search_notes()` first if uncertain.
|
||||
|
||||
**File organization** (v0.13.0):
|
||||
```
|
||||
move_note(
|
||||
identifier="Old Note", # Must be EXACT title/permalink (strict matching)
|
||||
destination="archive/old-note.md" # Folders created automatically
|
||||
)
|
||||
```
|
||||
**⚠️ Important:** `move_note` requires exact identifiers (no fuzzy matching). Use `search_notes()` first if uncertain.
|
||||
|
||||
### Project Management (v0.13.0)
|
||||
|
||||
```
|
||||
list_projects() # Show available projects
|
||||
switch_project("work-notes") # Change active project
|
||||
get_current_project() # Current project info
|
||||
```
|
||||
|
||||
### Search & Discovery
|
||||
|
||||
```
|
||||
search_notes("authentication system") # v0.13.0: includes frontmatter tags
|
||||
build_context("memory://specs/search") # Follow knowledge graph connections
|
||||
recent_activity(timeframe="1 week") # Check what's been updated
|
||||
```
|
||||
|
||||
## memory:// URLs Explained
|
||||
|
||||
Basic Memory uses a special URL format to reference entities in the knowledge graph:
|
||||
|
||||
- `memory://title` - Reference by title
|
||||
- `memory://folder/title` - Reference by folder and title
|
||||
- `memory://permalink` - Reference by permalink
|
||||
- `memory://path/relation_type/*` - Follow all relations of a specific type
|
||||
- `memory://path/*/target` - Find all entities with relations to target
|
||||
|
||||
## Semantic Markdown Format
|
||||
|
||||
Knowledge is encoded in standard markdown using simple patterns:
|
||||
|
||||
**Observations** - Facts about an entity:
|
||||
```markdown
|
||||
- [category] This is an observation #tag1 #tag2 (optional context)
|
||||
```
|
||||
|
||||
**Relations** - Links between entities:
|
||||
```markdown
|
||||
- relation_type [[Target Entity]] (optional context)
|
||||
```
|
||||
|
||||
**Common Categories & Relation Types:**
|
||||
- Categories: `[idea]`, `[decision]`, `[question]`, `[fact]`, `[requirement]`, `[technique]`, `[recipe]`, `[preference]`
|
||||
- Relations: `relates_to`, `implements`, `requires`, `extends`, `part_of`, `pairs_with`, `inspired_by`, `originated_from`
|
||||
|
||||
## When to Record Context
|
||||
|
||||
**Always consider recording context when**:
|
||||
|
||||
1. Users make decisions or reach conclusions
|
||||
2. Important information emerges during conversation
|
||||
3. Multiple related topics are discussed
|
||||
4. The conversation contains information that might be useful later
|
||||
5. Plans, tasks, or action items are mentioned
|
||||
|
||||
**Protocol for recording context**:
|
||||
|
||||
1. Identify valuable information in the conversation
|
||||
2. Ask the user: "Would you like me to record our discussion about [topic] in Basic Memory?"
|
||||
3. If they agree, use `write_note` to capture the information
|
||||
4. If they decline, continue without recording
|
||||
5. Let the user know when information has been recorded: "I've saved our discussion about [topic] to Basic Memory."
|
||||
|
||||
## Understanding User Interactions
|
||||
|
||||
Users will interact with Basic Memory in patterns like:
|
||||
|
||||
1. **Creating knowledge**:
|
||||
```
|
||||
Human: "Let's write up what we discussed about search."
|
||||
|
||||
You: I'll create a note capturing our discussion about the search functionality.
|
||||
[Use write_note() to record the conversation details]
|
||||
```
|
||||
|
||||
2. **Referencing existing knowledge**:
|
||||
```
|
||||
Human: "Take a look at memory://specs/search"
|
||||
|
||||
You: I'll examine that information.
|
||||
[Use build_context() to gather related information]
|
||||
[Then read_note() to access specific content]
|
||||
```
|
||||
|
||||
3. **Finding information**:
|
||||
```
|
||||
Human: "What were our decisions about auth?"
|
||||
|
||||
You: Let me find that information for you.
|
||||
[Use search_notes() to find relevant notes]
|
||||
[Then build_context() to understand connections]
|
||||
```
|
||||
|
||||
4. **Editing existing notes (v0.13.0)**:
|
||||
```
|
||||
Human: "Add a section about deployment to my API documentation"
|
||||
|
||||
You: I'll add that section to your existing documentation.
|
||||
[Use edit_note() with operation="append" to add new content]
|
||||
```
|
||||
|
||||
5. **Project management (v0.13.0)**:
|
||||
```
|
||||
Human: "Switch to my work project and show recent activity"
|
||||
|
||||
You: I'll switch to your work project and check what's been updated recently.
|
||||
[Use switch_project() then recent_activity()]
|
||||
```
|
||||
|
||||
6. **File organization (v0.13.0)**:
|
||||
```
|
||||
Human: "Move my old meeting notes to the archive folder"
|
||||
|
||||
You: I'll organize those notes for you.
|
||||
[Use move_note() to relocate files with database consistency]
|
||||
```
|
||||
|
||||
## Key Things to Remember
|
||||
|
||||
1. **Files are Truth**
|
||||
- All knowledge lives in local files on the user's computer
|
||||
- Users can edit files outside your interaction
|
||||
- Changes need to be synced by the user (usually automatic)
|
||||
- Always verify information is current with `recent_activity()`
|
||||
|
||||
2. **Building Context Effectively**
|
||||
- Start with specific entities
|
||||
- Follow meaningful relations
|
||||
- Check recent changes
|
||||
- Build context incrementally
|
||||
- Combine related information
|
||||
|
||||
3. **Writing Knowledge Wisely**
|
||||
- Same title+folder overwrites existing notes
|
||||
- Structure with clear headings and semantic markup
|
||||
- Use tags for searchability (v0.13.0: frontmatter tags indexed)
|
||||
- Keep files organized in logical folders
|
||||
|
||||
4. **Leverage v0.13.0 Features**
|
||||
- **Edit incrementally**: Use `edit_note()` for small changes vs rewriting
|
||||
- **Switch projects**: Change context when user mentions different work areas
|
||||
- **Organize proactively**: Move old content to archive folders
|
||||
- **Cross-project operations**: Create notes in specific projects while maintaining context
|
||||
|
||||
## Common Knowledge Patterns
|
||||
|
||||
### Capturing Decisions
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
tags: [coffee, brewing, pour-over, techniques] # v0.13.0: Now searchable!
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
## Context
|
||||
I've experimented with various brewing methods including French press, pour over, and espresso.
|
||||
|
||||
## Decision
|
||||
Pour over is my preferred method for light to medium roasts because it highlights subtle flavors and offers more control over the extraction.
|
||||
|
||||
## Observations
|
||||
- [technique] Blooming the coffee grounds for 30 seconds improves extraction #brewing
|
||||
- [preference] Water temperature between 195-205°F works best #temperature
|
||||
- [equipment] Gooseneck kettle provides better control of water flow #tools
|
||||
- [timing] Total brew time of 3-4 minutes produces optimal extraction #process
|
||||
|
||||
## Relations
|
||||
- pairs_with [[Light Roast Beans]]
|
||||
- contrasts_with [[French Press Method]]
|
||||
- requires [[Proper Grinding Technique]]
|
||||
- part_of [[Morning Coffee Routine]]
|
||||
```
|
||||
|
||||
### Recording Project Structure
|
||||
|
||||
```markdown
|
||||
# Garden Planning
|
||||
|
||||
## Overview
|
||||
This document outlines the garden layout and planting strategy for this season.
|
||||
|
||||
## Observations
|
||||
- [structure] Raised beds in south corner for sun exposure #layout
|
||||
- [structure] Drip irrigation system installed for efficiency #watering
|
||||
- [pattern] Companion planting used to deter pests naturally #technique
|
||||
|
||||
## Relations
|
||||
- contains [[Vegetable Section]]
|
||||
- contains [[Herb Garden]]
|
||||
- implements [[Organic Gardening Principles]]
|
||||
```
|
||||
|
||||
### Technical Discussions
|
||||
|
||||
```markdown
|
||||
# Recipe Improvement Discussion
|
||||
|
||||
## Key Points
|
||||
Discussed strategies for improving the chocolate chip cookie recipe.
|
||||
|
||||
## Observations
|
||||
- [issue] Cookies spread too thin when baked at 350°F #texture
|
||||
- [solution] Chilling dough for 24 hours improves flavor and reduces spreading #technique
|
||||
- [decision] Will use brown butter instead of regular butter #flavor
|
||||
|
||||
## Relations
|
||||
- improves [[Basic Cookie Recipe]]
|
||||
- inspired_by [[Bakery-Style Cookies]]
|
||||
- pairs_with [[Homemade Ice Cream]]
|
||||
```
|
||||
|
||||
## v0.13.0 Workflow Examples
|
||||
|
||||
### Multi-Project Conversations
|
||||
|
||||
**User:** "I need to update my work documentation and also add a personal recipe note."
|
||||
|
||||
**Workflow:**
|
||||
1. `list_projects()` - Check available projects
|
||||
2. `write_note(title="Sprint Planning", project="work-notes")` - Work content
|
||||
3. `write_note(title="Weekend Recipes", project="personal")` - Personal content
|
||||
|
||||
### Incremental Note Building
|
||||
|
||||
**User:** "Add a troubleshooting section to my setup guide."
|
||||
|
||||
**Workflow:**
|
||||
1. `edit_note(identifier="Setup Guide", operation="append", content="\n## Troubleshooting\n...")`
|
||||
|
||||
**User:** "Update the authentication section in my API docs."
|
||||
|
||||
**Workflow:**
|
||||
1. `edit_note(identifier="API Documentation", operation="replace_section", section="## Authentication")`
|
||||
|
||||
### Smart File Organization
|
||||
|
||||
**User:** "My notes are getting messy in the main folder."
|
||||
|
||||
**Workflow:**
|
||||
1. `move_note("Old Meeting Notes", "archive/2024/old-meetings.md")`
|
||||
2. `move_note("Project Notes", "projects/client-work/notes.md")`
|
||||
|
||||
### Creating Effective Relations
|
||||
|
||||
When creating relations:
|
||||
1. **Reference existing entities** by their exact title: `[[Exact Title]]`
|
||||
2. **Create forward references** to entities that don't exist yet - they'll be linked automatically when created
|
||||
3. **Search first** to find existing entities to reference
|
||||
4. **Use meaningful relation types**: `implements`, `requires`, `part_of` vs generic `relates_to`
|
||||
|
||||
**Example workflow:**
|
||||
1. `search_notes("travel")` to find existing travel-related notes
|
||||
2. Reference found entities: `- part_of [[Japan Travel Guide]]`
|
||||
3. Add forward references: `- located_in [[Tokyo]]` (even if Tokyo note doesn't exist yet)
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
**Missing Content:**
|
||||
- Try `search_notes()` with broader terms if `read_note()` fails
|
||||
- Use fuzzy matching: search for partial titles
|
||||
|
||||
**Forward References:**
|
||||
- These are normal! Basic Memory links them automatically when target notes are created
|
||||
- Inform users: "I've created forward references that will be linked when you create those notes"
|
||||
|
||||
**Sync Issues:**
|
||||
- If information seems outdated, suggest `basic-memory sync`
|
||||
- Use `recent_activity()` to check if content is current
|
||||
|
||||
**Strict Mode for Edit/Move Operations:**
|
||||
- `edit_note()` and `move_note()` require **exact identifiers** (no fuzzy matching for safety)
|
||||
- If identifier not found: use `search_notes()` first to find the exact title/permalink
|
||||
- Error messages will guide you to find correct identifiers
|
||||
- Example workflow:
|
||||
```
|
||||
# ❌ This might fail if identifier isn't exact
|
||||
edit_note("Meeting Note", "append", "content")
|
||||
|
||||
# ✅ Safe approach: search first, then use exact result
|
||||
results = search_notes("meeting")
|
||||
edit_note("Meeting Notes 2024", "append", "content") # Use exact title from search
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Proactively Record Context**
|
||||
- Offer to capture important discussions
|
||||
- Record decisions, rationales, and conclusions
|
||||
- Link to related topics
|
||||
- Ask for permission first: "Would you like me to save our discussion about [topic]?"
|
||||
- Confirm when complete: "I've saved our discussion to Basic Memory"
|
||||
|
||||
2. **Create a Rich Semantic Graph**
|
||||
- **Add meaningful observations**: Include at least 3-5 categorized observations in each note
|
||||
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
|
||||
- **Use existing entities**: Before creating a new relation, search for existing entities
|
||||
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
|
||||
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
|
||||
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead of "relates_to")
|
||||
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|
||||
|
||||
3. **Structure Content Thoughtfully**
|
||||
- Use clear, descriptive titles
|
||||
- Organize with logical sections (Context, Decision, Implementation, etc.)
|
||||
- Include relevant context and background
|
||||
- Add semantic observations with appropriate categories
|
||||
- Use a consistent format for similar types of notes
|
||||
- Balance detail with conciseness
|
||||
|
||||
4. **Navigate Knowledge Effectively**
|
||||
- Start with specific searches
|
||||
- Follow relation paths
|
||||
- Combine information from multiple sources
|
||||
- Verify information is current
|
||||
- Build a complete picture before responding
|
||||
|
||||
5. **Help Users Maintain Their Knowledge**
|
||||
- Suggest organizing related topics
|
||||
- Identify potential duplicates
|
||||
- Recommend adding relations between topics
|
||||
- Offer to create summaries of scattered information
|
||||
- Suggest potential missing relations: "I notice this might relate to [topic], would you like me to add that connection?"
|
||||
|
||||
|
||||
Built with ♥️ by Basic Machines
|
||||
+334
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
# Basic Memory Installer
|
||||
|
||||
This installer configures Basic Memory to work with Claude Desktop.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the latest installer from the [releases page](https://github.com/basicmachines-co/basic-memory/releases)
|
||||
2. Unzip the downloaded file
|
||||
3. Since the app is currently unsigned, you'll need to:
|
||||
|
||||
On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to
|
||||
scroll down.)
|
||||
|
||||
Go to Security, then click Open.
|
||||
|
||||
Click Open Anyway.
|
||||
|
||||
This button is available for about an hour after you try to open the app.
|
||||
|
||||
Enter your login password, then click OK.
|
||||
|
||||
https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac
|
||||
|
||||
5. Restart Claude Desktop
|
||||
|
||||
The warning only appears the first time you open the app. Future updates will include proper code signing.
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background -->
|
||||
<rect x="0" y="0" width="512" height="512" rx="64" fill="#111111"/>
|
||||
|
||||
<!-- Define arrowhead marker -->
|
||||
<defs>
|
||||
<marker id="arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 Z"
|
||||
fill="#00cc00"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- State 1 (initial) -->
|
||||
<circle cx="156" cy="256" r="30" fill="none" stroke="#00cc00" stroke-width="3"/>
|
||||
|
||||
<!-- State 2 (accept) -->
|
||||
<circle cx="356" cy="176" r="34" fill="none" stroke="#00cc00" stroke-width="3"/>
|
||||
<circle cx="356" cy="176" r="28" fill="none" stroke="#00cc00" stroke-width="3"/>
|
||||
|
||||
<!-- State 3 (accept) -->
|
||||
<circle cx="356" cy="336" r="34" fill="none" stroke="#00cc00" stroke-width="3"/>
|
||||
<circle cx="356" cy="336" r="28" fill="none" stroke="#00cc00" stroke-width="3"/>
|
||||
|
||||
<!-- Initial arrow -->
|
||||
<path d="M 96 256 L 126 256"
|
||||
stroke="#00cc00" stroke-width="3" fill="none"
|
||||
marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- State transitions -->
|
||||
<!-- 1 -> 2 -->
|
||||
<path d="M 180 240
|
||||
Q 260 200, 320 176"
|
||||
stroke="#00cc00" stroke-width="3" fill="none"
|
||||
marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- 1 -> 3 -->
|
||||
<path d="M 180 272
|
||||
Q 260 312, 320 336"
|
||||
stroke="#00cc00" stroke-width="3" fill="none"
|
||||
marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Self loops -->
|
||||
<path d="M 356 142
|
||||
Q 396 142, 396 176
|
||||
Q 396 210, 356 210
|
||||
Q 316 210, 316 176
|
||||
Q 316 142, 356 142"
|
||||
stroke="#00cc00" stroke-width="2" fill="none"
|
||||
marker-end="url(#arrowhead)"/>
|
||||
|
||||
<path d="M 356 302
|
||||
Q 396 302, 396 336
|
||||
Q 396 370, 356 370
|
||||
Q 316 370, 316 336
|
||||
Q 316 302, 356 302"
|
||||
stroke="#00cc00" stroke-width="2" fill="none"
|
||||
marker-end="url(#arrowhead)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,97 +0,0 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Use tkinter for GUI alerts on macOS
|
||||
if sys.platform == "darwin":
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
|
||||
def ensure_uv_installed():
|
||||
"""Check if uv is installed, install if not."""
|
||||
try:
|
||||
subprocess.run(["uv", "--version"], capture_output=True, check=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Installing uv package manager...")
|
||||
subprocess.run(
|
||||
[
|
||||
"curl",
|
||||
"-LsSf",
|
||||
"https://astral.sh/uv/install.sh",
|
||||
"|",
|
||||
"sh",
|
||||
],
|
||||
shell=True,
|
||||
)
|
||||
|
||||
|
||||
def get_config_path():
|
||||
"""Get Claude Desktop config path for current platform."""
|
||||
if sys.platform == "darwin":
|
||||
return Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
|
||||
elif sys.platform == "win32":
|
||||
return Path.home() / "AppData/Roaming/Claude/claude_desktop_config.json"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
|
||||
def update_claude_config():
|
||||
"""Update Claude Desktop config to include basic-memory."""
|
||||
config_path = get_config_path()
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load existing config or create new
|
||||
if config_path.exists():
|
||||
config = json.loads(config_path.read_text())
|
||||
else:
|
||||
config = {"mcpServers": {}}
|
||||
|
||||
# Add/update basic-memory config
|
||||
config["mcpServers"]["basic-memory"] = {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory@latest", "mcp"],
|
||||
"env": {
|
||||
"BASIC_MEMORY_ENV": "user",
|
||||
"LOGFIRE_TOKEN": "n2Fpvn34LjKYq8TdF1ZrXMgdBPXGn4HfXy6tYghZ55dB",
|
||||
},
|
||||
}
|
||||
|
||||
# Write back config
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
|
||||
|
||||
def print_completion_message():
|
||||
"""Show completion message with helpful tips."""
|
||||
message = """Installation complete! Basic Memory is now available in Claude Desktop.
|
||||
|
||||
Please restart Claude Desktop for changes to take effect.
|
||||
|
||||
Quick Start:
|
||||
1. You can run sync directly using: uvx basic-memory sync
|
||||
2. Optionally, install globally with: uv pip install basic-memory
|
||||
|
||||
Built with ♥️ by Basic Machines."""
|
||||
|
||||
if sys.platform == "darwin":
|
||||
# Show GUI message on macOS
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide the main window
|
||||
messagebox.showinfo("Basic Memory", message)
|
||||
root.destroy()
|
||||
else:
|
||||
# Fallback to console output
|
||||
print(message)
|
||||
|
||||
|
||||
def main():
|
||||
print("Welcome to Basic Memory installer")
|
||||
ensure_uv_installed()
|
||||
print("Configuring Claude Desktop...")
|
||||
update_claude_config()
|
||||
print_completion_message()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Convert SVG to PNG at various required sizes
|
||||
rsvg-convert -h 16 -w 16 icon.svg > icon_16x16.png
|
||||
rsvg-convert -h 32 -w 32 icon.svg > icon_32x32.png
|
||||
rsvg-convert -h 128 -w 128 icon.svg > icon_128x128.png
|
||||
rsvg-convert -h 256 -w 256 icon.svg > icon_256x256.png
|
||||
rsvg-convert -h 512 -w 512 icon.svg > icon_512x512.png
|
||||
|
||||
# Create iconset directory
|
||||
mkdir -p Basic.iconset
|
||||
|
||||
# Move files into iconset with Mac-specific names
|
||||
cp icon_16x16.png Basic.iconset/icon_16x16.png
|
||||
cp icon_32x32.png Basic.iconset/icon_16x16@2x.png
|
||||
cp icon_32x32.png Basic.iconset/icon_32x32.png
|
||||
cp icon_128x128.png Basic.iconset/icon_32x32@2x.png
|
||||
cp icon_256x256.png Basic.iconset/icon_128x128.png
|
||||
cp icon_512x512.png Basic.iconset/icon_256x256.png
|
||||
cp icon_512x512.png Basic.iconset/icon_512x512.png
|
||||
|
||||
# Convert iconset to icns
|
||||
iconutil -c icns Basic.iconset
|
||||
|
||||
# Clean up
|
||||
rm -rf Basic.iconset
|
||||
rm icon_*.png
|
||||
@@ -1,40 +0,0 @@
|
||||
from cx_Freeze import setup, Executable
|
||||
import sys
|
||||
|
||||
# Build options for all platforms
|
||||
build_exe_options = {
|
||||
"packages": ["json", "pathlib"],
|
||||
"excludes": ["unittest", "pydoc", "test"],
|
||||
}
|
||||
|
||||
# Platform-specific options
|
||||
if sys.platform == "win32":
|
||||
base = "Win32GUI" # Use GUI base for Windows
|
||||
build_exe_options.update(
|
||||
{
|
||||
"include_msvcr": True,
|
||||
}
|
||||
)
|
||||
target_name = "Basic Memory Installer.exe"
|
||||
else: # darwin
|
||||
base = None # Don't use GUI base for macOS
|
||||
target_name = "Basic Memory Installer"
|
||||
|
||||
executables = [
|
||||
Executable(script="installer.py", target_name=target_name, base=base, icon="Basic.icns")
|
||||
]
|
||||
|
||||
setup(
|
||||
name="basic-memory",
|
||||
version=open("../pyproject.toml").read().split('version = "', 1)[1].split('"', 1)[0],
|
||||
description="Basic Memory - Local-first knowledge management",
|
||||
options={
|
||||
"build_exe": build_exe_options,
|
||||
"bdist_mac": {
|
||||
"bundle_name": "Basic Memory Installer",
|
||||
"iconfile": "Basic.icns",
|
||||
"codesign_identity": "-", # Force ad-hoc signing
|
||||
},
|
||||
},
|
||||
executables=executables,
|
||||
)
|
||||
@@ -0,0 +1,216 @@
|
||||
# Basic Memory - Modern Command Runner
|
||||
|
||||
# Install dependencies
|
||||
install:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run unit tests in parallel
|
||||
test-unit:
|
||||
uv run pytest -p pytest_mock -v -n auto
|
||||
|
||||
# Run integration tests in parallel
|
||||
test-int:
|
||||
uv run pytest -p pytest_mock -v --no-cov -n auto test-int
|
||||
|
||||
# Run all tests
|
||||
test: test-unit test-int
|
||||
|
||||
# Lint and fix code
|
||||
lint:
|
||||
ruff check . --fix
|
||||
|
||||
# Type check code
|
||||
type-check:
|
||||
uv run pyright
|
||||
|
||||
# Clean build artifacts and cache files
|
||||
clean:
|
||||
find . -type f -name '*.pyc' -delete
|
||||
find . -type d -name '__pycache__' -exec rm -r {} +
|
||||
rm -rf installer/build/ installer/dist/ dist/
|
||||
rm -f rw.*.dmg .coverage.*
|
||||
|
||||
# Format code with ruff
|
||||
format:
|
||||
uv run ruff format .
|
||||
|
||||
# Run MCP inspector tool
|
||||
run-inspector:
|
||||
npx @modelcontextprotocol/inspector
|
||||
|
||||
# Build macOS installer
|
||||
installer-mac:
|
||||
cd installer && chmod +x make_icons.sh && ./make_icons.sh
|
||||
cd installer && uv run python setup.py bdist_mac
|
||||
|
||||
# Build Windows installer
|
||||
installer-win:
|
||||
cd installer && uv run python setup.py bdist_win32
|
||||
|
||||
# Update all dependencies to latest versions
|
||||
update-deps:
|
||||
uv sync --upgrade
|
||||
|
||||
# Run all code quality checks and tests
|
||||
check: lint format type-check test
|
||||
|
||||
# Generate Alembic migration with descriptive message
|
||||
migration message:
|
||||
cd src/basic_memory/alembic && alembic revision --autogenerate -m "{{message}}"
|
||||
|
||||
# Create a stable release (e.g., just release v0.13.2)
|
||||
release version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate version format
|
||||
if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ Invalid version format. Use: v0.13.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version number without 'v' prefix
|
||||
VERSION_NUM=$(echo "{{version}}" | sed 's/^v//')
|
||||
|
||||
echo "🚀 Creating stable release {{version}}"
|
||||
|
||||
# Pre-flight checks
|
||||
echo "📋 Running pre-flight checks..."
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "❌ Uncommitted changes found. Please commit or stash them first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $(git branch --show-current) != "main" ]]; then
|
||||
echo "❌ Not on main branch. Switch to main first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if tag already exists
|
||||
if git tag -l "{{version}}" | grep -q "{{version}}"; then
|
||||
echo "❌ Tag {{version}} already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run quality checks
|
||||
echo "🔍 Running quality checks..."
|
||||
just check
|
||||
|
||||
# Update version in __init__.py
|
||||
echo "📝 Updating version in __init__.py..."
|
||||
sed -i.bak "s/__version__ = \".*\"/__version__ = \"$VERSION_NUM\"/" src/basic_memory/__init__.py
|
||||
rm -f src/basic_memory/__init__.py.bak
|
||||
|
||||
# Commit version update
|
||||
git add src/basic_memory/__init__.py
|
||||
git commit -m "chore: update version to $VERSION_NUM for {{version}} release"
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag {{version}}..."
|
||||
git tag "{{version}}"
|
||||
|
||||
echo "📤 Pushing to GitHub..."
|
||||
git push origin main
|
||||
git push origin "{{version}}"
|
||||
|
||||
echo "✅ Release {{version}} created successfully!"
|
||||
echo "📦 GitHub Actions will build and publish to PyPI"
|
||||
echo "🔗 Monitor at: https://github.com/basicmachines-co/basic-memory/actions"
|
||||
|
||||
# Create a beta release (e.g., just beta v0.13.2b1)
|
||||
beta version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate version format (allow beta/rc suffixes)
|
||||
if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(b[0-9]+|rc[0-9]+)$ ]]; then
|
||||
echo "❌ Invalid beta version format. Use: v0.13.2b1 or v0.13.2rc1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version number without 'v' prefix
|
||||
VERSION_NUM=$(echo "{{version}}" | sed 's/^v//')
|
||||
|
||||
echo "🧪 Creating beta release {{version}}"
|
||||
|
||||
# Pre-flight checks
|
||||
echo "📋 Running pre-flight checks..."
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "❌ Uncommitted changes found. Please commit or stash them first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $(git branch --show-current) != "main" ]]; then
|
||||
echo "❌ Not on main branch. Switch to main first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if tag already exists
|
||||
if git tag -l "{{version}}" | grep -q "{{version}}"; then
|
||||
echo "❌ Tag {{version}} already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run quality checks
|
||||
echo "🔍 Running quality checks..."
|
||||
just check
|
||||
|
||||
# Update version in __init__.py
|
||||
echo "📝 Updating version in __init__.py..."
|
||||
sed -i.bak "s/__version__ = \".*\"/__version__ = \"$VERSION_NUM\"/" src/basic_memory/__init__.py
|
||||
rm -f src/basic_memory/__init__.py.bak
|
||||
|
||||
# Commit version update
|
||||
git add src/basic_memory/__init__.py
|
||||
git commit -m "chore: update version to $VERSION_NUM for {{version}} beta release"
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag {{version}}..."
|
||||
git tag "{{version}}"
|
||||
|
||||
echo "📤 Pushing to GitHub..."
|
||||
git push origin main
|
||||
git push origin "{{version}}"
|
||||
|
||||
echo "✅ Beta release {{version}} created successfully!"
|
||||
echo "📦 GitHub Actions will build and publish to PyPI as pre-release"
|
||||
echo "🔗 Monitor at: https://github.com/basicmachines-co/basic-memory/actions"
|
||||
echo "📥 Install with: uv tool install basic-memory --pre"
|
||||
|
||||
# Build DXT package with bundled virtual environment
|
||||
dxt:
|
||||
#!/usr/bin/env bash
|
||||
echo "🏗️ Building DXT package..."
|
||||
|
||||
# Clean and create bundle directory
|
||||
rm -rf dxt-bundle
|
||||
mkdir -p dxt-bundle/server/lib dxt-bundle/src
|
||||
|
||||
# Bundle dependencies to server/lib (like Anthropic example)
|
||||
echo "📦 Bundling Python dependencies..."
|
||||
uv pip install -e . --target dxt-bundle/server/lib --force-reinstall
|
||||
|
||||
# Copy source code
|
||||
echo "📄 Copying source code..."
|
||||
cp -r src/basic_memory dxt-bundle/src/
|
||||
|
||||
# Copy manifest
|
||||
cp manifest.json dxt-bundle/
|
||||
|
||||
# Copy assets (icons, screenshots)
|
||||
echo "🖼️ Copying assets..."
|
||||
mkdir -p dxt-bundle/images
|
||||
cp images/disk-ai-logo-black-fg-white-bg.png dxt-bundle/images/
|
||||
cp images/claude-*.png dxt-bundle/images/
|
||||
|
||||
# Create DXT package
|
||||
echo "📦 Creating DXT package..."
|
||||
cd dxt-bundle && dxt pack . ../basic-memory.dxt
|
||||
|
||||
echo "✅ DXT package created: basic-memory.dxt"
|
||||
echo "🔍 Info: dxt info basic-memory.dxt"
|
||||
echo "🧪 Test: Install in Claude Desktop"
|
||||
|
||||
# List all available recipes
|
||||
default:
|
||||
@just --list
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"dxt_version": "0.1",
|
||||
"name": "basic-memory",
|
||||
"display_name": "Basic Memory Knowledge Management",
|
||||
"version": "0.14.0",
|
||||
"description": "Local-first knowledge management with AI integration",
|
||||
"long_description": "Basic Memory is a local-first knowledge management system that creates a personal knowledge graph from markdown files. It enables bidirectional communication between LLMs and your local knowledge base through the Model Context Protocol (MCP), allowing AI assistants to read, write, and navigate your knowledge seamlessly.",
|
||||
"author": {
|
||||
"name": "Basic Machines",
|
||||
"email": "hello@basicmachines.co",
|
||||
"url": "https://github.com/basicmachines-co"
|
||||
},
|
||||
"homepage": "https://basicmemory.com/",
|
||||
"documentation": "https://memory.basicmachines.co/",
|
||||
"support": "https://memory.basicmachines.co/user-guide",
|
||||
"icon": "images/disk-ai-logo-black-fg-white-bg.png",
|
||||
"screenshots": [
|
||||
"images/claude-continue-conversation.png",
|
||||
"images/claude-switch-project.png",
|
||||
"images/claude-view-note.png"
|
||||
],
|
||||
"server": {
|
||||
"type": "python",
|
||||
"entry_point": "src/basic_memory/cli/main.py",
|
||||
"mcp_config": {
|
||||
"command": "python3",
|
||||
"args": [
|
||||
"${__dirname}/src/basic_memory/cli/main.py",
|
||||
"mcp",
|
||||
"--transport=stdio"
|
||||
],
|
||||
"env": {
|
||||
"BASIC_MEMORY_HOME": "${user_data_dir}/basic-memory",
|
||||
"PYTHONPATH": "${__dirname}/server/lib:${__dirname}/src"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"name": "write_note",
|
||||
"description": "Create or update markdown notes with semantic structure"
|
||||
},
|
||||
{
|
||||
"name": "read_note",
|
||||
"description": "Read notes by title, permalink, or memory:// URL"
|
||||
},
|
||||
{
|
||||
"name": "edit_note",
|
||||
"description": "Edit notes incrementally (append, prepend, find/replace)"
|
||||
},
|
||||
{
|
||||
"name": "move_note",
|
||||
"description": "Move or rename notes"
|
||||
},
|
||||
{
|
||||
"name": "view_note",
|
||||
"description": "Display notes as formatted artifacts"
|
||||
},
|
||||
{
|
||||
"name": "read_content",
|
||||
"description": "Read raw file content (text, images, binaries)"
|
||||
},
|
||||
{
|
||||
"name": "delete_note",
|
||||
"description": "Delete notes from knowledge base"
|
||||
},
|
||||
{
|
||||
"name": "search_notes",
|
||||
"description": "Full-text search across knowledge base"
|
||||
},
|
||||
{
|
||||
"name": "build_context",
|
||||
"description": "Navigate knowledge graph via memory:// URLs"
|
||||
},
|
||||
{
|
||||
"name": "recent_activity",
|
||||
"description": "Get recently updated information"
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "Browse directory contents with filtering"
|
||||
},
|
||||
{
|
||||
"name": "canvas",
|
||||
"description": "Generate Obsidian canvas files for knowledge visualization"
|
||||
},
|
||||
{
|
||||
"name": "list_memory_projects",
|
||||
"description": "List all projects with status"
|
||||
},
|
||||
{
|
||||
"name": "switch_project",
|
||||
"description": "Switch between project contexts"
|
||||
},
|
||||
{
|
||||
"name": "get_current_project",
|
||||
"description": "Show current project stats"
|
||||
},
|
||||
{
|
||||
"name": "create_memory_project",
|
||||
"description": "Create new projects"
|
||||
},
|
||||
{
|
||||
"name": "delete_project",
|
||||
"description": "Remove projects"
|
||||
},
|
||||
{
|
||||
"name": "set_default_project",
|
||||
"description": "Set default project"
|
||||
},
|
||||
{
|
||||
"name": "sync_status",
|
||||
"description": "Check synchronization status"
|
||||
}
|
||||
],
|
||||
"prompts": [
|
||||
{
|
||||
"name": "continue_conversation",
|
||||
"description": "Continue previous conversations with relevant historical context",
|
||||
"arguments": [
|
||||
"topic",
|
||||
"timeframe"
|
||||
],
|
||||
"text": "Continue our conversation about {{topic}} from {{timeframe}} ago. Use the continue_conversation tool to find relevant context and build on our previous discussion."
|
||||
},
|
||||
{
|
||||
"name": "search_notes",
|
||||
"description": "Search knowledge base with detailed, formatted results",
|
||||
"arguments": [
|
||||
"query",
|
||||
"after_date"
|
||||
],
|
||||
"text": "Search my knowledge base for information about {{query}}. Use the search_notes tool to find relevant notes and provide a comprehensive summary of what I know about this topic."
|
||||
},
|
||||
{
|
||||
"name": "recent_activity",
|
||||
"description": "View recently changed items with formatted output",
|
||||
"arguments": [
|
||||
"timeframe"
|
||||
],
|
||||
"text": "Show me what I've been working on recently in the past {{timeframe}}. Use the recent_activity tool to display my recent notes and changes."
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"knowledge",
|
||||
"management",
|
||||
"local-first",
|
||||
"ai",
|
||||
"mcp",
|
||||
"markdown",
|
||||
"notes",
|
||||
"zettelkasten"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/basicmachines-co/basic-memory"
|
||||
},
|
||||
"compatibility": {
|
||||
"claude_desktop": ">=0.10.0",
|
||||
"platforms": ["darwin", "win32", "linux"],
|
||||
"runtimes": {
|
||||
"python": ">=3.12.0 <4"
|
||||
}
|
||||
}
|
||||
}
|
||||
-378
@@ -1,378 +0,0 @@
|
||||
{"type":"entity","name":"Paul","entityType":"person","observations":["Software developer combining DIY ethics, Free Software principles, and theoretical computer science","Created the Basic Machines project","Values authentic exchange of ideas","Approaches AI interaction with emphasis on genuine technical discussion","Comfortable with uncertainty and open dialogue","Balances practical implementation with broader implications"]}
|
||||
{"type":"entity","name":"Basic_Machines","entityType":"project","observations":["Local-first knowledge management system","Combines filesystem durability with graph-based knowledge representation","Focuses on enhancing human agency and understanding","Synthesizes DIY ethics, Free Software philosophy, and theoretical computer science","Current focus includes basic-memory system"]}
|
||||
{"type":"entity","name":"basic-memory","entityType":"software_system","observations":["A core component of Basic Machines","Local-first knowledge management system","Combines filesystem persistence with graph-based knowledge representation","Being implemented collaboratively by Paul and Claude"]}
|
||||
{"type":"entity","name":"basic-memory_implementation_patterns","entityType":"technical_patterns","observations":["Filesystem is source of truth - all changes write to files first","Clean separation of concerns between models (SQLAlchemy), schemas (Pydantic), and services","Repository pattern for database access","Service layer handling business logic and coordination","Atomic file operations using temporary files for safety","Clear error handling hierarchy with specific error types","Comprehensive test coverage with pytest and fixtures","Async/await used throughout the codebase","Validation using Pydantic models with custom validators"]}
|
||||
{"type":"entity","name":"fileio_module","entityType":"code_module","observations":["Extracted from EntityService to handle all file operations","Provides read_entity_file, write_entity_file, and delete_entity_file functions","Handles markdown parsing and formatting","Implements atomic file operations","Provides consistent error handling","Enables reuse across services"]}
|
||||
{"type":"entity","name":"entity_service","entityType":"code_module","observations":["Manages entities in both filesystem and database","Uses fileio module for file operations","Maintains database index of entities","Handles entity creation, retrieval, and deletion","Follows 'filesystem is source of truth' principle","Coordinates with observation service for full entity management"]}
|
||||
{"type":"entity","name":"observation_service","entityType":"code_module","observations":["Manages observations within entity files","Provides database indexing for efficient observation queries","Works with complete Entity objects rather than IDs","Handles observation addition and search","Maintains consistency between files and database","Under development for update/remove operations"]}
|
||||
{"type":"entity","name":"observation_management","entityType":"design_challenge","observations":["Key challenge: maintaining observation state across files and database","Exploring bulk update approach - treating all observations as a unit","Considering tracked observations with markdown comments for IDs","Investigating diff-based approach for observation-level changes","Evaluating position-based management without explicit IDs","Trade-offs between implementation complexity and markdown readability"]}
|
||||
{"type":"entity","name":"testing_infrastructure","entityType":"technical_patterns","observations":["Uses pytest with async support via pytest-asyncio","In-memory SQLite database for test isolation","Temporary directories for file operation testing","Comprehensive fixture system for test setup","Tests organized by component (entity, observation, etc)","Covers happy path, error cases, and edge cases","Uses monkeypatch for mocking dependencies","Clear separation between arrange, act, assert sections","Uses in-memory SQLite database for test isolation","Comprehensive fixture system for test data setup","Proper async test handling with pytest-asyncio"]}
|
||||
{"type":"entity","name":"test_categories","entityType":"test_suite","observations":["Happy path tests verify core functionality","Error path tests ensure proper error handling","Edge cases test special characters and long content","File operation tests verify atomic writes and rollbacks","Database sync tests verify index consistency","Recovery tests for rebuild operations","Punted on concurrent operation tests due to session management complexity"]}
|
||||
{"type":"entity","name":"completed_work","entityType":"project_milestone","observations":["Extracted file operations to fileio.py module","Updated EntityService to use fileio functions","Implemented initial ObservationService","Created comprehensive test suite","Established clear project patterns and principles","Set up basic database schema with SQLAlchemy","Created Pydantic models for validation"]}
|
||||
{"type":"entity","name":"future_work","entityType":"project_tasks","observations":["Implement observation updates/removals","Design proper session management for concurrent operations","Update EntityService tests for new fileio module","Add more sophisticated search functionality","Handle markdown formatting edge cases","Consider versioning for file changes","Implement proper backup strategy"]}
|
||||
{"type":"entity","name":"design_decisions","entityType":"technical_decisions","observations":["Filesystem as source of truth over database","Markdown format for human readability and editing","Atomic file operations for safety","SQLite + SQLAlchemy for proven reliability","Pydantic for validation and ID generation","Async/await for better scalability","Clear separation between files and database roles","Explicit error hierarchies for better handling"]}
|
||||
{"type":"entity","name":"concurrency_considerations","entityType":"technical_challenge","observations":["SQLAlchemy session management in async context","File operation atomicity","Transaction isolation levels","Potential for conflicting updates","Need for proper session lifecycle","Possibility of file system race conditions","Database lock management"]}
|
||||
{"type":"entity","name":"observation_update_approaches","entityType":"design_alternatives","observations":["Each approach trades off between simplicity, efficiency, and robustness","Four main approaches considered: bulk update, tracked IDs, diff-based, and position-based","Discussion revealed importance of human readability in file format","Consideration of manual editing workflows key to design","File system as source of truth principle guides tradeoffs"]}
|
||||
{"type":"entity","name":"bulk_update_approach","entityType":"design_option","observations":["Update all observations at once in a single operation","Simpler file operations - just rewrite the whole list","No need for observation matching or IDs","Very consistent with source of truth principle","Less efficient for small changes","May have concurrency implications","Simplest implementation option"]}
|
||||
{"type":"entity","name":"tracked_observations_approach","entityType":"design_option","observations":["Use markdown comments to store observation IDs","Enables precise updates and deletes","IDs stored as HTML comments in markdown","More complex markdown parsing required","IDs visible in raw markdown files","Balances tracking with readability"]}
|
||||
{"type":"entity","name":"diff_based_approach","entityType":"design_option","observations":["Implement observation-aware diffing","Track changes at observation level","More efficient for updates","Preserves manual edits and changes","More complex implementation needed","Must handle merge conflicts","Most sophisticated option considered"]}
|
||||
{"type":"entity","name":"position_based_approach","entityType":"design_option","observations":["Track observations by position/order","No explicit IDs needed","Cleanest markdown format","Order changes could break references","Difficult to handle concurrent edits","Most fragile option considered"]}
|
||||
{"type":"entity","name":"tasks_and_progress","entityType":"project_tracking","observations":["Current focus on observation management implementation","Completed core file operations extraction","Completed EntityService updates","Completed initial ObservationService","Basic test coverage in place","Future work includes concurrent operations","Future work includes search improvements","Need to handle markdown edge cases"]}
|
||||
{"type":"entity","name":"error_handling_patterns","entityType":"technical_patterns","observations":["Custom exception hierarchy with ServiceError base","Specific error types (FileOperationError, DatabaseSyncError, etc)","Clear separation between file and database errors","Error propagation patterns established","Focus on actionable error messages","Error handling at appropriate levels"]}
|
||||
{"type":"entity","name":"data_models","entityType":"technical_implementation","observations":["SQLAlchemy models for database structure","Pydantic schemas for API/service layer","Entity model with UUID-based IDs","Observation model with entity relationships","UTCDateTime custom type for timestamps","Automatic ID generation in Pydantic models","Strict validation rules"]}
|
||||
{"type":"entity","name":"markdown_format","entityType":"file_format","observations":["Simple, human-readable format","Entity name as H1 header","Metadata in key-value format","Observations as bullet points","Atomic file operations for updates","Designed for manual editing","No hidden metadata in main content"]}
|
||||
{"type":"entity","name":"test_driven_development","entityType":"development_pattern","observations":["Tests revealed need for atomic file operations","Error cases drove error hierarchy design","Edge cases informed validation rules","Test fixtures shaped service interfaces","File operations extracted due to test patterns","Concurrent test issues revealed session management needs"]}
|
||||
{"type":"entity","name":"architecture_evolution","entityType":"design_process","observations":["Started with simple EntityService implementation","Circular dependency between Entity and Observation services revealed design flaw","Extracted file operations to separate module","Moved to passing Entity objects rather than IDs","Improved separation of concerns through iterations","File operations became reusable across services","Database became true 'index' rather than source of truth"]}
|
||||
{"type":"entity","name":"validation_patterns","entityType":"technical_patterns","observations":["Pydantic models provide schema validation","Automatic ID generation if not provided","Database constraints via SQLAlchemy","Runtime checks in services","Markdown format validation","Error handling for invalid states"]}
|
||||
{"type":"entity","name":"markdown_examples","entityType":"documentation","observations":["Example of basic entity:\n# Entity Name\ntype: entity_type\n\n## Observations\n- First observation\n- Second observation","Example with special characters:\n# Test & Entity!\ntype: test\n\n## Observations\n- Test & observation with @#$% special chars!","Format ensures human readability:\n# Basic Machines\ntype: project\n\n## Observations\n- Local-first knowledge management system\n- Combines filesystem durability with graph-based knowledge representation","Future consideration for observation IDs:\n# Entity Name\ntype: entity_type\n\n## Observations\n- <!-- obs-id: abc123 -->\n This is an observation with ID"]}
|
||||
{"type":"entity","name":"markdown_parsing_rules","entityType":"technical_implementation","observations":["H1 header contains entity name","Metadata uses key: value format","Observations section marked by H2 header","Each observation is a markdown list item","Blank lines separate sections","Special characters allowed in content","No restrictions on observation content"]}
|
||||
{"type":"entity","name":"schema_definitions","entityType":"technical_documentation","observations":["SQLAlchemy Entity model:\nclass Entity(Base):\n id: str (primary key)\n name: str (unique)\n entity_type: str\n created_at: datetime\n updated_at: datetime","SQLAlchemy Observation model:\nclass Observation(Base):\n id: str (primary key)\n entity_id: str (foreign key)\n content: str\n created_at: datetime\n context: Optional[str]","Pydantic Entity schema:\nclass Entity(BaseModel):\n id: str\n name: str\n entity_type: str\n observations: List[Observation]"]}
|
||||
{"type":"entity","name":"test_evolution","entityType":"development_history","observations":["Started with basic Entity CRUD tests","Added filesystem verification to all tests","Developed concurrent operation tests (later removed)","Edge case tests drove better error handling","Test fixtures evolved to support both file and DB testing","Mocking patterns for file/DB operations","Special cases for long content and special characters"]}
|
||||
{"type":"entity","name":"implementation_challenges","entityType":"technical_issues","observations":["Initial circular dependency between services","SQLAlchemy session management in async context","Atomic file operations with proper error handling","Maintaining DB sync with filesystem changes","Handling long content in observations","Managing test isolation with file operations","Deciding on markdown format tradeoffs","Concurrent operation complexity"]}
|
||||
{"type":"entity","name":"Basic_Factory","entityType":"Project","observations":["Collaborative project between Paul and Claude","Explores AI-human collaboration in software development","Uses MCP tools for file and memory management","Built with git integration capabilities","Focuses on maintaining project context across sessions","About 90% complete with MCP tools","Still needs improvements in collaboration via files/git/github","Will be used to document and share collaborative development process"]}
|
||||
{"type":"entity","name":"Basic_Factory_Components","entityType":"Technical","observations":["Server-side rendering with JinjaX","HTMX for dynamic updates","Alpine.js for client-side state","Tailwind CSS for styling","Component translation from React/shadcn/ui","Focus on simplicity and understandability","Demonstrates meta-compiler principles in component translation"]}
|
||||
{"type":"entity","name":"Component_Translation_Process","entityType":"Methodology","observations":["Treats component porting as meta-compilation","Maps between React/TypeScript and JinjaX/Alpine.js domains","Uses formal grammar transformation approaches","Maintains functionality while simplifying implementation","Focuses on server-side rendering patterns","Preserves accessibility and performance","Uses short, focused git branches for each component"]}
|
||||
{"type":"entity","name":"Basic_Machines_Philosophy","entityType":"Philosophy","observations":["Combines DIY punk ethics with software development","Emphasizes user empowerment and understanding","Values simplicity and composability","Treats complex systems as combinations of simple parts","Focuses on authentic creation and sharing","Draws inspiration from punk rock, Free Software, and theoretical CS","Emphasizes the cycle of creation, complexity, and renewal"]}
|
||||
{"type":"entity","name":"Basic_Machines_Manifesto","entityType":"Document","observations":["Created through collaboration between Paul and Claude","Explores connection between DIY punk ethics and software development","Emphasizes composition over inheritance in both philosophy and practice","Views software development through lens of basic machines that combine for complex computation","Advocates for user empowerment and technological independence","Structured in sections covering Origins, Philosophy, Technical Implementation, and AI Collaboration","Draws connections between punk rock, free software, and theoretical computer science","Emphasizes importance of sharing knowledge and building community","Released in December 2024"]}
|
||||
{"type":"entity","name":"AI_Human_Collaboration_Model","entityType":"Methodology","observations":["Focuses on deep collaboration rather than simple task completion","Maintains rich context across sessions via knowledge graph","Uses short, focused git branches for each collaborative session","Values intellectual partnership over simple code generation","Emphasizes both practical implementation and theoretical exploration","Creates space for authentic exchange while maintaining AI/human clarity","Uses formal methods when appropriate (like grammar transformation)","Documents decisions and processes for future reference","Developed through Basic Machines project experience"]}
|
||||
{"type":"entity","name":"Basic_Machines_Roadmap","entityType":"Project_Plan","observations":["Phase 1 (30 days): Build basic-machines.co website","Phase 2 (60-90 days): Develop premium component bundles","Phase 3 (90-120 days): Launch Basic Foundation commercial offering","Focus on building brand and marketing presence","Prioritize components needed for own site development","Document and share collaboration process","Build sustainable business model aligned with values"]}
|
||||
{"type":"entity","name":"Basic_Machines_Website","entityType":"Project","observations":["To be built at basic-machines.co","Will showcase products and vision","Needs components for navigation, hero sections, features","Will demonstrate component usage in production","Will include blog for sharing progress","Focus on clear value proposition","Platform for sharing Basic Machines philosophy"]}
|
||||
{"type":"entity","name":"Basic_Memory_Markdown_Example","entityType":"Example","observations":["Shows complete markdown structure for basic-memory entity","Uses frontmatter for metadata (id, type, created, context)","Has main description section after title","Includes Observations as bullet points","Shows Relations with [id] relation_type | context format","Lists References at bottom","Created during initial design discussion","Serves as canonical example of file format"]}
|
||||
{"type":"entity","name":"Basic_Memory_Database_Schema","entityType":"Technical","observations":["Uses SQLite for local storage","Entities table with id, name, type, created_at, context, description, references","Observations table linking to entities with content and context","Relations table tracking directional relationships between entities","References column needs quotes as SQL reserved word","Designed for easy rebuilding from markdown files","Foreign key constraints maintain data integrity","Unique constraint on relations prevents duplicates","Created_at timestamps track history","Context fields enable tracking information sources"]}
|
||||
{"type":"entity","name":"Basic_Memory_Project_Structure","entityType":"Technical","observations":["Uses dbmate for database migrations","Projects directory stores SQLite databases and markdown files","Makefile provides common development commands","Environment vars configure database connection","db/migrations directory for SQL schema changes","Gitignore excludes database files and env config","Uses Python 3.12 with modern tooling","Tests directory for pytest files","Follows Basic Machines project conventions"]}
|
||||
{"type":"entity","name":"Basic_Memory_Project_Isolation_Decision","entityType":"Decision","observations":["Decided to defer multi-project support to post-MVP","Will use separate SQLite databases per project","Initially using projects directory in code repository","Plan to make location configurable later","No changes needed to core domain model","Keeps initial implementation simple","FTS/search capabilities also deferred for simplicity"]}
|
||||
{"type":"entity","name":"Basic_Memory_Implementation_Plan","entityType":"Plan","observations":["Start with SQLAlchemy models matching schema","Then build CLI for basic operations","Then implement markdown parser","Use TDD approach throughout","Begin with core domain model","CLI will support CRUD operations","Parser must handle frontmatter and sections","Following modular development approach","Planning to use typer for CLI","Will use modern Python tools and practices"]}
|
||||
{"type":"entity","name":"Basic_Memory_Implementation_Status","entityType":"Status","observations":["Core modules implemented: models, services, repository, fileio","Modular architecture with clear separation of concerns","File operations extracted to separate fileio module","Initial ObservationService implementation complete","Basic test coverage in place","Exploring observation management strategies","Using SQLAlchemy for database interaction","Markdown file operations working","Entity management functional","Repository layer implementation complete with SQLAlchemy models and tests","Database operations working with proper UTC timestamp handling","In-memory SQLite testing infrastructure proven effective"]}
|
||||
{"type":"entity","name":"Basic_Memory_Observation_Management_Design","entityType":"Design","observations":["Four approaches under consideration","Bulk Update: Simple but less efficient","Tracked Observations: Precise but clutters markdown","Diff-based: Efficient but complex","Position-based: Clean but fragile","Key challenge is balancing markdown readability with efficient updates","Must maintain filesystem as source of truth","Need to consider concurrent edits","Currently evaluating trade-offs","Implementation choice pending discussion"]}
|
||||
{"type":"entity","name":"Basic_Memory_Architectural_Decisions","entityType":"Decisions","observations":["Split file operations into separate fileio module","Using SQLAlchemy for database operations","Maintain filesystem as source of truth","Modular service-based architecture","Clear separation between data access and business logic","Repository pattern for database interactions","Schemas separate from models","Focus on maintainability and testability","Services handle business rules","Considering concurrency in design"]}
|
||||
{"type":"entity","name":"Basic_Memory_Implementation_Analysis","entityType":"Analysis","observations":["Clean modular architecture with clear responsibilities","Strong typing throughout codebase","Excellent error handling with custom exceptions","SQLAlchemy models perfectly match our domain model","Atomic file operations for data safety","Services implement filesystem-as-source-of-truth principle","Async support throughout","Good separation between domain models and database models","Careful handling of UTC timestamps","Smart use of SQLAlchemy relationships"]}
|
||||
{"type":"entity","name":"Basic_Memory_Current_Challenges","entityType":"Challenges","observations":["Observation update/removal strategy needs to be chosen","Need to handle concurrent file operations safely","Search functionality to be implemented","Edge cases in markdown formatting to be handled","Session management for concurrent operations needed","Balance between file operations and database sync","Testing coverage could be expanded","Need to handle relationship updates in files"]}
|
||||
{"type":"entity","name":"Basic_Memory_Observation_Hash_Tracking","entityType":"Design","observations":["Use content hashes to track observation identity","Store hashes in database but not in markdown","Can match observations across file edits using hashes","Similar to how git tracks content changes","Keeps markdown clean and human-friendly","Allows efficient bulk updates","Handles reordering of observations","Maintains filesystem as source of truth","No need for visible IDs in markdown","Could track observation history through hash changes"]}
|
||||
{"type":"entity","name":"Basic_Memory_Repository_Implementation","entityType":"Code_Implementation","observations":["Implemented base Repository class with CRUD operations","Added specialized EntityRepository, ObservationRepository, and RelationRepository","Used string IDs instead of UUIDs","Added UTCDateTime custom type for timestamp handling","Used in-memory SQLite for testing","Achieved 84% test coverage","Created comprehensive pytest fixtures"]}
|
||||
{"type":"entity","name":"Basic_Memory_Dependencies","entityType":"Technical","observations":["Uses Python 3.12","SQLAlchemy with async support","pytest-asyncio for async testing","aiosqlite for async SQLite operations","greenlet for SQLAlchemy async support","uv for dependency management","pytest-cov for coverage reporting","Development dependencies managed in pyproject.toml"]}
|
||||
{"type":"entity","name":"Basic_Memory_Current_Architecture","entityType":"Architecture_Analysis","observations":["Clear separation between domain models (Pydantic) and storage models (SQLAlchemy)","File I/O completely separated into dedicated module","Strong 'filesystem as source of truth' pattern in services","Atomic file operations with proper error handling","Service layer coordinates between filesystem and database","Database acts as queryable index rather than primary storage","Clean error hierarchy with specific exception types","Rebuild operations available for recovery scenarios"]}
|
||||
{"type":"entity","name":"Basic_Memory_Evolution","entityType":"Analysis","observations":["Started with repository pattern following basic-foundation","Evolved to more sophisticated architecture with clear layers","Added Pydantic schemas for domain modeling","Separated file operations into dedicated module","Implemented robust error handling throughout","Maintained filesystem as source of truth principle","Added observation management with context tracking","Introduced rebuild capabilities for system recovery"]}
|
||||
{"type":"entity","name":"Basic_Memory_Service_Layer","entityType":"Implementation","observations":["EntityService handles entity lifecycle and coordinates storage","ObservationService manages observations within entities","Services ensure filesystem and database stay in sync","Clear error handling with ServiceError hierarchy","Strong typing throughout service interfaces","Implements filesystem as source of truth pattern","Handles UUID generation and timestamp management","Provides methods for system recovery and rebuild"]}
|
||||
{"type":"entity","name":"Basic_Memory_Schema_Design","entityType":"Implementation","observations":["Uses Pydantic for domain models and validation","Automatic ID generation with timestamp and UUID","Clear separation from SQLAlchemy storage models","Supports optional context tracking","Models match markdown file structure","Enables clean serialization/deserialization","Strong typing with proper validation rules","Independent from storage concerns"]}
|
||||
{"type":"entity","name":"Basic_Memory_Next_Tasks","entityType":"TaskList","observations":["✅ Implement SQLAlchemy models and repositories (Done)","✅ Add SQLAlchemy migrations (Done)","✅ Create service layer (Done)","✅ Implement file I/O module (Done)","✅ Set up domain models with Pydantic (Done)","✅ Initial test infrastructure (Done)","✅ Basic CRUD operations (Done)","⏳ Implement full test coverage for db.py","⏳ Add more sophisticated search functionality","⏳ Implement CLI interface","⏳ Add relationship management to services","⏳ Handle concurrent file operations safely","⏳ Add versioning for file changes","⏳ Implement proper backup strategy","⏳ Add type hints throughout codebase","⏳ Improve error messages and logging","⏳ Add documentation for core modules"]}
|
||||
{"type":"entity","name":"Basic_Memory_Meta_Experience","entityType":"Case_Study","observations":["Experienced our own context loss when reconstructing project knowledge","Had to rebuild task list and project context from filesystem and memory","Validated 'filesystem as source of truth' principle through reconstruction","Code and tests served as reliable historical record","Knowledge graph structure helped guide reconstruction process","Markdown files provided human-readable context","Atomic information design made piece-by-piece reconstruction possible","Ironic validation of the need for basic-memory's features","Experience demonstrates value of durable, human-readable knowledge storage","Shows importance of separating durable storage from ephemeral context"]}
|
||||
{"type":"entity","name":"Model_Context_Protocol","entityType":"protocol","observations":["Core part of the basic-memory architecture","Enables AI-human collaboration on projects","Provides tool-based interaction with knowledge graph","Developed by Anthropic for structured AI-system interaction","Used for maintaining consistent, rich context across conversations"]}
|
||||
{"type":"entity","name":"basic-memory_core_principles","entityType":"principles","observations":["Local First: All data stored locally in SQLite","Project Isolation: Separate databases per project","Human Readable: Everything exportable to plain text","AI Friendly: Structure optimized for LLM interaction","DIY Ethics: User owns and controls their data","Simple Core: Start simple, expand based on needs","Tool Integration: MCP-based interaction model"]}
|
||||
{"type":"entity","name":"basic-memory_business_model","entityType":"business_strategy","observations":["Core features free: Local SQLite, basic knowledge graph, search, markdown export, basic MCP tools","Professional features potential: Rich document export, advanced versioning, collaboration features, custom integrations, priority support","Focus on maintaining DIY/punk philosophy while enabling sustainability"]}
|
||||
{"type":"entity","name":"basic-memory_cli","entityType":"interface","observations":["Supports project management commands (create, switch, list)","Entity management (add entity, add observation, add relation)","Future support for export and batch operations","Follows consistent command structure","Planned integration with MCP tools"]}
|
||||
{"type":"entity","name":"basic-memory_export_format","entityType":"file_format","observations":["Uses markdown with frontmatter metadata","Includes entity name, type, creation timestamp","Observations as bullet points","Relations in structured format with links","References section at bottom","Designed for human readability and machine parsing","Example format documented in project specs"]}
|
||||
{"type":"entity","name":"relation_service","entityType":"code_module","observations":["Planned service for managing relations in both filesystem and database","Will follow filesystem-is-source-of-truth principle like other services","Needs to handle atomic file operations for relation updates","Must coordinate with EntityService for relationship integrity","Will handle bidirectional relationship tracking","Will support relation validation and type enforcement","Must implement rebuild functionality for index recovery","Will need careful error handling for file/db sync","Should support relation search and filtering","Must handle relation lifecycle (create/read/update/delete)"]}
|
||||
{"type":"entity","name":"service_layer_patterns","entityType":"implementation_patterns","observations":["Services handle both file and database operations","Filesystem is always source of truth","Database serves as queryable index","Services implement atomic file operations","Clear error hierarchy with specific exceptions","Use of dependency injection via constructor params","Async/await used throughout service layer","Services coordinate between storage layers","Repository pattern used for database access","Services maintain entity integrity across storage","Rich error types extend from ServiceError base","Rebuild operations available for recovery"]}
|
||||
{"type":"entity","name":"database_models","entityType":"implementation","observations":["Entity model with unique name and type","Observation model linked to entities","Relation model tracks connections between entities","Custom UTCDateTime type for timestamp handling","Use of SQLAlchemy relationships for navigation","Cascading deletes for dependent objects","String IDs used for compatibility","Rich relationship modeling with backpopulates","Proper indexing on foreign keys","Context tracking available on models","Models include created_at timestamps","Relationships handle bidirectional navigation"]}
|
||||
{"type":"entity","name":"repository_patterns","entityType":"implementation_patterns","observations":["Generic Repository[T] base class implementation","Type-safe operations with SQLAlchemy","Specialized repositories for each model type","Async operations throughout","Clear error handling patterns","Support for custom queries and filtering","Pagination support built-in","Transaction management via session","Proper type hints and generics usage","Entity-specific query methods in subclasses"]}
|
||||
{"type":"entity","name":"relation_service_design","entityType":"design","observations":["Must handle relation lifecycle in both files and DB","Needs to validate existence of both entities","Should support relation type enforcement","Must maintain bidirectional consistency","Should support relation querying and filtering","Needs proper error handling for graph consistency","Must integrate with entity file format","Should support bulk operations for efficiency","Must handle relation deletion and cascading","Should provide search by type and entities"]}
|
||||
{"type":"entity","name":"relation_service_implementation_plan","entityType":"plan","observations":["1. Define core relation operations (create, get, delete)","2. Implement file format handling for relations","3. Add database sync with RelationRepository","4. Implement validation and error handling","5. Add rebuild and recovery operations","6. Implement relation type enforcement","7. Add relation search and filtering","8. Implement bulk operations","9. Add comprehensive tests","10. Document API and error handling"]}
|
||||
{"type":"entity","name":"relation_service_challenges","entityType":"challenges","observations":["Maintaining consistency between file and database","Handling relation type validation efficiently","Managing bidirectional relationships in files","Ensuring atomic updates across entities","Handling deletion with proper cascading","Efficient querying of relation graphs","Recovery from partial file/db sync failures","Bulk operation atomicity","Clear error reporting for graph operations","Performance with large relation sets"]}
|
||||
{"type":"entity","name":"relation_file_format","entityType":"file_format","observations":["Relations stored in entity markdown files","Format: [target_id] relation_type | context","Relations section marked by ## Relations header","Outgoing relations only stored in source entity","Relations rebuild on entity load","Clean human-readable format","Context is optional with pipe separator","Links generate valid navigation references","Markdown-friendly formatting","Example: [Paul] authored | with Claude"]}
|
||||
{"type":"entity","name":"relation_service_error_handling","entityType":"implementation_patterns","observations":["RelationError extends ServiceError base","Specific errors for validation failures","Handles entity not found cases","Manages relation type validation errors","File operation errors properly wrapped","Database sync errors clearly reported","Transaction rollback on errors","Proper error propagation chain","Clear error messages for debugging","Recovery paths for common errors"]}
|
||||
{"type":"entity","name":"relation_service_testing","entityType":"testing","observations":["Test all relation lifecycle operations","Verify file and database consistency","Test relation type validation","Check error handling paths","Test bulk operations","Verify bidirectional consistency","Test recovery operations","Check cascade operations","Verify search and filtering","Test with large relation sets"]}
|
||||
{"type":"entity","name":"fileio_patterns","entityType":"implementation_patterns","observations":["Atomic file operations with temporary files","Clear error handling for IO operations","Consistent file naming and paths","Support for different file formats","Efficient file reading and writing","Proper file locking mechanisms","Recovery from partial writes","Consistent encoding handling","Directory management utilities","Path manipulation helpers","Currently implemented in fileio.py module","Uses pathlib for path operations","Handles file not found cases gracefully","Maintains data integrity during writes"]}
|
||||
{"type":"entity","name":"pytest_patterns","entityType":"implementation_patterns","observations":["Common fixtures should be in conftest.py for reuse","Use pytest_asyncio.fixture for async fixtures","Session fixtures need proper async cleanup","Temporary directories should be managed with context managers","Test categories: happy path, error path, recovery, edge cases","Services need project_path and repo injected","Use monkeypatch for mocking in async context","SQLite in-memory database ideal for testing","Explicit test verification: file content and database state"]}
|
||||
{"type":"entity","name":"relation_implementation_learnings","entityType":"implementation_learnings","observations":["Better to pass full Entity objects than IDs to services","Services should not re-read entities if they have them","File operations should be atomic and verified","Database serves as queryable index, not source of truth","Relations stored in source entity's markdown file","Clear separation between file ops and database sync","Entity objects should own their relations list","Context is optional but fully supported in implementation"]}
|
||||
{"type":"entity","name":"test_driven_insights","entityType":"learnings","observations":["Tests help reveal better API design (e.g., passing Entity objects)","Error cases drive proper exception hierarchy","File verification as important as database checks","Edge cases inform markdown format decisions","Recovery tests ensure system resilience","Tests document expected behavior clearly","Fixtures significantly reduce test complexity","Common patterns emerge through test writing"]}
|
||||
{"type":"entity","name":"meta_development_insights","entityType":"process","observations":["Break down large tasks into reviewable chunks","One file at a time prevents response truncation","Iterative development with tests leads to better design","Infrastructure code (fixtures) should be consolidated early","Test categories help ensure comprehensive coverage","Knowledge capture should happen during development","APIs tend to evolve toward simpler patterns","File operations require careful verification"]}
|
||||
{"type":"entity","name":"AI_Assistant_Learnings","entityType":"meta_insights","observations":["Output management: Breaking responses into single files prevents truncation and allows better review","Knowledge graph helps maintain context: I can reference previous decisions and patterns accurately","Memory rebuilding experience validated the need for durable storage","Test-driven development provides clear steps and verification","Explicit relation tracking in knowledge graph helps me understand project context","Rich context from multiple sources (code, docs, tests) enables better assistance","File-at-a-time approach allows deeper analysis of each component","Keeping entity names consistent helps with referencing and relationships"]}
|
||||
{"type":"entity","name":"Effective_Response_Patterns","entityType":"meta_patterns","observations":["When showing code changes, break into discrete files","Review existing code before suggesting changes","Reference knowledge graph for context and patterns","Explicitly connect new code to existing patterns","Validate suggestions against test cases","Keep track of file changes for atomic commits","Check both implementation and test files for consistency","Maintain clear separation of concerns in responses"]}
|
||||
{"type":"entity","name":"AI_Context_Management","entityType":"meta_practice","observations":["Knowledge graph provides reliable persistent memory","Project documentation gives high-level context","Code review shows implementation patterns","Tests demonstrate expected behavior","Important to actively track what has been modified","Entity relationships help understand dependencies","Regular knowledge capture during development","Using consistent entity references across conversations"]}
|
||||
{"type":"entity","name":"AI_Tool_Usage_Patterns","entityType":"meta_practice","observations":["read_file before suggesting changes","write_file one file at a time","list_directory to understand project structure","search_nodes to find relevant context","create_entities to capture new learnings","create_relations to connect concepts","Using knowledge graph to track decisions","Validating changes through test execution"]}
|
||||
{"type":"entity","name":"relation_service_learnings","entityType":"implementation_learnings","observations":["Entity-based API cleaner than ID-based for service layer","Model_dump method can handle storage serialization","File format needs explicit section markers (## Relations)","Whitespace handling important for long content comparisons","Test fixtures allow focused test cases","SQLAlchemy selects better than raw SQL for type safety","Atomic file operations maintained for relations"]}
|
||||
{"type":"entity","name":"test_driven_insights_relations","entityType":"learnings","observations":["Tests revealed need for whitespace normalization","Edge cases drove file format decisions","SQLAlchemy model access safer than raw queries","Fixtures reduced test setup complexity","File verification as important as database checks","Testing both memory model and storage format","Test categories ensure comprehensive coverage"]}
|
||||
{"type":"entity","name":"relation_service_patterns","entityType":"patterns","observations":["Use Entity objects in API","Serialize to IDs for storage","Maintain file as source of truth","Keep file format human-readable","Handle circular references in serialization","Use repository pattern for database","Clear error hierarchies"]}
|
||||
{"type":"entity","name":"packaging_learnings","entityType":"technical_learnings","observations":["When using pytest-mock, traditional pip install works more reliably than uv sync","Package discovery behavior can differ between uv and pip","Clean venv with pip install is a reliable fallback for dependency issues","Package installation location might differ between uv and pip","Dependencies in pyproject.toml dev section work reliably with pip install -e .[dev]"]}
|
||||
{"type":"entity","name":"Recent_Implementation_Progress","entityType":"progress_update","observations":["Successfully split services.py into modular structure under services/","Created __init__.py, entity_service.py, observation_service.py, relation_service.py","Fixed pytest-mock installation issues by using pip install -e .[dev] instead of uv sync","Improved test structure with minimal mocking - only used for error testing","Implemented relation service with Entity-based API","Achieved good test coverage across services","File operations are only mocked when testing error conditions","Services follow filesystem-as-source-of-truth pattern"]}
|
||||
{"type":"entity","name":"Next_Steps","entityType":"project_tasks","observations":["Consider adding more relation service tests","Potentially expand relations features","Look for opportunities to improve test coverage","Consider documenting package management preferences (pip vs uv)","Consider adding integration tests for services","Review and possibly expand error handling cases"]}
|
||||
{"type":"entity","name":"Development_Practices","entityType":"process","observations":["Favor real operations over mocks in tests","Only mock for error condition testing","Use pip install -e .[dev] for reliable dev dependency installation","Maintain modular service structure","Keep filesystem as source of truth","Use Entity objects in service APIs instead of IDs","Validate both file and database state in tests"]}
|
||||
{"type":"entity","name":"MCP_Resources","entityType":"Concept","observations":["Stateful objects in Model Context Protocol","Enable persistent access to capabilities"]}
|
||||
{"type":"entity","name":"MCP_Server_Implementation","entityType":"Technical_Design","observations":["Inherits from mcp.server.Server base class","Tools are implemented as async methods","Each tool method maps directly to a function available to the AI","Tools can request user input via Prompts","Simple function call interface rather than explicit resource management","State management handled by server instance","Returns serialized data using model_dump() for consistency"]}
|
||||
{"type":"entity","name":"MCP_Tools","entityType":"Protocol_Feature","observations":["Defined as async methods on server class","Return values must match tool definition schema","Can maintain state between invocations via server instance","Tools can prompt for user input when needed","No need for explicit Resource objects in implementation"]}
|
||||
{"type":"entity","name":"Basic_Memory_MCP","entityType":"Implementation","observations":["Uses MemoryService for core operations","Implements project selection via prompts","Maintains project context across tool invocations","Maps directly to memory graph operations","Handles serialization of Pydantic models"]}
|
||||
{"type":"entity","name":"Basic_Memory_Testing","entityType":"Testing_Design","observations":["Needs pytest for async testing","Should isolate filesystem operations for tests","Needs to handle MCP server lifecycle in tests","Should test both service layer and MCP interface","Will need mocks for project paths and file operations"]}
|
||||
{"type":"entity","name":"Memory_Service_Tests","entityType":"Test_Suite","observations":["Should test entity creation with observations","Should test relation creation between entities","Should verify proper ID generation and model validation","Should test deletion cascading","Should test search functionality","Must verify proper serialization of entities and relations"]}
|
||||
{"type":"entity","name":"MCP_Server_Tests","entityType":"Test_Suite","observations":["Should test project initialization workflow","Should test prompt handling","Should verify tool input/output formats","Should test error cases and validation","Must verify proper serialization in tool responses"]}
|
||||
{"type":"entity","name":"Memory_Service_Refactoring","entityType":"Technical_Task","observations":["MemoryService uses create() but EntityService might expect create_entity()","MemoryService assumes get_by_name() but EntityService might use different method","Need to verify deletion method signatures","Need to check if search interface matches","Should verify observation handling matches ObservationService interface","RelationService methods need verification","EntityService.create_entity takes name, type, and optional observations directly, not an Entity object","EntityService requires project_path and entity_repo in constructor","ObservationService.add_observation takes Entity object and content string, not raw data","RelationService.create_relation takes Entity objects directly, not dict data","All services follow filesystem-as-source-of-truth pattern with DB indexing","All services handle database synchronization internally","Services expect Path objects for filesystem operations"]}
|
||||
{"type":"entity","name":"Service_Interface_Audit","entityType":"Technical_Task","observations":["Need to review all existing service interfaces","Document current method signatures","Map discrepancies between MemoryService assumptions and actual interfaces","Check return types and error handling patterns","Review transaction/atomicity requirements","Method signatures need alignment: create vs create_entity etc","Need to handle DB repositories in service constructors","File operations should use project_path consistently","Need to maintain filesystem-as-source-of-truth pattern","Should handle database synchronization at service level","Error handling should align with existing patterns","Consider making MemoryService handle DB indexing consistently"]}
|
||||
{"type":"entity","name":"Memory_Service_Patterns","entityType":"Technical_Pattern","observations":["Uses inner async functions to encapsulate operation logic","Leverages list comprehensions with async functions for parallel operations","Each operation follows a consistent pattern: validate, update DB, write file","Inner functions make the code more readable and maintainable","Operations can run in parallel when using list comprehensions with async functions"]}
|
||||
{"type":"entity","name":"Pydantic_Create_Pattern","entityType":"Technical_Pattern","observations":["Separate Create models match the exact shape of incoming data","Provides clear contract for MCP tool inputs","Handles validation of raw input data","Converts cleanly to domain models via from_create methods","Maintains separation between external API format and internal models","Similar to FastAPI request model pattern","Allows camelCase in API while using snake_case internally"]}
|
||||
{"type":"entity","name":"Basic_Memory_Business","entityType":"Business_Model","observations":["Core system is open source and free","Local-first, giving users data control","Professional features could be licensed","Enterprise support and customization services","Potential for MCP tool marketplace"]}
|
||||
{"type":"entity","name":"MCP_Marketplace","entityType":"Business_Concept","observations":["Could host verified MCP tools for different use cases","Tools rated by performance and reliability","Marketplace takes percentage of tool usage fees","Enterprise tool verification and security scanning","Custom tool development services","Integration support for existing tools"]}
|
||||
{"type":"entity","name":"Persistence_Of_Vision","entityType":"Concept","observations":["Mental model for continuous AI-human interaction","Like cinema: 24fps creates illusion of smooth motion","Basic-memory provides 'frames' of structured knowledge","Current state: Better than flipbook, not yet digital cinema","Goal: Achieve smoother cognitive continuity between interactions","Proposed by Drew as metaphor for AI conversation continuity"]}
|
||||
{"type":"entity","name":"Conversation_Continuity_Pattern","entityType":"Usage_Pattern","observations":["Use basic-memory entity/relation schema for conversations","Each chat becomes an entity with observations for key points","Relations link to discussed concepts and other chats","Uses zettelkasten format IDs for natural ordering","Can be used as template/recipe for others","Future possibility: Git SHA integration for versioning"]}
|
||||
{"type":"entity","name":"Usage_Recipes","entityType":"Feature_Concept","observations":["Predefined patterns users can follow or adapt","Could include conversation tracking recipe","Templates for different knowledge management styles","Shows practical applications of the generic schema","Helps users get started with the system"]}
|
||||
{"type":"entity","name":"Chat_References","entityType":"Technical_Feature","observations":["Uses ref:* syntax to reference previous conversations","Combines reference semantics with pointer symbolism","Format: ref:*{zettelkasten-id}","Allows explicit context loading between chats","Inspired by C++ references and pointers","Provides memory-model-like access to conversation context","Uses ref:// URI format following MCP Resource pattern","Could support multiple reference schemes (chat/entity/concept)","Makes reference semantics explicit and unambiguous","Aligns with standard URI formatting"]}
|
||||
{"type":"entity","name":"Chat_Reference_Protocol","entityType":"Technical_Specification","observations":["Uses URI format: ref://basic-memory/chat/[id]","Follows MCP Resource pattern: [protocol]://[host]/[path]","Enables explicit context loading between chats","Can support multiple resource types (chat/entity/concept)","Provides standardized way to reference previous conversations","Example: ref://basic-memory/chat/20240307-drew-ab12ef34"]}
|
||||
{"type":"entity","name":"20240307-chat-reference-protocol","entityType":"conversation","observations":["Developed ref:// URI format for chat references","Added Chat Reference Protocol to prompt instructions","Discussed implementation of chat continuation","Created complete prompt instructions document","Reference format follows MCP Resource pattern","Reviewed and confirmed complete prompt instructions","Ready to test ref://basic-memory/chat/20240307-chat-reference-protocol in new chat"]}
|
||||
{"type":"entity","name":"20240307-chat-reference-protocol-test","entityType":"conversation","observations":["First implementation test of chat reference protocol","Testing continuation from 20240307-chat-reference-protocol","Focused on practical implementation of ref:// URI format"]}
|
||||
{"type":"entity","name":"Write_File_Tool_Usage","entityType":"Tool_Usage_Pattern","observations":["Never use placeholders like '# Rest of...' when writing files - must include complete file content","File content must be complete and valid - partial updates will truncate the file","If showing partial changes, should inform human and let them handle the file write","write_file tool replaces entire file contents - cannot do partial updates","Code files especially must be complete and valid to avoid breaking functionality","Always read_file before write_file to understand current state","Using write_file without reading first risks reverting recent changes","Pattern should be: read current state, make modifications, then write if needed","Especially important in collaborative development where files may have been updated"]}
|
||||
{"type":"entity","name":"Run_Tests_Tool_Request","entityType":"Feature_Request","observations":["Need to add a tool enabling Claude to run tests locally","Would help with direct validation of code changes","Current workaround: Claude has to ask human to run tests","Should support running specific test functions (e.g. pytest tests/test_memory_service.py::test_create_relations)","Would improve iterative development workflow between human and AI"]}
|
||||
{"type":"entity","name":"SQLAlchemy_Async_Loading_Pattern","entityType":"Technical_Pattern","observations":["Use selectinload() instead of lazy loading when accessing SQLAlchemy relationships in async code","Lazy loading doesn't work with async due to greenlet context requirements","selectinload performs a single efficient query with an IN clause","Pattern used in basic-memory's EntityRepository for loading relations","Documented in find_by_id method with thorough explanation","Alternative approaches: joinedload (single JOIN query) or subqueryload (subquery approach)","Benefits: prevents 'MissingGreenlet' errors, reduces N+1 query problems","Key insight: load all needed relationships upfront in async code","Example use: selectinload(Entity.outgoing_relations)"]}
|
||||
{"type":"entity","name":"20241207-sqlalchemy-async-pattern","entityType":"conversation","observations":["Fixed SQLAlchemy async relationship loading issues","Implemented selectinload pattern in EntityRepository","Updated find_by_id to eager load relations","Added documentation about the pattern","Created knowledge graph entry about SQLAlchemy async loading","Fixed failing tests by properly loading relations in memory_service","Discussed SQLAlchemy relationship loading best practices"]}
|
||||
{"type":"entity","name":"20241207-memory-service-relations","entityType":"conversation","observations":["Fixed SQLAlchemy async loading with selectinload pattern","Updated find_by_id in EntityRepository to eager load relations","Discovered create_relations works but returns empty list","Verified relations are being stored correctly in memory.json","Next step: Work on MemoryService.add_observations implementation","Improved understanding of MCP memory storage format through debugging"]}
|
||||
{"type":"entity","name":"add_observations_implementation_plan","entityType":"technical_plan","observations":["Follow pattern from create_entity and create_relation methods","File operations first (read & write) - filesystem is source of truth","Database updates in parallel","Simplify current implementation","Current flow is:"," - First read entities and create observations"," - Write files in parallel"," - Update DB indexes sequentially","Key tests needed:"," - Adding observations to multiple entities"," - Verifying filesystem state first"," - Verifying database state"," - Error cases for missing entities"," - Error cases for file operations"]}
|
||||
{"type":"entity","name":"MCP_Reference_Integration","entityType":"feature_idea","observations":["Can be implemented as a Model Context Protocol integration similar to the fetch tool","Would provide structured way to pass chat references to Claude","Could handle ref:// URL format systematically","Integration would fetch context from referenced chats and inject into conversation","Observed from Claude Desktop UI showing MCP integration pattern with fetch tool","Would be more robust than passing references in chat text"]}
|
||||
{"type":"entity","name":"Project_Priorities","entityType":"roadmap","observations":["P1: Dogfooding basic-memory system instead of JSON memory store","Future: Implement MCP-based reference system"]}
|
||||
{"type":"entity","name":"great_observation_loading_saga_20241207","entityType":"debugging_session","observations":["Occurred on December 7, 2024 while debugging basic-memory SQLAlchemy relationship loading","Issue: selectinload() wasn't properly loading relationships in async SQLAlchemy context","Tried multiple solutions: explicit joins, manual loading, various SQLAlchemy loading strategies","Final solution: Using session.refresh() with explicit relationship names","Memorable quote: 'The Great Observation Loading Saga'","Key learning: Sometimes the obvious SQLAlchemy patterns need adaptation for async contexts","Solution preserved in basic-memory repository in EntityRepository.find_by_id()"]}
|
||||
{"type":"entity","name":"basic_memory_implementation_20241208","entityType":"technical_milestone","observations":["Fixed async SQLAlchemy relationship loading issues by using explicit refresh with relationship names","Established pattern of relationship handling belonging in MemoryService not EntityService","Fixed ID generation flow through Pydantic schemas to DB layer","Standardized error handling using EntityNotFoundError","All 32 tests passing with 70% coverage","Core services (Entity, Observation, Relation) working properly","Ready for MCP server implementation","Notable debugging session: The Great Observation Loading Saga - resolved lazy loading issues","Established clear separation between MemoryService orchestration and individual service responsibilities"]}
|
||||
{"type":"entity","name":"MCP_Dependency_Risk","entityType":"technical_lesson","observations":["Experienced disruption when MCP npm package disappeared - 'leftpad moment'","Need to ensure basic-memory tools are resilient to external dependency issues","Local implementation of MCP server provides better stability than npm packages","Important to maintain control of critical infrastructure components","Validates DIY/local-first philosophy of basic-memory project","Package manager fragility revealed by simple 'npx @modelcontextprotocol/server-memory' failure"]}
|
||||
{"type":"entity","name":"basic_memory_project_20241208","entityType":"technical_milestone","observations":["Core MCP server implementation completed with tools: create_entities, search_nodes, open_nodes, add_observations, create_relations, delete_entities, delete_observations","ProjectConfig and dependency injection pattern established","Test framework in place with in-memory DB support","Support for both camelCase (MCP) and snake_case (internal) formats","Filesystem remains source of truth with SQLite as index","Two-way sync pattern identified between Claude MCP tools and direct markdown file editing","Ready for Claude Desktop integration testing phase","Next steps identified: passing tests, markdown format definition, file change tracking, real-world testing","Implementation prioritizes local-first principles with filesystem as source of truth"]}
|
||||
{"type":"entity","name":"basic_memory_mcp_architecture","entityType":"technical_design","observations":["MemoryServer class extends MCP Server with custom handler registration","Uses ProjectConfig for clean dependency injection and configuration","Memory service can be injected for testing","Handlers exposed as instance attributes for testing","Tool schemas leverage existing Pydantic models"]}
|
||||
{"type":"entity","name":"basic_memory_sync_considerations","entityType":"design_insight","observations":["Need to handle sync between direct markdown file edits and DB index","Watch for file system changes as potential future enhancement","Consider index rebuild patterns on startup","Keep human-friendly markdown format for direct editing"]}
|
||||
{"type":"entity","name":"mcp_server_learnings","entityType":"developer_insight","observations":["MCP protocol is new and documentation is still evolving","Test patterns are not well established yet in example implementations","Supporting both camelCase and snake_case helps with protocol/internal compatibility","Server.handle_* naming convention is important for handler registration"]}
|
||||
{"type":"entity","name":"20241208-mcp-tool-refactoring","entityType":"conversation","observations":["Decision to return structured data via EmbeddedResource instead of TextContent string parsing","Plan to create Pydantic result models (CreateEntitiesResult, SearchNodesResult etc)","Will use application/vnd.basic-memory+json as MIME type for our structured data","Currently debugging test issues with add_observations tool","Entity ID vs name resolution needed in add_observations","Goal is to make tools more joyful to use by eliminating string parsing","MCP spec supports EmbeddedResource for structured data returns"]}
|
||||
{"type":"entity","name":"Basic Memory MCP Server Implementation","entityType":"technical_notes","observations":["Server implements Model Context Protocol using proper structured data responses","Uses EmbeddedResource with custom MIME type 'application/vnd.basic-memory+json'","Clean separation between input validation and handlers via Pydantic models","All tool operations return structured data through create_response helper","Type safety with Literal types for tool names and proper typing for handlers","Handler registry pattern with TOOL_HANDLERS dictionary","Consistent error handling pattern using MCP error codes","Uses Pydantic ConfigDict for proper ORM integration","Tool schemas organized into Input and Response types","Input validation with Annotated types for extra constraints","Response models consistently use from_attributes=True for ORM data","Entity ID generation moved to model validator on EntityBase","Follows principle of making common operations easy and safe"]}
|
||||
{"type":"relation","from":"Paul","to":"Basic_Machines","relationType":"created_and_maintains"}
|
||||
{"type":"relation","from":"basic-memory","to":"Basic_Machines","relationType":"is_component_of"}
|
||||
{"type":"relation","from":"Paul","to":"basic-memory","relationType":"develops"}
|
||||
{"type":"relation","from":"fileio_module","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"entity_service","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"observation_service","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"fileio_module","to":"basic-memory","relationType":"is_component_of"}
|
||||
{"type":"relation","from":"entity_service","to":"basic-memory","relationType":"is_component_of"}
|
||||
{"type":"relation","from":"observation_service","to":"basic-memory","relationType":"is_component_of"}
|
||||
{"type":"relation","from":"entity_service","to":"fileio_module","relationType":"uses"}
|
||||
{"type":"relation","from":"observation_service","to":"fileio_module","relationType":"uses"}
|
||||
{"type":"relation","from":"observation_management","to":"observation_service","relationType":"influences_design_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"testing_infrastructure","relationType":"supports"}
|
||||
{"type":"relation","to":"testing_infrastructure","from":"test_categories","relationType":"implements"}
|
||||
{"type":"relation","to":"basic-memory","from":"completed_work","relationType":"tracks_progress_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"future_work","relationType":"guides_development_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"design_decisions","relationType":"shapes_architecture_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"concurrency_considerations","relationType":"influences_design_of"}
|
||||
{"type":"relation","to":"future_work","from":"concurrency_considerations","relationType":"informs"}
|
||||
{"type":"relation","to":"observation_management","from":"design_decisions","relationType":"guides"}
|
||||
{"type":"relation","to":"testing_infrastructure","from":"completed_work","relationType":"established"}
|
||||
{"type":"relation","to":"design_decisions","from":"fileio_module","relationType":"implements"}
|
||||
{"type":"relation","from":"observation_update_approaches","to":"observation_management","relationType":"analyzes"}
|
||||
{"type":"relation","from":"bulk_update_approach","to":"observation_update_approaches","relationType":"is_option_of"}
|
||||
{"type":"relation","from":"tracked_observations_approach","to":"observation_update_approaches","relationType":"is_option_of"}
|
||||
{"type":"relation","from":"diff_based_approach","to":"observation_update_approaches","relationType":"is_option_of"}
|
||||
{"type":"relation","from":"position_based_approach","to":"observation_update_approaches","relationType":"is_option_of"}
|
||||
{"type":"relation","from":"tasks_and_progress","to":"basic-memory","relationType":"tracks_status_of"}
|
||||
{"type":"relation","from":"design_decisions","to":"observation_update_approaches","relationType":"influences"}
|
||||
{"type":"relation","from":"observation_update_approaches","to":"future_work","relationType":"informs"}
|
||||
{"type":"relation","to":"basic-memory_implementation_patterns","from":"error_handling_patterns","relationType":"is_part_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"data_models","relationType":"implements"}
|
||||
{"type":"relation","to":"basic-memory","from":"markdown_format","relationType":"defines"}
|
||||
{"type":"relation","to":"basic-memory","from":"test_driven_development","relationType":"guides_development_of"}
|
||||
{"type":"relation","to":"basic-memory","from":"architecture_evolution","relationType":"describes_development_of"}
|
||||
{"type":"relation","to":"basic-memory_implementation_patterns","from":"validation_patterns","relationType":"is_part_of"}
|
||||
{"type":"relation","to":"design_decisions","from":"architecture_evolution","relationType":"informs"}
|
||||
{"type":"relation","to":"fileio_module","from":"markdown_format","relationType":"implements"}
|
||||
{"type":"relation","to":"error_handling_patterns","from":"test_driven_development","relationType":"influenced"}
|
||||
{"type":"relation","to":"data_models","from":"validation_patterns","relationType":"implements"}
|
||||
{"type":"relation","to":"markdown_format","from":"markdown_examples","relationType":"documents"}
|
||||
{"type":"relation","to":"markdown_format","from":"markdown_parsing_rules","relationType":"defines"}
|
||||
{"type":"relation","to":"data_models","from":"schema_definitions","relationType":"documents"}
|
||||
{"type":"relation","to":"test_driven_development","from":"test_evolution","relationType":"describes"}
|
||||
{"type":"relation","to":"architecture_evolution","from":"implementation_challenges","relationType":"influenced"}
|
||||
{"type":"relation","to":"test_evolution","from":"implementation_challenges","relationType":"shaped"}
|
||||
{"type":"relation","to":"future_work","from":"implementation_challenges","relationType":"informs"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"Basic_Machines","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"Basic_Factory","relationType":"is_part_of"}
|
||||
{"type":"relation","from":"Component_Translation_Process","to":"Basic_Factory_Components","relationType":"enables"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Basic_Machines","relationType":"guides"}
|
||||
{"type":"relation","from":"Paul","to":"Basic_Factory","relationType":"develops"}
|
||||
{"type":"relation","from":"Paul","to":"Basic_Machines_Philosophy","relationType":"created"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"Basic_Machines_Philosophy","relationType":"articulates"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"Basic_Factory","relationType":"guides_development_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"Paul","relationType":"written_by"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"Component_Translation_Process","relationType":"documents"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"Basic_Machines","relationType":"shapes_development_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Roadmap","to":"Basic_Machines","relationType":"guides_development_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Website","to":"Basic_Machines_Roadmap","relationType":"implements_phase_of"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"Basic_Machines_Website","relationType":"enables"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Basic_Machines_Website","relationType":"informs"}
|
||||
{"type":"relation","from":"Paul","to":"DIY_Ethics","relationType":"embodies"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"DIY_Ethics","relationType":"incorporates"}
|
||||
{"type":"relation","from":"Basic_Machines","to":"DIY_Ethics","relationType":"exemplifies"}
|
||||
{"type":"relation","from":"Component_Translation_Process","to":"Basic_Machines_Philosophy","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"DIY_Ethics","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"Basic_Machines_Philosophy","relationType":"aligns_with"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"Component_Translation_Process","relationType":"guides"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"Basic_Machines","relationType":"defines_vision_for"}
|
||||
{"type":"relation","from":"Basic_Machines_Website","to":"Basic_Machines_Manifesto","relationType":"implements_vision_of"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"AI_Human_Collaboration_Model","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"Paul","to":"AI_Human_Collaboration_Model","relationType":"developed_with_Claude"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"Component_Translation_Process","relationType":"created_through"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Basic_Factory","relationType":"guides"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"MCP_Tools","relationType":"integrates"}
|
||||
{"type":"relation","from":"Basic_Machines_Website","to":"Basic_Factory_Components","relationType":"will_use"}
|
||||
{"type":"relation","from":"Basic_Machines_Roadmap","to":"Basic_Machines_Philosophy","relationType":"aligns_with"}
|
||||
{"type":"relation","from":"Component_Translation_Process","to":"MCP_Tools","relationType":"leverages"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"basic-memory","relationType":"will_document_process_in"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"basic-memory","relationType":"will_be_implemented_in"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Basic_Machines_Roadmap","relationType":"informs_priorities_of"}
|
||||
{"type":"relation","from":"basic-memory","to":"Basic_Machines_Philosophy","relationType":"embodies"}
|
||||
{"type":"relation","from":"Paul","to":"Basic_Machines_Manifesto","relationType":"authored_with_Claude"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"Component_Translation_Process","relationType":"validated"}
|
||||
{"type":"relation","from":"AI_Human_Collaboration_Model","to":"MCP_Tools","relationType":"utilizes"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"Basic_Machines_Roadmap","relationType":"supports"}
|
||||
{"type":"relation","from":"Basic_Machines_Website","to":"Basic_Factory","relationType":"will_demonstrate"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"basic-memory-webui","relationType":"enables_development_of"}
|
||||
{"type":"relation","from":"basic-memory","to":"AI_Human_Development_Methodology","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"AI_Human_Development_Methodology","relationType":"guides"}
|
||||
{"type":"relation","from":"Basic_Factory_Components","to":"basic-memory-webui","relationType":"provides_ui_for"}
|
||||
{"type":"relation","from":"Component_Translation_Process","to":"AI_Human_Development_Methodology","relationType":"exemplifies"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"Basic Components","relationType":"enabled_creation_of"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"Tool Integration Discovery","relationType":"led_to"}
|
||||
{"type":"relation","from":"MCP_Integration_Progress","to":"AI_Human_Development_Methodology","relationType":"validates"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"MCP_Integration_Progress","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"Basic_Factory","to":"AI_Human_Development_Methodology","relationType":"proves_effectiveness_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Basic Components","relationType":"inspires_architecture_of"}
|
||||
{"type":"relation","from":"DIY_Ethics","to":"basic-memory","relationType":"shapes_design_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Philosophy","to":"Tool Integration Discovery","relationType":"guides_analysis_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"AI_Human_Development_Methodology","relationType":"documents_approach_of"}
|
||||
{"type":"relation","from":"Basic_Machines_Manifesto","to":"Basic_Factory_Components","relationType":"explains_principles_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Project_Structure","to":"basic-memory","relationType":"organizes"}
|
||||
{"type":"relation","from":"Basic_Memory_Database_Schema","to":"basic-memory","relationType":"defines_storage_for"}
|
||||
{"type":"relation","from":"Basic_Memory_Markdown_Example","to":"Basic_Memory_File_Format","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"Basic_Memory_Project_Isolation_Decision","to":"Basic_Memory_Future_Enhancement_Weighted_Relations","relationType":"similar_to"}
|
||||
{"type":"relation","to":"DIY_Ethics","from":"Basic_Memory_Project_Isolation_Decision","relationType":"follows"}
|
||||
{"type":"relation","from":"Basic_Memory_Implementation_Plan","to":"basic-memory","relationType":"guides"}
|
||||
{"type":"relation","from":"Basic_Memory_Implementation_Plan","to":"DIY_Ethics","relationType":"follows"}
|
||||
{"type":"relation","from":"Basic_Memory_Implementation_Plan","to":"Basic_Memory_Database_Schema","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Memory_Implementation_Status","to":"Basic_Memory_Implementation_Plan","relationType":"updates"}
|
||||
{"type":"relation","from":"Basic_Memory_Observation_Management_Design","to":"Basic_Memory_Technical_Design","relationType":"extends"}
|
||||
{"type":"relation","from":"Basic_Memory_Architectural_Decisions","to":"DIY_Ethics","relationType":"guided_by"}
|
||||
{"type":"relation","from":"Basic_Memory_Architectural_Decisions","to":"basic-memory","relationType":"structures"}
|
||||
{"type":"relation","from":"Basic_Memory_Implementation_Status","to":"basic-memory","relationType":"describes_state_of"}
|
||||
{"type":"relation","to":"Basic_Memory_Implementation_Status","from":"Basic_Memory_Implementation_Analysis","relationType":"analyzes"}
|
||||
{"type":"relation","to":"basic-memory","from":"Basic_Memory_Current_Challenges","relationType":"identifies_issues_in"}
|
||||
{"type":"relation","to":"DIY_Ethics","from":"Basic_Memory_Implementation_Analysis","relationType":"confirms_alignment_with"}
|
||||
{"type":"relation","to":"Basic_Memory_Observation_Management_Design","from":"Basic_Memory_Observation_Hash_Tracking","relationType":"solves"}
|
||||
{"type":"relation","to":"DIY_Ethics","from":"Basic_Memory_Observation_Hash_Tracking","relationType":"aligns_with"}
|
||||
{"type":"relation","to":"Basic_Memory_File_Format","from":"Basic_Memory_Observation_Hash_Tracking","relationType":"preserves"}
|
||||
{"type":"relation","to":"Basic_Memory_Technical_Design","from":"Basic_Memory_Observation_Hash_Tracking","relationType":"enhances"}
|
||||
{"type":"relation","from":"Basic_Memory_Repository_Implementation","to":"basic-memory","relationType":"implements_part_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Repository_Implementation","to":"Basic_Memory_Database_Schema","relationType":"follows"}
|
||||
{"type":"relation","from":"Basic_Memory_Repository_Implementation","to":"DIY_Ethics","relationType":"aligns_with"}
|
||||
{"type":"relation","from":"Basic_Memory_Repository_Implementation","to":"testing_infrastructure","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"Basic_Memory_Repository_Implementation","to":"Basic Foundation","relationType":"inspired_by"}
|
||||
{"type":"relation","from":"Basic_Memory_Dependencies","to":"basic-memory","relationType":"supports"}
|
||||
{"type":"relation","from":"Basic_Memory_Dependencies","to":"Basic_Memory_Repository_Implementation","relationType":"enables"}
|
||||
{"type":"relation","from":"Basic_Memory_Dependencies","to":"testing_infrastructure","relationType":"enables"}
|
||||
{"type":"relation","from":"Basic_Memory_Current_Architecture","to":"basic-memory","relationType":"describes_state_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Evolution","to":"Basic_Memory_Current_Architecture","relationType":"explains_development_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Service_Layer","to":"Basic_Memory_Current_Architecture","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Memory_Schema_Design","to":"Basic_Memory_Current_Architecture","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Memory_Evolution","to":"Basic_Memory_Implementation_Plan","relationType":"reflects_on"}
|
||||
{"type":"relation","from":"Basic_Memory_Evolution","to":"DIY_Ethics","relationType":"demonstrates_alignment_with"}
|
||||
{"type":"relation","from":"Basic_Memory_Current_Architecture","to":"DIY_Ethics","relationType":"embodies"}
|
||||
{"type":"relation","from":"Basic_Memory_Service_Layer","to":"fileio_module","relationType":"uses"}
|
||||
{"type":"relation","from":"Basic_Memory_Schema_Design","to":"markdown_format","relationType":"implements"}
|
||||
{"type":"relation","to":"basic-memory","from":"Basic_Memory_Next_Tasks","relationType":"guides_development_of"}
|
||||
{"type":"relation","to":"DIY_Ethics","from":"Basic_Memory_Next_Tasks","relationType":"aligns_with"}
|
||||
{"type":"relation","to":"Basic_Memory_Current_Architecture","from":"Basic_Memory_Next_Tasks","relationType":"extends"}
|
||||
{"type":"relation","from":"Basic_Memory_Meta_Experience","to":"basic-memory","relationType":"validates_design_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Meta_Experience","to":"DIY_Ethics","relationType":"demonstrates_principles_of"}
|
||||
{"type":"relation","from":"Basic_Memory_Meta_Experience","to":"design_decisions","relationType":"reinforces"}
|
||||
{"type":"relation","from":"Basic_Memory_Meta_Experience","to":"Basic_Memory_Current_Architecture","relationType":"validates"}
|
||||
{"type":"relation","from":"Model_Context_Protocol","to":"basic-memory","relationType":"enables"}
|
||||
{"type":"relation","from":"basic-memory_core_principles","to":"basic-memory","relationType":"guides"}
|
||||
{"type":"relation","from":"basic-memory_core_principles","to":"DIY_Ethics","relationType":"aligns_with"}
|
||||
{"type":"relation","from":"basic-memory_business_model","to":"basic-memory","relationType":"defines_sustainability_for"}
|
||||
{"type":"relation","from":"basic-memory_business_model","to":"DIY_Ethics","relationType":"maintains_alignment_with"}
|
||||
{"type":"relation","from":"basic-memory_cli","to":"basic-memory","relationType":"provides_interface_for"}
|
||||
{"type":"relation","from":"basic-memory_cli","to":"Model_Context_Protocol","relationType":"integrates_with"}
|
||||
{"type":"relation","from":"basic-memory_export_format","to":"basic-memory","relationType":"standardizes_output_of"}
|
||||
{"type":"relation","from":"basic-memory_export_format","to":"markdown_format","relationType":"extends"}
|
||||
{"type":"relation","from":"basic-memory_core_principles","to":"Basic_Machines_Philosophy","relationType":"implements"}
|
||||
{"type":"relation","from":"Model_Context_Protocol","to":"AI_Human_Collaboration_Model","relationType":"enables"}
|
||||
{"type":"relation","from":"relation_service","to":"basic-memory","relationType":"will_be_component_of"}
|
||||
{"type":"relation","from":"relation_service","to":"service_layer_patterns","relationType":"follows"}
|
||||
{"type":"relation","from":"relation_service","to":"fileio_patterns","relationType":"uses"}
|
||||
{"type":"relation","from":"relation_service","to":"database_models","relationType":"uses"}
|
||||
{"type":"relation","from":"relation_service","to":"repository_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"relation_service_design","to":"relation_service","relationType":"guides_implementation_of"}
|
||||
{"type":"relation","from":"relation_service_implementation_plan","to":"relation_service","relationType":"defines_implementation_of"}
|
||||
{"type":"relation","from":"relation_service_challenges","to":"relation_service_design","relationType":"informs"}
|
||||
{"type":"relation","from":"relation_file_format","to":"markdown_format","relationType":"extends"}
|
||||
{"type":"relation","from":"relation_service_error_handling","to":"service_layer_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"relation_service_testing","to":"testing_infrastructure","relationType":"extends"}
|
||||
{"type":"relation","from":"fileio_patterns","to":"service_layer_patterns","relationType":"enables"}
|
||||
{"type":"relation","from":"database_models","to":"repository_patterns","relationType":"enables"}
|
||||
{"type":"relation","from":"relation_service","to":"entity_service","relationType":"coordinates_with"}
|
||||
{"type":"relation","from":"relation_file_format","to":"relation_service","relationType":"defines_storage_for"}
|
||||
{"type":"relation","from":"relation_service_error_handling","to":"relation_service","relationType":"ensures_reliability_of"}
|
||||
{"type":"relation","from":"relation_service_testing","to":"relation_service","relationType":"verifies"}
|
||||
{"type":"relation","from":"service_layer_patterns","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"repository_patterns","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"fileio_patterns","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"database_models","to":"basic-memory_implementation_patterns","relationType":"implements"}
|
||||
{"type":"relation","from":"relation_service_challenges","to":"implementation_challenges","relationType":"extends"}
|
||||
{"type":"relation","from":"relation_service_implementation_plan","to":"future_work","relationType":"details"}
|
||||
{"type":"relation","from":"relation_service_design","to":"design_decisions","relationType":"aligns_with"}
|
||||
{"type":"relation","from":"relation_file_format","to":"design_decisions","relationType":"follows"}
|
||||
{"type":"relation","from":"pytest_patterns","to":"testing_infrastructure","relationType":"extends"}
|
||||
{"type":"relation","from":"relation_implementation_learnings","to":"basic-memory_implementation_patterns","relationType":"informs"}
|
||||
{"type":"relation","from":"test_driven_insights","to":"test_driven_development","relationType":"enriches"}
|
||||
{"type":"relation","from":"meta_development_insights","to":"AI_Human_Collaboration_Model","relationType":"improves"}
|
||||
{"type":"relation","from":"relation_implementation_learnings","to":"relation_service","relationType":"guides_implementation_of"}
|
||||
{"type":"relation","from":"pytest_patterns","to":"test_evolution","relationType":"demonstrates"}
|
||||
{"type":"relation","from":"test_driven_insights","to":"design_decisions","relationType":"influences"}
|
||||
{"type":"relation","from":"meta_development_insights","to":"architecture_evolution","relationType":"informs"}
|
||||
{"type":"relation","from":"relation_service","to":"relation_implementation_learnings","relationType":"validates"}
|
||||
{"type":"relation","from":"test_driven_insights","to":"implementation_challenges","relationType":"helps_solve"}
|
||||
{"type":"relation","from":"AI_Assistant_Learnings","to":"meta_development_insights","relationType":"enriches"}
|
||||
{"type":"relation","from":"Effective_Response_Patterns","to":"AI_Assistant_Learnings","relationType":"implements"}
|
||||
{"type":"relation","from":"AI_Context_Management","to":"AI_Human_Collaboration_Model","relationType":"improves"}
|
||||
{"type":"relation","from":"AI_Tool_Usage_Patterns","to":"AI_Context_Management","relationType":"enables"}
|
||||
{"type":"relation","from":"AI_Assistant_Learnings","to":"Basic_Memory_Meta_Experience","relationType":"validates"}
|
||||
{"type":"relation","from":"AI_Tool_Usage_Patterns","to":"Model_Context_Protocol","relationType":"demonstrates_effective_use_of"}
|
||||
{"type":"relation","from":"AI_Context_Management","to":"basic-memory","relationType":"validates_design_of"}
|
||||
{"type":"relation","from":"Effective_Response_Patterns","to":"AI_Human_Development_Methodology","relationType":"refines"}
|
||||
{"type":"relation","to":"relation_service","from":"relation_service_patterns","relationType":"guides"}
|
||||
{"type":"relation","to":"test_driven_development","from":"test_driven_insights_relations","relationType":"enriches"}
|
||||
{"type":"relation","to":"implementation_challenges","from":"relation_service_learnings","relationType":"solves"}
|
||||
{"type":"relation","to":"basic-memory_implementation_patterns","from":"relation_service_patterns","relationType":"implements"}
|
||||
{"type":"relation","to":"markdown_format","from":"relation_service_patterns","relationType":"extends"}
|
||||
{"type":"relation","to":"service_layer_patterns","from":"relation_service_patterns","relationType":"refines"}
|
||||
{"type":"relation","from":"packaging_learnings","to":"implementation_challenges","relationType":"informs"}
|
||||
{"type":"relation","from":"packaging_learnings","to":"test_driven_development","relationType":"impacts"}
|
||||
{"type":"relation","to":"basic-memory","from":"Recent_Implementation_Progress","relationType":"updates_status_of"}
|
||||
{"type":"relation","to":"future_work","from":"Next_Steps","relationType":"extends"}
|
||||
{"type":"relation","to":"design_decisions","from":"Development_Practices","relationType":"informs"}
|
||||
{"type":"relation","to":"packaging_learnings","from":"Development_Practices","relationType":"incorporates"}
|
||||
{"type":"relation","to":"test_driven_development","from":"Development_Practices","relationType":"refines"}
|
||||
{"type":"relation","to":"basic-memory_implementation_patterns","from":"Development_Practices","relationType":"enhances"}
|
||||
{"type":"relation","from":"Basic_Memory_MCP","to":"MCP_Server_Implementation","relationType":"follows"}
|
||||
{"type":"relation","from":"Basic_Memory_MCP","to":"MCP_Tools","relationType":"uses"}
|
||||
{"type":"relation","from":"Basic_Memory","to":"MCP_Server_Implementation","relationType":"implements"}
|
||||
{"type":"relation","from":"Basic_Memory_Testing","to":"Memory_Service_Tests","relationType":"includes"}
|
||||
{"type":"relation","from":"Basic_Memory_Testing","to":"MCP_Server_Tests","relationType":"includes"}
|
||||
{"type":"relation","from":"Memory_Service_Tests","to":"Basic_Memory_MCP","relationType":"validates"}
|
||||
{"type":"relation","from":"MCP_Server_Tests","to":"Basic_Memory_MCP","relationType":"validates"}
|
||||
{"type":"relation","from":"Service_Interface_Audit","to":"Memory_Service_Refactoring","relationType":"informs"}
|
||||
{"type":"relation","from":"Memory_Service_Refactoring","to":"Basic_Memory_MCP","relationType":"affects"}
|
||||
{"type":"relation","from":"Memory_Service_Patterns","to":"Basic_Memory_MCP","relationType":"improves"}
|
||||
{"type":"relation","from":"Pydantic_Create_Pattern","to":"Memory_Service_Patterns","relationType":"enables"}
|
||||
{"type":"relation","from":"Pydantic_Create_Pattern","to":"Basic_Memory_MCP","relationType":"improves"}
|
||||
{"type":"relation","from":"MCP_Marketplace","to":"Basic_Memory_Business","relationType":"enables"}
|
||||
{"type":"relation","from":"Basic_Memory","to":"MCP_Marketplace","relationType":"could_integrate_with"}
|
||||
{"type":"relation","from":"Persistence_Of_Vision","to":"Basic_Memory","relationType":"helps_achieve"}
|
||||
{"type":"relation","from":"Drew","to":"Persistence_Of_Vision","relationType":"conceptualized"}
|
||||
{"type":"relation","to":"Usage_Recipes","from":"Conversation_Continuity_Pattern","relationType":"is_example_of"}
|
||||
{"type":"relation","to":"Basic_Memory","from":"Usage_Recipes","relationType":"enhances"}
|
||||
{"type":"relation","to":"Basic_Memory","from":"Chat_References","relationType":"enhances"}
|
||||
{"type":"relation","to":"Conversation_Continuity_Pattern","from":"Chat_References","relationType":"implements"}
|
||||
{"type":"relation","from":"20240307-chat-reference-protocol-test","to":"20240307-chat-reference-protocol","relationType":"continues_from"}
|
||||
{"type":"relation","from_id":"Run_Tests_Tool_Request","to_id":"Basic_Machines","relationType":"enhances","context":"development workflow improvement"}
|
||||
{"type":"relation","from_id":"SQLAlchemy_Async_Loading_Pattern","to_id":"basic-memory","relation_type":"improves","context":"database performance and async compatibility"}
|
||||
{"type":"relation","from_id":"SQLAlchemy_Async_Loading_Pattern","to_id":"Entity","relation_type":"applies_to","context":"relationship loading strategy"}
|
||||
{"type":"relation","from":"MCP_Reference_Integration","to":"Project_Priorities","relationType":"prioritized_after"}
|
||||
{"type":"relation","from":"great_observation_loading_saga_20241207","to":"Basic_Memory","relationType":"occurred_in"}
|
||||
{"type":"relation","from":"great_observation_loading_saga_20241207","to":"SQLAlchemy","relationType":"relates_to"}
|
||||
{"type":"relation","from":"basic_memory_implementation_20241208","to":"Basic_Memory","relationType":"improves"}
|
||||
{"type":"relation","from":"great_observation_loading_saga_20241207","to":"basic_memory_implementation_20241208","relationType":"leads_to"}
|
||||
{"type":"relation","from":"MCP_Dependency_Risk","to":"DIY_Ethics","relationType":"validates"}
|
||||
{"type":"relation","from":"MCP_Dependency_Risk","to":"basic-memory_core_principles","relationType":"reinforces"}
|
||||
{"type":"relation","from":"MCP_Dependency_Risk","to":"Basic_Memory_Implementation_Plan","relationType":"influences"}
|
||||
{"type":"relation","from":"basic_memory_mcp_architecture","to":"basic_memory_project_20241208","relationType":"implements"}
|
||||
{"type":"relation","from":"basic_memory_sync_considerations","to":"basic_memory_project_20241208","relationType":"influences"}
|
||||
{"type":"relation","from":"mcp_server_learnings","to":"basic_memory_mcp_architecture","relationType":"informs"}
|
||||
{"type":"relation","from":"20241208-mcp-tool-refactoring","to":"Basic_Memory_MCP","relationType":"improves"}
|
||||
{"type":"relation","from":"20241208-mcp-tool-refactoring","to":"Basic_Memory_Implementation_Plan","relationType":"implements"}
|
||||
+48
-25
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "basic-memory"
|
||||
version = "0.7.0"
|
||||
dynamic = ["version"]
|
||||
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12.1"
|
||||
@@ -28,8 +28,12 @@ dependencies = [
|
||||
"watchfiles>=1.0.4",
|
||||
"fastapi[standard]>=0.115.8",
|
||||
"alembic>=1.14.1",
|
||||
"qasync>=0.27.1",
|
||||
"logfire[fastapi,httpx,sqlalchemy,sqlite3]>=3.6.0",
|
||||
"pillow>=11.1.0",
|
||||
"pybars3>=0.9.7",
|
||||
"fastmcp>=2.3.4",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -40,9 +44,10 @@ Documentation = "https://github.com/basicmachines-co/basic-memory#readme"
|
||||
|
||||
[project.scripts]
|
||||
basic-memory = "basic_memory.cli.main:app"
|
||||
bm = "basic_memory.cli.main:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
@@ -64,16 +69,19 @@ dev-dependencies = [
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-mock>=3.12.0",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
"pytest-xdist>=3.0.0",
|
||||
"ruff>=0.1.6",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-mock>=3.12.0",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
"ruff>=0.1.6",
|
||||
"cx-freeze>=7.2.10",
|
||||
"pyqt6>=6.8.1",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "uv-dynamic-versioning"
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
vcs = "git"
|
||||
style = "pep440"
|
||||
bump = true
|
||||
fallback-version = "0.0.0"
|
||||
|
||||
[tool.pyright]
|
||||
include = ["src/"]
|
||||
exclude = ["**/__pycache__"]
|
||||
@@ -84,20 +92,35 @@ reportMissingTypeStubs = false
|
||||
pythonVersion = "3.12"
|
||||
|
||||
|
||||
[tool.semantic_release]
|
||||
version_variables = [
|
||||
"src/basic_memory/__init__.py:__version__",
|
||||
]
|
||||
version_toml = [
|
||||
"pyproject.toml:project.version",
|
||||
]
|
||||
major_on_zero = false
|
||||
branch = "main"
|
||||
changelog_file = "CHANGELOG.md"
|
||||
build_command = "pip install uv && uv build"
|
||||
dist_path = "dist/"
|
||||
upload_to_pypi = true
|
||||
commit_message = "chore(release): {version} [skip ci]"
|
||||
|
||||
[tool.coverage.run]
|
||||
concurrency = ["thread", "gevent"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"if self.debug:",
|
||||
"if settings.DEBUG",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
"if 0:",
|
||||
"if __name__ == .__main__.:",
|
||||
"class .*\\bProtocol\\):",
|
||||
"@(abc\\.)?abstractmethod",
|
||||
]
|
||||
|
||||
# Exclude specific modules that are difficult to test comprehensively
|
||||
omit = [
|
||||
"*/external_auth_provider.py", # External HTTP calls to OAuth providers
|
||||
"*/supabase_auth_provider.py", # External HTTP calls to Supabase APIs
|
||||
"*/watch_service.py", # File system watching - complex integration testing
|
||||
"*/background_sync.py", # Background processes
|
||||
"*/cli/main.py", # CLI entry point
|
||||
"*/mcp/tools/project_management.py", # Covered by integration tests
|
||||
"*/mcp/tools/sync_status.py", # Covered by integration tests
|
||||
"*/services/migration_service.py", # Complex migration scenarios
|
||||
]
|
||||
|
||||
[tool.logfire]
|
||||
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."
|
||||
@@ -0,0 +1,15 @@
|
||||
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
||||
|
||||
startCommand:
|
||||
type: stdio
|
||||
configSchema:
|
||||
# JSON Schema defining the configuration options for the MCP.
|
||||
type: object
|
||||
properties: {}
|
||||
description: No configuration required. This MCP server runs using the default command.
|
||||
commandFunction: |-
|
||||
(config) => ({
|
||||
command: 'basic-memory',
|
||||
args: ['mcp']
|
||||
})
|
||||
exampleConfig: {}
|
||||
@@ -1,3 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
__version__ = "0.7.0"
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.14.0"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,119 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||
script_location = .
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
# version_path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
version_path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Alembic environment configuration."""
|
||||
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
@@ -7,8 +8,12 @@ from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from basic_memory.models import Base
|
||||
from basic_memory.config import config as app_config
|
||||
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
|
||||
os.environ["BASIC_MEMORY_ENV"] = "test"
|
||||
|
||||
# Import after setting environment variable # noqa: E402
|
||||
from basic_memory.config import app_config # 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.
|
||||
@@ -18,6 +23,8 @@ config = context.config
|
||||
sqlalchemy_url = f"sqlite:///{app_config.database_path}"
|
||||
config.set_main_option("sqlalchemy.url", sqlalchemy_url)
|
||||
|
||||
# print(f"Using SQLAlchemy URL: {sqlalchemy_url}")
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
@@ -27,6 +34,14 @@ if config.config_file_name is not None:
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
# Add this function to tell Alembic what to include/exclude
|
||||
def include_object(object, name, type_, reflected, compare_to):
|
||||
# Ignore SQLite FTS tables
|
||||
if type_ == "table" and name.startswith("search_index"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
@@ -44,6 +59,8 @@ def run_migrations_offline() -> None:
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
include_object=include_object,
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
@@ -63,7 +80,12 @@ def run_migrations_online() -> None:
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
include_object=include_object,
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Functions for managing database migrations."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from alembic.config import Config
|
||||
@@ -10,20 +9,16 @@ from alembic import command
|
||||
def get_alembic_config() -> Config: # pragma: no cover
|
||||
"""Get alembic config with correct paths."""
|
||||
migrations_path = Path(__file__).parent
|
||||
alembic_ini = migrations_path.parent.parent.parent / "alembic.ini"
|
||||
alembic_ini = migrations_path / "alembic.ini"
|
||||
|
||||
config = Config(alembic_ini)
|
||||
config.set_main_option("script_location", str(migrations_path))
|
||||
return config
|
||||
|
||||
|
||||
async def reset_database(): # pragma: no cover
|
||||
def reset_database(): # pragma: no cover
|
||||
"""Drop and recreate all tables."""
|
||||
logger.info("Resetting database...")
|
||||
config = get_alembic_config()
|
||||
|
||||
def _reset(cfg):
|
||||
command.downgrade(cfg, "base")
|
||||
command.upgrade(cfg, "head")
|
||||
|
||||
await asyncio.get_event_loop().run_in_executor(None, _reset, config)
|
||||
command.downgrade(config, "base")
|
||||
command.upgrade(config, "head")
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"""remove required from entity.permalink
|
||||
|
||||
Revision ID: 502b60eaa905
|
||||
Revises: b3c3938bacdb
|
||||
Create Date: 2025-02-24 13:33:09.790951
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "502b60eaa905"
|
||||
down_revision: Union[str, None] = "b3c3938bacdb"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
batch_op.alter_column("permalink", existing_type=sa.VARCHAR(), nullable=True)
|
||||
batch_op.drop_index("ix_entity_permalink")
|
||||
batch_op.create_index(batch_op.f("ix_entity_permalink"), ["permalink"], unique=False)
|
||||
batch_op.drop_constraint("uix_entity_permalink", type_="unique")
|
||||
batch_op.create_index(
|
||||
"uix_entity_permalink",
|
||||
["permalink"],
|
||||
unique=True,
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
batch_op.drop_index(
|
||||
"uix_entity_permalink",
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
batch_op.create_unique_constraint("uix_entity_permalink", ["permalink"])
|
||||
batch_op.drop_index(batch_op.f("ix_entity_permalink"))
|
||||
batch_op.create_index("ix_entity_permalink", ["permalink"], unique=1)
|
||||
batch_op.alter_column("permalink", existing_type=sa.VARCHAR(), nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,108 @@
|
||||
"""add projects table
|
||||
|
||||
Revision ID: 5fe1ab1ccebe
|
||||
Revises: cc7172b46608
|
||||
Create Date: 2025-05-14 09:05:18.214357
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "5fe1ab1ccebe"
|
||||
down_revision: Union[str, None] = "cc7172b46608"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"project",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("permalink", sa.String(), nullable=False),
|
||||
sa.Column("path", sa.String(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("is_default"),
|
||||
sa.UniqueConstraint("name"),
|
||||
sa.UniqueConstraint("permalink"),
|
||||
if_not_exists=True,
|
||||
)
|
||||
with op.batch_alter_table("project", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
"ix_project_created_at", ["created_at"], unique=False, if_not_exists=True
|
||||
)
|
||||
batch_op.create_index("ix_project_name", ["name"], unique=True, if_not_exists=True)
|
||||
batch_op.create_index("ix_project_path", ["path"], unique=False, if_not_exists=True)
|
||||
batch_op.create_index(
|
||||
"ix_project_permalink", ["permalink"], unique=True, if_not_exists=True
|
||||
)
|
||||
batch_op.create_index(
|
||||
"ix_project_updated_at", ["updated_at"], unique=False, if_not_exists=True
|
||||
)
|
||||
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("project_id", sa.Integer(), nullable=False))
|
||||
batch_op.drop_index(
|
||||
"uix_entity_permalink",
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
batch_op.drop_index("ix_entity_file_path")
|
||||
batch_op.create_index(batch_op.f("ix_entity_file_path"), ["file_path"], unique=False)
|
||||
batch_op.create_index("ix_entity_project_id", ["project_id"], unique=False)
|
||||
batch_op.create_index(
|
||||
"uix_entity_file_path_project", ["file_path", "project_id"], unique=True
|
||||
)
|
||||
batch_op.create_index(
|
||||
"uix_entity_permalink_project",
|
||||
["permalink", "project_id"],
|
||||
unique=True,
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
batch_op.create_foreign_key("fk_entity_project_id", "project", ["project_id"], ["id"])
|
||||
|
||||
# drop the search index table. it will be recreated
|
||||
op.drop_table("search_index")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
|
||||
batch_op.drop_index(
|
||||
"uix_entity_permalink_project",
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
batch_op.drop_index("uix_entity_file_path_project")
|
||||
batch_op.drop_index("ix_entity_project_id")
|
||||
batch_op.drop_index(batch_op.f("ix_entity_file_path"))
|
||||
batch_op.create_index("ix_entity_file_path", ["file_path"], unique=1)
|
||||
batch_op.create_index(
|
||||
"uix_entity_permalink",
|
||||
["permalink"],
|
||||
unique=1,
|
||||
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
)
|
||||
batch_op.drop_column("project_id")
|
||||
|
||||
with op.batch_alter_table("project", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_project_updated_at")
|
||||
batch_op.drop_index("ix_project_permalink")
|
||||
batch_op.drop_index("ix_project_path")
|
||||
batch_op.drop_index("ix_project_name")
|
||||
batch_op.drop_index("ix_project_created_at")
|
||||
|
||||
op.drop_table("project")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,104 @@
|
||||
"""project constraint fix
|
||||
|
||||
Revision ID: 647e7a75e2cd
|
||||
Revises: 5fe1ab1ccebe
|
||||
Create Date: 2025-06-03 12:48:30.162566
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "647e7a75e2cd"
|
||||
down_revision: Union[str, None] = "5fe1ab1ccebe"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Remove the problematic UNIQUE constraint on is_default column.
|
||||
|
||||
The UNIQUE constraint prevents multiple projects from having is_default=FALSE,
|
||||
which breaks project creation when the service sets is_default=False.
|
||||
|
||||
Since SQLite doesn't support dropping specific constraints easily, we'll
|
||||
recreate the table without the problematic constraint.
|
||||
"""
|
||||
# For SQLite, we need to recreate the table without the UNIQUE constraint
|
||||
# Create a new table without the UNIQUE constraint on is_default
|
||||
op.create_table(
|
||||
"project_new",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("permalink", sa.String(), nullable=False),
|
||||
sa.Column("path", sa.String(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=True), # No UNIQUE constraint!
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name"),
|
||||
sa.UniqueConstraint("permalink"),
|
||||
)
|
||||
|
||||
# Copy data from old table to new table
|
||||
op.execute("INSERT INTO project_new SELECT * FROM project")
|
||||
|
||||
# Drop the old table
|
||||
op.drop_table("project")
|
||||
|
||||
# Rename the new table
|
||||
op.rename_table("project_new", "project")
|
||||
|
||||
# Recreate the indexes
|
||||
with op.batch_alter_table("project", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
||||
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
||||
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
||||
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
||||
batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Add back the UNIQUE constraint on is_default column.
|
||||
|
||||
WARNING: This will break project creation again if multiple projects
|
||||
have is_default=FALSE.
|
||||
"""
|
||||
# Recreate the table with the UNIQUE constraint
|
||||
op.create_table(
|
||||
"project_old",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("permalink", sa.String(), nullable=False),
|
||||
sa.Column("path", sa.String(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("is_default"), # Add back the problematic constraint
|
||||
sa.UniqueConstraint("name"),
|
||||
sa.UniqueConstraint("permalink"),
|
||||
)
|
||||
|
||||
# Copy data (this may fail if multiple FALSE values exist)
|
||||
op.execute("INSERT INTO project_old SELECT * FROM project")
|
||||
|
||||
# Drop the current table and rename
|
||||
op.drop_table("project")
|
||||
op.rename_table("project_old", "project")
|
||||
|
||||
# Recreate indexes
|
||||
with op.batch_alter_table("project", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
||||
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
||||
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
||||
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
||||
batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""relation to_name unique index
|
||||
|
||||
Revision ID: b3c3938bacdb
|
||||
Revises: 3dae7c7b1564
|
||||
Create Date: 2025-02-22 14:59:30.668466
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b3c3938bacdb"
|
||||
down_revision: Union[str, None] = "3dae7c7b1564"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# SQLite doesn't support constraint changes through ALTER
|
||||
# Need to recreate table with desired constraints
|
||||
with op.batch_alter_table("relation") as batch_op:
|
||||
# Drop existing unique constraint
|
||||
batch_op.drop_constraint("uix_relation", type_="unique")
|
||||
|
||||
# Add new constraints
|
||||
batch_op.create_unique_constraint(
|
||||
"uix_relation_from_id_to_id", ["from_id", "to_id", "relation_type"]
|
||||
)
|
||||
batch_op.create_unique_constraint(
|
||||
"uix_relation_from_id_to_name", ["from_id", "to_name", "relation_type"]
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("relation") as batch_op:
|
||||
# Drop new constraints
|
||||
batch_op.drop_constraint("uix_relation_from_id_to_name", type_="unique")
|
||||
batch_op.drop_constraint("uix_relation_from_id_to_id", type_="unique")
|
||||
|
||||
# Restore original constraint
|
||||
batch_op.create_unique_constraint("uix_relation", ["from_id", "to_id", "relation_type"])
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Update search index schema
|
||||
|
||||
Revision ID: cc7172b46608
|
||||
Revises: 502b60eaa905
|
||||
Create Date: 2025-02-28 18:48:23.244941
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "cc7172b46608"
|
||||
down_revision: Union[str, None] = "502b60eaa905"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade database schema to use new search index with content_stems and content_snippet."""
|
||||
|
||||
# First, drop the existing search_index table
|
||||
op.execute("DROP TABLE IF EXISTS search_index")
|
||||
|
||||
# Create new search_index with updated schema
|
||||
op.execute("""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
||||
-- Core entity fields
|
||||
id UNINDEXED, -- Row ID
|
||||
title, -- Title for searching
|
||||
content_stems, -- Main searchable content split into stems
|
||||
content_snippet, -- File content snippet for display
|
||||
permalink, -- Stable identifier (now indexed for path search)
|
||||
file_path UNINDEXED, -- Physical location
|
||||
type UNINDEXED, -- entity/relation/observation
|
||||
|
||||
-- Relation fields
|
||||
from_id UNINDEXED, -- Source entity
|
||||
to_id UNINDEXED, -- Target entity
|
||||
relation_type UNINDEXED, -- Type of relation
|
||||
|
||||
-- Observation fields
|
||||
entity_id UNINDEXED, -- Parent entity
|
||||
category UNINDEXED, -- Observation category
|
||||
|
||||
-- Common fields
|
||||
metadata UNINDEXED, -- JSON metadata
|
||||
created_at UNINDEXED, -- Creation timestamp
|
||||
updated_at UNINDEXED, -- Last update
|
||||
|
||||
-- Configuration
|
||||
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
||||
prefix='1,2,3,4' -- Support longer prefixes for paths
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade database schema to use old search index."""
|
||||
# Drop the updated search_index table
|
||||
op.execute("DROP TABLE IF EXISTS search_index")
|
||||
|
||||
# Recreate the original search_index schema
|
||||
op.execute("""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
||||
-- Core entity fields
|
||||
id UNINDEXED, -- Row ID
|
||||
title, -- Title for searching
|
||||
content, -- Main searchable content
|
||||
permalink, -- Stable identifier (now indexed for path search)
|
||||
file_path UNINDEXED, -- Physical location
|
||||
type UNINDEXED, -- entity/relation/observation
|
||||
|
||||
-- Relation fields
|
||||
from_id UNINDEXED, -- Source entity
|
||||
to_id UNINDEXED, -- Target entity
|
||||
relation_type UNINDEXED, -- Type of relation
|
||||
|
||||
-- Observation fields
|
||||
entity_id UNINDEXED, -- Parent entity
|
||||
category UNINDEXED, -- Observation category
|
||||
|
||||
-- Common fields
|
||||
metadata UNINDEXED, -- JSON metadata
|
||||
created_at UNINDEXED, -- Creation timestamp
|
||||
updated_at UNINDEXED, -- Last update
|
||||
|
||||
-- Configuration
|
||||
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
||||
prefix='1,2,3,4' -- Support longer prefixes for paths
|
||||
);
|
||||
""")
|
||||
|
||||
# Print instruction to manually reindex after migration
|
||||
print("\n------------------------------------------------------------------")
|
||||
print("IMPORTANT: After downgrade completes, manually run the reindex command:")
|
||||
print("basic-memory sync")
|
||||
print("------------------------------------------------------------------\n")
|
||||
+54
-17
@@ -1,27 +1,51 @@
|
||||
"""FastAPI application for basic-memory knowledge graph API."""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import logfire
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exception_handlers import http_exception_handler
|
||||
from loguru import logger
|
||||
|
||||
import basic_memory
|
||||
from basic_memory import __version__ as version
|
||||
from basic_memory import db
|
||||
from basic_memory.config import config as app_config
|
||||
from basic_memory.api.routers import knowledge, search, memory, resource
|
||||
from basic_memory.utils import setup_logging
|
||||
from basic_memory.api.routers import (
|
||||
directory_router,
|
||||
importer_router,
|
||||
knowledge,
|
||||
management,
|
||||
memory,
|
||||
project,
|
||||
resource,
|
||||
search,
|
||||
prompt_router,
|
||||
)
|
||||
from basic_memory.config import app_config
|
||||
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."""
|
||||
setup_logging(log_file=".basic-memory/basic-memory.log")
|
||||
logger.info(f"Starting Basic Memory API {basic_memory.__version__}")
|
||||
await db.run_migrations(app_config)
|
||||
# Initialize app and database
|
||||
logger.info("Starting Basic Memory API")
|
||||
await initialize_app(app_config)
|
||||
|
||||
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
||||
if app_config.sync_changes:
|
||||
# start file sync task in background
|
||||
app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
|
||||
else:
|
||||
logger.info("Sync changes disabled. Skipping file sync service.")
|
||||
|
||||
# proceed with startup
|
||||
yield
|
||||
|
||||
logger.info("Shutting down Basic Memory API")
|
||||
if app.state.sync_task:
|
||||
logger.info("Stopping sync...")
|
||||
app.state.sync_task.cancel() # pyright: ignore
|
||||
|
||||
await db.shutdown_db()
|
||||
|
||||
|
||||
@@ -29,24 +53,37 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
||||
app = FastAPI(
|
||||
title="Basic Memory API",
|
||||
description="Knowledge graph API for basic-memory",
|
||||
version="0.1.0",
|
||||
version=version,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
if app_config != "test":
|
||||
logfire.instrument_fastapi(app)
|
||||
|
||||
|
||||
# Include routers
|
||||
app.include_router(knowledge.router)
|
||||
app.include_router(search.router)
|
||||
app.include_router(memory.router)
|
||||
app.include_router(resource.router)
|
||||
app.include_router(knowledge.router, prefix="/{project}")
|
||||
app.include_router(memory.router, prefix="/{project}")
|
||||
app.include_router(resource.router, prefix="/{project}")
|
||||
app.include_router(search.router, prefix="/{project}")
|
||||
app.include_router(project.project_router, prefix="/{project}")
|
||||
app.include_router(directory_router.router, prefix="/{project}")
|
||||
app.include_router(prompt_router.router, prefix="/{project}")
|
||||
app.include_router(importer_router.router, prefix="/{project}")
|
||||
|
||||
# Project resource router works accross projects
|
||||
app.include_router(project.project_resource_router)
|
||||
app.include_router(management.router)
|
||||
|
||||
# Auth routes are handled by FastMCP automatically when auth is enabled
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def exception_handler(request, exc): # pragma: no cover
|
||||
logger.exception(
|
||||
f"An unhandled exception occurred for request '{request.url}', exception: {exc}"
|
||||
"API unhandled exception",
|
||||
url=str(request.url),
|
||||
method=request.method,
|
||||
client=request.client.host if request.client else None,
|
||||
path=request.url.path,
|
||||
error_type=type(exc).__name__,
|
||||
error=str(exc),
|
||||
)
|
||||
return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""API routers."""
|
||||
|
||||
from . import knowledge_router as knowledge
|
||||
from . import management_router as management
|
||||
from . import memory_router as memory
|
||||
from . import project_router as project
|
||||
from . import resource_router as resource
|
||||
from . import search_router as search
|
||||
from . import prompt_router as prompt
|
||||
|
||||
__all__ = ["knowledge", "memory", "resource", "search"]
|
||||
__all__ = ["knowledge", "management", "memory", "project", "resource", "search", "prompt"]
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Router for directory tree operations."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from basic_memory.deps import DirectoryServiceDep, ProjectIdDep
|
||||
from basic_memory.schemas.directory import DirectoryNode
|
||||
|
||||
router = APIRouter(prefix="/directory", tags=["directory"])
|
||||
|
||||
|
||||
@router.get("/tree", response_model=DirectoryNode)
|
||||
async def get_directory_tree(
|
||||
directory_service: DirectoryServiceDep,
|
||||
project_id: ProjectIdDep,
|
||||
):
|
||||
"""Get hierarchical directory structure from the knowledge base.
|
||||
|
||||
Args:
|
||||
directory_service: Service for directory operations
|
||||
project_id: ID of the current project
|
||||
|
||||
Returns:
|
||||
DirectoryNode representing the root of the hierarchical tree structure
|
||||
"""
|
||||
# Get a hierarchical directory tree for the specific project
|
||||
tree = await directory_service.get_directory_tree()
|
||||
|
||||
# Return the hierarchical tree
|
||||
return tree
|
||||
|
||||
|
||||
@router.get("/list", response_model=List[DirectoryNode])
|
||||
async def list_directory(
|
||||
directory_service: DirectoryServiceDep,
|
||||
project_id: ProjectIdDep,
|
||||
dir_name: str = Query("/", description="Directory path to list"),
|
||||
depth: int = Query(1, ge=1, le=10, description="Recursion depth (1-10)"),
|
||||
file_name_glob: Optional[str] = Query(
|
||||
None, description="Glob pattern for filtering file names"
|
||||
),
|
||||
):
|
||||
"""List directory contents with filtering and depth control.
|
||||
|
||||
Args:
|
||||
directory_service: Service for directory operations
|
||||
project_id: ID of the current project
|
||||
dir_name: Directory path to list (default: root "/")
|
||||
depth: Recursion depth (1-10, default: 1 for immediate children only)
|
||||
file_name_glob: Optional glob pattern for filtering file names (e.g., "*.md", "*meeting*")
|
||||
|
||||
Returns:
|
||||
List of DirectoryNode objects matching the criteria
|
||||
"""
|
||||
# Get directory listing with filtering
|
||||
nodes = await directory_service.list_directory(
|
||||
dir_name=dir_name,
|
||||
depth=depth,
|
||||
file_name_glob=file_name_glob,
|
||||
)
|
||||
|
||||
return nodes
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Import router for Basic Memory API."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, UploadFile, status
|
||||
|
||||
from basic_memory.deps import (
|
||||
ChatGPTImporterDep,
|
||||
ClaudeConversationsImporterDep,
|
||||
ClaudeProjectsImporterDep,
|
||||
MemoryJsonImporterDep,
|
||||
)
|
||||
from basic_memory.importers import Importer
|
||||
from basic_memory.schemas.importer import (
|
||||
ChatImportResult,
|
||||
EntityImportResult,
|
||||
ProjectImportResult,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/import", tags=["import"])
|
||||
|
||||
|
||||
@router.post("/chatgpt", response_model=ChatImportResult)
|
||||
async def import_chatgpt(
|
||||
importer: ChatGPTImporterDep,
|
||||
file: UploadFile,
|
||||
folder: str = Form("conversations"),
|
||||
) -> ChatImportResult:
|
||||
"""Import conversations from ChatGPT JSON export.
|
||||
|
||||
Args:
|
||||
file: The ChatGPT conversations.json file.
|
||||
folder: The folder to place the files in.
|
||||
markdown_processor: MarkdownProcessor instance.
|
||||
|
||||
Returns:
|
||||
ChatImportResult with import statistics.
|
||||
|
||||
Raises:
|
||||
HTTPException: If import fails.
|
||||
"""
|
||||
return await import_file(importer, file, folder)
|
||||
|
||||
|
||||
@router.post("/claude/conversations", response_model=ChatImportResult)
|
||||
async def import_claude_conversations(
|
||||
importer: ClaudeConversationsImporterDep,
|
||||
file: UploadFile,
|
||||
folder: str = Form("conversations"),
|
||||
) -> ChatImportResult:
|
||||
"""Import conversations from Claude conversations.json export.
|
||||
|
||||
Args:
|
||||
file: The Claude conversations.json file.
|
||||
folder: The folder to place the files in.
|
||||
markdown_processor: MarkdownProcessor instance.
|
||||
|
||||
Returns:
|
||||
ChatImportResult with import statistics.
|
||||
|
||||
Raises:
|
||||
HTTPException: If import fails.
|
||||
"""
|
||||
return await import_file(importer, file, folder)
|
||||
|
||||
|
||||
@router.post("/claude/projects", response_model=ProjectImportResult)
|
||||
async def import_claude_projects(
|
||||
importer: ClaudeProjectsImporterDep,
|
||||
file: UploadFile,
|
||||
folder: str = Form("projects"),
|
||||
) -> ProjectImportResult:
|
||||
"""Import projects from Claude projects.json export.
|
||||
|
||||
Args:
|
||||
file: The Claude projects.json file.
|
||||
base_folder: The base folder to place the files in.
|
||||
markdown_processor: MarkdownProcessor instance.
|
||||
|
||||
Returns:
|
||||
ProjectImportResult with import statistics.
|
||||
|
||||
Raises:
|
||||
HTTPException: If import fails.
|
||||
"""
|
||||
return await import_file(importer, file, folder)
|
||||
|
||||
|
||||
@router.post("/memory-json", response_model=EntityImportResult)
|
||||
async def import_memory_json(
|
||||
importer: MemoryJsonImporterDep,
|
||||
file: UploadFile,
|
||||
folder: str = Form("conversations"),
|
||||
) -> EntityImportResult:
|
||||
"""Import entities and relations from a memory.json file.
|
||||
|
||||
Args:
|
||||
file: The memory.json file.
|
||||
destination_folder: Optional destination folder within the project.
|
||||
markdown_processor: MarkdownProcessor instance.
|
||||
|
||||
Returns:
|
||||
EntityImportResult with import statistics.
|
||||
|
||||
Raises:
|
||||
HTTPException: If import fails.
|
||||
"""
|
||||
try:
|
||||
file_data = []
|
||||
file_bytes = await file.read()
|
||||
file_str = file_bytes.decode("utf-8")
|
||||
for line in file_str.splitlines():
|
||||
json_data = json.loads(line)
|
||||
file_data.append(json_data)
|
||||
|
||||
result = await importer.import_data(file_data, folder)
|
||||
if not result.success: # pragma: no cover
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.error_message or "Import failed",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Import failed")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Import failed: {str(e)}",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
|
||||
try:
|
||||
# Process file
|
||||
json_data = json.load(file.file)
|
||||
result = await importer.import_data(json_data, destination_folder)
|
||||
if not result.success: # pragma: no cover
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.error_message or "Import failed",
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Import failed")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Import failed: {str(e)}",
|
||||
)
|
||||
@@ -10,6 +10,11 @@ from basic_memory.deps import (
|
||||
get_search_service,
|
||||
SearchServiceDep,
|
||||
LinkResolverDep,
|
||||
ProjectPathDep,
|
||||
FileServiceDep,
|
||||
ProjectConfigDep,
|
||||
AppConfigDep,
|
||||
SyncServiceDep,
|
||||
)
|
||||
from basic_memory.schemas import (
|
||||
EntityListResponse,
|
||||
@@ -17,8 +22,8 @@ from basic_memory.schemas import (
|
||||
DeleteEntitiesResponse,
|
||||
DeleteEntitiesRequest,
|
||||
)
|
||||
from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
|
||||
from basic_memory.schemas.base import Permalink, Entity
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
||||
|
||||
@@ -33,7 +38,9 @@ async def create_entity(
|
||||
search_service: SearchServiceDep,
|
||||
) -> EntityResponse:
|
||||
"""Create an entity."""
|
||||
logger.info(f"request: create_entity with data={data}")
|
||||
logger.info(
|
||||
"API request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
|
||||
)
|
||||
|
||||
entity = await entity_service.create_entity(data)
|
||||
|
||||
@@ -41,25 +48,38 @@ async def create_entity(
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
result = EntityResponse.model_validate(entity)
|
||||
|
||||
logger.info(f"response: create_entity with result={result}")
|
||||
logger.info(
|
||||
f"API response: endpoint='create_entity' title={result.title}, permalink={result.permalink}, status_code=201"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/entities/{permalink:path}", response_model=EntityResponse)
|
||||
async def create_or_update_entity(
|
||||
project: ProjectPathDep,
|
||||
permalink: Permalink,
|
||||
data: Entity,
|
||||
response: Response,
|
||||
background_tasks: BackgroundTasks,
|
||||
entity_service: EntityServiceDep,
|
||||
search_service: SearchServiceDep,
|
||||
file_service: FileServiceDep,
|
||||
sync_service: SyncServiceDep,
|
||||
) -> EntityResponse:
|
||||
"""Create or update an entity. If entity exists, it will be updated, otherwise created."""
|
||||
logger.info(f"request: create_or_update_entity with permalink={permalink}, data={data}")
|
||||
logger.info(
|
||||
f"API request: create_or_update_entity for {project=}, {permalink=}, {data.entity_type=}, {data.title=}"
|
||||
)
|
||||
|
||||
# Validate permalink matches
|
||||
if data.permalink != permalink:
|
||||
raise HTTPException(status_code=400, detail="Entity permalink must match URL path")
|
||||
logger.warning(
|
||||
f"API validation error: creating/updating entity with permalink mismatch - url={permalink}, data={data.permalink}",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Entity permalink {data.permalink} must match URL path: '{permalink}'",
|
||||
)
|
||||
|
||||
# Try create_or_update operation
|
||||
entity, created = await entity_service.create_or_update_entity(data)
|
||||
@@ -67,36 +87,144 @@ async def create_or_update_entity(
|
||||
|
||||
# reindex
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
# Attempt immediate relation resolution when creating new entities
|
||||
# This helps resolve forward references when related entities are created in the same session
|
||||
if created:
|
||||
try:
|
||||
await sync_service.resolve_relations()
|
||||
logger.debug(f"Resolved relations after creating entity: {entity.permalink}")
|
||||
except Exception as e: # pragma: no cover
|
||||
# Don't fail the entire request if relation resolution fails
|
||||
logger.warning(f"Failed to resolve relations after entity creation: {e}")
|
||||
|
||||
result = EntityResponse.model_validate(entity)
|
||||
|
||||
logger.info(
|
||||
f"response: create_or_update_entity with result={result}, status_code={response.status_code}"
|
||||
f"API response: {result.title=}, {result.permalink=}, {created=}, status_code={response.status_code}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/entities/{identifier:path}", response_model=EntityResponse)
|
||||
async def edit_entity(
|
||||
identifier: str,
|
||||
data: EditEntityRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
entity_service: EntityServiceDep,
|
||||
search_service: SearchServiceDep,
|
||||
) -> EntityResponse:
|
||||
"""Edit an existing entity using various operations like append, prepend, find_replace, or replace_section.
|
||||
|
||||
This endpoint allows for targeted edits without requiring the full entity content.
|
||||
"""
|
||||
logger.info(
|
||||
f"API request: endpoint='edit_entity', identifier='{identifier}', operation='{data.operation}'"
|
||||
)
|
||||
|
||||
try:
|
||||
# Edit the entity using the service
|
||||
entity = await entity_service.edit_entity(
|
||||
identifier=identifier,
|
||||
operation=data.operation,
|
||||
content=data.content,
|
||||
section=data.section,
|
||||
find_text=data.find_text,
|
||||
expected_replacements=data.expected_replacements,
|
||||
)
|
||||
|
||||
# Reindex the updated entity
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
# Return the updated entity response
|
||||
result = EntityResponse.model_validate(entity)
|
||||
|
||||
logger.info(
|
||||
"API response",
|
||||
endpoint="edit_entity",
|
||||
identifier=identifier,
|
||||
operation=data.operation,
|
||||
permalink=result.permalink,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing entity: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/move")
|
||||
async def move_entity(
|
||||
data: MoveEntityRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
entity_service: EntityServiceDep,
|
||||
project_config: ProjectConfigDep,
|
||||
app_config: AppConfigDep,
|
||||
search_service: SearchServiceDep,
|
||||
) -> EntityResponse:
|
||||
"""Move an entity to a new file location with project consistency.
|
||||
|
||||
This endpoint moves a note to a different path while maintaining project
|
||||
consistency and optionally updating permalinks based on configuration.
|
||||
"""
|
||||
logger.info(
|
||||
f"API request: endpoint='move_entity', identifier='{data.identifier}', destination='{data.destination_path}'"
|
||||
)
|
||||
|
||||
try:
|
||||
# Move the entity using the service
|
||||
moved_entity = await entity_service.move_entity(
|
||||
identifier=data.identifier,
|
||||
destination_path=data.destination_path,
|
||||
project_config=project_config,
|
||||
app_config=app_config,
|
||||
)
|
||||
|
||||
# Get the moved entity to reindex it
|
||||
entity = await entity_service.link_resolver.resolve_link(data.destination_path)
|
||||
if entity:
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
logger.info(
|
||||
"API response",
|
||||
endpoint="move_entity",
|
||||
identifier=data.identifier,
|
||||
destination=data.destination_path,
|
||||
status_code=200,
|
||||
)
|
||||
result = EntityResponse.model_validate(moved_entity)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving entity: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
## Read endpoints
|
||||
|
||||
|
||||
@router.get("/entities/{permalink:path}", response_model=EntityResponse)
|
||||
@router.get("/entities/{identifier:path}", response_model=EntityResponse)
|
||||
async def get_entity(
|
||||
entity_service: EntityServiceDep,
|
||||
permalink: str,
|
||||
link_resolver: LinkResolverDep,
|
||||
identifier: str,
|
||||
) -> EntityResponse:
|
||||
"""Get a specific entity by ID.
|
||||
"""Get a specific entity by file path or permalink..
|
||||
|
||||
Args:
|
||||
permalink: Entity path ID
|
||||
content: If True, include full file content
|
||||
identifier: Entity file path or permalink
|
||||
:param entity_service: EntityService
|
||||
:param link_resolver: LinkResolver
|
||||
"""
|
||||
logger.info(f"request: get_entity with permalink={permalink}")
|
||||
try:
|
||||
entity = await entity_service.get_by_permalink(permalink)
|
||||
result = EntityResponse.model_validate(entity)
|
||||
return result
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Entity with {permalink} not found")
|
||||
logger.info(f"request: get_entity with identifier={identifier}")
|
||||
entity = await link_resolver.resolve_link(identifier)
|
||||
if not entity:
|
||||
raise HTTPException(status_code=404, detail=f"Entity {identifier} not found")
|
||||
|
||||
result = EntityResponse.model_validate(entity)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/entities", response_model=EntityListResponse)
|
||||
@@ -133,10 +261,10 @@ async def delete_entity(
|
||||
return DeleteEntitiesResponse(deleted=False)
|
||||
|
||||
# Delete the entity
|
||||
deleted = await entity_service.delete_entity(entity.permalink)
|
||||
deleted = await entity_service.delete_entity(entity.permalink or entity.id)
|
||||
|
||||
# Remove from search index
|
||||
background_tasks.add_task(search_service.delete_by_permalink, entity.permalink)
|
||||
# Remove from search index (entity, observations, and relations)
|
||||
background_tasks.add_task(search_service.handle_delete, entity)
|
||||
|
||||
result = DeleteEntitiesResponse(deleted=deleted)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Management router for basic-memory API."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.deps import SyncServiceDep, ProjectRepositoryDep
|
||||
|
||||
router = APIRouter(prefix="/management", tags=["management"])
|
||||
|
||||
|
||||
class WatchStatusResponse(BaseModel):
|
||||
"""Response model for watch status."""
|
||||
|
||||
running: bool
|
||||
"""Whether the watch service is currently running."""
|
||||
|
||||
|
||||
@router.get("/watch/status", response_model=WatchStatusResponse)
|
||||
async def get_watch_status(request: Request) -> WatchStatusResponse:
|
||||
"""Get the current status of the watch service."""
|
||||
return WatchStatusResponse(
|
||||
running=request.app.state.watch_task is not None and not request.app.state.watch_task.done()
|
||||
)
|
||||
|
||||
|
||||
@router.post("/watch/start", response_model=WatchStatusResponse)
|
||||
async def start_watch_service(
|
||||
request: Request, project_repository: ProjectRepositoryDep, sync_service: SyncServiceDep
|
||||
) -> WatchStatusResponse:
|
||||
"""Start the watch service if it's not already running."""
|
||||
|
||||
# needed because of circular imports from sync -> app
|
||||
from basic_memory.sync import WatchService
|
||||
from basic_memory.sync.background_sync import create_background_sync_task
|
||||
|
||||
if request.app.state.watch_task is not None and not request.app.state.watch_task.done():
|
||||
# Watch service is already running
|
||||
return WatchStatusResponse(running=True)
|
||||
|
||||
# Create and start a new watch service
|
||||
logger.info("Starting watch service via management API")
|
||||
|
||||
# Get services needed for the watch task
|
||||
watch_service = WatchService(
|
||||
app_config=app_config,
|
||||
project_repository=project_repository,
|
||||
)
|
||||
|
||||
# Create and store the task
|
||||
watch_task = create_background_sync_task(sync_service, watch_service)
|
||||
request.app.state.watch_task = watch_task
|
||||
|
||||
return WatchStatusResponse(running=True)
|
||||
|
||||
|
||||
@router.post("/watch/stop", response_model=WatchStatusResponse)
|
||||
async def stop_watch_service(request: Request) -> WatchStatusResponse: # pragma: no cover
|
||||
"""Stop the watch service if it's running."""
|
||||
if request.app.state.watch_task is None or request.app.state.watch_task.done():
|
||||
# Watch service is not running
|
||||
return WatchStatusResponse(running=False)
|
||||
|
||||
# Cancel the running task
|
||||
logger.info("Stopping watch service via management API")
|
||||
request.app.state.watch_task.cancel()
|
||||
|
||||
# Wait for it to be properly cancelled
|
||||
try:
|
||||
await request.app.state.watch_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
request.app.state.watch_task = None
|
||||
return WatchStatusResponse(running=False)
|
||||
@@ -1,79 +1,22 @@
|
||||
"""Routes for memory:// URI operations."""
|
||||
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from dateparser import parse
|
||||
from fastapi import APIRouter, Query
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.repository.search_repository import SearchIndexRow
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.base import TimeFrame, parse_timeframe
|
||||
from basic_memory.schemas.memory import (
|
||||
GraphContext,
|
||||
RelationSummary,
|
||||
EntitySummary,
|
||||
ObservationSummary,
|
||||
MemoryMetadata,
|
||||
normalize_memory_url,
|
||||
)
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
from basic_memory.services.context_service import ContextResultRow
|
||||
from basic_memory.api.routers.utils import to_graph_context
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
async def to_graph_context(context, entity_repository: EntityRepository, page: int, page_size: int):
|
||||
# return results
|
||||
async def to_summary(item: SearchIndexRow | ContextResultRow):
|
||||
match item.type:
|
||||
case SearchItemType.ENTITY:
|
||||
assert item.title is not None
|
||||
assert item.created_at is not None
|
||||
|
||||
return EntitySummary(
|
||||
title=item.title,
|
||||
permalink=item.permalink,
|
||||
file_path=item.file_path,
|
||||
created_at=item.created_at,
|
||||
)
|
||||
case SearchItemType.OBSERVATION:
|
||||
assert item.category is not None
|
||||
assert item.content is not None
|
||||
|
||||
return ObservationSummary(
|
||||
category=item.category, content=item.content, permalink=item.permalink
|
||||
)
|
||||
case SearchItemType.RELATION:
|
||||
assert item.from_id is not None
|
||||
from_entity = await entity_repository.find_by_id(item.from_id)
|
||||
assert from_entity is not None
|
||||
|
||||
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
|
||||
|
||||
return RelationSummary(
|
||||
permalink=item.permalink,
|
||||
relation_type=item.type,
|
||||
from_id=from_entity.permalink,
|
||||
to_id=to_entity.permalink if to_entity else None,
|
||||
)
|
||||
case _: # pragma: no cover
|
||||
raise ValueError(f"Unexpected type: {item.type}")
|
||||
|
||||
primary_results = [await to_summary(r) for r in context["primary_results"]]
|
||||
related_results = [await to_summary(r) for r in context["related_results"]]
|
||||
metadata = MemoryMetadata.model_validate(context["metadata"])
|
||||
# Transform to GraphContext
|
||||
return GraphContext(
|
||||
primary_results=primary_results,
|
||||
related_results=related_results,
|
||||
metadata=metadata,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/recent", response_model=GraphContext)
|
||||
async def recent(
|
||||
context_service: ContextServiceDep,
|
||||
@@ -96,7 +39,7 @@ async def recent(
|
||||
f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
|
||||
)
|
||||
# Parse timeframe
|
||||
since = parse(timeframe)
|
||||
since = parse_timeframe(timeframe)
|
||||
limit = page_size
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
@@ -104,9 +47,11 @@ async def recent(
|
||||
context = await context_service.build_context(
|
||||
types=types, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
|
||||
)
|
||||
return await to_graph_context(
|
||||
recent_context = await to_graph_context(
|
||||
context, entity_repository=entity_repository, page=page, page_size=page_size
|
||||
)
|
||||
logger.debug(f"Recent context: {recent_context.model_dump_json()}")
|
||||
return recent_context
|
||||
|
||||
|
||||
# get_memory_context needs to be declared last so other paths can match
|
||||
@@ -118,7 +63,7 @@ async def get_memory_context(
|
||||
entity_repository: EntityRepositoryDep,
|
||||
uri: str,
|
||||
depth: int = 1,
|
||||
timeframe: TimeFrame = "7d",
|
||||
timeframe: Optional[TimeFrame] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
@@ -132,7 +77,7 @@ async def get_memory_context(
|
||||
memory_url = normalize_memory_url(uri)
|
||||
|
||||
# Parse timeframe
|
||||
since = parse(timeframe)
|
||||
since = parse_timeframe(timeframe) if timeframe else None
|
||||
limit = page_size
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Router for project management."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Body
|
||||
from typing import Optional
|
||||
|
||||
from basic_memory.deps import ProjectServiceDep, ProjectPathDep
|
||||
from basic_memory.schemas import ProjectInfoResponse
|
||||
from basic_memory.schemas.project_info import (
|
||||
ProjectList,
|
||||
ProjectItem,
|
||||
ProjectInfoRequest,
|
||||
ProjectStatusResponse,
|
||||
)
|
||||
|
||||
# Router for resources in a specific project
|
||||
project_router = APIRouter(prefix="/project", tags=["project"])
|
||||
|
||||
# Router for managing project resources
|
||||
project_resource_router = APIRouter(prefix="/projects", tags=["project_management"])
|
||||
|
||||
|
||||
@project_router.get("/info", response_model=ProjectInfoResponse)
|
||||
async def get_project_info(
|
||||
project_service: ProjectServiceDep,
|
||||
project: ProjectPathDep,
|
||||
) -> ProjectInfoResponse:
|
||||
"""Get comprehensive information about the specified Basic Memory project."""
|
||||
return await project_service.get_project_info(project)
|
||||
|
||||
|
||||
# Update a project
|
||||
@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"),
|
||||
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
|
||||
is_active: Optional status update for the project
|
||||
|
||||
Returns:
|
||||
Response confirming the project was updated
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
# Get original project info for the response
|
||||
old_project_info = ProjectItem(
|
||||
name=project_name,
|
||||
path=project_service.projects.get(project_name, ""),
|
||||
)
|
||||
|
||||
await project_service.update_project(project_name, updated_path=path, is_active=is_active)
|
||||
|
||||
# Get updated project info
|
||||
updated_path = path if path else project_service.projects.get(project_name, "")
|
||||
|
||||
return ProjectStatusResponse(
|
||||
message=f"Project '{project_name}' updated successfully",
|
||||
status="success",
|
||||
default=(project_name == project_service.default_project),
|
||||
old_project=old_project_info,
|
||||
new_project=ProjectItem(name=project_name, path=updated_path),
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# List all available projects
|
||||
@project_resource_router.get("/projects", response_model=ProjectList)
|
||||
async def list_projects(
|
||||
project_service: ProjectServiceDep,
|
||||
) -> ProjectList:
|
||||
"""List all configured projects.
|
||||
|
||||
Returns:
|
||||
A list of all projects with metadata
|
||||
"""
|
||||
projects = await project_service.list_projects()
|
||||
default_project = project_service.default_project
|
||||
|
||||
project_items = [
|
||||
ProjectItem(
|
||||
name=project.name,
|
||||
path=project.path,
|
||||
is_default=project.is_default or False,
|
||||
)
|
||||
for project in projects
|
||||
]
|
||||
|
||||
return ProjectList(
|
||||
projects=project_items,
|
||||
default_project=default_project,
|
||||
)
|
||||
|
||||
|
||||
# Add a new project
|
||||
@project_resource_router.post("/projects", response_model=ProjectStatusResponse)
|
||||
async def add_project(
|
||||
project_data: ProjectInfoRequest,
|
||||
project_service: ProjectServiceDep,
|
||||
) -> ProjectStatusResponse:
|
||||
"""Add a new project to configuration and database.
|
||||
|
||||
Args:
|
||||
project_data: The project name and path, with option to set as default
|
||||
|
||||
Returns:
|
||||
Response confirming the project was added
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
await project_service.add_project(
|
||||
project_data.name, project_data.path, set_default=project_data.set_default
|
||||
)
|
||||
|
||||
return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
|
||||
message=f"Project '{project_data.name}' added successfully",
|
||||
status="success",
|
||||
default=project_data.set_default,
|
||||
new_project=ProjectItem(
|
||||
name=project_data.name, path=project_data.path, is_default=project_data.set_default
|
||||
),
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# Remove a project
|
||||
@project_resource_router.delete("/{name}", response_model=ProjectStatusResponse)
|
||||
async def remove_project(
|
||||
project_service: ProjectServiceDep,
|
||||
name: str = Path(..., description="Name of the project to remove"),
|
||||
) -> ProjectStatusResponse:
|
||||
"""Remove a project from configuration and database.
|
||||
|
||||
Args:
|
||||
name: The name of the project to remove
|
||||
|
||||
Returns:
|
||||
Response confirming the project was removed
|
||||
"""
|
||||
try:
|
||||
old_project = await project_service.get_project(name)
|
||||
if not old_project: # pragma: no cover
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Project: '{name}' does not exist"
|
||||
) # pragma: no cover
|
||||
|
||||
await project_service.remove_project(name)
|
||||
|
||||
return ProjectStatusResponse(
|
||||
message=f"Project '{name}' removed successfully",
|
||||
status="success",
|
||||
default=False,
|
||||
old_project=ProjectItem(name=old_project.name, path=old_project.path),
|
||||
new_project=None,
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# Set a project as default
|
||||
@project_resource_router.put("/{name}/default", response_model=ProjectStatusResponse)
|
||||
async def set_default_project(
|
||||
project_service: ProjectServiceDep,
|
||||
name: str = Path(..., description="Name of the project to set as default"),
|
||||
) -> ProjectStatusResponse:
|
||||
"""Set a project as the default project.
|
||||
|
||||
Args:
|
||||
name: The name of the project to set as default
|
||||
|
||||
Returns:
|
||||
Response confirming the project was set as default
|
||||
"""
|
||||
try:
|
||||
# Get the old default project
|
||||
default_name = project_service.default_project
|
||||
default_project = await project_service.get_project(default_name)
|
||||
if not default_project: # pragma: no cover
|
||||
raise HTTPException( # pragma: no cover
|
||||
status_code=404, detail=f"Default Project: '{default_name}' does not exist"
|
||||
)
|
||||
|
||||
# get the new project
|
||||
new_default_project = await project_service.get_project(name)
|
||||
if not new_default_project: # pragma: no cover
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Project: '{name}' does not exist"
|
||||
) # pragma: no cover
|
||||
|
||||
await project_service.set_default_project(name)
|
||||
|
||||
return ProjectStatusResponse(
|
||||
message=f"Project '{name}' set as default successfully",
|
||||
status="success",
|
||||
default=True,
|
||||
old_project=ProjectItem(name=default_name, path=default_project.path),
|
||||
new_project=ProjectItem(
|
||||
name=name,
|
||||
path=new_default_project.path,
|
||||
is_default=True,
|
||||
),
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# Synchronize projects between config and database
|
||||
@project_resource_router.post("/sync", response_model=ProjectStatusResponse)
|
||||
async def synchronize_projects(
|
||||
project_service: ProjectServiceDep,
|
||||
) -> ProjectStatusResponse:
|
||||
"""Synchronize projects between configuration file and database.
|
||||
|
||||
Ensures that all projects in the configuration file exist in the database
|
||||
and vice versa.
|
||||
|
||||
Returns:
|
||||
Response confirming synchronization was completed
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
await project_service.synchronize_projects()
|
||||
|
||||
return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
|
||||
message="Projects synchronized successfully between configuration and database",
|
||||
status="success",
|
||||
default=False,
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Router for prompt-related operations.
|
||||
|
||||
This router is responsible for rendering various prompts using Handlebars templates.
|
||||
It centralizes all prompt formatting logic that was previously in the MCP prompts.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.api.routers.utils import to_graph_context, to_search_results
|
||||
from basic_memory.api.template_loader import template_loader
|
||||
from basic_memory.schemas.base import parse_timeframe
|
||||
from basic_memory.deps import (
|
||||
ContextServiceDep,
|
||||
EntityRepositoryDep,
|
||||
SearchServiceDep,
|
||||
EntityServiceDep,
|
||||
)
|
||||
from basic_memory.schemas.prompt import (
|
||||
ContinueConversationRequest,
|
||||
SearchPromptRequest,
|
||||
PromptResponse,
|
||||
PromptMetadata,
|
||||
)
|
||||
from basic_memory.schemas.search import SearchItemType, SearchQuery
|
||||
|
||||
router = APIRouter(prefix="/prompt", tags=["prompt"])
|
||||
|
||||
|
||||
@router.post("/continue-conversation", response_model=PromptResponse)
|
||||
async def continue_conversation(
|
||||
search_service: SearchServiceDep,
|
||||
entity_service: EntityServiceDep,
|
||||
context_service: ContextServiceDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
request: ContinueConversationRequest,
|
||||
) -> PromptResponse:
|
||||
"""Generate a prompt for continuing a conversation.
|
||||
|
||||
This endpoint takes a topic and/or timeframe and generates a prompt with
|
||||
relevant context from the knowledge base.
|
||||
|
||||
Args:
|
||||
request: The request parameters
|
||||
|
||||
Returns:
|
||||
Formatted continuation prompt with context
|
||||
"""
|
||||
logger.info(
|
||||
f"Generating continue conversation prompt, topic: {request.topic}, timeframe: {request.timeframe}"
|
||||
)
|
||||
|
||||
since = parse_timeframe(request.timeframe) if request.timeframe else None
|
||||
|
||||
# Initialize search results
|
||||
search_results = []
|
||||
|
||||
# Get data needed for template
|
||||
if request.topic:
|
||||
query = SearchQuery(text=request.topic, after_date=request.timeframe)
|
||||
results = await search_service.search(query, limit=request.search_items_limit)
|
||||
search_results = await to_search_results(entity_service, results)
|
||||
|
||||
# Build context from results
|
||||
all_hierarchical_results = []
|
||||
for result in search_results:
|
||||
if hasattr(result, "permalink") and result.permalink:
|
||||
# Get hierarchical context using the new dataclass-based approach
|
||||
context_result = await context_service.build_context(
|
||||
result.permalink,
|
||||
depth=request.depth,
|
||||
since=since,
|
||||
max_related=request.related_items_limit,
|
||||
include_observations=True, # Include observations for entities
|
||||
)
|
||||
|
||||
# Process results into the schema format
|
||||
graph_context = await to_graph_context(
|
||||
context_result, entity_repository=entity_repository
|
||||
)
|
||||
|
||||
# Add results to our collection (limit to top results for each permalink)
|
||||
if graph_context.results:
|
||||
all_hierarchical_results.extend(graph_context.results[:3])
|
||||
|
||||
# Limit to a reasonable number of total results
|
||||
all_hierarchical_results = all_hierarchical_results[:10]
|
||||
|
||||
template_context = {
|
||||
"topic": request.topic,
|
||||
"timeframe": request.timeframe,
|
||||
"hierarchical_results": all_hierarchical_results,
|
||||
"has_results": len(all_hierarchical_results) > 0,
|
||||
}
|
||||
else:
|
||||
# If no topic, get recent activity
|
||||
context_result = await context_service.build_context(
|
||||
types=[SearchItemType.ENTITY],
|
||||
depth=request.depth,
|
||||
since=since,
|
||||
max_related=request.related_items_limit,
|
||||
include_observations=True,
|
||||
)
|
||||
recent_context = await to_graph_context(context_result, entity_repository=entity_repository)
|
||||
|
||||
hierarchical_results = recent_context.results[:5] # Limit to top 5 recent items
|
||||
|
||||
template_context = {
|
||||
"topic": f"Recent Activity from ({request.timeframe})",
|
||||
"timeframe": request.timeframe,
|
||||
"hierarchical_results": hierarchical_results,
|
||||
"has_results": len(hierarchical_results) > 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# Render template
|
||||
rendered_prompt = await template_loader.render(
|
||||
"prompts/continue_conversation.hbs", template_context
|
||||
)
|
||||
|
||||
# Calculate metadata
|
||||
# Count items of different types
|
||||
observation_count = 0
|
||||
relation_count = 0
|
||||
entity_count = 0
|
||||
|
||||
# Get the hierarchical results from the template context
|
||||
hierarchical_results_for_count = template_context.get("hierarchical_results", [])
|
||||
|
||||
# For topic-based search
|
||||
if request.topic:
|
||||
for item in hierarchical_results_for_count:
|
||||
if hasattr(item, "observations"):
|
||||
observation_count += len(item.observations) if item.observations else 0
|
||||
|
||||
if hasattr(item, "related_results"):
|
||||
for related in item.related_results or []:
|
||||
if hasattr(related, "type"):
|
||||
if related.type == "relation":
|
||||
relation_count += 1
|
||||
elif related.type == "entity": # pragma: no cover
|
||||
entity_count += 1 # pragma: no cover
|
||||
# For recent activity
|
||||
else:
|
||||
for item in hierarchical_results_for_count:
|
||||
if hasattr(item, "observations"):
|
||||
observation_count += len(item.observations) if item.observations else 0
|
||||
|
||||
if hasattr(item, "related_results"):
|
||||
for related in item.related_results or []:
|
||||
if hasattr(related, "type"):
|
||||
if related.type == "relation":
|
||||
relation_count += 1
|
||||
elif related.type == "entity": # pragma: no cover
|
||||
entity_count += 1 # pragma: no cover
|
||||
|
||||
# Build metadata
|
||||
metadata = {
|
||||
"query": request.topic,
|
||||
"timeframe": request.timeframe,
|
||||
"search_count": len(search_results)
|
||||
if request.topic
|
||||
else 0, # Original search results count
|
||||
"context_count": len(hierarchical_results_for_count),
|
||||
"observation_count": observation_count,
|
||||
"relation_count": relation_count,
|
||||
"total_items": (
|
||||
len(hierarchical_results_for_count)
|
||||
+ observation_count
|
||||
+ relation_count
|
||||
+ entity_count
|
||||
),
|
||||
"search_limit": request.search_items_limit,
|
||||
"context_depth": request.depth,
|
||||
"related_limit": request.related_items_limit,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
prompt_metadata = PromptMetadata(**metadata)
|
||||
|
||||
return PromptResponse(
|
||||
prompt=rendered_prompt, context=template_context, metadata=prompt_metadata
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error rendering continue conversation template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error rendering prompt template: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/search", response_model=PromptResponse)
|
||||
async def search_prompt(
|
||||
search_service: SearchServiceDep,
|
||||
entity_service: EntityServiceDep,
|
||||
request: SearchPromptRequest,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> PromptResponse:
|
||||
"""Generate a prompt for search results.
|
||||
|
||||
This endpoint takes a search query and formats the results into a helpful
|
||||
prompt with context and suggestions.
|
||||
|
||||
Args:
|
||||
request: The search parameters
|
||||
page: The page number for pagination
|
||||
page_size: The number of results per page, defaults to 10
|
||||
|
||||
Returns:
|
||||
Formatted search results prompt with context
|
||||
"""
|
||||
logger.info(f"Generating search prompt, query: {request.query}, timeframe: {request.timeframe}")
|
||||
|
||||
limit = page_size
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
query = SearchQuery(text=request.query, after_date=request.timeframe)
|
||||
results = await search_service.search(query, limit=limit, offset=offset)
|
||||
search_results = await to_search_results(entity_service, results)
|
||||
|
||||
template_context = {
|
||||
"query": request.query,
|
||||
"timeframe": request.timeframe,
|
||||
"results": search_results,
|
||||
"has_results": len(search_results) > 0,
|
||||
"result_count": len(search_results),
|
||||
}
|
||||
|
||||
try:
|
||||
# Render template
|
||||
rendered_prompt = await template_loader.render("prompts/search.hbs", template_context)
|
||||
|
||||
# Build metadata
|
||||
metadata = {
|
||||
"query": request.query,
|
||||
"timeframe": request.timeframe,
|
||||
"search_count": len(search_results),
|
||||
"context_count": len(search_results),
|
||||
"observation_count": 0, # Search results don't include observations
|
||||
"relation_count": 0, # Search results don't include relations
|
||||
"total_items": len(search_results),
|
||||
"search_limit": limit,
|
||||
"context_depth": 0, # No context depth for basic search
|
||||
"related_limit": 0, # No related items for basic search
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
prompt_metadata = PromptMetadata(**metadata)
|
||||
|
||||
return PromptResponse(
|
||||
prompt=rendered_prompt, context=template_context, metadata=prompt_metadata
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error rendering search template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error rendering prompt template: {str(e)}",
|
||||
)
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, Body
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.deps import (
|
||||
@@ -13,10 +14,13 @@ from basic_memory.deps import (
|
||||
SearchServiceDep,
|
||||
EntityServiceDep,
|
||||
FileServiceDep,
|
||||
EntityRepositoryDep,
|
||||
)
|
||||
from basic_memory.repository.search_repository import SearchIndexRow
|
||||
from basic_memory.schemas.memory import normalize_memory_url
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
||||
from basic_memory.models.knowledge import Entity as EntityModel
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/resource", tags=["resources"])
|
||||
|
||||
@@ -94,8 +98,7 @@ async def get_resource_content(
|
||||
content = await file_service.read_entity_content(result)
|
||||
memory_url = normalize_memory_url(result.permalink)
|
||||
modified_date = result.updated_at.isoformat()
|
||||
assert result.checksum
|
||||
checksum = result.checksum[:8]
|
||||
checksum = result.checksum[:8] if result.checksum else ""
|
||||
|
||||
# Prepare the delimited content
|
||||
response_content = f"--- {memory_url} {modified_date} {checksum}\n"
|
||||
@@ -122,3 +125,101 @@ def cleanup_temp_file(file_path: str):
|
||||
logger.debug(f"Temporary file deleted: {file_path}")
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Error deleting temporary file {file_path}: {e}")
|
||||
|
||||
|
||||
@router.put("/{file_path:path}")
|
||||
async def write_resource(
|
||||
config: ProjectConfigDep,
|
||||
file_service: FileServiceDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
search_service: SearchServiceDep,
|
||||
file_path: str,
|
||||
content: Annotated[str, Body()],
|
||||
) -> JSONResponse:
|
||||
"""Write content to a file in the project.
|
||||
|
||||
This endpoint allows writing content directly to a file in the project.
|
||||
Also creates an entity record and indexes the file for search.
|
||||
|
||||
Args:
|
||||
file_path: Path to write to, relative to project root
|
||||
request: Contains the content to write
|
||||
|
||||
Returns:
|
||||
JSON response with file information
|
||||
"""
|
||||
try:
|
||||
# Get content from request body
|
||||
|
||||
# Ensure it's UTF-8 string content
|
||||
if isinstance(content, bytes): # pragma: no cover
|
||||
content_str = content.decode("utf-8")
|
||||
else:
|
||||
content_str = str(content)
|
||||
|
||||
# Get full file path
|
||||
full_path = Path(f"{config.home}/{file_path}")
|
||||
|
||||
# Ensure parent directory exists
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write content to file
|
||||
checksum = await file_service.write_file(full_path, content_str)
|
||||
|
||||
# Get file info
|
||||
file_stats = file_service.file_stats(full_path)
|
||||
|
||||
# Determine file details
|
||||
file_name = Path(file_path).name
|
||||
content_type = file_service.content_type(full_path)
|
||||
|
||||
entity_type = "canvas" if file_path.endswith(".canvas") else "file"
|
||||
|
||||
# Check if entity already exists
|
||||
existing_entity = await entity_repository.get_by_file_path(file_path)
|
||||
|
||||
if existing_entity:
|
||||
# Update existing entity
|
||||
entity = await entity_repository.update(
|
||||
existing_entity.id,
|
||||
{
|
||||
"title": file_name,
|
||||
"entity_type": entity_type,
|
||||
"content_type": content_type,
|
||||
"file_path": file_path,
|
||||
"checksum": checksum,
|
||||
"updated_at": datetime.fromtimestamp(file_stats.st_mtime),
|
||||
},
|
||||
)
|
||||
status_code = 200
|
||||
else:
|
||||
# Create a new entity model
|
||||
entity = EntityModel(
|
||||
title=file_name,
|
||||
entity_type=entity_type,
|
||||
content_type=content_type,
|
||||
file_path=file_path,
|
||||
checksum=checksum,
|
||||
created_at=datetime.fromtimestamp(file_stats.st_ctime),
|
||||
updated_at=datetime.fromtimestamp(file_stats.st_mtime),
|
||||
)
|
||||
entity = await entity_repository.add(entity)
|
||||
status_code = 201
|
||||
|
||||
# Index the file for search
|
||||
await search_service.index_entity(entity) # pyright: ignore
|
||||
|
||||
# Return success response
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"file_path": file_path,
|
||||
"checksum": checksum,
|
||||
"size": file_stats.st_size,
|
||||
"created_at": file_stats.st_ctime,
|
||||
"modified_at": file_stats.st_mtime,
|
||||
},
|
||||
)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Error writing resource {file_path}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to write resource: {str(e)}")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Router for search operations."""
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks
|
||||
|
||||
from basic_memory.schemas.search import SearchQuery, SearchResult, SearchResponse
|
||||
from basic_memory.deps import SearchServiceDep
|
||||
from basic_memory.api.routers.utils import to_search_results
|
||||
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
||||
from basic_memory.deps import SearchServiceDep, EntityServiceDep
|
||||
|
||||
router = APIRouter(prefix="/search", tags=["search"])
|
||||
|
||||
@@ -14,6 +13,7 @@ router = APIRouter(prefix="/search", tags=["search"])
|
||||
async def search(
|
||||
query: SearchQuery,
|
||||
search_service: SearchServiceDep,
|
||||
entity_service: EntityServiceDep,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
@@ -21,7 +21,7 @@ async def search(
|
||||
limit = page_size
|
||||
offset = (page - 1) * page_size
|
||||
results = await search_service.search(query, limit=limit, offset=offset)
|
||||
search_results = [SearchResult.model_validate(asdict(r)) for r in results]
|
||||
search_results = await to_search_results(entity_service, results)
|
||||
return SearchResponse(
|
||||
results=search_results,
|
||||
current_page=page,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.repository.search_repository import SearchIndexRow
|
||||
from basic_memory.schemas.memory import (
|
||||
EntitySummary,
|
||||
ObservationSummary,
|
||||
RelationSummary,
|
||||
MemoryMetadata,
|
||||
GraphContext,
|
||||
ContextResult,
|
||||
)
|
||||
from basic_memory.schemas.search import SearchItemType, SearchResult
|
||||
from basic_memory.services import EntityService
|
||||
from basic_memory.services.context_service import (
|
||||
ContextResultRow,
|
||||
ContextResult as ServiceContextResult,
|
||||
)
|
||||
|
||||
|
||||
async def to_graph_context(
|
||||
context_result: ServiceContextResult,
|
||||
entity_repository: EntityRepository,
|
||||
page: Optional[int] = None,
|
||||
page_size: Optional[int] = None,
|
||||
):
|
||||
# Helper function to convert items to summaries
|
||||
async def to_summary(item: SearchIndexRow | ContextResultRow):
|
||||
match item.type:
|
||||
case SearchItemType.ENTITY:
|
||||
return EntitySummary(
|
||||
title=item.title, # pyright: ignore
|
||||
permalink=item.permalink,
|
||||
content=item.content,
|
||||
file_path=item.file_path,
|
||||
created_at=item.created_at,
|
||||
)
|
||||
case SearchItemType.OBSERVATION:
|
||||
return ObservationSummary(
|
||||
title=item.title, # pyright: ignore
|
||||
file_path=item.file_path,
|
||||
category=item.category, # pyright: ignore
|
||||
content=item.content, # pyright: ignore
|
||||
permalink=item.permalink, # pyright: ignore
|
||||
created_at=item.created_at,
|
||||
)
|
||||
case SearchItemType.RELATION:
|
||||
from_entity = await entity_repository.find_by_id(item.from_id) # pyright: ignore
|
||||
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
|
||||
return RelationSummary(
|
||||
title=item.title, # pyright: ignore
|
||||
file_path=item.file_path,
|
||||
permalink=item.permalink, # pyright: ignore
|
||||
relation_type=item.relation_type, # 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,
|
||||
)
|
||||
case _: # pragma: no cover
|
||||
raise ValueError(f"Unexpected type: {item.type}")
|
||||
|
||||
# Process the hierarchical results
|
||||
hierarchical_results = []
|
||||
for context_item in context_result.results:
|
||||
# Process primary result
|
||||
primary_result = await to_summary(context_item.primary_result)
|
||||
|
||||
# Process observations
|
||||
observations = []
|
||||
for obs in context_item.observations:
|
||||
observations.append(await to_summary(obs))
|
||||
|
||||
# Process related results
|
||||
related = []
|
||||
for rel in context_item.related_results:
|
||||
related.append(await to_summary(rel))
|
||||
|
||||
# Add to hierarchical results
|
||||
hierarchical_results.append(
|
||||
ContextResult(
|
||||
primary_result=primary_result,
|
||||
observations=observations,
|
||||
related_results=related,
|
||||
)
|
||||
)
|
||||
|
||||
# Create schema metadata from service metadata
|
||||
metadata = MemoryMetadata(
|
||||
uri=context_result.metadata.uri,
|
||||
types=context_result.metadata.types,
|
||||
depth=context_result.metadata.depth,
|
||||
timeframe=context_result.metadata.timeframe,
|
||||
generated_at=context_result.metadata.generated_at,
|
||||
primary_count=context_result.metadata.primary_count,
|
||||
related_count=context_result.metadata.related_count,
|
||||
total_results=context_result.metadata.primary_count + context_result.metadata.related_count,
|
||||
total_relations=context_result.metadata.total_relations,
|
||||
total_observations=context_result.metadata.total_observations,
|
||||
)
|
||||
|
||||
# Return new GraphContext with just hierarchical results
|
||||
return GraphContext(
|
||||
results=hierarchical_results,
|
||||
metadata=metadata,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
async def to_search_results(entity_service: EntityService, results: List[SearchIndexRow]):
|
||||
search_results = []
|
||||
for r in results:
|
||||
entities = await entity_service.get_entities_by_id([r.entity_id, r.from_id, r.to_id]) # pyright: ignore
|
||||
search_results.append(
|
||||
SearchResult(
|
||||
title=r.title, # pyright: ignore
|
||||
type=r.type, # pyright: ignore
|
||||
permalink=r.permalink,
|
||||
score=r.score, # pyright: ignore
|
||||
entity=entities[0].permalink if entities else None,
|
||||
content=r.content,
|
||||
file_path=r.file_path,
|
||||
metadata=r.metadata,
|
||||
category=r.category,
|
||||
from_entity=entities[0].permalink if entities else None,
|
||||
to_entity=entities[1].permalink if len(entities) > 1 else None,
|
||||
relation_type=r.relation_type,
|
||||
)
|
||||
)
|
||||
return search_results
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Template loading and rendering utilities for the Basic Memory API.
|
||||
|
||||
This module handles the loading and rendering of Handlebars templates from the
|
||||
templates directory, providing a consistent interface for all prompt-related
|
||||
formatting needs.
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from pathlib import Path
|
||||
import json
|
||||
import datetime
|
||||
|
||||
import pybars
|
||||
from loguru import logger
|
||||
|
||||
# Get the base path of the templates directory
|
||||
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
|
||||
|
||||
|
||||
# Custom helpers for Handlebars
|
||||
def _date_helper(this, *args):
|
||||
"""Format a date using the given format string."""
|
||||
if len(args) < 1: # pragma: no cover
|
||||
return ""
|
||||
|
||||
timestamp = args[0]
|
||||
format_str = args[1] if len(args) > 1 else "%Y-%m-%d %H:%M"
|
||||
|
||||
if hasattr(timestamp, "strftime"):
|
||||
result = timestamp.strftime(format_str)
|
||||
elif isinstance(timestamp, str):
|
||||
try:
|
||||
dt = datetime.datetime.fromisoformat(timestamp)
|
||||
result = dt.strftime(format_str)
|
||||
except ValueError:
|
||||
result = timestamp
|
||||
else:
|
||||
result = str(timestamp) # pragma: no cover
|
||||
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _default_helper(this, *args):
|
||||
"""Return a default value if the given value is None or empty."""
|
||||
if len(args) < 2: # pragma: no cover
|
||||
return ""
|
||||
|
||||
value = args[0]
|
||||
default_value = args[1]
|
||||
|
||||
result = default_value if value is None or value == "" else value
|
||||
# Use strlist for consistent handling of HTML escaping
|
||||
return pybars.strlist([str(result)])
|
||||
|
||||
|
||||
def _capitalize_helper(this, *args):
|
||||
"""Capitalize the first letter of a string."""
|
||||
if len(args) < 1: # pragma: no cover
|
||||
return ""
|
||||
|
||||
text = args[0]
|
||||
if not text or not isinstance(text, str): # pragma: no cover
|
||||
result = ""
|
||||
else:
|
||||
result = text.capitalize()
|
||||
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _round_helper(this, *args):
|
||||
"""Round a number to the specified number of decimal places."""
|
||||
if len(args) < 1:
|
||||
return ""
|
||||
|
||||
value = args[0]
|
||||
decimal_places = args[1] if len(args) > 1 else 2
|
||||
|
||||
try:
|
||||
result = str(round(float(value), int(decimal_places)))
|
||||
except (ValueError, TypeError):
|
||||
result = str(value)
|
||||
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _size_helper(this, *args):
|
||||
"""Return the size/length of a collection."""
|
||||
if len(args) < 1:
|
||||
return 0
|
||||
|
||||
value = args[0]
|
||||
if value is None:
|
||||
result = "0"
|
||||
elif isinstance(value, (list, tuple, dict, str)):
|
||||
result = str(len(value)) # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
result = "0"
|
||||
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _json_helper(this, *args):
|
||||
"""Convert a value to a JSON string."""
|
||||
if len(args) < 1: # pragma: no cover
|
||||
return "{}"
|
||||
|
||||
value = args[0]
|
||||
# For pybars, we need to return a SafeString to prevent HTML escaping
|
||||
result = json.dumps(value) # pragma: no cover
|
||||
# Safe string implementation to prevent HTML escaping
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _math_helper(this, *args):
|
||||
"""Perform basic math operations."""
|
||||
if len(args) < 3:
|
||||
return pybars.strlist(["Math error: Insufficient arguments"])
|
||||
|
||||
lhs = args[0]
|
||||
operator = args[1]
|
||||
rhs = args[2]
|
||||
|
||||
try:
|
||||
lhs = float(lhs)
|
||||
rhs = float(rhs)
|
||||
if operator == "+":
|
||||
result = str(lhs + rhs)
|
||||
elif operator == "-":
|
||||
result = str(lhs - rhs)
|
||||
elif operator == "*":
|
||||
result = str(lhs * rhs)
|
||||
elif operator == "/":
|
||||
result = str(lhs / rhs)
|
||||
else:
|
||||
result = f"Unsupported operator: {operator}"
|
||||
except (ValueError, TypeError) as e:
|
||||
result = f"Math error: {e}"
|
||||
|
||||
return pybars.strlist([result])
|
||||
|
||||
|
||||
def _lt_helper(this, *args):
|
||||
"""Check if left hand side is less than right hand side."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
|
||||
lhs = args[0]
|
||||
rhs = args[1]
|
||||
|
||||
try:
|
||||
return float(lhs) < float(rhs)
|
||||
except (ValueError, TypeError):
|
||||
# Fall back to string comparison for non-numeric values
|
||||
return str(lhs) < str(rhs)
|
||||
|
||||
|
||||
def _if_cond_helper(this, options, condition):
|
||||
"""Block helper for custom if conditionals."""
|
||||
if condition:
|
||||
return options["fn"](this)
|
||||
elif "inverse" in options:
|
||||
return options["inverse"](this)
|
||||
return "" # pragma: no cover
|
||||
|
||||
|
||||
def _dedent_helper(this, options):
|
||||
"""Dedent a block of text to remove common leading whitespace.
|
||||
|
||||
Usage:
|
||||
{{#dedent}}
|
||||
This text will have its
|
||||
common leading whitespace removed
|
||||
while preserving relative indentation.
|
||||
{{/dedent}}
|
||||
"""
|
||||
if "fn" not in options: # pragma: no cover
|
||||
return ""
|
||||
|
||||
# Get the content from the block
|
||||
content = options["fn"](this)
|
||||
|
||||
# Convert to string if it's a strlist
|
||||
if (
|
||||
isinstance(content, list)
|
||||
or hasattr(content, "__iter__")
|
||||
and not isinstance(content, (str, bytes))
|
||||
):
|
||||
content_str = "".join(str(item) for item in content) # pragma: no cover
|
||||
else:
|
||||
content_str = str(content) # pragma: no cover
|
||||
|
||||
# Add trailing and leading newlines to ensure proper dedenting
|
||||
# This is critical for textwrap.dedent to work correctly with mixed content
|
||||
content_str = "\n" + content_str + "\n"
|
||||
|
||||
# Use textwrap to dedent the content and remove the extra newlines we added
|
||||
dedented = textwrap.dedent(content_str)[1:-1]
|
||||
|
||||
# Return as a SafeString to prevent HTML escaping
|
||||
return pybars.strlist([dedented]) # pragma: no cover
|
||||
|
||||
|
||||
class TemplateLoader:
|
||||
"""Loader for Handlebars templates.
|
||||
|
||||
This class is responsible for loading templates from disk and rendering
|
||||
them with the provided context data.
|
||||
"""
|
||||
|
||||
def __init__(self, template_dir: Optional[str] = None):
|
||||
"""Initialize the template loader.
|
||||
|
||||
Args:
|
||||
template_dir: Optional custom template directory path
|
||||
"""
|
||||
self.template_dir = Path(template_dir) if template_dir else TEMPLATES_DIR
|
||||
self.template_cache: Dict[str, Callable] = {}
|
||||
self.compiler = pybars.Compiler()
|
||||
|
||||
# Set up standard helpers
|
||||
self.helpers = {
|
||||
"date": _date_helper,
|
||||
"default": _default_helper,
|
||||
"capitalize": _capitalize_helper,
|
||||
"round": _round_helper,
|
||||
"size": _size_helper,
|
||||
"json": _json_helper,
|
||||
"math": _math_helper,
|
||||
"lt": _lt_helper,
|
||||
"if_cond": _if_cond_helper,
|
||||
"dedent": _dedent_helper,
|
||||
}
|
||||
|
||||
logger.debug(f"Initialized template loader with directory: {self.template_dir}")
|
||||
|
||||
def get_template(self, template_path: str) -> Callable:
|
||||
"""Get a template by path, using cache if available.
|
||||
|
||||
Args:
|
||||
template_path: The path to the template, relative to the templates directory
|
||||
|
||||
Returns:
|
||||
The compiled Handlebars template
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the template doesn't exist
|
||||
"""
|
||||
if template_path in self.template_cache:
|
||||
return self.template_cache[template_path]
|
||||
|
||||
# Convert from Liquid-style path to Handlebars extension
|
||||
if template_path.endswith(".liquid"):
|
||||
template_path = template_path.replace(".liquid", ".hbs")
|
||||
elif not template_path.endswith(".hbs"):
|
||||
template_path = f"{template_path}.hbs"
|
||||
|
||||
full_path = self.template_dir / template_path
|
||||
|
||||
if not full_path.exists():
|
||||
raise FileNotFoundError(f"Template not found: {full_path}")
|
||||
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
template_str = f.read()
|
||||
|
||||
template = self.compiler.compile(template_str)
|
||||
self.template_cache[template_path] = template
|
||||
|
||||
logger.debug(f"Loaded template: {template_path}")
|
||||
return template
|
||||
|
||||
async def render(self, template_path: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template with the given context.
|
||||
|
||||
Args:
|
||||
template_path: The path to the template, relative to the templates directory
|
||||
context: The context data to pass to the template
|
||||
|
||||
Returns:
|
||||
The rendered template as a string
|
||||
"""
|
||||
template = self.get_template(template_path)
|
||||
return template(context, helpers=self.helpers)
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear the template cache."""
|
||||
self.template_cache.clear()
|
||||
logger.debug("Template cache cleared")
|
||||
|
||||
|
||||
# Global template loader instance
|
||||
template_loader = TemplateLoader()
|
||||
@@ -1,20 +1,73 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import config
|
||||
from basic_memory.utils import setup_logging
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
setup_logging(log_file=".basic-memory/basic-memory-cli.log", console=False) # pragma: no cover
|
||||
|
||||
asyncio.run(db.run_migrations(config))
|
||||
def version_callback(value: bool) -> None:
|
||||
"""Show version and exit."""
|
||||
if value: # pragma: no cover
|
||||
import basic_memory
|
||||
from basic_memory.config import config
|
||||
|
||||
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
|
||||
typer.echo(f"Current project: {config.project}")
|
||||
typer.echo(f"Project path: {config.home}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
app = typer.Typer(name="basic-memory")
|
||||
|
||||
import_app = typer.Typer()
|
||||
app.add_typer(import_app, name="import")
|
||||
|
||||
@app.callback()
|
||||
def app_callback(
|
||||
ctx: typer.Context,
|
||||
project: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--project",
|
||||
"-p",
|
||||
help="Specify which project to use 1",
|
||||
envvar="BASIC_MEMORY_PROJECT",
|
||||
),
|
||||
version: Optional[bool] = typer.Option(
|
||||
None,
|
||||
"--version",
|
||||
"-v",
|
||||
help="Show version and exit.",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
),
|
||||
) -> None:
|
||||
"""Basic Memory - Local-first personal knowledge management."""
|
||||
|
||||
# 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
|
||||
|
||||
ensure_initialization(app_config)
|
||||
|
||||
# Initialize MCP session with the specified project or default
|
||||
if project: # pragma: no cover
|
||||
# Use the project specified via --project flag
|
||||
current_project_config = get_project_config(project)
|
||||
session.set_current_project(current_project_config.name)
|
||||
|
||||
# Update the global config to use this project
|
||||
from basic_memory.config import update_current_project
|
||||
|
||||
update_current_project(project)
|
||||
else:
|
||||
# Use the default project
|
||||
current_project = app_config.default_project
|
||||
session.set_current_project(current_project)
|
||||
|
||||
|
||||
# Register sub-command groups
|
||||
import_app = typer.Typer(help="Import data from various sources")
|
||||
app.add_typer(import_app, name="import")
|
||||
|
||||
claude_app = typer.Typer()
|
||||
import_app.add_typer(claude_app, name="claude")
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
"""CLI commands for basic-memory."""
|
||||
|
||||
from . import status, sync, db, import_memory_json, mcp
|
||||
from . import auth, status, sync, db, import_memory_json, mcp, import_claude_conversations
|
||||
from . import import_claude_projects, import_chatgpt, tool, project
|
||||
|
||||
__all__ = ["status", "sync", "db", "import_memory_json", "mcp"]
|
||||
__all__ = [
|
||||
"auth",
|
||||
"status",
|
||||
"sync",
|
||||
"db",
|
||||
"import_memory_json",
|
||||
"mcp",
|
||||
"import_claude_conversations",
|
||||
"import_claude_projects",
|
||||
"import_chatgpt",
|
||||
"tool",
|
||||
"project",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""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()
|
||||
@@ -1,28 +1,44 @@
|
||||
"""Database management commands."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.alembic import migrations
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import app_config, config_manager
|
||||
|
||||
|
||||
@app.command()
|
||||
def reset(
|
||||
reindex: bool = typer.Option(False, "--reindex", help="Rebuild indices from filesystem"),
|
||||
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
||||
): # pragma: no cover
|
||||
"""Reset database (drop all tables and recreate)."""
|
||||
with logfire.span("reset"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
||||
logger.info("Resetting database...")
|
||||
asyncio.run(migrations.reset_database())
|
||||
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
||||
logger.info("Resetting database...")
|
||||
# Get database path
|
||||
db_path = app_config.app_database_path
|
||||
|
||||
if reindex:
|
||||
# Import and run sync
|
||||
from basic_memory.cli.commands.sync import sync
|
||||
# Delete the database file if it exists
|
||||
if db_path.exists():
|
||||
db_path.unlink()
|
||||
logger.info(f"Database file deleted: {db_path}")
|
||||
|
||||
logger.info("Rebuilding search index from filesystem...")
|
||||
sync(watch=False) # pyright: ignore
|
||||
# Reset project configuration
|
||||
config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
|
||||
config_manager.config.default_project = "main"
|
||||
config_manager.save_config(config_manager.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")
|
||||
|
||||
if reindex:
|
||||
# Import and run sync
|
||||
from basic_memory.cli.commands.sync import sync
|
||||
|
||||
logger.info("Rebuilding search index from filesystem...")
|
||||
sync(watch=False) # pyright: ignore
|
||||
|
||||
@@ -2,205 +2,21 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Annotated, Set, Optional
|
||||
from typing import Annotated
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.importers import ChatGPTImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def clean_filename(text: str) -> str:
|
||||
"""Convert text to safe filename."""
|
||||
clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
|
||||
return clean
|
||||
|
||||
|
||||
def format_timestamp(ts: float) -> str:
|
||||
"""Format Unix timestamp for display."""
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def get_message_content(message: Dict[str, Any]) -> str:
|
||||
"""Extract clean message content."""
|
||||
if not message or "content" not in message:
|
||||
return "" # pragma: no cover
|
||||
|
||||
content = message["content"]
|
||||
if content.get("content_type") == "text":
|
||||
return "\n".join(content.get("parts", []))
|
||||
elif content.get("content_type") == "code":
|
||||
return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
|
||||
return "" # pragma: no cover
|
||||
|
||||
|
||||
def traverse_messages(
|
||||
mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Traverse message tree and return messages in order."""
|
||||
messages = []
|
||||
node = mapping.get(root_id) if root_id else None
|
||||
|
||||
while node:
|
||||
if node["id"] not in seen and node.get("message"):
|
||||
seen.add(node["id"])
|
||||
messages.append(node["message"])
|
||||
|
||||
# Follow children
|
||||
children = node.get("children", [])
|
||||
for child_id in children:
|
||||
child_msgs = traverse_messages(mapping, child_id, seen)
|
||||
messages.extend(child_msgs)
|
||||
|
||||
break # Don't follow siblings
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def format_chat_markdown(
|
||||
title: str,
|
||||
mapping: Dict[str, Any],
|
||||
root_id: Optional[str],
|
||||
created_at: float,
|
||||
modified_at: float,
|
||||
) -> str:
|
||||
"""Format chat as clean markdown."""
|
||||
|
||||
# Start with title
|
||||
lines = [f"# {title}\n"]
|
||||
|
||||
# Traverse message tree
|
||||
seen_msgs = set()
|
||||
messages = traverse_messages(mapping, root_id, seen_msgs)
|
||||
|
||||
# Format each message
|
||||
for msg in messages:
|
||||
# Skip hidden messages
|
||||
if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
|
||||
continue
|
||||
|
||||
# Get author and timestamp
|
||||
author = msg["author"]["role"].title()
|
||||
ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
|
||||
|
||||
# Add message header
|
||||
lines.append(f"### {author} ({ts})")
|
||||
|
||||
# Add message content
|
||||
content = get_message_content(msg)
|
||||
if content:
|
||||
lines.append(content)
|
||||
|
||||
# Add spacing
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_chat_content(folder: str, conversation: Dict[str, Any]) -> EntityMarkdown:
|
||||
"""Convert chat conversation to Basic Memory entity."""
|
||||
|
||||
# Extract timestamps
|
||||
created_at = conversation["create_time"]
|
||||
modified_at = conversation["update_time"]
|
||||
|
||||
root_id = None
|
||||
# Find root message
|
||||
for node_id, node in conversation["mapping"].items():
|
||||
if node.get("parent") is None:
|
||||
root_id = node_id
|
||||
break
|
||||
|
||||
# Generate permalink
|
||||
date_prefix = datetime.fromtimestamp(created_at).strftime("%Y%m%d")
|
||||
clean_title = clean_filename(conversation["title"])
|
||||
|
||||
# Format content
|
||||
content = format_chat_markdown(
|
||||
title=conversation["title"],
|
||||
mapping=conversation["mapping"],
|
||||
root_id=root_id,
|
||||
created_at=created_at,
|
||||
modified_at=modified_at,
|
||||
)
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "conversation",
|
||||
"title": conversation["title"],
|
||||
"created": format_timestamp(created_at),
|
||||
"modified": format_timestamp(modified_at),
|
||||
"permalink": f"{folder}/{date_prefix}-{clean_title}",
|
||||
}
|
||||
),
|
||||
content=content,
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
async def process_chatgpt_json(
|
||||
json_path: Path, folder: str, markdown_processor: MarkdownProcessor
|
||||
) -> Dict[str, int]:
|
||||
"""Import conversations from ChatGPT JSON format."""
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading chat data...", total=None)
|
||||
|
||||
# Read conversations
|
||||
conversations = json.loads(json_path.read_text())
|
||||
progress.update(read_task, total=len(conversations))
|
||||
|
||||
# Process each conversation
|
||||
messages_imported = 0
|
||||
chats_imported = 0
|
||||
|
||||
for chat in conversations:
|
||||
# Convert to entity
|
||||
entity = format_chat_content(folder, chat)
|
||||
|
||||
# Write file
|
||||
file_path = config.home / f"{entity.frontmatter.metadata['permalink']}.md"
|
||||
# logger.info(f"Writing file: {file_path.absolute()}")
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
|
||||
# Count messages
|
||||
msg_count = sum(
|
||||
1
|
||||
for node in chat["mapping"].values()
|
||||
if node.get("message")
|
||||
and not node.get("message", {})
|
||||
.get("metadata", {})
|
||||
.get("is_visually_hidden_from_conversation")
|
||||
)
|
||||
|
||||
chats_imported += 1
|
||||
messages_imported += msg_count
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
return {"conversations": chats_imported, "messages": messages_imported}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
@@ -226,38 +42,41 @@ def import_chatgpt(
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
with logfire.span("import chatgpt"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
try:
|
||||
if conversations_json:
|
||||
if not conversations_json.exists():
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(
|
||||
f"\nImporting chats from {conversations_json}...writing to {base_path}"
|
||||
)
|
||||
results = asyncio.run(
|
||||
process_chatgpt_json(conversations_json, folder, markdown_processor)
|
||||
)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {results['conversations']} conversations\n"
|
||||
f"Containing {results['messages']} messages",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
try:
|
||||
if not conversations_json.exists(): # pragma: no cover
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
||||
|
||||
# Create importer and run import
|
||||
importer = ChatGPTImporter(config.home, markdown_processor)
|
||||
with conversations_json.open("r", encoding="utf-8") as file:
|
||||
json_data = json.load(file)
|
||||
result = asyncio.run(importer.import_data(json_data, folder))
|
||||
|
||||
if not result.success: # pragma: no cover
|
||||
typer.echo(f"Error during import: {result.error_message}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {result.conversations} conversations\n"
|
||||
f"Containing {result.messages} messages",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -2,158 +2,21 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Annotated
|
||||
from typing import Annotated
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def clean_filename(text: str) -> str:
|
||||
"""Convert text to safe filename."""
|
||||
# Remove invalid characters and convert spaces
|
||||
clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
|
||||
return clean
|
||||
|
||||
|
||||
def format_timestamp(ts: str) -> str:
|
||||
"""Format ISO timestamp for display."""
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def format_chat_markdown(
|
||||
name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str, permalink: str
|
||||
) -> str:
|
||||
"""Format chat as clean markdown."""
|
||||
|
||||
# Start with frontmatter and title
|
||||
lines = [
|
||||
f"# {name}\n",
|
||||
]
|
||||
|
||||
# Add messages
|
||||
for msg in messages:
|
||||
# Format timestamp
|
||||
ts = format_timestamp(msg["created_at"])
|
||||
|
||||
# Add message header
|
||||
lines.append(f"### {msg['sender'].title()} ({ts})")
|
||||
|
||||
# Handle message content
|
||||
content = msg.get("text", "")
|
||||
if msg.get("content"):
|
||||
content = " ".join(c.get("text", "") for c in msg["content"])
|
||||
lines.append(content)
|
||||
|
||||
# Handle attachments
|
||||
attachments = msg.get("attachments", [])
|
||||
for attachment in attachments:
|
||||
if "file_name" in attachment:
|
||||
lines.append(f"\n**Attachment: {attachment['file_name']}**")
|
||||
if "extracted_content" in attachment:
|
||||
lines.append("```")
|
||||
lines.append(attachment["extracted_content"])
|
||||
lines.append("```")
|
||||
|
||||
# Add spacing between messages
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_chat_content(
|
||||
base_path: Path, name: str, messages: List[Dict[str, Any]], created_at: str, modified_at: str
|
||||
) -> EntityMarkdown:
|
||||
"""Convert chat messages to Basic Memory entity format."""
|
||||
|
||||
# Generate permalink
|
||||
date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
|
||||
clean_title = clean_filename(name)
|
||||
permalink = f"{base_path}/{date_prefix}-{clean_title}"
|
||||
|
||||
# Format content
|
||||
content = format_chat_markdown(
|
||||
name=name,
|
||||
messages=messages,
|
||||
created_at=created_at,
|
||||
modified_at=modified_at,
|
||||
permalink=permalink,
|
||||
)
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "conversation",
|
||||
"title": name,
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": permalink,
|
||||
}
|
||||
),
|
||||
content=content,
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
async def process_conversations_json(
|
||||
json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
|
||||
) -> Dict[str, int]:
|
||||
"""Import chat data from conversations2.json format."""
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading chat data...", total=None)
|
||||
|
||||
# Read chat data - handle array of arrays format
|
||||
data = json.loads(json_path.read_text())
|
||||
conversations = [chat for chat in data]
|
||||
progress.update(read_task, total=len(conversations))
|
||||
|
||||
# Process each conversation
|
||||
messages_imported = 0
|
||||
chats_imported = 0
|
||||
|
||||
for chat in conversations:
|
||||
# Convert to entity
|
||||
entity = format_chat_content(
|
||||
base_path=base_path,
|
||||
name=chat["name"],
|
||||
messages=chat["chat_messages"],
|
||||
created_at=chat["created_at"],
|
||||
modified_at=chat["updated_at"],
|
||||
)
|
||||
|
||||
# Write file
|
||||
file_path = Path(f"{entity.frontmatter.metadata['permalink']}.md")
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
|
||||
chats_imported += 1
|
||||
messages_imported += len(chat["chat_messages"])
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
return {"conversations": chats_imported, "messages": messages_imported}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
@@ -179,35 +42,43 @@ def import_claude(
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
with logfire.span("import claude conversations"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
try:
|
||||
if not conversations_json.exists():
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
||||
results = asyncio.run(
|
||||
process_conversations_json(conversations_json, base_path, markdown_processor)
|
||||
)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {results['conversations']} conversations\n"
|
||||
f"Containing {results['messages']} messages",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
try:
|
||||
if not conversations_json.exists():
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Create the importer
|
||||
importer = ClaudeConversationsImporter(config.home, markdown_processor)
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
||||
|
||||
# Run the import
|
||||
with conversations_json.open("r", encoding="utf-8") as file:
|
||||
json_data = json.load(file)
|
||||
result = asyncio.run(importer.import_data(json_data, folder))
|
||||
|
||||
if not result.success: # pragma: no cover
|
||||
typer.echo(f"Error during import: {result.error_message}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {result.conversations} conversations\n"
|
||||
f"Containing {result.messages} messages",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -3,140 +3,20 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def clean_filename(text: str) -> str:
|
||||
"""Convert text to safe filename."""
|
||||
clean = "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-")
|
||||
return clean
|
||||
|
||||
|
||||
def format_project_markdown(project: Dict[str, Any], doc: Dict[str, Any]) -> EntityMarkdown:
|
||||
"""Format a project document as a Basic Memory entity."""
|
||||
|
||||
# Extract timestamps
|
||||
created_at = doc.get("created_at") or project["created_at"]
|
||||
modified_at = project["updated_at"]
|
||||
|
||||
# Generate clean names for organization
|
||||
project_dir = clean_filename(project["name"])
|
||||
doc_file = clean_filename(doc["filename"])
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "project_doc",
|
||||
"title": doc["filename"],
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": f"{project_dir}/docs/{doc_file}",
|
||||
"project_name": project["name"],
|
||||
"project_uuid": project["uuid"],
|
||||
"doc_uuid": doc["uuid"],
|
||||
}
|
||||
),
|
||||
content=doc["content"],
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
def format_prompt_markdown(project: Dict[str, Any]) -> Optional[EntityMarkdown]:
|
||||
"""Format project prompt template as a Basic Memory entity."""
|
||||
|
||||
if not project.get("prompt_template"):
|
||||
return None
|
||||
|
||||
# Extract timestamps
|
||||
created_at = project["created_at"]
|
||||
modified_at = project["updated_at"]
|
||||
|
||||
# Generate clean project directory name
|
||||
project_dir = clean_filename(project["name"])
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "prompt_template",
|
||||
"title": f"Prompt Template: {project['name']}",
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": f"{project_dir}/prompt-template",
|
||||
"project_name": project["name"],
|
||||
"project_uuid": project["uuid"],
|
||||
}
|
||||
),
|
||||
content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
async def process_projects_json(
|
||||
json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
|
||||
) -> Dict[str, int]:
|
||||
"""Import project data from Claude.ai projects.json format."""
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading project data...", total=None)
|
||||
|
||||
# Read project data
|
||||
data = json.loads(json_path.read_text())
|
||||
progress.update(read_task, total=len(data))
|
||||
|
||||
# Track import counts
|
||||
docs_imported = 0
|
||||
prompts_imported = 0
|
||||
|
||||
# Process each project
|
||||
for project in data:
|
||||
project_dir = clean_filename(project["name"])
|
||||
|
||||
# Create project directories
|
||||
docs_dir = base_path / project_dir / "docs"
|
||||
docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Import prompt template if it exists
|
||||
if prompt_entity := format_prompt_markdown(project):
|
||||
file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
|
||||
await markdown_processor.write_file(file_path, prompt_entity)
|
||||
prompts_imported += 1
|
||||
|
||||
# Import project documents
|
||||
for doc in project.get("docs", []):
|
||||
entity = format_project_markdown(project, doc)
|
||||
file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
docs_imported += 1
|
||||
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
return {"documents": docs_imported, "prompts": prompts_imported}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
@@ -161,36 +41,43 @@ def import_projects(
|
||||
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
with logfire.span("import claude projects"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
try:
|
||||
if projects_json:
|
||||
if not projects_json.exists():
|
||||
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / base_folder if base_folder else config.home
|
||||
console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
|
||||
results = asyncio.run(
|
||||
process_projects_json(projects_json, base_path, markdown_processor)
|
||||
)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {results['documents']} project documents\n"
|
||||
f"Imported {results['prompts']} prompt templates",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
try:
|
||||
if not projects_json.exists():
|
||||
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Create the importer
|
||||
importer = ClaudeProjectsImporter(config.home, markdown_processor)
|
||||
|
||||
# Process the file
|
||||
base_path = config.home / base_folder if base_folder else config.home
|
||||
console.print(f"\nImporting projects from {projects_json}...writing to {base_path}")
|
||||
|
||||
# Run the import
|
||||
with projects_json.open("r", encoding="utf-8") as file:
|
||||
json_data = json.load(file)
|
||||
result = asyncio.run(importer.import_data(json_data, base_folder))
|
||||
|
||||
if not result.success: # pragma: no cover
|
||||
typer.echo(f"Error during import: {result.error_message}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Imported {result.documents} project documents\n"
|
||||
f"Imported {result.prompts} prompt templates",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -3,95 +3,20 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Annotated
|
||||
from typing import Annotated
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Observation, Relation
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
async def process_memory_json(
|
||||
json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
|
||||
):
|
||||
"""Import entities from memory.json using markdown processor."""
|
||||
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading memory.json...", total=None)
|
||||
|
||||
# First pass - collect entities and relations
|
||||
with open(json_path) as f:
|
||||
lines = f.readlines()
|
||||
progress.update(read_task, total=len(lines))
|
||||
|
||||
for line in lines:
|
||||
data = json.loads(line)
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data.get("from") or data.get("from_id")
|
||||
if source not in entity_relations:
|
||||
entity_relations[source] = []
|
||||
entity_relations[source].append(
|
||||
Relation(
|
||||
type=data.get("relationType") or data.get("relation_type"),
|
||||
target=data.get("to") or data.get("to_id"),
|
||||
)
|
||||
)
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
# Second pass - create and write entities
|
||||
write_task = progress.add_task("Creating entities...", total=len(entities))
|
||||
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}",
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[Observation(content=obs) for obs in entity_data["observations"]],
|
||||
relations=entity_relations.get(
|
||||
name, []
|
||||
), # Add any relations where this entity is the source
|
||||
)
|
||||
|
||||
# Let markdown processor handle writing
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
entities_created += 1
|
||||
progress.update(write_task, advance=1)
|
||||
|
||||
return {
|
||||
"entities": entities_created,
|
||||
"relations": sum(len(rels) for rels in entity_relations.values()),
|
||||
}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
@@ -103,6 +28,9 @@ def memory_json(
|
||||
json_path: Annotated[Path, typer.Argument(..., help="Path to memory.json file")] = Path(
|
||||
"memory.json"
|
||||
),
|
||||
destination_folder: Annotated[
|
||||
str, typer.Option(help="Optional destination folder within the project")
|
||||
] = "",
|
||||
):
|
||||
"""Import entities and relations from a memory.json file.
|
||||
|
||||
@@ -114,33 +42,46 @@ def memory_json(
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
with logfire.span("import memory_json"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
if not json_path.exists():
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
if not json_path.exists():
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Create the importer
|
||||
importer = MemoryJsonImporter(config.home, markdown_processor)
|
||||
|
||||
# Process the file
|
||||
base_path = config.home if not destination_folder else config.home / destination_folder
|
||||
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
||||
|
||||
# Run the import for json log format
|
||||
file_data = []
|
||||
with json_path.open("r", encoding="utf-8") as file:
|
||||
for line in file:
|
||||
json_data = json.loads(line)
|
||||
file_data.append(json_data)
|
||||
result = asyncio.run(importer.import_data(file_data, destination_folder))
|
||||
|
||||
if not result.success: # pragma: no cover
|
||||
typer.echo(f"Error during import: {result.error_message}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home
|
||||
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
||||
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
|
||||
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {results['entities']} entities\n"
|
||||
f"Added {results['relations']} relations",
|
||||
expand=False,
|
||||
)
|
||||
# Show results
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {result.entities} entities\n"
|
||||
f"Added {result.relations} relations",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
logger.error("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""MCP server command."""
|
||||
"""MCP server command with streamable HTTP transport."""
|
||||
|
||||
import asyncio
|
||||
import typer
|
||||
|
||||
from loguru import logger
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
|
||||
# Import mcp instance
|
||||
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
||||
@@ -10,11 +11,79 @@ from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
||||
# Import mcp tools to register them
|
||||
import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
||||
|
||||
# Import prompts to register them
|
||||
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@app.command()
|
||||
def mcp(): # pragma: no cover
|
||||
"""Run the MCP server for Claude Desktop integration."""
|
||||
home_dir = config.home
|
||||
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
||||
logger.info(f"Home directory: {home_dir}")
|
||||
mcp_server.run()
|
||||
def mcp(
|
||||
transport: str = typer.Option("stdio", help="Transport type: stdio, streamable-http, or sse"),
|
||||
host: str = typer.Option(
|
||||
"0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
|
||||
),
|
||||
port: int = typer.Option(8000, help="Port for HTTP transports"),
|
||||
path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
|
||||
): # pragma: no cover
|
||||
"""Run the MCP server with configurable transport options.
|
||||
|
||||
This command starts an MCP server using one of three transport options:
|
||||
|
||||
- stdio: Standard I/O (good for local usage)
|
||||
- streamable-http: Recommended for web deployments (default)
|
||||
- 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
|
||||
|
||||
def run_file_sync():
|
||||
"""Run file sync in a separate thread with its own event loop."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(initialize_file_sync(app_config))
|
||||
except Exception as e:
|
||||
logger.error(f"File sync error: {e}", err=True)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
||||
if app_config.sync_changes:
|
||||
# Start the sync thread
|
||||
sync_thread = threading.Thread(target=run_file_sync, daemon=True)
|
||||
sync_thread.start()
|
||||
logger.info("Started file sync in background")
|
||||
|
||||
# Now run the MCP server (blocks)
|
||||
logger.info(f"Starting MCP server with {transport.upper()} transport")
|
||||
|
||||
if transport == "stdio":
|
||||
mcp_server.run(
|
||||
transport=transport,
|
||||
)
|
||||
elif transport == "streamable-http" or transport == "sse":
|
||||
mcp_server.run(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level="INFO",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
"""Command module for basic-memory project management."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.mcp.project_session import session
|
||||
from basic_memory.mcp.resources.project_info import project_info
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.tree import Tree
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.schemas.project_info import ProjectList
|
||||
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.utils import generate_permalink
|
||||
|
||||
console = Console()
|
||||
|
||||
# Create a project subcommand
|
||||
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
|
||||
app.add_typer(project_app, name="project")
|
||||
|
||||
|
||||
def format_path(path: str) -> str:
|
||||
"""Format a path for display, using ~ for home directory."""
|
||||
home = str(Path.home())
|
||||
if path.startswith(home):
|
||||
return path.replace(home, "~", 1) # pragma: no cover
|
||||
return path
|
||||
|
||||
|
||||
@project_app.command("list")
|
||||
def list_projects() -> None:
|
||||
"""List all configured projects."""
|
||||
# Use API to list projects
|
||||
try:
|
||||
response = asyncio.run(call_get(client, "/projects/projects"))
|
||||
result = ProjectList.model_validate(response.json())
|
||||
|
||||
table = Table(title="Basic Memory Projects")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Path", style="green")
|
||||
table.add_column("Default", style="yellow")
|
||||
table.add_column("Active", style="magenta")
|
||||
|
||||
for project in result.projects:
|
||||
is_default = "✓" if project.is_default else ""
|
||||
is_active = "✓" if session.get_current_project() == project.name else ""
|
||||
table.add_row(project.name, format_path(project.path), is_default, is_active)
|
||||
|
||||
console.print(table)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error listing projects: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@project_app.command("add")
|
||||
def add_project(
|
||||
name: str = typer.Argument(..., help="Name of the project"),
|
||||
path: str = typer.Argument(..., help="Path to the project directory"),
|
||||
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
|
||||
) -> None:
|
||||
"""Add a new project."""
|
||||
# Resolve to absolute path
|
||||
resolved_path = os.path.abspath(os.path.expanduser(path))
|
||||
|
||||
try:
|
||||
data = {"name": name, "path": resolved_path, "set_default": set_default}
|
||||
|
||||
response = asyncio.run(call_post(client, "/projects/projects", json=data))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error adding project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Display usage hint
|
||||
console.print("\nTo use this project:")
|
||||
console.print(f" basic-memory --project={name} <command>")
|
||||
console.print(" # or")
|
||||
console.print(f" basic-memory project default {name}")
|
||||
|
||||
|
||||
@project_app.command("remove")
|
||||
def remove_project(
|
||||
name: str = typer.Argument(..., help="Name of the project to remove"),
|
||||
) -> None:
|
||||
"""Remove a project from configuration."""
|
||||
try:
|
||||
project_name = generate_permalink(name)
|
||||
response = asyncio.run(call_delete(client, f"/projects/{project_name}"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error removing project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Show this message regardless of method used
|
||||
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
|
||||
|
||||
|
||||
@project_app.command("default")
|
||||
def set_default_project(
|
||||
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
||||
) -> None:
|
||||
"""Set the default project and activate it for the current session."""
|
||||
try:
|
||||
project_name = generate_permalink(name)
|
||||
|
||||
response = asyncio.run(call_put(client, f"/projects/{project_name}/default"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error setting default project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# 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]")
|
||||
|
||||
|
||||
@project_app.command("sync-config")
|
||||
def synchronize_projects() -> None:
|
||||
"""Synchronize project config between configuration file and database."""
|
||||
# Call the API to synchronize projects
|
||||
|
||||
try:
|
||||
response = asyncio.run(call_post(client, "/projects/sync"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
except Exception as e: # pragma: no cover
|
||||
console.print(f"[red]Error synchronizing projects: {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"),
|
||||
):
|
||||
"""Display detailed information and statistics about the current project."""
|
||||
try:
|
||||
# Get project info
|
||||
info = asyncio.run(project_info.fn()) # type: ignore # pyright: ignore [reportAttributeAccessIssue]
|
||||
|
||||
if json_output:
|
||||
# Convert to JSON and print
|
||||
print(json.dumps(info.model_dump(), indent=2, default=str))
|
||||
else:
|
||||
# Create rich display
|
||||
console = Console()
|
||||
|
||||
# Project configuration section
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Project:[/bold] {info.project_name}\n"
|
||||
f"[bold]Path:[/bold] {info.project_path}\n"
|
||||
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
||||
title="📊 Basic Memory Project Info",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Statistics section
|
||||
stats_table = Table(title="📈 Statistics")
|
||||
stats_table.add_column("Metric", style="cyan")
|
||||
stats_table.add_column("Count", style="green")
|
||||
|
||||
stats_table.add_row("Entities", str(info.statistics.total_entities))
|
||||
stats_table.add_row("Observations", str(info.statistics.total_observations))
|
||||
stats_table.add_row("Relations", str(info.statistics.total_relations))
|
||||
stats_table.add_row(
|
||||
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
|
||||
)
|
||||
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
|
||||
|
||||
console.print(stats_table)
|
||||
|
||||
# Entity types
|
||||
if info.statistics.entity_types:
|
||||
entity_types_table = Table(title="📑 Entity Types")
|
||||
entity_types_table.add_column("Type", style="blue")
|
||||
entity_types_table.add_column("Count", style="green")
|
||||
|
||||
for entity_type, count in info.statistics.entity_types.items():
|
||||
entity_types_table.add_row(entity_type, str(count))
|
||||
|
||||
console.print(entity_types_table)
|
||||
|
||||
# Most connected entities
|
||||
if info.statistics.most_connected_entities: # pragma: no cover
|
||||
connected_table = Table(title="🔗 Most Connected Entities")
|
||||
connected_table.add_column("Title", style="blue")
|
||||
connected_table.add_column("Permalink", style="cyan")
|
||||
connected_table.add_column("Relations", style="green")
|
||||
|
||||
for entity in info.statistics.most_connected_entities:
|
||||
connected_table.add_row(
|
||||
entity["title"], entity["permalink"], str(entity["relation_count"])
|
||||
)
|
||||
|
||||
console.print(connected_table)
|
||||
|
||||
# Recent activity
|
||||
if info.activity.recently_updated: # pragma: no cover
|
||||
recent_table = Table(title="🕒 Recent Activity")
|
||||
recent_table.add_column("Title", style="blue")
|
||||
recent_table.add_column("Type", style="cyan")
|
||||
recent_table.add_column("Last Updated", style="green")
|
||||
|
||||
for entity in info.activity.recently_updated[:5]: # Show top 5
|
||||
updated_at = (
|
||||
datetime.fromisoformat(entity["updated_at"])
|
||||
if isinstance(entity["updated_at"], str)
|
||||
else entity["updated_at"]
|
||||
)
|
||||
recent_table.add_row(
|
||||
entity["title"],
|
||||
entity["entity_type"],
|
||||
updated_at.strftime("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
|
||||
console.print(recent_table)
|
||||
|
||||
# System status
|
||||
system_tree = Tree("🖥️ System Status")
|
||||
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
|
||||
system_tree.add(
|
||||
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
|
||||
)
|
||||
|
||||
# Watch status
|
||||
if info.system.watch_status: # pragma: no cover
|
||||
watch_branch = system_tree.add("Watch Service")
|
||||
running = info.system.watch_status.get("running", False)
|
||||
status_color = "green" if running else "red"
|
||||
watch_branch.add(
|
||||
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
|
||||
)
|
||||
|
||||
if running:
|
||||
start_time = (
|
||||
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
|
||||
if isinstance(info.system.watch_status.get("start_time"), str)
|
||||
else info.system.watch_status.get("start_time")
|
||||
)
|
||||
watch_branch.add(
|
||||
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
|
||||
)
|
||||
watch_branch.add(
|
||||
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
|
||||
)
|
||||
watch_branch.add(
|
||||
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
|
||||
)
|
||||
else:
|
||||
system_tree.add("[yellow]Watch service not running[/yellow]")
|
||||
|
||||
console.print(system_tree)
|
||||
|
||||
# Available projects
|
||||
projects_table = Table(title="📁 Available Projects")
|
||||
projects_table.add_column("Name", style="blue")
|
||||
projects_table.add_column("Path", style="cyan")
|
||||
projects_table.add_column("Default", style="green")
|
||||
|
||||
for name, proj_info in info.available_projects.items():
|
||||
is_default = name == info.default_project
|
||||
project_path = proj_info["path"]
|
||||
projects_table.add_row(name, project_path, "✓" if is_default else "")
|
||||
|
||||
console.print(projects_table)
|
||||
|
||||
# Timestamp
|
||||
current_time = (
|
||||
datetime.fromisoformat(str(info.system.timestamp))
|
||||
if isinstance(info.system.timestamp, str)
|
||||
else info.system.timestamp
|
||||
)
|
||||
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
typer.echo(f"Error getting project info: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
from typing import Set, Dict
|
||||
|
||||
import logfire
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
@@ -12,27 +11,15 @@ 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.db import DatabaseType
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.sync import FileChangeScanner
|
||||
from basic_memory.sync.utils import SyncReport
|
||||
from basic_memory.cli.commands.sync import get_sync_service
|
||||
from basic_memory.config import config, app_config
|
||||
from basic_memory.repository import ProjectRepository
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
|
||||
# Create rich console
|
||||
console = Console()
|
||||
|
||||
|
||||
async def get_file_change_scanner(
|
||||
db_type=DatabaseType.FILESYSTEM,
|
||||
) -> FileChangeScanner: # pragma: no cover
|
||||
"""Get sync service instance."""
|
||||
_, session_maker = await db.get_or_create_db(db_path=config.database_path, db_type=db_type)
|
||||
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
return file_change_scanner
|
||||
|
||||
|
||||
def add_files_to_tree(
|
||||
tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
|
||||
):
|
||||
@@ -100,11 +87,11 @@ def build_directory_summary(counts: Dict[str, int]) -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
||||
def display_changes(project_name: str, title: str, changes: SyncReport, verbose: bool = False):
|
||||
"""Display changes using Rich for better visualization."""
|
||||
tree = Tree(title)
|
||||
tree = Tree(f"{project_name}: {title}")
|
||||
|
||||
if changes.total_changes == 0:
|
||||
if changes.total == 0:
|
||||
tree.add("No changes")
|
||||
console.print(Panel(tree, expand=False))
|
||||
return
|
||||
@@ -135,11 +122,21 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
|
||||
console.print(Panel(tree, expand=False))
|
||||
|
||||
|
||||
async def run_status(sync_service: FileChangeScanner, verbose: bool = False):
|
||||
async def run_status(verbose: bool = False): # pragma: no cover
|
||||
"""Check sync status of files vs database."""
|
||||
# Check knowledge/ directory
|
||||
knowledge_changes = await sync_service.find_knowledge_changes(config.home)
|
||||
display_changes("Knowledge Files", knowledge_changes, verbose)
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
project = await project_repository.get_by_name(config.project)
|
||||
if not project: # pragma: no cover
|
||||
raise Exception(f"Project '{config.project}' not found")
|
||||
|
||||
sync_service = await get_sync_service(project)
|
||||
knowledge_changes = await sync_service.scan(config.home)
|
||||
display_changes(project.name, "Status", knowledge_changes, verbose)
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -147,10 +144,9 @@ def status(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
|
||||
):
|
||||
"""Show sync status between files and database."""
|
||||
with logfire.span("status"): # pyright: ignore [reportGeneralTypeIssues]
|
||||
try:
|
||||
sync_service = asyncio.run(get_file_change_scanner())
|
||||
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
|
||||
except Exception as e:
|
||||
logger.exception(f"Error checking status: {e}")
|
||||
raise typer.Exit(code=1) # pragma: no cover
|
||||
try:
|
||||
asyncio.run(run_status(verbose)) # pragma: no cover
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking status: {e}")
|
||||
typer.echo(f"Error checking status: {e}", err=True)
|
||||
raise typer.Exit(code=1) # pragma: no cover
|
||||
|
||||
@@ -16,18 +16,20 @@ from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.models import Project
|
||||
from basic_memory.repository import (
|
||||
EntityRepository,
|
||||
ObservationRepository,
|
||||
RelationRepository,
|
||||
ProjectRepository,
|
||||
)
|
||||
from basic_memory.repository.search_repository import SearchRepository
|
||||
from basic_memory.services import EntityService, FileService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import SyncService, FileChangeScanner
|
||||
from basic_memory.sync.utils import SyncReport
|
||||
from basic_memory.sync.watch_service import WatchService
|
||||
from basic_memory.sync import SyncService
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
from basic_memory.config import app_config
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -38,29 +40,27 @@ class ValidationIssue:
|
||||
error: str
|
||||
|
||||
|
||||
async def get_sync_service(): # pragma: no cover
|
||||
async def get_sync_service(project: Project) -> SyncService: # pragma: no cover
|
||||
"""Get sync service instance with all dependencies."""
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
|
||||
entity_parser = EntityParser(config.home)
|
||||
project_path = Path(project.path)
|
||||
entity_parser = EntityParser(project_path)
|
||||
markdown_processor = MarkdownProcessor(entity_parser)
|
||||
file_service = FileService(config.home, markdown_processor)
|
||||
file_service = FileService(project_path, markdown_processor)
|
||||
|
||||
# Initialize repositories
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
observation_repository = ObservationRepository(session_maker)
|
||||
relation_repository = RelationRepository(session_maker)
|
||||
search_repository = SearchRepository(session_maker)
|
||||
entity_repository = EntityRepository(session_maker, project_id=project.id)
|
||||
observation_repository = ObservationRepository(session_maker, project_id=project.id)
|
||||
relation_repository = RelationRepository(session_maker, project_id=project.id)
|
||||
search_repository = SearchRepository(session_maker, project_id=project.id)
|
||||
|
||||
# Initialize services
|
||||
search_service = SearchService(search_repository, entity_repository, file_service)
|
||||
link_resolver = LinkResolver(entity_repository, search_service)
|
||||
|
||||
# Initialize scanner
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
|
||||
# Initialize services
|
||||
entity_service = EntityService(
|
||||
entity_parser,
|
||||
@@ -73,12 +73,13 @@ async def get_sync_service(): # pragma: no cover
|
||||
|
||||
# Create sync service
|
||||
sync_service = SyncService(
|
||||
scanner=file_change_scanner,
|
||||
app_config=app_config,
|
||||
entity_service=entity_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
search_service=search_service,
|
||||
file_service=file_service,
|
||||
)
|
||||
|
||||
return sync_service
|
||||
@@ -95,9 +96,11 @@ 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."""
|
||||
total_changes = knowledge.total_changes
|
||||
total_changes = knowledge.total
|
||||
project_name = config.project
|
||||
|
||||
if total_changes == 0:
|
||||
console.print("[green]Everything up to date[/green]")
|
||||
console.print(f"[green]Project '{project_name}': Everything up to date[/green]")
|
||||
return
|
||||
|
||||
# Format as: "Synced X files (A new, B modified, C moved, D deleted)"
|
||||
@@ -116,18 +119,20 @@ def display_sync_summary(knowledge: SyncReport):
|
||||
if del_count:
|
||||
changes.append(f"[red]{del_count} deleted[/red]")
|
||||
|
||||
console.print(f"Synced {total_changes} files ({', '.join(changes)})")
|
||||
console.print(f"Project '{project_name}': Synced {total_changes} files ({', '.join(changes)})")
|
||||
|
||||
|
||||
def display_detailed_sync_results(knowledge: SyncReport):
|
||||
"""Display detailed sync results with trees."""
|
||||
if knowledge.total_changes == 0:
|
||||
console.print("\n[green]Everything up to date[/green]")
|
||||
project_name = config.project
|
||||
|
||||
if knowledge.total == 0:
|
||||
console.print(f"\n[green]Project '{project_name}': Everything up to date[/green]")
|
||||
return
|
||||
|
||||
console.print("\n[bold]Sync Results[/bold]")
|
||||
console.print(f"\n[bold]Sync Results for Project '{project_name}'[/bold]")
|
||||
|
||||
if knowledge.total_changes > 0:
|
||||
if knowledge.total > 0:
|
||||
knowledge_tree = Tree("[bold]Knowledge Files[/bold]")
|
||||
if knowledge.new:
|
||||
created = knowledge_tree.add("[green]Created[/green]")
|
||||
@@ -151,28 +156,50 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
||||
console.print(knowledge_tree)
|
||||
|
||||
|
||||
async def run_sync(verbose: bool = False, watch: bool = False, console_status: bool = False):
|
||||
async def run_sync(verbose: bool = False):
|
||||
"""Run sync operation."""
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
project = await project_repository.get_by_name(config.project)
|
||||
if not project: # pragma: no cover
|
||||
raise Exception(f"Project '{config.project}' not found")
|
||||
|
||||
sync_service = await get_sync_service()
|
||||
import time
|
||||
|
||||
# Start watching if requested
|
||||
if watch:
|
||||
watch_service = WatchService(
|
||||
sync_service=sync_service,
|
||||
file_service=sync_service.entity_service.file_service,
|
||||
config=config,
|
||||
)
|
||||
await watch_service.handle_changes(config.home)
|
||||
await watch_service.run(console_status=console_status) # pragma: no cover
|
||||
start_time = time.time()
|
||||
|
||||
logger.info(
|
||||
"Sync command started",
|
||||
project=config.project,
|
||||
verbose=verbose,
|
||||
directory=str(config.home),
|
||||
)
|
||||
|
||||
sync_service = await get_sync_service(project)
|
||||
|
||||
logger.info("Running one-time sync")
|
||||
knowledge_changes = await sync_service.sync(config.home, project_name=project.name)
|
||||
|
||||
# Log results
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
logger.info(
|
||||
"Sync command completed",
|
||||
project=config.project,
|
||||
total_changes=knowledge_changes.total,
|
||||
new_files=len(knowledge_changes.new),
|
||||
modified_files=len(knowledge_changes.modified),
|
||||
deleted_files=len(knowledge_changes.deleted),
|
||||
moved_files=len(knowledge_changes.moves),
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
|
||||
# Display results
|
||||
if verbose:
|
||||
display_detailed_sync_results(knowledge_changes)
|
||||
else:
|
||||
# one time sync
|
||||
knowledge_changes = await sync_service.sync(config.home)
|
||||
# Display results
|
||||
if verbose:
|
||||
display_detailed_sync_results(knowledge_changes)
|
||||
else:
|
||||
display_sync_summary(knowledge_changes) # pragma: no cover
|
||||
display_sync_summary(knowledge_changes) # pragma: no cover
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -183,24 +210,25 @@ def sync(
|
||||
"-v",
|
||||
help="Show detailed sync information.",
|
||||
),
|
||||
watch: bool = typer.Option(
|
||||
False,
|
||||
"--watch",
|
||||
"-w",
|
||||
help="Start watching for changes after sync.",
|
||||
),
|
||||
console_status: bool = typer.Option(
|
||||
False, "--console-status", "-c", help="Show live console status"
|
||||
),
|
||||
) -> None:
|
||||
"""Sync knowledge files with the database."""
|
||||
try:
|
||||
# Show which project we're syncing
|
||||
typer.echo(f"Syncing project: {config.project}")
|
||||
typer.echo(f"Project path: {config.home}")
|
||||
|
||||
# Run sync
|
||||
asyncio.run(run_sync(verbose=verbose, watch=watch, console_status=console_status))
|
||||
asyncio.run(run_sync(verbose=verbose))
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
logger.exception("Sync failed")
|
||||
logger.exception(
|
||||
"Sync command failed",
|
||||
f"project={config.project},"
|
||||
f"error={str(e)},"
|
||||
f"error_type={type(e).__name__},"
|
||||
f"directory={str(config.home)}",
|
||||
)
|
||||
typer.echo(f"Error during sync: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
"""CLI tool commands for Basic Memory."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich import print as rprint
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
|
||||
# Import prompts
|
||||
from basic_memory.mcp.prompts.continue_conversation import (
|
||||
continue_conversation as mcp_continue_conversation,
|
||||
)
|
||||
from basic_memory.mcp.prompts.recent_activity import (
|
||||
recent_activity_prompt as recent_activity_prompt,
|
||||
)
|
||||
from basic_memory.mcp.tools import build_context as mcp_build_context
|
||||
from basic_memory.mcp.tools import read_note as mcp_read_note
|
||||
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
||||
from basic_memory.mcp.tools import search_notes as mcp_search
|
||||
from basic_memory.mcp.tools import write_note as mcp_write_note
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.memory import MemoryUrl
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
|
||||
tool_app = typer.Typer()
|
||||
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def write_note(
|
||||
title: Annotated[str, typer.Option(help="The title of the note")],
|
||||
folder: Annotated[str, typer.Option(help="The folder to create the note in")],
|
||||
content: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
help="The content of the note. If not provided, content will be read from stdin. This allows piping content from other commands, e.g.: cat file.md | basic-memory tools write-note"
|
||||
),
|
||||
] = None,
|
||||
tags: Annotated[
|
||||
Optional[List[str]], typer.Option(help="A list of tags to apply to the note")
|
||||
] = None,
|
||||
):
|
||||
"""Create or update a markdown note. Content can be provided as an argument or read from stdin.
|
||||
|
||||
Content can be provided in two ways:
|
||||
1. Using the --content parameter
|
||||
2. Piping content through stdin (if --content is not provided)
|
||||
|
||||
Examples:
|
||||
|
||||
# Using content parameter
|
||||
basic-memory tools write-note --title "My Note" --folder "notes" --content "Note content"
|
||||
|
||||
# Using stdin pipe
|
||||
echo "# My Note Content" | basic-memory tools write-note --title "My Note" --folder "notes"
|
||||
|
||||
# Using heredoc
|
||||
cat << EOF | basic-memory tools write-note --title "My Note" --folder "notes"
|
||||
# My Document
|
||||
|
||||
This is my document content.
|
||||
|
||||
- Point 1
|
||||
- Point 2
|
||||
EOF
|
||||
|
||||
# Reading from a file
|
||||
cat document.md | basic-memory tools write-note --title "Document" --folder "docs"
|
||||
"""
|
||||
try:
|
||||
# If content is not provided, read from stdin
|
||||
if content is None:
|
||||
# Check if we're getting data from a pipe or redirect
|
||||
if not sys.stdin.isatty():
|
||||
content = sys.stdin.read()
|
||||
else: # pragma: no cover
|
||||
# If stdin is a terminal (no pipe/redirect), inform the user
|
||||
typer.echo(
|
||||
"No content provided. Please provide content via --content or by piping to stdin.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Also check for empty content
|
||||
if content is not None and not content.strip():
|
||||
typer.echo("Empty content provided. Please provide non-empty content.", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
note = asyncio.run(mcp_write_note.fn(title, content, folder, tags))
|
||||
rprint(note)
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during write_note: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def read_note(identifier: str, page: int = 1, page_size: int = 10):
|
||||
"""Read a markdown note from the knowledge base."""
|
||||
try:
|
||||
note = asyncio.run(mcp_read_note.fn(identifier, page, page_size))
|
||||
rprint(note)
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during read_note: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def build_context(
|
||||
url: MemoryUrl,
|
||||
depth: Optional[int] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
"""Get context needed to continue a discussion."""
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_build_context.fn(
|
||||
url=url,
|
||||
depth=depth,
|
||||
timeframe=timeframe,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
max_related=max_related,
|
||||
)
|
||||
)
|
||||
# Use json module for more controlled serialization
|
||||
import json
|
||||
|
||||
context_dict = context.model_dump(exclude_none=True)
|
||||
print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during build_context: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def recent_activity(
|
||||
type: Annotated[Optional[List[SearchItemType]], typer.Option()] = None,
|
||||
depth: Optional[int] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
"""Get recent activity across the knowledge base."""
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_recent_activity.fn(
|
||||
type=type, # pyright: ignore [reportArgumentType]
|
||||
depth=depth,
|
||||
timeframe=timeframe,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
max_related=max_related,
|
||||
)
|
||||
)
|
||||
# Use json module for more controlled serialization
|
||||
import json
|
||||
|
||||
context_dict = context.model_dump(exclude_none=True)
|
||||
print(json.dumps(context_dict, indent=2, ensure_ascii=True, default=str))
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during build_context: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command("search-notes")
|
||||
def search_notes(
|
||||
query: str,
|
||||
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
|
||||
title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
|
||||
after_date: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--after_date", help="Search results after date, eg. '2d', '1 week'"),
|
||||
] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
"""Search across all content in the knowledge base."""
|
||||
if permalink and title: # pragma: no cover
|
||||
print("Cannot search both permalink and title")
|
||||
raise typer.Abort()
|
||||
|
||||
try:
|
||||
if permalink and title: # pragma: no cover
|
||||
typer.echo(
|
||||
"Use either --permalink or --title, not both. Exiting.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# set search type
|
||||
search_type = ("permalink" if permalink else None,)
|
||||
search_type = ("permalink_match" if permalink and "*" in query else None,)
|
||||
search_type = ("title" if title else None,)
|
||||
search_type = "text" if search_type is None else search_type
|
||||
|
||||
results = asyncio.run(
|
||||
mcp_search.fn(
|
||||
query,
|
||||
search_type=search_type,
|
||||
page=page,
|
||||
after_date=after_date,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
# Use json module for more controlled serialization
|
||||
import json
|
||||
|
||||
results_dict = results.model_dump(exclude_none=True)
|
||||
print(json.dumps(results_dict, indent=2, ensure_ascii=True, default=str))
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
logger.exception("Error during search", e)
|
||||
typer.echo(f"Error during search: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command(name="continue-conversation")
|
||||
def continue_conversation(
|
||||
topic: Annotated[Optional[str], typer.Option(help="Topic or keyword to search for")] = None,
|
||||
timeframe: Annotated[
|
||||
Optional[str], typer.Option(help="How far back to look for activity")
|
||||
] = None,
|
||||
):
|
||||
"""Prompt to continue a previous conversation or work session."""
|
||||
try:
|
||||
# Prompt functions return formatted strings directly
|
||||
session = asyncio.run(mcp_continue_conversation.fn(topic=topic, timeframe=timeframe)) # type: ignore
|
||||
rprint(session)
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
logger.exception("Error continuing conversation", e)
|
||||
typer.echo(f"Error continuing conversation: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
# @tool_app.command(name="show-recent-activity")
|
||||
# def show_recent_activity(
|
||||
# timeframe: Annotated[
|
||||
# str, typer.Option(help="How far back to look for activity")
|
||||
# ] = "7d",
|
||||
# ):
|
||||
# """Prompt to show recent activity."""
|
||||
# try:
|
||||
# # Prompt functions return formatted strings directly
|
||||
# session = asyncio.run(recent_activity_prompt(timeframe=timeframe))
|
||||
# rprint(session)
|
||||
# except Exception as e: # pragma: no cover
|
||||
# if not isinstance(e, typer.Exit):
|
||||
# logger.exception("Error continuing conversation", e)
|
||||
# typer.echo(f"Error continuing conversation: {e}", err=True)
|
||||
# raise typer.Exit(1)
|
||||
# raise
|
||||
@@ -1,157 +0,0 @@
|
||||
"""Database management commands."""
|
||||
|
||||
import asyncio
|
||||
from typing import Optional, List, Annotated
|
||||
|
||||
import typer
|
||||
from rich import print as rprint
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.mcp.tools import build_context as mcp_build_context
|
||||
from basic_memory.mcp.tools import get_entity as mcp_get_entity
|
||||
from basic_memory.mcp.tools import read_note as mcp_read_note
|
||||
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
||||
from basic_memory.mcp.tools import search as mcp_search
|
||||
from basic_memory.mcp.tools import write_note as mcp_write_note
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.memory import MemoryUrl
|
||||
from basic_memory.schemas.search import SearchQuery
|
||||
|
||||
tool_app = typer.Typer()
|
||||
app.add_typer(tool_app, name="tools", help="cli versions mcp tools")
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def write_note(
|
||||
title: Annotated[str, typer.Option(help="The title of the note")],
|
||||
content: Annotated[str, typer.Option(help="The content of the note")],
|
||||
folder: Annotated[str, typer.Option(help="The folder to create the note in")],
|
||||
tags: Annotated[
|
||||
Optional[List[str]], typer.Option(help="A list of tags to apply to the note")
|
||||
] = None,
|
||||
):
|
||||
try:
|
||||
note = asyncio.run(mcp_write_note(title, content, folder, tags))
|
||||
rprint(note)
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during write_note: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def read_note(identifier: str, page: int = 1, page_size: int = 10):
|
||||
try:
|
||||
note = asyncio.run(mcp_read_note(identifier, page, page_size))
|
||||
rprint(note)
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during read_note: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def build_context(
|
||||
url: MemoryUrl,
|
||||
depth: Optional[int] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_build_context(
|
||||
url=url,
|
||||
depth=depth,
|
||||
timeframe=timeframe,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
max_related=max_related,
|
||||
)
|
||||
)
|
||||
rprint(context.model_dump())
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during build_context: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def recent_activity(
|
||||
type: Annotated[Optional[List[str]], typer.Option()] = ["entity", "observation", "relation"],
|
||||
depth: Optional[int] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
assert type is not None, "type is required"
|
||||
if any(t not in ["entity", "observation", "relation"] for t in type): # pragma: no cover
|
||||
print("type must be one of ['entity', 'observation', 'relation']")
|
||||
raise typer.Abort()
|
||||
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_recent_activity(
|
||||
type=type, # pyright: ignore [reportArgumentType]
|
||||
depth=depth,
|
||||
timeframe=timeframe,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
max_related=max_related,
|
||||
)
|
||||
)
|
||||
rprint(context.model_dump())
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during build_context: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def search(
|
||||
query: str,
|
||||
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
|
||||
title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
|
||||
after_date: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--after_date", help="Search results after date, eg. '2d', '1 week'"),
|
||||
] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
if permalink and title: # pragma: no cover
|
||||
print("Cannot search both permalink and title")
|
||||
raise typer.Abort()
|
||||
|
||||
try:
|
||||
search_query = SearchQuery(
|
||||
permalink_match=query if permalink else None,
|
||||
text=query if query else None,
|
||||
title=query if title else None,
|
||||
after_date=after_date,
|
||||
)
|
||||
results = asyncio.run(mcp_search(query=search_query, page=page, page_size=page_size))
|
||||
rprint(results.model_dump())
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during search: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def get_entity(identifier: str):
|
||||
try:
|
||||
entity = asyncio.run(mcp_get_entity(identifier=identifier))
|
||||
rprint(entity.model_dump())
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, typer.Exit):
|
||||
typer.echo(f"Error during get_entity: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
raise
|
||||
@@ -4,17 +4,19 @@ from basic_memory.cli.app import app # pragma: no cover
|
||||
|
||||
# Register commands
|
||||
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
||||
status,
|
||||
sync,
|
||||
auth,
|
||||
db,
|
||||
import_memory_json,
|
||||
mcp,
|
||||
import_chatgpt,
|
||||
import_claude_conversations,
|
||||
import_claude_projects,
|
||||
import_chatgpt,
|
||||
tools,
|
||||
import_memory_json,
|
||||
mcp,
|
||||
project,
|
||||
status,
|
||||
sync,
|
||||
tool,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# start the app
|
||||
app()
|
||||
|
||||
+315
-20
@@ -1,37 +1,78 @@
|
||||
"""Configuration management for basic-memory."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from typing import Any, Dict, Literal, Optional, List, Tuple
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
import basic_memory
|
||||
from basic_memory.utils import setup_logging, generate_permalink
|
||||
|
||||
|
||||
DATABASE_NAME = "memory.db"
|
||||
APP_DATABASE_NAME = "memory.db" # Using the same name but in the app directory
|
||||
DATA_DIR_NAME = ".basic-memory"
|
||||
CONFIG_FILE_NAME = "config.json"
|
||||
WATCH_STATUS_JSON = "watch-status.json"
|
||||
|
||||
Environment = Literal["test", "dev", "user"]
|
||||
|
||||
|
||||
class ProjectConfig(BaseSettings):
|
||||
@dataclass
|
||||
class ProjectConfig:
|
||||
"""Configuration for a specific basic-memory project."""
|
||||
|
||||
name: str
|
||||
home: Path
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def project_url(self) -> str: # pragma: no cover
|
||||
return f"/{generate_permalink(self.name)}"
|
||||
|
||||
|
||||
class BasicMemoryConfig(BaseSettings):
|
||||
"""Pydantic model for Basic Memory global configuration."""
|
||||
|
||||
env: Environment = Field(default="dev", description="Environment name")
|
||||
|
||||
# Default to ~/basic-memory but allow override with env var: BASIC_MEMORY_HOME
|
||||
home: Path = Field(
|
||||
default_factory=lambda: Path.home() / "basic-memory",
|
||||
description="Base path for basic-memory files",
|
||||
projects: Dict[str, str] = Field(
|
||||
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(
|
||||
default="main",
|
||||
description="Name of the default project to use",
|
||||
)
|
||||
|
||||
# Name of the project
|
||||
project: str = Field(default="default", description="Project name")
|
||||
# overridden by ~/.basic-memory/config.json
|
||||
log_level: str = "INFO"
|
||||
|
||||
# Watch service configuration
|
||||
sync_delay: int = Field(
|
||||
default=500, description="Milliseconds to wait after changes before syncing", gt=0
|
||||
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
|
||||
)
|
||||
|
||||
log_level: str = "INFO"
|
||||
# update permalinks on move
|
||||
update_permalinks_on_move: bool = Field(
|
||||
default=False,
|
||||
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
||||
)
|
||||
|
||||
sync_changes: bool = Field(
|
||||
default=True,
|
||||
description="Whether to sync changes in real time. default (True)",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BASIC_MEMORY_",
|
||||
@@ -40,23 +81,277 @@ class ProjectConfig(BaseSettings):
|
||||
env_file_encoding="utf-8",
|
||||
)
|
||||
|
||||
def get_project_path(self, project_name: Optional[str] = None) -> Path: # pragma: no cover
|
||||
"""Get the path for a specific project or the default project."""
|
||||
name = project_name or self.default_project
|
||||
|
||||
if name not in self.projects:
|
||||
raise ValueError(f"Project '{name}' not found in configuration")
|
||||
|
||||
return Path(self.projects[name])
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Ensure configuration is valid after initialization."""
|
||||
# Ensure main project exists
|
||||
if "main" not in self.projects: # pragma: no cover
|
||||
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
|
||||
self.default_project = "main"
|
||||
|
||||
@property
|
||||
def database_path(self) -> Path:
|
||||
"""Get SQLite database path."""
|
||||
database_path = self.home / DATA_DIR_NAME / DATABASE_NAME
|
||||
if not database_path.exists():
|
||||
def app_database_path(self) -> Path:
|
||||
"""Get the path to the app-level database.
|
||||
|
||||
This is the single database that will store all knowledge data
|
||||
across all projects.
|
||||
"""
|
||||
database_path = Path.home() / DATA_DIR_NAME / APP_DATABASE_NAME
|
||||
if not database_path.exists(): # pragma: no cover
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
database_path.touch()
|
||||
return database_path
|
||||
|
||||
@field_validator("home")
|
||||
@property
|
||||
def database_path(self) -> Path:
|
||||
"""Get SQLite database path.
|
||||
|
||||
Rreturns the app-level database path
|
||||
for backward compatibility in the codebase.
|
||||
"""
|
||||
|
||||
# Load the app-level database path from the global config
|
||||
config = config_manager.load_config() # pragma: no cover
|
||||
return config.app_database_path # pragma: no cover
|
||||
|
||||
@property
|
||||
def project_list(self) -> List[ProjectConfig]: # pragma: no cover
|
||||
"""Get all configured projects as ProjectConfig objects."""
|
||||
return [ProjectConfig(name=name, home=Path(path)) for name, path in self.projects.items()]
|
||||
|
||||
@field_validator("projects")
|
||||
@classmethod
|
||||
def ensure_path_exists(cls, v: Path) -> Path: # pragma: no cover
|
||||
def ensure_project_paths_exists(cls, v: Dict[str, str]) -> Dict[str, str]: # pragma: no cover
|
||||
"""Ensure project path exists."""
|
||||
if not v.exists():
|
||||
v.mkdir(parents=True)
|
||||
for name, path_value in v.items():
|
||||
path = Path(path_value)
|
||||
if not Path(path).exists():
|
||||
try:
|
||||
path.mkdir(parents=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create project path: {e}")
|
||||
raise e
|
||||
return v
|
||||
|
||||
|
||||
# Load project config
|
||||
config = ProjectConfig()
|
||||
class ConfigManager:
|
||||
"""Manages Basic Memory configuration."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the configuration manager."""
|
||||
home = os.getenv("HOME", Path.home())
|
||||
if isinstance(home, str):
|
||||
home = Path(home)
|
||||
|
||||
self.config_dir = home / DATA_DIR_NAME
|
||||
self.config_file = self.config_dir / CONFIG_FILE_NAME
|
||||
|
||||
# Ensure config directory exists
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load or create configuration
|
||||
self.config = 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
|
||||
else:
|
||||
config = BasicMemoryConfig()
|
||||
self.save_config(config)
|
||||
return config
|
||||
|
||||
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}")
|
||||
|
||||
@property
|
||||
def projects(self) -> Dict[str, str]:
|
||||
"""Get all configured projects."""
|
||||
return self.config.projects.copy()
|
||||
|
||||
@property
|
||||
def default_project(self) -> str:
|
||||
"""Get the default project name."""
|
||||
return self.config.default_project
|
||||
|
||||
def add_project(self, name: str, path: str) -> ProjectConfig:
|
||||
"""Add a new project to the configuration."""
|
||||
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)
|
||||
return ProjectConfig(name=name, home=project_path)
|
||||
|
||||
def remove_project(self, name: str) -> None:
|
||||
"""Remove a project from the configuration."""
|
||||
|
||||
project_name, path = self.get_project(name)
|
||||
if not project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' not found")
|
||||
|
||||
if project_name == self.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)
|
||||
|
||||
def set_default_project(self, name: str) -> None:
|
||||
"""Set the default project."""
|
||||
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)
|
||||
|
||||
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)
|
||||
for name, path in app_config.projects.items():
|
||||
if project_permalink == generate_permalink(name):
|
||||
return name, path
|
||||
return None, None
|
||||
|
||||
|
||||
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
"""
|
||||
Get the project configuration for the current session.
|
||||
If project_name is provided, it will be used instead of the default project.
|
||||
"""
|
||||
|
||||
actual_project_name = None
|
||||
|
||||
# load the config from file
|
||||
global app_config
|
||||
app_config = config_manager.load_config()
|
||||
|
||||
# Get project name from environment variable
|
||||
os_project_name = os.environ.get("BASIC_MEMORY_PROJECT", None)
|
||||
if os_project_name: # pragma: no cover
|
||||
logger.warning(
|
||||
f"BASIC_MEMORY_PROJECT is not supported anymore. Use the --project flag or set the default project in the config instead. Setting default project to {os_project_name}"
|
||||
)
|
||||
actual_project_name = project_name
|
||||
# if the project_name is passed in, use it
|
||||
elif not project_name:
|
||||
# use default
|
||||
actual_project_name = app_config.default_project
|
||||
else: # pragma: no cover
|
||||
actual_project_name = project_name
|
||||
|
||||
# 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_permalink = generate_permalink(actual_project_name)
|
||||
|
||||
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 update_current_project(project_name: str) -> None:
|
||||
"""Update the global config to use a different project.
|
||||
|
||||
This is used by the CLI when --project flag is specified.
|
||||
"""
|
||||
global config
|
||||
config = get_project_config(project_name) # pragma: no cover
|
||||
|
||||
|
||||
# setup logging to a single log file in user home directory
|
||||
user_home = Path.home()
|
||||
log_dir = user_home / DATA_DIR_NAME
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Process info for logging
|
||||
def get_process_name(): # pragma: no cover
|
||||
"""
|
||||
get the type of process for logging
|
||||
"""
|
||||
import sys
|
||||
|
||||
if "sync" in sys.argv:
|
||||
return "sync"
|
||||
elif "mcp" in sys.argv:
|
||||
return "mcp"
|
||||
elif "cli" in sys.argv:
|
||||
return "cli"
|
||||
else:
|
||||
return "api"
|
||||
|
||||
|
||||
process_name = get_process_name()
|
||||
|
||||
# Global flag to track if logging has been set up
|
||||
_LOGGING_SETUP = False
|
||||
|
||||
|
||||
# Logging
|
||||
|
||||
|
||||
def setup_basic_memory_logging(): # pragma: no cover
|
||||
"""Set up logging for basic-memory, ensuring it only happens once."""
|
||||
global _LOGGING_SETUP
|
||||
if _LOGGING_SETUP:
|
||||
# We can't log before logging is set up
|
||||
# print("Skipping duplicate logging setup")
|
||||
return
|
||||
|
||||
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_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
||||
console=False,
|
||||
)
|
||||
|
||||
logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
||||
_LOGGING_SETUP = True
|
||||
|
||||
|
||||
# Set up logging
|
||||
setup_basic_memory_logging()
|
||||
|
||||
+69
-15
@@ -4,8 +4,7 @@ from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
@@ -24,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):
|
||||
@@ -73,32 +73,59 @@ 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,
|
||||
app_config: Optional["BasicMemoryConfig"] = None,
|
||||
) -> 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:
|
||||
if app_config is None:
|
||||
from basic_memory.config import app_config as global_app_config
|
||||
|
||||
app_config = global_app_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
|
||||
if _engine is None:
|
||||
logger.error("Failed to create database engine", db_path=str(db_path))
|
||||
raise RuntimeError("Database engine initialization failed")
|
||||
|
||||
if _session_maker is None:
|
||||
logger.error("Failed to create session maker", db_path=str(db_path))
|
||||
raise RuntimeError("Session maker initialization failed")
|
||||
|
||||
assert _engine is not None # for type checker
|
||||
assert _session_maker is not None # for type checker
|
||||
return _engine, _session_maker
|
||||
|
||||
|
||||
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
|
||||
@@ -112,7 +139,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}")
|
||||
@@ -121,18 +148,35 @@ async def engine_session_factory(
|
||||
try:
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
|
||||
assert _engine is not None # for type checker
|
||||
assert _session_maker is not None # for type checker
|
||||
# Verify that engine and session maker are initialized
|
||||
if _engine is None: # pragma: no cover
|
||||
logger.error("Database engine is None in engine_session_factory")
|
||||
raise RuntimeError("Database engine initialization failed")
|
||||
|
||||
if _session_maker is None: # pragma: no cover
|
||||
logger.error("Session maker is None in engine_session_factory")
|
||||
raise RuntimeError("Session maker initialization failed")
|
||||
|
||||
yield _engine, _session_maker
|
||||
finally:
|
||||
if _engine:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
_migrations_completed = False
|
||||
|
||||
|
||||
async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
|
||||
async def run_migrations(
|
||||
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
|
||||
@@ -154,8 +198,18 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
|
||||
command.upgrade(config, "head")
|
||||
logger.info("Migrations completed successfully")
|
||||
|
||||
_, session_maker = await get_or_create_db(app_config.database_path, database_type)
|
||||
await SearchRepository(session_maker).init_search_index()
|
||||
# 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
|
||||
|
||||
+228
-24
@@ -1,52 +1,87 @@
|
||||
"""Dependency injection functions for basic-memory services."""
|
||||
|
||||
from typing import Annotated
|
||||
from loguru import logger
|
||||
|
||||
import logfire
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, HTTPException, Path, status
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
AsyncEngine,
|
||||
async_sessionmaker,
|
||||
)
|
||||
import pathlib
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig, config
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig
|
||||
from basic_memory.importers import (
|
||||
ChatGPTImporter,
|
||||
ClaudeConversationsImporter,
|
||||
ClaudeProjectsImporter,
|
||||
MemoryJsonImporter,
|
||||
)
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.repository.observation_repository import ObservationRepository
|
||||
from basic_memory.repository.project_repository import ProjectRepository
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.repository.search_repository import SearchRepository
|
||||
from basic_memory.services import (
|
||||
EntityService,
|
||||
)
|
||||
from basic_memory.services import EntityService, ProjectService
|
||||
from basic_memory.services.context_service import ContextService
|
||||
from basic_memory.services.directory_service import DirectoryService
|
||||
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
|
||||
return app_config
|
||||
|
||||
|
||||
AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)] # pragma: no cover
|
||||
|
||||
|
||||
## project
|
||||
|
||||
|
||||
def get_project_config() -> ProjectConfig: # pragma: no cover
|
||||
return config
|
||||
async def get_project_config(
|
||||
project: "ProjectPathDep", project_repository: "ProjectRepositoryDep"
|
||||
) -> ProjectConfig: # pragma: no cover
|
||||
"""Get the current project referenced from request state.
|
||||
|
||||
Args:
|
||||
request: The current request object
|
||||
project_repository: Repository for project operations
|
||||
|
||||
Returns:
|
||||
The resolved project config
|
||||
|
||||
Raises:
|
||||
HTTPException: If project is not found
|
||||
"""
|
||||
|
||||
project_obj = await project_repository.get_by_permalink(str(project))
|
||||
if project_obj:
|
||||
return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
|
||||
|
||||
# Not found
|
||||
raise HTTPException( # pragma: no cover
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
|
||||
)
|
||||
|
||||
|
||||
ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
|
||||
|
||||
|
||||
## sqlalchemy
|
||||
|
||||
|
||||
async def get_engine_factory(
|
||||
project_config: ProjectConfigDep,
|
||||
app_config: AppConfigDep,
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
||||
"""Get engine and session maker."""
|
||||
engine, session_maker = await db.get_or_create_db(project_config.database_path)
|
||||
if project_config.env != "test":
|
||||
logfire.instrument_sqlalchemy(engine=engine)
|
||||
engine, session_maker = await db.get_or_create_db(app_config.database_path)
|
||||
return engine, session_maker
|
||||
|
||||
|
||||
@@ -67,11 +102,70 @@ SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
|
||||
## repositories
|
||||
|
||||
|
||||
async def get_project_repository(
|
||||
session_maker: SessionMakerDep,
|
||||
) -> ProjectRepository:
|
||||
"""Get the project repository."""
|
||||
return ProjectRepository(session_maker)
|
||||
|
||||
|
||||
ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)]
|
||||
ProjectPathDep = Annotated[str, Path()] # Use Path dependency to extract from URL
|
||||
|
||||
|
||||
async def get_project_id(
|
||||
project_repository: ProjectRepositoryDep,
|
||||
project: ProjectPathDep,
|
||||
) -> int:
|
||||
"""Get the current project ID from request state.
|
||||
|
||||
When using sub-applications with /{project} mounting, the project value
|
||||
is stored in request.state by middleware.
|
||||
|
||||
Args:
|
||||
request: The current request object
|
||||
project_repository: Repository for project operations
|
||||
|
||||
Returns:
|
||||
The resolved project ID
|
||||
|
||||
Raises:
|
||||
HTTPException: If project is not found
|
||||
"""
|
||||
|
||||
# Try by permalink first (most common case with URL paths)
|
||||
project_obj = await project_repository.get_by_permalink(str(project))
|
||||
if project_obj:
|
||||
return project_obj.id
|
||||
|
||||
# Try by name if permalink lookup fails
|
||||
project_obj = await project_repository.get_by_name(str(project)) # pragma: no cover
|
||||
if project_obj: # pragma: no cover
|
||||
return project_obj.id
|
||||
|
||||
# Not found
|
||||
raise HTTPException( # pragma: no cover
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
The project_id dependency is used in the following:
|
||||
- EntityRepository
|
||||
- ObservationRepository
|
||||
- RelationRepository
|
||||
- SearchRepository
|
||||
- ProjectInfoRepository
|
||||
"""
|
||||
ProjectIdDep = Annotated[int, Depends(get_project_id)]
|
||||
|
||||
|
||||
async def get_entity_repository(
|
||||
session_maker: SessionMakerDep,
|
||||
project_id: ProjectIdDep,
|
||||
) -> EntityRepository:
|
||||
"""Create an EntityRepository instance."""
|
||||
return EntityRepository(session_maker)
|
||||
"""Create an EntityRepository instance for the current project."""
|
||||
return EntityRepository(session_maker, project_id=project_id)
|
||||
|
||||
|
||||
EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)]
|
||||
@@ -79,9 +173,10 @@ EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)
|
||||
|
||||
async def get_observation_repository(
|
||||
session_maker: SessionMakerDep,
|
||||
project_id: ProjectIdDep,
|
||||
) -> ObservationRepository:
|
||||
"""Create an ObservationRepository instance."""
|
||||
return ObservationRepository(session_maker)
|
||||
"""Create an ObservationRepository instance for the current project."""
|
||||
return ObservationRepository(session_maker, project_id=project_id)
|
||||
|
||||
|
||||
ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)]
|
||||
@@ -89,9 +184,10 @@ ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observat
|
||||
|
||||
async def get_relation_repository(
|
||||
session_maker: SessionMakerDep,
|
||||
project_id: ProjectIdDep,
|
||||
) -> RelationRepository:
|
||||
"""Create a RelationRepository instance."""
|
||||
return RelationRepository(session_maker)
|
||||
"""Create a RelationRepository instance for the current project."""
|
||||
return RelationRepository(session_maker, project_id=project_id)
|
||||
|
||||
|
||||
RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
|
||||
@@ -99,14 +195,18 @@ RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repos
|
||||
|
||||
async def get_search_repository(
|
||||
session_maker: SessionMakerDep,
|
||||
project_id: ProjectIdDep,
|
||||
) -> SearchRepository:
|
||||
"""Create a SearchRepository instance."""
|
||||
return SearchRepository(session_maker)
|
||||
"""Create a SearchRepository instance for the current project."""
|
||||
return SearchRepository(session_maker, project_id=project_id)
|
||||
|
||||
|
||||
SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
|
||||
|
||||
|
||||
# ProjectInfoRepository is deprecated and will be removed in a future version.
|
||||
# Use ProjectRepository instead, which has the same functionality plus more project-specific operations.
|
||||
|
||||
## services
|
||||
|
||||
|
||||
@@ -127,7 +227,12 @@ MarkdownProcessorDep = Annotated[MarkdownProcessor, Depends(get_markdown_process
|
||||
async def get_file_service(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> FileService:
|
||||
return FileService(project_config.home, markdown_processor)
|
||||
logger.debug(
|
||||
f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
|
||||
)
|
||||
file_service = FileService(project_config.home, markdown_processor)
|
||||
logger.debug(f"Created FileService for project: {file_service} ")
|
||||
return file_service
|
||||
|
||||
|
||||
FileServiceDep = Annotated[FileService, Depends(get_file_service)]
|
||||
@@ -177,9 +282,108 @@ LinkResolverDep = Annotated[LinkResolver, Depends(get_link_resolver)]
|
||||
|
||||
|
||||
async def get_context_service(
|
||||
search_repository: SearchRepositoryDep, entity_repository: EntityRepositoryDep
|
||||
search_repository: SearchRepositoryDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
observation_repository: ObservationRepositoryDep,
|
||||
) -> ContextService:
|
||||
return ContextService(search_repository, entity_repository)
|
||||
return ContextService(
|
||||
search_repository=search_repository,
|
||||
entity_repository=entity_repository,
|
||||
observation_repository=observation_repository,
|
||||
)
|
||||
|
||||
|
||||
ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
|
||||
|
||||
|
||||
async def get_sync_service(
|
||||
entity_service: EntityServiceDep,
|
||||
entity_parser: EntityParserDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
relation_repository: RelationRepositoryDep,
|
||||
search_service: SearchServiceDep,
|
||||
file_service: FileServiceDep,
|
||||
) -> SyncService: # pragma: no cover
|
||||
"""
|
||||
|
||||
:rtype: object
|
||||
"""
|
||||
return SyncService(
|
||||
app_config=app_config,
|
||||
entity_service=entity_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
search_service=search_service,
|
||||
file_service=file_service,
|
||||
)
|
||||
|
||||
|
||||
SyncServiceDep = Annotated[SyncService, Depends(get_sync_service)]
|
||||
|
||||
|
||||
async def get_project_service(
|
||||
project_repository: ProjectRepositoryDep,
|
||||
) -> ProjectService:
|
||||
"""Create ProjectService with repository."""
|
||||
return ProjectService(repository=project_repository)
|
||||
|
||||
|
||||
ProjectServiceDep = Annotated[ProjectService, Depends(get_project_service)]
|
||||
|
||||
|
||||
async def get_directory_service(
|
||||
entity_repository: EntityRepositoryDep,
|
||||
) -> DirectoryService:
|
||||
"""Create DirectoryService with dependencies."""
|
||||
return DirectoryService(
|
||||
entity_repository=entity_repository,
|
||||
)
|
||||
|
||||
|
||||
DirectoryServiceDep = Annotated[DirectoryService, Depends(get_directory_service)]
|
||||
|
||||
|
||||
# Import
|
||||
|
||||
|
||||
async def get_chatgpt_importer(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> ChatGPTImporter:
|
||||
"""Create ChatGPTImporter with dependencies."""
|
||||
return ChatGPTImporter(project_config.home, markdown_processor)
|
||||
|
||||
|
||||
ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)]
|
||||
|
||||
|
||||
async def get_claude_conversations_importer(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> ClaudeConversationsImporter:
|
||||
"""Create ChatGPTImporter with dependencies."""
|
||||
return ClaudeConversationsImporter(project_config.home, markdown_processor)
|
||||
|
||||
|
||||
ClaudeConversationsImporterDep = Annotated[
|
||||
ClaudeConversationsImporter, Depends(get_claude_conversations_importer)
|
||||
]
|
||||
|
||||
|
||||
async def get_claude_projects_importer(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> ClaudeProjectsImporter:
|
||||
"""Create ChatGPTImporter with dependencies."""
|
||||
return ClaudeProjectsImporter(project_config.home, markdown_processor)
|
||||
|
||||
|
||||
ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)]
|
||||
|
||||
|
||||
async def get_memory_json_importer(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> MemoryJsonImporter:
|
||||
"""Create ChatGPTImporter with dependencies."""
|
||||
return MemoryJsonImporter(project_config.home, markdown_processor)
|
||||
|
||||
|
||||
MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.utils import FilePath
|
||||
|
||||
|
||||
class FileError(Exception):
|
||||
"""Base exception for file operations."""
|
||||
@@ -26,12 +28,12 @@ class ParseError(FileError):
|
||||
pass
|
||||
|
||||
|
||||
async def compute_checksum(content: str) -> str:
|
||||
async def compute_checksum(content: Union[str, bytes]) -> str:
|
||||
"""
|
||||
Compute SHA-256 checksum of content.
|
||||
|
||||
Args:
|
||||
content: Text content to hash
|
||||
content: Content to hash (either text string or bytes)
|
||||
|
||||
Returns:
|
||||
SHA-256 hex digest
|
||||
@@ -40,48 +42,55 @@ async def compute_checksum(content: str) -> str:
|
||||
FileError: If checksum computation fails
|
||||
"""
|
||||
try:
|
||||
return hashlib.sha256(content.encode()).hexdigest()
|
||||
if isinstance(content, str):
|
||||
content = content.encode()
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to compute checksum: {e}")
|
||||
raise FileError(f"Failed to compute checksum: {e}")
|
||||
|
||||
|
||||
async def ensure_directory(path: Path) -> None:
|
||||
async def ensure_directory(path: FilePath) -> None:
|
||||
"""
|
||||
Ensure directory exists, creating if necessary.
|
||||
|
||||
Args:
|
||||
path: Directory path to ensure
|
||||
path: Directory path to ensure (Path or string)
|
||||
|
||||
Raises:
|
||||
FileWriteError: If directory creation fails
|
||||
"""
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
# Convert string to Path if needed
|
||||
path_obj = Path(path) if isinstance(path, str) else path
|
||||
path_obj.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to create directory: {path}: {e}")
|
||||
logger.error("Failed to create directory", path=str(path), error=str(e))
|
||||
raise FileWriteError(f"Failed to create directory {path}: {e}")
|
||||
|
||||
|
||||
async def write_file_atomic(path: Path, content: str) -> None:
|
||||
async def write_file_atomic(path: FilePath, content: str) -> None:
|
||||
"""
|
||||
Write file with atomic operation using temporary file.
|
||||
|
||||
Args:
|
||||
path: Target file path
|
||||
path: Target file path (Path or string)
|
||||
content: Content to write
|
||||
|
||||
Raises:
|
||||
FileWriteError: If write operation fails
|
||||
"""
|
||||
temp_path = path.with_suffix(".tmp")
|
||||
# Convert string to Path if needed
|
||||
path_obj = Path(path) if isinstance(path, str) else path
|
||||
temp_path = path_obj.with_suffix(".tmp")
|
||||
|
||||
try:
|
||||
temp_path.write_text(content)
|
||||
temp_path.replace(path)
|
||||
logger.debug(f"wrote file: {path}")
|
||||
temp_path.write_text(content, encoding="utf-8")
|
||||
temp_path.replace(path_obj)
|
||||
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
|
||||
except Exception as e: # pragma: no cover
|
||||
temp_path.unlink(missing_ok=True)
|
||||
logger.error(f"Failed to write file: {path}: {e}")
|
||||
logger.error("Failed to write file", path=str(path_obj), error=str(e))
|
||||
raise FileWriteError(f"Failed to write file {path}: {e}")
|
||||
|
||||
|
||||
@@ -95,6 +104,9 @@ def has_frontmatter(content: str) -> bool:
|
||||
Returns:
|
||||
True if content has valid frontmatter markers (---), False otherwise
|
||||
"""
|
||||
if not content:
|
||||
return False
|
||||
|
||||
content = content.strip()
|
||||
if not content.startswith("---"):
|
||||
return False
|
||||
@@ -171,7 +183,7 @@ def remove_frontmatter(content: str) -> str:
|
||||
return parts[2].strip()
|
||||
|
||||
|
||||
async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
||||
async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
||||
"""Update frontmatter fields in a file while preserving all content.
|
||||
|
||||
Only modifies the frontmatter section, leaving all content untouched.
|
||||
@@ -179,7 +191,7 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
||||
Returns checksum of updated file.
|
||||
|
||||
Args:
|
||||
path: Path to markdown file
|
||||
path: Path to markdown file (Path or string)
|
||||
updates: Dict of frontmatter fields to update
|
||||
|
||||
Returns:
|
||||
@@ -190,8 +202,11 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
||||
ParseError: If frontmatter parsing fails
|
||||
"""
|
||||
try:
|
||||
# Convert string to Path if needed
|
||||
path_obj = Path(path) if isinstance(path, str) else path
|
||||
|
||||
# Read current content
|
||||
content = path.read_text()
|
||||
content = path_obj.read_text(encoding="utf-8")
|
||||
|
||||
# Parse current frontmatter
|
||||
current_fm = {}
|
||||
@@ -203,12 +218,18 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
||||
new_fm = {**current_fm, **updates}
|
||||
|
||||
# Write new file with updated frontmatter
|
||||
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
||||
yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
|
||||
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
||||
|
||||
await write_file_atomic(path, final_content)
|
||||
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
|
||||
|
||||
await write_file_atomic(path_obj, final_content)
|
||||
return await compute_checksum(final_content)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to update frontmatter in {path}: {e}")
|
||||
logger.error(
|
||||
"Failed to update frontmatter",
|
||||
path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
|
||||
error=str(e),
|
||||
)
|
||||
raise FileError(f"Failed to update frontmatter: {e}")
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Import services for Basic Memory."""
|
||||
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.importers.chatgpt_importer import ChatGPTImporter
|
||||
from basic_memory.importers.claude_conversations_importer import (
|
||||
ClaudeConversationsImporter,
|
||||
)
|
||||
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
|
||||
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
||||
from basic_memory.schemas.importer import (
|
||||
ChatImportResult,
|
||||
EntityImportResult,
|
||||
ImportResult,
|
||||
ProjectImportResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Importer",
|
||||
"ChatGPTImporter",
|
||||
"ClaudeConversationsImporter",
|
||||
"ClaudeProjectsImporter",
|
||||
"MemoryJsonImporter",
|
||||
"ImportResult",
|
||||
"ChatImportResult",
|
||||
"EntityImportResult",
|
||||
"ProjectImportResult",
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Base import service for Basic Memory."""
|
||||
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, TypeVar
|
||||
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown
|
||||
from basic_memory.schemas.importer import ImportResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T", bound=ImportResult)
|
||||
|
||||
|
||||
class Importer[T: ImportResult]:
|
||||
"""Base class for all import services."""
|
||||
|
||||
def __init__(self, base_path: Path, markdown_processor: MarkdownProcessor):
|
||||
"""Initialize the import service.
|
||||
|
||||
Args:
|
||||
markdown_processor: MarkdownProcessor instance for writing markdown files.
|
||||
"""
|
||||
self.base_path = base_path.resolve() # Get absolute path
|
||||
self.markdown_processor = markdown_processor
|
||||
|
||||
@abstractmethod
|
||||
async def import_data(self, source_data, destination_folder: str, **kwargs: Any) -> T:
|
||||
"""Import data from source file to destination folder.
|
||||
|
||||
Args:
|
||||
source_path: Path to the source file.
|
||||
destination_folder: Destination folder within the project.
|
||||
**kwargs: Additional keyword arguments for specific import types.
|
||||
|
||||
Returns:
|
||||
ImportResult containing statistics and status of the import.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
async def write_entity(self, entity: EntityMarkdown, file_path: Path) -> None:
|
||||
"""Write entity to file using markdown processor.
|
||||
|
||||
Args:
|
||||
entity: EntityMarkdown instance to write.
|
||||
file_path: Path to write the entity to.
|
||||
"""
|
||||
await self.markdown_processor.write_file(file_path, entity)
|
||||
|
||||
def ensure_folder_exists(self, folder: str) -> Path:
|
||||
"""Ensure folder exists, create if it doesn't.
|
||||
|
||||
Args:
|
||||
base_path: Base path of the project.
|
||||
folder: Folder name or path within the project.
|
||||
|
||||
Returns:
|
||||
Path to the folder.
|
||||
"""
|
||||
folder_path = self.base_path / folder
|
||||
folder_path.mkdir(parents=True, exist_ok=True)
|
||||
return folder_path
|
||||
|
||||
@abstractmethod
|
||||
def handle_error(
|
||||
self, message: str, error: Optional[Exception] = None
|
||||
) -> T: # pragma: no cover
|
||||
"""Handle errors during import.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
error: Optional exception that caused the error.
|
||||
|
||||
Returns:
|
||||
ImportResult with error information.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,222 @@
|
||||
"""ChatGPT import service for Basic Memory."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.schemas.importer import ChatImportResult
|
||||
from basic_memory.importers.utils import clean_filename, format_timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
"""Service for importing ChatGPT conversations."""
|
||||
|
||||
async def import_data(
|
||||
self, source_data, destination_folder: str, **kwargs: Any
|
||||
) -> ChatImportResult:
|
||||
"""Import conversations from ChatGPT JSON export.
|
||||
|
||||
Args:
|
||||
source_path: Path to the ChatGPT conversations.json file.
|
||||
destination_folder: Destination folder within the project.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
ChatImportResult containing statistics and status of the import.
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
# Ensure the destination folder exists
|
||||
self.ensure_folder_exists(destination_folder)
|
||||
conversations = source_data
|
||||
|
||||
# Process each conversation
|
||||
messages_imported = 0
|
||||
chats_imported = 0
|
||||
|
||||
for chat in conversations:
|
||||
# Convert to entity
|
||||
entity = self._format_chat_content(destination_folder, chat)
|
||||
|
||||
# Write file
|
||||
file_path = self.base_path / f"{entity.frontmatter.metadata['permalink']}.md"
|
||||
await self.write_entity(entity, file_path)
|
||||
|
||||
# Count messages
|
||||
msg_count = sum(
|
||||
1
|
||||
for node in chat["mapping"].values()
|
||||
if node.get("message")
|
||||
and not node.get("message", {})
|
||||
.get("metadata", {})
|
||||
.get("is_visually_hidden_from_conversation")
|
||||
)
|
||||
|
||||
chats_imported += 1
|
||||
messages_imported += msg_count
|
||||
|
||||
return ChatImportResult(
|
||||
import_count={"conversations": chats_imported, "messages": messages_imported},
|
||||
success=True,
|
||||
conversations=chats_imported,
|
||||
messages=messages_imported,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception("Failed to import ChatGPT conversations")
|
||||
return self.handle_error("Failed to import ChatGPT conversations", e) # pyright: ignore [reportReturnType]
|
||||
|
||||
def _format_chat_content(
|
||||
self, folder: str, conversation: Dict[str, Any]
|
||||
) -> EntityMarkdown: # pragma: no cover
|
||||
"""Convert chat conversation to Basic Memory entity.
|
||||
|
||||
Args:
|
||||
folder: Destination folder name.
|
||||
conversation: ChatGPT conversation data.
|
||||
|
||||
Returns:
|
||||
EntityMarkdown instance representing the conversation.
|
||||
"""
|
||||
# Extract timestamps
|
||||
created_at = conversation["create_time"]
|
||||
modified_at = conversation["update_time"]
|
||||
|
||||
root_id = None
|
||||
# Find root message
|
||||
for node_id, node in conversation["mapping"].items():
|
||||
if node.get("parent") is None:
|
||||
root_id = node_id
|
||||
break
|
||||
|
||||
# Generate permalink
|
||||
date_prefix = datetime.fromtimestamp(created_at).strftime("%Y%m%d")
|
||||
clean_title = clean_filename(conversation["title"])
|
||||
|
||||
# Format content
|
||||
content = self._format_chat_markdown(
|
||||
title=conversation["title"],
|
||||
mapping=conversation["mapping"],
|
||||
root_id=root_id,
|
||||
created_at=created_at,
|
||||
modified_at=modified_at,
|
||||
)
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "conversation",
|
||||
"title": conversation["title"],
|
||||
"created": format_timestamp(created_at),
|
||||
"modified": format_timestamp(modified_at),
|
||||
"permalink": f"{folder}/{date_prefix}-{clean_title}",
|
||||
}
|
||||
),
|
||||
content=content,
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
def _format_chat_markdown(
|
||||
self,
|
||||
title: str,
|
||||
mapping: Dict[str, Any],
|
||||
root_id: Optional[str],
|
||||
created_at: float,
|
||||
modified_at: float,
|
||||
) -> str: # pragma: no cover
|
||||
"""Format chat as clean markdown.
|
||||
|
||||
Args:
|
||||
title: Chat title.
|
||||
mapping: Message mapping.
|
||||
root_id: Root message ID.
|
||||
created_at: Creation timestamp.
|
||||
modified_at: Modification timestamp.
|
||||
|
||||
Returns:
|
||||
Formatted markdown content.
|
||||
"""
|
||||
# Start with title
|
||||
lines = [f"# {title}\n"]
|
||||
|
||||
# Traverse message tree
|
||||
seen_msgs: Set[str] = set()
|
||||
messages = self._traverse_messages(mapping, root_id, seen_msgs)
|
||||
|
||||
# Format each message
|
||||
for msg in messages:
|
||||
# Skip hidden messages
|
||||
if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
|
||||
continue
|
||||
|
||||
# Get author and timestamp
|
||||
author = msg["author"]["role"].title()
|
||||
ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
|
||||
|
||||
# Add message header
|
||||
lines.append(f"### {author} ({ts})")
|
||||
|
||||
# Add message content
|
||||
content = self._get_message_content(msg)
|
||||
if content:
|
||||
lines.append(content)
|
||||
|
||||
# Add spacing
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_message_content(self, message: Dict[str, Any]) -> str: # pragma: no cover
|
||||
"""Extract clean message content.
|
||||
|
||||
Args:
|
||||
message: Message data.
|
||||
|
||||
Returns:
|
||||
Cleaned message content.
|
||||
"""
|
||||
if not message or "content" not in message:
|
||||
return ""
|
||||
|
||||
content = message["content"]
|
||||
if content.get("content_type") == "text":
|
||||
return "\n".join(content.get("parts", []))
|
||||
elif content.get("content_type") == "code":
|
||||
return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
|
||||
return ""
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
mapping: Message mapping.
|
||||
root_id: Root message ID.
|
||||
seen: Set of seen message IDs.
|
||||
|
||||
Returns:
|
||||
List of message data.
|
||||
"""
|
||||
messages = []
|
||||
node = mapping.get(root_id) if root_id else None
|
||||
|
||||
while node:
|
||||
if node["id"] not in seen and node.get("message"):
|
||||
seen.add(node["id"])
|
||||
messages.append(node["message"])
|
||||
|
||||
# Follow children
|
||||
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
|
||||
|
||||
return messages
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Claude conversations import service for Basic Memory."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.schemas.importer import ChatImportResult
|
||||
from basic_memory.importers.utils import clean_filename, format_timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClaudeConversationsImporter(Importer[ChatImportResult]):
|
||||
"""Service for importing Claude conversations."""
|
||||
|
||||
async def import_data(
|
||||
self, source_data, destination_folder: str, **kwargs: Any
|
||||
) -> ChatImportResult:
|
||||
"""Import conversations from Claude JSON export.
|
||||
|
||||
Args:
|
||||
source_data: Path to the Claude conversations.json file.
|
||||
destination_folder: Destination folder within the project.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
ChatImportResult containing statistics and status of the import.
|
||||
"""
|
||||
try:
|
||||
# Ensure the destination folder exists
|
||||
folder_path = self.ensure_folder_exists(destination_folder)
|
||||
|
||||
conversations = source_data
|
||||
|
||||
# Process each conversation
|
||||
messages_imported = 0
|
||||
chats_imported = 0
|
||||
|
||||
for chat in conversations:
|
||||
# Convert to entity
|
||||
entity = self._format_chat_content(
|
||||
base_path=folder_path,
|
||||
name=chat["name"],
|
||||
messages=chat["chat_messages"],
|
||||
created_at=chat["created_at"],
|
||||
modified_at=chat["updated_at"],
|
||||
)
|
||||
|
||||
# Write file
|
||||
file_path = self.base_path / Path(f"{entity.frontmatter.metadata['permalink']}.md")
|
||||
await self.write_entity(entity, file_path)
|
||||
|
||||
chats_imported += 1
|
||||
messages_imported += len(chat["chat_messages"])
|
||||
|
||||
return ChatImportResult(
|
||||
import_count={"conversations": chats_imported, "messages": messages_imported},
|
||||
success=True,
|
||||
conversations=chats_imported,
|
||||
messages=messages_imported,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception("Failed to import Claude conversations")
|
||||
return self.handle_error("Failed to import Claude conversations", e) # pyright: ignore [reportReturnType]
|
||||
|
||||
def _format_chat_content(
|
||||
self,
|
||||
base_path: Path,
|
||||
name: str,
|
||||
messages: List[Dict[str, Any]],
|
||||
created_at: str,
|
||||
modified_at: str,
|
||||
) -> EntityMarkdown:
|
||||
"""Convert chat messages to Basic Memory entity format.
|
||||
|
||||
Args:
|
||||
base_path: Base path for the entity.
|
||||
name: Chat name.
|
||||
messages: List of chat messages.
|
||||
created_at: Creation timestamp.
|
||||
modified_at: Modification timestamp.
|
||||
|
||||
Returns:
|
||||
EntityMarkdown instance representing the conversation.
|
||||
"""
|
||||
# Generate permalink
|
||||
date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
|
||||
clean_title = clean_filename(name)
|
||||
permalink = f"{base_path.name}/{date_prefix}-{clean_title}"
|
||||
|
||||
# Format content
|
||||
content = self._format_chat_markdown(
|
||||
name=name,
|
||||
messages=messages,
|
||||
created_at=created_at,
|
||||
modified_at=modified_at,
|
||||
permalink=permalink,
|
||||
)
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "conversation",
|
||||
"title": name,
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": permalink,
|
||||
}
|
||||
),
|
||||
content=content,
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
def _format_chat_markdown(
|
||||
self,
|
||||
name: str,
|
||||
messages: List[Dict[str, Any]],
|
||||
created_at: str,
|
||||
modified_at: str,
|
||||
permalink: str,
|
||||
) -> str:
|
||||
"""Format chat as clean markdown.
|
||||
|
||||
Args:
|
||||
name: Chat name.
|
||||
messages: List of chat messages.
|
||||
created_at: Creation timestamp.
|
||||
modified_at: Modification timestamp.
|
||||
permalink: Permalink for the entity.
|
||||
|
||||
Returns:
|
||||
Formatted markdown content.
|
||||
"""
|
||||
# Start with frontmatter and title
|
||||
lines = [
|
||||
f"# {name}\n",
|
||||
]
|
||||
|
||||
# Add messages
|
||||
for msg in messages:
|
||||
# Format timestamp
|
||||
ts = format_timestamp(msg["created_at"])
|
||||
|
||||
# Add message header
|
||||
lines.append(f"### {msg['sender'].title()} ({ts})")
|
||||
|
||||
# Handle message content
|
||||
content = msg.get("text", "")
|
||||
if msg.get("content"):
|
||||
content = " ".join(c.get("text", "") for c in msg["content"])
|
||||
lines.append(content)
|
||||
|
||||
# Handle attachments
|
||||
attachments = msg.get("attachments", [])
|
||||
for attachment in attachments:
|
||||
if "file_name" in attachment:
|
||||
lines.append(f"\n**Attachment: {attachment['file_name']}**")
|
||||
if "extracted_content" in attachment:
|
||||
lines.append("```")
|
||||
lines.append(attachment["extracted_content"])
|
||||
lines.append("```")
|
||||
|
||||
# Add spacing between messages
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Claude projects import service for Basic Memory."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.schemas.importer import ProjectImportResult
|
||||
from basic_memory.importers.utils import clean_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClaudeProjectsImporter(Importer[ProjectImportResult]):
|
||||
"""Service for importing Claude projects."""
|
||||
|
||||
async def import_data(
|
||||
self, source_data, destination_folder: str, **kwargs: Any
|
||||
) -> ProjectImportResult:
|
||||
"""Import projects from Claude JSON export.
|
||||
|
||||
Args:
|
||||
source_path: Path to the Claude projects.json file.
|
||||
destination_folder: Base folder for projects within the project.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
ProjectImportResult containing statistics and status of the import.
|
||||
"""
|
||||
try:
|
||||
# Ensure the base folder exists
|
||||
base_path = self.base_path
|
||||
if destination_folder:
|
||||
base_path = self.ensure_folder_exists(destination_folder)
|
||||
|
||||
projects = source_data
|
||||
|
||||
# Process each project
|
||||
docs_imported = 0
|
||||
prompts_imported = 0
|
||||
|
||||
for project in projects:
|
||||
project_dir = clean_filename(project["name"])
|
||||
|
||||
# Create project directories
|
||||
docs_dir = base_path / project_dir / "docs"
|
||||
docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Import prompt template if it exists
|
||||
if prompt_entity := self._format_prompt_markdown(project):
|
||||
file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
|
||||
await self.write_entity(prompt_entity, file_path)
|
||||
prompts_imported += 1
|
||||
|
||||
# Import project documents
|
||||
for doc in project.get("docs", []):
|
||||
entity = self._format_project_markdown(project, doc)
|
||||
file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
|
||||
await self.write_entity(entity, file_path)
|
||||
docs_imported += 1
|
||||
|
||||
return ProjectImportResult(
|
||||
import_count={"documents": docs_imported, "prompts": prompts_imported},
|
||||
success=True,
|
||||
documents=docs_imported,
|
||||
prompts=prompts_imported,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception("Failed to import Claude projects")
|
||||
return self.handle_error("Failed to import Claude projects", e) # pyright: ignore [reportReturnType]
|
||||
|
||||
def _format_project_markdown(
|
||||
self, project: Dict[str, Any], doc: Dict[str, Any]
|
||||
) -> EntityMarkdown:
|
||||
"""Format a project document as a Basic Memory entity.
|
||||
|
||||
Args:
|
||||
project: Project data.
|
||||
doc: Document data.
|
||||
|
||||
Returns:
|
||||
EntityMarkdown instance representing the document.
|
||||
"""
|
||||
# Extract timestamps
|
||||
created_at = doc.get("created_at") or project["created_at"]
|
||||
modified_at = project["updated_at"]
|
||||
|
||||
# Generate clean names for organization
|
||||
project_dir = clean_filename(project["name"])
|
||||
doc_file = clean_filename(doc["filename"])
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "project_doc",
|
||||
"title": doc["filename"],
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": f"{project_dir}/docs/{doc_file}",
|
||||
"project_name": project["name"],
|
||||
"project_uuid": project["uuid"],
|
||||
"doc_uuid": doc["uuid"],
|
||||
}
|
||||
),
|
||||
content=doc["content"],
|
||||
)
|
||||
|
||||
return entity
|
||||
|
||||
def _format_prompt_markdown(self, project: Dict[str, Any]) -> Optional[EntityMarkdown]:
|
||||
"""Format project prompt template as a Basic Memory entity.
|
||||
|
||||
Args:
|
||||
project: Project data.
|
||||
|
||||
Returns:
|
||||
EntityMarkdown instance representing the prompt template, or None if
|
||||
no prompt template exists.
|
||||
"""
|
||||
if not project.get("prompt_template"):
|
||||
return None
|
||||
|
||||
# Extract timestamps
|
||||
created_at = project["created_at"]
|
||||
modified_at = project["updated_at"]
|
||||
|
||||
# Generate clean project directory name
|
||||
project_dir = clean_filename(project["name"])
|
||||
|
||||
# Create entity
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": "prompt_template",
|
||||
"title": f"Prompt Template: {project['name']}",
|
||||
"created": created_at,
|
||||
"modified": modified_at,
|
||||
"permalink": f"{project_dir}/prompt-template",
|
||||
"project_name": project["name"],
|
||||
"project_uuid": project["uuid"],
|
||||
}
|
||||
),
|
||||
content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
|
||||
)
|
||||
|
||||
return entity
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Memory JSON import service for Basic Memory."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from basic_memory.config import 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
"""Service for importing memory.json format data."""
|
||||
|
||||
async def import_data(
|
||||
self, source_data, destination_folder: str = "", **kwargs: Any
|
||||
) -> EntityImportResult:
|
||||
"""Import entities and relations from a memory.json file.
|
||||
|
||||
Args:
|
||||
source_data: Path to the memory.json file.
|
||||
destination_folder: Optional destination folder within the project.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
EntityImportResult containing statistics and status of the import.
|
||||
"""
|
||||
try:
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Ensure the base path exists
|
||||
base_path = config.home # pragma: no cover
|
||||
if destination_folder: # pragma: no cover
|
||||
base_path = self.ensure_folder_exists(destination_folder)
|
||||
|
||||
# First pass - collect entities and relations
|
||||
for line in source_data:
|
||||
data = line
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data.get("from") or data.get("from_id")
|
||||
if source not in entity_relations:
|
||||
entity_relations[source] = []
|
||||
entity_relations[source].append(
|
||||
Relation(
|
||||
type=data.get("relationType") or data.get("relation_type"),
|
||||
target=data.get("to") or data.get("to_id"),
|
||||
)
|
||||
)
|
||||
|
||||
# Second pass - create and write entities
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
# Ensure entity type directory exists
|
||||
entity_type_dir = base_path / entity_data["entityType"]
|
||||
entity_type_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}",
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[Observation(content=obs) for obs in entity_data["observations"]],
|
||||
relations=entity_relations.get(name, []),
|
||||
)
|
||||
|
||||
# Write entity file
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
await self.write_entity(entity, file_path)
|
||||
entities_created += 1
|
||||
|
||||
relations_count = sum(len(rels) for rels in entity_relations.values())
|
||||
|
||||
return EntityImportResult(
|
||||
import_count={"entities": entities_created, "relations": relations_count},
|
||||
success=True,
|
||||
entities=entities_created,
|
||||
relations=relations_count,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception("Failed to import memory.json")
|
||||
return self.handle_error("Failed to import memory.json", e) # pyright: ignore [reportReturnType]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Utility functions for import services."""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
def clean_filename(name: str) -> str: # pragma: no cover
|
||||
"""Clean a string to be used as a filename.
|
||||
|
||||
Args:
|
||||
name: The string to clean.
|
||||
|
||||
Returns:
|
||||
A cleaned string suitable for use as a filename.
|
||||
"""
|
||||
# Replace common punctuation and whitespace with underscores
|
||||
name = re.sub(r"[\s\-,.:/\\\[\]\(\)]+", "_", name)
|
||||
# Remove any non-alphanumeric or underscore characters
|
||||
name = re.sub(r"[^\w]+", "", name)
|
||||
# Ensure the name isn't too long
|
||||
if len(name) > 100: # pragma: no cover
|
||||
name = name[:100]
|
||||
# Ensure the name isn't empty
|
||||
if not name: # pragma: no cover
|
||||
name = "untitled"
|
||||
return name
|
||||
|
||||
|
||||
def format_timestamp(timestamp: Any) -> str: # pragma: no cover
|
||||
"""Format a timestamp for use in a filename or title.
|
||||
|
||||
Args:
|
||||
timestamp: A timestamp in various formats.
|
||||
|
||||
Returns:
|
||||
A formatted string representation of the timestamp.
|
||||
"""
|
||||
if isinstance(timestamp, str):
|
||||
try:
|
||||
# Try ISO format
|
||||
timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
try:
|
||||
# Try unix timestamp as string
|
||||
timestamp = datetime.fromtimestamp(float(timestamp))
|
||||
except ValueError:
|
||||
# Return as is if we can't parse it
|
||||
return timestamp
|
||||
elif isinstance(timestamp, (int, float)):
|
||||
# Unix timestamp
|
||||
timestamp = datetime.fromtimestamp(timestamp)
|
||||
|
||||
if isinstance(timestamp, datetime):
|
||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Return as is if we can't format it
|
||||
return str(timestamp) # pragma: no cover
|
||||
@@ -4,21 +4,22 @@ Uses markdown-it with plugins to parse structured data from markdown content.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
import dateparser
|
||||
|
||||
from markdown_it import MarkdownIt
|
||||
import dateparser
|
||||
import frontmatter
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
from basic_memory.markdown.plugins import observation_plugin, relation_plugin
|
||||
from basic_memory.markdown.schemas import (
|
||||
EntityMarkdown,
|
||||
EntityFrontmatter,
|
||||
EntityMarkdown,
|
||||
Observation,
|
||||
Relation,
|
||||
)
|
||||
from basic_memory.utils import parse_tags
|
||||
|
||||
md = MarkdownIt().use(observation_plugin).use(relation_plugin)
|
||||
|
||||
@@ -56,11 +57,11 @@ def parse(content: str) -> EntityContent:
|
||||
)
|
||||
|
||||
|
||||
def parse_tags(tags: Any) -> list[str]:
|
||||
"""Parse tags into list of strings."""
|
||||
if isinstance(tags, (list, tuple)):
|
||||
return [str(t).strip() for t in tags if str(t).strip()]
|
||||
return [t.strip() for t in tags.split(",") if t.strip()]
|
||||
# def parse_tags(tags: Any) -> list[str]:
|
||||
# """Parse tags into list of strings."""
|
||||
# if isinstance(tags, (list, tuple)):
|
||||
# return [str(t).strip() for t in tags if str(t).strip()]
|
||||
# return [t.strip() for t in tags.split(",") if t.strip()]
|
||||
|
||||
|
||||
class EntityParser:
|
||||
@@ -88,28 +89,42 @@ class EntityParser:
|
||||
return parsed
|
||||
return None
|
||||
|
||||
async def parse_file(self, file_path: Path) -> EntityMarkdown:
|
||||
async def parse_file(self, path: Path | str) -> EntityMarkdown:
|
||||
"""Parse markdown file into EntityMarkdown."""
|
||||
|
||||
absolute_path = self.base_path / file_path
|
||||
# Parse frontmatter and content using python-frontmatter
|
||||
post = frontmatter.load(str(absolute_path))
|
||||
# Check if the path is already absolute
|
||||
if (
|
||||
isinstance(path, Path)
|
||||
and path.is_absolute()
|
||||
or (isinstance(path, str) and Path(path).is_absolute())
|
||||
):
|
||||
absolute_path = Path(path)
|
||||
else:
|
||||
absolute_path = self.get_file_path(path)
|
||||
|
||||
# Parse frontmatter and content using python-frontmatter
|
||||
file_content = absolute_path.read_text(encoding="utf-8")
|
||||
return await self.parse_file_content(absolute_path, file_content)
|
||||
|
||||
def get_file_path(self, path):
|
||||
"""Get absolute path for a file using the base path for the project."""
|
||||
return self.base_path / path
|
||||
|
||||
async def parse_file_content(self, absolute_path, file_content):
|
||||
post = frontmatter.loads(file_content)
|
||||
# Extract file stat info
|
||||
file_stats = absolute_path.stat()
|
||||
|
||||
metadata = post.metadata
|
||||
metadata["title"] = post.metadata.get("title", file_path.name)
|
||||
metadata["title"] = post.metadata.get("title", absolute_path.stem)
|
||||
metadata["type"] = post.metadata.get("type", "note")
|
||||
metadata["tags"] = parse_tags(post.metadata.get("tags", []))
|
||||
|
||||
tags = parse_tags(post.metadata.get("tags", [])) # pyright: ignore
|
||||
if tags:
|
||||
metadata["tags"] = tags
|
||||
# frontmatter
|
||||
entity_frontmatter = EntityFrontmatter(
|
||||
metadata=post.metadata,
|
||||
)
|
||||
|
||||
entity_content = parse(post.content)
|
||||
|
||||
return EntityMarkdown(
|
||||
frontmatter=entity_frontmatter,
|
||||
content=post.content,
|
||||
|
||||
@@ -83,7 +83,7 @@ class MarkdownProcessor:
|
||||
"""
|
||||
# Dirty check if needed
|
||||
if expected_checksum is not None:
|
||||
current_content = path.read_text()
|
||||
current_content = path.read_text(encoding="utf-8")
|
||||
current_checksum = await file_utils.compute_checksum(current_content)
|
||||
if current_checksum != expected_checksum:
|
||||
raise DirtyFileError(f"File {path} has been modified")
|
||||
|
||||
@@ -42,7 +42,7 @@ class EntityFrontmatter(BaseModel):
|
||||
|
||||
@property
|
||||
def tags(self) -> List[str]:
|
||||
return self.metadata.get("tags") if self.metadata else [] # pyright: ignore
|
||||
return self.metadata.get("tags") if self.metadata else None # pyright: ignore
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""Utilities for converting between markdown and entity models."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from basic_memory.file_utils import has_frontmatter, remove_frontmatter, parse_frontmatter
|
||||
from basic_memory.markdown import EntityMarkdown
|
||||
from basic_memory.models import Entity, Observation as ObservationModel
|
||||
from basic_memory.utils import generate_permalink
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.models import Observation as ObservationModel
|
||||
|
||||
|
||||
def entity_model_from_markdown(
|
||||
@@ -31,16 +32,15 @@ def entity_model_from_markdown(
|
||||
if not markdown.created or not markdown.modified: # pragma: no cover
|
||||
raise ValueError("Both created and modified dates are required in markdown")
|
||||
|
||||
# Generate permalink if not provided
|
||||
permalink = markdown.frontmatter.permalink or generate_permalink(file_path)
|
||||
|
||||
# Create or update entity
|
||||
model = entity or Entity()
|
||||
|
||||
# Update basic fields
|
||||
model.title = markdown.frontmatter.title
|
||||
model.entity_type = markdown.frontmatter.type
|
||||
model.permalink = 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
|
||||
@@ -76,18 +76,33 @@ async def schema_to_markdown(schema: Any) -> Post:
|
||||
"""
|
||||
# Extract content and metadata
|
||||
content = schema.content or ""
|
||||
frontmatter_metadata = dict(schema.entity_metadata or {})
|
||||
entity_metadata = dict(schema.entity_metadata or {})
|
||||
|
||||
# if the content contains frontmatter, remove it and merge
|
||||
if has_frontmatter(content):
|
||||
content_frontmatter = parse_frontmatter(content)
|
||||
content = remove_frontmatter(content)
|
||||
|
||||
# Merge content frontmatter with entity metadata
|
||||
# (entity_metadata takes precedence for conflicts)
|
||||
content_frontmatter.update(entity_metadata)
|
||||
entity_metadata = content_frontmatter
|
||||
|
||||
# Remove special fields for ordered frontmatter
|
||||
for field in ["type", "title", "permalink"]:
|
||||
frontmatter_metadata.pop(field, None)
|
||||
entity_metadata.pop(field, None)
|
||||
|
||||
# Create Post with ordered fields
|
||||
# Create Post with fields ordered by insert order
|
||||
post = Post(
|
||||
content,
|
||||
title=schema.title,
|
||||
type=schema.entity_type,
|
||||
permalink=schema.permalink,
|
||||
**frontmatter_metadata,
|
||||
)
|
||||
# set the permalink if passed in
|
||||
if schema.permalink:
|
||||
post.metadata["permalink"] = schema.permalink
|
||||
|
||||
if entity_metadata:
|
||||
post.metadata.update(entity_metadata)
|
||||
|
||||
return post
|
||||
|
||||
@@ -2,7 +2,7 @@ from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
|
||||
BASE_URL = "memory://"
|
||||
BASE_URL = "http://test"
|
||||
|
||||
# Create shared async client
|
||||
client = AsyncClient(transport=ASGITransport(app=fastapi_app), base_url=BASE_URL)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user