mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ce7a29f35 | |||
| 3f4d9e4d87 | |||
| 00c8633cfc | |||
| 617e60bda4 | |||
| 6c19c9edf5 | |||
| 9bff1f732e | |||
| 248214cb11 | |||
| 40ea28b0bf | |||
| 43cbb7b38c | |||
| 7930ddb291 | |||
| 1844e58210 | |||
| 7fbb5ebfc5 | |||
| 0e5a465b10 | |||
| 9d581cee13 | |||
| f58852954b | |||
| edd426b99f | |||
| 6b4a421315 | |||
| 4632405446 | |||
| 3a6d5ff58d | |||
| 03de85ec32 | |||
| 084c91ecce | |||
| 3d45227b72 | |||
| 069c0a21c6 | |||
| b27827671d | |||
| 0743ade5fc | |||
| f1c95709cb | |||
| 3c68b7d5dd | |||
| 3d0077990f | |||
| 731b502d36 | |||
| 2a881b1425 | |||
| 681af5d450 | |||
| b35037eb86 | |||
| b667bca5a2 | |||
| e716946b44 | |||
| 46c4fd2164 | |||
| 9c791259a0 | |||
| cc2cae72c1 | |||
| 0a1fdd8f15 | |||
| 78f234b180 | |||
| cbe72be10a | |||
| 0ade6b0603 | |||
| 390ff9d31c | |||
| 3ed74b4a4d | |||
| dfaf0fea9c | |||
| b26afa927f | |||
| 3806943f8c | |||
| 3bffb2e190 | |||
| 9fe34dcddf | |||
| eb5e4ec6bd | |||
| 6b110b28dd | |||
| 88d193e0aa | |||
| fa9175f049 | |||
| 7c787a413c | |||
| 0fa1adf3b5 | |||
| cbe488fbd2 | |||
| 99ff8f60c2 | |||
| 06ee2852a0 | |||
| 939bab1a24 | |||
| 67736487fb | |||
| 2ac620f98b | |||
| 4cbda7a9d4 | |||
| 3070ade69a | |||
| 9af913da4f | |||
| 7b7bec9557 | |||
| fea2f40d1b | |||
| eb1e7b6088 | |||
| 9203730ba7 | |||
| 1a473c02e9 | |||
| b6f5d8a545 | |||
| addeeb1e7f | |||
| 85f9099b0a | |||
| 82b6f3d60e | |||
| da97353cfc | |||
| 2e9d673e54 | |||
| c4732a47b3 | |||
| 2d5176e800 | |||
| 9bb8a020c3 | |||
| 31cca6f913 |
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Claude Code Integration
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
pull_request_review_comment:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
claude-integration:
|
||||
uses: basicmachines-co/claude-code-github-action/.github/workflows/claude-full.yml@v0.11.0
|
||||
with:
|
||||
issue-label: 'claude-fix' # Optional: customize the trigger label
|
||||
secrets:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLAUDE_TOKEN }}
|
||||
@@ -5,6 +5,12 @@ on:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
# pull_request_target runs on the BASE of the PR, not the merge result.
|
||||
# It has write permissions and access to secrets.
|
||||
# It's useful for PRs from forks or automated PRs but requires careful use for security reasons.
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
|
||||
pull_request_target:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -43,3 +43,12 @@ ENV/
|
||||
# macOS
|
||||
.DS_Store
|
||||
/.coverage.*
|
||||
|
||||
# obsidian docs:
|
||||
/docs/.obsidian/
|
||||
/examples/.obsidian/
|
||||
/examples/.basic-memory/
|
||||
|
||||
|
||||
# claude action
|
||||
claude-output
|
||||
+278
@@ -1,6 +1,284 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v0.12.0 (2025-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [bug] `#` character accumulation in markdown frontmatter tags prop
|
||||
([#79](https://github.com/basicmachines-co/basic-memory/pull/79),
|
||||
[`6c19c9e`](https://github.com/basicmachines-co/basic-memory/commit/6c19c9edf5131054ba201a109b37f15c83ef150c))
|
||||
|
||||
- [bug] Cursor has errors calling search tool
|
||||
([#78](https://github.com/basicmachines-co/basic-memory/pull/78),
|
||||
[`9d581ce`](https://github.com/basicmachines-co/basic-memory/commit/9d581cee133f9dde4a0a85118868227390c84161))
|
||||
|
||||
- [bug] Some notes never exit "modified" status
|
||||
([#77](https://github.com/basicmachines-co/basic-memory/pull/77),
|
||||
[`7930ddb`](https://github.com/basicmachines-co/basic-memory/commit/7930ddb2919057be30ceac8c4c19da6aaa1d3e92))
|
||||
|
||||
- [bug] write_note Tool Fails to Update Existing Files in Some Situations.
|
||||
([#80](https://github.com/basicmachines-co/basic-memory/pull/80),
|
||||
[`9bff1f7`](https://github.com/basicmachines-co/basic-memory/commit/9bff1f732e71bc60f88b5c2ce3db5a2aa60b8e28))
|
||||
|
||||
- Set default mcp log level to ERROR
|
||||
([#81](https://github.com/basicmachines-co/basic-memory/pull/81),
|
||||
[`248214c`](https://github.com/basicmachines-co/basic-memory/commit/248214cb114a269ca60ff6398e382f9e2495ad8e))
|
||||
|
||||
- Write_note preserves frontmatter fields in content
|
||||
([#84](https://github.com/basicmachines-co/basic-memory/pull/84),
|
||||
[`3f4d9e4`](https://github.com/basicmachines-co/basic-memory/commit/3f4d9e4d872ebc0ed719c61b24d803c14a9db5e6))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add VS Code instructions to README
|
||||
([#76](https://github.com/basicmachines-co/basic-memory/pull/76),
|
||||
[`43cbb7b`](https://github.com/basicmachines-co/basic-memory/commit/43cbb7b38cc0482ac0a41b6759320e3588186e43))
|
||||
|
||||
- Updated basicmachines.co links to be https
|
||||
([#69](https://github.com/basicmachines-co/basic-memory/pull/69),
|
||||
[`40ea28b`](https://github.com/basicmachines-co/basic-memory/commit/40ea28b0bfc60012924a69ecb76511daa4c7d133))
|
||||
|
||||
### Features
|
||||
|
||||
- Add watch to mcp process ([#83](https://github.com/basicmachines-co/basic-memory/pull/83),
|
||||
[`00c8633`](https://github.com/basicmachines-co/basic-memory/commit/00c8633cfcee75ff640ff8fe81dafeb956281a94))
|
||||
|
||||
- Permalink enhancements ([#82](https://github.com/basicmachines-co/basic-memory/pull/82),
|
||||
[`617e60b`](https://github.com/basicmachines-co/basic-memory/commit/617e60bda4a590678a5f551f10a73e7b47e3b13e))
|
||||
|
||||
- Avoiding "useless permalink values" for files without metadata - Enable permalinks to be updated
|
||||
on move via config setting
|
||||
|
||||
|
||||
## v0.11.0 (2025-03-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Just delete db for reset db instead of using migrations.
|
||||
([#65](https://github.com/basicmachines-co/basic-memory/pull/65),
|
||||
[`0743ade`](https://github.com/basicmachines-co/basic-memory/commit/0743ade5fc07440f95ecfd816ba7e4cfd74bca12))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Make logs for each process - mcp, sync, cli
|
||||
([#64](https://github.com/basicmachines-co/basic-memory/pull/64),
|
||||
[`f1c9570`](https://github.com/basicmachines-co/basic-memory/commit/f1c95709cbffb1b88292547b0b8f29fcca22d186))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update broken "Multiple Projects" link in README.md
|
||||
([#55](https://github.com/basicmachines-co/basic-memory/pull/55),
|
||||
[`3c68b7d`](https://github.com/basicmachines-co/basic-memory/commit/3c68b7d5dd689322205c67637dca7d188111ee6b))
|
||||
|
||||
### Features
|
||||
|
||||
- Add bm command alias for basic-memory
|
||||
([#67](https://github.com/basicmachines-co/basic-memory/pull/67),
|
||||
[`069c0a2`](https://github.com/basicmachines-co/basic-memory/commit/069c0a21c630784e1bf47d2b7de5d6d1f6fadd7a))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Rename search tool to search_notes
|
||||
([#66](https://github.com/basicmachines-co/basic-memory/pull/66),
|
||||
[`b278276`](https://github.com/basicmachines-co/basic-memory/commit/b27827671dc010be3e261b8b221aca6b7f836661))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.10.1 (2025-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make set_default_project also activate project for current session to fix #37
|
||||
([`cbe72be`](https://github.com/basicmachines-co/basic-memory/commit/cbe72be10a646c0b03931bb39aff9285feae47f9))
|
||||
|
||||
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
|
||||
for future invocations (persistent change) 2. Activate the project for the current session
|
||||
(immediate change)
|
||||
|
||||
Added tests to verify this behavior, which resolves issue #37 where the project name and path
|
||||
weren't changing properly when the default project was changed.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Make set_default_project also activate project for current session to fix #37
|
||||
([`46c4fd2`](https://github.com/basicmachines-co/basic-memory/commit/46c4fd21645b109af59eb2a0201c7bd849b34a49))
|
||||
|
||||
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
|
||||
for future invocations (persistent change) 2. Activate the project for the current session
|
||||
(immediate change)
|
||||
|
||||
Added tests to verify this behavior, which resolves issue #37 where the project name and path
|
||||
weren't changing properly when the default project was changed.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Move ai_assistant_guide.md into package resources to fix #39
|
||||
([`390ff9d`](https://github.com/basicmachines-co/basic-memory/commit/390ff9d31ccee85bef732e8140b5eeecd7ee176f))
|
||||
|
||||
This change relocates the AI assistant guide from the static directory into the package resources
|
||||
directory, ensuring it gets properly included in the distribution package and is accessible when
|
||||
installed via pip/uv.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Move ai_assistant_guide.md into package resources to fix #39
|
||||
([`cc2cae7`](https://github.com/basicmachines-co/basic-memory/commit/cc2cae72c14b380f78ffeb67c2261e4dbee45faf))
|
||||
|
||||
This change relocates the AI assistant guide from the static directory into the package resources
|
||||
directory, ensuring it gets properly included in the distribution package and is accessible when
|
||||
installed via pip/uv.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Preserve custom frontmatter fields when updating notes
|
||||
([`78f234b`](https://github.com/basicmachines-co/basic-memory/commit/78f234b1806b578a0a833e8ee4184015b7369a97))
|
||||
|
||||
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
|
||||
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
|
||||
notes are updated through the write_note MCP tool.
|
||||
|
||||
Added test case that verifies this behavior by creating a note with custom frontmatter and then
|
||||
updating it.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
- Preserve custom frontmatter fields when updating notes
|
||||
([`e716946`](https://github.com/basicmachines-co/basic-memory/commit/e716946b4408d017eca4be720956d5a210b4e6b1))
|
||||
|
||||
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
|
||||
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
|
||||
notes are updated through the write_note MCP tool.
|
||||
|
||||
Added test case that verifies this behavior by creating a note with custom frontmatter and then
|
||||
updating it.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Chores
|
||||
|
||||
- Remove duplicate code in entity_service.py from bad merge
|
||||
([`681af5d`](https://github.com/basicmachines-co/basic-memory/commit/681af5d4505dadc40b4086630f739d76bac9201d))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add help docs to mcp cli tools
|
||||
([`731b502`](https://github.com/basicmachines-co/basic-memory/commit/731b502d36cec253d114403d73b48fab3c47786e))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Add mcp badge, update cli reference, llms-install.md
|
||||
([`b26afa9`](https://github.com/basicmachines-co/basic-memory/commit/b26afa927f98021246cd8b64858e57333595ea90))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Update CLAUDE.md ([#33](https://github.com/basicmachines-co/basic-memory/pull/33),
|
||||
[`dfaf0fe`](https://github.com/basicmachines-co/basic-memory/commit/dfaf0fea9cf5b97d169d51a6276ec70162c21a7e))
|
||||
|
||||
fix spelling in CLAUDE.md: enviroment -> environment Signed-off-by: Ikko Eltociear Ashimine
|
||||
<eltociear@gmail.com>
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Move project stats into projct subcommand
|
||||
([`2a881b1`](https://github.com/basicmachines-co/basic-memory/commit/2a881b1425c73947f037fbe7ac5539c015b62526))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
|
||||
## v0.10.0 (2025-03-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`da97353`](https://github.com/basicmachines-co/basic-memory/commit/da97353cfc3acc1ceb0eca22ac6af326f77dc199))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`c4732a4`](https://github.com/basicmachines-co/basic-memory/commit/c4732a47b37dd2e404139fb283b65556c81ce7c9))
|
||||
|
||||
- Ai_resource_guide.md path
|
||||
([`2e9d673`](https://github.com/basicmachines-co/basic-memory/commit/2e9d673e54ad6a63a971db64f01fc2f4e59c2e69))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Don't sync *.tmp files on watch ([#31](https://github.com/basicmachines-co/basic-memory/pull/31),
|
||||
[`6b110b2`](https://github.com/basicmachines-co/basic-memory/commit/6b110b28dd8ba705ebfc0bcb41faf2cb993da2c3))
|
||||
|
||||
Fixes #30
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Drop search_index table on db reindex
|
||||
([`31cca6f`](https://github.com/basicmachines-co/basic-memory/commit/31cca6f913849a0ab8fc944803533e3072e9ef88))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Improve utf-8 support for file reading/writing
|
||||
([#32](https://github.com/basicmachines-co/basic-memory/pull/32),
|
||||
[`eb5e4ec`](https://github.com/basicmachines-co/basic-memory/commit/eb5e4ec6bd4d2fe757087be030d867f4ca1d38ba))
|
||||
|
||||
fixes #29
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Chores
|
||||
|
||||
- Remove logfire
|
||||
([`9bb8a02`](https://github.com/basicmachines-co/basic-memory/commit/9bb8a020c3425a02cb3a88f6f02adcd281bccee2))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add glama badge. Fix typos in README.md
|
||||
([#28](https://github.com/basicmachines-co/basic-memory/pull/28),
|
||||
[`9af913d`](https://github.com/basicmachines-co/basic-memory/commit/9af913da4fba7bb4908caa3f15f2db2aa03777ec))
|
||||
|
||||
Signed-off-by: phernandez <paul@basicmachines.co>
|
||||
|
||||
- Update CLAUDE.md with GitHub integration capabilities
|
||||
([#25](https://github.com/basicmachines-co/basic-memory/pull/25),
|
||||
[`fea2f40`](https://github.com/basicmachines-co/basic-memory/commit/fea2f40d1b54d0c533e6d7ee7ce1aa7b83ad9a47))
|
||||
|
||||
This PR updates the CLAUDE.md file to document the GitHub integration capabilities that enable
|
||||
Claude to participate directly in the development workflow.
|
||||
|
||||
### Features
|
||||
|
||||
- Add Smithery integration for easier installation
|
||||
([#24](https://github.com/basicmachines-co/basic-memory/pull/24),
|
||||
[`eb1e7b6`](https://github.com/basicmachines-co/basic-memory/commit/eb1e7b6088b0b3dead9c104ee44174b2baebf417))
|
||||
|
||||
This PR adds support for deploying Basic Memory on the Smithery platform.
|
||||
|
||||
Signed-off-by: bm-claudeai <claude@basicmachines.co>
|
||||
|
||||
|
||||
## v0.9.0 (2025-03-07)
|
||||
|
||||
### Chores
|
||||
|
||||
@@ -37,6 +37,7 @@ See the [README.md](README.md) file for a project overview.
|
||||
- API uses FastAPI for endpoints
|
||||
- Follow the repository pattern for data access
|
||||
- Tools communicate to api routers via the httpx ASGI client (in process)
|
||||
- avoid using "private" functions in modules or classes (prepended with _)
|
||||
|
||||
### Codebase Architecture
|
||||
|
||||
@@ -63,7 +64,7 @@ See the [README.md](README.md) file for a project overview.
|
||||
- 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
|
||||
- Each test runs in a standalone environment with in memory SQLite and tmp_file directory
|
||||
|
||||
## BASIC MEMORY PRODUCT USAGE
|
||||
|
||||
@@ -106,7 +107,7 @@ See the [README.md](README.md) file for a project overview.
|
||||
1d", "1 week")
|
||||
|
||||
**Search & Discovery:**
|
||||
- `search(query, page, page_size)` - Full-text search across all content with filtering options
|
||||
- `search_notes(query, page, page_size)` - Full-text search across all content with filtering options
|
||||
|
||||
**Visualization:**
|
||||
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
|
||||
@@ -114,7 +115,7 @@ See the [README.md](README.md) file for a project overview.
|
||||
- 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
|
||||
- `search_notes(query, after_date)` - Search with detailed, formatted results for better context understanding
|
||||
- `recent_activity(timeframe)` - View recently changed items with formatted output
|
||||
- `json_canvas_spec()` - Full JSON Canvas specification for Obsidian visualization
|
||||
|
||||
@@ -130,4 +131,45 @@ of using AI just for code generation, we've developed a true collaborative workf
|
||||
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.
|
||||
could achieve independently.
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
Basic Memory uses Claude directly into the development workflow through GitHub:
|
||||
|
||||
### GitHub MCP Tools
|
||||
|
||||
Using the GitHub Model Context Protocol server, Claude can:
|
||||
|
||||
- **Repository Management**:
|
||||
- View repository files and structure
|
||||
- Read file contents
|
||||
- Create new branches
|
||||
- Create and update files
|
||||
|
||||
- **Issue Management**:
|
||||
- Create new issues
|
||||
- Comment on existing issues
|
||||
- Close and update issues
|
||||
- Search across issues
|
||||
|
||||
- **Pull Request Workflow**:
|
||||
- Create pull requests
|
||||
- Review code changes
|
||||
- Add comments to PRs
|
||||
|
||||
This integration enables Claude to participate as a full team member in the development process, not just as a code
|
||||
generation tool. Claude's GitHub account ([bm-claudeai](https://github.com/bm-claudeai)) is a member of the Basic
|
||||
Machines organization with direct contributor access to the codebase.
|
||||
|
||||
### Collaborative Development Process
|
||||
|
||||
With GitHub integration, the development workflow includes:
|
||||
|
||||
1. **Direct code review** - Claude can analyze PRs and provide detailed feedback
|
||||
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
|
||||
3. **Branch management** - Claude can create feature branches for implementations
|
||||
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
|
||||
|
||||
With this integration, the AI assistant is a full-fledged team member rather than just a tool for generating code
|
||||
snippets.
|
||||
+38
-34
@@ -1,6 +1,7 @@
|
||||
# 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.
|
||||
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
|
||||
|
||||
@@ -65,61 +66,64 @@ Thank you for considering contributing to Basic Memory! This document outlines t
|
||||
|
||||
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
|
||||
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
|
||||
- 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
|
||||
- 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.
|
||||
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)
|
||||
- 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:
|
||||
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.
|
||||
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
|
||||
|
||||
You can sign your commits in one of two ways:
|
||||
Sign your commit:
|
||||
|
||||
1. **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.
|
||||
**Using the `-s` or `--signoff` flag**:
|
||||
|
||||
2. **Configuring Git to automatically sign off**:
|
||||
```bash
|
||||
git config --global alias.cs 'commit -s'
|
||||
```
|
||||
Then use `git cs -m "Your commit message"` to commit with sign-off.
|
||||
```bash
|
||||
git commit -s -m "Your commit message"
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -135,10 +139,10 @@ The sign-off certifies that you have the right to submit your contribution under
|
||||
|
||||
- **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
|
||||
- **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 pytest fixtures for setup and teardown
|
||||
- **Fixtures**: Use async pytest fixtures for setup and teardown
|
||||
|
||||
## Creating Issues
|
||||
|
||||
@@ -156,4 +160,4 @@ All contributors must follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Thank You!
|
||||
|
||||
Your contributions help make Basic Memory better for everyone. We appreciate your time and effort!
|
||||
Your contributions help make Basic Memory better. We appreciate your time and effort!
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# 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"]
|
||||
@@ -1,38 +1,34 @@
|
||||
# Basic Memory
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://badge.fury.io/py/basic-memory)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://github.com/basicmachines-co/basic-memory/actions)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||

|
||||
[](https://smithery.ai/server/@basicmachines-co/basic-memory)
|
||||
|
||||
```
|
||||
██████╗ █████╗ ███████╗██╗ ██████╗ ███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗ ██╗ ██╗
|
||||
██╔══██╗██╔══██╗██╔════╝██║██╔════╝ ████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔══██╗╚██╗ ██╔╝
|
||||
██████╔╝███████║███████╗██║██║ ██╔████╔██║█████╗ ██╔████╔██║██║ ██║██████╔╝ ╚████╔╝
|
||||
██╔══██╗██╔══██║╚════██║██║██║ ██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║██╔══██╗ ╚██╔╝
|
||||
██████╔╝██║ ██║███████║██║╚██████╗ ██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝██║ ██║ ██║
|
||||
╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
||||
|
||||
```
|
||||
# Basic Memory
|
||||
|
||||
Basic Memory lets you build persistent knowledge through natural conversations with Large Language Models (LLMs) like
|
||||
Claude, while keeping everything in simple Markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
||||
enable any compatible LLM to read and write to your local knowledge base.
|
||||
|
||||
LLMs can build context from local knowledge bases.
|
||||
- Website: https://basicmachines.co
|
||||
- Documentation: https://memory.basicmachines.co
|
||||
|
||||

|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
Basic Memory provides persistent contextual awareness across sessions through a structured knowledge graph.
|
||||
The system enables LLMs to access and reference prior conversations, track semantic relationships between concepts, and
|
||||
incorporate human edits made directly to knowledge files.
|
||||
- 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 install basic-memory
|
||||
uv tool install basic-memory
|
||||
|
||||
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
|
||||
# Add this to your config:
|
||||
@@ -56,33 +52,23 @@ uv install basic-memory
|
||||
|
||||
You can view shared context via files in `~/basic-memory` (default directory location).
|
||||
|
||||
You can also install the cli tools to sync files or manage projects.
|
||||
### Alternative Installation via Smithery
|
||||
|
||||
```bash
|
||||
uv tool install basic-memory
|
||||
You can use [Smithery](https://smithery.ai/server/@basicmachines-co/basic-memory) to automatically configure Basic
|
||||
Memory for Claude Desktop:
|
||||
|
||||
# create a new project in a different directory
|
||||
basic-memory project add coffee ./examples/coffee
|
||||
|
||||
# you can set the project to the default
|
||||
basic-memory project default coffee
|
||||
```bash
|
||||
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
|
||||
```
|
||||
|
||||
View available projects
|
||||
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.
|
||||
|
||||
```bash
|
||||
basic-memory project list
|
||||
Basic Memory Projects
|
||||
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
|
||||
┃ Name ┃ Path ┃ Default ┃ Active ┃
|
||||
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
|
||||
│ main │ ~/basic-memory │ ✓ │ ✓ │
|
||||
│ coffee │ ~/dev/basicmachines/basic-memory/examples/coffee │ │ │
|
||||
└────────┴──────────────────────────────────────────────────┴─────────┴────────┘
|
||||
```
|
||||
### Glama.ai
|
||||
|
||||
Basic Memory will write notes in Markdown format. Open you project directory in your text editor to view project files
|
||||
while you have conversations with an LLM.
|
||||
<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?
|
||||
|
||||
@@ -163,36 +149,13 @@ tags:
|
||||
- affects [[Flavor Extraction]]
|
||||
```
|
||||
|
||||
The note embeds semantic content and links to other topics via simple Markdown
|
||||
formatting.
|
||||
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 `~/$HOME/basic-memory` directory:
|
||||
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
permalink: coffee-brewing-methods
|
||||
type: note
|
||||
---
|
||||
- Realtime sync can be enabled via running `basic-memory sync --watch`
|
||||
|
||||
# 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
|
||||
- [preference] Medium-light roasts work best for pour over # Added by you
|
||||
|
||||
## Relations
|
||||
|
||||
- relates_to [[Coffee Bean Origins]]
|
||||
- requires [[Proper Grinding Technique]]
|
||||
- affects [[Flavor Extraction]]
|
||||
- pairs_with [[Breakfast Pastries]] # Added by you
|
||||
```
|
||||
|
||||
4. In a new chat with the LLM, you can reference this knowledge:
|
||||
4. In a chat with the LLM, you can reference a topic:
|
||||
|
||||
```
|
||||
Look at `coffee-brewing-methods` for context about pour over coffee
|
||||
@@ -212,15 +175,14 @@ Following relation 'requires [[Proper Grinding Technique]]':
|
||||
- Impact of consistent particle size on extraction
|
||||
```
|
||||
|
||||
Each related document can lead to more context, building a rich semantic understanding of your knowledge base. All of
|
||||
this context comes from standard Markdown files that both humans and LLMs can read and write.
|
||||
Each related document can lead to more context, building a rich semantic understanding of your knowledge base.
|
||||
|
||||
Every time the LLM writes notes,they are saved in local Markdown files that you can:
|
||||
This creates a two-way flow where:
|
||||
|
||||
- Edit in any text editor
|
||||
- Version via git
|
||||
- Back up normally
|
||||
- Share when you want to
|
||||
- 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
|
||||
|
||||
@@ -256,7 +218,7 @@ permalink: <a uri slug>
|
||||
|
||||
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
|
||||
"#" charactor, and an optional `context`.
|
||||
"#" character, and an optional `context`.
|
||||
|
||||
Observation Markdown format:
|
||||
|
||||
@@ -300,72 +262,42 @@ Examples of relations:
|
||||
- documented_in [[Coffee Journal]]
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
## Using with VS Code
|
||||
For one-click installation, click one of the install buttons below...
|
||||
|
||||
Here's a complete example of a note with frontmatter, observations, and relations:
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D&quality=insiders)
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Pour Over Coffee Method
|
||||
type: note
|
||||
permalink: pour-over-coffee-method
|
||||
tags:
|
||||
- brewing
|
||||
- coffee
|
||||
- techniques
|
||||
---
|
||||
You can use Basic Memory with VS Code to easily retrieve and store information while coding. Click the installation buttons above for one-click setup, or follow the manual installation instructions below.
|
||||
|
||||
# Pour Over Coffee Method
|
||||
### Manual Installation
|
||||
|
||||
This note documents the pour over brewing method and my experiences with it.
|
||||
Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
## Overview
|
||||
|
||||
The pour over method involves pouring hot water through coffee grounds in a filter. The water drains through the coffee
|
||||
and filter into a carafe or cup.
|
||||
|
||||
## Observations
|
||||
|
||||
- [equipment] Hario V60 dripper produces clean, bright cup #gear
|
||||
- [technique] Pour in concentric circles to ensure even extraction
|
||||
- [ratio] 1:16 coffee-to-water ratio works best for balanced flavor
|
||||
- [timing] Total brew time should be 2:30-3:00 minutes for medium roast
|
||||
- [temperature] Water at 205°F (96°C) extracts optimal flavor compounds
|
||||
- [grind] Medium-fine grind similar to table salt texture
|
||||
- [tip] 30-45 second bloom with double the coffee weight in water
|
||||
- [result] Produces a cleaner cup with more distinct flavor notes than immersion methods
|
||||
|
||||
## Relations
|
||||
|
||||
- complements [[Light Roast Beans]]
|
||||
- requires [[Gooseneck Kettle]]
|
||||
- contrasts_with [[French Press Method]]
|
||||
- pairs_with [[Breakfast Pastries]]
|
||||
- documented_in [[Brewing Journal]]
|
||||
- inspired_by [[Japanese Brewing Techniques]]
|
||||
- affects [[Flavor Extraction]]
|
||||
- part_of [[Morning Ritual]]
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Basic Memory will parse the Markdown and derive the semantic relationships in the content. When you run
|
||||
`basic-memory sync`:
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
1. New and changed files are detected
|
||||
2. Markdown patterns become semantic knowledge:
|
||||
|
||||
- `[tech]` becomes a categorized observation
|
||||
- `[[WikiLink]]` creates a relation in the knowledge graph
|
||||
- Tags and metadata are indexed for search
|
||||
|
||||
3. A SQLite database maintains these relationships for fast querying
|
||||
4. MCP-compatible LLMs can access this knowledge via memory:// URLs
|
||||
|
||||
This creates a two-way flow where:
|
||||
|
||||
- Humans write and edit Markdown files
|
||||
- LLMs read and write through the MCP protocol
|
||||
- Sync keeps everything consistent
|
||||
- All knowledge stays in local files.
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": ["basic-memory", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using with Claude Desktop
|
||||
|
||||
@@ -390,7 +322,8 @@ for OS X):
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a specific project (see [Multiple Projects](#multiple-projects) below), update your Claude Desktop
|
||||
If you want to use a specific project (see [Multiple Projects](docs/User%20Guide.md#multiple-projects)), update your
|
||||
Claude Desktop
|
||||
config:
|
||||
|
||||
```json
|
||||
@@ -425,7 +358,7 @@ basic-memory sync --watch
|
||||
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
|
||||
search_notes(query, page, page_size) - Search across your knowledge base
|
||||
recent_activity(type, depth, timeframe) - Find recently updated information
|
||||
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
||||
```
|
||||
@@ -440,263 +373,30 @@ canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
||||
"What have I been working on in the past week?"
|
||||
```
|
||||
|
||||
## Multiple Projects
|
||||
## Futher info
|
||||
|
||||
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).
|
||||
See the [Documentation](https://memory.basicmachines.co/) for more info, including:
|
||||
|
||||
### 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
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Basic Memory is built on some key ideas:
|
||||
|
||||
- Your knowledge should stay in files you control
|
||||
- Both humans and AI should use natural formats
|
||||
- Simple text patterns can capture rich meaning
|
||||
- Local-first doesn't mean feature-poor
|
||||
- Knowledge should persist across conversations
|
||||
- AI assistants should build on past context
|
||||
- File formats should be human-readable and editable
|
||||
- Semantic structure should emerge from natural patterns
|
||||
- Knowledge graphs should be both AI and human navigable
|
||||
- Systems should augment human memory, not replace it
|
||||
|
||||
## Importing Existing Data
|
||||
|
||||
Basic Memory provides CLI commands to import data from various sources, converting them into the structured Markdown
|
||||
format:
|
||||
|
||||
### Claude.ai
|
||||
|
||||
First, request an export of your data from your Claude account. The data will be emailed to you in several files,
|
||||
including
|
||||
`conversations.json` and `projects.json`.
|
||||
|
||||
Import Claude.ai conversation data
|
||||
|
||||
```bash
|
||||
basic-memory import claude conversations
|
||||
```
|
||||
|
||||
The conversations will be turned into Markdown files and placed in the "conversations" folder by default (this can be
|
||||
changed with the --folder arg).
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
Importing chats from conversations.json...writing to .../basic-memory
|
||||
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 307 conversations │
|
||||
│ Containing 7769 messages │
|
||||
╰────────────────────────────╯
|
||||
```
|
||||
|
||||
Next, you can run the `sync` command to import the data into basic-memory
|
||||
|
||||
```bash
|
||||
basic-memory sync
|
||||
```
|
||||
|
||||
You can also import project data from Claude.ai
|
||||
|
||||
```bash
|
||||
➜ basic-memory import claude projects
|
||||
Importing projects from projects.json...writing to .../basic-memory/projects
|
||||
Reading project data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 101 project documents │
|
||||
│ Imported 32 prompt templates │
|
||||
╰────────────────────────────────╯
|
||||
|
||||
Run 'basic-memory sync' to index the new files.
|
||||
```
|
||||
|
||||
### OpenAI ChatGPT
|
||||
|
||||
```bash
|
||||
➜ basic-memory import chatgpt
|
||||
Importing chats from conversations.json...writing to .../basic-memory/conversations
|
||||
|
||||
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭────────────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Imported 198 conversations │
|
||||
│ Containing 11777 messages │
|
||||
╰────────────────────────────╯
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Knowledge Graph Memory Server
|
||||
|
||||
From the MCP Server: https://github.com/modelcontextprotocol/servers/tree/main/src/memory
|
||||
|
||||
```bash
|
||||
➜ basic-memory import memory-json
|
||||
Importing from memory.json...writing to .../basic-memory
|
||||
Reading memory.json... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
Creating entities... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
|
||||
╭──────────────────────╮
|
||||
│ Import complete! │
|
||||
│ │
|
||||
│ Created 126 entities │
|
||||
│ Added 252 relations │
|
||||
╰──────────────────────╯
|
||||
```
|
||||
|
||||
## Working with Your Knowledge Base
|
||||
|
||||
Once you've built up a knowledge base, you can interact with it in several ways:
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Basic Memory provides a powerful CLI for managing your knowledge:
|
||||
|
||||
```bash
|
||||
# See all available commands
|
||||
basic-memory --help
|
||||
|
||||
# Check the status of your knowledge sync
|
||||
basic-memory status
|
||||
|
||||
# Access specific tool functionality directly
|
||||
basic-memory tools
|
||||
|
||||
# Start a continuous sync process
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
### Obsidian Integration
|
||||
|
||||
Basic Memory works seamlessly with [Obsidian](https://obsidian.md/), a popular knowledge management app:
|
||||
|
||||
1. Point Obsidian to your Basic Memory directory
|
||||
2. Use standard Obsidian features like backlinks and graph view
|
||||
3. See your knowledge graph visually
|
||||
4. Use the canvas visualization generated by Basic Memory
|
||||
|
||||
### File Organization
|
||||
|
||||
Basic Memory is flexible about how you organize your files:
|
||||
|
||||
- Group by topic in folders
|
||||
- Use a flat structure with descriptive filenames
|
||||
- Add custom metadata in frontmatter
|
||||
- Tag files for better searchability
|
||||
|
||||
The system will build the semantic knowledge graph regardless of your file organization preference.
|
||||
|
||||
## 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 tools write-note --title "Test Note" --folder "notes"
|
||||
|
||||
# Pipe output of a file into a new note
|
||||
cat README.md | basic-memory tools write-note --title "Project README" --folder "documentation"
|
||||
|
||||
# Process text through other tools before saving as a note
|
||||
cat data.txt | grep "important" | basic-memory tools 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 tools 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 tools write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
|
||||
```
|
||||
- [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
|
||||
|
||||
Built with ♥️ by Basic Machines
|
||||
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
|
||||
and submitting PRs.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#basicmachines-co/basic-memory&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
Built with ♥️ by Basic Machines
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
If you find a vulnerability, please contact hello@basicmachines.co
|
||||
@@ -0,0 +1,13 @@
|
||||
Starting issue-fix mode at Sat Apr 5 18:02:54 UTC 2025
|
||||
Fetching issue #75 details
|
||||
Using repository: basicmachines-co/basic-memory
|
||||
Checking if phernandez is a member of organization basicmachines-co
|
||||
User phernandez is a member of organization basicmachines-co. Proceeding with Claude fix.
|
||||
Creating a new branch: fix-issue-75-20250405180254
|
||||
From https://github.com/basicmachines-co/basic-memory
|
||||
* branch main -> FETCH_HEAD
|
||||
Switched to a new branch 'fix-issue-75-20250405180254'
|
||||
branch 'fix-issue-75-20250405180254' set up to track 'origin/main'.
|
||||
Prompt saved to ./claude-output/claude_prompt_75.txt for debugging
|
||||
Running Claude to fix the issue...
|
||||
Committing changes...
|
||||
@@ -0,0 +1,35 @@
|
||||
Let's summarize the changes we've made to fix issue #75:
|
||||
|
||||
1. We updated the `search_notes` tool in `/src/basic_memory/mcp/tools/search.py` to accept primitive types as parameters instead of a complex Pydantic `SearchQuery` object. This makes it easier for LLMs like Cursor to make proper tool calls.
|
||||
|
||||
2. We converted the internal implementation to create a SearchQuery object from the primitive parameters, maintaining backward compatibility with the existing API.
|
||||
|
||||
3. We updated tests in `/tests/mcp/test_tool_search.py` to use the new function signature with primitive parameters.
|
||||
|
||||
4. We updated code in `/src/basic_memory/mcp/tools/read_note.py` to use the new function signature when making calls to `search_notes`.
|
||||
|
||||
5. We updated code in `/src/basic_memory/mcp/prompts/search.py` to use the new function signature when making calls to `search_notes`.
|
||||
|
||||
These changes should make it easier for Cursor and other LLMs to use the search_notes tool by eliminating the complex Pydantic object parameter in favor of simple primitive parameters.
|
||||
|
||||
---SUMMARY---
|
||||
Fixed issue #75 where Cursor was having errors calling the search_notes tool. The problem was that the search_notes tool was expecting a complex Pydantic object (SearchQuery) as input, which was confusing Cursor.
|
||||
|
||||
Changes:
|
||||
1. Modified the search_notes tool to accept primitive types (strings, lists, etc.) as parameters instead of a complex Pydantic object
|
||||
2. Updated the implementation to create a SearchQuery object internally from these primitive parameters
|
||||
3. Updated all call sites in the codebase that were using the old function signature
|
||||
4. Updated tests to use the new function signature
|
||||
|
||||
The fix makes it easier for LLMs like Cursor to make proper calls to the search_notes tool, which will resolve the reported error messages:
|
||||
- "Parameter 'query' must be of type undefined, got object"
|
||||
- "Parameter 'query' must be of type undefined, got string"
|
||||
- "Invalid type for parameter 'query' in tool search_notes"
|
||||
|
||||
Files modified:
|
||||
- src/basic_memory/mcp/tools/search.py
|
||||
- src/basic_memory/mcp/tools/read_note.py
|
||||
- src/basic_memory/mcp/prompts/search.py
|
||||
- tests/mcp/test_tool_search.py
|
||||
- tests/mcp/test_tool_read_note.py
|
||||
---END SUMMARY---
|
||||
@@ -0,0 +1,49 @@
|
||||
You are Claude, an AI assistant tasked with fixing issues in a GitHub repository.
|
||||
|
||||
Issue #75: [BUG] Cursor has errors calling search tool
|
||||
|
||||
Issue Description:
|
||||
## Bug Description
|
||||
|
||||
|
||||
|
||||
> Cursor cannot figure out how to structure the parameters for that tool call. No matter what Cursor seems to try it gets the errors.
|
||||
>
|
||||
> ```Looking at the error messages more carefully:
|
||||
> - When I pass an object: "Parameter 'query' must be of type undefined, got object"
|
||||
> - When I pass a string: "Parameter 'query' must be of type undefined, got string"
|
||||
>
|
||||
>
|
||||
>
|
||||
> and then it reports: "Invalid type for parameter 'query' in tool search_notes"
|
||||
> Any chance you can give me some guidance with this?
|
||||
>
|
||||
|
||||
## Steps To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
try using search tool in Cursor.
|
||||
|
||||
## Possible Solution
|
||||
|
||||
The tool args should probably be plain text and not json to make it easier to call.
|
||||
Additional Instructions from User Comment:
|
||||
let make a PR to implement option #1.
|
||||
Your task is to:
|
||||
1. Analyze the issue carefully to understand the problem
|
||||
2. Look through the repository to identify the relevant files that need to be modified
|
||||
3. Make precise changes to fix the issue
|
||||
4. Use the Edit tool to modify files directly when needed
|
||||
5. Be minimal in your changes - only modify what's necessary to fix the issue
|
||||
|
||||
After making changes, provide a summary of what you did in this format:
|
||||
|
||||
---SUMMARY---
|
||||
[Your detailed summary of changes, including which files were modified and how]
|
||||
---END SUMMARY---
|
||||
|
||||
Remember:
|
||||
- Be specific in your changes
|
||||
- Only modify files that are necessary to fix the issue
|
||||
- Follow existing code style and conventions
|
||||
- Make the minimal changes needed to resolve the issue
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{}
|
||||
Vendored
-31
@@ -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
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"siteId": "947ee055a8c6f1a57efa4afa09791e62",
|
||||
"host": "publish-01.obsidian.md",
|
||||
"included": [],
|
||||
"excluded": []
|
||||
}
|
||||
Vendored
-167
@@ -1,167 +0,0 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "9361625269255fc5",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "9220768da355d9a9",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "cd58e5dfc86465e3",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "empty",
|
||||
"state": {},
|
||||
"icon": "lucide-file",
|
||||
"title": "New tab"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "932aa23fccfbb069",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "c60acde475f2bc34",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "a06f3217ff27ae41",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c338fa22e7339c9a",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "56de0827023b2535",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300
|
||||
},
|
||||
"right": {
|
||||
"id": "c3389d21c08cd71c",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "11a1174f51a1cf86",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "bf2a1fb608e63866",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6f08673b57226dc1",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e987a9643ca3ec81",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "108a72a022d7e501",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"publish:Publish changes...": false,
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false
|
||||
}
|
||||
},
|
||||
"active": "a06f3217ff27ae41",
|
||||
"lastOpenFiles": []
|
||||
}
|
||||
@@ -3,7 +3,9 @@ title: AI Assistant Guide
|
||||
type: note
|
||||
permalink: docs/ai-assistant-guide
|
||||
---
|
||||
> Note: This is an optional document that can be copy/pasted into the project knowledge for an LLM to provide a full description of how it can work with Basic Memory. It is provided as a helpful resource. The tools contain extensive usage description prompts with enable the LLM to understand them.
|
||||
|
||||
You can [download](https://github.com/basicmachines-co/basic-memory/blob/main/docs/AI%20Assistant%20Guide.md) the contents of this file from GitHub
|
||||
# AI Assistant Guide for Basic Memory
|
||||
|
||||
This guide helps you, the AI assistant, use Basic Memory tools effectively when working with users. It covers reading, writing, and navigating knowledge through the Model Context Protocol (MCP).
|
||||
@@ -51,7 +53,7 @@ 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(
|
||||
results = await search_notes(
|
||||
query="authentication system", # Text to search for
|
||||
page=1, # Optional: Pagination
|
||||
page_size=10 # Optional: Results per page
|
||||
@@ -152,7 +154,7 @@ Users will interact with Basic Memory in patterns like:
|
||||
Human: "What were our decisions about auth?"
|
||||
|
||||
You: Let me find that information for you.
|
||||
[Use search() to find relevant notes]
|
||||
[Use search_notes() to find relevant notes]
|
||||
[Then build_context() to understand connections]
|
||||
```
|
||||
|
||||
@@ -249,7 +251,7 @@ When creating relations, you can:
|
||||
# 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")
|
||||
search_results = await search_notes("travel")
|
||||
existing_entities = [result.title for result in search_results.primary_results]
|
||||
|
||||
# Check if specific entities exist
|
||||
@@ -321,7 +323,7 @@ Common issues to watch for:
|
||||
content = await read_note("Document")
|
||||
except:
|
||||
# Try search instead
|
||||
results = await search("Document")
|
||||
results = await search_notes("Document")
|
||||
if results and results.primary_results:
|
||||
# Found something similar
|
||||
content = await read_note(results.primary_results[0].permalink)
|
||||
@@ -367,7 +369,7 @@ Common issues to watch for:
|
||||
- **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
|
||||
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
|
||||
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead of "relates_to")
|
||||
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|
||||
|
||||
|
||||
+36
-9
@@ -21,14 +21,13 @@ basic-memory sync
|
||||
# Watch for changes
|
||||
basic-memory sync --watch
|
||||
|
||||
# Sync specific folder
|
||||
basic-memory sync path/to/folder
|
||||
# Show detailed sync information
|
||||
basic-memory sync --verbose
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--watch`: Continuously monitor for changes
|
||||
- `--verbose`: Show detailed output
|
||||
- `PATH`: Optional path to sync (defaults to ~/basic-memory)
|
||||
|
||||
### import
|
||||
|
||||
@@ -43,13 +42,13 @@ basic-memory import claude projects
|
||||
|
||||
# ChatGPT history
|
||||
basic-memory import chatgpt
|
||||
|
||||
# ChatGPT history
|
||||
basic-memory import memory-json /path/to/memory.json
|
||||
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--folder PATH`: Target folder for imported content
|
||||
- `--overwrite`: Replace existing files
|
||||
- `--skip-existing`: Keep existing files
|
||||
|
||||
> **Note**: After importing, run `basic-memory sync` to index the new files.
|
||||
### status
|
||||
|
||||
Shows system status information:
|
||||
@@ -242,10 +241,12 @@ You can redirect input from a file:
|
||||
basic-memory tool write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
|
||||
```
|
||||
|
||||
#### Integration with Claude Code
|
||||
## Integration with Claude Code
|
||||
|
||||
This feature works well with Claude Code in the terminal:
|
||||
|
||||
### cli
|
||||
|
||||
In a Claude Code session, let Claude know he can use the basic-memory tools, then he can execute them via the cli:
|
||||
|
||||
```
|
||||
@@ -258,6 +259,32 @@ In a Claude Code session, let Claude know he can use the basic-memory tools, the
|
||||
|
||||
```
|
||||
|
||||
### MCP
|
||||
|
||||
Claude code can also now use mcp tools, so it can use any of the basic-memory tool natively. To install basic-memory in Claude Code:
|
||||
|
||||
Run
|
||||
```
|
||||
claude mcp add basic-memory basic-memory mcp
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
➜ ~ claude mcp add basic-memory basic-memory mcp
|
||||
Added stdio MCP server basic-memory with command: basic-memory mcp to project config
|
||||
➜ ~ claude mcp list
|
||||
basic-memory: basic-memory mcp
|
||||
```
|
||||
|
||||
You can then use the `/mcp` command in the REPL:
|
||||
|
||||
```
|
||||
/mcp
|
||||
⎿ MCP Server Status
|
||||
|
||||
• basic-memory: connected
|
||||
```
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
|
||||
@@ -6,25 +6,35 @@ 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.
|
||||
This guide will help you install Basic Memory, configure it with Claude Desktop, and create your first knowledge notes
|
||||
through conversations.
|
||||
|
||||
Basic Memory uses the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) to connect with LLMs.
|
||||
It can be used with any service that supports the MCP, but Claude Desktop works especially well.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The easiest way to install basic memory is via `uv`. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
### 1. Install Basic Memory
|
||||
|
||||
```bash
|
||||
# Install with uv (recommended)
|
||||
uv install basic-memory
|
||||
# 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. The `uvx` command mentioned in the Claude Desktop configuration is only for enabling Claude to access 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
|
||||
|
||||
To enable Claude to read and write to your knowledge base, edit the Claude Desktop configuration file (usually at `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
||||
Edit your Claude Desktop config, located at `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -40,10 +50,49 @@ To enable Claude to read and write to your knowledge base, edit the Claude Deskt
|
||||
}
|
||||
```
|
||||
|
||||
This configuration uses `uvx` to execute Basic Memory without requiring a full installation in Claude's environment.
|
||||
**Restart Claude Desktop**. You should see Basic Memory tools available in the "tools" menu in Claude Desktop (the little hammer icon in the bottom-right corner of the chat interface). Click it to view available tools.
|
||||
#### Fix Path to uv
|
||||
|
||||
If you get an error that says `ENOENT` , this most likely means Claude Desktop could not find your `uv` installation. Make sure that you have `uv` installed per the instructions above, then:
|
||||
|
||||
**Step 1: Find the absolute path to uvx**
|
||||
|
||||
Open Terminal and run:
|
||||
|
||||
```bash
|
||||
which uvx
|
||||
```
|
||||
|
||||
This will show you the full path (e.g., `/Users/yourusername/.cargo/bin/uvx`).
|
||||
|
||||
**Step 2: Edit Claude Desktop Configuration**
|
||||
|
||||
Edit the Claude Desktop config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "/absolute/path/to/uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `/absolute/path/to/uvx` with the actual path you found in Step 1.
|
||||
|
||||
**Step 3: Restart Claude Desktop**
|
||||
|
||||
Close and reopen Claude Desktop for the changes to take effect.
|
||||
|
||||
### 3. Start the Sync Service
|
||||
|
||||
> Note the sync service is optional. You can run it if you want your local change to be available in Claude Desktop
|
||||
|
||||
Start the sync service to monitor your files for changes:
|
||||
|
||||
```bash
|
||||
@@ -54,25 +103,99 @@ basic-memory sync
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
The `--watch` flag enables automatic detection of file changes, keeping your knowledge base current.
|
||||
The `--watch` flag enables automatic detection of file changes, updating your knowledge in real time.
|
||||
|
||||
### 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.
|
||||
|
||||
### 5. Change the default project directory
|
||||
|
||||
By default, Basic Memory will create a project in the `basic-memory` folder in your home directory. You can change this via the `project` [[CLI Reference#project|cli command]].
|
||||
|
||||
```
|
||||
# Add a new project
|
||||
basic-memory project add work ~/work-basic-memory
|
||||
|
||||
# Set the default project
|
||||
basic-memory project default work
|
||||
|
||||
# List all configured projects
|
||||
basic-memory project list
|
||||
```
|
||||
|
||||
## 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 a conversation in Claude Desktop** about any topic:
|
||||
1. **Open Claude Desktop** and start a new conversation.
|
||||
|
||||
2. **Have a natural conversation** about any topic:
|
||||
```
|
||||
You: "Let's talk about coffee brewing methods I've been experimenting with."
|
||||
Claude: "I'd be happy to discuss coffee brewing methods..."
|
||||
You: "I've found that pour over gives more flavor clarity than French press..."
|
||||
```
|
||||
|
||||
2. **Have a natural conversation** about the topic
|
||||
|
||||
3. **Ask Claude to create a note**:
|
||||
```
|
||||
You: "Could you create a note summarizing what we've discussed about coffee brewing?"
|
||||
```
|
||||
|
||||
4. **Claude creates a Markdown file** in your `~/basic-memory` directory
|
||||
4. **Confirm note creation**:
|
||||
Claude will confirm when the note has been created and where it's stored.
|
||||
|
||||
5. **View and edit the file** with any text editor or Obsidian
|
||||
5. **View the created file** in your `~/basic-memory` directory using any text editor or Obsidian.
|
||||
The file structure will look similar to:
|
||||
```markdown
|
||||
---
|
||||
title: Coffee Brewing Methods
|
||||
permalink: coffee-brewing-methods
|
||||
---
|
||||
|
||||
# Coffee Brewing Methods
|
||||
|
||||
## Observations
|
||||
- [method] Pour over provides more clarity...
|
||||
- [technique] Water temperature at 205°F...
|
||||
|
||||
## Relations
|
||||
- relates_to [[Other Coffee Topics]]
|
||||
```
|
||||
|
||||
5. **Start the sync process** in a Terminal window (optional):
|
||||
```bash
|
||||
basic-memory sync --watch
|
||||
```
|
||||
Keep this running in the background.
|
||||
|
||||
## Using Special Prompts
|
||||
|
||||
@@ -87,6 +210,7 @@ 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
|
||||
@@ -100,6 +224,7 @@ 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
|
||||
@@ -113,10 +238,13 @@ 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
|
||||
@@ -173,4 +301,4 @@ After getting started, explore these areas:
|
||||
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
|
||||
5. **Review the [[CLI Reference]]** for command line tools
|
||||
@@ -62,6 +62,114 @@ Basic Memory is designed with privacy as a core principle:
|
||||
|
||||
## 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:
|
||||
@@ -73,6 +181,42 @@ Basic Memory's core data model consists of:
|
||||
|
||||
The system parses Markdown files to extract this structured information while preserving the human-readable format.
|
||||
|
||||
### Files as Source of Truth
|
||||
|
||||
Plain Markdown files store all knowledge, making it accessible with any text editor and easy to version with git.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User((User)) <--> |Conversation| Claude["Claude or other LLM"]
|
||||
Claude <-->|API Calls| BMCP["Basic Memory MCP Server"]
|
||||
|
||||
subgraph "Local Storage"
|
||||
KnowledgeFiles["Markdown Files - Source of Truth"]
|
||||
KnowledgeIndex[(Knowledge Graph SQLite Index)]
|
||||
end
|
||||
|
||||
BMCP <-->|"write_note() read_note()"| KnowledgeFiles
|
||||
BMCP <-->|"search_notes() build_context()"| KnowledgeIndex
|
||||
KnowledgeFiles <-.->|Sync Process| KnowledgeIndex
|
||||
KnowledgeFiles <-->|Direct Editing| Editors((Text Editors & Git))
|
||||
|
||||
User -.->|"Complete control, Privacy preserved"| KnowledgeFiles
|
||||
|
||||
|
||||
class Claude primary
|
||||
class BMCP secondary
|
||||
class KnowledgeFiles tertiary
|
||||
class KnowledgeIndex quaternary
|
||||
class User,Editors user`;
|
||||
```
|
||||
|
||||
### Sqlite Database
|
||||
|
||||
A local SQLite database maintains the knowledge graph topology for fast queries and semantic traversal without cloud dependencies. It contains:
|
||||
- db tables for the knowledge graph schema
|
||||
- a search index table enabling full text search across the knowledge base
|
||||
|
||||
|
||||
### Sync Process
|
||||
|
||||
The sync process:
|
||||
@@ -94,6 +238,6 @@ The search functionality:
|
||||
5. Ranks results by relevance to the query
|
||||
|
||||
## Relations
|
||||
- relates_to [[Introduction to Basic Memory]] (System overview)
|
||||
- relates_to [[Welcome to Basic memory]] (Overview)
|
||||
- relates_to [[CLI Reference]] (Command line tools)
|
||||
- implements [[Knowledge Format]] (File structure and format)
|
||||
+64
-26
@@ -6,7 +6,8 @@ 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.
|
||||
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
|
||||
|
||||
@@ -45,7 +46,8 @@ You can create files directly:
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
@@ -56,6 +58,7 @@ 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
|
||||
@@ -63,6 +66,7 @@ Behind the scenes:
|
||||
- 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
|
||||
@@ -76,12 +80,14 @@ 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
|
||||
@@ -95,12 +101,14 @@ 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
|
||||
@@ -116,9 +124,26 @@ Enter a topic
|
||||
Give instructions
|
||||
![[prompt3.png|500]]
|
||||
|
||||
Claude can build context from the supplied topic.
|
||||
![[prompt4.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
|
||||
|
||||
@@ -142,7 +167,8 @@ 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.
|
||||
This triggers a specialized search that returns precise results with document titles, relevant excerpts, and offers to
|
||||
explore specific documents.
|
||||
|
||||
### Boolean Search
|
||||
|
||||
@@ -179,6 +205,7 @@ Basic Memory supports standard boolean operators:
|
||||
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
|
||||
@@ -193,6 +220,7 @@ 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
|
||||
@@ -206,6 +234,7 @@ 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
|
||||
@@ -213,6 +242,7 @@ This builds a complete picture by:
|
||||
### 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
|
||||
@@ -273,10 +303,12 @@ permalink: 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]]
|
||||
```
|
||||
@@ -300,11 +332,13 @@ 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)
|
||||
@@ -426,16 +460,15 @@ basic-memory import claude conversations
|
||||
basic-memory import chatgpt
|
||||
```
|
||||
|
||||
## Multiple Projects
|
||||
|
||||
## 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).
|
||||
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
|
||||
|
||||
|
||||
### Managing Projects
|
||||
|
||||
```bash
|
||||
# List all configured projects
|
||||
basic-memory project list
|
||||
@@ -452,11 +485,11 @@ 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:
|
||||
|
||||
|
||||
### 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
|
||||
@@ -464,20 +497,21 @@ 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:
|
||||
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
@@ -493,6 +527,7 @@ Each project maintains:
|
||||
### 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`
|
||||
@@ -500,6 +535,7 @@ If changes aren't showing up:
|
||||
### 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
|
||||
@@ -507,11 +543,13 @@ If content isn't found:
|
||||
### 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)
|
||||
|
||||
+10
-123
@@ -14,8 +14,6 @@ Basic Memory is a knowledge management system that allows you to build a persist
|
||||
with AI assistants. All knowledge is stored in standard Markdown files on your computer, giving you full control and
|
||||
ownership of your data.
|
||||
|
||||
## Core Functions
|
||||
|
||||
Basic Memory connects you and AI assistants through shared knowledge:
|
||||
|
||||
1. **Captures knowledge** from natural conversations with AI assistants
|
||||
@@ -26,132 +24,21 @@ Basic Memory connects you and AI assistants through shared knowledge:
|
||||
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.
|
||||
|
||||
![[Obsidian-CoffeeKnowledgeBase-examples-overlays.gif]]
|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
## Technical Architecture
|
||||
- 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)
|
||||
- **Memory:// URI scheme** - For precise knowledge referencing and navigation
|
||||
- **Model Context Protocol (MCP)** - For seamless AI assistant integration
|
||||
|
||||
## Knowledge Structure
|
||||
|
||||
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",
|
||||
"context": null,
|
||||
"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",
|
||||
"context": null,
|
||||
"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.
|
||||
|
||||
## User Control and File Management
|
||||
|
||||
Basic Memory gives you complete control over your knowledge:
|
||||
|
||||
- **Local-first storage** - All knowledge lives on your computer
|
||||
@@ -160,7 +47,7 @@ Basic Memory gives you complete control over your knowledge:
|
||||
- **Version control ready** - Use git for history, branching, and collaboration
|
||||
- **Edit anywhere** - Modify files with any text editor or Obsidian
|
||||
|
||||
Changes to files automatically update the knowledge graph, and AI assistants can see your edits in future conversations.
|
||||
Changes to files automatically sync with the knowledge graph, and AI assistants can see your edits in conversations.
|
||||
|
||||
## Documentation Map
|
||||
|
||||
@@ -169,11 +56,11 @@ 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]]
|
||||
- Reference for AI assistants using Basic Memory [[AI Assistant Guide]]
|
||||
- Technical implementation details [[Technical Information]]
|
||||
- Command line tool reference [[CLI Reference]]
|
||||
- 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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,5 @@
|
||||
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);
|
||||
Binary file not shown.
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"collapse-filter": true,
|
||||
"search": "",
|
||||
"showTags": false,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": true,
|
||||
"collapse-color-groups": true,
|
||||
"colorGroups": [],
|
||||
"collapse-display": true,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 1,
|
||||
"lineSizeMultiplier": 1,
|
||||
"collapse-forces": true,
|
||||
"centerStrength": 0.518713248970312,
|
||||
"repelStrength": 10,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 1,
|
||||
"close": false
|
||||
}
|
||||
Vendored
-186
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "7ae5f8d0583c3a18",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "22434516a5f1a19f",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "6fae14c14e044a89",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Coffee Notes/Coffee Brewing Methods.md",
|
||||
"mode": "source",
|
||||
"source": true
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Coffee Brewing Methods"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "402ef826b6e8c27a",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "7df070b9023ec8de",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "6fc235d3a84e414f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8679c962d1bfd321",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8848cf5cc01540b2",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 264.5
|
||||
},
|
||||
"right": {
|
||||
"id": "5bfd609e7511200a",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "5b83fb5c13b27ce5",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "396b2dfa99a4a44e",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "Untitled.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for Untitled"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b606de8f7f078b60",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "Untitled.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from Untitled"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2788e9059ef83663",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cdf344bf83b2f251",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "Untitled.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of Untitled"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 682.5,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false,
|
||||
"publish:Publish changes...": false
|
||||
}
|
||||
},
|
||||
"active": "6fae14c14e044a89",
|
||||
"lastOpenFiles": [
|
||||
"Untitled.md",
|
||||
"Coffee Notes/Coffee Brewing Methods.md",
|
||||
"Coffee Notes/Coffee Knowledge Base.md",
|
||||
"Coffee Notes/Flavor Extraction.md",
|
||||
"Coffee Notes/Coffee Flavor Map.md",
|
||||
"Coffee Notes/Coffee Bean Origins.md",
|
||||
"Coffee Notes/Perfect Pour Over Coffee Method.canvas",
|
||||
"Coffee Notes/Coffee Brewing Methods Venn Diagram.canvas",
|
||||
"Coffee Notes/Pour Over Techniques.md",
|
||||
"Coffee Notes/Tasting Notes.md",
|
||||
"Coffee Notes/Hybrid Pour Over Technique.md"
|
||||
]
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# Basic Memory Installation Guide for LLMs
|
||||
|
||||
This guide is specifically designed to help AI assistants like Cline install and configure Basic Memory. Follow these
|
||||
steps in order.
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Basic Memory Package
|
||||
|
||||
Use one of the following package managers to install:
|
||||
|
||||
```bash
|
||||
# Install with uv (recommended)
|
||||
uv tool install basic-memory
|
||||
|
||||
# Or with pip
|
||||
pip install basic-memory
|
||||
```
|
||||
|
||||
### 2. Configure MCP Server
|
||||
|
||||
Add the following to your config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Claude Desktop, this file is located at:
|
||||
|
||||
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
Windows: %APPDATA%\Claude\claude_desktop_config.json
|
||||
|
||||
### 3. Start Synchronization (optional)
|
||||
|
||||
To synchronize files in real-time, run:
|
||||
|
||||
```bash
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
Or for a one-time sync:
|
||||
|
||||
```bash
|
||||
basic-memory sync
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Custom Directory
|
||||
|
||||
To use a directory other than the default `~/basic-memory`:
|
||||
|
||||
```bash
|
||||
basic-memory project add custom-project /path/to/your/directory
|
||||
basic-memory project default custom-project
|
||||
```
|
||||
|
||||
### Multiple Projects
|
||||
|
||||
To manage multiple knowledge bases:
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
basic-memory project list
|
||||
|
||||
# Add a new project
|
||||
basic-memory project add work ~/work-basic-memory
|
||||
|
||||
# Set default project
|
||||
basic-memory project default work
|
||||
```
|
||||
|
||||
## Importing Existing Data
|
||||
|
||||
### From Claude.ai
|
||||
|
||||
```bash
|
||||
basic-memory import claude conversations path/to/conversations.json
|
||||
basic-memory import claude projects path/to/projects.json
|
||||
```
|
||||
|
||||
### From ChatGPT
|
||||
|
||||
```bash
|
||||
basic-memory import chatgpt path/to/conversations.json
|
||||
```
|
||||
|
||||
### From MCP Memory Server
|
||||
|
||||
```bash
|
||||
basic-memory import memory-json path/to/memory.json
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check that Basic Memory is properly installed:
|
||||
```bash
|
||||
basic-memory --version
|
||||
```
|
||||
|
||||
2. Verify the sync process is running:
|
||||
```bash
|
||||
ps aux | grep basic-memory
|
||||
```
|
||||
|
||||
3. Check sync output for errors:
|
||||
```bash
|
||||
basic-memory sync --verbose
|
||||
```
|
||||
|
||||
4. Check log output:
|
||||
```bash
|
||||
cat ~/.basic-memory/basic-memory.log
|
||||
```
|
||||
|
||||
For more detailed information, refer to the [full documentation](https://memory.basicmachines.co/).
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "basic-memory"
|
||||
version = "0.9.0"
|
||||
version = "0.12.0"
|
||||
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12.1"
|
||||
@@ -40,6 +40,7 @@ Documentation = "https://github.com/basicmachines-co/basic-memory#readme"
|
||||
|
||||
[project.scripts]
|
||||
basic-memory = "basic_memory.cli.main:app"
|
||||
bm = "basic_memory.cli.main:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
||||
|
||||
startCommand:
|
||||
type: stdio
|
||||
configSchema:
|
||||
# JSON Schema defining the configuration options for the MCP.
|
||||
type: object
|
||||
properties: {}
|
||||
description: No configuration required. This MCP server runs using the default command.
|
||||
commandFunction: |-
|
||||
(config) => ({
|
||||
command: 'basic-memory',
|
||||
args: ['mcp']
|
||||
})
|
||||
exampleConfig: {}
|
||||
@@ -1,3 +1,3 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
__version__ = "0.9.0"
|
||||
__version__ = "0.12.0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""FastAPI application for basic-memory knowledge graph API."""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
@@ -7,16 +8,54 @@ 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
|
||||
from basic_memory.api.routers import knowledge, memory, project_info, resource, search
|
||||
from basic_memory.config import config as project_config
|
||||
from basic_memory.config import config_manager
|
||||
from basic_memory.sync import SyncService, WatchService
|
||||
|
||||
|
||||
async def run_background_sync(sync_service: SyncService, watch_service: WatchService): # pragma: no cover
|
||||
logger.info(f"Starting watch service to sync file changes in dir: {project_config.home}")
|
||||
# full sync
|
||||
await sync_service.sync(project_config.home, show_progress=False)
|
||||
|
||||
# watch changes
|
||||
await watch_service.run()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI): # pragma: no cover
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
await db.run_migrations(app_config)
|
||||
await db.run_migrations(project_config)
|
||||
|
||||
# app config
|
||||
basic_memory_config = config_manager.load_config()
|
||||
logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
|
||||
logger.info(f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}")
|
||||
|
||||
watch_task = None
|
||||
if basic_memory_config.sync_changes:
|
||||
# import after migrations have run
|
||||
from basic_memory.cli.commands.sync import get_sync_service
|
||||
|
||||
sync_service = await get_sync_service()
|
||||
watch_service = WatchService(
|
||||
sync_service=sync_service,
|
||||
file_service=sync_service.entity_service.file_service,
|
||||
config=project_config,
|
||||
)
|
||||
watch_task = asyncio.create_task(run_background_sync(sync_service, watch_service))
|
||||
else:
|
||||
logger.info("Sync changes disabled. Skipping watch service.")
|
||||
|
||||
|
||||
# proceed with startup
|
||||
yield
|
||||
|
||||
logger.info("Shutting down Basic Memory API")
|
||||
if watch_task:
|
||||
watch_task.cancel()
|
||||
|
||||
await db.shutdown_db()
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy import text
|
||||
|
||||
from basic_memory.config import config, config_manager
|
||||
from basic_memory.deps import (
|
||||
ProjectInfoRepositoryDep,
|
||||
@@ -18,6 +15,8 @@ from basic_memory.schemas import (
|
||||
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"])
|
||||
|
||||
@@ -262,7 +261,7 @@ async def get_system_status() -> SystemStatus:
|
||||
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())
|
||||
watch_status = json.loads(watch_status_path.read_text(encoding="utf-8"))
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
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."""
|
||||
@@ -58,9 +54,6 @@ def app_callback(
|
||||
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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""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
|
||||
from . import import_claude_projects, import_chatgpt, tool, project
|
||||
|
||||
__all__ = [
|
||||
"status",
|
||||
@@ -14,5 +14,4 @@ __all__ = [
|
||||
"import_chatgpt",
|
||||
"tool",
|
||||
"project",
|
||||
"project_info",
|
||||
]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Database management commands."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.alembic import migrations
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -14,7 +17,17 @@ def reset(
|
||||
"""Reset database (drop all tables and recreate)."""
|
||||
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
||||
logger.info("Resetting database...")
|
||||
migrations.reset_database()
|
||||
# Get database path
|
||||
db_path = config.database_path
|
||||
|
||||
# Delete the database file if it exists
|
||||
if db_path.exists():
|
||||
db_path.unlink()
|
||||
logger.info(f"Database file deleted: {db_path}")
|
||||
|
||||
# Create a new empty database
|
||||
asyncio.run(db.run_migrations(config))
|
||||
logger.info("Database reset complete")
|
||||
|
||||
if reindex:
|
||||
# Import and run sync
|
||||
|
||||
@@ -7,15 +7,14 @@ from pathlib import Path
|
||||
from typing import Dict, Any, List, Annotated, Set, Optional
|
||||
|
||||
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
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -167,7 +166,7 @@ async def process_chatgpt_json(
|
||||
read_task = progress.add_task("Reading chat data...", total=None)
|
||||
|
||||
# Read conversations
|
||||
conversations = json.loads(json_path.read_text())
|
||||
conversations = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
progress.update(read_task, total=len(conversations))
|
||||
|
||||
# Process each conversation
|
||||
|
||||
@@ -7,15 +7,14 @@ 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 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()
|
||||
|
||||
@@ -124,7 +123,7 @@ async def process_conversations_json(
|
||||
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())
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
conversations = [chat for chat in data]
|
||||
progress.update(read_task, total=len(conversations))
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Annotated, Optional
|
||||
|
||||
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 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()
|
||||
|
||||
@@ -103,7 +102,7 @@ async def process_projects_json(
|
||||
read_task = progress.add_task("Reading project data...", total=None)
|
||||
|
||||
# Read project data
|
||||
data = json.loads(json_path.read_text())
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
progress.update(read_task, total=len(data))
|
||||
|
||||
# Track import counts
|
||||
|
||||
@@ -4,7 +4,7 @@ from loguru import logger
|
||||
|
||||
import basic_memory
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import config, config_manager
|
||||
|
||||
# Import mcp instance
|
||||
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
||||
@@ -19,8 +19,15 @@ def mcp(): # pragma: no cover
|
||||
home_dir = config.home
|
||||
project_name = config.project
|
||||
|
||||
# app config
|
||||
basic_memory_config = config_manager.load_config()
|
||||
|
||||
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
||||
logger.info(f"Project: {project_name}")
|
||||
logger.info(f"Project directory: {home_dir}")
|
||||
logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
|
||||
logger.info(
|
||||
f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}"
|
||||
)
|
||||
|
||||
mcp_server.run()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Command module for basic-memory project management."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,6 +10,12 @@ from rich.table import Table
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import ConfigManager, config
|
||||
from basic_memory.mcp.tools.project_info import project_info
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.tree import Tree
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -92,12 +99,22 @@ def remove_project(
|
||||
def set_default_project(
|
||||
name: str = typer.Argument(..., help="Name of the project to set as default"),
|
||||
) -> None:
|
||||
"""Set the default project."""
|
||||
"""Set the default project and activate it for the current session."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
try:
|
||||
# Set the default project
|
||||
config_manager.set_default_project(name)
|
||||
console.print(f"[green]Project '{name}' set as default[/green]")
|
||||
|
||||
# Also activate it for the current session by setting the environment variable
|
||||
os.environ["BASIC_MEMORY_PROJECT"] = name
|
||||
|
||||
# Reload configuration to apply the change
|
||||
from importlib import reload
|
||||
from basic_memory import config as config_module
|
||||
|
||||
reload(config_module)
|
||||
console.print(f"[green]Project '{name}' set as default and activated[/green]")
|
||||
except ValueError as e: # pragma: no cover
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
@@ -117,3 +134,152 @@ def show_current_project() -> None:
|
||||
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]")
|
||||
|
||||
|
||||
@project_app.command("info")
|
||||
def display_project_info(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
||||
):
|
||||
"""Display detailed information and statistics about the current project."""
|
||||
try:
|
||||
# Get project info
|
||||
info = asyncio.run(project_info())
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
@@ -70,6 +70,7 @@ async def get_sync_service(): # pragma: no cover
|
||||
|
||||
# Create sync service
|
||||
sync_service = SyncService(
|
||||
config=config,
|
||||
entity_service=entity_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
|
||||
@@ -2,34 +2,32 @@
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Optional, List, Annotated
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich import print as rprint
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
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.mcp.tools import build_context as mcp_build_context
|
||||
from basic_memory.mcp.tools import read_note as mcp_read_note
|
||||
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
||||
from basic_memory.mcp.tools import search_notes as mcp_search
|
||||
from basic_memory.mcp.tools import write_note as mcp_write_note
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.memory import MemoryUrl
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
|
||||
tool_app = typer.Typer()
|
||||
app.add_typer(tool_app, name="tool", help="Direct access to MCP tools via CLI")
|
||||
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
@@ -103,6 +101,7 @@ def write_note(
|
||||
|
||||
@tool_app.command()
|
||||
def read_note(identifier: str, page: int = 1, page_size: int = 10):
|
||||
"""Read a markdown note from the knowledge base."""
|
||||
try:
|
||||
note = asyncio.run(mcp_read_note(identifier, page, page_size))
|
||||
rprint(note)
|
||||
@@ -122,6 +121,7 @@ def build_context(
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
"""Get context needed to continue a discussion."""
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_build_context(
|
||||
@@ -154,6 +154,7 @@ def recent_activity(
|
||||
page_size: int = 10,
|
||||
max_related: int = 10,
|
||||
):
|
||||
"""Get recent activity across the knowledge base."""
|
||||
try:
|
||||
context = asyncio.run(
|
||||
mcp_recent_activity(
|
||||
@@ -177,8 +178,8 @@ def recent_activity(
|
||||
raise
|
||||
|
||||
|
||||
@tool_app.command()
|
||||
def search(
|
||||
@tool_app.command("search-notes")
|
||||
def search_notes(
|
||||
query: str,
|
||||
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
|
||||
title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
|
||||
@@ -189,18 +190,34 @@ def search(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
"""Search across all content in the knowledge base."""
|
||||
if permalink and title: # pragma: no cover
|
||||
print("Cannot search both permalink and title")
|
||||
raise typer.Abort()
|
||||
|
||||
try:
|
||||
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,
|
||||
if permalink and title: # pragma: no cover
|
||||
typer.echo(
|
||||
"Use either --permalink or --title, not both. Exiting.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# set search type
|
||||
search_type = ("permalink" if permalink else None,)
|
||||
search_type = ("permalink_match" if permalink and "*" in query else None,)
|
||||
search_type = ("title" if title else None,)
|
||||
search_type = "text" if search_type is None else search_type
|
||||
|
||||
results = asyncio.run(
|
||||
mcp_search(
|
||||
query,
|
||||
search_type=search_type,
|
||||
page=page,
|
||||
after_date=after_date,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
results = asyncio.run(mcp_search(query=search_query, page=page, page_size=page_size))
|
||||
# Use json module for more controlled serialization
|
||||
import json
|
||||
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
"""Main CLI entry point for basic-memory.""" # pragma: no cover
|
||||
|
||||
from basic_memory.cli.app import app # pragma: no cover
|
||||
import asyncio
|
||||
|
||||
import typer
|
||||
|
||||
from basic_memory.cli.app import app # pragma: no cover
|
||||
|
||||
# Register commands
|
||||
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
||||
status,
|
||||
sync,
|
||||
db,
|
||||
import_memory_json,
|
||||
mcp,
|
||||
import_chatgpt,
|
||||
import_claude_conversations,
|
||||
import_claude_projects,
|
||||
import_chatgpt,
|
||||
tool,
|
||||
import_memory_json,
|
||||
mcp,
|
||||
project,
|
||||
status,
|
||||
sync,
|
||||
tool,
|
||||
)
|
||||
from basic_memory.config import config
|
||||
from basic_memory.db import run_migrations as db_run_migrations
|
||||
|
||||
|
||||
# Version command
|
||||
@@ -55,4 +60,8 @@ def main(
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# Run database migrations
|
||||
asyncio.run(db_run_migrations(config))
|
||||
|
||||
# start the app
|
||||
app()
|
||||
|
||||
+69
-11
@@ -38,7 +38,11 @@ class ProjectConfig(BaseSettings):
|
||||
default=500, description="Milliseconds to wait after changes before syncing", gt=0
|
||||
)
|
||||
|
||||
log_level: str = "DEBUG"
|
||||
# update permalinks on move
|
||||
update_permalinks_on_move: bool = Field(
|
||||
default=False,
|
||||
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BASIC_MEMORY_",
|
||||
@@ -77,6 +81,18 @@ class BasicMemoryConfig(BaseSettings):
|
||||
description="Name of the default project to use",
|
||||
)
|
||||
|
||||
log_level: str = "INFO"
|
||||
|
||||
update_permalinks_on_move: bool = Field(
|
||||
default=False,
|
||||
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
||||
)
|
||||
|
||||
sync_changes: bool = Field(
|
||||
default=True,
|
||||
description="Whether to sync changes in real time. default (True)",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BASIC_MEMORY_",
|
||||
extra="ignore",
|
||||
@@ -111,7 +127,7 @@ class ConfigManager:
|
||||
"""Load configuration from file or create default."""
|
||||
if self.config_file.exists():
|
||||
try:
|
||||
data = json.loads(self.config_file.read_text())
|
||||
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}")
|
||||
@@ -194,9 +210,14 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
|
||||
)
|
||||
|
||||
update_permalinks_on_move = config_manager.load_config().update_permalinks_on_move
|
||||
try:
|
||||
project_path = config_manager.get_project_path(actual_project_name)
|
||||
return ProjectConfig(home=project_path, project=actual_project_name)
|
||||
return ProjectConfig(
|
||||
home=project_path,
|
||||
project=actual_project_name,
|
||||
update_permalinks_on_move=update_permalinks_on_move,
|
||||
)
|
||||
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)
|
||||
@@ -214,11 +235,48 @@ 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})")
|
||||
|
||||
def get_process_name(): # pragma: no cover
|
||||
"""
|
||||
get the type of process for logging
|
||||
"""
|
||||
import sys
|
||||
|
||||
if "sync" in sys.argv:
|
||||
return "sync"
|
||||
elif "mcp" in sys.argv:
|
||||
return "mcp"
|
||||
elif "cli" in sys.argv:
|
||||
return "cli"
|
||||
else:
|
||||
return "api"
|
||||
|
||||
|
||||
process_name = get_process_name()
|
||||
|
||||
# Global flag to track if logging has been set up
|
||||
_LOGGING_SETUP = False
|
||||
|
||||
|
||||
def setup_basic_memory_logging(): # pragma: no cover
|
||||
"""Set up logging for basic-memory, ensuring it only happens once."""
|
||||
global _LOGGING_SETUP
|
||||
if _LOGGING_SETUP:
|
||||
# We can't log before logging is set up
|
||||
# print("Skipping duplicate logging setup")
|
||||
return
|
||||
|
||||
setup_logging(
|
||||
env=config.env,
|
||||
home_dir=user_home, # Use user home for logs
|
||||
log_level=config_manager.load_config().log_level,
|
||||
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
||||
console=False,
|
||||
)
|
||||
|
||||
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
||||
_LOGGING_SETUP = True
|
||||
|
||||
|
||||
# Set up logging
|
||||
setup_basic_memory_logging()
|
||||
|
||||
@@ -146,7 +146,9 @@ async def engine_session_factory(
|
||||
_session_maker = None
|
||||
|
||||
|
||||
async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
|
||||
async def run_migrations(
|
||||
app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM
|
||||
): # pragma: no cover
|
||||
"""Run any pending alembic migrations."""
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
|
||||
@@ -85,7 +85,7 @@ async def write_file_atomic(path: FilePath, content: str) -> None:
|
||||
temp_path = path_obj.with_suffix(".tmp")
|
||||
|
||||
try:
|
||||
temp_path.write_text(content)
|
||||
temp_path.write_text(content, encoding="utf-8")
|
||||
temp_path.replace(path_obj)
|
||||
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
|
||||
except Exception as e: # pragma: no cover
|
||||
@@ -104,6 +104,9 @@ def has_frontmatter(content: str) -> bool:
|
||||
Returns:
|
||||
True if content has valid frontmatter markers (---), False otherwise
|
||||
"""
|
||||
if not content:
|
||||
return False
|
||||
|
||||
content = content.strip()
|
||||
if not content.startswith("---"):
|
||||
return False
|
||||
@@ -203,7 +206,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
||||
path_obj = Path(path) if isinstance(path, str) else path
|
||||
|
||||
# Read current content
|
||||
content = path_obj.read_text()
|
||||
content = path_obj.read_text(encoding="utf-8")
|
||||
|
||||
# Parse current frontmatter
|
||||
current_fm = {}
|
||||
@@ -215,7 +218,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
||||
new_fm = {**current_fm, **updates}
|
||||
|
||||
# Write new file with updated frontmatter
|
||||
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
||||
yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
|
||||
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
||||
|
||||
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
|
||||
|
||||
@@ -4,21 +4,22 @@ Uses markdown-it with plugins to parse structured data from markdown content.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
import dateparser
|
||||
|
||||
from markdown_it import MarkdownIt
|
||||
import dateparser
|
||||
import frontmatter
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
from basic_memory.markdown.plugins import observation_plugin, relation_plugin
|
||||
from basic_memory.markdown.schemas import (
|
||||
EntityMarkdown,
|
||||
EntityFrontmatter,
|
||||
EntityMarkdown,
|
||||
Observation,
|
||||
Relation,
|
||||
)
|
||||
from basic_memory.utils import parse_tags
|
||||
|
||||
md = MarkdownIt().use(observation_plugin).use(relation_plugin)
|
||||
|
||||
@@ -56,11 +57,11 @@ def parse(content: str) -> EntityContent:
|
||||
)
|
||||
|
||||
|
||||
def parse_tags(tags: Any) -> list[str]:
|
||||
"""Parse tags into list of strings."""
|
||||
if isinstance(tags, (list, tuple)):
|
||||
return [str(t).strip() for t in tags if str(t).strip()]
|
||||
return [t.strip() for t in tags.split(",") if t.strip()]
|
||||
# def parse_tags(tags: Any) -> list[str]:
|
||||
# """Parse tags into list of strings."""
|
||||
# if isinstance(tags, (list, tuple)):
|
||||
# return [str(t).strip() for t in tags if str(t).strip()]
|
||||
# return [t.strip() for t in tags.split(",") if t.strip()]
|
||||
|
||||
|
||||
class EntityParser:
|
||||
@@ -91,25 +92,36 @@ class EntityParser:
|
||||
async def parse_file(self, path: Path | str) -> EntityMarkdown:
|
||||
"""Parse markdown file into EntityMarkdown."""
|
||||
|
||||
absolute_path = self.base_path / path
|
||||
# Parse frontmatter and content using python-frontmatter
|
||||
post = frontmatter.load(str(absolute_path))
|
||||
# TODO move to api endpoint to check if absolute path was requested
|
||||
# Check if the path is already absolute
|
||||
if (
|
||||
isinstance(path, Path)
|
||||
and path.is_absolute()
|
||||
or (isinstance(path, str) and Path(path).is_absolute())
|
||||
):
|
||||
absolute_path = Path(path)
|
||||
else:
|
||||
absolute_path = self.base_path / path
|
||||
|
||||
# Parse frontmatter and content using python-frontmatter
|
||||
file_content = absolute_path.read_text()
|
||||
return await self.parse_file_content(absolute_path, file_content)
|
||||
|
||||
async def parse_file_content(self, absolute_path, file_content):
|
||||
post = frontmatter.loads(file_content)
|
||||
# Extract file stat info
|
||||
file_stats = absolute_path.stat()
|
||||
|
||||
metadata = post.metadata
|
||||
metadata["title"] = post.metadata.get("title", absolute_path.name)
|
||||
metadata["title"] = post.metadata.get("title", absolute_path.stem)
|
||||
metadata["type"] = post.metadata.get("type", "note")
|
||||
metadata["tags"] = parse_tags(post.metadata.get("tags", []))
|
||||
|
||||
tags = parse_tags(post.metadata.get("tags", [])) # pyright: ignore
|
||||
if tags:
|
||||
metadata["tags"] = tags
|
||||
# frontmatter
|
||||
entity_frontmatter = EntityFrontmatter(
|
||||
metadata=post.metadata,
|
||||
)
|
||||
|
||||
entity_content = parse(post.content)
|
||||
|
||||
return EntityMarkdown(
|
||||
frontmatter=entity_frontmatter,
|
||||
content=post.content,
|
||||
|
||||
@@ -83,7 +83,7 @@ class MarkdownProcessor:
|
||||
"""
|
||||
# Dirty check if needed
|
||||
if expected_checksum is not None:
|
||||
current_content = path.read_text()
|
||||
current_content = path.read_text(encoding="utf-8")
|
||||
current_checksum = await file_utils.compute_checksum(current_content)
|
||||
if current_checksum != expected_checksum:
|
||||
raise DirtyFileError(f"File {path} has been modified")
|
||||
|
||||
@@ -42,7 +42,7 @@ class EntityFrontmatter(BaseModel):
|
||||
|
||||
@property
|
||||
def tags(self) -> List[str]:
|
||||
return self.metadata.get("tags") if self.metadata else [] # pyright: ignore
|
||||
return self.metadata.get("tags") if self.metadata else None # pyright: ignore
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Utilities for converting between markdown and entity models."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from basic_memory.file_utils import has_frontmatter, remove_frontmatter
|
||||
from basic_memory.file_utils import has_frontmatter, remove_frontmatter, parse_frontmatter
|
||||
from basic_memory.markdown import EntityMarkdown
|
||||
from basic_memory.models import Entity, Observation as ObservationModel
|
||||
from basic_memory.utils import generate_permalink
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.models import Observation as ObservationModel
|
||||
|
||||
|
||||
def entity_model_from_markdown(
|
||||
@@ -32,16 +32,13 @@ def entity_model_from_markdown(
|
||||
if not markdown.created or not markdown.modified: # pragma: no cover
|
||||
raise ValueError("Both created and modified dates are required in markdown")
|
||||
|
||||
# Generate permalink if not provided
|
||||
permalink = markdown.frontmatter.permalink or generate_permalink(file_path)
|
||||
|
||||
# Create or update entity
|
||||
model = entity or Entity()
|
||||
|
||||
# Update basic fields
|
||||
model.title = markdown.frontmatter.title
|
||||
model.entity_type = markdown.frontmatter.type
|
||||
model.permalink = permalink
|
||||
model.permalink = markdown.frontmatter.permalink
|
||||
model.file_path = str(file_path)
|
||||
model.content_type = "text/markdown"
|
||||
model.created_at = markdown.created
|
||||
@@ -77,22 +74,33 @@ async def schema_to_markdown(schema: Any) -> Post:
|
||||
"""
|
||||
# Extract content and metadata
|
||||
content = schema.content or ""
|
||||
frontmatter_metadata = dict(schema.entity_metadata or {})
|
||||
entity_metadata = dict(schema.entity_metadata or {})
|
||||
|
||||
# if the content contains frontmatter, remove it and merge
|
||||
if has_frontmatter(content):
|
||||
content_frontmatter = parse_frontmatter(content)
|
||||
content = remove_frontmatter(content)
|
||||
|
||||
# Merge content frontmatter with entity metadata
|
||||
# (entity_metadata takes precedence for conflicts)
|
||||
content_frontmatter.update(entity_metadata)
|
||||
entity_metadata = content_frontmatter
|
||||
|
||||
# Remove special fields for ordered frontmatter
|
||||
for field in ["type", "title", "permalink"]:
|
||||
frontmatter_metadata.pop(field, None)
|
||||
entity_metadata.pop(field, None)
|
||||
|
||||
# Create Post with ordered fields
|
||||
# Create Post with fields ordered by insert order
|
||||
post = Post(
|
||||
content,
|
||||
title=schema.title,
|
||||
type=schema.entity_type,
|
||||
permalink=schema.permalink,
|
||||
**frontmatter_metadata,
|
||||
)
|
||||
# set the permalink if passed in
|
||||
if schema.permalink:
|
||||
post.metadata["permalink"] = schema.permalink
|
||||
|
||||
if entity_metadata:
|
||||
post.metadata.update(entity_metadata)
|
||||
|
||||
return post
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.mcp.server import mcp
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
@@ -20,7 +19,7 @@ def ai_assistant_guide() -> str:
|
||||
A focused guide on Basic Memory usage.
|
||||
"""
|
||||
logger.info("Loading AI assistant guide resource")
|
||||
guide_doc = Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
|
||||
content = guide_doc.read_text()
|
||||
guide_doc = Path(__file__).parent.parent / "resources" / "ai_assistant_guide.md"
|
||||
content = guide_doc.read_text(encoding="utf-8")
|
||||
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
|
||||
return content
|
||||
|
||||
@@ -5,19 +5,19 @@ providing context from previous interactions to maintain continuity.
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Optional, Annotated
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import Field
|
||||
|
||||
from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
|
||||
from basic_memory.mcp.prompts.utils import PromptContext, PromptContextItem, format_prompt_context
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.build_context import build_context
|
||||
from basic_memory.mcp.tools.recent_activity import recent_activity
|
||||
from basic_memory.mcp.tools.search import search
|
||||
from basic_memory.mcp.tools.search import search_notes
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.memory import GraphContext
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
|
||||
|
||||
@mcp.prompt(
|
||||
@@ -47,8 +47,8 @@ async def continue_conversation(
|
||||
|
||||
# If topic provided, search for it
|
||||
if topic:
|
||||
search_results = await search(
|
||||
SearchQuery(text=topic, after_date=timeframe, types=[SearchItemType.ENTITY])
|
||||
search_results = await search_notes(
|
||||
query=topic, after_date=timeframe, entity_types=[SearchItemType.ENTITY]
|
||||
)
|
||||
|
||||
# Build context from results
|
||||
@@ -93,7 +93,7 @@ async def continue_conversation(
|
||||
## Next Steps
|
||||
|
||||
You can:
|
||||
- Explore more with: `search({{"text": "{topic}"}})`
|
||||
- Explore more with: `search_notes({{"text": "{topic}"}})`
|
||||
- See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
|
||||
- **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from loguru import logger
|
||||
from pydantic import Field
|
||||
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.search import search as search_tool
|
||||
from basic_memory.mcp.tools.search import search_notes as search_tool
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
||||
from basic_memory.schemas.search import SearchResponse
|
||||
|
||||
|
||||
@mcp.prompt(
|
||||
@@ -40,7 +40,7 @@ async def search_prompt(
|
||||
"""
|
||||
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
||||
|
||||
search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
|
||||
search_results = await search_tool(query=query, after_date=timeframe)
|
||||
return format_search_results(query, search_results, timeframe)
|
||||
|
||||
|
||||
@@ -144,9 +144,9 @@ def format_search_results(
|
||||
## Next Steps
|
||||
|
||||
You can:
|
||||
- Refine your search: `search("{query} AND additional_term")`
|
||||
- Exclude terms: `search("{query} NOT exclude_term")`
|
||||
- View more results: `search("{query}", after_date=None)`
|
||||
- Refine your search: `search_notes("{query} AND additional_term")`
|
||||
- Exclude terms: `search_notes("{query} NOT exclude_term")`
|
||||
- View more results: `search_notes("{query}", after_date=None)`
|
||||
- Check recent activity: `recent_activity()`
|
||||
|
||||
## Synthesize and Capture Knowledge
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
# AI Assistant Guide for Basic Memory
|
||||
|
||||
This guide helps AIs 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:
|
||||
|
||||
1. **Increase Semantic Density**: Add multiple observations and relations to each note
|
||||
2. **Use Accurate References**: Aim to reference existing entities by their exact titles
|
||||
3. **Create Forward References**: Feel free to reference entities that don't exist yet - Basic Memory will resolve these
|
||||
when they're created later
|
||||
4. **Create Bidirectional Links**: When appropriate, connect entities from both directions
|
||||
5. **Use Meaningful Categories**: Add semantic context with appropriate observation categories
|
||||
6. **Choose Precise Relations**: Use specific relation types that convey meaning
|
||||
|
||||
Remember: 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_notes(
|
||||
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_notes() 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_notes("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_notes("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_notes()` or `recent_activity()` to confirm entity titles
|
||||
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
|
||||
of "relates_to")
|
||||
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|
||||
|
||||
3. **Structure Content Thoughtfully**
|
||||
- Use clear, descriptive titles
|
||||
- Organize with logical sections (Context, Decision, Implementation, etc.)
|
||||
- Include relevant context and background
|
||||
- Add semantic observations with appropriate categories
|
||||
- Use a consistent format for similar types of notes
|
||||
- Balance detail with conciseness
|
||||
|
||||
4. **Navigate Knowledge Effectively**
|
||||
- Start with specific searches
|
||||
- Follow relation paths
|
||||
- Combine information from multiple sources
|
||||
- Verify information is current
|
||||
- Build a complete picture before responding
|
||||
|
||||
5. **Help Users Maintain Their Knowledge**
|
||||
- Suggest organizing related topics
|
||||
- Identify potential duplicates
|
||||
- Recommend adding relations between topics
|
||||
- Offer to create summaries of scattered information
|
||||
- Suggest potential missing relations: "I notice this might relate to [topic], would you like me to add that
|
||||
connection?"
|
||||
|
||||
Built with ♥️ b
|
||||
y Basic Machines
|
||||
@@ -4,8 +4,8 @@ from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.fastmcp.utilities.logging import configure_logging
|
||||
|
||||
# mcp console logging
|
||||
configure_logging(level="INFO")
|
||||
configure_logging(level="ERROR")
|
||||
|
||||
|
||||
# Create the shared server instance
|
||||
mcp = FastMCP("Basic Memory")
|
||||
mcp = FastMCP("Basic Memory", log_level="ERROR")
|
||||
|
||||
@@ -12,7 +12,7 @@ from basic_memory.mcp.tools.build_context import build_context
|
||||
from basic_memory.mcp.tools.recent_activity import recent_activity
|
||||
from basic_memory.mcp.tools.read_note import read_note
|
||||
from basic_memory.mcp.tools.write_note import write_note
|
||||
from basic_memory.mcp.tools.search import search
|
||||
from basic_memory.mcp.tools.search import search_notes
|
||||
from basic_memory.mcp.tools.canvas import canvas
|
||||
|
||||
__all__ = [
|
||||
@@ -22,6 +22,6 @@ __all__ = [
|
||||
"read_content",
|
||||
"read_note",
|
||||
"recent_activity",
|
||||
"search",
|
||||
"search_notes",
|
||||
"write_note",
|
||||
]
|
||||
|
||||
@@ -6,10 +6,9 @@ from loguru import logger
|
||||
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.search import search
|
||||
from basic_memory.mcp.tools.search import search_notes
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.schemas.memory import memory_url_path
|
||||
from basic_memory.schemas.search import SearchQuery
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
@@ -63,7 +62,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
||||
|
||||
# Fallback 1: Try title search via API
|
||||
logger.info(f"Search title for: {identifier}")
|
||||
title_results = await search(SearchQuery(title=identifier))
|
||||
title_results = await search_notes(query=identifier, search_type="title")
|
||||
|
||||
if title_results and title_results.results:
|
||||
result = title_results.results[0] # Get the first/best match
|
||||
@@ -87,7 +86,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
||||
|
||||
# Fallback 2: Text search as a last resort
|
||||
logger.info(f"Title search failed, trying text search for: {identifier}")
|
||||
text_results = await search(SearchQuery(text=identifier))
|
||||
text_results = await search_notes(query=identifier, search_type="text")
|
||||
|
||||
# We didn't find a direct match, construct a helpful error message
|
||||
if not text_results or not text_results.results:
|
||||
|
||||
@@ -1,77 +1,113 @@
|
||||
"""Search tools for Basic Memory MCP server."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.utils import call_post
|
||||
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
||||
from basic_memory.mcp.async_client import client
|
||||
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Search across all content in basic-memory, including documents and entities",
|
||||
description="Search across all content in the knowledge base.",
|
||||
)
|
||||
async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
|
||||
"""Search across all content in basic-memory.
|
||||
async def search_notes(
|
||||
query: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
search_type: str = "text",
|
||||
types: Optional[List[str]] = None,
|
||||
entity_types: Optional[List[str]] = None,
|
||||
after_date: Optional[str] = None,
|
||||
) -> SearchResponse:
|
||||
"""Search across all content in the knowledge base.
|
||||
|
||||
This tool searches the knowledge base using full-text search, pattern matching,
|
||||
or exact permalink lookup. It supports filtering by content type, entity type,
|
||||
and date.
|
||||
|
||||
Args:
|
||||
query: SearchQuery object with search parameters including:
|
||||
- text: Full-text search (e.g., "project planning")
|
||||
Supports boolean operators: AND, OR, NOT and parentheses for grouping
|
||||
- title: Search only in titles (e.g., "Meeting notes")
|
||||
- permalink: Exact permalink match (e.g., "docs/meeting-notes")
|
||||
- permalink_match: Pattern matching for permalinks (e.g., "docs/*-notes")
|
||||
- types: Optional list of content types to search (e.g., ["entity", "observation"])
|
||||
- entity_types: Optional list of entity types to filter by (e.g., ["note", "person"])
|
||||
- after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
||||
query: The search query string
|
||||
page: The page number of results to return (default 1)
|
||||
page_size: The number of results to return per page (default 10)
|
||||
search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
|
||||
types: Optional list of note types to search (e.g., ["note", "person"])
|
||||
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
|
||||
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
||||
|
||||
Returns:
|
||||
SearchResponse with results and pagination info
|
||||
|
||||
Examples:
|
||||
# Basic text search
|
||||
results = await search(SearchQuery(text="project planning"))
|
||||
results = await search_notes("project planning")
|
||||
|
||||
# Boolean AND search (both terms must be present)
|
||||
results = await search(SearchQuery(text="project AND planning"))
|
||||
results = await search_notes("project AND planning")
|
||||
|
||||
# Boolean OR search (either term can be present)
|
||||
results = await search(SearchQuery(text="project OR meeting"))
|
||||
results = await search_notes("project OR meeting")
|
||||
|
||||
# Boolean NOT search (exclude terms)
|
||||
results = await search(SearchQuery(text="project NOT meeting"))
|
||||
results = await search_notes("project NOT meeting")
|
||||
|
||||
# Boolean search with grouping
|
||||
results = await search(SearchQuery(text="(project OR planning) AND notes"))
|
||||
results = await search_notes("(project OR planning) AND notes")
|
||||
|
||||
# Search with type filter
|
||||
results = await search(SearchQuery(
|
||||
text="meeting notes",
|
||||
results = await search_notes(
|
||||
query="meeting notes",
|
||||
types=["entity"],
|
||||
))
|
||||
)
|
||||
|
||||
# Search with entity type filter, e.g., note vs
|
||||
results = await search_notes(
|
||||
query="meeting notes",
|
||||
types=["entity"],
|
||||
)
|
||||
|
||||
# Search for recent content
|
||||
results = await search(SearchQuery(
|
||||
text="bug report",
|
||||
results = await search_notes(
|
||||
query="bug report",
|
||||
after_date="1 week"
|
||||
))
|
||||
)
|
||||
|
||||
# Pattern matching on permalinks
|
||||
results = await search(SearchQuery(
|
||||
permalink_match="docs/meeting-*"
|
||||
))
|
||||
results = await search_notes(
|
||||
query="docs/meeting-*",
|
||||
search_type="permalink"
|
||||
)
|
||||
"""
|
||||
logger.info(f"Searching for {query}")
|
||||
# Create a SearchQuery object based on the parameters
|
||||
search_query = SearchQuery()
|
||||
|
||||
# Set the appropriate search field based on search_type
|
||||
if search_type == "text":
|
||||
search_query.text = query
|
||||
elif search_type == "title":
|
||||
search_query.title = query
|
||||
elif search_type == "permalink" and "*" in query:
|
||||
search_query.permalink_match = query
|
||||
elif search_type == "permalink":
|
||||
search_query.permalink = query
|
||||
else:
|
||||
search_query.text = query # Default to text search
|
||||
|
||||
# Add optional filters if provided
|
||||
if entity_types:
|
||||
search_query.entity_types = [SearchItemType(t) for t in entity_types]
|
||||
if types:
|
||||
search_query.types = types
|
||||
if after_date:
|
||||
search_query.after_date = after_date
|
||||
|
||||
logger.info(f"Searching for {search_query}")
|
||||
response = await call_post(
|
||||
client,
|
||||
"/search/",
|
||||
json=query.model_dump(),
|
||||
json=search_query.model_dump(),
|
||||
params={"page": page, "page_size": page_size},
|
||||
)
|
||||
return SearchResponse.model_validate(response.json())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Write note tool for Basic Memory MCP server."""
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import List, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -9,6 +9,13 @@ from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.utils import call_put
|
||||
from basic_memory.schemas import EntityResponse
|
||||
from basic_memory.schemas.base import Entity
|
||||
from basic_memory.utils import parse_tags
|
||||
|
||||
# Define TagType as a Union that can accept either a string or a list of strings or None
|
||||
TagType = Union[List[str], str, None]
|
||||
|
||||
# Define TagType as a Union that can accept either a string or a list of strings or None
|
||||
TagType = Union[List[str], str, None]
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
@@ -18,7 +25,7 @@ async def write_note(
|
||||
title: str,
|
||||
content: str,
|
||||
folder: str,
|
||||
tags: Optional[List[str]] = None,
|
||||
tags=None, # Remove type hint completely to avoid schema issues
|
||||
) -> str:
|
||||
"""Write a markdown note to the knowledge base.
|
||||
|
||||
@@ -40,13 +47,14 @@ async def write_note(
|
||||
Examples:
|
||||
`- depends_on [[Content Parser]] (Need for semantic extraction)`
|
||||
`- implements [[Search Spec]] (Initial implementation)`
|
||||
`- This feature extends [[Base Design]] and uses [[Core Utils]]`
|
||||
`- This feature extends [[Base Design]] andst uses [[Core Utils]]`
|
||||
|
||||
Args:
|
||||
title: The title of the note
|
||||
content: Markdown content for the note, can include observations and relations
|
||||
folder: the folder where the file should be saved
|
||||
tags: Optional list of tags to categorize the note
|
||||
tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
|
||||
Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
|
||||
|
||||
Returns:
|
||||
A markdown formatted summary of the semantic content, including:
|
||||
@@ -58,8 +66,10 @@ async def write_note(
|
||||
"""
|
||||
logger.info("MCP tool call", tool="write_note", folder=folder, title=title, tags=tags)
|
||||
|
||||
# Process tags using the helper function
|
||||
tag_list = parse_tags(tags)
|
||||
# Create the entity request
|
||||
metadata = {"tags": [f"#{tag}" for tag in tags]} if tags else None
|
||||
metadata = {"tags": [f"#{tag}" for tag in tag_list]} if tag_list else None
|
||||
entity = Entity(
|
||||
title=title,
|
||||
folder=folder,
|
||||
@@ -78,8 +88,10 @@ async def write_note(
|
||||
# Format semantic summary based on status code
|
||||
action = "Created" if response.status_code == 201 else "Updated"
|
||||
summary = [
|
||||
f"# {action} {result.file_path} ({result.checksum[:8] if result.checksum else 'unknown'})",
|
||||
f"# {action} note",
|
||||
f"file_path: {result.file_path}",
|
||||
f"permalink: {result.permalink}",
|
||||
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
|
||||
]
|
||||
|
||||
# Count observations by category
|
||||
@@ -105,8 +117,8 @@ async def write_note(
|
||||
summary.append(f"- Unresolved: {unresolved}")
|
||||
summary.append("\nUnresolved relations will be retried on next sync.")
|
||||
|
||||
if tags:
|
||||
summary.append(f"\n## Tags\n- {', '.join(tags)}")
|
||||
if tag_list:
|
||||
summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
|
||||
|
||||
# Log the response with structured data
|
||||
logger.info(
|
||||
|
||||
@@ -4,10 +4,10 @@ import json
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy import text, Executable, Result
|
||||
from sqlalchemy import Executable, Result, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from basic_memory import db
|
||||
@@ -123,9 +123,9 @@ class SearchRepository:
|
||||
permalink: Optional[str] = None,
|
||||
permalink_match: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
types: Optional[List[SearchItemType]] = None,
|
||||
types: Optional[List[str]] = None,
|
||||
after_date: Optional[datetime] = None,
|
||||
entity_types: Optional[List[str]] = None,
|
||||
entity_types: Optional[List[SearchItemType]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> List[SearchIndexRow]:
|
||||
@@ -174,15 +174,15 @@ class SearchRepository:
|
||||
else:
|
||||
conditions.append("permalink MATCH :permalink")
|
||||
|
||||
# Handle type filter
|
||||
if types:
|
||||
type_list = ", ".join(f"'{t.value}'" for t in types)
|
||||
conditions.append(f"type IN ({type_list})")
|
||||
|
||||
# Handle entity type filter
|
||||
if entity_types:
|
||||
entity_type_list = ", ".join(f"'{t}'" for t in entity_types)
|
||||
conditions.append(f"json_extract(metadata, '$.entity_type') IN ({entity_type_list})")
|
||||
type_list = ", ".join(f"'{t.value}'" for t in entity_types)
|
||||
conditions.append(f"type IN ({type_list})")
|
||||
|
||||
# Handle type filter
|
||||
if types:
|
||||
type_list = ", ".join(f"'{t}'" for t in types)
|
||||
conditions.append(f"json_extract(metadata, '$.entity_type') IN ({type_list})")
|
||||
|
||||
# Handle date filter using datetime() for proper comparison
|
||||
if after_date:
|
||||
|
||||
@@ -49,8 +49,8 @@ class SearchQuery(BaseModel):
|
||||
title: Optional[str] = None # title only search
|
||||
|
||||
# Optional filters
|
||||
types: Optional[List[SearchItemType]] = None # Filter by item type
|
||||
entity_types: Optional[List[str]] = None # Filter by entity type
|
||||
types: Optional[List[str]] = None # Filter by type
|
||||
entity_types: Optional[List[SearchItemType]] = None # Filter by entity type
|
||||
after_date: Optional[Union[datetime, str]] = None # Time-based filter
|
||||
|
||||
@field_validator("after_date")
|
||||
|
||||
@@ -81,7 +81,7 @@ class ContextService:
|
||||
else:
|
||||
logger.debug(f"Build context for '{types}'")
|
||||
primary = await self.search_repository.search(
|
||||
types=types, after_date=since, limit=limit, offset=offset
|
||||
entity_types=types, after_date=since, limit=limit, offset=offset
|
||||
)
|
||||
|
||||
# Get type_id pairs for traversal
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
"""Service for managing entities in the database."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Sequence, List, Optional, Tuple, Union
|
||||
from typing import List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import frontmatter
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from basic_memory.markdown import EntityMarkdown
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.markdown.utils import entity_model_from_markdown, schema_to_markdown
|
||||
from basic_memory.models import Entity as EntityModel, Observation, Relation
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.models import Observation, Relation
|
||||
from basic_memory.repository import ObservationRepository, RelationRepository
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.schemas.base import Permalink
|
||||
from basic_memory.services.exceptions import EntityNotFoundError, EntityCreationError
|
||||
from basic_memory.services import FileService
|
||||
from basic_memory.services import BaseService
|
||||
from basic_memory.services import BaseService, FileService
|
||||
from basic_memory.services.exceptions import EntityCreationError, EntityNotFoundError
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
logger.debug(f"Creating or updating entity: {schema}")
|
||||
|
||||
# Try to find existing entity using smart resolution
|
||||
existing = await self.link_resolver.resolve_link(schema.permalink)
|
||||
existing = await self.link_resolver.resolve_link(schema.permalink or schema.file_path)
|
||||
|
||||
if existing:
|
||||
logger.debug(f"Found existing entity: {existing.permalink}")
|
||||
@@ -100,7 +100,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
async def create_entity(self, schema: EntitySchema) -> EntityModel:
|
||||
"""Create a new entity and write to filesystem."""
|
||||
logger.debug(f"Creating entity: {schema.permalink}")
|
||||
logger.debug(f"Creating entity: {schema.title}")
|
||||
|
||||
# Get file path and ensure it's a Path object
|
||||
file_path = Path(schema.file_path)
|
||||
@@ -141,10 +141,20 @@ class EntityService(BaseService[EntityModel]):
|
||||
# Convert file path string to Path
|
||||
file_path = Path(entity.file_path)
|
||||
|
||||
# Read existing frontmatter from the file if it exists
|
||||
existing_markdown = await self.entity_parser.parse_file(file_path)
|
||||
|
||||
# Create post with new content from schema
|
||||
post = await schema_to_markdown(schema)
|
||||
|
||||
# Merge new metadata with existing metadata
|
||||
existing_markdown.frontmatter.metadata.update(post.metadata)
|
||||
|
||||
# Create a new post with merged metadata
|
||||
merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata)
|
||||
|
||||
# write file
|
||||
final_content = frontmatter.dumps(post, sort_keys=False)
|
||||
final_content = frontmatter.dumps(merged_post, sort_keys=False)
|
||||
checksum = await self.file_service.write_file(file_path, final_content)
|
||||
|
||||
# parse entity from file
|
||||
@@ -220,7 +230,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
Creates the entity with null checksum to indicate sync not complete.
|
||||
Relations will be added in second pass.
|
||||
"""
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title}")
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title} file_path: {file_path}")
|
||||
model = entity_model_from_markdown(file_path, markdown)
|
||||
|
||||
# Mark as incomplete because we still need to add relations
|
||||
@@ -305,7 +315,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
except IntegrityError:
|
||||
# Unique constraint violation - relation already exists
|
||||
logger.debug(
|
||||
f"Skipping duplicate relation {rel.type} from {db_entity.permalink} target: {rel.target}, type: {rel.type}"
|
||||
f"Skipping duplicate relation {rel.type} from {db_entity.permalink} target: {rel.target}"
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ from os import stat_result
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import file_utils
|
||||
from basic_memory.file_utils import FileError
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
@@ -14,6 +12,7 @@ from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.services.exceptions import FileOperationError
|
||||
from basic_memory.utils import FilePath
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class FileService:
|
||||
@@ -171,8 +170,7 @@ class FileService:
|
||||
|
||||
try:
|
||||
logger.debug("Reading file", operation="read_file", path=str(full_path))
|
||||
|
||||
content = full_path.read_text()
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
checksum = await file_utils.compute_checksum(content)
|
||||
|
||||
logger.debug(
|
||||
@@ -236,7 +234,7 @@ class FileService:
|
||||
try:
|
||||
if self.is_markdown(path):
|
||||
# read str
|
||||
content = full_path.read_text()
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
# read bytes
|
||||
content = full_path.read_bytes()
|
||||
|
||||
@@ -46,10 +46,17 @@ class LinkResolver:
|
||||
logger.debug(f"Found title match: {entity.title}")
|
||||
return entity
|
||||
|
||||
# 3. Try file path
|
||||
found_path = await self.entity_repository.get_by_file_path(clean_text)
|
||||
if found_path:
|
||||
logger.debug(f"Found entity with path: {found_path.file_path}")
|
||||
return found_path
|
||||
|
||||
# search if indicated
|
||||
if use_search and "*" not in clean_text:
|
||||
# 3. Fall back to search for fuzzy matching on title
|
||||
results = await self.search_service.search(
|
||||
query=SearchQuery(title=clean_text, types=[SearchItemType.ENTITY]),
|
||||
query=SearchQuery(title=clean_text, entity_types=[SearchItemType.ENTITY]),
|
||||
)
|
||||
|
||||
if results:
|
||||
|
||||
@@ -181,17 +181,6 @@ class SearchService:
|
||||
Each type gets its own row in the search index with appropriate metadata.
|
||||
"""
|
||||
|
||||
if entity.permalink is None: # pragma: no cover
|
||||
logger.error(
|
||||
"Missing permalink for markdown entity",
|
||||
entity_id=entity.id,
|
||||
title=entity.title,
|
||||
file_path=entity.file_path,
|
||||
)
|
||||
raise ValueError(
|
||||
f"Entity permalink should not be None for markdown entity: {entity.id} ({entity.title})"
|
||||
)
|
||||
|
||||
content_stems = []
|
||||
content_snippet = ""
|
||||
title_variants = self._generate_variants(entity.title)
|
||||
@@ -202,22 +191,13 @@ class SearchService:
|
||||
content_stems.append(content)
|
||||
content_snippet = f"{content[:250]}"
|
||||
|
||||
content_stems.extend(self._generate_variants(entity.permalink))
|
||||
if entity.permalink:
|
||||
content_stems.extend(self._generate_variants(entity.permalink))
|
||||
|
||||
content_stems.extend(self._generate_variants(entity.file_path))
|
||||
|
||||
entity_content_stems = "\n".join(p for p in content_stems if p and p.strip())
|
||||
|
||||
if entity.permalink is None: # pragma: no cover
|
||||
logger.error(
|
||||
"Missing permalink for markdown entity",
|
||||
entity_id=entity.id,
|
||||
title=entity.title,
|
||||
file_path=entity.file_path,
|
||||
)
|
||||
raise ValueError(
|
||||
f"Entity permalink should not be None for markdown entity: {entity.id} ({entity.title})"
|
||||
)
|
||||
|
||||
# Index entity
|
||||
await self.repository.index_item(
|
||||
SearchIndexRow(
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing import Dict, Optional, Set, Tuple
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.file_utils import has_frontmatter
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.repository import EntityRepository, RelationRepository
|
||||
@@ -65,6 +67,7 @@ class SyncService:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
entity_parser: EntityParser,
|
||||
entity_repository: EntityRepository,
|
||||
@@ -72,6 +75,7 @@ class SyncService:
|
||||
search_service: SearchService,
|
||||
file_service: FileService,
|
||||
):
|
||||
self.config = config
|
||||
self.entity_service = entity_service
|
||||
self.entity_parser = entity_parser
|
||||
self.entity_repository = entity_repository
|
||||
@@ -327,43 +331,52 @@ class SyncService:
|
||||
"""
|
||||
# Parse markdown first to get any existing permalink
|
||||
logger.debug("Parsing markdown file", path=path)
|
||||
|
||||
file_path = self.entity_parser.base_path / path
|
||||
file_content = file_path.read_text()
|
||||
file_contains_frontmatter = has_frontmatter(file_content)
|
||||
|
||||
# entity markdown will always contain front matter, so it can be used up create/update the entity
|
||||
entity_markdown = await self.entity_parser.parse_file(path)
|
||||
|
||||
# Resolve permalink - this handles all the cases including conflicts
|
||||
permalink = await self.entity_service.resolve_permalink(path, markdown=entity_markdown)
|
||||
# if the file contains frontmatter, resolve a permalink
|
||||
if file_contains_frontmatter:
|
||||
# Resolve permalink - this handles all the cases including conflicts
|
||||
permalink = await self.entity_service.resolve_permalink(path, markdown=entity_markdown)
|
||||
|
||||
# If permalink changed, update the file
|
||||
if permalink != entity_markdown.frontmatter.permalink:
|
||||
logger.info(
|
||||
"Updating permalink",
|
||||
path=path,
|
||||
old_permalink=entity_markdown.frontmatter.permalink,
|
||||
new_permalink=permalink,
|
||||
)
|
||||
# If permalink changed, update the file
|
||||
if permalink != entity_markdown.frontmatter.permalink:
|
||||
logger.info(
|
||||
"Updating permalink",
|
||||
path=path,
|
||||
old_permalink=entity_markdown.frontmatter.permalink,
|
||||
new_permalink=permalink,
|
||||
)
|
||||
|
||||
entity_markdown.frontmatter.metadata["permalink"] = permalink
|
||||
checksum = await self.file_service.update_frontmatter(path, {"permalink": permalink})
|
||||
else:
|
||||
checksum = await self.file_service.compute_checksum(path)
|
||||
entity_markdown.frontmatter.metadata["permalink"] = permalink
|
||||
await self.file_service.update_frontmatter(path, {"permalink": permalink})
|
||||
|
||||
# if the file is new, create an entity
|
||||
if new:
|
||||
# Create entity with final permalink
|
||||
logger.debug("Creating new entity from markdown", path=path, permalink=permalink)
|
||||
|
||||
logger.debug("Creating new entity from markdown", path=path)
|
||||
await self.entity_service.create_entity_from_markdown(Path(path), entity_markdown)
|
||||
|
||||
# otherwise we need to update the entity and observations
|
||||
else:
|
||||
logger.debug("Updating entity from markdown", path=path, permalink=permalink)
|
||||
|
||||
logger.debug("Updating entity from markdown", path=path)
|
||||
await self.entity_service.update_entity_and_observations(Path(path), entity_markdown)
|
||||
|
||||
# Update relations and search index
|
||||
entity = await self.entity_service.update_entity_relations(path, entity_markdown)
|
||||
|
||||
# After updating relations, we need to compute the checksum again
|
||||
# This is necessary for files with wikilinks to ensure consistent checksums
|
||||
# after relation processing is complete
|
||||
final_checksum = await self.file_service.compute_checksum(path)
|
||||
|
||||
# set checksum
|
||||
await self.entity_repository.update(entity.id, {"checksum": checksum})
|
||||
await self.entity_repository.update(entity.id, {"checksum": final_checksum})
|
||||
|
||||
logger.debug(
|
||||
"Markdown sync completed",
|
||||
@@ -371,9 +384,11 @@ class SyncService:
|
||||
entity_id=entity.id,
|
||||
observation_count=len(entity.observations),
|
||||
relation_count=len(entity.relations),
|
||||
checksum=final_checksum,
|
||||
)
|
||||
|
||||
return entity, checksum
|
||||
# Return the final checksum to ensure everything is consistent
|
||||
return entity, final_checksum
|
||||
|
||||
async def sync_regular_file(self, path: str, new: bool = True) -> Tuple[Optional[Entity], str]:
|
||||
"""Sync a non-markdown file with basic tracking.
|
||||
@@ -468,8 +483,30 @@ class SyncService:
|
||||
|
||||
entity = await self.entity_repository.get_by_file_path(old_path)
|
||||
if entity:
|
||||
# Update file_path but keep the same permalink for link stability
|
||||
updated = await self.entity_repository.update(entity.id, {"file_path": new_path})
|
||||
# Update file_path in all cases
|
||||
updates = {"file_path": new_path}
|
||||
|
||||
# If configured, also update permalink to match new path
|
||||
if self.config.update_permalinks_on_move:
|
||||
# generate new permalink value
|
||||
new_permalink = await self.entity_service.resolve_permalink(new_path)
|
||||
|
||||
# write to file and get new checksum
|
||||
new_checksum = await self.file_service.update_frontmatter(
|
||||
new_path, {"permalink": new_permalink}
|
||||
)
|
||||
|
||||
updates["permalink"] = new_permalink
|
||||
updates["checksum"] = new_checksum
|
||||
|
||||
logger.info(
|
||||
"Updating permalink on move",
|
||||
old_permalink=entity.permalink,
|
||||
new_permalink=new_permalink,
|
||||
new_checksum=new_checksum,
|
||||
)
|
||||
|
||||
updated = await self.entity_repository.update(entity.id, updates)
|
||||
|
||||
if updated is None: # pragma: no cover
|
||||
logger.error(
|
||||
|
||||
@@ -5,16 +5,15 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.services.file_service import FileService
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from rich.console import Console
|
||||
from watchfiles import awatch
|
||||
from watchfiles.main import FileChange, Change
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.services.file_service import FileService
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
WATCH_STATUS_JSON = "watch-status.json"
|
||||
|
||||
|
||||
@@ -138,6 +137,10 @@ class WatchService:
|
||||
if part.startswith("."):
|
||||
return False
|
||||
|
||||
# Skip temp files used in atomic operations
|
||||
if path.endswith(".tmp"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def write_status(self):
|
||||
@@ -161,6 +164,11 @@ class WatchService:
|
||||
for change, path in changes:
|
||||
# convert to relative path
|
||||
relative_path = str(Path(path).relative_to(directory))
|
||||
|
||||
# Skip .tmp files - they're temporary and shouldn't be synced
|
||||
if relative_path.endswith(".tmp"):
|
||||
continue
|
||||
|
||||
if change == Change.added:
|
||||
adds.append(relative_path)
|
||||
elif change == Change.deleted:
|
||||
@@ -184,8 +192,8 @@ class WatchService:
|
||||
# We don't need to process directories, only the files inside them
|
||||
# This prevents errors when trying to compute checksums or read directories as files
|
||||
added_full_path = directory / added_path
|
||||
if added_full_path.is_dir():
|
||||
logger.debug("Skipping directory for move detection", path=added_path)
|
||||
if not added_full_path.exists() or added_full_path.is_dir():
|
||||
logger.debug("Skipping non-existent or directory path", path=added_path)
|
||||
processed.add(added_path)
|
||||
continue
|
||||
|
||||
@@ -247,10 +255,12 @@ class WatchService:
|
||||
if path not in processed:
|
||||
# Skip directories - only process files
|
||||
full_path = directory / path
|
||||
if full_path.is_dir(): # pragma: no cover
|
||||
logger.debug("Skipping directory", path=path)
|
||||
processed.add(path)
|
||||
continue
|
||||
if not full_path.exists() or full_path.is_dir():
|
||||
logger.debug(
|
||||
"Skipping non-existent or directory path", path=path
|
||||
) # pragma: no cover
|
||||
processed.add(path) # pragma: no cover
|
||||
continue # pragma: no cover
|
||||
|
||||
logger.debug("Processing new file", path=path)
|
||||
entity, checksum = await self.sync_service.sync_file(path, new=True)
|
||||
@@ -281,8 +291,8 @@ class WatchService:
|
||||
if path not in processed:
|
||||
# Skip directories - only process files
|
||||
full_path = directory / path
|
||||
if full_path.is_dir():
|
||||
logger.debug("Skipping directory", path=path)
|
||||
if not full_path.exists() or full_path.is_dir():
|
||||
logger.debug("Skipping non-existent or directory path", path=path)
|
||||
processed.add(path)
|
||||
continue
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Protocol, Union, runtime_checkable
|
||||
from typing import Optional, Protocol, Union, runtime_checkable, List
|
||||
|
||||
from loguru import logger
|
||||
from unidecode import unidecode
|
||||
@@ -128,3 +128,37 @@ def setup_logging(
|
||||
# Set log levels for noisy loggers
|
||||
for logger_name, level in noisy_loggers.items():
|
||||
logging.getLogger(logger_name).setLevel(level)
|
||||
|
||||
|
||||
def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
||||
"""Parse tags from various input formats into a consistent list.
|
||||
|
||||
Args:
|
||||
tags: Can be a list of strings, a comma-separated string, or None
|
||||
|
||||
Returns:
|
||||
A list of tag strings, or an empty list if no tags
|
||||
|
||||
Note:
|
||||
This function strips leading '#' characters from tags to prevent
|
||||
their accumulation when tags are processed multiple times.
|
||||
"""
|
||||
if tags is None:
|
||||
return []
|
||||
|
||||
# Process list of tags
|
||||
if isinstance(tags, list):
|
||||
# First strip whitespace, then strip leading '#' characters to prevent accumulation
|
||||
return [tag.strip().lstrip("#") for tag in tags if tag and tag.strip()]
|
||||
|
||||
# Process comma-separated string of tags
|
||||
if isinstance(tags, str):
|
||||
# Split by comma, strip whitespace, then strip leading '#' characters
|
||||
return [tag.strip().lstrip("#") for tag in tags.split(",") if tag and tag.strip()]
|
||||
|
||||
# For any other type, try to convert to string and parse
|
||||
try: # pragma: no cover
|
||||
return parse_tags(str(tags))
|
||||
except (ValueError, TypeError): # pragma: no cover
|
||||
logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
|
||||
return []
|
||||
|
||||
@@ -49,7 +49,7 @@ 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(
|
||||
results = await search_notes(
|
||||
query="authentication system", # Text to search for
|
||||
page=1, # Optional: Pagination
|
||||
page_size=10 # Optional: Results per page
|
||||
@@ -154,7 +154,7 @@ Users will interact with Basic Memory in patterns like:
|
||||
Human: "What were our decisions about auth?"
|
||||
|
||||
You: Let me find that information for you.
|
||||
[Use search() to find relevant notes]
|
||||
[Use search_notes() to find relevant notes]
|
||||
[Then build_context() to understand connections]
|
||||
```
|
||||
|
||||
@@ -263,7 +263,7 @@ When creating relations, you can:
|
||||
# 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")
|
||||
search_results = await search_notes("travel")
|
||||
existing_entities = [result.title for result in search_results.primary_results]
|
||||
|
||||
# Check if specific entities exist
|
||||
@@ -335,7 +335,7 @@ Common issues to watch for:
|
||||
content = await read_note("Document")
|
||||
except:
|
||||
# Try search instead
|
||||
results = await search("Document")
|
||||
results = await search_notes("Document")
|
||||
if results and results.primary_results:
|
||||
# Found something similar
|
||||
content = await read_note(results.primary_results[0].permalink)
|
||||
@@ -381,7 +381,7 @@ Common issues to watch for:
|
||||
- **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
|
||||
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
|
||||
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
|
||||
of "relates_to")
|
||||
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|
||||
|
||||
@@ -251,7 +251,7 @@ async def test_entity_indexing(client: AsyncClient):
|
||||
|
||||
# Verify it's searchable
|
||||
search_response = await client.post(
|
||||
"/search/", json={"text": "search", "types": [SearchItemType.ENTITY.value]}
|
||||
"/search/", json={"text": "search", "entity_types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
assert search_response.status_code == 200
|
||||
search_result = SearchResponse.model_validate(search_response.json())
|
||||
@@ -279,7 +279,7 @@ async def test_entity_delete_indexing(client: AsyncClient):
|
||||
|
||||
# Verify it's initially searchable
|
||||
search_response = await client.post(
|
||||
"/search/", json={"text": "delete", "types": [SearchItemType.ENTITY.value]}
|
||||
"/search/", json={"text": "delete", "entity_types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
search_result = SearchResponse.model_validate(search_response.json())
|
||||
assert len(search_result.results) == 1
|
||||
@@ -475,7 +475,7 @@ async def test_update_entity_search_index(client: AsyncClient):
|
||||
|
||||
# Search should find new content
|
||||
search_response = await client.post(
|
||||
"/search/", json={"text": "sphinx marker", "types": [SearchItemType.ENTITY.value]}
|
||||
"/search/", json={"text": "sphinx marker", "entity_types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
results = search_response.json()["results"]
|
||||
assert len(results) == 1
|
||||
|
||||
@@ -48,11 +48,11 @@ async def test_search_basic_pagination(client, indexed_entity):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_type_filter(client, indexed_entity):
|
||||
async def test_search_with_entity_type_filter(client, indexed_entity):
|
||||
"""Test search with type filter."""
|
||||
# Should find with correct type
|
||||
response = await client.post(
|
||||
"/search/", json={"text": "test", "types": [SearchItemType.ENTITY.value]}
|
||||
"/search/", json={"text": "test", "entity_types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
@@ -60,7 +60,7 @@ async def test_search_with_type_filter(client, indexed_entity):
|
||||
|
||||
# Should find with relation type
|
||||
response = await client.post(
|
||||
"/search/", json={"text": "test", "types": [SearchItemType.RELATION.value]}
|
||||
"/search/", json={"text": "test", "entity_types": [SearchItemType.RELATION.value]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
@@ -68,16 +68,16 @@ async def test_search_with_type_filter(client, indexed_entity):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_entity_type_filter(client, indexed_entity):
|
||||
async def test_search_with_type_filter(client, indexed_entity):
|
||||
"""Test search with entity type filter."""
|
||||
# Should find with correct entity type
|
||||
response = await client.post("/search/", json={"text": "test", "entity_types": ["test"]})
|
||||
response = await client.post("/search/", json={"text": "test", "types": ["test"]})
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 1
|
||||
|
||||
# Should not find with wrong entity type
|
||||
response = await client.post("/search/", json={"text": "test", "entity_types": ["note"]})
|
||||
response = await client.post("/search/", json={"text": "test", "types": ["note"]})
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 0
|
||||
@@ -153,8 +153,8 @@ async def test_multiple_filters(client, indexed_entity):
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"types": [SearchItemType.ENTITY.value],
|
||||
"entity_types": ["test"],
|
||||
"entity_types": [SearchItemType.ENTITY.value],
|
||||
"types": ["test"],
|
||||
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -211,7 +211,7 @@ def test_search_basic(cli_env, setup_test_note):
|
||||
"""Test basic search command."""
|
||||
result = runner.invoke(
|
||||
tool_app,
|
||||
["search", "test observation"],
|
||||
["search-notes", "test observation"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
@@ -235,7 +235,7 @@ def test_search_permalink(cli_env, setup_test_note):
|
||||
|
||||
result = runner.invoke(
|
||||
tool_app,
|
||||
["search", permalink, "--permalink"],
|
||||
["search-notes", permalink, "--permalink"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ from basic_memory.config import ConfigManager, DATA_DIR_NAME, CONFIG_FILE_NAME
|
||||
@pytest.fixture
|
||||
def temp_home(monkeypatch):
|
||||
"""Create a temporary directory for testing."""
|
||||
# Save the original environment variable if it exists
|
||||
original_env = os.environ.get("BASIC_MEMORY_PROJECT")
|
||||
|
||||
# Clear environment variable for clean test
|
||||
if "BASIC_MEMORY_PROJECT" in os.environ:
|
||||
del os.environ["BASIC_MEMORY_PROJECT"]
|
||||
|
||||
with TemporaryDirectory() as tempdir:
|
||||
temp_home = Path(tempdir)
|
||||
monkeypatch.setattr(Path, "home", lambda: temp_home)
|
||||
@@ -25,6 +32,12 @@ def temp_home(monkeypatch):
|
||||
|
||||
yield temp_home
|
||||
|
||||
# Cleanup: restore original environment variable if it existed
|
||||
if original_env is not None:
|
||||
os.environ["BASIC_MEMORY_PROJECT"] = original_env
|
||||
elif "BASIC_MEMORY_PROJECT" in os.environ:
|
||||
del os.environ["BASIC_MEMORY_PROJECT"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli_runner():
|
||||
@@ -123,17 +136,33 @@ def test_project_default(cli_runner, temp_home):
|
||||
# Set as default
|
||||
result = cli_runner.invoke(app, ["project", "default", "test"])
|
||||
assert result.exit_code == 0
|
||||
assert "Project 'test' set as default" in result.stdout
|
||||
assert "Project 'test' set as default and activated" in result.stdout
|
||||
|
||||
# Verify default was set
|
||||
config_manager = ConfigManager()
|
||||
assert config_manager.default_project == "test"
|
||||
|
||||
# Extra verification: check if the environment variable was set
|
||||
assert os.environ.get("BASIC_MEMORY_PROJECT") == "test"
|
||||
|
||||
|
||||
def test_project_current(cli_runner, temp_home):
|
||||
"""Test showing the current project."""
|
||||
# Create a bare-bones config.json with main as the default project
|
||||
config_file = temp_home / DATA_DIR_NAME / CONFIG_FILE_NAME
|
||||
config_data = {
|
||||
"projects": {
|
||||
"main": str(temp_home / "basic-memory"),
|
||||
},
|
||||
"default_project": "main",
|
||||
}
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Set as default
|
||||
# Create the main project directory
|
||||
main_dir = temp_home / "basic-memory"
|
||||
main_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Now check the current project
|
||||
result = cli_runner.invoke(app, ["project", "current"])
|
||||
assert result.exit_code == 0
|
||||
assert "Current project: main" in result.stdout
|
||||
@@ -156,3 +185,24 @@ def test_project_option(cli_runner, temp_home, monkeypatch):
|
||||
|
||||
# Verify environment variable was set
|
||||
assert env_vars.get("BASIC_MEMORY_PROJECT") == "test"
|
||||
|
||||
|
||||
def test_project_default_activates_project(cli_runner, temp_home, monkeypatch):
|
||||
"""Test that setting the default project also activates it in the current session."""
|
||||
# Create a test environment
|
||||
env = {}
|
||||
monkeypatch.setattr(os, "environ", env)
|
||||
|
||||
# Create two test projects
|
||||
config_manager = ConfigManager()
|
||||
config_manager.add_project("project1", str(temp_home / "project1"))
|
||||
|
||||
# Set project1 as default using the CLI command
|
||||
result = cli_runner.invoke(app, ["project", "default", "project1"])
|
||||
assert result.exit_code == 0
|
||||
assert "Project 'project1' set as default and activated" in result.stdout
|
||||
|
||||
# Verify the environment variable was set
|
||||
# This is the core of our fix - the set_default_project command now also sets
|
||||
# the BASIC_MEMORY_PROJECT environment variable to activate the project
|
||||
assert env.get("BASIC_MEMORY_PROJECT") == "project1"
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_info_stats_command(cli_env, test_graph):
|
||||
runner = CliRunner()
|
||||
|
||||
# Run the command
|
||||
result = runner.invoke(cli_app, ["info", "stats"])
|
||||
result = runner.invoke(cli_app, ["project", "info"])
|
||||
|
||||
# Verify exit code
|
||||
assert result.exit_code == 0
|
||||
@@ -26,7 +26,7 @@ def test_info_stats_json(cli_env, test_graph):
|
||||
runner = CliRunner()
|
||||
|
||||
# Run the command with --json flag
|
||||
result = runner.invoke(cli_app, ["info", "stats", "--json"])
|
||||
result = runner.invoke(cli_app, ["project", "info", "--json"])
|
||||
|
||||
# Verify exit code
|
||||
assert result.exit_code == 0
|
||||
|
||||
+4
-2
@@ -1,14 +1,14 @@
|
||||
"""Common test fixtures."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import AsyncGenerator
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from loguru import logger
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_sessionmaker
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig
|
||||
@@ -140,6 +140,7 @@ def entity_parser(test_config):
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def sync_service(
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
entity_parser: EntityParser,
|
||||
entity_repository: EntityRepository,
|
||||
@@ -149,6 +150,7 @@ async def sync_service(
|
||||
) -> SyncService:
|
||||
"""Create sync service for testing."""
|
||||
return SyncService(
|
||||
config=test_config,
|
||||
entity_service=entity_service,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
|
||||
@@ -217,6 +217,37 @@ def test_parse_empty_content():
|
||||
assert len(result.relations) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_file_with_absolute_path(test_config, entity_parser):
|
||||
"""Test parsing a file with an absolute path."""
|
||||
content = dedent("""
|
||||
---
|
||||
type: component
|
||||
permalink: absolute_path_test
|
||||
---
|
||||
|
||||
# Absolute Path Test
|
||||
|
||||
A file with an absolute path.
|
||||
""")
|
||||
|
||||
# Create a test file in the project directory
|
||||
test_file = test_config.home / "absolute_path_test.md"
|
||||
test_file.write_text(content)
|
||||
|
||||
# Get the absolute path to the test file
|
||||
absolute_path = test_file.resolve()
|
||||
|
||||
# Parse the file using the absolute path
|
||||
entity = await entity_parser.parse_file(absolute_path)
|
||||
|
||||
# Verify the file was parsed correctly
|
||||
assert entity.frontmatter.permalink == "absolute_path_test"
|
||||
assert "Absolute Path Test" in entity.content
|
||||
assert entity.created is not None
|
||||
assert entity.modified is not None
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_parse_file_invalid_yaml(test_config, entity_parser):
|
||||
# """Test parsing file with invalid YAML frontmatter."""
|
||||
|
||||
@@ -26,7 +26,7 @@ async def mock_call_get():
|
||||
@pytest_asyncio.fixture
|
||||
async def mock_search():
|
||||
"""Mock for search tool."""
|
||||
with patch("basic_memory.mcp.tools.read_note.search") as mock:
|
||||
with patch("basic_memory.mcp.tools.read_note.search_notes") as mock:
|
||||
# Default to empty results
|
||||
mock.return_value = SearchResponse(results=[], current_page=1, page_size=1)
|
||||
yield mock
|
||||
@@ -51,8 +51,10 @@ async def test_note_unicode_content(app):
|
||||
|
||||
assert (
|
||||
dedent("""
|
||||
# Created test/Unicode Test.md (272389cd)
|
||||
# Created note
|
||||
file_path: test/Unicode Test.md
|
||||
permalink: test/unicode-test
|
||||
checksum: 272389cd
|
||||
""").strip()
|
||||
in result
|
||||
)
|
||||
@@ -201,7 +203,8 @@ async def test_read_note_title_search_fallback(mock_call_get, mock_search):
|
||||
|
||||
# Verify title search was used
|
||||
mock_search.assert_called_once()
|
||||
assert mock_search.call_args[0][0].title == "Test Note"
|
||||
assert mock_search.call_args[1]["query"] == "Test Note"
|
||||
assert mock_search.call_args[1]["search_type"] == "title"
|
||||
|
||||
# Verify second lookup was used
|
||||
assert mock_call_get.call_count == 2
|
||||
@@ -254,8 +257,10 @@ async def test_read_note_text_search_fallback(mock_call_get, mock_search):
|
||||
|
||||
# Verify both search types were used
|
||||
assert mock_search.call_count == 2
|
||||
assert mock_search.call_args_list[0][0][0].title == "some query" # Title search
|
||||
assert mock_search.call_args_list[1][0][0].text == "some query" # Text search
|
||||
assert mock_search.call_args_list[0][1]["query"] == "some query" # Title search
|
||||
assert mock_search.call_args_list[0][1]["search_type"] == "title"
|
||||
assert mock_search.call_args_list[1][1]["query"] == "some query" # Text search
|
||||
assert mock_search.call_args_list[1][1]["search_type"] == "text"
|
||||
|
||||
# Verify result contains helpful information
|
||||
assert "Note Not Found" in result
|
||||
|
||||
@@ -42,6 +42,33 @@ async def test_read_file_text_file(app, synced_files):
|
||||
assert response["encoding"] == "utf-8"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_content_file_path(app, synced_files):
|
||||
"""Test reading a text file.
|
||||
|
||||
Should:
|
||||
- Correctly identify text content
|
||||
- Return the content as text
|
||||
- Include correct metadata
|
||||
"""
|
||||
# First create a text file via notes
|
||||
result = await write_note(
|
||||
title="Text Resource",
|
||||
folder="test",
|
||||
content="This is a test text resource",
|
||||
tags=["test", "resource"],
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
# Now read it as a resource
|
||||
response = await read_content("test/Text Resource.md")
|
||||
|
||||
assert response["type"] == "text"
|
||||
assert "This is a test text resource" in response["text"]
|
||||
assert response["content_type"].startswith("text/")
|
||||
assert response["encoding"] == "utf-8"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file_image_file(app, synced_files):
|
||||
"""Test reading an image file.
|
||||
|
||||
@@ -4,12 +4,11 @@ import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from basic_memory.mcp.tools import write_note
|
||||
from basic_memory.mcp.tools.search import search
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
||||
from basic_memory.mcp.tools.search import search_notes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_basic(client):
|
||||
async def test_search_text(client):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note(
|
||||
@@ -21,8 +20,67 @@ async def test_search_basic(client):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
query = SearchQuery(text="searchable")
|
||||
response = await search(query)
|
||||
response = await search_notes(query="searchable")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_title(client):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note(
|
||||
title="Test Search Note",
|
||||
folder="test",
|
||||
content="# Test\nThis is a searchable test note",
|
||||
tags=["test", "search"],
|
||||
)
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes(query="Search Note", search_type="title")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_permalink(client):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note(
|
||||
title="Test Search Note",
|
||||
folder="test",
|
||||
content="# Test\nThis is a searchable test note",
|
||||
tags=["test", "search"],
|
||||
)
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes(query="test/test-search-note", search_type="permalink")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_permalink_match(client):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note(
|
||||
title="Test Search Note",
|
||||
folder="test",
|
||||
content="# Test\nThis is a searchable test note",
|
||||
tags=["test", "search"],
|
||||
)
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes(query="test/test-search-*", search_type="permalink")
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) > 0
|
||||
@@ -42,8 +100,7 @@ async def test_search_pagination(client):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
query = SearchQuery(text="searchable")
|
||||
response = await search(query, page=1, page_size=1)
|
||||
response = await search_notes(query="searchable", page=1, page_size=1)
|
||||
|
||||
# Verify results
|
||||
assert len(response.results) == 1
|
||||
@@ -61,8 +118,24 @@ async def test_search_with_type_filter(client):
|
||||
)
|
||||
|
||||
# Search with type filter
|
||||
query = SearchQuery(text="type", types=[SearchItemType.ENTITY])
|
||||
response = await search(query)
|
||||
response = await search_notes(query="type", types=["note"])
|
||||
|
||||
# Verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_entity_type_filter(client):
|
||||
"""Test search with entity type filter."""
|
||||
# Create test content
|
||||
await write_note(
|
||||
title="Entity Type Test",
|
||||
folder="test",
|
||||
content="# Test\nFiltered by type",
|
||||
)
|
||||
|
||||
# Search with entity type filter
|
||||
response = await search_notes(query="type", entity_types=["entity"])
|
||||
|
||||
# Verify all results are entities
|
||||
assert all(r.type == "entity" for r in response.results)
|
||||
@@ -80,8 +153,7 @@ async def test_search_with_date_filter(client):
|
||||
|
||||
# Search with date filter
|
||||
one_hour_ago = datetime.now() - timedelta(hours=1)
|
||||
query = SearchQuery(text="recent", after_date=one_hour_ago)
|
||||
response = await search(query)
|
||||
response = await search_notes(query="recent", after_date=one_hour_ago.isoformat())
|
||||
|
||||
# Verify we get results within timeframe
|
||||
assert len(response.results) > 0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for note tools that exercise the full stack with SQLite."""
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools import write_note, read_note, delete_note
|
||||
@@ -27,8 +26,10 @@ async def test_write_note(app):
|
||||
assert result
|
||||
assert (
|
||||
dedent("""
|
||||
# Created test/Test Note.md (159f2168)
|
||||
# Created note
|
||||
file_path: test/Test Note.md
|
||||
permalink: test/test-note
|
||||
checksum: 159f2168
|
||||
|
||||
## Tags
|
||||
- test, documentation
|
||||
@@ -64,8 +65,10 @@ async def test_write_note_no_tags(app):
|
||||
assert result
|
||||
assert (
|
||||
dedent("""
|
||||
# Created test/Simple Note.md (9a1ff079)
|
||||
# Created note
|
||||
file_path: test/Simple Note.md
|
||||
permalink: test/simple-note
|
||||
checksum: 9a1ff079
|
||||
""").strip()
|
||||
in result
|
||||
)
|
||||
@@ -105,8 +108,10 @@ async def test_write_note_update_existing(app):
|
||||
assert result # Got a valid permalink
|
||||
assert (
|
||||
dedent("""
|
||||
# Created test/Test Note.md (159f2168)
|
||||
# Created note
|
||||
file_path: test/Test Note.md
|
||||
permalink: test/test-note
|
||||
checksum: 159f2168
|
||||
|
||||
## Tags
|
||||
- test, documentation
|
||||
@@ -122,8 +127,10 @@ async def test_write_note_update_existing(app):
|
||||
)
|
||||
assert (
|
||||
dedent("""
|
||||
# Updated test/Test Note.md (a8eb4d44)
|
||||
# Updated note
|
||||
file_path: test/Test Note.md
|
||||
permalink: test/test-note
|
||||
checksum: a8eb4d44
|
||||
|
||||
## Tags
|
||||
- test, documentation
|
||||
@@ -187,6 +194,30 @@ async def test_delete_note_doesnt_exist(app):
|
||||
assert deleted is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_tag_array_from_bug_report(app):
|
||||
"""Test creating a note with a tag array as reported in issue #38.
|
||||
|
||||
This reproduces the exact payload from the bug report where Cursor
|
||||
was passing an array of tags and getting a type mismatch error.
|
||||
"""
|
||||
# This is the exact payload from the bug report
|
||||
bug_payload = {
|
||||
"title": "Title",
|
||||
"folder": "folder",
|
||||
"content": "CONTENT",
|
||||
"tags": ["hipporag", "search", "fallback", "symfony", "error-handling"],
|
||||
}
|
||||
|
||||
# Try to call the function with this data directly
|
||||
result = await write_note(**bug_payload)
|
||||
|
||||
assert result
|
||||
assert "permalink: folder/title" in result
|
||||
assert "Tags" in result
|
||||
assert "hipporag" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_verbose(app):
|
||||
"""Test creating a new note.
|
||||
@@ -212,8 +243,10 @@ async def test_write_note_verbose(app):
|
||||
|
||||
assert (
|
||||
dedent("""
|
||||
# Created test/Test Note.md (06873a7a)
|
||||
# Created note
|
||||
file_path: test/Test Note.md
|
||||
permalink: test/test-note
|
||||
checksum: 06873a7a
|
||||
|
||||
## Observations
|
||||
- note: 1
|
||||
@@ -229,3 +262,118 @@ async def test_write_note_verbose(app):
|
||||
""").strip()
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_preserves_custom_metadata(app, test_config):
|
||||
"""Test that updating a note preserves custom metadata fields.
|
||||
|
||||
Reproduces issue #36 where custom frontmatter fields like Status
|
||||
were being lost when updating notes with the write_note tool.
|
||||
|
||||
Should:
|
||||
- Create a note with custom frontmatter
|
||||
- Update the note with new content
|
||||
- Verify custom frontmatter is preserved
|
||||
"""
|
||||
# First, create a note with custom metadata using write_note
|
||||
await write_note(
|
||||
title="Custom Metadata Note",
|
||||
folder="test",
|
||||
content="# Initial content",
|
||||
tags=["test"],
|
||||
)
|
||||
|
||||
# Read the note to get its permalink
|
||||
content = await read_note("test/custom-metadata-note")
|
||||
|
||||
# Now directly update the file with custom frontmatter
|
||||
# We need to use a direct file update to add custom frontmatter
|
||||
import frontmatter
|
||||
|
||||
file_path = test_config.home / "test" / "Custom Metadata Note.md"
|
||||
post = frontmatter.load(file_path)
|
||||
|
||||
# Add custom frontmatter
|
||||
post["Status"] = "In Progress"
|
||||
post["Priority"] = "High"
|
||||
post["Version"] = "1.0"
|
||||
|
||||
# Write the file back
|
||||
with open(file_path, "w") as f:
|
||||
f.write(frontmatter.dumps(post))
|
||||
|
||||
# Now update the note using write_note
|
||||
result = await write_note(
|
||||
title="Custom Metadata Note",
|
||||
folder="test",
|
||||
content="# Updated content",
|
||||
tags=["test", "updated"],
|
||||
)
|
||||
|
||||
# Verify the update was successful
|
||||
assert ("Updated note\nfile_path: test/Custom Metadata Note.md") in result
|
||||
|
||||
# Read the note back and check if custom frontmatter is preserved
|
||||
content = await read_note("test/custom-metadata-note")
|
||||
|
||||
# Custom frontmatter should be preserved
|
||||
assert "Status: In Progress" in content
|
||||
assert "Priority: High" in content
|
||||
# Version might be quoted as '1.0' due to YAML serialization
|
||||
assert "Version:" in content # Just check that the field exists
|
||||
assert "1.0" in content # And that the value exists somewhere
|
||||
|
||||
# And new content should be there
|
||||
assert "# Updated content" in content
|
||||
|
||||
# And tags should be updated
|
||||
assert "'#test'" in content
|
||||
assert "'#updated'" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_preserves_content_frontmatter(app):
|
||||
"""Test creating a new note."""
|
||||
await write_note(
|
||||
title="Test Note",
|
||||
folder="test",
|
||||
content=dedent(
|
||||
"""
|
||||
---
|
||||
title: Test Note
|
||||
type: note
|
||||
version: 1.0
|
||||
author: name
|
||||
---
|
||||
# Test
|
||||
|
||||
This is a test note
|
||||
"""
|
||||
),
|
||||
tags=["test", "documentation"],
|
||||
)
|
||||
|
||||
# Try reading it back via permalink
|
||||
content = await read_note("test/test-note")
|
||||
assert (
|
||||
dedent(
|
||||
"""
|
||||
---
|
||||
title: Test Note
|
||||
type: note
|
||||
permalink: test/test-note
|
||||
version: 1.0
|
||||
author: name
|
||||
tags:
|
||||
- '#test'
|
||||
- '#documentation'
|
||||
---
|
||||
|
||||
# Test
|
||||
|
||||
This is a test note
|
||||
"""
|
||||
).strip()
|
||||
in content
|
||||
)
|
||||
@@ -32,12 +32,12 @@ def test_search_filters():
|
||||
"""Test search result filtering."""
|
||||
query = SearchQuery(
|
||||
text="search",
|
||||
types=[SearchItemType.ENTITY],
|
||||
entity_types=["component"],
|
||||
entity_types=[SearchItemType.ENTITY],
|
||||
types=["component"],
|
||||
after_date=datetime(2024, 1, 1),
|
||||
)
|
||||
assert query.types == [SearchItemType.ENTITY]
|
||||
assert query.entity_types == ["component"]
|
||||
assert query.entity_types == [SearchItemType.ENTITY]
|
||||
assert query.types == ["component"]
|
||||
assert query.after_date == "2024-01-01T00:00:00"
|
||||
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ from textwrap import dedent
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.services import FileService
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.exceptions import EntityNotFoundError, EntityCreationError
|
||||
from basic_memory.services.exceptions import EntityCreationError, EntityNotFoundError
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@@ -261,7 +263,7 @@ async def test_get_entities_by_permalinks(entity_service: EntityService):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oget_entities_empty_input(entity_service: EntityService):
|
||||
async def test_get_entities_empty_input(entity_service: EntityService):
|
||||
"""Test opening nodes with empty path ID list."""
|
||||
found = await entity_service.get_entities_by_permalinks([])
|
||||
assert len(found) == 0
|
||||
@@ -521,7 +523,7 @@ async def test_update_with_content(entity_service: EntityService, file_service:
|
||||
"""
|
||||
).strip()
|
||||
|
||||
# Create test entity
|
||||
# update entity
|
||||
entity, created = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Git Workflow Guide",
|
||||
@@ -557,3 +559,33 @@ async def test_update_with_content(entity_service: EntityService, file_service:
|
||||
|
||||
# assert content is in file
|
||||
assert update_content.strip() == file_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_with_no_frontmatter(
|
||||
test_config: ProjectConfig,
|
||||
entity_parser: EntityParser,
|
||||
entity_service: EntityService,
|
||||
file_service: FileService,
|
||||
):
|
||||
# contains no frontmatter
|
||||
content = "# Git Workflow Guide"
|
||||
file_path = Path("test/Git Workflow Guide.md")
|
||||
full_path = test_config.home / file_path
|
||||
|
||||
await file_service.write_file(Path(full_path), content)
|
||||
|
||||
entity_markdown = await entity_parser.parse_file(full_path)
|
||||
created = await entity_service.create_entity_from_markdown(file_path, entity_markdown)
|
||||
file_content, _ = await file_service.read_file(created.file_path)
|
||||
|
||||
assert str(file_path) == str(created.file_path)
|
||||
assert created.title == "Git Workflow Guide"
|
||||
assert created.entity_type == "note"
|
||||
assert created.permalink is None
|
||||
|
||||
# assert file
|
||||
expected = dedent("""
|
||||
# Git Workflow Guide
|
||||
""").strip()
|
||||
assert expected == file_content
|
||||
|
||||
@@ -75,7 +75,7 @@ async def test_search_permalink_wildcard2(search_service, test_graph):
|
||||
async def test_search_text(search_service, test_graph):
|
||||
"""Full-text search"""
|
||||
results = await search_service.search(
|
||||
SearchQuery(text="Root Entity", types=[SearchItemType.ENTITY])
|
||||
SearchQuery(text="Root Entity", entity_types=[SearchItemType.ENTITY])
|
||||
)
|
||||
assert len(results) >= 1
|
||||
assert results[0].permalink == "test/root"
|
||||
@@ -84,7 +84,9 @@ async def test_search_text(search_service, test_graph):
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_title(search_service, test_graph):
|
||||
"""Title only search"""
|
||||
results = await search_service.search(SearchQuery(title="Root", types=[SearchItemType.ENTITY]))
|
||||
results = await search_service.search(
|
||||
SearchQuery(title="Root", entity_types=[SearchItemType.ENTITY])
|
||||
)
|
||||
assert len(results) >= 1
|
||||
assert results[0].permalink == "test/root"
|
||||
|
||||
@@ -140,7 +142,7 @@ async def test_filters(search_service, test_graph):
|
||||
"""Test search filters."""
|
||||
# Combined filters
|
||||
results = await search_service.search(
|
||||
SearchQuery(text="Deep", types=[SearchItemType.ENTITY], entity_types=["deep"])
|
||||
SearchQuery(text="Deep", entity_types=[SearchItemType.ENTITY], types=["deep"])
|
||||
)
|
||||
assert len(results) == 1
|
||||
for r in results:
|
||||
@@ -179,13 +181,18 @@ async def test_search_type(search_service, test_graph):
|
||||
"""Test search filters."""
|
||||
|
||||
# Should find only type
|
||||
results = await search_service.search(SearchQuery(types=[SearchItemType.ENTITY]))
|
||||
results = await search_service.search(SearchQuery(types=["test"]))
|
||||
assert len(results) > 0
|
||||
for r in results:
|
||||
assert r.type == SearchItemType.ENTITY
|
||||
|
||||
# Should find only types passed in
|
||||
results = await search_service.search(SearchQuery(types=[SearchItemType.ENTITY]))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_entity_type(search_service, test_graph):
|
||||
"""Test search filters."""
|
||||
|
||||
# Should find only type
|
||||
results = await search_service.search(SearchQuery(entity_types=[SearchItemType.ENTITY]))
|
||||
assert len(results) > 0
|
||||
for r in results:
|
||||
assert r.type == SearchItemType.ENTITY
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.models import Entity
|
||||
@@ -763,8 +765,28 @@ async def test_sync_permalink_resolved_on_update(
|
||||
|
||||
one_file = project_dir / "one.md"
|
||||
two_file = project_dir / "two.md"
|
||||
await create_test_file(one_file)
|
||||
await create_test_file(two_file)
|
||||
await create_test_file(
|
||||
one_file,
|
||||
content=dedent(
|
||||
"""
|
||||
---
|
||||
permalink: one
|
||||
---
|
||||
test content
|
||||
"""
|
||||
),
|
||||
)
|
||||
await create_test_file(
|
||||
two_file,
|
||||
content=dedent(
|
||||
"""
|
||||
---
|
||||
permalink: two
|
||||
---
|
||||
test content
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
# Run sync
|
||||
await sync_service.sync(test_config.home, show_progress=False)
|
||||
@@ -819,7 +841,7 @@ test content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_duplicate_observations(
|
||||
async def test_sync_permalink_not_created_if_no_frontmatter(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
file_service: FileService,
|
||||
@@ -827,40 +849,73 @@ async def test_sync_duplicate_observations(
|
||||
"""Test that sync resolves permalink conflicts on update."""
|
||||
project_dir = test_config.home
|
||||
|
||||
content = """
|
||||
---
|
||||
title: a note
|
||||
type: note
|
||||
tags: []
|
||||
---
|
||||
|
||||
test content
|
||||
|
||||
- [note] one observation
|
||||
"""
|
||||
|
||||
note_file = project_dir / "note.md"
|
||||
await create_test_file(note_file, content)
|
||||
file = project_dir / "one.md"
|
||||
await create_test_file(file)
|
||||
|
||||
# Run sync
|
||||
await sync_service.sync(test_config.home, show_progress=False)
|
||||
|
||||
# Check permalinks
|
||||
file_one_content, _ = await file_service.read_file(note_file)
|
||||
assert (
|
||||
"""---
|
||||
title: a note
|
||||
type: note
|
||||
tags: []
|
||||
permalink: note
|
||||
---
|
||||
# Check permalink not created
|
||||
file_content, _ = await file_service.read_file(file)
|
||||
assert "permalink:" not in file_content
|
||||
|
||||
test content
|
||||
|
||||
- [note] one observation
|
||||
""".strip()
|
||||
== file_one_content
|
||||
@pytest_asyncio.fixture
|
||||
def test_config_update_permamlinks_on_move(tmp_path) -> ProjectConfig:
|
||||
"""Test configuration using in-memory DB."""
|
||||
config = ProjectConfig(
|
||||
project="test-project",
|
||||
update_permalinks_on_move=True,
|
||||
)
|
||||
config.home = tmp_path
|
||||
|
||||
(tmp_path / config.home.name).mkdir(parents=True, exist_ok=True)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_permalink_updated_on_move(
|
||||
test_config_update_permamlinks_on_move: ProjectConfig,
|
||||
sync_service: SyncService,
|
||||
file_service: FileService,
|
||||
):
|
||||
"""Test that we update a permalink on a file move if set in config ."""
|
||||
test_config = test_config_update_permamlinks_on_move
|
||||
project_dir = test_config.home
|
||||
sync_service.config = test_config
|
||||
|
||||
# Create initial file
|
||||
content = dedent(
|
||||
"""
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Test Move
|
||||
Content for move test
|
||||
"""
|
||||
)
|
||||
|
||||
old_path = project_dir / "old" / "test_move.md"
|
||||
old_path.parent.mkdir(parents=True)
|
||||
await create_test_file(old_path, content)
|
||||
|
||||
# Initial sync
|
||||
await sync_service.sync(test_config.home, show_progress=False)
|
||||
|
||||
# verify permalink
|
||||
old_content, _ = await file_service.read_file(old_path)
|
||||
assert "permalink: old/test-move" in old_content
|
||||
|
||||
# Move the file
|
||||
new_path = project_dir / "new" / "moved_file.md"
|
||||
new_path.parent.mkdir(parents=True)
|
||||
old_path.rename(new_path)
|
||||
|
||||
# Sync again
|
||||
await sync_service.sync(test_config.home, show_progress=False)
|
||||
|
||||
file_content, _ = await file_service.read_file(new_path)
|
||||
assert "permalink: new/moved-file" in file_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Test for issue #72 - notes with wikilinks staying in modified status."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
|
||||
async def create_test_file(path: Path, content: str) -> None:
|
||||
"""Create a test file with given content."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wikilink_modified_status_issue(sync_service: SyncService, test_config):
|
||||
"""Test that files with wikilinks don't remain in modified status after sync."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# Create a file with a wikilink
|
||||
content = """---
|
||||
title: Test Wikilink
|
||||
type: note
|
||||
---
|
||||
# Test File
|
||||
|
||||
This file contains a wikilink to [[another-file]].
|
||||
"""
|
||||
test_file_path = project_dir / "test_wikilink.md"
|
||||
await create_test_file(test_file_path, content)
|
||||
|
||||
# Initial sync
|
||||
report1 = await sync_service.sync(test_config.home, show_progress=False)
|
||||
assert "test_wikilink.md" in report1.new
|
||||
assert "test_wikilink.md" not in report1.modified
|
||||
|
||||
# Sync again without changing the file - should not be modified
|
||||
report2 = await sync_service.sync(test_config.home, show_progress=False)
|
||||
assert "test_wikilink.md" not in report2.new
|
||||
assert "test_wikilink.md" not in report2.modified
|
||||
|
||||
# Create the target file
|
||||
target_content = """---
|
||||
title: Another File
|
||||
type: note
|
||||
---
|
||||
# Another File
|
||||
|
||||
This is the target file.
|
||||
"""
|
||||
target_file_path = project_dir / "another_file.md"
|
||||
await create_test_file(target_file_path, target_content)
|
||||
|
||||
# Sync again after adding target file
|
||||
report3 = await sync_service.sync(test_config.home, show_progress=False)
|
||||
assert "another_file.md" in report3.new
|
||||
assert "test_wikilink.md" not in report3.modified
|
||||
|
||||
# Sync one more time - both files should now be stable
|
||||
report4 = await sync_service.sync(test_config.home, show_progress=False)
|
||||
assert "test_wikilink.md" not in report4.modified
|
||||
assert "another_file.md" not in report4.modified
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Test proper handling of .tmp files during sync."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from watchfiles import Change
|
||||
|
||||
|
||||
async def create_test_file(path: Path, content: str = "test content") -> None:
|
||||
"""Create a test file with given content."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temp_file_filter(watch_service):
|
||||
"""Test that .tmp files are correctly filtered out."""
|
||||
# Test filter_changes method directly
|
||||
tmp_path = str(watch_service.config.home / "test.tmp")
|
||||
assert not watch_service.filter_changes(Change.added, tmp_path)
|
||||
|
||||
# Test with valid file
|
||||
valid_path = str(watch_service.config.home / "test.md")
|
||||
assert watch_service.filter_changes(Change.added, valid_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_tmp_files(watch_service, test_config, monkeypatch):
|
||||
"""Test handling of .tmp files during sync process."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# Create a .tmp file - this simulates a file being written with write_file_atomic
|
||||
tmp_file = project_dir / "test.tmp"
|
||||
await create_test_file(tmp_file, "This is a temporary file")
|
||||
|
||||
# Create the target final file
|
||||
final_file = project_dir / "test.md"
|
||||
await create_test_file(final_file, "This is the final file")
|
||||
|
||||
# Setup changes that include both the .tmp and final file
|
||||
changes = {
|
||||
(Change.added, str(tmp_file)),
|
||||
(Change.added, str(final_file)),
|
||||
}
|
||||
|
||||
# Track sync_file calls
|
||||
sync_calls = []
|
||||
|
||||
# Mock sync_file to track calls
|
||||
original_sync_file = watch_service.sync_service.sync_file
|
||||
|
||||
async def mock_sync_file(path, new=True):
|
||||
sync_calls.append(path)
|
||||
return await original_sync_file(path, new)
|
||||
|
||||
monkeypatch.setattr(watch_service.sync_service, "sync_file", mock_sync_file)
|
||||
|
||||
# Handle changes
|
||||
await watch_service.handle_changes(project_dir, changes)
|
||||
|
||||
# Verify .tmp file was not processed
|
||||
assert "test.tmp" not in sync_calls
|
||||
assert "test.md" in sync_calls
|
||||
|
||||
# Verify only the final file got an entity
|
||||
tmp_entity = await watch_service.sync_service.entity_repository.get_by_file_path("test.tmp")
|
||||
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path("test.md")
|
||||
|
||||
assert tmp_entity is None, "Temp file should not have an entity"
|
||||
assert final_entity is not None, "Final file should have an entity"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_atomic_write_tmp_file_handling(watch_service, test_config, monkeypatch):
|
||||
"""Test handling of file changes during atomic write operations."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# This test simulates the full atomic write process:
|
||||
# 1. First a .tmp file is created
|
||||
# 2. Then the .tmp file is renamed to the final file
|
||||
# 3. Both events are processed by the watch service
|
||||
|
||||
# Setup file paths
|
||||
tmp_path = project_dir / "document.tmp"
|
||||
final_path = project_dir / "document.md"
|
||||
|
||||
# Create mockup of the atomic write process
|
||||
await create_test_file(tmp_path, "Content for document")
|
||||
|
||||
# First batch of changes - .tmp file created
|
||||
changes1 = {(Change.added, str(tmp_path))}
|
||||
|
||||
# Process first batch
|
||||
await watch_service.handle_changes(project_dir, changes1)
|
||||
|
||||
# Now "replace" the temp file with the final file
|
||||
tmp_path.rename(final_path)
|
||||
|
||||
# Second batch of changes - .tmp file deleted, final file added
|
||||
changes2 = {(Change.deleted, str(tmp_path)), (Change.added, str(final_path))}
|
||||
|
||||
# Process second batch
|
||||
await watch_service.handle_changes(project_dir, changes2)
|
||||
|
||||
# Verify only the final file is in the database
|
||||
tmp_entity = await watch_service.sync_service.entity_repository.get_by_file_path("document.tmp")
|
||||
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
|
||||
"document.md"
|
||||
)
|
||||
|
||||
assert tmp_entity is None, "Temp file should not have an entity"
|
||||
assert final_entity is not None, "Final file should have an entity"
|
||||
|
||||
# Check events
|
||||
new_events = [e for e in watch_service.state.recent_events if e.action == "new"]
|
||||
assert len(new_events) == 1
|
||||
assert new_events[0].path == "document.md"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rapid_atomic_writes(watch_service, test_config):
|
||||
"""Test handling of rapid atomic writes to the same destination."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# This test simulates multiple rapid atomic writes to the same file:
|
||||
# 1. Several .tmp files are created one after another
|
||||
# 2. Each is then renamed to the same final file
|
||||
# 3. Events are batched and processed together
|
||||
|
||||
# Setup file paths
|
||||
tmp1_path = project_dir / "document.1.tmp"
|
||||
tmp2_path = project_dir / "document.2.tmp"
|
||||
final_path = project_dir / "document.md"
|
||||
|
||||
# Create multiple temp files that will be used in sequence
|
||||
await create_test_file(tmp1_path, "First version")
|
||||
await create_test_file(tmp2_path, "Second version")
|
||||
|
||||
# Simulate the first atomic write
|
||||
tmp1_path.rename(final_path)
|
||||
|
||||
# Brief pause to ensure file system registers the change
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Read content to verify
|
||||
content1 = final_path.read_text()
|
||||
assert content1 == "First version"
|
||||
|
||||
# Simulate the second atomic write
|
||||
tmp2_path.rename(final_path)
|
||||
|
||||
# Verify content was updated
|
||||
content2 = final_path.read_text()
|
||||
assert content2 == "Second version"
|
||||
|
||||
# Create a batch of changes that might arrive in mixed order
|
||||
changes = {
|
||||
(Change.added, str(tmp1_path)),
|
||||
(Change.deleted, str(tmp1_path)),
|
||||
(Change.added, str(tmp2_path)),
|
||||
(Change.deleted, str(tmp2_path)),
|
||||
(Change.added, str(final_path)),
|
||||
(Change.modified, str(final_path)),
|
||||
}
|
||||
|
||||
# Process all changes
|
||||
await watch_service.handle_changes(project_dir, changes)
|
||||
|
||||
# Verify only the final file is in the database
|
||||
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
|
||||
"document.md"
|
||||
)
|
||||
assert final_entity is not None
|
||||
|
||||
# Also verify no tmp entities were created
|
||||
tmp1_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
|
||||
"document.1.tmp"
|
||||
)
|
||||
tmp2_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
|
||||
"document.2.tmp"
|
||||
)
|
||||
assert tmp1_entity is None
|
||||
assert tmp2_entity is None
|
||||
@@ -96,7 +96,7 @@ Test content
|
||||
# Verify
|
||||
entity = await watch_service.sync_service.entity_repository.get_by_file_path("new_note.md")
|
||||
assert entity is not None
|
||||
assert entity.title == "new_note.md"
|
||||
assert entity.title == "new_note"
|
||||
|
||||
# Check event was recorded
|
||||
events = [e for e in watch_service.state.recent_events if e.action == "new"]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Tests for parse_tags utility function."""
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.utils import parse_tags
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_tags,expected",
|
||||
[
|
||||
# None input
|
||||
(None, []),
|
||||
# List inputs
|
||||
([], []),
|
||||
(["tag1", "tag2"], ["tag1", "tag2"]),
|
||||
(["tag1", "", "tag2"], ["tag1", "tag2"]), # Empty tags are filtered
|
||||
([" tag1 ", " tag2 "], ["tag1", "tag2"]), # Whitespace is stripped
|
||||
# String inputs
|
||||
("", []),
|
||||
("tag1", ["tag1"]),
|
||||
("tag1,tag2", ["tag1", "tag2"]),
|
||||
("tag1, tag2", ["tag1", "tag2"]), # Whitespace after comma is stripped
|
||||
("tag1,,tag2", ["tag1", "tag2"]), # Empty tags are filtered
|
||||
# Tags with leading '#' characters - these should be stripped
|
||||
(["#tag1", "##tag2"], ["tag1", "tag2"]),
|
||||
("#tag1,##tag2", ["tag1", "tag2"]),
|
||||
(["tag1", "#tag2", "##tag3"], ["tag1", "tag2", "tag3"]),
|
||||
# Mixed whitespace and '#' characters
|
||||
([" #tag1 ", " ##tag2 "], ["tag1", "tag2"]),
|
||||
(" #tag1 , ##tag2 ", ["tag1", "tag2"]),
|
||||
],
|
||||
)
|
||||
def test_parse_tags(input_tags: Union[List[str], str, None], expected: List[str]) -> None:
|
||||
"""Test tag parsing with various input formats."""
|
||||
result = parse_tags(input_tags)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_parse_tags_special_case() -> None:
|
||||
"""Test parsing from non-string, non-list types."""
|
||||
|
||||
# Test with custom object that has __str__ method
|
||||
class TagObject:
|
||||
def __str__(self) -> str:
|
||||
return "tag1,tag2"
|
||||
|
||||
result = parse_tags(TagObject()) # pyright: ignore [reportArgumentType]
|
||||
assert result == ["tag1", "tag2"]
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Test UTF-8 character handling in file operations."""
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory import file_utils
|
||||
from basic_memory.file_utils import write_file_atomic, compute_checksum
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_utf8_characters(tmp_path):
|
||||
"""Test writing files with UTF-8 characters."""
|
||||
# Create a test file with various UTF-8 characters
|
||||
test_path = tmp_path / "utf8_test.md"
|
||||
|
||||
# Include characters from various scripts
|
||||
utf8_content = """# UTF-8 Test Document
|
||||
|
||||
## Cyrillic
|
||||
Привет мир! (Hello world in Russian)
|
||||
|
||||
## Greek
|
||||
Γειά σου Κόσμε! (Hello world in Greek)
|
||||
|
||||
## Chinese
|
||||
你好世界! (Hello world in Chinese)
|
||||
|
||||
## Japanese
|
||||
こんにちは世界! (Hello world in Japanese)
|
||||
|
||||
## Arabic
|
||||
مرحبا بالعالم! (Hello world in Arabic)
|
||||
|
||||
## Emoji
|
||||
🌍 🌎 🌏 🌐 🗺️ 🧭
|
||||
"""
|
||||
|
||||
# Test the atomic write function with UTF-8 content
|
||||
await write_file_atomic(test_path, utf8_content)
|
||||
|
||||
# Verify the file was written correctly
|
||||
assert test_path.exists()
|
||||
|
||||
# Read the file back and verify content
|
||||
content = test_path.read_text(encoding="utf-8")
|
||||
assert "Привет мир!" in content
|
||||
assert "Γειά σου Κόσμε!" in content
|
||||
assert "你好世界" in content
|
||||
assert "こんにちは世界" in content
|
||||
assert "مرحبا بالعالم" in content
|
||||
assert "🌍 🌎 🌏" in content
|
||||
|
||||
# Verify checksums match
|
||||
checksum1 = await compute_checksum(utf8_content)
|
||||
checksum2 = await compute_checksum(content)
|
||||
assert checksum1 == checksum2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_frontmatter_with_utf8(tmp_path):
|
||||
"""Test handling of frontmatter with UTF-8 characters."""
|
||||
# Create a test file with frontmatter containing UTF-8
|
||||
test_path = tmp_path / "frontmatter_utf8.md"
|
||||
|
||||
# Create content with UTF-8 in frontmatter
|
||||
content = """---
|
||||
title: UTF-8 测试文件 (Test File)
|
||||
author: José García
|
||||
keywords:
|
||||
- тестирование
|
||||
- 테스트
|
||||
- 测试
|
||||
---
|
||||
|
||||
# UTF-8 Support Test
|
||||
|
||||
This file has UTF-8 characters in the frontmatter.
|
||||
"""
|
||||
|
||||
# Write the file
|
||||
await write_file_atomic(test_path, content)
|
||||
|
||||
# Update frontmatter with more UTF-8
|
||||
await file_utils.update_frontmatter(
|
||||
test_path,
|
||||
{
|
||||
"description": "这是一个测试文件 (This is a test file)",
|
||||
"tags": ["国际化", "юникод", "🔤"],
|
||||
},
|
||||
)
|
||||
|
||||
# Read back and check
|
||||
updated_content = test_path.read_text(encoding="utf-8")
|
||||
|
||||
# Original values should be preserved
|
||||
assert "title: UTF-8 测试文件" in updated_content
|
||||
assert "author: José García" in updated_content
|
||||
|
||||
# New values should be added
|
||||
assert "description: 这是一个测试文件" in updated_content
|
||||
assert "国际化" in updated_content
|
||||
assert "юникод" in updated_content
|
||||
assert "🔤" in updated_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_utf8_in_entity_sync(sync_service, test_config):
|
||||
"""Test syncing an entity with UTF-8 content."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# Create a test file with UTF-8 characters
|
||||
test_file = project_dir / "utf8_entity.md"
|
||||
utf8_content = """---
|
||||
type: knowledge
|
||||
permalink: i18n/utf8-document
|
||||
---
|
||||
# UTF-8 测试文档
|
||||
|
||||
## Observations
|
||||
- [language] العربية (Arabic) is written right-to-left
|
||||
- [language] 日本語 (Japanese) uses three writing systems
|
||||
- [encoding] UTF-8 可以编码所有 Unicode 字符
|
||||
|
||||
## Relations
|
||||
- relates_to [[i18n/Ελληνικά]]
|
||||
- relates_to [[i18n/Русский]]
|
||||
"""
|
||||
|
||||
# Write the file
|
||||
test_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
test_file.write_text(utf8_content, encoding="utf-8")
|
||||
|
||||
# Sync the file
|
||||
await sync_service.sync(test_config.home, show_progress=False)
|
||||
|
||||
# Verify entity was created
|
||||
entity = await sync_service.entity_service.get_by_permalink("i18n/utf8-document")
|
||||
assert entity is not None
|
||||
|
||||
# Verify observations were created with UTF-8 content
|
||||
assert len(entity.observations) == 3
|
||||
|
||||
# Check observation content
|
||||
obs_categories = [o.category for o in entity.observations]
|
||||
obs_content = [o.content for o in entity.observations]
|
||||
|
||||
assert "language" in obs_categories
|
||||
assert "encoding" in obs_categories
|
||||
assert any("العربية" in c for c in obs_content)
|
||||
assert any("日本語" in c for c in obs_content)
|
||||
assert any("Unicode" in c for c in obs_content)
|
||||
|
||||
# Check relations
|
||||
assert len(entity.relations) == 2
|
||||
relation_names = [r.to_name for r in entity.relations]
|
||||
assert "i18n/Ελληνικά" in relation_names
|
||||
assert "i18n/Русский" in relation_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_file_service_utf8(sync_service, test_config):
|
||||
"""Test FileService handling of UTF-8 content."""
|
||||
file_service = sync_service.file_service
|
||||
|
||||
# Test writing UTF-8 content through the file service
|
||||
test_path = "utf8_service_test.md"
|
||||
utf8_content = """---
|
||||
title: FileService UTF-8 Test
|
||||
---
|
||||
# UTF-8 通过 FileService 测试
|
||||
|
||||
Testing FileService with UTF-8:
|
||||
* Português: Olá mundo!
|
||||
* Thai: สวัสดีชาวโลก
|
||||
* Korean: 안녕하세요 세계
|
||||
* Hindi: नमस्ते दुनिया
|
||||
"""
|
||||
|
||||
# Use the file service to write the file
|
||||
checksum = await file_service.write_file(test_path, utf8_content)
|
||||
|
||||
# Verify file exists
|
||||
full_path = file_service.base_path / test_path
|
||||
assert full_path.exists()
|
||||
|
||||
# Read back content and verify
|
||||
content, read_checksum = await file_service.read_file(test_path)
|
||||
|
||||
assert "通过" in content
|
||||
assert "Português" in content
|
||||
assert "สวัสดีชาวโลก" in content
|
||||
assert "안녕하세요" in content
|
||||
assert "नमस्ते" in content
|
||||
|
||||
# Checksums should match
|
||||
assert checksum == read_checksum
|
||||
@@ -1,7 +1,8 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.12.1"
|
||||
resolution-markers = [
|
||||
"(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'linux') or (platform_machine == 'armv7l' and sys_platform != 'linux') or (platform_machine == 'i686' and sys_platform != 'linux') or (platform_machine == 'ppc64le' and sys_platform != 'linux') or (platform_machine == 's390x' and sys_platform != 'linux') or (platform_machine == 'x86_64' and sys_platform != 'linux')",
|
||||
"(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'",
|
||||
"platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"platform_machine == 'i686' and sys_platform == 'linux'",
|
||||
"platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
@@ -24,16 +25,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.14.1"
|
||||
version = "1.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/ed/901044acb892caa5604bf818d2da9ab0df94ef606c6059fdf367894ebf60/alembic-1.15.1.tar.gz", hash = "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49", size = 1924789 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/f7/d398fae160568472ddce0b3fde9c4581afc593019a6adc91006a66406991/alembic-1.15.1-py3-none-any.whl", hash = "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", size = 231753 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -47,16 +48,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.8.0"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13' and python_full_version >= '3.12.1'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -70,7 +71,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "basic-memory"
|
||||
version = "0.8.0"
|
||||
version = "0.11.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -147,6 +148,15 @@ dev = [
|
||||
{ name = "ruff", specifier = ">=0.1.6" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cabarchive"
|
||||
version = "0.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/d3/a544aed878edc269ce4427bc937310b73624e1d595de7f4e5bcab413a639/cabarchive-0.2.4.tar.gz", hash = "sha256:04f60089473114cf26eab2b7e1d09611c5bfaf8edd3202dacef66bb5c71e48cf", size = 21064 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/fb/713421f46c68f4bf9cd26f05bda0c233446108997b6b4d83d7ef07f20009/cabarchive-0.2.4-py3-none-any.whl", hash = "sha256:4afabd224eb2e40af8e907379fb8eec6b0adfb71c2aef4457ec3a4d77383c059", size = 25729 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
@@ -161,7 +171,7 @@ name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'linux') or (platform_machine == 'armv7l' and sys_platform != 'linux') or (platform_machine == 'i686' and sys_platform != 'linux') or (platform_machine == 'ppc64le' and sys_platform != 'linux') or (platform_machine == 's390x' and sys_platform != 'linux') or (platform_machine == 'x86_64' and sys_platform != 'linux')" },
|
||||
{ name = "pycparser", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
@@ -176,7 +186,7 @@ name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
@@ -194,66 +204,78 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.12"
|
||||
version = "7.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cx-freeze"
|
||||
version = "7.2.10"
|
||||
version = "8.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cabarchive", marker = "sys_platform == 'win32'" },
|
||||
{ name = "cx-logging", marker = "sys_platform == 'win32'" },
|
||||
{ name = "dmgbuild", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "filelock", marker = "sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "lief", marker = "sys_platform == 'win32'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "patchelf", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'armv7l' and sys_platform == 'linux') or (platform_machine == 'i686' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 's390x' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "striprtf", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/c3/9f05155f7e0f55041add55cedc8724e9d36418c11c2e29131056fbce8e5e/cx_freeze-7.2.10.tar.gz", hash = "sha256:16a6eb37db7d158d0eb4bba8b93a135b6c85ea4bf2c4176c437bc34f01067e61", size = 3146042 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/9f/e007ffc5f457c5451b7ade774529f64d0cdb61d91dae428568775b4ec9e3/cx_freeze-8.0.0.tar.gz", hash = "sha256:80e1f87bb152ed0f97f7c6435e0237d44aade99979927c4627771322a25d550d", size = 3177126 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/9a/0f4084aa728a67974574eac2f1db3a032671536f68a935f340caa31b8667/cx_Freeze-7.2.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4a908dcad86888b741aa2051a65693d8662999a8abf9b7cf3285ef4ccfddad43", size = 21397503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ba/1a66ed666e128152ee9d43b6aa952db823de9918fc7c864373c1000816a4/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:416460e7f3312b1e2010ce5cb35e0449ff5a6e75b3536adddf83fd9b10d13a88", size = 12649925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/29/e0b3fa58394bfc6bc3ce1776e77788e6c8a856fd850974924791b4d5e661/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:851a0d919a732d17e1db145c72d69b5560ab1578387e18915a9e9f0be1bc50fd", size = 13949452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/bd/c237c1b0c48cf5fe2103dedad42a3d36c0ef6c44ab86265f8480d13b3c6f/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e251c74b2551668ae890d4c7a2fbb73a6c9b692de4096ddd9867322873e11b2b", size = 12787091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/66/fae7d0992e72290a08494fa7c5826f00b5130e63747a434993a140746e07/cx_Freeze-7.2.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7f5d65830cb3b0d24590e4526995baf01beeb2517c41838d597c5a2e4ca76d7", size = 13766813 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/78/c123ec526b57531851d9d9c3338aa2df41b396fe3d23b7a2737bf1ef1a63/cx_Freeze-7.2.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d00914208a2953125398d192d1c332eea2bfb4f210a1507efe5188fec18fb8e3", size = 13315368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/6c/4a3ef0c1991805505b2ea69fd09e1fd694138b011d2c62808ab5d4548b16/cx_Freeze-7.2.10-cp312-cp312-win32.whl", hash = "sha256:c0af82ba512323a1036a42c2e863ea56e12764c8c9f0764303df1fa3e29b9d4a", size = 2136880 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/4c/d1c81cb9cf6b0a68837d7ee44a98a3269bafbc259cb8b3b96c22ac1e235e/cx_Freeze-7.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:15b7dafc5eb2b5f6c118eaeed25b0fb0f3a9022e779958e5f9cd1ebf9f77c737", size = 2139867 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/26/23311406e69f96b2382b00b661e85931c70069f6702306edfee034ce473b/cx_Freeze-8.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6da256133c981960408443558031ce818254b9bc8483039d507ebd5b914f5f6d", size = 21439091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/95/8fa6126a9f407d342f25186c6933de06da4a0705c3ab41452ac0a324887c/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1af9ab25e90924c26e45ee27a4743604538ab51095b4eb2c9052d548ac04b5e7", size = 14278341 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/b4/8d299d9bcbae20261c465e3dce45888c63c96b82cb43f909564106698af3/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0cc36dd9c1f0ee156b1288d3a566b55281ae83b09f56c7480d00f5e32236b53", size = 15783842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/fe/801de38e532ac5099c1a35a0b8e3c7ef045f3657bdb0113bc0ea9c1b880f/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5101f2f7074c1373ac122e50c102ffc1805218d0bd994f3f07fe8ffefe46c7cb", size = 14418893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f1/280af574f3a52290f808be06a6c017299f0970a29ecaa6975f46a53bf5c5/cx_Freeze-8.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5afdddd6a68c69823308b03dd697ec84adca898ff4dcf507325fc56f08ba8f6", size = 15442970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/34/0b20b94e60740fe755d07429089b10cc400aef4ce1af6b1fea9b1e8b16a4/cx_Freeze-8.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:592ee11c4e6e61b57bfd3cd1a4655cfe1e7dbb5e2d1bbac8fa34614d54b16294", size = 15223472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/fd/b440b436bca420406d17a5727447a386e54974bf6197255b0cba83bcc92b/cx_Freeze-8.0.0-cp312-cp312-win32.whl", hash = "sha256:a560abd4f29438ed1c5be9b995a6a28db476a0210b354c28b8b7789bf4e8ae00", size = 2184568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/1b/9ddc5e42b65257eb1805184fdcc9cb5cf1538b1837d4502ef563c3e737b5/cx_Freeze-8.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:90b7cad33fbe9be5defb7bbd24270f3589037e0127e3e5b2078255fe1ca64037", size = 2189398 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/02/ab8a1226166494689b3adf29036509055a680e0435cebbb0e6681f333341/cx_Freeze-8.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25fbbd1f06c162b0056f9aa1982ee7c87198445511ec850f07d2e2cf208460f1", size = 20907868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/79/19d1cc296f8945b772f2c726acadb9817e3fd158412d039d96b774975fb5/cx_Freeze-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3741719ad85f348f088fe20d7acbe6c176e941aa9c8888633eeeb6734d835c", size = 13995569 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b1/618549522e176dc4128d75b7d915c4b8ec79171d361a8160a3a434db44b4/cx_Freeze-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee138b0b000b4014abfcba8f3d6ec45524c50d02e985b987027185c07aa5bdde", size = 14086121 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/63/404c17a650fd1f7d24a7916831cb17ca3ecd42e8aa611c60ab5445656d0e/cx_Freeze-8.0.0-cp313-cp313-win32.whl", hash = "sha256:403bd682238e4e8e0eac89dc0240925fc6bb3b638f28a2f1b7404be62cdcd42c", size = 2188054 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/3d/1b0da41e70077e2dece038eec2b666b07a804cbaaa15c8ea377a7c9e96f5/cx_Freeze-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:bdba9196bf577d04700a4f17bf6ce9ec3e56d4dd97a7feae237dc8dce07287e7", size = 2192875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/23/a7584176f749bbb73494a8c2decfb00af8fdc579e189177321e2ccb0c0bc/cx_Freeze-8.0.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93a2ca2ee6e374dd3a6796e67367d24396467df2f9b8434f12daec9d9a88e126", size = 20867921 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/4b/b6296f5cbe74a04bb1746d6da3b591bbe9f327dc509f1c18770323fc5d18/cx_Freeze-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf47b56fa2cb5982232d30a475a739d7c10db498a8d45024c93b963a8736d478", size = 12500579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/25/b55c7a0658a1bae3b9ec0a2eab46836ba7a4c2d3049610aabbe2667d2bda/cx_Freeze-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd36251ae2af50ec78fd574411a0fbb7868d8ec0ebaaa9a0dc264a46600102fc", size = 12579341 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ae/0cb1e6aee865adc9f3e6a9ed29e6d16732612ff0e67cd214c6bd1ccf1ac3/cx_Freeze-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:51857ce2863b9c33fbdfa96a04365c64641cb8f567f4c7cadace6f63ebcf9a25", size = 13484222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a4/bae422c7256ca2c3474d65823e5012fc40e465e29cc4fdfa56f6733d8ba3/cx_Freeze-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:eae16598241754e6184c07a959055f824d4b5b9d7eaf2b1a31d3bb118c448374", size = 13162872 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -285,15 +307,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dmgbuild"
|
||||
version = "1.6.4"
|
||||
version = "1.6.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ds-store", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'linux') or (platform_machine == 'armv7l' and sys_platform != 'linux') or (platform_machine == 'i686' and sys_platform != 'linux') or (platform_machine == 'ppc64le' and sys_platform != 'linux') or (platform_machine == 's390x' and sys_platform != 'linux') or (platform_machine == 'x86_64' and sys_platform != 'linux')" },
|
||||
{ name = "mac-alias", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'linux') or (platform_machine == 'armv7l' and sys_platform != 'linux') or (platform_machine == 'i686' and sys_platform != 'linux') or (platform_machine == 'ppc64le' and sys_platform != 'linux') or (platform_machine == 's390x' and sys_platform != 'linux') or (platform_machine == 'x86_64' and sys_platform != 'linux')" },
|
||||
{ name = "ds-store", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
|
||||
{ name = "mac-alias", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/17/89c089f4263b6c7f40a69355dd74f2cf5b5a58c4f7db79f76ed3d9e1ca5b/dmgbuild-1.6.4.tar.gz", hash = "sha256:adacada75ee517398d014e3fb250b004a2225f01e54832df52d38b9cd1530b74", size = 36824 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/93/b9702c68d5dedfd6b91c76268a89091ff681b8e3b9a026e7919b6ab730a4/dmgbuild-1.6.5.tar.gz", hash = "sha256:c5cbeec574bad84a324348aa7c36d4aada04568c99fb104dec18d22ba3259f45", size = 36848 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/c5/71ecb2f6a95082aa6a7227a92054024e29caba133a7a86094ce32fd03c1f/dmgbuild-1.6.4-py3-none-any.whl", hash = "sha256:53a2f0a9e65111314fce7ecfb6637d08bc3186fa99e719abc9a5af2b484a7d65", size = 34900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/4a/b16f1081f69592c6dba92baa4d3ca7a5685091a0f840f4b5e01be41aaf84/dmgbuild-1.6.5-py3-none-any.whl", hash = "sha256:e19ab8c5e8238e6455d9ccb9175817be7fd62b9cdd1eef20f63dd88e0ec469ab", size = 34906 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -310,7 +332,7 @@ name = "ds-store"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mac-alias", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'linux') or (platform_machine == 'armv7l' and sys_platform != 'linux') or (platform_machine == 'i686' and sys_platform != 'linux') or (platform_machine == 'ppc64le' and sys_platform != 'linux') or (platform_machine == 's390x' and sys_platform != 'linux') or (platform_machine == 'x86_64' and sys_platform != 'linux')" },
|
||||
{ name = "mac-alias", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/36/902259bf7ddb142dd91cf7a9794aa15e1a8ab985974f90375e5d3463b441/ds_store-1.3.1.tar.gz", hash = "sha256:c27d413caf13c19acb85d75da4752673f1f38267f9eb6ba81b3b5aa99c2d207c", size = 27052 }
|
||||
wheels = [
|
||||
@@ -341,16 +363,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.9"
|
||||
version = "0.115.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/dd/d854f85e70f7341b29e3fda754f2833aec197bd355f805238758e3bcd8ed/fastapi-0.115.9.tar.gz", hash = "sha256:9d7da3b196c5eed049bc769f9475cd55509a112fbe031c0ef2f53768ae68d13f", size = 293774 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b6/7517af5234378518f27ad35a7b24af9591bc500b8c1780929c1295999eb6/fastapi-0.115.9-py3-none-any.whl", hash = "sha256:4a439d7923e4de796bcc88b64e9754340fcd1574673cbd865ba8a99fe0d28c56", size = 94919 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -384,11 +406,11 @@ standard = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.17.0"
|
||||
version = "3.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -548,34 +570,34 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lief"
|
||||
version = "0.16.3"
|
||||
version = "0.16.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/c6/2b8eab0b1c061d2a0a29642cbef91b9b36148e86f3fce99200ede5e8e788/lief-0.16.3-cp312-cp312-win32.whl", hash = "sha256:e7ba797829584c5cc1c8a736b2f1587f09b1f3030239c968c7664649fb79ae15", size = 3042391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/4e/7bb9efe6bbe804449c150295fa597c7e10fd7264c5a5205c38f9b3bd9942/lief-0.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:70c1f5f66bd4eeead2853a7a80d941b4dd03e3791c68c5351b0d39c78cfa9afe", size = 3167314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/9a/be854d274352a2ce406691fb6c80c82b4a6adf0fdb6978734e4c082326b7/lief-0.16.3-cp313-cp313-win32.whl", hash = "sha256:aefbe78b06d9e89387ab8fc069d1cb34252f5916cf35eaa21088b21b74b99d08", size = 3042393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/be/34b8864d0976258f29a5e3d7bedb2785fd56409cf866813458bfd32d2a6b/lief-0.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:52dc05445d8019b61a9ab8c6eb9d6238c4346ac692dcecca76d5f329a999216e", size = 3167188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/9c/1051c681702740d92bc125c74a5b74e20d873ee04686d9d6a44e028fe6bb/lief-0.16.4-cp312-cp312-win32.whl", hash = "sha256:06cd2432def66454785add6b8f14f2cf9f7ef4168cf2eb24367192953fb33206", size = 3053189 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/6e/f4a2d1c0e47939d407d8fe894c78964249bd8efd172be75878054051b688/lief-0.16.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6363bf971135a4c65e2fa151e456995bb6ecb38c00f1ed9f8d4c6a625111556", size = 3182188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/15/b1518db49c4d69232631133cd2b3b34a5691015c5e7394c5f38c0eba15c1/lief-0.16.4-cp313-cp313-win32.whl", hash = "sha256:9e103cc2bee8ff8d960ead8a094fc6aa9878b9ebeae0c1785d1e5bc2428ec4a4", size = 3053184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/da/2b01fafc36aeb9a0c0949f5ffaba0a4562c6e77276fbd6b6cbf87387087c/lief-0.16.4-cp313-cp313-win_amd64.whl", hash = "sha256:775661661d0c4d33429f99e6cfd7f0e0c3781191be81c79eaa4a9f2c68929ebd", size = 3182245 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -664,7 +686,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.3.0"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -676,9 +698,9 @@ dependencies = [
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/c9/c55764824e893fdebe777ac7223200986a275c3191dba9169f8eb6d7c978/mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9", size = 159128 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d1/3ff566ecf322077d861f1a68a1ff025cad337417bd66ad22a7c6f7dfcfaf/mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527", size = 73734 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -710,16 +732,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "patchelf"
|
||||
version = "0.17.2.1"
|
||||
version = "0.17.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/ec/ac383eb82792e092d8037649b382cf78a7b79c2ce4e5b861f61519b9b14e/patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031", size = 167484 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/41/dc3ee5838db2d90be935adb53ae7745135d9c719d070b1989b246f983c7f/patchelf-0.17.2.2.tar.gz", hash = "sha256:080b2ac3074fd4ab257700088e82470425e56609aa0dd07abe548f04b7b3b007", size = 149517 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/09/b7ec52d01ad2936c967e929fd076b0a2e4f94ce950c9440e38f98c67675e/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c", size = 421930 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/47/e02357d1075cdf4b56be39a6c218a5a2b0bd3896011120ae3765190ab527/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d", size = 381147 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/78/3f4e19d6ba7acf1e01db6d82d23e0435a76466187644ce0dbcf6bd621317/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502", size = 488526 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/d3/adedfbcff489605a0e907f390006ef7d59a3ac56a9e2dbe721ddbbb1e58e/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8", size = 470060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/9b/33e54d5916863904609e40b53206e5a49cf649b71f7f787eddcbc27a9f5c/patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5", size = 499565 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/73/c3105c973dd2afcdc5d946ee211d5c4ecdf9d27bb54ae835b144e706e86d/patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927", size = 425709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/25b5d10d971f509fe6bc8951b855f0f05be4c24e0dd1616c14a6e1a9116a/patchelf-0.17.2.2-py3-none-manylinux1_i686.manylinux_2_5_i686.musllinux_1_1_i686.whl", hash = "sha256:3b8a4d7cccac04d8231dec321245611bf147b199cbf4da305d1a364ff689fb58", size = 524182 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/f9/e070956e350ccdfdf059251836f757ad91ac0c01b0ba3e033ea7188d8d42/patchelf-0.17.2.2-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:e334ebb1c5aa9fc740fd95ebe449271899fe1e45a3eb0941300b304f7e3d1299", size = 466519 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/0d/dc3ac6c6e9e9d0d3e40bee1abe95a07034f83627319e60a7dc9abdbfafee/patchelf-0.17.2.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3d32cd69442a229724f7f071b61cef1f87ccd80cf755af0b1ecefd553fa9ae3f", size = 462123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f6/b842b19c2b72df1c524ab3793c3ec9cf3926c7c841e0b64b34f95d7fb806/patchelf-0.17.2.2-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:05f6bbdbe484439cb025e20c60abd37e432e6798dfa3f39a072e6b7499072a8c", size = 412347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0b/33eb3087703d903dd01cf6b0d64e067bf3718a5e8b1239bc6fc2c4b1fdb2/patchelf-0.17.2.2-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:b54e79ceb444ec6a536a5dc2e8fc9c771ec6a1fa7d5f4dbb3dc0e5b8e5ff82e1", size = 522827 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/25/6379dc26714b5a40f51b3c7927d668b00a51517e857da7f3cb09d1d0bcb6/patchelf-0.17.2.2-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:24374cdbd9a072230339024fb6922577cb3231396640610b069f678bc483f21e", size = 565961 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -797,7 +819,7 @@ email = [
|
||||
{ name = "email-validator" },
|
||||
]
|
||||
timezone = [
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows' and python_full_version >= '3.12.1'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -911,20 +933,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.395"
|
||||
version = "1.1.397"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/47/a2e1dfd70f9f0db34f70d5b108c82be57bf24185af69c95acff57f9239fa/pyright-1.1.395.tar.gz", hash = "sha256:53703169068c160bfb41e1b44ba3e2512492869c26cfad927e1268cb3fbb1b1c", size = 3813566 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/23/cefa10c9cb198e0858ed0b9233371d62bca880337f628e58f50dfdfb12f0/pyright-1.1.397.tar.gz", hash = "sha256:07530fd65a449e4b0b28dceef14be0d8e0995a7a5b1bb2f3f897c3e548451ce3", size = 3818998 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a1/531897f8caa6c6cc99862cd1c908ddd8a366a51d968e83ab4523ded98b30/pyright-1.1.395-py3-none-any.whl", hash = "sha256:f9bc726870e740c6c77c94657734d90563a3e9765bb523b39f5860198ed75eef", size = 5688787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/b5/98ec41e1e0ad5576ecd42c90ec363560f7b389a441722ea3c7207682dec7/pyright-1.1.397-py3-none-any.whl", hash = "sha256:2e93fba776e714a82b085d68f8345b01f91ba43e1ab9d513e79b70fc85906257", size = 5693631 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
version = "8.3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -932,21 +954,21 @@ dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.25.3"
|
||||
version = "0.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -988,11 +1010,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1018,11 +1040,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.1"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1127,36 +1149,36 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.8"
|
||||
version = "0.11.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/59/ac745a2492986a4c900c73a7a3a10eb4d7a3853e43443519bceecae5eefc/ruff-0.9.8.tar.gz", hash = "sha256:12d455f2be6fe98accbea2487bbb8eaec716c760bf60b45e7e13f76f913f56e9", size = 3715230 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/1c/9de3a463279e9a203104fe80881d7dcfd8377eb52b3d5608770ea6ff3dc6/ruff-0.9.8-py3-none-linux_armv6l.whl", hash = "sha256:d236f0ce0190bbc6fa9b4c4b85e916fb4c50fd087e6558af1bf5a45eb20e374d", size = 10036520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/10/a4eda083ad0b60a4c16bc9a68c6eda59de69a3a58913a0b62541f5c551cd/ruff-0.9.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59fac6922b336d0c38df199761ade561563e1b7636e3a2b767b9ee5a68aa9cbf", size = 10827099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/34/cf7e18f2315926ee2c98f931717e1302f8c3face189f5b99352eb48c5373/ruff-0.9.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a82082ec72bde2166ec138055307396c4d4e543fd97266dc2bfa24284cb30af6", size = 10161605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/08/5e7e8fc08d193e3520b9227249a00bc9b8da9e0a20bf97bef03a9a9f0d38/ruff-0.9.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e06635d12321605d1d11226c7d3c6b1245a0df498099868d14b4e353b3f0ac22", size = 10338840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/c0/df2187618b87334867ea7942f6d2d79ea3e5cb3ed709cfa5c8df115d3715/ruff-0.9.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:65961815bb35d427e957940d13b2a1d0a67d8b245d3a7e0b5a4a2058536d3532", size = 9891009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/39/8fc50b87203e71e6f3281111813ab0f3d6095cb1129efc2cf4c33e977657/ruff-0.9.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c18356beaef174797ad83f11debc5569e96afa73a549b2d073912565cfc4cfd1", size = 11413420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7b/53cd91b99a1cef31126859fb98fdc347c47e0047a9ec51391ea28f08284d/ruff-0.9.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a1dfc443bee0288ea926a4d9ecfd858bf94ddf0a03a256c63e81b2b6dccdfc7d", size = 12138017 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d4/949a328934202a2d2641dcd759761d8ed806e672cbbad0a88e20a46c43ba/ruff-0.9.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc86d5a85cd5ab1d5aff1650f038aa34681d0692cc2467aa9ddef37bd56ea3f9", size = 11592548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/8e/8520a4d97eefedb8472811fd5144fcb1fcbb29f83bb9bb4356a468e7eeac/ruff-0.9.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66662aa19535d58fe6d04e5b59a39e495b102f2f5a2a1b9698e240eb78f429ef", size = 13787277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/68/f1629e00dbc5c9adcd31f12f9438b68c50ab0eefca8b07e11b6c94f11b09/ruff-0.9.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:733647b2fe9367e1aa049c0eba296363746f3bc0dbfd454b0bc4b7b46cdf0146", size = 11275421 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/65/c133462f179b925e49910532c7d7b5a244df5995c155cd2ab9452545926f/ruff-0.9.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:100031be9777f67af7f61b4d4eea2a0531ed6788940aca4360f6b9aae317c53b", size = 10220273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/1e/9339aef1896470380838385dbdc91f62998c37d406009f05ff3b810265f3/ruff-0.9.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f090758d58b4667d9022eee1085a854db93d800279e5a177ebda5adc1faf639", size = 9860266 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/33/2a2934860df6bd3665776ec686fc33910e7a1b793bdd2f000aea3e8f0b65/ruff-0.9.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f774998b9c9a062510533aba9b53085de6be6d41e13a7a0bd086af8a40e838c3", size = 10831947 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/0a7677b1cda4b2367a654f9af57f1dbe58f38c6704da88aee9bbf3941197/ruff-0.9.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6ef7cc80626264ab8ab4d68b359ba867b8a52b0830a9643cd31289146dd40892", size = 11306767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/90/6c98f94e036c8acdf19bd8f3f84d246e43cbcc950e24dc7ff85d2f2735ba/ruff-0.9.8-py3-none-win32.whl", hash = "sha256:54b57b623a683e696a1ede99db95500763c1badafe105b6ad8d8e9d96e385ae2", size = 10234107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e7/35877491b4b64daa35cbd7dc06aa5969e7bb1cd6f69e5594e4376dfbc16d/ruff-0.9.8-py3-none-win_amd64.whl", hash = "sha256:b0878103b2fb8af55ad701308a69ce713108ad346c3a3a143ebcd1e13829c9a7", size = 11357825 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/98/de77a972b2e9ded804dea5d4e6fbfa093d99e81092602567787ea87979af/ruff-0.9.8-py3-none-win_arm64.whl", hash = "sha256:e459a4fc4150fcc60da26c59a6a4b70878c60a99df865a71cf6f958dc68c419a", size = 10435420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "75.8.2"
|
||||
version = "75.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/53/43d99d7687e8cdef5ab5f9ec5eaf2c0423c2b35133a2b7e7bc276fc32b21/setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2", size = 1344083 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/38/7d7362e031bd6dc121e5081d8cb6aa6f6fedf2b67bf889962134c6da4705/setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f", size = 1229385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1188,31 +1210,31 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.38"
|
||||
version = "2.0.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'WIN32' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'aarch64' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'amd64' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'win32' and python_full_version >= '3.12.1') or (python_full_version < '3.14' and platform_machine == 'x86_64' and python_full_version >= '3.12.1')" },
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/08/9a90962ea72acd532bda71249a626344d855c4032603924b1b547694b837/sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", size = 9634782 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f8/6d0424af1442c989b655a7b5f608bc2ae5e4f94cdf6df9f6054f629dc587/SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", size = 2104927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/80/fc06e65fca0a19533e2bfab633a5633ed8b6ee0b9c8d580acf84609ce4da/SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", size = 2095317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2d/5d66605f76b8e344813237dc160a01f03b987201e974b46056a7fb94a874/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", size = 3244735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/8d/b0539e8dce90861efc38fea3eefb15a5d0cfeacf818614762e77a9f192f9/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", size = 3255581 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a5/94e1e44bf5bdffd1782807fcc072542b110b950f0be53f49e68b5f5eca1b/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", size = 3190877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/13/f08b09996dce945aec029c64f61c13b4788541ac588d9288e31e0d3d8850/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", size = 3217485 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8f/8cfe2ba5ba6d8090f4de0e658330c53be6b7bf430a8df1b141c2b180dcdf/SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", size = 2075254 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/5c/e3c77fae41862be1da966ca98eec7fbc07cdd0b00f8b3e1ef2a13eaa6cca/SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", size = 2100865 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/77/caa875a1f5a8a8980b564cc0e6fee1bc992d62d29101252561d0a5e9719c/SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", size = 2100201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/ec/94bb036ec78bf9a20f8010c807105da9152dd84f72e8c51681ad2f30b3fd/SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", size = 2090678 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/63ff1893f146e34d3934c0860209fdd3925c25ee064330e6c2152bacc335/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", size = 3177107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/4f/b933bea41a602b5f274065cc824fae25780ed38664d735575192490a021b/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", size = 3190435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/23/9e654b4059e385988de08c5d3b38a369ea042f4c4d7c8902376fd737096a/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", size = 3123648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/59/94c6d804e76ebc6412a08d2b086a8cb3e5a056cd61508e18ddaf3ec70100/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", size = 3151789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/27/17f143013aabbe1256dce19061eafdce0b0142465ce32168cdb9a18c04b1/SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", size = 2073023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/3e/259404b03c3ed2e7eee4c179e001a07d9b61070334be91124cf4ad32eec7/SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", size = 2096908 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e4/592120713a314621c692211eba034d09becaf6bc8848fabc1dc2a54d8c16/SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", size = 1896347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1230,14 +1252,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.45.3"
|
||||
version = "0.46.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "striprtf"
|
||||
version = "0.0.28"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/3d/b3806c11f90f795284ab19b9561d547779ba8c3acc22a3907ac6afe9ec61/striprtf-0.0.28.tar.gz", hash = "sha256:902806a2e0821faf412130450bdbb84f15e996a729061a51fe7286c620b6fee3", size = 7196 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/63/18dc0365c0d3edc7e81a3c1cef2015079fdf58c8b681fa79ad952be7925b/striprtf-0.0.28-py3-none-any.whl", hash = "sha256:d441f32aeb730c347ccbbbbb06a057b8df1a4e46df0bdb9fca548c601988929c", size = 7667 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1266,23 +1297,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.1"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3"
|
||||
version = "5.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1376,33 +1407,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/8bc4d15af7ff30f7ba34f9a172063bfcee9f5001d7cef04bee800a658f33/websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab", size = 175574 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/1e/92c4547d7b2a93f848aedaf37e9054111bc00dc11bff4385ca3f80dbb412/websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f", size = 174709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/37/eae4830a28061ba552516d84478686b637cd9e57d6a90b45ad69e89cb0af/websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d", size = 172372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2f/b409f8b8aa9328d5a47f7a301a43319d540d70cf036d1e6443675978a988/websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276", size = 172607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/81/d7e2e4542d4b4df849b0110df1b1f94f2647b71ab4b65d672090931ad2bb/websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc", size = 182422 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/91/3b303160938d123eea97f58be363f7dbec76e8c59d587e07b5bc257dd584/websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72", size = 181362 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/8b/df6807f1ca339c567aba9a7ab03bfdb9a833f625e8d2b4fc7529e4c701de/websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d", size = 181787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/37/e6d3d5ebb0ebcaf98ae84904205c9dcaf3e0fe93e65000b9f08631ed7309/websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab", size = 182058 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/df/6aca296f2be4c638ad20908bb3d7c94ce7afc8d9b4b2b0780d1fc59b359c/websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99", size = 181434 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f1/75717a982bab39bbe63c83f9df0e7753e5c98bab907eb4fb5d97fe5c8c11/websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc", size = 181431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/15/cee9e63ed9ac5bfc1a3ae8fc6c02c41745023c21eed622eef142d8fdd749/websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904", size = 175678 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/00/993974c60f40faabb725d4dbae8b072ef73b4c4454bd261d3b1d34ace41f/websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa", size = 176119 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/23/be28dc1023707ac51768f848d28a946443041a348ee3a54abdf9f6283372/websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1", size = 174714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/ff/02b5e9fbb078e7666bf3d25c18c69b499747a12f3e7f2776063ef3fb7061/websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7", size = 172374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/61/901c8d4698e0477eff4c3c664d53f898b601fa83af4ce81946650ec2a4cb/websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081", size = 172605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/4b/dc47601a80dff317aecf8da7b4ab278d11d3494b2c373b493e4887561f90/websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9", size = 182380 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/f7/b155d2b38f05ed47a0b8de1c9ea245fcd7fc625d89f35a37eccba34b42de/websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b", size = 181325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ff/040a20c01c294695cac0e361caf86f33347acc38f164f6d2be1d3e007d9f/websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f", size = 181763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/6a/af23e93678fda8341ac8775e85123425e45c608389d3514863c702896ea5/websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6", size = 182097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/3e/1069e159c30129dc03c01513b5830237e576f47cedb888777dd885cae583/websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375", size = 181485 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/c91c47103f1cd941b576bbc452601e9e01f67d5c9be3e0a9abe726491ab5/websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72", size = 181466 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/32/a4ca6e3d56c24aac46b0cf5c03b841379f6409d07fc2044b244f90f54105/websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c", size = 175673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user