Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] b6f9bdcc71 chore: publish PR 937 infographic 2026-06-10 01:45:38 +00:00
224 changed files with 0 additions and 32618 deletions
-38
View File
@@ -1,38 +0,0 @@
---
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.
-8
View File
@@ -1,8 +0,0 @@
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
-19
View File
@@ -1,19 +0,0 @@
---
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.
-28
View File
@@ -1,28 +0,0 @@
---
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?
-12
View File
@@ -1,12 +0,0 @@
# 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"
-41
View File
@@ -1,41 +0,0 @@
name: "Pull Request Title"
on:
pull_request:
types:
- opened
- edited
- synchronize
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure allowed types based on what we want in our changelog
types: |
feat
fix
chore
docs
style
refactor
perf
test
build
ci
# Require at least one from scope list (optional)
scopes: |
core
cli
api
mcp
sync
ui
deps
installer
# Allow breaking changes (needs "!" after type/scope)
requireScopeForBreakingChange: true
-96
View File
@@ -1,96 +0,0 @@
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
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
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
run: |
uv venv
- name: Install dependencies
run: |
uv sync
- name: Build macOS installer
run: |
make installer-mac
xattr -dr com.apple.quarantine "installer/build/Basic Memory Installer.app"
- 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
with:
files: installer/build/Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip
tag_name: ${{ needs.release.outputs.tag }}
token: ${{ secrets.GITHUB_TOKEN }}
-47
View File
@@ -1,47 +0,0 @@
name: Tests
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.12" ]
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
run: |
pip install uv
- name: Create virtual env
run: |
uv venv
- name: Install dependencies
run: |
uv pip install -e .[dev]
- name: Run type checks
run: |
uv run make type-check
- name: Run tests
run: |
uv pip install pytest pytest-cov
uv run make test
-50
View File
@@ -1,50 +0,0 @@
*.py[cod]
__pycache__/
.pytest_cache/
.coverage
htmlcov/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Installer artifacts
installer/build/
installer/dist/
rw.*.dmg # Temporary disk images
# Virtual environments
.env
.venv
env/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# macOS
.DS_Store
/.coverage.*
# obsidian docs:
/docs/.obsidian/
/examples/.obsidian/
/examples/.basic-memory/
-1
View File
@@ -1 +0,0 @@
3.12
-545
View File
@@ -1,545 +0,0 @@
# CHANGELOG
## 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)
### Bug Fixes
- Add logfire instrumentation to tools
([`3e8e3e8`](https://github.com/basicmachines-co/basic-memory/commit/3e8e3e8961eae2e82839746e28963191b0aef0a0))
- Add logfire spans to cli
([`00d23a5`](https://github.com/basicmachines-co/basic-memory/commit/00d23a5ee15ddac4ea45e702dcd02ab9f0509276))
- Add logfire spans to cli
([`812136c`](https://github.com/basicmachines-co/basic-memory/commit/812136c8c22ad191d14ff32dcad91aae076d4120))
- Search query pagination params
([`bc9ca07`](https://github.com/basicmachines-co/basic-memory/commit/bc9ca0744ffe4296d7d597b4dd9b7c73c2d63f3f))
### Chores
- Fix tests
([`57984aa`](https://github.com/basicmachines-co/basic-memory/commit/57984aa912625dcde7877afb96d874c164af2896))
- Remove unused tests
([`2c8ed17`](https://github.com/basicmachines-co/basic-memory/commit/2c8ed1737d6769fe1ef5c96f8a2bd75b9899316a))
### Features
- Add cli commands for mcp tools
([`f5a7541`](https://github.com/basicmachines-co/basic-memory/commit/f5a7541da17e97403b7a702720a05710f68b223a))
- Add pagination to build_context and recent_activity
([`0123544`](https://github.com/basicmachines-co/basic-memory/commit/0123544556513af943d399d70b849b142b834b15))
- Add pagination to read_notes
([`02f8e86`](https://github.com/basicmachines-co/basic-memory/commit/02f8e866923d5793d2620076c709c920d99f2c4f))
## v0.6.0 (2025-02-18)
### Chores
- Re-add sync status console on watch
([`66b57e6`](https://github.com/basicmachines-co/basic-memory/commit/66b57e682f2e9c432bffd4af293b0d1db1d3469b))
### Features
- Configure logfire telemetry ([#12](https://github.com/basicmachines-co/basic-memory/pull/12),
[`6da1438`](https://github.com/basicmachines-co/basic-memory/commit/6da143898bd45cdab8db95b5f2b75810fbb741ba))
Co-authored-by: phernandez <phernandez@basicmachines.co>
## v0.5.0 (2025-02-18)
### Features
- Return semantic info in markdown after write_note
([#11](https://github.com/basicmachines-co/basic-memory/pull/11),
[`0689e7a`](https://github.com/basicmachines-co/basic-memory/commit/0689e7a730497827bf4e16156ae402ddc5949077))
Co-authored-by: phernandez <phernandez@basicmachines.co>
## v0.4.3 (2025-02-18)
### Bug Fixes
- Re do enhanced read note format ([#10](https://github.com/basicmachines-co/basic-memory/pull/10),
[`39bd5ca`](https://github.com/basicmachines-co/basic-memory/commit/39bd5ca08fd057220b95a8b5d82c5e73a1f5722b))
Co-authored-by: phernandez <phernandez@basicmachines.co>
## v0.4.2 (2025-02-17)
## v0.4.1 (2025-02-17)
### Bug Fixes
- Fix alemic config
([`71de8ac`](https://github.com/basicmachines-co/basic-memory/commit/71de8acfd0902fc60f27deb3638236a3875787ab))
- More alembic fixes
([`30cd74e`](https://github.com/basicmachines-co/basic-memory/commit/30cd74ec95c04eaa92b41b9815431f5fbdb46ef8))
## v0.4.0 (2025-02-16)
### Features
- Import chatgpt conversation data ([#9](https://github.com/basicmachines-co/basic-memory/pull/9),
[`56f47d6`](https://github.com/basicmachines-co/basic-memory/commit/56f47d6812982437f207629e6ac9a82e0e56514e))
Co-authored-by: phernandez <phernandez@basicmachines.co>
- Import claude.ai data ([#8](https://github.com/basicmachines-co/basic-memory/pull/8),
[`a15c346`](https://github.com/basicmachines-co/basic-memory/commit/a15c346d5ebd44344b76bad877bb4d1073fcbc3b))
Import Claude.ai conversation and project data to basic-memory Markdown format.
---------
Co-authored-by: phernandez <phernandez@basicmachines.co>
## v0.3.0 (2025-02-15)
### Bug Fixes
- Refactor db schema migrate handling
([`ca632be`](https://github.com/basicmachines-co/basic-memory/commit/ca632beb6fed5881f4d8ba5ce698bb5bc681e6aa))
## v0.2.21 (2025-02-15)
### Bug Fixes
- Fix osx installer github action
([`65ebe5d`](https://github.com/basicmachines-co/basic-memory/commit/65ebe5d19491e5ff047c459d799498ad5dd9cd1a))
- Handle memory:// url format in read_note tool
([`e080373`](https://github.com/basicmachines-co/basic-memory/commit/e0803734e69eeb6c6d7432eea323c7a264cb8347))
- Remove create schema from init_db
([`674dd1f`](https://github.com/basicmachines-co/basic-memory/commit/674dd1fd47be9e60ac17508476c62254991df288))
### Features
- Set version in var, output version at startup
([`a91da13`](https://github.com/basicmachines-co/basic-memory/commit/a91da1396710e62587df1284da00137d156fc05e))
## v0.2.20 (2025-02-14)
### Bug Fixes
- Fix installer artifact
([`8de84c0`](https://github.com/basicmachines-co/basic-memory/commit/8de84c0221a1ee32780aa84dac4d3ea60895e05c))
## v0.2.19 (2025-02-14)
### Bug Fixes
- Get app artifact for installer
([`fe8c3d8`](https://github.com/basicmachines-co/basic-memory/commit/fe8c3d87b003166252290a87cbe958301cccf797))
## v0.2.18 (2025-02-14)
### Bug Fixes
- Don't zip app on release
([`8664c57`](https://github.com/basicmachines-co/basic-memory/commit/8664c57bb331d7f3f7e0239acb5386c7a3c6144e))
## v0.2.17 (2025-02-14)
### Bug Fixes
- Fix app zip in installer release
([`8fa197e`](https://github.com/basicmachines-co/basic-memory/commit/8fa197e2ec8a1b6caaf6dbb39c3c6626bba23e2e))
## v0.2.16 (2025-02-14)
### Bug Fixes
- Debug inspect build on ci
([`1d6054d`](https://github.com/basicmachines-co/basic-memory/commit/1d6054d30a477a4e6a5d6ac885632e50c01945d3))
## v0.2.15 (2025-02-14)
### Bug Fixes
- Debug installer ci
([`dab9573`](https://github.com/basicmachines-co/basic-memory/commit/dab957314aec9ed0e12abca2265552494ae733a2))
## v0.2.14 (2025-02-14)
## v0.2.13 (2025-02-14)
### Bug Fixes
- Refactor release.yml installer
([`a152657`](https://github.com/basicmachines-co/basic-memory/commit/a15265783e47c22d8c7931396281d023b3694e27))
- Try using symlinks in installer build
([`8dd923d`](https://github.com/basicmachines-co/basic-memory/commit/8dd923d5bc0587276f92b5f1db022ad9c8687e45))
## v0.2.12 (2025-02-14)
### Bug Fixes
- Fix cx_freeze options for installer
([`854cf83`](https://github.com/basicmachines-co/basic-memory/commit/854cf8302e2f83578030db05e29b8bdc4348795a))
## v0.2.11 (2025-02-14)
### Bug Fixes
- Ci installer app fix #37
([`2e215fe`](https://github.com/basicmachines-co/basic-memory/commit/2e215fe83ca421b921186c7f1989dc2cb5cca278))
## v0.2.10 (2025-02-14)
### Bug Fixes
- Fix build on github ci for app installer
([`29a2594`](https://github.com/basicmachines-co/basic-memory/commit/29a259421a0ccb10cfa68e3707eaa506ad5e55c0))
## v0.2.9 (2025-02-14)
## v0.2.8 (2025-02-14)
### Bug Fixes
- Fix installer on ci, maybe
([`edbc04b`](https://github.com/basicmachines-co/basic-memory/commit/edbc04be601d234bb1f5eb3ba24d6ad55244b031))
## v0.2.7 (2025-02-14)
### Bug Fixes
- Try to fix installer ci
([`230738e`](https://github.com/basicmachines-co/basic-memory/commit/230738ee9c110c0509e0a09cb0e101a92cfcb729))
## v0.2.6 (2025-02-14)
### Bug Fixes
- Bump project patch version
([`01d4672`](https://github.com/basicmachines-co/basic-memory/commit/01d46727b40c24b017ea9db4b741daef565ac73e))
- Fix installer setup.py change ci to use make
([`3e78fcc`](https://github.com/basicmachines-co/basic-memory/commit/3e78fcc2c208d83467fe7199be17174d7ffcad1a))
## v0.2.5 (2025-02-14)
### Bug Fixes
- Refix vitual env in installer build
([`052f491`](https://github.com/basicmachines-co/basic-memory/commit/052f491fff629e8ead629c9259f8cb46c608d584))
## v0.2.4 (2025-02-14)
## v0.2.3 (2025-02-14)
### Bug Fixes
- Workaround unsigned app
([`41d4d81`](https://github.com/basicmachines-co/basic-memory/commit/41d4d81c1ad1dc2923ba0e903a57454a0c8b6b5c))
## v0.2.2 (2025-02-14)
### Bug Fixes
- Fix path to intaller app artifact
([`53d220d`](https://github.com/basicmachines-co/basic-memory/commit/53d220df585561f9edd0d49a9e88f1d4055059cf))
## v0.2.1 (2025-02-14)
### Bug Fixes
- Activate vitualenv in installer build
([`d4c8293`](https://github.com/basicmachines-co/basic-memory/commit/d4c8293687a52eaf3337fe02e2f7b80e4cc9a1bb))
- Trigger installer build on release
([`f11bf78`](https://github.com/basicmachines-co/basic-memory/commit/f11bf78f3f600d0e1b01996cf8e1f9c39e3dd218))
## v0.2.0 (2025-02-14)
### Features
- Build installer via github action ([#7](https://github.com/basicmachines-co/basic-memory/pull/7),
[`7c381a5`](https://github.com/basicmachines-co/basic-memory/commit/7c381a59c962053c78da096172e484f28ab47e96))
* feat(ci): build installer via github action
* enforce conventional commits in PR titles
* feat: add icon to installer
---------
Co-authored-by: phernandez <phernandez@basicmachines.co>
## v0.1.2 (2025-02-14)
### Bug Fixes
- Fix installer for mac
([`dde9ff2`](https://github.com/basicmachines-co/basic-memory/commit/dde9ff228b72852b5abc58faa1b5e7c6f8d2c477))
- Remove unused FileChange dataclass
([`eb3360c`](https://github.com/basicmachines-co/basic-memory/commit/eb3360cc221f892b12a17137ae740819d48248e8))
- Update uv installer url
([`2f9178b`](https://github.com/basicmachines-co/basic-memory/commit/2f9178b0507b3b69207d5c80799f2d2f573c9a04))
## v0.1.1 (2025-02-07)
## v0.1.0 (2025-02-07)
### Bug Fixes
- Create virtual env in test workflow
([`8092e6d`](https://github.com/basicmachines-co/basic-memory/commit/8092e6d38d536bfb6f93c3d21ea9baf1814f9b0a))
- Fix permalink uniqueness violations on create/update/sync
([`135bec1`](https://github.com/basicmachines-co/basic-memory/commit/135bec181d9b3d53725c8af3a0959ebc1aa6afda))
- Fix recent activity bug
([`3d2c0c8`](https://github.com/basicmachines-co/basic-memory/commit/3d2c0c8c32fcfdaf70a1f96a59d8f168f38a1aa9))
- Install fastapi deps after removing basic-foundation
([`51a741e`](https://github.com/basicmachines-co/basic-memory/commit/51a741e7593a1ea0e5eb24e14c70ff61670f9663))
- Recreate search index on db reset
([`1fee436`](https://github.com/basicmachines-co/basic-memory/commit/1fee436bf903a35c9ebb7d87607fc9cc9f5ff6e7))
- Remove basic-foundation from deps
([`b8d0c71`](https://github.com/basicmachines-co/basic-memory/commit/b8d0c7160f29c97cdafe398a7e6a5240473e0c89))
- Run tests via uv
([`4eec820`](https://github.com/basicmachines-co/basic-memory/commit/4eec820a32bc059a405e2f4dac4c73b245ca4722))
### Chores
- Rename import tool
([`af6b7dc`](https://github.com/basicmachines-co/basic-memory/commit/af6b7dc40a55eaa2aa78d6ea831e613851081d52))
### Features
- Add memory-json importer, tweak observation content
([`3484e26`](https://github.com/basicmachines-co/basic-memory/commit/3484e26631187f165ee6eb85517e94717b7cf2cf))
## v0.0.1 (2025-02-04)
### Bug Fixes
- Fix versioning for 0.0.1 release
([`ba1e494`](https://github.com/basicmachines-co/basic-memory/commit/ba1e494ed1afbb7af3f97c643126bced425da7e0))
## v0.0.0 (2025-02-04)
### Chores
- Remove basic-foundation src ref in pyproject.toml
([`29fce8b`](https://github.com/basicmachines-co/basic-memory/commit/29fce8b0b922d54d7799bf2534107ee6cfb961b8))
-10
View File
@@ -1,10 +0,0 @@
cff-version: 1.0.3
message: "If you use this project, please cite it as follows:"
authors:
- family-names: "Hernandez"
given-names: "Paul"
affiliation: "Basic Machines"
title: "Basic Memory"
version: "0.0.1"
date-released: "2025-02-03"
url: "https://github.com/basicmachines-co/basic-memory"
-34
View File
@@ -1,34 +0,0 @@
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.
-171
View File
@@ -1,171 +0,0 @@
# 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: `make install` or `pip install -e ".[dev]"`
- Run tests: `uv run pytest -p pytest_mock -v` or `make test`
- Single test: `pytest tests/path/to/test_file.py::test_function_name`
- Lint: `make lint` or `ruff check . --fix`
- Type check: `make type-check` or `uv run pyright`
- Format: `make format` or `uv run ruff format .`
- Run all code checks: `make check` (runs lint, format, type-check, test)
- Create db migration: `make migration m="Your migration message"`
- Run development MCP Inspector: `make 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)
### 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 enviroment with in memory SQLite and tmp_file directory
## 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
- `read_file(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
**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")
**Search & Discovery:**
- `search(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(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 has taken AI-Human collaboration to the next level by integrating Claude directly into the development workflow through GitHub:
### GitHub MCP Tools
Using the GitHub Model Context Protocol server, Claude can now:
- **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
This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.
-19
View File
@@ -1,19 +0,0 @@
# Code of Conduct
## Purpose
Maintain a respectful and professional environment where contributions can be made without harassment or
negativity.
## Standards
Respectful communication and collaboration are expected. Offensive behavior, harassment, or personal attacks will not be
tolerated.
## Reporting Issues
To report inappropriate behavior, contact [paul@basicmachines.co].
## Consequences
Violations of this code may lead to consequences, including being banned from contributing to the project.
-163
View File
@@ -1,163 +0,0 @@
# Contributing to Basic Memory
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.
## Getting Started
### Development Environment
1. **Clone the Repository**:
```bash
git clone https://github.com/basicmachines-co/basic-memory.git
cd basic-memory
```
2. **Install Dependencies**:
```bash
# Using make (recommended)
make install
# Or using uv
uv install -e ".[dev]"
# Or using pip
pip install -e ".[dev]"
```
3. **Run the Tests**:
```bash
# Run all tests
make 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
make check
# Or run individual checks
make lint # Run linting
make format # Format code
make type-check # Type checking
```
5. **Test Your Changes**: Ensure all tests pass locally and maintain 100% test coverage.
```bash
make 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
## 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!
-16
View File
@@ -1,16 +0,0 @@
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM python:3.12-slim
WORKDIR /app
# Copy the project files
COPY . .
# Install pip and build dependencies
RUN pip install --upgrade pip \
&& pip install . --no-cache-dir --ignore-installed
# Expose port if necessary (e.g., uv might use a port, but MCP over stdio so not needed here)
# Use the basic-memory entrypoint to run the MCP server
CMD ["basic-memory", "mcp"]
-661
View File
@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
-67
View File
@@ -1,67 +0,0 @@
.PHONY: install test test-module lint clean format type-check installer-mac installer-win check
install:
pip install -e ".[dev]"
test:
uv run pytest -p pytest_mock -v
# Run tests for a specific module
# Usage: make test-module m=path/to/module.py [cov=module_path]
test-module:
@if [ -z "$(m)" ]; then \
echo "Usage: make test-module m=path/to/module.py [cov=module_path]"; \
exit 1; \
fi; \
if [ -z "$(cov)" ]; then \
uv run pytest $(m) -v; \
else \
uv run pytest $(m) -v --cov=$(cov); \
fi
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-inspector:
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 --upgrade
check: lint format type-check test
# Target for generating Alembic migrations with a message from command line
migration:
@if [ -z "$(m)" ]; then \
echo "Usage: make migration m=\"Your migration message\""; \
exit 1; \
fi; \
cd src/basic_memory/alembic && alembic revision --autogenerate -m "$(m)"
-352
View File
@@ -1,352 +0,0 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![PyPI version](https://badge.fury.io/py/basic-memory.svg)](https://badge.fury.io/py/basic-memory)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://github.com/basicmachines-co/basic-memory/workflows/Tests/badge.svg)](https://github.com/basicmachines-co/basic-memory/actions)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![smithery badge](https://smithery.ai/badge/@basicmachines-co/basic-memory)](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
enable any compatible LLM to read and write to your local knowledge base.
- Website: http://basicmachines.co
- Documentation: http://memory.basicmachines.co
## Pick up your conversation right where you left off
- AI assistants can load context from local files in a new conversation
- Notes are saved locally as Markdown files in real time
- No project knowledge or special prompting required
https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
## Quick Start
```bash
# Install with uv (recommended)
uv tool 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",
"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. The
Smithery server hosts the MCP server component, 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 can be enabled via running `basic-memory sync --watch`
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 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](#multiple-projects) below), update your Claude Desktop
config:
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp",
"--project",
"your-project-name"
]
}
}
}
```
2. Sync your knowledge:
```bash
# One-time sync of local knowledge updates
basic-memory sync
# Run realtime sync process (recommended)
basic-memory sync --watch
```
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
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
search(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
```
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?"
```
## 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)
## License
AGPL-3.0
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
and submitting PRs.
Built with ♥️ by Basic Machines
-13
View File
@@ -1,13 +0,0 @@
# 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
View File
File diff suppressed because it is too large Load Diff
-396
View File
@@ -1,396 +0,0 @@
---
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.
# 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).
## 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
```python
# Writing knowledge - THE MOST IMPORTANT TOOL!
response = await write_note(
title="Search Design", # Required: Note title
content="# Search Design\n...", # Required: Note content
folder="specs", # Optional: Folder to save in
tags=["search", "design"], # Optional: Tags for categorization
verbose=True # Optional: Get parsing details
)
# Reading knowledge
content = await read_note("Search Design") # By title
content = await read_note("specs/search-design") # By path
content = await read_note("memory://specs/search") # By memory URL
# Searching for knowledge
results = await search(
query="authentication system", # Text to search for
page=1, # Optional: Pagination
page_size=10 # Optional: Results per page
)
# Building context from the knowledge graph
context = await build_context(
url="memory://specs/search", # Starting point
depth=2, # Optional: How many hops to follow
timeframe="1 month" # Optional: Recent timeframe
)
# Checking recent changes
activity = await recent_activity(
type="all", # Optional: Entity types to include
depth=1, # Optional: Related items to include
timeframe="1 week" # Optional: Time window
)
# Creating a knowledge visualization
canvas_result = await canvas(
nodes=[{"id": "note1", "label": "Search Design"}], # Nodes to display
edges=[{"from": "note1", "to": "note2"}], # Connections
title="Project Overview", # Canvas title
folder="diagrams" # Storage location
)
```
## 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() to find relevant notes]
[Then build_context() to understand connections]
```
## 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**
- Using the same title+folder will overwrite existing notes
- Structure content with clear headings and sections
- Use semantic markup for observations and relations
- Keep files organized in logical folders
## Common Knowledge Patterns
### Capturing Decisions
```markdown
# 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
## Relations
- pairs_with [[Light Roast Beans]]
- contrasts_with [[French Press Method]]
- requires [[Proper Grinding Technique]]
```
### 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]]
```
### Creating Effective Relations
When creating relations, you can:
1. Reference existing entities by their exact title
2. Create forward references to entities that don't exist yet
```python
# Example workflow for creating notes with effective relations
async def create_note_with_effective_relations():
# Search for existing entities to reference
search_results = await search("travel")
existing_entities = [result.title for result in search_results.primary_results]
# Check if specific entities exist
packing_tips_exists = "Packing Tips" in existing_entities
japan_travel_exists = "Japan Travel Guide" in existing_entities
# Prepare relations section - include both existing and forward references
relations_section = "## Relations\n"
# Existing reference - exact match to known entity
if packing_tips_exists:
relations_section += "- references [[Packing Tips]]\n"
else:
# Forward reference - will be linked when that entity is created later
relations_section += "- references [[Packing Tips]]\n"
# Another possible reference
if japan_travel_exists:
relations_section += "- part_of [[Japan Travel Guide]]\n"
# You can also check recently modified notes to reference them
recent = await recent_activity(timeframe="1 week")
recent_titles = [item.title for item in recent.primary_results]
if "Transportation Options" in recent_titles:
relations_section += "- relates_to [[Transportation Options]]\n"
# Always include meaningful forward references, even if they don't exist yet
relations_section += "- located_in [[Tokyo]]\n"
relations_section += "- visited_during [[Spring 2023 Trip]]\n"
# Now create the note with both verified and forward relations
content = f"""# Tokyo Neighborhood Guide
## Overview
Details about different Tokyo neighborhoods and their unique characteristics.
## Observations
- [area] Shibuya is a busy shopping district #shopping
- [transportation] Yamanote Line connects major neighborhoods #transit
- [recommendation] Visit Shimokitazawa for vintage shopping #unique
- [tip] Get a Suica card for easy train travel #convenience
{relations_section}
"""
result = await write_note(
title="Tokyo Neighborhood Guide",
content=content,
verbose=True
)
# You can check which relations were resolved and which are forward references
if result and 'relations' in result:
resolved = [r['to_name'] for r in result['relations'] if r.get('target_id')]
forward_refs = [r['to_name'] for r in result['relations'] if not r.get('target_id')]
print(f"Resolved relations: {resolved}")
print(f"Forward references that will be resolved later: {forward_refs}")
```
## Error Handling
Common issues to watch for:
1. **Missing Content**
```python
try:
content = await read_note("Document")
except:
# Try search instead
results = await search("Document")
if results and results.primary_results:
# Found something similar
content = await read_note(results.primary_results[0].permalink)
```
2. **Forward References (Unresolved Relations)**
```python
response = await write_note(..., verbose=True)
# Check for forward references (unresolved relations)
forward_refs = []
for relation in response.get('relations', []):
if not relation.get('target_id'):
forward_refs.append(relation.get('to_name'))
if forward_refs:
# This is a feature, not an error! Inform the user about forward references
print(f"Note created with forward references to: {forward_refs}")
print("These will be automatically linked when those notes are created.")
# Optionally suggest creating those entities now
print("Would you like me to create any of these notes now to complete the connections?")
```
3. **Sync Issues**
```python
# If information seems outdated
activity = await recent_activity(timeframe="1 hour")
if not activity or not activity.primary_results:
print("It seems there haven't been recent updates. You might need to run 'basic-memory sync'.")
```
## 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()` 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
-289
View File
@@ -1,289 +0,0 @@
---
title: CLI Reference
type: note
permalink: docs/cli-reference
---
# CLI Reference
Basic Memory provides command line tools for managing your knowledge base. This reference covers the available commands and their options.
## Core Commands
### sync
Keeps files and the knowledge graph in sync:
```bash
# Basic sync
basic-memory sync
# Watch for changes
basic-memory sync --watch
# Sync specific folder
basic-memory sync path/to/folder
```
Options:
- `--watch`: Continuously monitor for changes
- `--verbose`: Show detailed output
- `PATH`: Optional path to sync (defaults to ~/basic-memory)
### import
Imports external knowledge sources:
```bash
# Claude conversations
basic-memory import claude conversations
# Claude projects
basic-memory import claude projects
# ChatGPT history
basic-memory import chatgpt
```
Options:
- `--folder PATH`: Target folder for imported content
- `--overwrite`: Replace existing files
- `--skip-existing`: Keep existing files
### status
Shows system status information:
```bash
# Basic status check
basic-memory status
# Detailed status
basic-memory status --verbose
# JSON output
basic-memory status --json
```
### project
Create multiple projects to manage your knowledge.
```bash
# List all configured projects
basic-memory project list
# Add a new project
basic-memory project add work ~/work-basic-memory
# Set the default project
basic-memory project default work
# Remove a project (doesn't delete files)
basic-memory project remove personal
# Show current project
basic-memory project current
```
> Be sure to restart Claude Desktop after changing projects.
#### Using Projects in Commands
All commands support the `--project` flag to specify which project to use:
```bash
# Sync a specific project
basic-memory --project=work sync
# Run MCP server for a specific project
basic-memory --project=personal mcp
```
You can also set the `BASIC_MEMORY_PROJECT` environment variable:
```bash
BASIC_MEMORY_PROJECT=work basic-memory sync
```
### help
The full list of commands and help for each can be viewed with the `--help` argument.
```
✗ basic-memory --help
Usage: basic-memory [OPTIONS] COMMAND [ARGS]...
Basic Memory - Local-first personal knowledge management system.
╭─ Options ─────────────────────────────────────────────────────────────────────────────────╮
│ --project -p TEXT Specify which project to use │
│ [env var: BASIC_MEMORY_PROJECT] │
│ [default: None] │
│ --version -V Show version information and exit. │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it or │
│ customize the installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────────────────────────────╮
│ sync Sync knowledge files with the database. │
│ status Show sync status between files and database. │
│ reset Reset database (drop all tables and recreate). │
│ mcp Run the MCP server for Claude Desktop integration. │
│ import Import data from various sources │
│ tool Direct access to MCP tools via CLI │
│ project Manage multiple Basic Memory projects │
╰───────────────────────────────────────────────────────────────────────────────────────────╯
```
## Initial Setup
```bash
# Install Basic Memory
uv install basic-memory
# First sync
basic-memory sync
# Start watching mode
basic-memory sync --watch
```
> **Important**: You need to install Basic Memory via `uv` or `pip` to use the command line tools, see [[Getting Started with Basic Memory#Installation]].
## Regular Usage
```bash
# Check status
basic-memory status
# Import new content
basic-memory import claude conversations
# Sync changes
basic-memory sync
# Sync changes continuously
basic-memory sync --watch
```
## Maintenance Tasks
```bash
# Check system status in detail
basic-memory status --verbose
# Full resync of all files
basic-memory sync
# Import updates to specific folder
basic-memory import claude conversations --folder new
```
## Using stdin with Basic Memory's `write_note` Tool
The `write-note` tool supports reading content from standard input (stdin), allowing for more flexible workflows when creating or updating notes in your Basic Memory knowledge base.
### Use Cases
This feature is particularly useful for:
1. **Piping output from other commands** directly into Basic Memory notes
2. **Creating notes with multi-line content** without having to escape quotes or special characters
3. **Integrating with AI assistants** like Claude Code that can generate content and pipe it to Basic Memory
4. **Processing text data** from files or other sources
### Basic Usage
#### Method 1: Using a Pipe
You can pipe content from another command into `write_note`:
```bash
# Pipe output of a command into a new note
echo "# My Note\n\nThis is a test note" | basic-memory tool write-note --title "Test Note" --folder "notes"
# Pipe output of a file into a new note
cat README.md | basic-memory tool write-note --title "Project README" --folder "documentation"
# Process text through other tools before saving as a note
cat data.txt | grep "important" | basic-memory tool write-note --title "Important Data" --folder "data"
```
#### Method 2: Using Heredoc Syntax
For multi-line content, you can use heredoc syntax:
```bash
# Create a note with heredoc
cat << EOF | basic-memory tool write_note --title "Project Ideas" --folder "projects"
# Project Ideas for Q2
## AI Integration
- Improve recommendation engine
- Add semantic search to product catalog
## Infrastructure
- Migrate to Kubernetes
- Implement CI/CD pipeline
EOF
```
#### Method 3: Input Redirection
You can redirect input from a file:
```bash
# Create a note from file content
basic-memory tool write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
```
#### Integration with Claude Code
This feature works well with Claude Code in the terminal:
In a Claude Code session, let Claude know he can use the basic-memory tools, then he can execute them via the cli:
```
⏺ Bash(echo "# Test Note from Claude\n\nThis is a test note created by Claude to test the stdin functionality." | basic-memory tool write-note --title "Claude Test Note" --folder "test" --tags "test" --tags "claude")…
  ⎿  # Created test/Claude Test Note.md (23e00eec)
permalink: test/claude-test-note
## Tags
- test, claude
```
## Troubleshooting Common Issues
### Sync Conflicts
If you encounter a file changed during sync error:
1. Check the file referenced in the error message
2. Resolve any conflicts manually
3. Run sync again
### Import Errors
If import fails:
1. Check that the source file is in the correct format
2. Verify permissions on the target directory
3. Use --verbose flag for detailed error information
### Status Issues
If status shows problems:
1. Note any unresolved relations or warnings
2. Run a full sync to attempt automatic resolution
3. Check file permissions if database access errors occur
## Relations
- used_by [[Getting Started with Basic Memory]] (Installation instructions)
- complements [[User Guide]] (How to use Basic Memory)
- relates_to [[Introduction to Basic Memory]] (System overview)
-107
View File
@@ -1,107 +0,0 @@
---
title: Canvas Visualizations
type: note
permalink: docs/canvas
tags:
- visualization
- mapping
- obsidian
---
# Canvas Visualizations
Basic Memory can create visual knowledge maps using Obsidian's Canvas feature. These visualizations help you understand relationships between concepts, map out processes, and visualize your knowledge structure.
## Creating Canvas Visualizations
Ask Claude to create a visualization by describing what you want to map:
```
You: "Create a canvas visualization of my project components and their relationships."
You: "Make a concept map showing the main themes from our discussion about climate change."
You: "Can you make a canvas diagram of the perfect pour over method?"
```
![[Canvas.png]]
## Types of Visualizations
Basic Memory can create several types of visual maps:
### Document Maps
Visualize connections between your notes and documents
### Concept Maps
Create visual representations of ideas and their relationships
### Process Diagrams
Map workflows, sequences, and procedures
### Thematic Analysis
Organize ideas around central themes
### Relationship Networks
Show how different entities relate to each other
## Visualization Sources
Claude can create visualizations based on:
### Documents in Your Knowledge Base
```
You: "Create a canvas showing the connections between my project planning documents"
```
### Conversation Content
```
You: "Make a canvas visualization of the main points we just discussed"
```
### Search Results
```
You: "Find all my notes about psychology and create a visual map of the concepts"
```
### Themes and Relationships
```
You: "Create a visual map showing how different philosophical schools relate to each other"
```
## Visualization Workflow
1. **Request a visualization** by describing what you want to see
2. **Claude creates the canvas file** in your Basic Memory directory
3. **Open the file in Obsidian** to view the visualization
4. **Refine the visualization** by asking Claude for adjustments:
```
You: "Could you reorganize the canvas to group related components together?"
You: "Please add more detail about the connection between these two concepts."
```
## Technical Details
Behind the scenes, Claude:
1. Creates a `.canvas` file in JSON format
2. Adds nodes for each concept or document
3. Creates edges to represent relationships
4. Sets positions for visual clarity
5. Includes any relevant metadata
The resulting file is fully compatible with Obsidian's Canvas feature and can be edited directly in Obsidian.
## Tips for Effective Visualizations
- **Be specific** about what you want to visualize
- **Specify the level of detail** you need
- **Mention the visualization type** you want (concept map, process flow, etc.)
- **Start simple** and ask for refinements
- **Provide context** about what documents or concepts to include
## Relations
- enhances [[Obsidian Integration]] (Using Basic Memory with Obsidian)
- visualizes [[Knowledge Format]] (The structure of your knowledge)
- complements [[User Guide]] (Ways to use Basic Memory)
-267
View File
@@ -1,267 +0,0 @@
---
title: Getting Started with Basic Memory
type: note
permalink: docs/getting-started
---
# Getting Started with Basic Memory
This guide will help you install Basic Memory, configure it with Claude Desktop, and create your first knowledge notes
through conversations.
Basic Memory uses the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) to connect with LLMs.
It can be used with any service that supports the MCP, but Claude Desktop works especially well.
## Installation
### 1. Install Basic Memory
```bash
# Install with uv (recommended).
uv tool install basic-memory
# Or with pip
pip install basic-memory
```
> **Important**: You need to install Basic Memory using one of the commands above to use the command line tools.
Using `uv tool install` will install the basic-memory package in a standalone virtual environment.
See the [UV docs](https://docs.astral.sh/uv/concepts/tools/) for more info.
### 2. Configure Claude Desktop
Claude Desktop often has trouble finding executables in your user path. Follow these steps for a reliable setup:
#### Step 1: Find the absolute path to uvx
Open Terminal and run:
```bash
which uvx
```
This will show you the full path (e.g., `/Users/yourusername/.cargo/bin/uvx`).
#### Step 2: Edit Claude Desktop Configuration
Edit the configuration file located at `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"basic-memory": {
"command": "/absolute/path/to/uvx",
"args": [
"basic-memory",
"mcp"
]
}
}
}
```
Replace `/absolute/path/to/uvx` with the actual path you found in Step 1.
> **Note**: Using absolute paths is necessary because Claude Desktop cannot access binaries in your user PATH.
#### Step 3: Restart Claude Desktop
Close and reopen Claude Desktop for the changes to take effect.
### 3. Start the Sync Service
Start the sync service to monitor your files for changes:
```bash
# One-time sync
basic-memory sync
# For continuous monitoring (recommended)
basic-memory sync --watch
```
The `--watch` flag enables automatic detection of file changes, keeping your knowledge base current.
### 4. Staying Updated
To update Basic Memory when new versions are released:
```bash
# Update with uv (recommended)
uv tool upgrade basic-memory
# Or with pip
pip install --upgrade basic-memory
```
> **Note**: After updating, you'll need to restart Claude Desktop and your sync process for changes to take effect.
## Troubleshooting Installation
### Common Issues
#### Claude Says "No Basic Memory Tools Available"
If Claude cannot find Basic Memory tools:
1. **Check absolute paths**: Ensure you're using complete absolute paths to uvx in the Claude Desktop configuration
2. **Verify installation**: Run `basic-memory --version` in Terminal to confirm Basic Memory is installed
3. **Restart applications**: Restart both Terminal and Claude Desktop after making configuration changes
4. **Check sync status**: Ensure `basic-memory sync --watch` is running
#### Permission Issues
If you encounter permission errors:
1. Check that Basic Memory has access to create files in your home directory
2. Ensure Claude Desktop has permission to execute the uvx command
## Creating Your First Knowledge Note
1. **Start the sync process** in a Terminal window:
```bash
basic-memory sync --watch
```
Keep this running in the background.
2. **Open Claude Desktop** and start a new conversation.
3. **Have a natural conversation** about any topic:
```
You: "Let's talk about coffee brewing methods I've been experimenting with."
Claude: "I'd be happy to discuss coffee brewing methods..."
You: "I've found that pour over gives more flavor clarity than French press..."
```
4. **Ask Claude to create a note**:
```
You: "Could you create a note summarizing what we've discussed about coffee brewing?"
```
5. **Confirm note creation**:
Claude will confirm when the note has been created and where it's stored.
6. **View the created file** in your `~/basic-memory` directory using any text editor or Obsidian.
The file structure will look similar to:
```markdown
---
title: Coffee Brewing Methods
permalink: coffee-brewing-methods
---
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more clarity...
- [technique] Water temperature at 205°F...
## Relations
- relates_to [[Other Coffee Topics]]
```
## Using Special Prompts
Basic Memory includes special prompts that help you start conversations with context from your knowledge base:
### Continue Conversation
To resume a previous topic:
```
You: "Let's continue our conversation about coffee brewing."
```
This prompt triggers Claude to:
1. Search your knowledge base for relevant content about coffee brewing
2. Build context from these documents
3. Resume the conversation with full awareness of previous discussions
### Recent Activity
To see what you've been working on:
```
You: "What have we been discussing recently?"
```
This prompt causes Claude to:
1. Retrieve documents modified in the recent past
2. Summarize the topics and main points
3. Offer to continue any of those discussions
### Search
To find specific information:
```
You: "Find information about pour over coffee methods."
```
Claude will:
1. Search your knowledge base for relevant documents
2. Summarize the key findings
3. Offer to explore specific documents in more detail
See [[User Guide#Using Special Prompts]] for further information.
## Using Your Knowledge Base
### Referencing Knowledge
In future conversations, reference your existing knowledge:
```
You: "What water temperature did we decide was optimal for coffee brewing?"
```
Or directly reference notes using memory:// URLs:
```
You: "Take a look at memory://coffee-brewing-methods and let's discuss how to improve my technique."
```
### Building On Previous Knowledge
Basic Memory enables continuous knowledge building:
1. **Reference previous discussions** in new conversations
2. **Add to existing notes** through conversations
3. **Create connections** between related topics
4. **Follow relationships** to build comprehensive context
## Importing Existing Conversations
Import your existing AI conversations:
```bash
# From Claude
basic-memory import claude conversations
# From ChatGPT
basic-memory import chatgpt
```
After importing, run `basic-memory sync` to index everything.
## Quick Tips
- Keep `basic-memory sync --watch` running in a terminal window
- Use special prompts (Continue Conversation, Recent Activity, Search) to start contextual discussions
- Build connections between notes for a richer knowledge graph
- Use direct memory:// URLs when you need precise context
- Use git to version control your knowledge base
- Review and edit AI-generated notes for accuracy
## Next Steps
After getting started, explore these areas:
1. **Read the [[User Guide]]** for comprehensive usage instructions
2. **Understand the [[Knowledge Format]]** to learn how knowledge is structured
3. **Set up [[Obsidian Integration]]** for visual knowledge navigation
4. **Learn about [[Canvas]]** visualizations for mapping concepts
5. **Review the [[CLI Reference]]** for command line tools
-195
View File
@@ -1,195 +0,0 @@
---
title: Knowledge Format
type: note
permalink: docs/knowledge-format
tags:
- architecture
- patterns
- knowledge
- design
---
# Knowledge Format
Basic Memory uses standard Markdown with simple semantic patterns to create a knowledge graph. This document details the file structure and patterns used to organize knowledge.
## File-First Architecture
All knowledge in Basic Memory is stored in plain text Markdown files:
- Files are the source of truth for all knowledge
- Changes to files automatically update the knowledge graph
- You maintain complete ownership and control
- Files work with git and other version control systems
- Knowledge persists independently of any AI conversation
## Core Document Structure
Every document uses this basic structure:
```markdown
---
title: Document Title
type: note
tags: [tag1, tag2]
permalink: custom-path
---
# Document Title
Regular markdown content...
## Observations
- [category] Content with #tags (optional context)
## Relations
- relation_type [[Other Document]] (optional context)
```
### Frontmatter
The YAML frontmatter at the top of each file defines essential metadata:
```yaml
---
title: Document Title # Used for linking and references
type: note # Document type
tags: [tag1, tag2] # For organization and searching
permalink: custom-link # Optional custom URL path
---
```
The title is particularly important as it's used to create links between documents.
### Observations
Observations are facts or statements about a topic:
```markdown
## Observations
- [tech] Uses SQLite for storage #database
- [design] Follows local-first architecture #architecture
- [decision] Selected bcrypt for passwords #security (Based on audit)
```
Each observation contains:
- **Category** in [brackets] - classifies the information type
- **Content text** - the main information
- Optional **#tags** - additional categorization
- Optional **(context)** - supporting details
Common categories include:
- `[tech]`: Technical details
- `[design]`: Architecture decisions
- `[feature]`: User capabilities
- `[decision]`: Choices that were made
- `[principle]`: Fundamental concepts
- `[method]`: Approaches or techniques
- `[preference]`: Personal opinions
### Relations
Relations connect documents to form the knowledge graph:
```markdown
## Relations
- implements [[Search Design]]
- depends_on [[Database Schema]]
- relates_to [[User Interface]]
```
You can also create inline references:
```markdown
This builds on [[Core Design]] and uses [[Utility Functions]].
```
Common relation types include:
- `implements`: Implementation of a specification
- `depends_on`: Required dependency
- `relates_to`: General connection
- `inspired_by`: Source of ideas
- `extends`: Enhancement
- `part_of`: Component relationship
- `contains`: Hierarchical relationship
- `pairs_with`: Complementary relationship
## Knowledge Graph
Basic Memory automatically builds a knowledge graph from your document connections:
- Each document becomes a node in the graph
- Relations create edges between nodes
- Relation types add semantic meaning to connections
- Forward references can link to documents that don't exist yet
This graph enables rich context building and navigation across your knowledge base.
## Permalinks and memory:// URLs
Every document in Basic Memory has a unique permalink that serves as its stable identifier:
### How Permalinks Work
- **Automatically assigned**: The system generates a permalink for each document
- **Based on title**: By default, derived from the document title
- **Always unique**: If conflicts exist, the system adds a suffix to ensure uniqueness
- **Stable reference**: Remains the same even if the file moves in the directory structure
- **Used in memory:// URLs**: Forms the basis of the memory:// addressing scheme
You can specify a custom permalink in the frontmatter:
```yaml
---
title: Authentication Approaches
permalink: auth-approaches-2024
---
```
If not specified, one will be generated automatically from the title.
### Using memory:// URLs
The memory:// URL scheme provides a reliable way to reference knowledge:
```
memory://auth-approaches-2024 # Direct access by permalink
memory://Authentication Approaches # Access by title (automatically resolves)
memory://project/auth-approaches # Access by path
```
Memory URLs support pattern matching for more powerful queries:
```
memory://auth* # All documents with permalinks starting with "auth"
memory://*/approaches # All documents with permalinks ending with "approaches"
memory://project/*/requirements # All requirements documents in the project folder
memory://docs/search/implements/* # Follow all implements relations from search docs
```
This addressing scheme ensures content remains accessible even as your knowledge base evolves and files are reorganized.
## File Organization
Organize files in any structure that suits your needs:
```
docs/
architecture/
design.md
patterns.md
features/
search.md
auth.md
```
You can:
- Group by topic in folders
- Use a flat structure with descriptive filenames
- Tag files for easier discovery
- Add custom metadata in frontmatter
The system will build the semantic knowledge graph regardless of how you organize your files.
## Relations
- implemented_by [[User Guide]] (How to work with this format)
- relates_to [[Getting Started with Basic Memory]] (Setup instructions)
- explained_in [[Introduction to Basic Memory]] (Overview of the system)
-146
View File
@@ -1,146 +0,0 @@
---
title: Obsidian Integration
type: note
permalink: docs/obsidian-integration
---
# Obsidian Integration
Basic Memory integrates seamlessly with [Obsidian](https://obsidian.md), providing powerful visualization and navigation capabilities for your knowledge graph.
## Setup
### Creating an Obsidian Vault
1. Download and install [Obsidian](https://obsidian.md)
2. Create a new vault
3. Point it to your Basic Memory directory (~/basic-memory by default)
4. Enable core plugins like Graph View, Backlinks, and Tags
## Visualization Features
### Graph View
Obsidian's Graph View provides a visual representation of your knowledge network:
- Each document appears as a node
- Relations appear as connections between nodes
- Colors can be customized to distinguish types
- Filters let you focus on specific aspects
- Local graphs show connections for individual documents
### Backlinks
Obsidian automatically tracks references between documents:
- View all documents that reference the current one
- See the exact context of each reference
- Navigate easily through connections
- Track how concepts relate to each other
### Tag Explorer
Use tags to organize and filter content:
- View all tags in your knowledge base
- See how many documents use each tag
- Filter documents by tag combinations
- Create hierarchical tag structures
## Knowledge Elements
Basic Memory's knowledge format works natively with Obsidian:
### Wiki Links
```markdown
## Relations
- implements [[Search Design]]
- depends_on [[Database Schema]]
```
These display as clickable links in Obsidian and appear in the graph view.
### Observations with Tags
```markdown
## Observations
- [tech] Using SQLite #database
- [design] Local-first #architecture
```
Tags become searchable and filterable in Obsidian's tag pane.
### Frontmatter
```yaml
---
title: Document Title
type: note
tags: [search, design]
---
```
Frontmatter provides metadata for Obsidian to use in search and filtering.
## Canvas Integration
Basic Memory can create [Obsidian Canvas](https://obsidian.md/canvas) files:
1. Ask Claude to create a visualization:
```
You: "Create a canvas showing the structure of our project components."
```
2. Claude generates a .canvas file in your knowledge base
3. Open the file in Obsidian to view and edit the visual representation
4. Canvas files maintain references to your documents
## Recommended Plugins
These Obsidian plugins work especially well with Basic Memory:
- **Dataview**: Query your knowledge base programmatically
- **Kanban**: Organize tasks from knowledge files
- **Calendar**: View and navigate temporal knowledge
- **Templates**: Create consistent knowledge structures
## Workflow Suggestions
### Daily Notes
```markdown
# 2024-01-21
## Progress
- Updated [[Search Design]]
- Fixed [[Bug Report 123]]
## Notes
- [idea] Better indexing #enhancement
- [todo] Update docs #documentation
## Links
- relates_to [[Current Sprint]]
- updates [[Project Status]]
```
### Project Tracking
```markdown
# Current Sprint
## Tasks
- [ ] Update [[Search]]
- [ ] Fix [[Auth Bug]]
## Tags
#sprint #planning #current
```
## Relations
- enhances [[Introduction to Basic Memory]] (Overview of system)
- relates_to [[Canvas]] (Visual knowledge mapping)
- complements [[User Guide]] (Using Basic Memory)
-243
View File
@@ -1,243 +0,0 @@
---
title: Technical Information
type: note
permalink: docs/technical-information
---
# Technical Information
This document provides technical details about Basic Memory's implementation, licensing, and integration with the Model Context Protocol (MCP).
## Architecture
Basic Memory consists of:
1. **Core Knowledge Engine**: Parses and indexes Markdown files
2. **SQLite Database**: Provides fast querying and search
3. **MCP Server**: Implements the Model Context Protocol
4. **CLI Tools**: Command-line utilities for management
5. **Sync Service**: Monitors file changes and updates the database
The system follows a file-first architecture where all knowledge is represented in standard Markdown files and the database serves as a secondary index.
## Model Context Protocol (MCP)
Basic Memory implements the [Model Context Protocol](https://github.com/modelcontextprotocol/spec), an open standard for enabling AI models to access external tools:
- **Standardized Interface**: Common protocol for tool integration
- **Tool Registration**: Basic Memory registers as a tool provider
- **Asynchronous Communication**: Enables efficient interaction with AI models
- **Standardized Schema**: Structured data exchange format
Integration with Claude Desktop uses the MCP to grant Claude access to your knowledge base through a set of specialized tools that search, read, and write knowledge.
## Licensing
Basic Memory is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.en.html):
- **Free Software**: You can use, study, share, and modify the software
- **Copyleft**: Derivative works must be distributed under the same license
- **Network Use**: Network users must be able to receive the source code
- **Commercial Use**: Allowed, subject to license requirements
The AGPL license ensures Basic Memory remains open source while protecting against proprietary forks.
## Source Code
Basic Memory is developed as an open-source project:
- **GitHub Repository**: [https://github.com/basicmachines-co/basic-memory](https://github.com/basicmachines-co/basic-memory)
- **Issue Tracker**: Report bugs and request features on GitHub
- **Contributions**: Pull requests are welcome following the contributing guidelines
- **Documentation**: Source for this documentation is also available in the repository
## Data Storage and Privacy
Basic Memory is designed with privacy as a core principle:
- **Local-First**: All data remains on your local machine
- **No Cloud Dependency**: No remote servers or accounts required
- **Telemetry**: Optional and disabled by default
- **Standard Formats**: All data is stored in standard file formats you control
## Implementation Details
Knowledge in Basic Memory is organized as a semantic graph:
1. **Entities** - Distinct concepts represented by Markdown documents
2. **Observations** - Categorized facts and information about entities
3. **Relations** - Connections between entities that form the knowledge graph
This structure emerges from simple text patterns in standard Markdown:
```markdown
---
title: Coffee Brewing Methods
type: note
permalink: coffee/coffee-brewing-methods
tags:
- '#coffee'
- '#brewing'
- '#methods'
- '#demo'
---
# Coffee Brewing Methods
An exploration of different coffee brewing techniques, their characteristics, and how they affect flavor extraction.
## Overview
Coffee brewing is both an art and a science. Different brewing methods extract different compounds from coffee beans,
resulting in unique flavor profiles, body, and mouthfeel. The key variables in any brewing method are:
- Grind size
- Water temperature
- Brew time
- Coffee-to-water ratio
- Agitation/turbulence
## Observations
- [principle] Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds
#extraction
- [method] Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity
## Relations
- requires [[Proper Grinding Technique]]
- affects [[Flavor Extraction]]
```
Becomes
```json
{
"entities": [
{
"permalink": "coffee/coffee-brewing-methods",
"title": "Coffee Brewing Methods",
"file_path": "Coffee Notes/Coffee Brewing Methods.md",
"entity_type": "note",
"entity_metadata": {
"title": "Coffee Brewing Methods",
"type": "note",
"permalink": "coffee/coffee-brewing-methods",
"tags": "['#coffee', '#brewing', '#methods', '#demo']"
},
"checksum": "bfa32a0f23fa124b53f0694c344d2788b0ce50bd090b55b6d738401d2a349e4c",
"content_type": "text/markdown",
"observations": [
{
"category": "principle",
"content": "Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds #extraction",
"tags": [
"extraction"
],
"permalink": "coffee/coffee-brewing-methods/observations/principle/coffee-extraction-follows-a-predictable-pattern-acids-extract-first-then-sugars-then-bitter-compounds-extraction"
},
{
"category": "method",
"content": "Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity",
"tags": [
"clarity"
],
"permalink": "coffee/coffee-brewing-methods/observations/method/pour-over-methods-generally-produce-cleaner-brighter-cups-with-more-distinct-flavor-notes-clarity"
}
],
"relations": [
{
"from_id": "coffee/coffee-bean-origins",
"to_id": "coffee/coffee-brewing-methods",
"relation_type": "pairs_with",
"permalink": "coffee/coffee-bean-origins/pairs-with/coffee/coffee-brewing-methods",
"to_name": "Coffee Brewing Methods"
},
{
"from_id": "coffee/flavor-extraction",
"to_id": "coffee/coffee-brewing-methods",
"relation_type": "affected_by",
"permalink": "coffee/flavor-extraction/affected-by/coffee/coffee-brewing-methods",
"to_name": "Coffee Brewing Methods"
}
],
"created_at": "2025-03-06T14:01:23.445071",
"updated_at": "2025-03-06T13:34:48.563606"
}
]
}
```
Basic Memory understands how to build context via its semantic graph.
### Entity Model
Basic Memory's core data model consists of:
- **Entities**: Documents in your knowledge base
- **Observations**: Facts or statements about entities
- **Relations**: Connections between entities
- **Tags**: Additional categorization for entities and observations
The system parses Markdown files to extract this structured information while preserving the human-readable format.
### Files as Source of Truth
Plain Markdown files store all knowledge, making it accessible with any text editor and easy to version with git.
```mermaid
flowchart TD
User((User)) <--> |Conversation| Claude["Claude or other LLM"]
Claude <-->|API Calls| BMCP["Basic Memory MCP Server"]
subgraph "Local Storage"
KnowledgeFiles["Markdown Files - Source of Truth"]
KnowledgeIndex[(Knowledge Graph SQLite Index)]
end
BMCP <-->|"write_note() read_note()"| KnowledgeFiles
BMCP <-->|"search() build_context()"| KnowledgeIndex
KnowledgeFiles <-.->|Sync Process| KnowledgeIndex
KnowledgeFiles <-->|Direct Editing| Editors((Text Editors & Git))
User -.->|"Complete control, Privacy preserved"| KnowledgeFiles
class Claude primary
class BMCP secondary
class KnowledgeFiles tertiary
class KnowledgeIndex quaternary
class User,Editors user`;
```
### Sqlite Database
A local SQLite database maintains the knowledge graph topology for fast queries and semantic traversal without cloud dependencies. It contains:
- db tables for the knowledge graph schema
- a search index table enabling full text search across the knowledge base
### Sync Process
The sync process:
1. Detects changes to files in the knowledge directory
2. Parses modified files to extract structured data
3. Updates the SQLite database with changes
4. Resolves forward references when new entities are created
5. Updates the search index for fast querying
### Search Engine
The search functionality:
1. Uses a combination of full-text search and semantic matching
2. Indexes observations, relations, and content
3. Supports wildcards and pattern matching in memory:// URLs
4. Traverses the knowledge graph to follow relationships
5. Ranks results by relevance to the query
## Relations
- relates_to [[Welcome to Basic memory]] (Overview)
- relates_to [[CLI Reference]] (Command line tools)
- implements [[Knowledge Format]] (File structure and format)
-556
View File
@@ -1,556 +0,0 @@
---
title: User Guide
type: note
permalink: docs/user-guide
---
# User Guide
This guide explains how to effectively use Basic Memory in your daily workflow, from creating knowledge through
conversations to building a rich semantic network.
## Basic Memory Workflow
Using Basic Memory follows a natural cycle:
1. **Have conversations** with AI assistants like Claude
2. **Capture knowledge** in Markdown files
3. **Build connections** between pieces of knowledge
4. **Reference your knowledge** in future conversations
5. **Edit files directly** when needed
6. **Sync changes** automatically
## Creating Knowledge
### Through Conversations
To create knowledge during conversations with Claude:
```
You: We've covered several authentication approaches. Could you create a note summarizing what we've discussed?
Claude: I'll create a note summarizing our authentication discussion.
```
This creates a Markdown file in your `~/basic-memory` directory with semantic markup.
### Direct File Creation
You can create files directly:
1. Create a new Markdown file in your `~/basic-memory` directory
2. Add frontmatter with title, type, and optional tags
3. Structure content with observations and relations
4. Save the file
5. Run `basic-memory sync` if not in watch mode
## Using Special Prompts
Basic Memory includes several special prompts that help you leverage your knowledge base more effectively. In apps like
Claude Desktop, these prompts trigger specific tools to search and analyze your knowledge base.
### Continue Conversation
When you want to pick up where you left off on a topic:
```
You: Let's continue our conversation about authentication systems.
```
Behind the scenes:
- Claude searches your knowledge base for content about "authentication systems"
- It retrieves relevant documents and their relations
- It analyzes the context to understand where you left off
- It builds a comprehensive picture of what you've previously discussed
- It can then resume the conversation with all that context
This is particularly useful when:
- Starting a new session days or weeks after your last discussion
- Switching between multiple ongoing projects
- Building on previous work without repeating yourself
### Recent Activity
To get an overview of what you've been working on:
```
You: What have we been discussing recently?
```
Behind the scenes:
- Claude retrieves documents modified recently
- It analyzes patterns and themes
- It summarizes the key topics and changes
- It offers to continue working on any of those topics
This is useful for:
- Coming back after a break
- Getting a quick reminder of ongoing projects
- Deciding what to work on next
### Search
To find specific information in your knowledge base:
```
You: Find information about JWT authentication in my notes.
```
Behind the scenes:
- Claude performs a semantic search for "JWT authentication"
- It retrieves and ranks the most relevant documents
- It summarizes the key findings
- It offers to explore specific areas in more detail
This is useful for:
- Finding specific information quickly
- Exploring what you know about a topic
- Starting work on an existing topic
### Example
Choose "Continue Conversation"
![[prompt 1.png|500]]
Enter a topic
![[prompt2.png|500]]
Give instructions
![[prompt3.png|500]]
Claude Desktop lets you send a prompt to provide context. You can use this at the beginning of a chat to preload context
without needing to copy paste all the time. By using one of the supplied prompts, Basic Memory will search the knowledge
base and give the AI instructions for how to build context.
Choose "Continue Conversation":
![[prompt 1.png|500]]
Enter a topic:
![[prompt2.png|500]]
Give optional additional instructions:
![[prompt3.png|500]]
Claude can build context from the supplied topic. This works independently of Claude Project information. All the
context comes from your local knowledge base.
![[prompt4.png|500]]
## Searching Your Knowledge Base
Basic Memory provides multiple ways to search and explore your knowledge base:
### Natural Language Search
The simplest way to search is to ask Claude directly:
```
You: What do I know about authentication methods?
```
Claude will search your knowledge base semantically and return relevant information.
### Search Prompt
Use the dedicated search prompt for more focused searches:
```
You: Search for "JWT authentication"
```
This triggers a specialized search that returns precise results with document titles, relevant excerpts, and offers to
explore specific documents.
### Boolean Search
For more precise searches, use boolean operators to refine your queries:
```
You: Search for "authentication AND OAuth NOT basic"
```
Basic Memory supports standard boolean operators:
- **AND**: Find documents containing both terms
```
You: Search for "python AND flask"
```
This finds documents containing both "python" and "flask"
- **OR**: Find documents containing either term
```
You: Search for "python OR javascript"
```
This finds documents containing either "python" or "javascript"
- **NOT**: Exclude documents containing specific terms
```
You: Search for "python NOT django"
```
This finds documents containing "python" but excludes those containing "django"
- **Grouping with parentheses**: Control operator precedence
```
You: Search for "(python OR javascript) AND web"
```
This finds documents about web development that mention either Python or JavaScript
Boolean search is particularly useful for:
- Narrowing down results in large knowledge bases
- Finding specific combinations of concepts
- Excluding irrelevant content from search results
- Creating complex queries for precise information retrieval
### Memory URL Pattern Matching
For advanced searches, use memory:// URL patterns with wildcards:
```
You: Look at memory://auth* and summarize all authentication approaches.
```
Pattern matching supports:
- **Wildcards**: `memory://auth*` matches all permalinks starting with "auth"
- **Path patterns**: `memory://project/*/auth` matches auth documents in any project subfolder
- **Relation traversal**: `memory://auth-system/implements/*` finds all documents that implement the auth system
### Combining Search with Context Building
The most powerful searches build comprehensive context by following relationships:
```
You: Search for JWT authentication and then follow all implementation relations.
```
This builds a complete picture by:
1. Finding documents about JWT authentication
2. Following implementation relationships from those documents
3. Building a complete picture of how JWT is implemented across your system
### Search Best Practices
For effective searching:
1. **Be specific** with search terms and phrases
2. **Use boolean operators** to refine searches and find precise information
3. **Use technical terms** when searching for technical content
4. **Follow up** on search results by asking for more details about specific documents
5. **Combine approaches** by starting with search and then using memory:// URLs for precision
6. **Use relation traversal** to explore connected concepts after finding initial documents
## Referencing Knowledge
### Using memory:// URLs
Reference specific knowledge directly:
```
You: Please look at memory://authentication-approaches and suggest which approach would be best for our mobile app.
```
### Natural Language References
Reference knowledge conversationally:
```
You: What did we decide about authentication for the project?
```
### Advanced References
Follow connections across your knowledge graph:
```
You: Look at memory://project-architecture and check related documents to give me a complete picture.
```
## Working with Files
### File Location and Organization
By default, Basic Memory stores files in `~/basic-memory`:
- Browse this directory in your file explorer
- Organize files into subfolders
- Use git for version control
### File Format
Each knowledge file follows this structure:
```markdown
---
title: Authentication Approaches
type: note
tags: [security, architecture]
permalink: authentication-approaches
---
# Authentication Approaches
A comparison of authentication methods.
## Observations
- [approach] JWT provides stateless authentication #security
- [limitation] Session tokens require server-side storage #infrastructure
## Relations
- implements [[Security Requirements]]
- affects [[User Login Flow]]
```
### Editing Files
Modify files in any text editor:
1. Open the file in your preferred editor
2. Make changes to content, observations, or relations
3. Save the file
4. Basic Memory detects changes automatically when running in watch mode
## Building a Knowledge Graph
The value of Basic Memory comes from connections between pieces of knowledge.
### Creating Relations
When creating or editing notes, build connections:
```markdown
## Relations
- implements [[Security Requirements]]
- depends_on [[User Authentication]]
```
Relations can be:
- Hierarchical (part_of, contains)
- Directional (implements, depends_on)
- Associative (relates_to, similar_to)
- Temporal (precedes, follows)
Relations are also created via regular wiki-link style links within the body text.
### Forward References
Reference documents that don't exist yet:
```markdown
- will_impact [[Future Feature]]
```
These references resolve automatically when you create the referenced document.
## Conversation Continuity
Basic Memory maintains context across different conversations.
### Starting New Sessions with Context
When starting a new conversation with Claude, you can:
1. **Use special prompts** like "Continue conversation about..." or "What were we working on?"
2. **Reference specific documents** with memory:// URLs
3. **Ask about recent work** with "What have we been discussing recently?"
4. **Search for specific topics** with "Find information about..."
### Long-Term Projects
Maintain context for complex projects over time:
1. **Document key decisions** as you make them
2. **Create relationships** between project components
3. **Reference past decisions** when implementing features
4. **Update documentation** as the project evolves
### Tips for Effective Continuity
1. **Be specific about topics** when continuing a conversation
2. **Reference documents directly** with memory:// URLs for precision
3. **Create summary notes** after important discussions
4. **Update existing notes** rather than creating duplicates
5. **Build robust connections** between related topics
## Advanced Features
### Importing External Knowledge
Import existing conversations:
```bash
# From Claude
basic-memory import claude conversations
# From ChatGPT
basic-memory import chatgpt
```
After importing, run `basic-memory sync` to index everything.
### Obsidian Integration
Use with [Obsidian](https://obsidian.md):
1. Point Obsidian to your `~/basic-memory` directory
2. Use Obsidian's graph view to visualize your knowledge network
3. All changes sync back to Basic Memory
### Canvas Visualizations
Create visual knowledge maps:
```
You: Could you create a canvas visualization of our project components?
```
This generates an Obsidian canvas file showing the relationships between concepts.
### Advanced Memory URI Patterns
Use wildcards and patterns:
```
You: Review memory://project/*/requirements to summarize all project requirements.
```
## Command Line Interface
### Sync Commands
```bash
# One-time sync
basic-memory sync
# Watch for changes
basic-memory sync --watch
```
### Status and Information
```bash
# Check system status
basic-memory status
# View CLI help
basic-memory --help
```
### Import Commands
```bash
# Import from Claude
basic-memory import claude conversations
# Import from ChatGPT
basic-memory import chatgpt
```
## Multiple Projects
Basic Memory supports managing multiple separate knowledge bases through projects. This feature allows you to maintain
separate knowledge graphs for different purposes (e.g., personal notes, work projects, research topics).
Basic Memory keeps a list of projects in a config file: ` ~/.basic-memory/config.json`
### Managing Projects
```bash
# List all configured projects
basic-memory project list
# Add a new project
basic-memory project add work ~/work-basic-memory
# Set the default project
basic-memory project default work
# Remove a project (doesn't delete files)
basic-memory project remove personal
# Show current project
basic-memory project current
```
### Using Projects in Commands
All commands support the `--project` flag to specify which project to use:
```bash
# Sync a specific project
basic-memory --project=work sync
# Run MCP server for a specific project
basic-memory --project=personal mcp
```
You can also set the `BASIC_MEMORY_PROJECT` environment variable:
```bash
BASIC_MEMORY_PROJECT=work basic-memory sync
```
### Project Isolation
Each project maintains:
- Its own collection of markdown files in the specified directory
- A separate SQLite database for that project
- Complete knowledge graph isolation from other projects
## Workflow Tips
1. Run sync in watch mode for automatic updates
2. Use git for version control of your knowledge base
3. Review and edit AI-created content for accuracy
4. Periodically organize and refine your knowledge structure
5. Build rich connections between related ideas
6. Use forward references to plan future documentation
7. Start conversations with special prompts to leverage existing knowledge
## Troubleshooting
### Sync Issues
If changes aren't showing up:
1. Verify `basic-memory sync --watch` is running
2. Run `basic-memory status` to check system state
3. Try a manual sync with `basic-memory sync`
### Missing Content
If content isn't found:
1. Check the exact path and permalink
2. Try searching with more general terms
3. Verify the file exists in your knowledge base
### Relation Problems
If relations aren't working:
1. Ensure exact title matching in [[WikiLinks]]
2. Check for typos in relation types
3. Verify both documents exist
## Relations
- implements [[Knowledge Format]] (How knowledge is structured)
- relates_to [[Getting Started with Basic Memory]] (Setup and first steps)
- relates_to [[Canvas]] (Creating visual knowledge maps)
- relates_to [[CLI Reference]] (Command line tools)
-68
View File
@@ -1,68 +0,0 @@
---
title: Introduction to Basic Memory
type: docs
permalink: docs/introduction
tags:
- documentation
- index
- overview
---
# BASIC MEMORY
Basic Memory is a knowledge management system that allows you to build a persistent semantic graph from conversations
with AI assistants. All knowledge is stored in standard Markdown files on your computer, giving you full control and
ownership of your data.
Basic Memory connects you and AI assistants through shared knowledge:
1. **Captures knowledge** from natural conversations with AI assistants
2. **Structures information** using simple semantic patterns in Markdown
3. **Enables knowledge reuse** across different conversations and sessions
4. **Maintains persistence** through local files you control completely
Both you and AI assistants like Claude can read from and write to the same knowledge base, creating a continuous
learning environment where each conversation builds upon previous ones.
## Pick up your conversation right where you left off
- AI assistants can load context from local files in a new conversation
- Notes are saved locally as Markdown files in real time
- No project knowledge or special prompting required
![[Claude-Obsidian-Demo.mp4]]
Basic Memory uses:
- **Files as the source of truth** - Everything is stored in plain Markdown files
- **Git-compatible storage** - All knowledge can be versioned, branched, and merged
- **Local SQLite database** - For fast indexing and searching only (not primary storage)
- **Model Context Protocol (MCP)** - For seamless AI assistant integration
Basic Memory gives you complete control over your knowledge:
- **Local-first storage** - All knowledge lives on your computer
- **Standard file formats** - Plain Markdown compatible with any editor
- **Directory organization** - Knowledge stored in `~/basic-memory` by default
- **Version control ready** - Use git for history, branching, and collaboration
- **Edit anywhere** - Modify files with any text editor or Obsidian
Changes to files automatically sync with the knowledge graph, and AI assistants can see your edits in conversations.
## Documentation Map
Continue exploring Basic Memory with these guides:
- Installation and setup [[Getting Started with Basic Memory]]
- Comprehensive usage instructions [[User Guide]]
- Detailed explanation of knowledge structure [[Knowledge Format]]
- Obsidian integration guide [[Obsidian Integration]]
- Canvas visualization guide [[Canvas]]
- Command line tool reference [[CLI Reference]]
- Reference for AI assistants using Basic Memory [[AI Assistant Guide]]
- Technical implementation details [[Technical Information]]
## Next Steps
Start with the [[Getting Started with Basic Memory]] guide to install Basic Memory and configure it with your AI
assistant.
Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

-5
View File
@@ -1,5 +0,0 @@
var analyticsScript = document.createElement('script');
analyticsScript.defer = true;
analyticsScript.setAttribute('data-website-id', '8d51086e-5c67-401e-97b0-b24706a6d4f3');
analyticsScript.src = 'https://cloud.umami.is/script.js';
document.head.appendChild(analyticsScript);
-3
View File
@@ -1,3 +0,0 @@
{
"promptDelete": false
}
-1
View File
@@ -1 +0,0 @@
{}
-3
View File
@@ -1,3 +0,0 @@
[
"optimize-canvas-connections"
]
-31
View File
@@ -1,31 +0,0 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": true,
"sync": true,
"webviewer": false
}
@@ -1,158 +0,0 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// main.ts
var main_exports = {};
__export(main_exports, {
default: () => OptimizeCanvasConnectionsPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian = require("obsidian");
var OptimizeCanvasConnectionsPlugin = class extends import_obsidian.Plugin {
async onload() {
this.addCommand({
id: "optimize-preserve-axes-selection",
name: "Optimize selection (preserve axes)",
checkCallback: (checking) => {
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
if ((canvasView == null ? void 0 : canvasView.getViewType()) == "canvas") {
if (!checking) {
this.optimize("preserve-axes");
}
return true;
}
return false;
}
});
this.addCommand({
id: "optimize-shortest-path-selection",
name: "Optimize selection (shortest path)",
checkCallback: (checking) => {
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
if ((canvasView == null ? void 0 : canvasView.getViewType()) == "canvas") {
if (!checking) {
this.optimize("shortest-path");
}
return true;
}
return false;
}
});
}
onunload() {
}
async optimize(option) {
const canvasView = app.workspace.getActiveViewOfType(import_obsidian.ItemView);
const canvas = canvasView == null ? void 0 : canvasView.canvas;
const currentSelection = canvas == null ? void 0 : canvas.selection;
let selectedIDs = new Array();
currentSelection.forEach(function(selection) {
selectedIDs.push(selection.id);
});
let applyToAll = false;
if (selectedIDs.length == 0) {
applyToAll = true;
}
for (let [edgeKey, edge] of canvas["edges"]) {
let fromNode = edge["from"]["node"];
let toNode = edge["to"]["node"];
let fromPossibilities = [edge["from"]["side"]];
if (applyToAll || selectedIDs.includes(fromNode["id"])) {
switch (option) {
case "shortest-path":
fromPossibilities = ["top", "bottom", "left", "right"];
break;
case "preserve-axes":
switch (edge["from"]["side"]) {
case "top":
case "bottom":
fromPossibilities = ["top", "bottom"];
break;
case "left":
case "right":
fromPossibilities = ["left", "right"];
break;
}
}
}
let toPossibilities = [edge["to"]["side"]];
if (applyToAll || selectedIDs.includes(toNode["id"])) {
switch (option) {
case "shortest-path":
toPossibilities = ["top", "bottom", "left", "right"];
break;
case "preserve-axes":
switch (edge["to"]["side"]) {
case "top":
case "bottom":
toPossibilities = ["top", "bottom"];
break;
case "left":
case "right":
toPossibilities = ["left", "right"];
break;
}
}
}
let distances = [];
for (const fromSide of fromPossibilities) {
let fromPoint = { "x": 0, "y": 0 };
if (fromSide == "top") {
fromPoint = { "x": fromNode["x"] + fromNode["width"] / 2, "y": fromNode["y"] };
} else if (fromSide == "bottom") {
fromPoint = { "x": fromNode["x"] + fromNode["width"] / 2, "y": fromNode["y"] + fromNode["height"] };
} else if (fromSide == "left") {
fromPoint = { "x": fromNode["x"], "y": fromNode["y"] + fromNode["height"] / 2 };
} else if (fromSide == "right") {
fromPoint = { "x": fromNode["x"] + fromNode["width"], "y": fromNode["y"] + fromNode["height"] / 2 };
}
for (const toSide of toPossibilities) {
let toPoint = { "x": 0, "y": 0 };
if (toSide == "top") {
toPoint = { "x": toNode["x"] + toNode["width"] / 2, "y": toNode["y"] };
} else if (toSide == "bottom") {
toPoint = { "x": toNode["x"] + toNode["width"] / 2, "y": toNode["y"] + toNode["height"] };
} else if (toSide == "left") {
toPoint = { "x": toNode["x"], "y": toNode["y"] + toNode["height"] / 2 };
} else if (toSide == "right") {
toPoint = { "x": toNode["x"] + toNode["width"], "y": toNode["y"] + toNode["height"] / 2 };
}
distances.push({
"fromSide": fromSide,
"toSide": toSide,
"distance": (toPoint.x - fromPoint.x) ** 2 + (toPoint.y - fromPoint.y) ** 2
});
}
}
distances = distances.sort(function(a, b) {
return a.distance - b.distance;
});
edge["from"]["side"] = distances[0]["fromSide"];
edge["to"]["side"] = distances[0]["toSide"];
edge.render();
}
canvas.requestSave();
}
};
/* nosourcemap */
@@ -1,10 +0,0 @@
{
"id": "optimize-canvas-connections",
"name": "Optimize Canvas Connections",
"version": "1.0.0",
"minAppVersion": "1.1.9",
"description": "An Obsidian plugin that declutters a canvas by reconnecting notes using their nearest edges.",
"author": "Félix Chénier",
"authorUrl": "https://felixchenier.uqam.ca",
"isDesktopOnly": false
}
-6
View File
@@ -1,6 +0,0 @@
{
"siteId": null,
"host": null,
"included": [],
"excluded": []
}
@@ -1,83 +0,0 @@
---
title: Brewing Equipment
type: note
permalink: coffee/brewing-equipment
tags:
- '#coffee'
- '#equipment'
- '#gear'
- '#brewing'
- '#demo'
---
# Brewing Equipment
Essential tools and equipment for brewing coffee, their characteristics, and how they affect the brewing process.
## Overview
The equipment used to brew coffee plays a crucial role in determining the final cup quality. From grinders to brewers to kettles, each piece of equipment contributes to different aspects of the brewing process.
## Observations
- [principle] Equipment quality often has a bigger impact on consistency than on absolute quality potential #quality
- [principle] Good grind consistency is the most important technical factor in extraction quality #grind
- [investment] A good burr grinder is often the most important investment for improving home coffee #gear
- [technique] Equipment maintenance and cleaning significantly impact flavor consistency over time #maintenance
## Grinders
- [equipment] Burr grinders crush beans between two abrasive surfaces for more consistent particle size #grinders
- [equipment] Blade grinders chop beans unevenly, leading to inconsistent extraction #grinders
- [equipment] Flat burr grinders produce very consistent particle size but generate more heat #burrs
- [equipment] Conical burr grinders create slightly less uniform grounds but with less heat and noise #burrs
- [feature] Grind adjustment mechanisms range from stepped to stepless for different precision levels #adjustment
- [feature] Retention (grounds trapped in grinder) affects dose consistency and freshness #retention
- [price] Hand grinders offer excellent value, with models like Timemore C2 and 1Zpresso JX providing excellent results around $100-150 #budget
- [price] Entry-level electric burr grinders like Baratza Encore start around $170 but provide significant improvement over blade grinders #value
## Brewers
### Pour Over Brewers
- [equipment] Hario V60 uses a conical design with spiral ridges to control flow rate #pourover
- [equipment] Kalita Wave has a flat bottom with three small holes for more consistent extraction #pourover
- [equipment] Chemex combines brewer and server with thick proprietary filters for ultra-clean cup #pourover
- [material] Ceramic brewers retain heat better than plastic but are more fragile #materials
- [material] Glass brewers provide neutral flavor but less heat retention #materials
- [material] Plastic brewers are inexpensive, durable, and surprisingly good for heat retention #materials
### Immersion Brewers
- [equipment] French Press uses a metal mesh to separate grounds, allowing oils and fine particles to pass #immersion
- [equipment] AeroPress uses pressure and paper filter for clean, versatile brewing #immersion
- [equipment] Clever Dripper combines immersion and drip methods with a valve mechanism #hybrid
- [material] Glass French presses look elegant but break easily and have poor heat retention #materials
- [material] Stainless steel or ceramic French presses offer better durability and heat retention #materials
### Pressure Brewers
- [equipment] Espresso machines use 9 bars of pressure, requiring significant investment for good results #espresso
- [equipment] Moka pot uses steam pressure for strong, concentrated coffee at affordable price #moka
- [equipment] Manual lever machines like Flair or Robot provide espresso-style coffee with manual control #manual_espresso
## Kettles
- [equipment] Gooseneck kettles provide precision pouring control essential for pour over methods #kettles
- [feature] Variable temperature kettles allow precise temperature control for different roast levels #temp_control
- [feature] Flow restrictors can help beginners maintain consistent pour rates #pour_control
- [material] Electric kettles offer convenience and temperature stability #convenience
- [material] Stovetop kettles may be more durable but offer less temperature control #durability
## Accessories
- [equipment] Coffee scale with 0.1g precision helps maintain consistent ratios #measurement
- [equipment] Timer ensures consistent extraction times #consistency
- [equipment] Quality filters significantly impact flavor clarity and body #filters
- [equipment] Storage containers with one-way valves help preserve bean freshness #storage
- [equipment] Blind shaker or dosing cup reduces grinder mess and improves workflow #workflow
## Relations
- improves [[Coffee Brewing Methods]]
- affects [[Flavor Extraction]]
- requires [[Proper Maintenance]]
- enhances [[Home Coffee Setup]]
- part_of [[Coffee Knowledge Base]]
@@ -1,78 +0,0 @@
---
title: Coffee Bean Origins
type: note
permalink: coffee/coffee-bean-origins
tags:
- '#coffee'
- '#origins'
- '#beans'
- '#regions'
- '#demo'
---
# Coffee Bean Origins
An exploration of coffee-growing regions around the world and how geography, climate, and processing methods affect flavor profiles.
## Overview
Coffee beans are grown in various regions around the world, primarily in what's known as the "Coffee Belt" - the area between the Tropics of Cancer and Capricorn. The flavor characteristics of coffee beans are influenced by:
- Geographic region and climate
- Altitude
- Soil composition
- Variety of coffee plant
- Processing method
- Harvest and sorting practices
## Observations
- [principle] Higher altitude generally produces harder, denser beans with more complex acidity #altitude
- [region] Ethiopian beans often feature bright, fruity notes with floral aromatics #ethiopia
- [region] Colombian coffee typically offers balanced acidity with caramel sweetness and nutty undertones #colombia
- [region] Guatemalan coffee presents complex acidity with chocolate notes and sometimes spice characteristics #guatemala
- [region] Brazilian coffee tends toward nutty, chocolate notes with lower acidity and fuller body #brazil
- [region] Kenyan coffee is known for bright, wine-like acidity and berry or citrus notes #kenya
- [processing] Natural (dry) processing tends to create fruitier, more fermented flavors #processing
- [processing] Washed (wet) processing generally results in cleaner, brighter cups with more clarity #processing
- [processing] Honey processing creates a middle ground with some fruity notes while maintaining clarity #processing
- [factor] Shade-grown coffee typically develops more slowly, resulting in more complex flavors #cultivation
- [factor] Soil volcanic soil often imparts distinctive mineral characteristics to coffee #terroir
- [variety] Gesha/Geisha variety is known for exceptional floral and tea-like qualities #varieties
- [variety] Bourbon varieties often feature sweet, complex cup profiles #varieties
- [variety] Robusta beans have higher caffeine content but generally less complex flavor than Arabica #varieties
## Major Growing Regions
- [africa] Ethiopian coffees: Yirgacheffe, Sidamo, Harrar regions each with distinctive profiles #ethiopia
- [africa] Kenyan coffees: Often categorized by grade (AA, AB, etc.) based on bean size #kenya
- [americas] Colombian regions: Huila, Nariño, Antioquia each with unique characteristics #colombia
- [americas] Central American producers: Guatemala, Costa Rica, Panama known for balanced profiles #central_america
- [americas] Brazilian regions: Cerrado, Sul de Minas, Mogiana with varying profiles #brazil
- [asia] Indonesian islands: Sumatra, Java, Sulawesi producing earthy, full-bodied coffees #indonesia
- [asia] Vietnamese coffee: World's largest Robusta producer, often used in blends and commercial coffee #vietnam
## Processing Methods
- [natural] Beans dried inside the fruit, creating fruity, fermented notes and heavier body #processing
- [washed] Fruit removed before drying, resulting in cleaner cup with more pronounced acidity #processing
- [honey] Some fruit mucilage left on during drying, creates balanced sweetness and body #processing
- [wet-hulled] Unique to Indonesia, creates earthy, herbal, low-acid profiles #processing
- [experimental] Anaerobic fermentation, wine-yeast inoculation, and other newer methods #innovation
## Tasting Notes by Region
- [ethiopia] Blueberry, jasmine, bergamot, stone fruit, citrus #flavor_notes
- [kenya] Blackcurrant, tomato, tropical fruit, wine-like acidity #flavor_notes
- [colombia] Caramel, nuts, red apple, chocolate, balanced acidity #flavor_notes
- [guatemala] Chocolate, spice, green apple, balanced #flavor_notes
- [brazil] Nuts, chocolate, low acidity, full body #flavor_notes
- [indonesia] Earthy, herbal, spice, cedar, full body, low acidity #flavor_notes
## Relations
- influences [[Flavor Extraction]]
- pairs_with [[Coffee Brewing Methods]]
- affects [[Tasting Notes]]
- relates_to [[Specialty Coffee]]
- part_of [[Coffee Knowledge Base]]
@@ -1,70 +0,0 @@
---
title: Coffee Brewing Methods
type: note
permalink: coffee/coffee-brewing-methods
tags:
- '#coffee'
- '#brewing'
- '#methods'
- '#demo'
---
# Coffee Brewing Methods
An exploration of different coffee brewing techniques, their characteristics, and how they affect flavor extraction.
## Overview
Coffee brewing is both an art and a science. Different brewing methods extract different compounds from coffee beans, resulting in unique flavor profiles, body, and mouthfeel. The key variables in any brewing method are:
- Grind size
- Water temperature
- Brew time
- Coffee-to-water ratio
- Agitation/turbulence
## Observations
- [principle] Coffee extraction follows a predictable pattern: acids extract first, then sugars, then bitter compounds #extraction
- [method] Pour over methods generally produce cleaner, brighter cups with more distinct flavor notes #clarity
- [method] Immersion methods like French press create fuller body and more rounded flavors #body
- [technique] Water at 195-205°F (90-96°C) extracts optimal flavor compounds for most brewing methods #temperature
- [technique] Grind size directly correlates with ideal extraction time (finer = shorter, coarser = longer) #grind
- [preference] Medium-light roasts often showcase more origin characteristics in pour over methods #roast
- [equipment] Burr grinders produce more consistent particle size than blade grinders, resulting in more even extraction #gear
- [ratio] 1:15 to 1:17 coffee-to-water ratio (by weight) works well for most brew methods #brewing
- [science] Different brewing temperatures extract different chemical compounds from the beans #chemistry
- [technique] Bloom phase (pre-infusion with small amount of water) allows CO2 to escape and improves extraction #bloom
## Pour Over Methods
- [method] V60 produces very clean cup with excellent clarity of flavor #pourover
- [method] Chemex uses thicker filter paper, resulting in even cleaner cup with fewer oils #pourover
- [method] Kalita Wave provides more consistent extraction due to flat bottom design #pourover
- [technique] Concentric circular pouring pattern ensures even saturation of grounds #technique
- [timing] Most pour over methods complete in 2:30-3:30 total brew time #brewing
## Immersion Methods
- [method] French Press creates full-bodied cup with rich mouthfeel due to metal filter allowing oils to pass #immersion
- [method] AeroPress is versatile, capable of producing both espresso-like and filter-style coffee #immersion
- [method] Cold brew uses time instead of heat to extract, resulting in lower acidity #immersion
- [technique] French press ideal steep time is 4-5 minutes before plunging #timing
- [technique] AeroPress inverted method prevents dripping during extraction phase #technique
## Pressure Methods
- [method] Espresso uses 9 bars of pressure to force water through finely ground coffee #pressure
- [method] Moka pot uses steam pressure to push water through grounds, creating strong, concentrated coffee #pressure
- [technique] Espresso requires very fine grind, almost powder-like consistency #grind
- [timing] Espresso shots typically extract in 25-30 seconds #timing
- [principle] Pressure methods can extract compounds that aren't soluble in regular brewing methods #extraction
## Relations
- requires [[Proper Grinding Technique]]
- affects [[Flavor Extraction]]
- pairs_with [[Coffee Bean Origins]]
- uses [[Brewing Equipment]]
- influences [[Tasting Notes]]
- part_of [[Coffee Knowledge Base]]
@@ -1,89 +0,0 @@
---
title: Coffee Flavor Map
type: note
permalink: coffee/coffee-flavor-map
tags:
- '#coffee'
- '#visualization'
- '#canvas'
- '#demo'
---
# Coffee Flavor Map
A visual mapping of coffee flavor attributes, brewing methods, and their relationships. This note describes a canvas visualization that could be generated to demonstrate Basic Memory's visualization capabilities.
## Overview
The Coffee Flavor Map provides a visual representation of how different brewing methods, coffee origins, and equipment choices affect flavor outcomes. This canvas visualization helps users understand the complex relationships in coffee brewing and tasting.
## Canvas Visualization Elements
### Core Nodes
- **Flavor Attributes**: Acidity, Sweetness, Body, Clarity, Bitterness, Complexity
- **Brewing Methods**: Pour Over, French Press, AeroPress, Espresso, Moka Pot, Cold Brew
- **Origin Regions**: Ethiopia, Kenya, Colombia, Brazil, Guatemala, Indonesia
- **Equipment Elements**: Grinder Quality, Water Temperature, Brewing Device, Filter Type
### Node Connections
- Lines connecting brewing methods to their typical flavor outcomes
- Arrows showing how equipment choices affect extraction variables
- Connections between origins and their characteristic flavor profiles
- Highlighting of optimal brewing methods for different origins
### Visual Organization
- Flavor outcomes in the center
- Brewing methods on the left side
- Origins on the right side
- Equipment variables at the bottom
- Color coding by category (methods, origins, equipment, flavors)
## Using This Visualization
### For Coffee Exploration
- Identify which brewing methods might highlight the characteristics you prefer
- See which origins naturally pair well with your preferred brewing method
- Understand how equipment changes can modify flavor outcomes
- Visualize the complex interplay between all coffee variables
### As a Basic Memory Demo
- Demonstrates Canvas visualization capabilities
- Shows how relations can be visually mapped
- Illustrates complex knowledge organization
- Provides an intuitive way to navigate coffee knowledge
## How To Generate This Canvas
In a conversation with Claude, you could request:
```
Please create a canvas visualization mapping the relationships between coffee brewing methods, origins, and flavor outcomes. Show how different equipment and techniques influence extraction and resulting flavor profiles.
```
This would generate a `.canvas` file in your Basic Memory directory that could be opened with Obsidian for an interactive visualization of these coffee relationships.
## Example Visualization Snippets
### Pour Over Method Node
- Connected to: High Clarity, Bright Acidity, Medium Body
- Best pairs with: Ethiopian and Kenyan beans
- Equipment dependencies: Gooseneck Kettle, Paper Filter, Burr Grinder
### Ethiopian Coffee Node
- Characteristic flavors: Floral, Fruity, Bright
- Best brewing methods: Pour Over, AeroPress
- Challenging with: French Press (loses clarity of delicate notes)
### Grind Size Node
- Affects: Extraction Rate, Flavor Balance
- Fine grind increases: Extraction Speed, Surface Area
- Coarse grind increases: Flow Rate, Reduces Bitter Compounds
## Relations
- visualizes [[Coffee Knowledge Base]]
- relates_to [[Coffee Brewing Methods]]
- relates_to [[Coffee Bean Origins]]
- relates_to [[Flavor Extraction]]
- relates_to [[Tasting Notes]]
- demonstrates [[Canvas]]
@@ -1,73 +0,0 @@
---
title: Coffee Knowledge Base
type: note
permalink: coffee/coffee-knowledge-base
tags:
- '#coffee'
- '#index'
- '#demo'
- '#knowledge'
---
# Coffee Knowledge Base
A comprehensive collection of coffee knowledge, from bean origins to brewing methods to tasting notes. This knowledge base demonstrates Basic Memory's ability to organize and connect information in a meaningful way.
## Overview
This Coffee Knowledge Base captures key information about coffee, structured with semantic observations and relations that connect different aspects of coffee knowledge. It serves as both a useful reference for coffee enthusiasts and a demonstration of how Basic Memory organizes information.
## Key Topics
### Core Coffee Knowledge
- [[Coffee Brewing Methods]] - Different techniques for preparing coffee
- [[Coffee Bean Origins]] - Where coffee comes from and how region affects flavor
- [[Brewing Equipment]] - Tools and devices used to prepare coffee
- [[Flavor Extraction]] - The science of dissolving flavor compounds from coffee
- [[Tasting Notes]] - How to taste and describe coffee flavors
### Brewing Techniques
- Proper grinding is fundamental to good extraction
- Water quality significantly impacts flavor
- Different brewing methods highlight different characteristics
- Time, temperature, and grind size are the key variables to control
- Freshness of beans dramatically affects quality
### Coffee Preferences
- Light roasts preserve more origin characteristics and acidity
- Dark roasts emphasize body and chocolatey/roasted flavors
- Pour over methods highlight clarity and distinct flavor notes
- Immersion methods create fuller body and rounded flavor
- Personal preference matters more than "correctness"
## Using This Knowledge Base
### For Learning
Use this knowledge base to:
- Understand coffee fundamentals
- Explore connections between brewing methods and flavor outcomes
- Learn how different origins produce distinct flavor profiles
- Discover how equipment affects the brewing process
- Develop a vocabulary for describing coffee experiences
### As a Demo
This knowledge base demonstrates:
- Semantic knowledge organization with categories and relations
- Building connections between related concepts
- Creating a navigable knowledge graph
- Structuring information in a way both humans and AI assistants can understand
- How Basic Memory enables persistent knowledge across conversations
## Relations
- contains [[Coffee Brewing Methods]]
- contains [[Coffee Bean Origins]]
- contains [[Brewing Equipment]]
- contains [[Flavor Extraction]]
- contains [[Tasting Notes]]
- demonstrates [[Basic Memory Capabilities]]
@@ -1,79 +0,0 @@
---
title: Flavor Extraction
type: note
permalink: coffee/flavor-extraction
tags:
- '#coffee'
- '#extraction'
- '#brewing'
- '#science'
- '#demo'
---
# Flavor Extraction
Understanding the science of coffee extraction, how different compounds dissolve at different rates, and how to control extraction to achieve desired flavor profiles.
## Overview
Coffee extraction is the process of dissolving flavor compounds from ground coffee into water. The science of extraction is key to producing a balanced, flavorful cup. Extraction is affected by numerous variables including grind size, water temperature, contact time, agitation, and pressure.
## Observations
- [science] Coffee contains over 1,000 aroma compounds and hundreds of flavor compounds #chemistry
- [principle] Extraction occurs in a predictable sequence: acids → sugars → bitter compounds #extraction_order
- [principle] Under-extraction results in sour, bright, thin coffee lacking sweetness and body #under_extraction
- [principle] Over-extraction results in bitter, hollow, astringent flavors #over_extraction
- [principle] The goal is typically balanced extraction (18-22% of coffee solubles dissolved) #balanced_extraction
- [technique] Finer grind size increases extraction rate due to greater surface area #grind_size
- [technique] Higher water temperature increases extraction rate and solubility of compounds #temperature
- [technique] Longer contact time allows more complete extraction #brew_time
- [technique] Agitation (stirring, turbulence) increases extraction rate by preventing saturation zones #agitation
- [technique] Pressure (as in espresso) can extract compounds that aren't water-soluble at atmospheric pressure #pressure
## Factors Affecting Extraction
- [factor] Grind size: Finer = faster extraction, coarser = slower extraction #grind
- [factor] Water temperature: Higher = faster extraction, lower = slower extraction #temperature
- [factor] Contact time: Longer = more extraction, shorter = less extraction #time
- [factor] Agitation: More = faster extraction, less = slower extraction #agitation
- [factor] Coffee-to-water ratio: More coffee = lower extraction percentage #ratio
- [factor] Water quality: Mineral content affects extraction of different compounds #water
- [factor] Roast level: Darker roasts extract more easily than lighter roasts #roast
- [factor] Bean density: Denser beans (typically high-altitude) require more effort to extract #density
- [factor] Freshness: Freshly roasted coffee extracts differently than aged coffee #freshness
- [factor] Brewing method: Different methods extract different compounds at different rates #method
## Signs of Extraction Levels
- [under] Sour, bright, lack of sweetness, thin body, quick finish #flavor
- [under] Typically from: too coarse grind, too cool water, too short brew time #causes
- [balanced] Sweet, bright but not sour, rich but not bitter, pleasing finish #flavor
- [balanced] Achieved through proper ratio of variables for given coffee #technique
- [over] Bitter, hollow, astringent, dry finish, sometimes papery #flavor
- [over] Typically from: too fine grind, too hot water, too long brew time #causes
## Measuring Extraction
- [method] Total Dissolved Solids (TDS) meters measure concentration of coffee solution #measurement
- [method] Extraction yield = percentage of coffee grounds dissolved in the final brew #calculation
- [preference] Specialty coffee typically targets 18-22% extraction yield #standards
- [preference] Some specialty light roasts may taste best at higher extraction percentages #speciality
## Controlling Extraction
- [technique] Adjust grind size as primary extraction control #basics
- [technique] Use water temperature to fine-tune extraction #fine_tuning
- [technique] Modify pour technique to control agitation level #technique
- [technique] Adjust coffee-to-water ratio to balance strength and extraction #ratio
- [technique] Pre-infusion (blooming) helps achieve even extraction #blooming
- [technique] Pulse pouring creates different extraction dynamics than continuous pour #pour_technique
## Relations
- affected_by [[Coffee Brewing Methods]]
- influenced_by [[Coffee Bean Origins]]
- enhanced_by [[Brewing Equipment]]
- determines [[Tasting Notes]]
- requires [[Water Quality]]
- part_of [[Coffee Knowledge Base]]
@@ -1,161 +0,0 @@
{
"nodes":[
{
"id":"node-5",
"type":"text",
"text":"## Main Pour Phase\n- Use concentric circles from center outward\n- Maintain steady, controlled flow rate\n- Avoid pouring directly on filter walls\n- Keep water level consistent\n- Pulse pour in 2-3 stages (or continuous pour)\n- Total brew time target: 2:30-3:30",
"position":{"x":450,"y":200},
"x":530,
"y":-100,
"width":300,
"height":200,
"color":"1"
},
{
"id":"node-8",
"type":"text",
"text":"## Drawdown\n- Allow water to fully drain\n- Flat bed indicates even extraction\n- Total brew time should be ~2:30-3:30\n- Remove filter promptly after brewing",
"position":{"x":450,"y":700},
"x":540,
"y":375,
"width":300,
"height":150,
"color":"1"
},
{
"id":"node-6",
"type":"text",
"text":"## Pour Pattern\n\nConcentric circles ensure even saturation of coffee grounds. Begin at the center and work outward, avoiding filter edges. Pour height of 1-2 inches above coffee bed.",
"position":{"x":250,"y":450},
"x":960,
"y":25,
"width":300,
"height":150,
"color":"5"
},
{
"id":"node-12",
"type":"text",
"text":"## Tasting Notes\n\n- Balanced extraction: sweet, bright, complex\n- Under-extraction: sour, lacking sweetness\n- Over-extraction: bitter, astringent, hollow\n\nTake notes on each brew to track improvements and preferences.",
"position":{"x":-250,"y":700},
"x":1020,
"y":420,
"width":300,
"height":150,
"color":"6"
},
{
"id":"node-9",
"type":"text",
"text":"## Troubleshooting\n\n- Too sour/weak: Grind finer, water hotter, increase brew time\n- Too bitter/strong: Grind coarser, water cooler, decrease brew time\n- Uneven extraction: Improve pour technique, better grinder\n- Channeling: More careful pouring, better bloom\n- Slow drawdown: Coarser grind, less agitation\n- Fast drawdown: Finer grind, more careful pouring",
"position":{"x":100,"y":700},
"x":30,
"y":570,
"width":300,
"height":200,
"color":"6"
},
{
"id":"node-3",
"type":"text",
"text":"## Preparation\n- Heat water to 195-205°F (90-96°C)\n- Measure coffee (1:15 to 1:17 ratio)\n- Medium-fine grind (sea salt consistency)\n- Rinse filter with hot water\n- Discard rinse water\n- Add ground coffee to filter\n- Level coffee bed",
"position":{"x":-250,"y":200},
"x":30,
"y":-500,
"width":300,
"height":200,
"color":"3"
},
{
"id":"node-1",
"type":"text",
"text":"# Perfect Pour Over Method\n\nA systematic approach to brewing exceptional pour over coffee by controlling key variables and following proper technique.",
"position":{"x":0,"y":0},
"x":-580,
"y":-760,
"width":400,
"height":120,
"color":"4"
},
{
"id":"node-10",
"type":"text",
"text":"## Grinding Parameters\n\n- V60: Medium-fine (sea salt)\n- Chemex: Medium (slightly coarser than V60)\n- Kalita Wave: Medium (between V60 and Chemex)\n\nConsistent particle size is critical; use quality burr grinder.",
"position":{"x":-250,"y":450},
"x":30,
"y":-910,
"width":300,
"height":150,
"color":"5"
},
{
"id":"node-2",
"type":"text",
"text":"## Equipment Setup\n- Clean V60/Chemex/Kalita Wave\n- Paper filter (rinsed)\n- Server/mug\n- Scale with timer\n- Gooseneck kettle\n- Burr grinder\n- Fresh coffee beans",
"position":{"x":-600,"y":200},
"x":-530,
"y":-500,
"width":300,
"height":200,
"color":"3"
},
{
"id":"node-4",
"type":"text",
"text":"## The Bloom\n- Start timer\n- Pour 2-3x coffee weight water\n- Ensure all grounds are saturated\n- Gentle stir or swirl if needed\n- Allow 30-45 seconds for degassing\n- Look for bubbling and dome formation",
"position":{"x":100,"y":200},
"x":530,
"y":-500,
"width":300,
"height":200,
"color":"1"
},
{
"id":"node-13",
"type":"text",
"text":"## Coffee-to-Water Ratio\n\n- Standard: 1:15 to 1:17 (coffee:water)\n- Stronger cup: 1:15 (67g/L)\n- Medium cup: 1:16 (62.5g/L)\n- Lighter cup: 1:17 (58.8g/L)\n\nExample: For 300ml water, use ~18-20g coffee",
"position":{"x":-600,"y":700},
"x":30,
"y":-100,
"width":300,
"height":150,
"color":"6"
},
{
"id":"node-7",
"type":"text",
"text":"## Brew Time Guideline\n\n- Bloom: 30-45 seconds\n- First pour: 1:00-1:15\n- Second pour: 1:45-2:00\n- Final pour: 2:15-2:30\n- Drawdown complete: 2:45-3:30\n\nAdjust for taste: shorter for lighter, longer for stronger",
"position":{"x":600,"y":450},
"x":-80,
"y":220,
"width":300,
"height":200,
"color":"5"
},
{
"id":"node-11",
"type":"text",
"text":"## Water Quality\n\n- Clean, filtered water\n- No strong odors or flavors\n- Ideal TDS: 75-150 ppm\n- Ideal pH: 7.0-7.5\n- Avoid distilled water (lacks minerals)\n- Avoid hard water (scaling issues)",
"position":{"x":-600,"y":450},
"x":-780,
"y":-125,
"width":300,
"height":150,
"color":"5"
}
],
"edges":[
{"id":"edge-1","fromNode":"node-1","fromSide":"bottom","toNode":"node-2","toSide":"top","label":"Step 1"},
{"id":"edge-2","fromNode":"node-2","fromSide":"right","toNode":"node-3","toSide":"left","label":"Step 2"},
{"id":"edge-3","fromNode":"node-3","fromSide":"right","toNode":"node-4","toSide":"left","label":"Step 3"},
{"id":"edge-4","fromNode":"node-4","fromSide":"bottom","toNode":"node-5","toSide":"top","label":"Step 4"},
{"id":"edge-5","fromNode":"node-5","fromSide":"bottom","toNode":"node-8","toSide":"top","label":"Step 5"},
{"id":"edge-6","fromNode":"node-5","fromSide":"left","toNode":"node-7","toSide":"right","label":"Timing"},
{"id":"edge-7","fromNode":"node-5","fromSide":"right","toNode":"node-6","toSide":"left","label":"Technique"},
{"id":"edge-8","fromNode":"node-8","fromSide":"left","toNode":"node-9","toSide":"right","label":"if problems"},
{"id":"edge-9","fromNode":"node-3","fromSide":"top","toNode":"node-10","toSide":"bottom","label":"Grinding details"},
{"id":"edge-10","fromNode":"node-2","fromSide":"bottom","toNode":"node-11","toSide":"right","label":"Water details"},
{"id":"edge-11","fromNode":"node-8","fromSide":"right","toNode":"node-12","toSide":"left","label":"Evaluate"},
{"id":"edge-12","fromNode":"node-3","fromSide":"bottom","toNode":"node-13","toSide":"top","label":"Ratio details"}
]
}
-84
View File
@@ -1,84 +0,0 @@
---
title: Tasting Notes
type: note
permalink: coffee/tasting-notes
tags:
- '#coffee'
- '#tasting'
- '#flavor'
- '#cupping'
- '#demo'
---
# Tasting Notes
How to taste and evaluate coffee, identify flavor characteristics, and develop a personal coffee palate.
## Overview
Coffee tasting, or "cupping" in professional contexts, is the practice of observing the tastes and aromas of brewed coffee. Developing a coffee palate helps identify preferences, communicate about coffee experiences, and better understand how brewing variables affect the cup.
## Observations
- [principle] Flavor perception includes taste, aroma, mouthfeel, and retronasal perception #sensory
- [principle] Our taste buds can only perceive sweet, sour, salty, bitter, and umami #taste
- [principle] Most of what we call "flavor" is actually aroma detected retronasally #aroma
- [technique] Professional coffee tasting (cupping) uses a standardized protocol for consistency #cupping
- [technique] Slurping coffee aerates it and spreads it across all taste receptors #technique
- [technique] Allowing coffee to cool reveals different flavor notes at different temperatures #temperature
## Coffee Flavor Wheel
- [tool] The SCA Coffee Flavor Wheel provides a standardized vocabulary for describing coffee #flavor_wheel
- [category] Primary categories include: Fruity, Floral, Sweet, Nutty/Cocoa, Spice, Roasted, Other #categories
- [subcategory] Fruity breaks down into: Berry, Dried Fruit, Citrus Fruit, Stone Fruit, Tropical Fruit, etc. #fruit_notes
- [subcategory] Floral includes: Floral, Black Tea, Chamomile, Rose, Jasmine, etc. #floral_notes
- [subcategory] Sweet includes: Brown Sugar, Molasses, Honey, Maple Syrup, Vanilla, etc. #sweet_notes
- [subcategory] Nutty/Cocoa includes: Nut, Cocoa, Dark Chocolate, Chocolate, etc. #nutty_notes
- [subcategory] Spice includes: Brown Spice, Pepper, Anise, Nutmeg, Cinnamon, etc. #spice_notes
## Basic Tasting Components
- [component] Acidity: The bright, tangy quality (not sourness from under-extraction) #acidity
- [component] Sweetness: The pleasant, sugary quality balancing other elements #sweetness
- [component] Body: The physical mouthfeel and weight of the coffee #body
- [component] Finish/Aftertaste: The flavor that lingers after swallowing #finish
- [component] Balance: How well all elements work together #balance
- [component] Complexity: The range and layers of distinct flavors #complexity
- [component] Cleanliness: Absence of defects or off-flavors #cleanliness
## Common Flavor Notes by Origin
- [ethiopia] Blueberry, jasmine, bergamot, lemon, tea-like #flavor_notes
- [kenya] Blackcurrant, grapefruit, tomato-like acidity, winey #flavor_notes
- [colombia] Caramel, red apple, nuts, chocolate, balanced acidity #flavor_notes
- [guatemala] Chocolate, spice, apple, medium acidity #flavor_notes
- [brazil] Nuts, chocolate, low-to-medium acidity, full body #flavor_notes
- [indonesia] Earthy, herbal, spice, cedar, full body, low acidity #flavor_notes
- [costa_rica] Clean, bright, citrus, balanced, light chocolate #flavor_notes
## Developing Your Palate
- [technique] Taste coffees side-by-side to identify differences #comparison
- [technique] Try describing flavors before looking at roaster's notes #blind_tasting
- [technique] Keep a coffee journal with detailed notes about each coffee #journaling
- [technique] Explore different processing methods of the same origin #processing
- [technique] Try the same coffee brewed with different methods #brewing_comparison
- [technique] Use reference flavors (actual fruits, chocolates, etc.) to calibrate your palate #calibration
## Personal Coffee Experiences
- [experience] Ethiopian Yirgacheffe prepared as pour over: intense blueberry, jasmine aromatics, tea-like body
- [experience] Sumatra Mandheling in French press: earthy, cedar, herbal, tobacco, full body
- [experience] Panama Gesha as pour over: intense floral notes, jasmine, bergamot, delicate body
- [experience] Brazil Cerrado as espresso: nutty, chocolate, caramel, low acidity, great crema
- [experience] Kenya AA as pour over: bright blackcurrant, tomato-like acidity, winey finish
## Relations
- determined_by [[Flavor Extraction]]
- influenced_by [[Coffee Bean Origins]]
- varies_with [[Coffee Brewing Methods]]
- enhanced_by [[Proper Grinding Technique]]
- documented_in [[Coffee Journal]]
- part_of [[Coffee Knowledge Base]]
Binary file not shown.
-26
View File
@@ -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.
-64
View File
@@ -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

-93
View File
@@ -1,93 +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"],
}
# 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()
-27
View File
@@ -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
-40
View File
@@ -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,
)
-378
View File
@@ -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"}
-106
View File
@@ -1,106 +0,0 @@
[project]
name = "basic-memory"
version = "0.10.0"
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
readme = "README.md"
requires-python = ">=3.12.1"
license = { text = "AGPL-3.0-or-later" }
authors = [
{ name = "Basic Machines", email = "hello@basic-machines.co" }
]
dependencies = [
"sqlalchemy>=2.0.0",
"pyyaml>=6.0.1",
"typer>=0.9.0",
"aiosqlite>=0.20.0",
"greenlet>=3.1.1",
"pydantic[email,timezone]>=2.10.3",
"icecream>=2.1.3",
"mcp>=1.2.0",
"pydantic-settings>=2.6.1",
"loguru>=0.7.3",
"pyright>=1.1.390",
"markdown-it-py>=3.0.0",
"python-frontmatter>=1.1.0",
"rich>=13.9.4",
"unidecode>=1.3.8",
"dateparser>=1.2.0",
"watchfiles>=1.0.4",
"fastapi[standard]>=0.115.8",
"alembic>=1.14.1",
"qasync>=0.27.1",
"pillow>=11.1.0",
]
[project.urls]
Homepage = "https://github.com/basicmachines-co/basic-memory"
Repository = "https://github.com/basicmachines-co/basic-memory"
Documentation = "https://github.com/basicmachines-co/basic-memory#readme"
[project.scripts]
basic-memory = "basic_memory.cli.main:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
pythonpath = ["src", "tests"]
addopts = "--cov=basic_memory --cov-report term-missing -ra -q"
testpaths = ["tests"]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.uv]
dev-dependencies = [
"gevent>=24.11.1",
"icecream>=2.1.3",
"pytest>=8.3.4",
"pytest-cov>=4.1.0",
"pytest-mock>=3.12.0",
"pytest-asyncio>=0.24.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.pyright]
include = ["src/"]
exclude = ["**/__pycache__"]
ignore = ["test/"]
defineConstant = { DEBUG = true }
reportMissingImports = "error"
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.logfire]
ignore_no_config = true
-36
View File
@@ -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."
-15
View File
@@ -1,15 +0,0 @@
# 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: {}
-3
View File
@@ -1,3 +0,0 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
__version__ = "0.10.0"
-119
View File
@@ -1,119 +0,0 @@
# 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
-97
View File
@@ -1,97 +0,0 @@
"""Alembic environment configuration."""
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from basic_memory.models import Base
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
os.environ["BASIC_MEMORY_ENV"] = "test"
from basic_memory.config import config as app_config
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Set the SQLAlchemy URL from our app config
sqlalchemy_url = f"sqlite:///{app_config.database_path}"
config.set_main_option("sqlalchemy.url", sqlalchemy_url)
# 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)
# add your model's MetaData object here
# for 'autogenerate' support
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.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
include_object=include_object,
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
-24
View File
@@ -1,24 +0,0 @@
"""Functions for managing database migrations."""
from pathlib import Path
from loguru import logger
from alembic.config import Config
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 / "alembic.ini"
config = Config(alembic_ini)
config.set_main_option("script_location", str(migrations_path))
return config
def reset_database(): # pragma: no cover
"""Drop and recreate all tables."""
logger.info("Resetting database...")
config = get_alembic_config()
command.downgrade(config, "base")
command.upgrade(config, "head")
-26
View File
@@ -1,26 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -1,93 +0,0 @@
"""initial schema
Revision ID: 3dae7c7b1564
Revises:
Create Date: 2025-02-12 21:23:00.336344
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "3dae7c7b1564"
down_revision: Union[str, None] = None
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(
"entity",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sa.String(), nullable=False),
sa.Column("entity_type", sa.String(), nullable=False),
sa.Column("entity_metadata", sa.JSON(), nullable=True),
sa.Column("content_type", sa.String(), nullable=False),
sa.Column("permalink", sa.String(), nullable=False),
sa.Column("file_path", sa.String(), nullable=False),
sa.Column("checksum", sa.String(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("permalink", name="uix_entity_permalink"),
)
op.create_index("ix_entity_created_at", "entity", ["created_at"], unique=False)
op.create_index(op.f("ix_entity_file_path"), "entity", ["file_path"], unique=True)
op.create_index(op.f("ix_entity_permalink"), "entity", ["permalink"], unique=True)
op.create_index("ix_entity_title", "entity", ["title"], unique=False)
op.create_index("ix_entity_type", "entity", ["entity_type"], unique=False)
op.create_index("ix_entity_updated_at", "entity", ["updated_at"], unique=False)
op.create_table(
"observation",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("entity_id", sa.Integer(), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("category", sa.String(), nullable=False),
sa.Column("context", sa.Text(), nullable=True),
sa.Column("tags", sa.JSON(), server_default="[]", nullable=True),
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_observation_category", "observation", ["category"], unique=False)
op.create_index("ix_observation_entity_id", "observation", ["entity_id"], unique=False)
op.create_table(
"relation",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("from_id", sa.Integer(), nullable=False),
sa.Column("to_id", sa.Integer(), nullable=True),
sa.Column("to_name", sa.String(), nullable=False),
sa.Column("relation_type", sa.String(), nullable=False),
sa.Column("context", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(["from_id"], ["entity.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["to_id"], ["entity.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("from_id", "to_id", "relation_type", name="uix_relation"),
)
op.create_index("ix_relation_from_id", "relation", ["from_id"], unique=False)
op.create_index("ix_relation_to_id", "relation", ["to_id"], unique=False)
op.create_index("ix_relation_type", "relation", ["relation_type"], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("ix_relation_type", table_name="relation")
op.drop_index("ix_relation_to_id", table_name="relation")
op.drop_index("ix_relation_from_id", table_name="relation")
op.drop_table("relation")
op.drop_index("ix_observation_entity_id", table_name="observation")
op.drop_index("ix_observation_category", table_name="observation")
op.drop_table("observation")
op.drop_index("ix_entity_updated_at", table_name="entity")
op.drop_index("ix_entity_type", table_name="entity")
op.drop_index("ix_entity_title", table_name="entity")
op.drop_index(op.f("ix_entity_permalink"), table_name="entity")
op.drop_index(op.f("ix_entity_file_path"), table_name="entity")
op.drop_index("ix_entity_created_at", table_name="entity")
op.drop_table("entity")
# ### end Alembic commands ###
@@ -1,51 +0,0 @@
"""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 ###
@@ -1,44 +0,0 @@
"""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"])
@@ -1,106 +0,0 @@
"""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
);
""")
# Print instruction to manually reindex after migration
print("\n------------------------------------------------------------------")
print("IMPORTANT: After migration completes, manually run the reindex command:")
print("basic-memory sync")
print("------------------------------------------------------------------\n")
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")
-5
View File
@@ -1,5 +0,0 @@
"""Basic Memory API module."""
from .app import app
__all__ = ["app"]
-51
View File
@@ -1,51 +0,0 @@
"""FastAPI application for basic-memory knowledge graph API."""
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import http_exception_handler
from loguru import logger
from basic_memory import db
from basic_memory.config import config as app_config
from basic_memory.api.routers import knowledge, search, memory, resource, project_info
@asynccontextmanager
async def lifespan(app: FastAPI): # pragma: no cover
"""Lifecycle manager for the FastAPI app."""
await db.run_migrations(app_config)
yield
logger.info("Shutting down Basic Memory API")
await db.shutdown_db()
# Initialize FastAPI app
app = FastAPI(
title="Basic Memory API",
description="Knowledge graph API for basic-memory",
version="0.1.0",
lifespan=lifespan,
)
# 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(project_info.router)
@app.exception_handler(Exception)
async def exception_handler(request, exc): # pragma: no cover
logger.exception(
"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)))
-9
View File
@@ -1,9 +0,0 @@
"""API routers."""
from . import knowledge_router as knowledge
from . import memory_router as memory
from . import resource_router as resource
from . import search_router as search
from . import project_info_router as project_info
__all__ = ["knowledge", "memory", "resource", "search", "project_info"]
@@ -1,188 +0,0 @@
"""Router for knowledge graph operations."""
from typing import Annotated
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Query, Response
from loguru import logger
from basic_memory.deps import (
EntityServiceDep,
get_search_service,
SearchServiceDep,
LinkResolverDep,
)
from basic_memory.schemas import (
EntityListResponse,
EntityResponse,
DeleteEntitiesResponse,
DeleteEntitiesRequest,
)
from basic_memory.schemas.base import Permalink, Entity
from basic_memory.services.exceptions import EntityNotFoundError
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
## Create endpoints
@router.post("/entities", response_model=EntityResponse)
async def create_entity(
data: Entity,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
) -> EntityResponse:
"""Create an entity."""
logger.info(
"API request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
)
entity = await entity_service.create_entity(data)
# reindex
await search_service.index_entity(entity, background_tasks=background_tasks)
result = EntityResponse.model_validate(entity)
logger.info(
"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(
permalink: Permalink,
data: Entity,
response: Response,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
) -> EntityResponse:
"""Create or update an entity. If entity exists, it will be updated, otherwise created."""
logger.info(
"API request",
endpoint="create_or_update_entity",
permalink=permalink,
entity_type=data.entity_type,
title=data.title,
)
# Validate permalink matches
if data.permalink != permalink:
logger.warning(
"API validation error",
endpoint="create_or_update_entity",
permalink=permalink,
data_permalink=data.permalink,
error="Permalink mismatch",
)
raise HTTPException(status_code=400, detail="Entity permalink must match URL path")
# Try create_or_update operation
entity, created = await entity_service.create_or_update_entity(data)
response.status_code = 201 if created else 200
# reindex
await search_service.index_entity(entity, background_tasks=background_tasks)
result = EntityResponse.model_validate(entity)
logger.info(
"API response",
endpoint="create_or_update_entity",
title=result.title,
permalink=result.permalink,
created=created,
status_code=response.status_code,
)
return result
## Read endpoints
@router.get("/entities/{permalink:path}", response_model=EntityResponse)
async def get_entity(
entity_service: EntityServiceDep,
permalink: str,
) -> EntityResponse:
"""Get a specific entity by ID.
Args:
permalink: Entity path ID
content: If True, include full file content
:param entity_service: EntityService
"""
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")
@router.get("/entities", response_model=EntityListResponse)
async def get_entities(
entity_service: EntityServiceDep,
permalink: Annotated[list[str] | None, Query()] = None,
) -> EntityListResponse:
"""Open specific entities"""
logger.info(f"request: get_entities with permalinks={permalink}")
entities = await entity_service.get_entities_by_permalinks(permalink) if permalink else []
result = EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in entities]
)
return result
## Delete endpoints
@router.delete("/entities/{identifier:path}", response_model=DeleteEntitiesResponse)
async def delete_entity(
identifier: str,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
link_resolver: LinkResolverDep,
search_service=Depends(get_search_service),
) -> DeleteEntitiesResponse:
"""Delete a single entity and remove from search index."""
logger.info(f"request: delete_entity with identifier={identifier}")
entity = await link_resolver.resolve_link(identifier)
if entity is None:
return DeleteEntitiesResponse(deleted=False)
# Delete the entity
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)
result = DeleteEntitiesResponse(deleted=deleted)
return result
@router.post("/entities/delete", response_model=DeleteEntitiesResponse)
async def delete_entities(
data: DeleteEntitiesRequest,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service=Depends(get_search_service),
) -> DeleteEntitiesResponse:
"""Delete entities and remove from search index."""
logger.info(f"request: delete_entities with data={data}")
deleted = False
# Remove each deleted entity from search index
for permalink in data.permalinks:
deleted = await entity_service.delete_entity(permalink)
background_tasks.add_task(search_service.delete_by_permalink, permalink)
result = DeleteEntitiesResponse(deleted=deleted)
return result
@@ -1,146 +0,0 @@
"""Routes for memory:// URI operations."""
from typing import Annotated
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.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
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:
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.type,
from_entity=from_entity.permalink, # pyright: ignore
to_entity=to_entity.permalink if to_entity else None,
created_at=item.created_at,
)
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,
entity_repository: EntityRepositoryDep,
type: Annotated[list[SearchItemType] | None, Query()] = None,
depth: int = 1,
timeframe: TimeFrame = "7d",
page: int = 1,
page_size: int = 10,
max_related: int = 10,
) -> GraphContext:
# return all types by default
types = (
[SearchItemType.ENTITY, SearchItemType.RELATION, SearchItemType.OBSERVATION]
if not type
else type
)
logger.debug(
f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
)
# Parse timeframe
since = parse(timeframe)
limit = page_size
offset = (page - 1) * page_size
# Build context
context = await context_service.build_context(
types=types, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
)
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
@router.get("/{uri:path}", response_model=GraphContext)
async def get_memory_context(
context_service: ContextServiceDep,
entity_repository: EntityRepositoryDep,
uri: str,
depth: int = 1,
timeframe: TimeFrame = "7d",
page: int = 1,
page_size: int = 10,
max_related: int = 10,
) -> GraphContext:
"""Get rich context from memory:// URI."""
# add the project name from the config to the url as the "host
# Parse URI
logger.debug(
f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
)
memory_url = normalize_memory_url(uri)
# Parse timeframe
since = parse(timeframe)
limit = page_size
offset = (page - 1) * page_size
# Build context
context = await context_service.build_context(
memory_url, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
)
return await to_graph_context(
context, entity_repository=entity_repository, page=page, page_size=page_size
)
@@ -1,274 +0,0 @@
"""Router for statistics and system information."""
import json
from datetime import datetime
from basic_memory.config import config, config_manager
from basic_memory.deps import (
ProjectInfoRepositoryDep,
)
from basic_memory.repository.project_info_repository import ProjectInfoRepository
from basic_memory.schemas import (
ProjectInfoResponse,
ProjectStatistics,
ActivityMetrics,
SystemStatus,
)
from basic_memory.sync.watch_service import WATCH_STATUS_JSON
from fastapi import APIRouter
from sqlalchemy import text
router = APIRouter(prefix="/stats", tags=["statistics"])
@router.get("/project-info", response_model=ProjectInfoResponse)
async def get_project_info(
repository: ProjectInfoRepositoryDep,
) -> ProjectInfoResponse:
"""Get comprehensive information about the current Basic Memory project."""
# Get statistics
statistics = await get_statistics(repository)
# Get activity metrics
activity = await get_activity_metrics(repository)
# Get system status
system = await get_system_status()
# Get project configuration information
project_name = config.project
project_path = str(config.home)
available_projects = config_manager.projects
default_project = config_manager.default_project
# Construct the response
return ProjectInfoResponse(
project_name=project_name,
project_path=project_path,
available_projects=available_projects,
default_project=default_project,
statistics=statistics,
activity=activity,
system=system,
)
async def get_statistics(repository: ProjectInfoRepository) -> ProjectStatistics:
"""Get statistics about the current project."""
# Get basic counts
entity_count_result = await repository.execute_query(text("SELECT COUNT(*) FROM entity"))
total_entities = entity_count_result.scalar() or 0
observation_count_result = await repository.execute_query(
text("SELECT COUNT(*) FROM observation")
)
total_observations = observation_count_result.scalar() or 0
relation_count_result = await repository.execute_query(text("SELECT COUNT(*) FROM relation"))
total_relations = relation_count_result.scalar() or 0
unresolved_count_result = await repository.execute_query(
text("SELECT COUNT(*) FROM relation WHERE to_id IS NULL")
)
total_unresolved = unresolved_count_result.scalar() or 0
# Get entity counts by type
entity_types_result = await repository.execute_query(
text("SELECT entity_type, COUNT(*) FROM entity GROUP BY entity_type")
)
entity_types = {row[0]: row[1] for row in entity_types_result.fetchall()}
# Get observation counts by category
category_result = await repository.execute_query(
text("SELECT category, COUNT(*) FROM observation GROUP BY category")
)
observation_categories = {row[0]: row[1] for row in category_result.fetchall()}
# Get relation counts by type
relation_types_result = await repository.execute_query(
text("SELECT relation_type, COUNT(*) FROM relation GROUP BY relation_type")
)
relation_types = {row[0]: row[1] for row in relation_types_result.fetchall()}
# Find most connected entities (most outgoing relations)
connected_result = await repository.execute_query(
text("""
SELECT e.id, e.title, e.permalink, COUNT(r.id) AS relation_count
FROM entity e
JOIN relation r ON e.id = r.from_id
GROUP BY e.id
ORDER BY relation_count DESC
LIMIT 10
""")
)
most_connected = [
{"id": row[0], "title": row[1], "permalink": row[2], "relation_count": row[3]}
for row in connected_result.fetchall()
]
# Count isolated entities (no relations)
isolated_result = await repository.execute_query(
text("""
SELECT COUNT(e.id)
FROM entity e
LEFT JOIN relation r1 ON e.id = r1.from_id
LEFT JOIN relation r2 ON e.id = r2.to_id
WHERE r1.id IS NULL AND r2.id IS NULL
""")
)
isolated_count = isolated_result.scalar() or 0
return ProjectStatistics(
total_entities=total_entities,
total_observations=total_observations,
total_relations=total_relations,
total_unresolved_relations=total_unresolved,
entity_types=entity_types,
observation_categories=observation_categories,
relation_types=relation_types,
most_connected_entities=most_connected,
isolated_entities=isolated_count,
)
async def get_activity_metrics(repository: ProjectInfoRepository) -> ActivityMetrics:
"""Get activity metrics for the current project."""
# Get recently created entities
created_result = await repository.execute_query(
text("""
SELECT id, title, permalink, entity_type, created_at
FROM entity
ORDER BY created_at DESC
LIMIT 10
""")
)
recently_created = [
{
"id": row[0],
"title": row[1],
"permalink": row[2],
"entity_type": row[3],
"created_at": row[4],
}
for row in created_result.fetchall()
]
# Get recently updated entities
updated_result = await repository.execute_query(
text("""
SELECT id, title, permalink, entity_type, updated_at
FROM entity
ORDER BY updated_at DESC
LIMIT 10
""")
)
recently_updated = [
{
"id": row[0],
"title": row[1],
"permalink": row[2],
"entity_type": row[3],
"updated_at": row[4],
}
for row in updated_result.fetchall()
]
# Get monthly growth over the last 6 months
# Calculate the start of 6 months ago
now = datetime.now()
six_months_ago = datetime(
now.year - (1 if now.month <= 6 else 0), ((now.month - 6) % 12) or 12, 1
)
# Query for monthly entity creation
entity_growth_result = await repository.execute_query(
text(f"""
SELECT
strftime('%Y-%m', created_at) AS month,
COUNT(*) AS count
FROM entity
WHERE created_at >= '{six_months_ago.isoformat()}'
GROUP BY month
ORDER BY month
""")
)
entity_growth = {row[0]: row[1] for row in entity_growth_result.fetchall()}
# Query for monthly observation creation
observation_growth_result = await repository.execute_query(
text(f"""
SELECT
strftime('%Y-%m', created_at) AS month,
COUNT(*) AS count
FROM observation
INNER JOIN entity ON observation.entity_id = entity.id
WHERE entity.created_at >= '{six_months_ago.isoformat()}'
GROUP BY month
ORDER BY month
""")
)
observation_growth = {row[0]: row[1] for row in observation_growth_result.fetchall()}
# Query for monthly relation creation
relation_growth_result = await repository.execute_query(
text(f"""
SELECT
strftime('%Y-%m', created_at) AS month,
COUNT(*) AS count
FROM relation
INNER JOIN entity ON relation.from_id = entity.id
WHERE entity.created_at >= '{six_months_ago.isoformat()}'
GROUP BY month
ORDER BY month
""")
)
relation_growth = {row[0]: row[1] for row in relation_growth_result.fetchall()}
# Combine all monthly growth data
monthly_growth = {}
for month in set(
list(entity_growth.keys()) + list(observation_growth.keys()) + list(relation_growth.keys())
):
monthly_growth[month] = {
"entities": entity_growth.get(month, 0),
"observations": observation_growth.get(month, 0),
"relations": relation_growth.get(month, 0),
"total": (
entity_growth.get(month, 0)
+ observation_growth.get(month, 0)
+ relation_growth.get(month, 0)
),
}
return ActivityMetrics(
recently_created=recently_created,
recently_updated=recently_updated,
monthly_growth=monthly_growth,
)
async def get_system_status() -> SystemStatus:
"""Get system status information."""
import basic_memory
# Get database information
db_path = config.database_path
db_size = db_path.stat().st_size if db_path.exists() else 0
db_size_readable = f"{db_size / (1024 * 1024):.2f} MB"
# Get watch service status if available
watch_status = None
watch_status_path = config.home / ".basic-memory" / WATCH_STATUS_JSON
if watch_status_path.exists():
try:
watch_status = json.loads(watch_status_path.read_text(encoding="utf-8"))
except Exception: # pragma: no cover
pass
return SystemStatus(
version=basic_memory.__version__,
database_path=str(db_path),
database_size=db_size_readable,
watch_status=watch_status,
timestamp=datetime.now(),
)
@@ -1,225 +0,0 @@
"""Routes for getting entity content."""
import tempfile
from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, HTTPException, BackgroundTasks, Body
from fastapi.responses import FileResponse, JSONResponse
from loguru import logger
from basic_memory.deps import (
ProjectConfigDep,
LinkResolverDep,
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"])
def get_entity_ids(item: SearchIndexRow) -> set[int]:
match item.type:
case SearchItemType.ENTITY:
return {item.id}
case SearchItemType.OBSERVATION:
return {item.entity_id} # pyright: ignore [reportReturnType]
case SearchItemType.RELATION:
from_entity = item.from_id
to_entity = item.to_id # pyright: ignore [reportReturnType]
return {from_entity, to_entity} if to_entity else {from_entity} # pyright: ignore [reportReturnType]
case _: # pragma: no cover
raise ValueError(f"Unexpected type: {item.type}")
@router.get("/{identifier:path}")
async def get_resource_content(
config: ProjectConfigDep,
link_resolver: LinkResolverDep,
search_service: SearchServiceDep,
entity_service: EntityServiceDep,
file_service: FileServiceDep,
background_tasks: BackgroundTasks,
identifier: str,
page: int = 1,
page_size: int = 10,
) -> FileResponse:
"""Get resource content by identifier: name or permalink."""
logger.debug(f"Getting content for: {identifier}")
# Find single entity by permalink
entity = await link_resolver.resolve_link(identifier)
results = [entity] if entity else []
# pagination for multiple results
limit = page_size
offset = (page - 1) * page_size
# search using the identifier as a permalink
if not results:
# if the identifier contains a wildcard, use GLOB search
query = (
SearchQuery(permalink_match=identifier)
if "*" in identifier
else SearchQuery(permalink=identifier)
)
search_results = await search_service.search(query, limit, offset)
if not search_results:
raise HTTPException(status_code=404, detail=f"Resource not found: {identifier}")
# get the deduplicated entities related to the search results
entity_ids = {id for result in search_results for id in get_entity_ids(result)}
results = await entity_service.get_entities_by_id(list(entity_ids))
# return single response
if len(results) == 1:
entity = results[0]
file_path = Path(f"{config.home}/{entity.file_path}")
if not file_path.exists():
raise HTTPException(
status_code=404,
detail=f"File not found: {file_path}",
)
return FileResponse(path=file_path)
# for multiple files, initialize a temporary file for writing the results
with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".md") as tmp_file:
temp_file_path = tmp_file.name
for result in results:
# Read content for each entity
content = await file_service.read_entity_content(result)
memory_url = normalize_memory_url(result.permalink)
modified_date = result.updated_at.isoformat()
checksum = result.checksum[:8] if result.checksum else ""
# Prepare the delimited content
response_content = f"--- {memory_url} {modified_date} {checksum}\n"
response_content += f"\n{content}\n"
response_content += "\n"
# Write content directly to the temporary file in append mode
tmp_file.write(response_content)
# Ensure all content is written to disk
tmp_file.flush()
# Schedule the temporary file to be deleted after the response
background_tasks.add_task(cleanup_temp_file, temp_file_path)
# Return the file response
return FileResponse(path=temp_file_path)
def cleanup_temp_file(file_path: str):
"""Delete the temporary file."""
try:
Path(file_path).unlink() # Deletes the file
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,54 +0,0 @@
"""Router for search operations."""
from fastapi import APIRouter, BackgroundTasks
from basic_memory.schemas.search import SearchQuery, SearchResult, SearchResponse
from basic_memory.deps import SearchServiceDep, EntityServiceDep
router = APIRouter(prefix="/search", tags=["search"])
@router.post("/", response_model=SearchResponse)
async def search(
query: SearchQuery,
search_service: SearchServiceDep,
entity_service: EntityServiceDep,
page: int = 1,
page_size: int = 10,
):
"""Search across all knowledge and documents."""
limit = page_size
offset = (page - 1) * page_size
results = await search_service.search(query, limit=limit, offset=offset)
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 SearchResponse(
results=search_results,
current_page=page,
page_size=page_size,
)
@router.post("/reindex")
async def reindex(background_tasks: BackgroundTasks, search_service: SearchServiceDep):
"""Recreate and populate the search index."""
await search_service.reindex_all(background_tasks=background_tasks)
return {"status": "ok", "message": "Reindex initiated"}
-1
View File
@@ -1 +0,0 @@
"""CLI tools for basic-memory"""
-69
View File
@@ -1,69 +0,0 @@
import asyncio
from typing import Optional
import typer
from basic_memory import db
from basic_memory.config import config
def version_callback(value: bool) -> None:
"""Show version and exit."""
if value: # pragma: no cover
import basic_memory
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
raise typer.Exit()
app = typer.Typer(name="basic-memory")
@app.callback()
def app_callback(
project: Optional[str] = typer.Option(
None,
"--project",
"-p",
help="Specify which project to use",
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."""
# We use the project option to set the BASIC_MEMORY_PROJECT environment variable
# The config module will pick this up when loading
if project: # pragma: no cover
import os
import importlib
from basic_memory import config as config_module
# Set the environment variable
os.environ["BASIC_MEMORY_PROJECT"] = project
# Reload the config module to pick up the new project
importlib.reload(config_module)
# Update the local reference
global config
from basic_memory.config import config as new_config
config = new_config
# Run database migrations
asyncio.run(db.run_migrations(config))
# 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")
-18
View File
@@ -1,18 +0,0 @@
"""CLI commands for basic-memory."""
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
from . import import_claude_projects, import_chatgpt, tool, project, project_info
__all__ = [
"status",
"sync",
"db",
"import_memory_json",
"mcp",
"import_claude_conversations",
"import_claude_projects",
"import_chatgpt",
"tool",
"project",
"project_info",
]
-24
View File
@@ -1,24 +0,0 @@
"""Database management commands."""
import typer
from loguru import logger
from basic_memory.alembic import migrations
from basic_memory.cli.app import app
@app.command()
def reset(
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
): # pragma: no cover
"""Reset database (drop all tables and recreate)."""
if typer.confirm("This will delete all data in your db. Are you sure?"):
logger.info("Resetting database...")
migrations.reset_database()
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
@@ -1,258 +0,0 @@
"""Import command for ChatGPT conversations."""
import asyncio
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, List, Annotated, Set, Optional
import typer
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
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
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(encoding="utf-8"))
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)
return MarkdownProcessor(entity_parser)
@import_app.command(name="chatgpt", help="Import conversations from ChatGPT JSON export.")
def import_chatgpt(
conversations_json: Annotated[
Path, typer.Argument(help="Path to ChatGPT conversations.json file")
] = Path("conversations.json"),
folder: Annotated[
str, typer.Option(help="The folder to place the files in.")
] = "conversations",
):
"""Import chat conversations from ChatGPT JSON format.
This command will:
1. Read the complex tree structure of messages
2. Convert them to linear markdown conversations
3. Save as clean, readable markdown files
After importing, run 'basic-memory sync' to index the new files.
"""
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)
raise typer.Exit(1)
@@ -1,210 +0,0 @@
"""Import command for basic-memory CLI to import chat data from conversations2.json format."""
import asyncio
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, List, Annotated
import typer
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
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
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(encoding="utf-8"))
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)
return MarkdownProcessor(entity_parser)
@claude_app.command(name="conversations", help="Import chat conversations from Claude.ai.")
def import_claude(
conversations_json: Annotated[
Path, typer.Argument(..., help="Path to conversations.json file")
] = Path("conversations.json"),
folder: Annotated[
str, typer.Option(help="The folder to place the files in.")
] = "conversations",
):
"""Import chat conversations from conversations2.json format.
This command will:
1. Read chat data and nested messages
2. Create markdown files for each conversation
3. Format content in clean, readable markdown
After importing, run 'basic-memory sync' to index the new files.
"""
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)
raise typer.Exit(1)
@@ -1,193 +0,0 @@
"""Import command for basic-memory CLI to import project data from Claude.ai."""
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, Annotated, Optional
import typer
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
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
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(encoding="utf-8"))
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)
return MarkdownProcessor(entity_parser)
@claude_app.command(name="projects", help="Import projects from Claude.ai.")
def import_projects(
projects_json: Annotated[Path, typer.Argument(..., help="Path to projects.json file")] = Path(
"projects.json"
),
base_folder: Annotated[
str, typer.Option(help="The base folder to place project files in.")
] = "projects",
):
"""Import project data from Claude.ai.
This command will:
1. Create a directory for each project
2. Store docs in a docs/ subdirectory
3. Place prompt template in project root
After importing, run 'basic-memory sync' to index the new files.
"""
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)
raise typer.Exit(1)
@@ -1,144 +0,0 @@
"""Import command for basic-memory CLI to import from JSON memory format."""
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, List, Annotated
import typer
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)
return MarkdownProcessor(entity_parser)
@import_app.command()
def memory_json(
json_path: Annotated[Path, typer.Argument(..., help="Path to memory.json file")] = Path(
"memory.json"
),
):
"""Import entities and relations from a memory.json file.
This command will:
1. Read entities and relations from the JSON file
2. Create markdown files for each entity
3. Include outgoing relations in each entity's markdown
After importing, run 'basic-memory sync' to index the new files.
"""
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())
# 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,
)
)
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)
-26
View File
@@ -1,26 +0,0 @@
"""MCP server command."""
from loguru import logger
import basic_memory
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
# Import mcp tools to register them
import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
@app.command()
def mcp(): # pragma: no cover
"""Run the MCP server for Claude Desktop integration."""
home_dir = config.home
project_name = config.project
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
logger.info(f"Project: {project_name}")
logger.info(f"Project directory: {home_dir}")
mcp_server.run()
-119
View File
@@ -1,119 +0,0 @@
"""Command module for basic-memory project management."""
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.config import ConfigManager, config
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)
return path
@project_app.command("list")
def list_projects() -> None:
"""List all configured projects."""
config_manager = ConfigManager()
projects = config_manager.projects
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")
default_project = config_manager.default_project
active_project = config.project
for name, path in projects.items():
is_default = "" if name == default_project else ""
is_active = "" if name == active_project else ""
table.add_row(name, format_path(path), is_default, is_active)
console.print(table)
@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"),
) -> None:
"""Add a new project."""
config_manager = ConfigManager()
try:
# Resolve to absolute path
resolved_path = os.path.abspath(os.path.expanduser(path))
config_manager.add_project(name, resolved_path)
console.print(f"[green]Project '{name}' added at {format_path(resolved_path)}[/green]")
# 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}")
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
@project_app.command("remove")
def remove_project(
name: str = typer.Argument(..., help="Name of the project to remove"),
) -> None:
"""Remove a project from configuration."""
config_manager = ConfigManager()
try:
config_manager.remove_project(name)
console.print(f"[green]Project '{name}' removed from configuration[/green]")
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
except ValueError as e: # pragma: no cover
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
@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."""
config_manager = ConfigManager()
try:
config_manager.set_default_project(name)
console.print(f"[green]Project '{name}' set as default[/green]")
except ValueError as e: # pragma: no cover
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
@project_app.command("current")
def show_current_project() -> None:
"""Show the current project."""
config_manager = ConfigManager()
current = os.environ.get("BASIC_MEMORY_PROJECT", config_manager.default_project)
try:
path = config_manager.get_project_path(current)
console.print(f"Current project: [cyan]{current}[/cyan]")
console.print(f"Path: [green]{format_path(str(path))}[/green]")
console.print(f"Database: [blue]{format_path(str(config.database_path))}[/blue]")
except ValueError: # pragma: no cover
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
@@ -1,167 +0,0 @@
"""CLI command for project info status."""
import asyncio
import json
from datetime import datetime
import typer
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.tree import Tree
from basic_memory.cli.app import app
from basic_memory.mcp.tools.project_info import project_info
info_app = typer.Typer()
app.add_typer(info_app, name="info", help="Get information about your Basic Memory project")
@info_app.command("stats")
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())
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:
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:
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, path in info.available_projects.items():
is_default = name == info.default_project
projects_table.add_row(name, 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)
-142
View File
@@ -1,142 +0,0 @@
"""Status command for basic-memory CLI."""
import asyncio
from typing import Set, Dict
import typer
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree
from basic_memory.cli.app import app
from basic_memory.cli.commands.sync import get_sync_service
from basic_memory.config import config
from basic_memory.sync import SyncService
from basic_memory.sync.sync_service import SyncReport
# Create rich console
console = Console()
def add_files_to_tree(
tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
):
"""Add files to tree, grouped by directory."""
# Group by directory
by_dir = {}
for path in sorted(paths):
parts = path.split("/", 1)
dir_name = parts[0] if len(parts) > 1 else ""
file_name = parts[1] if len(parts) > 1 else parts[0]
by_dir.setdefault(dir_name, []).append((file_name, path))
# Add to tree
for dir_name, files in sorted(by_dir.items()):
if dir_name:
branch = tree.add(f"[bold]{dir_name}/[/bold]")
else:
branch = tree
for file_name, full_path in sorted(files):
if checksums and full_path in checksums:
checksum_short = checksums[full_path][:8]
branch.add(f"[{style}]{file_name}[/{style}] ({checksum_short})")
else:
branch.add(f"[{style}]{file_name}[/{style}]")
def group_changes_by_directory(changes: SyncReport) -> Dict[str, Dict[str, int]]:
"""Group changes by directory for summary view."""
by_dir = {}
for change_type, paths in [
("new", changes.new),
("modified", changes.modified),
("deleted", changes.deleted),
]:
for path in paths:
dir_name = path.split("/", 1)[0]
by_dir.setdefault(dir_name, {"new": 0, "modified": 0, "deleted": 0, "moved": 0})
by_dir[dir_name][change_type] += 1
# Handle moves - count in both source and destination directories
for old_path, new_path in changes.moves.items():
old_dir = old_path.split("/", 1)[0]
new_dir = new_path.split("/", 1)[0]
by_dir.setdefault(old_dir, {"new": 0, "modified": 0, "deleted": 0, "moved": 0})
by_dir.setdefault(new_dir, {"new": 0, "modified": 0, "deleted": 0, "moved": 0})
by_dir[old_dir]["moved"] += 1
if old_dir != new_dir:
by_dir[new_dir]["moved"] += 1
return by_dir
def build_directory_summary(counts: Dict[str, int]) -> str:
"""Build summary string for directory changes."""
parts = []
if counts["new"]:
parts.append(f"[green]+{counts['new']} new[/green]")
if counts["modified"]:
parts.append(f"[yellow]~{counts['modified']} modified[/yellow]")
if counts["moved"]:
parts.append(f"[blue]↔{counts['moved']} moved[/blue]")
if counts["deleted"]:
parts.append(f"[red]-{counts['deleted']} deleted[/red]")
return " ".join(parts)
def display_changes(title: str, changes: SyncReport, verbose: bool = False):
"""Display changes using Rich for better visualization."""
tree = Tree(title)
if changes.total == 0:
tree.add("No changes")
console.print(Panel(tree, expand=False))
return
if verbose:
# Full file listing with checksums
if changes.new:
new_branch = tree.add("[green]New Files[/green]")
add_files_to_tree(new_branch, changes.new, "green", changes.checksums)
if changes.modified:
mod_branch = tree.add("[yellow]Modified[/yellow]")
add_files_to_tree(mod_branch, changes.modified, "yellow", changes.checksums)
if changes.moves:
move_branch = tree.add("[blue]Moved[/blue]")
for old_path, new_path in sorted(changes.moves.items()):
move_branch.add(f"[blue]{old_path}[/blue] → [blue]{new_path}[/blue]")
if changes.deleted:
del_branch = tree.add("[red]Deleted[/red]")
add_files_to_tree(del_branch, changes.deleted, "red")
else:
# Show directory summaries
by_dir = group_changes_by_directory(changes)
for dir_name, counts in sorted(by_dir.items()):
summary = build_directory_summary(counts)
if summary: # Only show directories with changes
tree.add(f"[bold]{dir_name}/[/bold] {summary}")
console.print(Panel(tree, expand=False))
async def run_status(sync_service: SyncService, verbose: bool = False):
"""Check sync status of files vs database."""
# Check knowledge/ directory
knowledge_changes = await sync_service.scan(config.home)
display_changes("Status", knowledge_changes, verbose)
@app.command()
def status(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed file information"),
):
"""Show sync status between files and database."""
try:
sync_service = asyncio.run(get_sync_service())
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
except Exception as e:
logger.exception(f"Error checking status: {e}")
typer.echo(f"Error checking status: {e}", err=True)
raise typer.Exit(code=1) # pragma: no cover
-247
View File
@@ -1,247 +0,0 @@
"""Command module for basic-memory sync operations."""
import asyncio
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict
import typer
from loguru import logger
from rich.console import Console
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.markdown import EntityParser
from basic_memory.markdown.markdown_processor import MarkdownProcessor
from basic_memory.repository import (
EntityRepository,
ObservationRepository,
RelationRepository,
)
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
from basic_memory.sync.sync_service import SyncReport
from basic_memory.sync.watch_service import WatchService
console = Console()
@dataclass
class ValidationIssue:
file_path: str
error: str
async def get_sync_service(): # 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
)
entity_parser = EntityParser(config.home)
markdown_processor = MarkdownProcessor(entity_parser)
file_service = FileService(config.home, markdown_processor)
# Initialize repositories
entity_repository = EntityRepository(session_maker)
observation_repository = ObservationRepository(session_maker)
relation_repository = RelationRepository(session_maker)
search_repository = SearchRepository(session_maker)
# Initialize services
search_service = SearchService(search_repository, entity_repository, file_service)
link_resolver = LinkResolver(entity_repository, search_service)
# Initialize services
entity_service = EntityService(
entity_parser,
entity_repository,
observation_repository,
relation_repository,
file_service,
link_resolver,
)
# Create sync service
sync_service = SyncService(
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
def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[ValidationIssue]]:
"""Group validation issues by directory."""
grouped = defaultdict(list)
for issue in issues:
dir_name = Path(issue.file_path).parent.name
grouped[dir_name].append(issue)
return dict(grouped)
def display_sync_summary(knowledge: SyncReport):
"""Display a one-line summary of sync changes."""
total_changes = knowledge.total
project_name = config.project
if total_changes == 0:
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)"
changes = []
new_count = len(knowledge.new)
mod_count = len(knowledge.modified)
move_count = len(knowledge.moves)
del_count = len(knowledge.deleted)
if new_count:
changes.append(f"[green]{new_count} new[/green]")
if mod_count:
changes.append(f"[yellow]{mod_count} modified[/yellow]")
if move_count:
changes.append(f"[blue]{move_count} moved[/blue]")
if del_count:
changes.append(f"[red]{del_count} deleted[/red]")
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."""
project_name = config.project
if knowledge.total == 0:
console.print(f"\n[green]Project '{project_name}': Everything up to date[/green]")
return
console.print(f"\n[bold]Sync Results for Project '{project_name}'[/bold]")
if knowledge.total > 0:
knowledge_tree = Tree("[bold]Knowledge Files[/bold]")
if knowledge.new:
created = knowledge_tree.add("[green]Created[/green]")
for path in sorted(knowledge.new):
checksum = knowledge.checksums.get(path, "")
created.add(f"[green]{path}[/green] ({checksum[:8]})")
if knowledge.modified:
modified = knowledge_tree.add("[yellow]Modified[/yellow]")
for path in sorted(knowledge.modified):
checksum = knowledge.checksums.get(path, "")
modified.add(f"[yellow]{path}[/yellow] ({checksum[:8]})")
if knowledge.moves:
moved = knowledge_tree.add("[blue]Moved[/blue]")
for old_path, new_path in sorted(knowledge.moves.items()):
checksum = knowledge.checksums.get(new_path, "")
moved.add(f"[blue]{old_path}[/blue] → [blue]{new_path}[/blue] ({checksum[:8]})")
if knowledge.deleted:
deleted = knowledge_tree.add("[red]Deleted[/red]")
for path in sorted(knowledge.deleted):
deleted.add(f"[red]{path}[/red]")
console.print(knowledge_tree)
async def run_sync(verbose: bool = False, watch: bool = False, console_status: bool = False):
"""Run sync operation."""
import time
start_time = time.time()
logger.info(
"Sync command started",
project=config.project,
watch_mode=watch,
verbose=verbose,
directory=str(config.home),
)
sync_service = await get_sync_service()
# Start watching if requested
if watch:
logger.info("Starting watch service after initial sync")
watch_service = WatchService(
sync_service=sync_service,
file_service=sync_service.entity_service.file_service,
config=config,
)
# full sync - no progress bars in watch mode
await sync_service.sync(config.home, show_progress=False)
# watch changes
await watch_service.run() # pragma: no cover
else:
# one time sync - use progress bars for better UX
logger.info("Running one-time sync")
knowledge_changes = await sync_service.sync(config.home, show_progress=True)
# 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:
display_sync_summary(knowledge_changes) # pragma: no cover
@app.command()
def sync(
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
help="Show detailed sync information.",
),
watch: bool = typer.Option(
False,
"--watch",
"-w",
help="Start watching for changes after sync.",
),
) -> None:
"""Sync knowledge files with the database."""
try:
# Show which project we're syncing
if not watch: # Don't show in watch mode as it would break the UI
typer.echo(f"Syncing project: {config.project}")
typer.echo(f"Project path: {config.home}")
# Run sync
asyncio.run(run_sync(verbose=verbose, watch=watch))
except Exception as e: # pragma: no cover
if not isinstance(e, typer.Exit):
logger.exception(
"Sync command failed",
project=config.project,
error=str(e),
error_type=type(e).__name__,
watch_mode=watch,
directory=str(config.home),
)
typer.echo(f"Error during sync: {e}", err=True)
raise typer.Exit(1)
raise
-253
View File
@@ -1,253 +0,0 @@
"""CLI tool commands for Basic Memory."""
import asyncio
import sys
from typing import Optional, List, Annotated
import typer
from loguru import logger
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 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
# 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.schemas.base import TimeFrame
from basic_memory.schemas.memory import MemoryUrl
from basic_memory.schemas.search import SearchQuery, SearchItemType
tool_app = typer.Typer()
app.add_typer(tool_app, name="tool", help="Direct 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(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,
)
)
# 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,
):
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,
)
)
# 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 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 not (permalink or title) 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))
# 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(topic=topic, 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
# @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
-58
View File
@@ -1,58 +0,0 @@
"""Main CLI entry point for basic-memory.""" # pragma: no cover
from basic_memory.cli.app import app # pragma: no cover
import typer
# Register commands
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
status,
sync,
db,
import_memory_json,
mcp,
import_claude_conversations,
import_claude_projects,
import_chatgpt,
tool,
project,
)
# Version command
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
project: str = typer.Option( # noqa
"main",
"--project",
"-p",
help="Specify which project to use",
envvar="BASIC_MEMORY_PROJECT",
),
version: bool = typer.Option(
False,
"--version",
"-V",
help="Show version information and exit.",
is_eager=True,
),
):
"""Basic Memory - Local-first personal knowledge management system."""
if version: # pragma: no cover
from basic_memory import __version__
from basic_memory.config import config
typer.echo(f"Basic Memory v{__version__}")
typer.echo(f"Current project: {config.project}")
typer.echo(f"Project path: {config.home}")
raise typer.Exit()
# Handle project selection via environment variable
if project:
import os
os.environ["BASIC_MEMORY_PROJECT"] = project
if __name__ == "__main__": # pragma: no cover
app()
-223
View File
@@ -1,223 +0,0 @@
"""Configuration management for basic-memory."""
import json
import os
from pathlib import Path
from typing import Any, Dict, Literal, Optional
import basic_memory
from basic_memory.utils import setup_logging
from loguru import logger
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
DATABASE_NAME = "memory.db"
DATA_DIR_NAME = ".basic-memory"
CONFIG_FILE_NAME = "config.json"
Environment = Literal["test", "dev", "user"]
class ProjectConfig(BaseSettings):
"""Configuration for a specific basic-memory project."""
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",
)
# Name of the project
project: str = Field(default="default", description="Project name")
# Watch service configuration
sync_delay: int = Field(
default=500, description="Milliseconds to wait after changes before syncing", gt=0
)
log_level: str = "DEBUG"
model_config = SettingsConfigDict(
env_prefix="BASIC_MEMORY_",
extra="ignore",
env_file=".env",
env_file_encoding="utf-8",
)
@property
def database_path(self) -> Path:
"""Get SQLite database path."""
database_path = self.home / DATA_DIR_NAME / DATABASE_NAME
if not database_path.exists():
database_path.parent.mkdir(parents=True, exist_ok=True)
database_path.touch()
return database_path
@field_validator("home")
@classmethod
def ensure_path_exists(cls, v: Path) -> Path: # pragma: no cover
"""Ensure project path exists."""
if not v.exists():
v.mkdir(parents=True)
return v
class BasicMemoryConfig(BaseSettings):
"""Pydantic model for Basic Memory global configuration."""
projects: Dict[str, str] = Field(
default_factory=lambda: {"main": str(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",
)
model_config = SettingsConfigDict(
env_prefix="BASIC_MEMORY_",
extra="ignore",
)
def model_post_init(self, __context: Any) -> None:
"""Ensure configuration is valid after initialization."""
# Ensure main project exists
if "main" not in self.projects:
self.projects["main"] = str(Path.home() / "basic-memory")
# Ensure default project is valid
if self.default_project not in self.projects:
self.default_project = "main"
class ConfigManager:
"""Manages Basic Memory configuration."""
def __init__(self) -> None:
"""Initialize the configuration manager."""
self.config_dir = Path.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:
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 get_project_path(self, project_name: Optional[str] = None) -> Path:
"""Get the path for a specific project or the default project."""
name = project_name or self.config.default_project
# Check if specified in environment variable
if not project_name and "BASIC_MEMORY_PROJECT" in os.environ:
name = os.environ["BASIC_MEMORY_PROJECT"]
if name not in self.config.projects:
raise ValueError(f"Project '{name}' not found in configuration")
return Path(self.config.projects[name])
def add_project(self, name: str, path: str) -> None:
"""Add a new project to the configuration."""
if name in self.config.projects:
raise ValueError(f"Project '{name}' already exists")
# Ensure the path exists
project_path = Path(path)
project_path.mkdir(parents=True, exist_ok=True)
self.config.projects[name] = str(project_path)
self.save_config(self.config)
def remove_project(self, name: str) -> None:
"""Remove a project from the configuration."""
if name not in self.config.projects:
raise ValueError(f"Project '{name}' not found")
if name == self.config.default_project:
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."""
if name not in self.config.projects: # pragma: no cover
raise ValueError(f"Project '{name}' not found")
self.config.default_project = name
self.save_config(self.config)
def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
"""Get a project configuration for the specified project."""
config_manager = ConfigManager()
# Get project name from environment variable or use provided name or default
actual_project_name = os.environ.get(
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
)
try:
project_path = config_manager.get_project_path(actual_project_name)
return ProjectConfig(home=project_path, project=actual_project_name)
except ValueError: # pragma: no cover
logger.warning(f"Project '{actual_project_name}' not found, using default")
project_path = config_manager.get_project_path(config_manager.default_project)
return ProjectConfig(home=project_path, project=config_manager.default_project)
# Create config manager
config_manager = ConfigManager()
# Load project config for current context
config = get_project_config()
# 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)
setup_logging(
env=config.env,
home_dir=user_home, # Use user home for logs
log_level=config.log_level,
log_file=f"{DATA_DIR_NAME}/basic-memory.log",
console=False,
)
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
-176
View File
@@ -1,176 +0,0 @@
import asyncio
from contextlib import asynccontextmanager
from enum import Enum, auto
from pathlib import Path
from typing import AsyncGenerator, Optional
from basic_memory.config import ProjectConfig
from alembic import command
from alembic.config import Config
from loguru import logger
from sqlalchemy import text
from sqlalchemy.ext.asyncio import (
create_async_engine,
async_sessionmaker,
AsyncSession,
AsyncEngine,
async_scoped_session,
)
from basic_memory.repository.search_repository import SearchRepository
# Module level state
_engine: Optional[AsyncEngine] = None
_session_maker: Optional[async_sessionmaker[AsyncSession]] = None
class DatabaseType(Enum):
"""Types of supported databases."""
MEMORY = auto()
FILESYSTEM = auto()
@classmethod
def get_db_url(cls, db_path: Path, db_type: "DatabaseType") -> str:
"""Get SQLAlchemy URL for database path."""
if db_type == cls.MEMORY:
logger.info("Using in-memory SQLite database")
return "sqlite+aiosqlite://"
return f"sqlite+aiosqlite:///{db_path}" # pragma: no cover
def get_scoped_session_factory(
session_maker: async_sessionmaker[AsyncSession],
) -> async_scoped_session:
"""Create a scoped session factory scoped to current task."""
return async_scoped_session(session_maker, scopefunc=asyncio.current_task)
@asynccontextmanager
async def scoped_session(
session_maker: async_sessionmaker[AsyncSession],
) -> AsyncGenerator[AsyncSession, None]:
"""
Get a scoped session with proper lifecycle management.
Args:
session_maker: Session maker to create scoped sessions from
"""
factory = get_scoped_session_factory(session_maker)
session = factory()
try:
await session.execute(text("PRAGMA foreign_keys=ON"))
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
await factory.remove()
async def get_or_create_db(
db_path: Path,
db_type: DatabaseType = DatabaseType.FILESYSTEM,
) -> 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)
# 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")
return _engine, _session_maker
async def shutdown_db() -> None: # pragma: no cover
"""Clean up database connections."""
global _engine, _session_maker
if _engine:
await _engine.dispose()
_engine = None
_session_maker = None
@asynccontextmanager
async def engine_session_factory(
db_path: Path,
db_type: DatabaseType = DatabaseType.MEMORY,
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
"""Create engine and session factory.
Note: This is primarily used for testing where we want a fresh database
for each test. For production use, use get_or_create_db() instead.
"""
global _engine, _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})
try:
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
# 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
async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
"""Run any pending alembic migrations."""
logger.info("Running database migrations...")
try:
# Get the absolute path to the alembic directory relative to this file
alembic_dir = Path(__file__).parent / "alembic"
config = Config()
# Set required Alembic config options programmatically
config.set_main_option("script_location", str(alembic_dir))
config.set_main_option(
"file_template",
"%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s",
)
config.set_main_option("timezone", "UTC")
config.set_main_option("revision_environment", "false")
config.set_main_option(
"sqlalchemy.url", DatabaseType.get_db_url(app_config.database_path, database_type)
)
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()
except Exception as e: # pragma: no cover
logger.error(f"Error running migrations: {e}")
raise

Some files were not shown because too many files have changed in this diff Show More