mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
46 Commits
dxt-packaging
...
v0.14.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 73cade27ab | |||
| 7e024a8674 | |||
| 22f7bfa398 | |||
| cd7cee650f | |||
| 105bcaa025 | |||
| 74e12eb782 | |||
| 7a8b08d11e | |||
| 9aa40246a8 | |||
| 7aff836c57 | |||
| 285e96baea | |||
| 2cd2a62f30 | |||
| f3d8d8d617 | |||
| 9743fcd13e | |||
| 65d1984a53 | |||
| b814d40ab1 | |||
| 2438094914 | |||
| 5d74d7407c | |||
| b6aeb3217c | |||
| 08ee7e1201 | |||
| 63ae9ee0e4 | |||
| 0e78751d34 | |||
| 59eae34dee | |||
| b1e55e169e | |||
| 173bff35c1 | |||
| 629c8e47c9 | |||
| 9e4b8bca8f | |||
| b0cc559426 | |||
| 7460a938df | |||
| 43fa5762a8 | |||
| fb1350b294 | |||
| 7585a29c96 | |||
| 752c78c379 | |||
| a4a3b1b689 | |||
| 6361574a20 | |||
| 24a1d6195d | |||
| a0cf62375d | |||
| 473f70c949 | |||
| 2c29dcc2b2 | |||
| 448210e552 | |||
| 3621bb7b4d | |||
| f80ac0ee72 | |||
| 23ddf1918c | |||
| 2aca19aa05 | |||
| 827f7cf3e3 | |||
| bd4f55158b | |||
| 5360005122 |
@@ -14,11 +14,12 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: [ "3.12" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -35,10 +36,18 @@ jobs:
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Install just
|
||||
- name: Install just (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
||||
|
||||
- name: Install just (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
# Install just using Chocolatey (pre-installed on GitHub Actions Windows runners)
|
||||
choco install just --yes
|
||||
shell: pwsh
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -54,4 +63,4 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
uv pip install pytest pytest-cov
|
||||
just test
|
||||
just test
|
||||
@@ -1,5 +1,51 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.14.2 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#204**: Fix MCP Error with MCP-Hub integration
|
||||
([`3621bb7`](https://github.com/basicmachines-co/basic-memory/commit/3621bb7b4d6ac12d892b18e36bb8f7c9101c7b10))
|
||||
- Resolve compatibility issues with MCP-Hub
|
||||
- Improve error handling in project management tools
|
||||
- Ensure stable MCP tool integration across different environments
|
||||
|
||||
- **Modernize datetime handling and suppress SQLAlchemy warnings**
|
||||
([`f80ac0e`](https://github.com/basicmachines-co/basic-memory/commit/f80ac0e3e74b7a737a7fc7b956b5c1d61b0c67b8))
|
||||
- Replace deprecated `datetime.utcnow()` with timezone-aware alternatives
|
||||
- Suppress SQLAlchemy deprecation warnings for cleaner output
|
||||
- Improve future compatibility with Python datetime best practices
|
||||
|
||||
## v0.14.1 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#203**: Constrain fastmcp version to prevent breaking changes
|
||||
([`827f7cf`](https://github.com/basicmachines-co/basic-memory/commit/827f7cf86e7b84c56e7a43bb83f2e5d84a1ad8b8))
|
||||
- Pin fastmcp to compatible version range to avoid API breaking changes
|
||||
- Ensure stable MCP server functionality across updates
|
||||
- Improve dependency management for production deployments
|
||||
|
||||
- **#190**: Fix Problems with MCP integration
|
||||
([`bd4f551`](https://github.com/basicmachines-co/basic-memory/commit/bd4f551a5bb0b7b4d3a5b04de70e08987c6ab2f9))
|
||||
- Resolve MCP server initialization and communication issues
|
||||
- Improve error handling and recovery in MCP operations
|
||||
- Enhance stability for AI assistant integrations
|
||||
|
||||
### Features
|
||||
|
||||
- **Add Cursor IDE integration button** - One-click setup for Cursor IDE users
|
||||
([`5360005`](https://github.com/basicmachines-co/basic-memory/commit/536000512294d66090bf87abc8014f4dfc284310))
|
||||
- Direct installation button for Cursor IDE in README
|
||||
- Streamlined setup process for Cursor users
|
||||
- Enhanced developer experience for AI-powered coding
|
||||
|
||||
- **Add Homebrew installation instructions** - Official Homebrew tap support
|
||||
([`39f811f`](https://github.com/basicmachines-co/basic-memory/commit/39f811f8b57dd998445ae43537cd492c680b2e11))
|
||||
- Official Homebrew formula in basicmachines-co/basic-memory tap
|
||||
- Simplified installation process for macOS users
|
||||
- Package manager integration for easier dependency management
|
||||
|
||||
## v0.14.0 (2025-06-26)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,34 +1,71 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
https://developercertificate.org/
|
||||
# Contributor License Agreement
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
## Copyright Assignment and License Grant
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
By signing this Contributor License Agreement ("Agreement"), you accept and agree to the following terms and conditions
|
||||
for your present and future Contributions submitted
|
||||
to Basic Machines LLC. Except for the license granted herein to Basic Machines LLC and recipients of software
|
||||
distributed by Basic Machines LLC, you reserve all right,
|
||||
title, and interest in and to your Contributions.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
### 1. Definitions
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this
|
||||
Agreement with Basic Machines LLC.
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work,
|
||||
that is intentionally submitted by You to Basic
|
||||
Machines LLC for inclusion in, or documentation of, any of the products owned or managed by Basic Machines LLC (the "
|
||||
Work").
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
### 2. Grant of Copyright License
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
Subject to the terms and conditions of this Agreement, You hereby grant to Basic Machines LLC and to recipients of
|
||||
software distributed by Basic Machines LLC a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the
|
||||
Work, and to permit persons to whom the Work is furnished to do so.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
### 3. Assignment of Copyright
|
||||
|
||||
You hereby assign to Basic Machines LLC all right, title, and interest worldwide in all Copyright covering your
|
||||
Contributions. Basic Machines LLC may license the
|
||||
Contributions under any license terms, including copyleft, permissive, commercial, or proprietary licenses.
|
||||
|
||||
### 4. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this Agreement, You hereby grant to Basic Machines LLC and to recipients of
|
||||
software distributed by Basic Machines LLC a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to
|
||||
make, have made, use, offer to sell, sell, import, and
|
||||
otherwise transfer the Work.
|
||||
|
||||
### 5. Developer Certificate of Origin
|
||||
|
||||
By making a Contribution to this project, You certify that:
|
||||
|
||||
(a) The Contribution was created in whole or in part by You and You have the right to submit it under this Agreement; or
|
||||
|
||||
(b) The Contribution is based upon previous work that, to the best of Your knowledge, is covered under an appropriate
|
||||
open source license and You have the right under that
|
||||
license to submit that work with modifications, whether created in whole or in part by You, under this Agreement; or
|
||||
|
||||
(c) The Contribution was provided directly to You by some other person who certified (a), (b) or (c) and You have not
|
||||
modified it.
|
||||
|
||||
(d) You understand and agree that this project and the Contribution are public and that a record of the Contribution (
|
||||
including all personal information You submit with
|
||||
it, including Your sign-off) is maintained indefinitely and may be redistributed consistent with this project or the
|
||||
open source license(s) involved.
|
||||
|
||||
### 6. Representations
|
||||
|
||||
You represent that you are legally entitled to grant the above license and assignment. If your employer(s) has rights to
|
||||
intellectual property that you create that
|
||||
includes your Contributions, you represent that you have received permission to make Contributions on behalf of that
|
||||
employer, or that your employer has waived such rights
|
||||
for your Contributions to Basic Machines LLC.
|
||||
|
||||
---
|
||||
|
||||
This Agreement is effective as of the date you first submit a Contribution to Basic Machines LLC.
|
||||
|
||||
@@ -14,15 +14,15 @@ See the [README.md](README.md) file for a project overview.
|
||||
|
||||
### Build and Test Commands
|
||||
|
||||
- Install: `just install` or `pip install -e ".[dev]"`
|
||||
- Run tests: `uv run pytest -p pytest_mock -v` or `just test`
|
||||
- Install: `make install` or `pip install -e ".[dev]"`
|
||||
- Run tests: `uv run pytest -p pytest_mock -v` or `make test`
|
||||
- Single test: `pytest tests/path/to/test_file.py::test_function_name`
|
||||
- Lint: `just lint` or `ruff check . --fix`
|
||||
- Type check: `just type-check` or `uv run pyright`
|
||||
- Format: `just format` or `uv run ruff format .`
|
||||
- Run all code checks: `just check` (runs lint, format, type-check, test)
|
||||
- Create db migration: `just migration "Your migration message"`
|
||||
- Run development MCP Inspector: `just run-inspector`
|
||||
- Lint: `make lint` or `ruff check . --fix`
|
||||
- Type check: `make type-check` or `uv run pyright`
|
||||
- Format: `make format` or `uv run ruff format .`
|
||||
- Run all code checks: `make check` (runs lint, format, type-check, test)
|
||||
- Create db migration: `make migration m="Your migration message"`
|
||||
- Run development MCP Inspector: `make run-inspector`
|
||||
|
||||
### Code Style Guidelines
|
||||
|
||||
@@ -37,7 +37,6 @@ 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
|
||||
|
||||
@@ -65,7 +64,6 @@ See the [README.md](README.md) file for a project overview.
|
||||
- Test database uses in-memory SQLite
|
||||
- Avoid creating mocks in tests in most circumstances.
|
||||
- Each test runs in a standalone environment with in memory SQLite and tmp_file directory
|
||||
- Do not use mocks in tests if possible. Tests run with an in memory sqlite db, so they are not needed. See fixtures in conftest.py
|
||||
|
||||
## BASIC MEMORY PRODUCT USAGE
|
||||
|
||||
@@ -97,29 +95,18 @@ See the [README.md](README.md) file for a project overview.
|
||||
|
||||
**Content Management:**
|
||||
- `write_note(title, content, folder, tags)` - Create/update markdown notes with semantic observations and relations
|
||||
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph awareness
|
||||
- `edit_note(identifier, operation, content)` - Edit notes incrementally (append, prepend, find/replace, section replace)
|
||||
- `move_note(identifier, destination_path)` - Move notes with database consistency and search reindexing
|
||||
- `view_note(identifier)` - Display notes as formatted artifacts for better readability in Claude Desktop
|
||||
- `read_content(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
|
||||
- `delete_note(identifier)` - Delete notes from knowledge base
|
||||
|
||||
**Project Management:**
|
||||
- `list_memory_projects()` - List all available projects with status indicators
|
||||
- `switch_project(project_name)` - Switch to different project context during conversations
|
||||
- `get_current_project()` - Show currently active project with statistics
|
||||
- `create_memory_project(name, path, set_default)` - Create new Basic Memory projects
|
||||
- `delete_project(name)` - Delete projects from configuration and database
|
||||
- `set_default_project(name)` - Set default project in config
|
||||
- `sync_status()` - Check file synchronization status and background operations
|
||||
- `read_note(identifier, page, page_size)` - Read notes by title, permalink, or memory:// URL with knowledge graph
|
||||
awareness
|
||||
- `read_file(path)` - Read raw file content (text, images, binaries) without knowledge graph processing
|
||||
|
||||
**Knowledge Graph Navigation:**
|
||||
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation continuity
|
||||
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "1d", "1 week")
|
||||
- `list_directory(dir_name, depth, file_name_glob)` - List directory contents with filtering and depth control
|
||||
- `build_context(url, depth, timeframe)` - Navigate the knowledge graph via memory:// URLs for conversation
|
||||
continuity
|
||||
- `recent_activity(type, depth, timeframe)` - Get recently updated information with specified timeframe (e.g., "
|
||||
1d", "1 week")
|
||||
|
||||
**Search & Discovery:**
|
||||
- `search_notes(query, page, page_size)` - Full-text search across all content with filtering options
|
||||
- `search(query, page, page_size)` - Full-text search across all content with filtering options
|
||||
|
||||
**Visualization:**
|
||||
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
|
||||
@@ -127,7 +114,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_notes(query, after_date)` - Search with detailed, formatted results for better context understanding
|
||||
- `search(query, after_date)` - Search with detailed, formatted results for better context understanding
|
||||
- `recent_activity(timeframe)` - View recently changed items with formatted output
|
||||
- `json_canvas_spec()` - Full JSON Canvas specification for Obsidian visualization
|
||||
|
||||
@@ -147,32 +134,30 @@ could achieve independently.
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
Basic Memory uses Claude directly into the development workflow through GitHub:
|
||||
Basic Memory has taken AI-Human collaboration to the next level by integrating Claude directly into the development workflow through GitHub:
|
||||
|
||||
### GitHub MCP Tools
|
||||
|
||||
Using the GitHub Model Context Protocol server, Claude can:
|
||||
Using the GitHub Model Context Protocol server, Claude can now:
|
||||
|
||||
- **Repository Management**:
|
||||
- View repository files and structure
|
||||
- Read file contents
|
||||
- Create new branches
|
||||
- Create and update files
|
||||
- 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
|
||||
- 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
|
||||
- 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.
|
||||
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
|
||||
|
||||
@@ -183,75 +168,4 @@ With GitHub integration, the development workflow includes:
|
||||
3. **Branch management** - Claude can create feature branches for implementations
|
||||
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
|
||||
|
||||
With this integration, the AI assistant is a full-fledged team member rather than just a tool for generating code
|
||||
snippets.
|
||||
|
||||
|
||||
### Basic Memory Pro
|
||||
|
||||
Basic Memory Pro is a desktop GUI application that wraps the basic-memory CLI/MCP tools:
|
||||
|
||||
- Built with Tauri (Rust), React (TypeScript), and a Python FastAPI sidecar
|
||||
- Provides visual knowledge graph exploration and project management
|
||||
- Uses the same core codebase but adds a desktop-friendly interface
|
||||
- Project configuration is shared between CLI and Pro versions
|
||||
- Multiple project support with visual switching interface
|
||||
|
||||
local repo: /Users/phernandez/dev/basicmachines/basic-memory-pro
|
||||
github: https://github.com/basicmachines-co/basic-memory-pro
|
||||
|
||||
## Release and Version Management
|
||||
|
||||
Basic Memory uses `uv-dynamic-versioning` for automatic version management based on git tags:
|
||||
|
||||
### Version Types
|
||||
- **Development versions**: Automatically generated from commits (e.g., `0.12.4.dev26+468a22f`)
|
||||
- **Beta releases**: Created by tagging with beta suffixes (e.g., `v0.13.0b1`, `v0.13.0rc1`)
|
||||
- **Stable releases**: Created by tagging with version numbers (e.g., `v0.13.0`)
|
||||
|
||||
### Release Workflows
|
||||
|
||||
#### Development Builds (Automatic)
|
||||
- Triggered on every push to `main` branch
|
||||
- Publishes dev versions like `0.12.4.dev26+468a22f` to PyPI
|
||||
- Allows continuous testing of latest changes
|
||||
- Users install with: `pip install basic-memory --pre --force-reinstall`
|
||||
|
||||
#### Beta/RC Releases (Manual)
|
||||
- Create beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
|
||||
- Automatically builds and publishes to PyPI as pre-release
|
||||
- Users install with: `pip install basic-memory --pre`
|
||||
- Use for milestone testing before stable release
|
||||
|
||||
#### Stable Releases (Automated)
|
||||
- Use the automated release system: `just release v0.13.0`
|
||||
- Includes comprehensive quality checks (lint, format, type-check, tests)
|
||||
- Automatically updates version in `__init__.py`
|
||||
- Creates git tag and pushes to GitHub
|
||||
- Triggers GitHub Actions workflow for:
|
||||
- PyPI publication
|
||||
- Homebrew formula update (requires HOMEBREW_TOKEN secret)
|
||||
|
||||
**Manual method (legacy):**
|
||||
- Create version tag: `git tag v0.13.0 && git push origin v0.13.0`
|
||||
|
||||
#### Homebrew Formula Updates
|
||||
- Automatically triggered after successful PyPI release for **stable releases only**
|
||||
- **Stable releases** (e.g., v0.13.7) automatically update the main `basic-memory` formula
|
||||
- **Pre-releases** (dev/beta/rc) are NOT automatically updated - users must specify version manually
|
||||
- Updates formula in `basicmachines-co/homebrew-basic-memory` repo
|
||||
- Requires `HOMEBREW_TOKEN` secret in GitHub repository settings:
|
||||
- Create a fine-grained Personal Access Token with `Contents: Read and Write` and `Actions: Read` scopes on `basicmachines-co/homebrew-basic-memory`
|
||||
- Add as repository secret named `HOMEBREW_TOKEN` in `basicmachines-co/basic-memory`
|
||||
- Formula updates include new version URL and SHA256 checksum
|
||||
|
||||
### For Development
|
||||
- **Automated releases**: Use `just release v0.13.x` for stable releases and `just beta v0.13.0b1` for beta releases
|
||||
- **Quality gates**: All releases require passing lint, format, type-check, and test suites
|
||||
- **Version management**: Versions automatically derived from git tags via `uv-dynamic-versioning`
|
||||
- **Configuration**: `pyproject.toml` uses `dynamic = ["version"]`
|
||||
- **Release automation**: `__init__.py` updated automatically during release process
|
||||
- **CI/CD**: GitHub Actions handles building and PyPI publication
|
||||
|
||||
## Development Notes
|
||||
- make sure you sign off on commits
|
||||
This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.
|
||||
+6
-1
@@ -27,7 +27,12 @@ project and how to get started as a developer.
|
||||
|
||||
> **Note**: Basic Memory uses [just](https://just.systems) as a modern command runner. Install with `brew install just` or `cargo install just`.
|
||||
|
||||
3. **Run the Tests**:
|
||||
3. **Activate the Virtual Environment**
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
4. **Run the Tests**:
|
||||
```bash
|
||||
# Run all tests
|
||||
just test
|
||||
|
||||
+15
-2
@@ -1,5 +1,9 @@
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
# Build arguments for user ID and group ID (defaults to 1000)
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
|
||||
# Copy uv from official image
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
@@ -7,6 +11,11 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Create a group and user with the provided UID/GID
|
||||
# Check if the GID already exists, if not create appgroup
|
||||
RUN (getent group ${GID} || groupadd --gid ${GID} appgroup) && \
|
||||
useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash appuser
|
||||
|
||||
# Copy the project into the image
|
||||
ADD . /app
|
||||
|
||||
@@ -14,13 +23,17 @@ ADD . /app
|
||||
WORKDIR /app
|
||||
RUN uv sync --locked
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
# Create necessary directories and set ownership
|
||||
RUN mkdir -p /app/data /app/.basic-memory && \
|
||||
chown -R appuser:${GID} /app
|
||||
|
||||
# Set default data directory and add venv to PATH
|
||||
ENV BASIC_MEMORY_HOME=/app/data \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Switch to the non-root user
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# Legal File Inventory Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide documents the comprehensive file inventory script created for Basic Memory's legal documentation needs, including copyright assignments and company agreement exhibits.
|
||||
|
||||
## Repository Analysis Summary
|
||||
|
||||
### Repository Structure
|
||||
- **Primary Language**: Python (3,958 files)
|
||||
- **License**: GNU Affero General Public License v3.0
|
||||
- **Total Contributors**: 12 identified contributors
|
||||
- **Main Contributors**:
|
||||
- `phernandez@basicmachines.co` / `paul@basicmachines.co` (Paul Hernandez) - 700+ commits
|
||||
- `groksrc@gmail.com` / `groksrc@users.noreply.github.com` (Drew Cain) - 18+ commits
|
||||
- Various AI bots and automated systems
|
||||
|
||||
### File Type Distribution
|
||||
Based on the analysis, the repository contains:
|
||||
|
||||
| Extension | Count | Purpose |
|
||||
|-----------|-------|---------|
|
||||
| `.py` | 3,958 | Python source code |
|
||||
| `.pyi` | 4,536 | Python type stubs (dependencies) |
|
||||
| `.pyc` | 1,435 | Compiled Python (excluded from inventory) |
|
||||
| `.md` | 34 | Documentation files |
|
||||
| `.toml` | 179 | Configuration files |
|
||||
| `.html` | 119 | Coverage reports and documentation |
|
||||
| `.txt` | 127 | Various text files |
|
||||
|
||||
### Contributors by Email Domain
|
||||
|
||||
**Basic Machines Contributors:**
|
||||
- `phernandez@basicmachines.co` - 547 commits (Primary maintainer)
|
||||
- `paul@basicmachines.co` / `paulmh@gmail.com` - 170 commits (Paul Hernandez)
|
||||
- `claude@basicmachines.co` - 3 commits (AI assistant)
|
||||
|
||||
**External Contributors:**
|
||||
- `groksrc@gmail.com` / `groksrc@users.noreply.github.com` - 18 commits (Drew Cain)
|
||||
- Various one-time contributors (1-2 commits each)
|
||||
|
||||
**Automated Systems:**
|
||||
- GitHub Actions, semantic-release, and other bots
|
||||
|
||||
## Legal Inventory Script Features
|
||||
|
||||
### What It Includes
|
||||
|
||||
**Source Files:**
|
||||
- All Python source code (`.py` files)
|
||||
- Configuration files (`.toml`, `.yaml`, `.json`, etc.)
|
||||
- Documentation (`.md`, `.rst`, `.txt`)
|
||||
- Build and deployment scripts
|
||||
- Database migrations and SQL files
|
||||
- Legal and license files
|
||||
|
||||
**Metadata for Each File:**
|
||||
- File path, size, and creation/modification dates
|
||||
- Git history (creation date, last modified, commit count)
|
||||
- Contributors and their line contributions
|
||||
- Primary author identification
|
||||
- File categorization
|
||||
- SHA-256 hash for integrity verification
|
||||
|
||||
### What It Excludes
|
||||
|
||||
**Generated/Build Artifacts:**
|
||||
- `__pycache__/` and `.pyc` files
|
||||
- Build directories (`build/`, `dist/`, `htmlcov/`)
|
||||
- Coverage reports and cache files
|
||||
|
||||
**Dependencies:**
|
||||
- Virtual environment files (`.venv/`, `venv/`)
|
||||
- Third-party packages (`site-packages/`, `*.dist-info/`)
|
||||
- Lock files (`uv.lock`, `package-lock.json`)
|
||||
|
||||
**IDE/Editor Files:**
|
||||
- `.idea/`, `.vscode/`, `.DS_Store`
|
||||
- Temporary and swap files
|
||||
|
||||
**Version Control:**
|
||||
- `.git/` directory contents
|
||||
- Git configuration files
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Generate CSV inventory (default)
|
||||
python3 legal_file_inventory.py
|
||||
|
||||
# Generate Markdown report
|
||||
python3 legal_file_inventory.py --format markdown --output legal_report.md
|
||||
|
||||
# Generate JSON with full metadata
|
||||
python3 legal_file_inventory.py --format json --output legal_data.json
|
||||
|
||||
# Specify different repository path
|
||||
python3 legal_file_inventory.py --repo-path /path/to/repo --output inventory.csv
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
- `--output`, `-o`: Output file path (default: `basic_memory_legal_inventory.csv`)
|
||||
- `--format`, `-f`: Output format - `csv`, `json`, or `markdown` (default: `csv`)
|
||||
- `--repo-path`, `-r`: Repository path (default: current directory)
|
||||
|
||||
### Output Formats
|
||||
|
||||
**CSV Format:**
|
||||
- Suitable for spreadsheet applications
|
||||
- Contains all metadata fields
|
||||
- Contributors stored as JSON string in separate column
|
||||
|
||||
**JSON Format:**
|
||||
- Complete structured data
|
||||
- Includes summary statistics and detailed file information
|
||||
- Best for programmatic processing
|
||||
|
||||
**Markdown Format:**
|
||||
- Human-readable report
|
||||
- Summary statistics and contributor rankings
|
||||
- Detailed table of all files
|
||||
|
||||
## Legal Documentation Applications
|
||||
|
||||
### Copyright Assignment Use Cases
|
||||
|
||||
1. **Contributor Identification**: The script identifies all contributors to each file based on git blame analysis
|
||||
2. **Primary Author Recognition**: Determines the primary author of each file (contributor with most lines)
|
||||
3. **Contribution Metrics**: Provides line counts and commit counts per contributor
|
||||
4. **File Categorization**: Groups files by purpose (source code, documentation, configuration, etc.)
|
||||
|
||||
### Company Agreement Exhibits
|
||||
|
||||
The inventory provides comprehensive documentation of:
|
||||
|
||||
1. **Intellectual Property Scope**: All source code files and their origins
|
||||
2. **Contributor Tracking**: Complete list of all individuals who have contributed code
|
||||
3. **File Integrity**: SHA-256 hashes for verification of file contents
|
||||
4. **Historical Documentation**: Git creation dates and modification history
|
||||
|
||||
### Due Diligence Documentation
|
||||
|
||||
The script generates data suitable for:
|
||||
|
||||
1. **Legal Review**: Comprehensive file listing with contributor information
|
||||
2. **IP Audit**: Identification of all copyright holders
|
||||
3. **License Compliance**: Verification of file ownership and licensing
|
||||
4. **Asset Documentation**: Complete inventory of company code assets
|
||||
|
||||
## Integration with Legal Processes
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. **Generate Inventory**: Run the script to create current file inventory
|
||||
2. **Legal Review**: Have legal counsel review the contributor list and file categorization
|
||||
3. **Copyright Assignment**: Use contributor data to ensure proper copyright assignments
|
||||
4. **Document Attachment**: Include inventory as exhibit in company agreements
|
||||
5. **Regular Updates**: Re-run inventory for significant releases or legal milestones
|
||||
|
||||
### Key Legal Considerations
|
||||
|
||||
**AGPL-3.0 License:**
|
||||
- All files are under AGPL-3.0 unless otherwise specified
|
||||
- Contributors retain copyright but license under AGPL-3.0 terms
|
||||
- Company needs proper copyright assignments for proprietary licensing
|
||||
|
||||
**Contributor Rights:**
|
||||
- External contributors may retain rights to their contributions
|
||||
- Proper contributor license agreements (CLAs) should be in place
|
||||
- AI-generated content may have different legal status
|
||||
|
||||
**File Categories for Legal Review:**
|
||||
- **Source Code**: Core IP, requires copyright assignment
|
||||
- **Configuration**: May contain proprietary deployment information
|
||||
- **Documentation**: Usually less sensitive but may contain trade secrets
|
||||
- **Legal/License**: Critical for compliance verification
|
||||
|
||||
## Maintenance and Updates
|
||||
|
||||
### When to Regenerate Inventory
|
||||
|
||||
- Before major releases
|
||||
- During legal document preparation
|
||||
- After significant contributor additions
|
||||
- For annual compliance reviews
|
||||
- During acquisition or investment processes
|
||||
|
||||
### Validation Steps
|
||||
|
||||
1. Verify contributor email mapping is accurate
|
||||
2. Check that file categorization makes sense
|
||||
3. Ensure excluded files are appropriate
|
||||
4. Review contributor counts against expectations
|
||||
5. Validate file hashes for integrity
|
||||
|
||||
## Technical Implementation Notes
|
||||
|
||||
### Git Integration
|
||||
- Uses `git blame --line-porcelain` for detailed contributor analysis
|
||||
- Tracks file history with `git log --follow`
|
||||
- Handles renamed files and complex git histories
|
||||
|
||||
### Performance Considerations
|
||||
- Scans repository incrementally to handle large codebases
|
||||
- Excludes binary dependencies to reduce processing time
|
||||
- Caches git operations where possible
|
||||
|
||||
### Error Handling
|
||||
- Gracefully handles files not in git
|
||||
- Continues processing if individual files fail
|
||||
- Provides detailed error reporting
|
||||
|
||||
## Security and Privacy
|
||||
|
||||
### Data Sensitivity
|
||||
- Contains contributor names and email addresses
|
||||
- May reveal internal file structure and organization
|
||||
- Should be treated as confidential legal documentation
|
||||
|
||||
### Recommended Handling
|
||||
- Limit access to legal and executive team
|
||||
- Store in secure, access-controlled systems
|
||||
- Consider redacting contributor emails for external sharing
|
||||
- Regular cleanup of generated inventory files
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Git Not Available:**
|
||||
- Ensure git is installed and repository is initialized
|
||||
- Check that the script is run from within the repository
|
||||
|
||||
**Permission Errors:**
|
||||
- Ensure read access to all repository files
|
||||
- Check write permissions for output directory
|
||||
|
||||
**Large Repository Performance:**
|
||||
- Consider running on subsets of files for very large repositories
|
||||
- Use `--repo-path` to target specific subdirectories
|
||||
|
||||
**Contributor Mapping Issues:**
|
||||
- Git usernames may not match real identities
|
||||
- Consider post-processing to normalize contributor names
|
||||
- Review .mailmap files for git identity consolidation
|
||||
|
||||
This comprehensive legal file inventory system provides the foundation for proper intellectual property documentation and legal compliance for the Basic Memory project.
|
||||
@@ -13,11 +13,8 @@ Basic Memory lets you build persistent knowledge through natural conversations w
|
||||
Claude, while keeping everything in simple Markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
||||
enable any compatible LLM to read and write to your local knowledge base.
|
||||
|
||||
- Website: https://basicmemory.com
|
||||
- Company: https://basicmachines.co
|
||||
- Website: https://basicmachines.co
|
||||
- Documentation: https://memory.basicmachines.co
|
||||
- Discord: https://discord.gg/tyvKNccgqN
|
||||
- YouTube: https://www.youtube.com/@basicmachines-co
|
||||
|
||||
## Pick up your conversation right where you left off
|
||||
|
||||
@@ -33,10 +30,6 @@ https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
|
||||
# Install with uv (recommended)
|
||||
uv tool install basic-memory
|
||||
|
||||
# or with Homebrew
|
||||
brew tap basicmachines-co/basic-memory
|
||||
brew install basic-memory
|
||||
|
||||
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
|
||||
# Add this to your config:
|
||||
{
|
||||
@@ -68,7 +61,8 @@ Memory for Claude Desktop:
|
||||
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
|
||||
```
|
||||
|
||||
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. Note: The Smithery installation uses their hosted MCP server, while your data remains stored locally as Markdown files.
|
||||
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. The
|
||||
Smithery server hosts the MCP server component, while your data remains stored locally as Markdown files.
|
||||
|
||||
### Glama.ai
|
||||
|
||||
@@ -159,8 +153,7 @@ The note embeds semantic content and links to other topics via simple Markdown f
|
||||
|
||||
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
|
||||
|
||||
- Realtime sync is enabled by default starting with v0.12.0
|
||||
- Project switching during conversations is supported starting with v0.13.0
|
||||
- Realtime sync can be enabled via running `basic-memory sync --watch`
|
||||
|
||||
4. In a chat with the LLM, you can reference a topic:
|
||||
|
||||
@@ -270,13 +263,6 @@ Examples of relations:
|
||||
```
|
||||
|
||||
## Using with VS Code
|
||||
For one-click installation, click one of the install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=basic-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22basic-memory%22%2C%22mcp%22%5D%7D&quality=insiders)
|
||||
|
||||
You can use Basic Memory with VS Code to easily retrieve and store information while coding. Click the installation buttons above for one-click setup, or follow the manual installation instructions below.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
@@ -306,6 +292,8 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
|
||||
}
|
||||
```
|
||||
|
||||
You can use Basic Memory with VS Code to easily retrieve and store information while coding.
|
||||
|
||||
## Using with Claude Desktop
|
||||
|
||||
Basic Memory is built using the MCP (Model Context Protocol) and works with the Claude desktop app (https://claude.ai/):
|
||||
@@ -329,8 +317,7 @@ for OS X):
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a specific project (see [Multiple Projects](docs/User%20Guide.md#multiple-projects)), update your
|
||||
Claude Desktop
|
||||
If you want to use a specific project (see [Multiple Projects](#multiple-projects) below), update your Claude Desktop
|
||||
config:
|
||||
|
||||
```json
|
||||
@@ -340,9 +327,9 @@ config:
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp",
|
||||
"--project",
|
||||
"your-project-name",
|
||||
"mcp"
|
||||
"your-project-name"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -351,27 +338,23 @@ config:
|
||||
|
||||
2. Sync your knowledge:
|
||||
|
||||
Basic Memory will sync the files in your project in real time if you make manual edits.
|
||||
```bash
|
||||
# One-time sync of local knowledge updates
|
||||
basic-memory sync
|
||||
|
||||
# Run realtime sync process (recommended)
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
3. In Claude Desktop, the LLM can now use these tools:
|
||||
|
||||
```
|
||||
write_note(title, content, folder, tags) - Create or update notes
|
||||
read_note(identifier, page, page_size) - Read notes by title or permalink
|
||||
edit_note(identifier, operation, content) - Edit notes incrementally (append, prepend, find/replace)
|
||||
move_note(identifier, destination_path) - Move notes with database consistency
|
||||
view_note(identifier) - Display notes as formatted artifacts for better readability
|
||||
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
|
||||
search_notes(query, page, page_size) - Search across your knowledge base
|
||||
search(query, page, page_size) - Search across your knowledge base
|
||||
recent_activity(type, depth, timeframe) - Find recently updated information
|
||||
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
|
||||
list_memory_projects() - List all available projects with status
|
||||
switch_project(project_name) - Switch to different project context
|
||||
get_current_project() - Show current project and statistics
|
||||
create_memory_project(name, path, set_default) - Create new projects
|
||||
delete_project(name) - Delete projects from configuration
|
||||
set_default_project(name) - Set default project
|
||||
sync_status() - Check file synchronization status
|
||||
```
|
||||
|
||||
5. Example prompts to try:
|
||||
@@ -382,10 +365,6 @@ sync_status() - Check file synchronization status
|
||||
"Create a canvas visualization of my project components"
|
||||
"Read my notes on the authentication system"
|
||||
"What have I been working on in the past week?"
|
||||
"Switch to my work-notes project"
|
||||
"List all my available projects"
|
||||
"Edit my coffee brewing note to add a new technique"
|
||||
"Move my old meeting notes to the archive folder"
|
||||
```
|
||||
|
||||
## Futher info
|
||||
@@ -397,49 +376,6 @@ See the [Documentation](https://memory.basicmachines.co/) for more info, includi
|
||||
- [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
|
||||
- [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Stable Release
|
||||
```bash
|
||||
pip install basic-memory
|
||||
```
|
||||
|
||||
### Beta/Pre-releases
|
||||
```bash
|
||||
pip install basic-memory --pre
|
||||
```
|
||||
|
||||
### Development Builds
|
||||
Development versions are automatically published on every commit to main with versions like `0.12.4.dev26+468a22f`:
|
||||
```bash
|
||||
pip install basic-memory --pre --force-reinstall
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Run Basic Memory in a container with volume mounting for your Obsidian vault:
|
||||
|
||||
```bash
|
||||
# Clone and start with Docker Compose
|
||||
git clone https://github.com/basicmachines-co/basic-memory.git
|
||||
cd basic-memory
|
||||
|
||||
# Edit docker-compose.yml to point to your Obsidian vault
|
||||
# Then start the container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Or use Docker directly:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name basic-memory-server \
|
||||
-v /path/to/your/obsidian-vault:/data/knowledge:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
ghcr.io/basicmachines-co/basic-memory:latest
|
||||
```
|
||||
|
||||
See [Docker Setup Guide](docs/Docker.md) for detailed configuration options, multiple project setup, and integration examples.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
@@ -457,4 +393,4 @@ and submitting PRs.
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
Built with ♥️ by Basic Machines
|
||||
Built with ♥️ by Basic Machines
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create CSV exhibits for individual contributors
|
||||
"""
|
||||
|
||||
import json
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
def create_csv_exhibits():
|
||||
"""Create CSV Exhibit A files for each contributor."""
|
||||
|
||||
# Read the JSON inventory
|
||||
inventory_files = list(Path("legal_inventory_main").glob("*.json"))
|
||||
if not inventory_files:
|
||||
print("Error: No JSON inventory files found")
|
||||
return
|
||||
|
||||
inventory_file = inventory_files[0] # Use the most recent one
|
||||
print(f"Using inventory file: {inventory_file}")
|
||||
|
||||
with open(inventory_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
files = data['files']
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path("legal_exhibits")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Contributors we need exhibits for
|
||||
target_contributors = {
|
||||
'jope-bm': 'joe_exhibit_a.csv',
|
||||
'Drew Cain': 'drew_cain_exhibit_a.csv'
|
||||
}
|
||||
|
||||
print("Creating CSV exhibits for contributors...")
|
||||
|
||||
for contributor_key, filename in target_contributors.items():
|
||||
# Find files for this contributor
|
||||
contributor_files = []
|
||||
|
||||
for file_info in files:
|
||||
# Check if this contributor is listed in the file's contributors
|
||||
for contrib in file_info.get('contributors', []):
|
||||
if contributor_key in contrib['name']:
|
||||
contributor_files.append(file_info)
|
||||
break
|
||||
|
||||
if not contributor_files:
|
||||
print(f"No files found for {contributor_key}")
|
||||
continue
|
||||
|
||||
# Sort files by path
|
||||
contributor_files.sort(key=lambda x: x['path'])
|
||||
|
||||
# Create CSV file
|
||||
csv_file = output_dir / filename
|
||||
|
||||
with open(csv_file, 'w', newline='', encoding='utf-8') as csvfile:
|
||||
fieldnames = [
|
||||
'file_path', 'file_name', 'category', 'size_bytes',
|
||||
'modified_date', 'primary_author', 'all_contributors', 'sha256_hash'
|
||||
]
|
||||
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for file_info in contributor_files:
|
||||
contributors_list = '; '.join([c['name'] for c in file_info['contributors']])
|
||||
|
||||
writer.writerow({
|
||||
'file_path': file_info['path'],
|
||||
'file_name': file_info['name'],
|
||||
'category': file_info['category'],
|
||||
'size_bytes': file_info['size_bytes'],
|
||||
'modified_date': file_info['modified_time'][:10],
|
||||
'primary_author': file_info['primary_author'],
|
||||
'all_contributors': contributors_list,
|
||||
'sha256_hash': file_info['sha256_hash']
|
||||
})
|
||||
|
||||
print(f"Created CSV exhibit for {contributor_key}: {csv_file}")
|
||||
print(f" - {len(contributor_files)} files")
|
||||
print(f" - {sum(f['size_bytes'] for f in contributor_files):,} bytes")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_csv_exhibits()
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create individual Exhibit A files for copyright assignments
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def create_individual_exhibits():
|
||||
"""Create individual Exhibit A files for each contributor."""
|
||||
|
||||
# Read the JSON inventory
|
||||
inventory_file = Path("legal_inventory_main/basic_memory_inventory_20250730_101521.json")
|
||||
|
||||
if not inventory_file.exists():
|
||||
print(f"Error: {inventory_file} not found")
|
||||
return
|
||||
|
||||
with open(inventory_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
files = data['files']
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path("legal_exhibits")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Contributors we need exhibits for (based on copyright assignments)
|
||||
target_contributors = {
|
||||
'jope-bm': 'Joseph "Joe" [Last Name]', # Need to get his full name
|
||||
'Drew Cain': 'Drew Cain'
|
||||
}
|
||||
|
||||
print("Creating individual contributor exhibits...")
|
||||
|
||||
for contributor_key, full_name in target_contributors.items():
|
||||
# Find files for this contributor
|
||||
contributor_files = []
|
||||
|
||||
for file_info in files:
|
||||
# Check if this contributor is listed in the file's contributors
|
||||
for contrib in file_info.get('contributors', []):
|
||||
if contributor_key in contrib['name']:
|
||||
contributor_files.append(file_info)
|
||||
break
|
||||
|
||||
if not contributor_files:
|
||||
print(f"No files found for {contributor_key}")
|
||||
continue
|
||||
|
||||
# Sort files by path
|
||||
contributor_files.sort(key=lambda x: x['path'])
|
||||
|
||||
# Create exhibit markdown
|
||||
exhibit_content = f"""# Exhibit A - Assigned Works
|
||||
## Copyright Assignment: {full_name} to Basic Memory LLC
|
||||
|
||||
**Date:** [To be filled]
|
||||
**Assignor:** {full_name}
|
||||
**Assignee:** Basic Memory LLC
|
||||
|
||||
## Summary
|
||||
- **Total Files:** {len(contributor_files)}
|
||||
- **Total Size:** {sum(f['size_bytes'] for f in contributor_files):,} bytes
|
||||
- **Categories:** {', '.join(set(f['category'] for f in contributor_files))}
|
||||
|
||||
## Detailed File List
|
||||
|
||||
"""
|
||||
|
||||
# Group by category
|
||||
categories = {}
|
||||
for file_info in contributor_files:
|
||||
category = file_info['category']
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(file_info)
|
||||
|
||||
# Add files by category
|
||||
for category, category_files in sorted(categories.items()):
|
||||
exhibit_content += f"### {category.replace('_', ' ').title()}\n\n"
|
||||
|
||||
for file_info in category_files:
|
||||
exhibit_content += f"**{file_info['path']}**\n"
|
||||
exhibit_content += f"- Size: {file_info['size_bytes']:,} bytes\n"
|
||||
exhibit_content += f"- Modified: {file_info['modified_time'][:10]}\n"
|
||||
exhibit_content += f"- Primary Author: {file_info['primary_author']}\n"
|
||||
|
||||
# Show all contributors for this file
|
||||
if len(file_info['contributors']) > 1:
|
||||
contributors_list = ', '.join([c['name'] for c in file_info['contributors']])
|
||||
exhibit_content += f"- All Contributors: {contributors_list}\n"
|
||||
|
||||
exhibit_content += f"- SHA-256: `{file_info['sha256_hash']}`\n\n"
|
||||
|
||||
# Add verification section
|
||||
exhibit_content += f"""
|
||||
## Verification
|
||||
This exhibit lists all files in the Basic Memory repository where {full_name} is identified as a contributor based on git commit history analysis.
|
||||
|
||||
**Analysis Date:** {datetime.now().strftime('%Y-%m-%d')}
|
||||
**Repository State:** Basic Memory main branch
|
||||
**Method:** Git history analysis via `git log --follow` for each file
|
||||
|
||||
## Legal Representation
|
||||
{full_name} hereby represents and warrants that they are the author of the contributions listed above and have the right to assign copyright in these works to Basic Memory LLC.
|
||||
|
||||
---
|
||||
|
||||
*This exhibit is attached to and forms part of the Copyright Assignment Agreement between {full_name} and Basic Memory LLC.*
|
||||
"""
|
||||
|
||||
# Write exhibit file
|
||||
safe_name = contributor_key.replace(' ', '_').replace('-', '_').lower()
|
||||
exhibit_file = output_dir / f"exhibit_a_{safe_name}.md"
|
||||
|
||||
with open(exhibit_file, 'w') as f:
|
||||
f.write(exhibit_content)
|
||||
|
||||
print(f"Created exhibit for {full_name}: {exhibit_file}")
|
||||
print(f" - {len(contributor_files)} files")
|
||||
print(f" - {sum(f['size_bytes'] for f in contributor_files):,} bytes")
|
||||
|
||||
# Create overall summary exhibit (for Paul's assignment to Basic Machines LLC)
|
||||
create_overall_summary_exhibit(data, output_dir)
|
||||
|
||||
def create_overall_summary_exhibit(data, output_dir):
|
||||
"""Create overall summary exhibit for Company Agreement."""
|
||||
|
||||
files = data['files']
|
||||
summary = data['summary']
|
||||
contributors = data['contributors']
|
||||
|
||||
summary_content = f"""# Basic Memory Repository - Complete IP Inventory
|
||||
## For Basic Memory LLC Company Agreement
|
||||
|
||||
**Analysis Date:** {summary['scan_date'][:10]}
|
||||
**Repository:** {summary['repository_path']}
|
||||
|
||||
## Executive Summary
|
||||
- **Total Files:** {summary['total_files']:,}
|
||||
- **Total Size:** {summary['total_size_bytes']:,} bytes
|
||||
- **Contributors:** {summary['contributor_count']}
|
||||
- **Primary Author:** Paul Hernandez ({len(contributors.get('Paul Hernandez', {}).get('files', []))} files)
|
||||
|
||||
## File Categories
|
||||
"""
|
||||
|
||||
for category, count in sorted(summary['categories'].items()):
|
||||
summary_content += f"- **{category.replace('_', ' ').title()}:** {count} files\n"
|
||||
|
||||
summary_content += """
|
||||
|
||||
## Contributor Summary
|
||||
"""
|
||||
|
||||
for contrib in summary['top_contributors']:
|
||||
summary_content += f"- **{contrib['name']}** ({contrib['email']}): {contrib['file_count']} files, {contrib['commit_count']} commits\n"
|
||||
|
||||
summary_content += """
|
||||
|
||||
## Legal Significance
|
||||
This inventory represents the complete codebase of Basic Memory as licensed from Basic Machines LLC to Basic Memory LLC under the copyright license agreement dated [DATE].
|
||||
|
||||
### IP Rights Chain
|
||||
1. **Paul Hernandez** → Basic Machines LLC (copyright assignment)
|
||||
2. **Basic Machines LLC** → Basic Memory LLC (15% royalty license)
|
||||
3. **Co-founders** → Basic Memory LLC (direct copyright assignments)
|
||||
|
||||
### Due Diligence Documentation
|
||||
This comprehensive file inventory serves as:
|
||||
- **Company Agreement Exhibit:** Original codebase licensed to Basic Memory LLC
|
||||
- **Acquisition Documentation:** Complete IP inventory for due diligence
|
||||
- **Copyright Verification:** Establishes chain of title for all repository contents
|
||||
|
||||
## Repository Contents by Category
|
||||
|
||||
"""
|
||||
|
||||
# Add sample files by category (first 10 in each category)
|
||||
for category in sorted(summary['categories'].keys()):
|
||||
category_files = [f for f in files if f['category'] == category][:10]
|
||||
if category_files:
|
||||
summary_content += f"### {category.replace('_', ' ').title()} (Sample)\n\n"
|
||||
for file_info in category_files:
|
||||
summary_content += f"- `{file_info['path']}` ({file_info['size_bytes']:,} bytes)\n"
|
||||
|
||||
if len([f for f in files if f['category'] == category]) > 10:
|
||||
remaining = len([f for f in files if f['category'] == category]) - 10
|
||||
summary_content += f"- *... and {remaining} more files*\n"
|
||||
summary_content += "\n"
|
||||
|
||||
summary_content += f"""
|
||||
---
|
||||
|
||||
*This inventory was generated automatically from git repository analysis and represents the complete intellectual property foundation of Basic Memory as of {summary['scan_date'][:10]}.*
|
||||
"""
|
||||
|
||||
# Write summary file
|
||||
summary_file = output_dir / "basic_memory_complete_inventory.md"
|
||||
with open(summary_file, 'w') as f:
|
||||
f.write(summary_content)
|
||||
|
||||
print(f"Created complete inventory summary: {summary_file}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_individual_exhibits()
|
||||
+47
-16
@@ -15,7 +15,7 @@ Basic Memory provides pre-built Docker images on GitHub Container Registry that
|
||||
--name basic-memory-server \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/your/obsidian-vault:/app/data:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
-v basic-memory-config:/app/.basic-memory:rw \
|
||||
ghcr.io/basicmachines-co/basic-memory:latest
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ Basic Memory provides pre-built Docker images on GitHub Container Registry that
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- /path/to/your/obsidian-vault:/app/data:rw
|
||||
- basic-memory-config:/root/.basic-memory:rw
|
||||
- basic-memory-config:/app/.basic-memory:rw
|
||||
environment:
|
||||
- BASIC_MEMORY_DEFAULT_PROJECT=main
|
||||
restart: unless-stopped
|
||||
@@ -67,7 +67,7 @@ docker build -t basic-memory .
|
||||
docker run -d \
|
||||
--name basic-memory-server \
|
||||
-v /path/to/your/obsidian-vault:/app/data:rw \
|
||||
-v basic-memory-config:/root/.basic-memory:rw \
|
||||
-v basic-memory-config:/app/.basic-memory:rw \
|
||||
-e BASIC_MEMORY_DEFAULT_PROJECT=main \
|
||||
basic-memory
|
||||
```
|
||||
@@ -86,11 +86,11 @@ Basic Memory requires several volume mounts for proper operation:
|
||||
|
||||
2. **Configuration and Database** (Recommended):
|
||||
```yaml
|
||||
- basic-memory-config:/root/.basic-memory:rw
|
||||
- basic-memory-config:/app/.basic-memory:rw
|
||||
```
|
||||
Persistent storage for configuration and SQLite database.
|
||||
|
||||
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json after Basic Memory starts.
|
||||
You can edit the basic-memory config.json file located in the /app/.basic-memory/config.json after Basic Memory starts.
|
||||
|
||||
3. **Multiple Projects** (Optional):
|
||||
```yaml
|
||||
@@ -98,7 +98,7 @@ You can edit the basic-memory config.json file located in the /root/.basic-memor
|
||||
- /path/to/project2:/app/data/project2:rw
|
||||
```
|
||||
|
||||
You can edit the basic-memory config.json file located in the /root/.basic-memory/config.json
|
||||
You can edit the basic-memory config.json file located in the /app/.basic-memory/config.json
|
||||
|
||||
## CLI Commands via Docker
|
||||
|
||||
@@ -123,7 +123,7 @@ When using Docker volumes, you'll need to configure projects to point to your mo
|
||||
|
||||
1. **Check current configuration:**
|
||||
```bash
|
||||
docker exec basic-memory-server cat /root/.basic-memory/config.json
|
||||
docker exec basic-memory-server cat /app/.basic-memory/config.json
|
||||
```
|
||||
|
||||
2. **Add a project for your mounted volume:**
|
||||
@@ -184,16 +184,47 @@ environment:
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
Ensure your knowledge directories have proper permissions:
|
||||
The Docker container now runs as a non-root user to avoid file ownership issues. By default, the container uses UID/GID 1000, but you can customize this to match your user:
|
||||
|
||||
```bash
|
||||
# Make directories readable/writable
|
||||
chmod -R 755 /path/to/your/obsidian-vault
|
||||
# Build with custom UID/GID to match your user
|
||||
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t basic-memory .
|
||||
|
||||
# If using specific user/group
|
||||
chown -R $USER:$USER /path/to/your/obsidian-vault
|
||||
# Or use docker-compose with build args
|
||||
```
|
||||
|
||||
**Example docker-compose.yml with custom user:**
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
basic-memory:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
UID: 1000 # Replace with your UID
|
||||
GID: 1000 # Replace with your GID
|
||||
container_name: basic-memory-server
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- /path/to/your/obsidian-vault:/app/data:rw
|
||||
- basic-memory-config:/app/.basic-memory:rw
|
||||
environment:
|
||||
- BASIC_MEMORY_DEFAULT_PROJECT=main
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
**Using pre-built images:**
|
||||
If using the pre-built image from GitHub Container Registry, files will be created with UID/GID 1000. You can either:
|
||||
|
||||
1. Change your local directory ownership to match:
|
||||
```bash
|
||||
sudo chown -R 1000:1000 /path/to/your/obsidian-vault
|
||||
```
|
||||
|
||||
2. Or build your own image with custom UID/GID as shown above.
|
||||
|
||||
### Windows
|
||||
|
||||
When using Docker Desktop on Windows, ensure the directories are shared:
|
||||
@@ -217,7 +248,7 @@ When using Docker Desktop on Windows, ensure the directories are shared:
|
||||
```
|
||||
|
||||
2. **Configuration Not Persisting:**
|
||||
- Use named volumes for `/root/.basic-memory`
|
||||
- Use named volumes for `/app/.basic-memory`
|
||||
- Check volume mount permissions
|
||||
|
||||
3. **Network Connectivity:**
|
||||
@@ -243,10 +274,10 @@ docker-compose logs -f basic-memory
|
||||
## Security Considerations
|
||||
|
||||
1. **Docker Security:**
|
||||
The container runs as root for simplicity. For production, consider additional security measures.
|
||||
The container runs as a non-root user (UID/GID 1000 by default) for improved security. You can customize the user ID using build arguments to match your local user.
|
||||
|
||||
2. **Volume Permissions:**
|
||||
Ensure mounted directories have appropriate permissions and don't expose sensitive data.
|
||||
Ensure mounted directories have appropriate permissions and don't expose sensitive data. With the non-root container, files will be created with the specified user ownership.
|
||||
|
||||
3. **Network Security:**
|
||||
If using HTTP transport, consider using reverse proxy with SSL/TLS and authentication if the endpoint is available on
|
||||
@@ -288,7 +319,7 @@ For Docker-specific issues:
|
||||
1. Check the [troubleshooting section](#troubleshooting) above
|
||||
2. Review container logs: `docker-compose logs basic-memory`
|
||||
3. Verify volume mounts: `docker inspect basic-memory-server`
|
||||
4. Test file permissions: `docker exec basic-memory-server ls -la /root`
|
||||
4. Test file permissions: `docker exec basic-memory-server ls -la /app`
|
||||
|
||||
For general Basic Memory support, see the main [README](../README.md)
|
||||
and [documentation](https://memory.basicmachines.co/).
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# Character Handling and Conflict Resolution
|
||||
|
||||
Basic Memory handles various character encoding scenarios and file naming conventions to provide consistent permalink generation and conflict resolution. This document explains how the system works and how to resolve common character-related issues.
|
||||
|
||||
## Overview
|
||||
|
||||
Basic Memory uses a sophisticated system to generate permalinks from file paths while maintaining consistency across different operating systems and character encodings. The system normalizes file paths and generates unique permalinks to prevent conflicts.
|
||||
|
||||
## Character Normalization Rules
|
||||
|
||||
### 1. Permalink Generation
|
||||
|
||||
When Basic Memory processes a file path, it applies these normalization rules:
|
||||
|
||||
```
|
||||
Original: "Finance/My Investment Strategy.md"
|
||||
Permalink: "finance/my-investment-strategy"
|
||||
```
|
||||
|
||||
**Transformation process:**
|
||||
1. Remove file extension (`.md`)
|
||||
2. Convert to lowercase (case-insensitive)
|
||||
3. Replace spaces with hyphens
|
||||
4. Replace underscores with hyphens
|
||||
5. Handle international characters (transliteration for Latin, preservation for non-Latin)
|
||||
6. Convert camelCase to kebab-case
|
||||
|
||||
### 2. International Character Support
|
||||
|
||||
**Latin characters with diacritics** are transliterated:
|
||||
- `ø` → `o` (Søren → soren)
|
||||
- `ü` → `u` (Müller → muller)
|
||||
- `é` → `e` (Café → cafe)
|
||||
- `ñ` → `n` (Niño → nino)
|
||||
|
||||
**Non-Latin characters** are preserved:
|
||||
- Chinese: `中文/测试文档.md` → `中文/测试文档`
|
||||
- Japanese: `日本語/文書.md` → `日本語/文書`
|
||||
|
||||
## Common Conflict Scenarios
|
||||
|
||||
### 1. Hyphen vs Space Conflicts
|
||||
|
||||
**Problem:** Files with existing hyphens conflict with generated permalinks from spaces.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
|
||||
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug" (CONFLICT!)
|
||||
```
|
||||
|
||||
**Resolution:** The system automatically resolves this by adding suffixes:
|
||||
```
|
||||
File 1: "basic memory bug.md" → permalink: "basic-memory-bug"
|
||||
File 2: "basic-memory-bug.md" → permalink: "basic-memory-bug-1"
|
||||
```
|
||||
|
||||
**Best Practice:** Choose consistent naming conventions within your project.
|
||||
|
||||
### 2. Case Sensitivity Conflicts
|
||||
|
||||
**Problem:** Different case variations that normalize to the same permalink.
|
||||
|
||||
**Example on macOS:**
|
||||
```
|
||||
Directory: Finance/investment.md
|
||||
Directory: finance/investment.md (different on filesystem, same permalink)
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory detects case conflicts and prevents them during sync operations with helpful error messages.
|
||||
|
||||
**Best Practice:** Use consistent casing for directory and file names.
|
||||
|
||||
### 3. Character Encoding Conflicts
|
||||
|
||||
**Problem:** Different Unicode normalizations of the same logical character.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
File 1: "café.md" (é as single character)
|
||||
File 2: "café.md" (e + combining accent)
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory normalizes Unicode characters using NFD normalization to detect these conflicts.
|
||||
|
||||
### 4. Forward Slash Conflicts
|
||||
|
||||
**Problem:** Forward slashes in frontmatter or file names interpreted as path separators.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
---
|
||||
permalink: finance/investment/strategy
|
||||
---
|
||||
```
|
||||
|
||||
**Resolution:** Basic Memory validates frontmatter permalinks and warns about path separator conflicts.
|
||||
|
||||
## Error Messages and Troubleshooting
|
||||
|
||||
### "UNIQUE constraint failed: entity.file_path, entity.project_id"
|
||||
|
||||
**Cause:** Two entities trying to use the same file path within a project.
|
||||
|
||||
**Common scenarios:**
|
||||
1. File move operation where destination is already occupied
|
||||
2. Case sensitivity differences on macOS
|
||||
3. Character encoding conflicts
|
||||
4. Concurrent file operations
|
||||
|
||||
**Resolution steps:**
|
||||
1. Check for duplicate file names with different cases
|
||||
2. Look for files with similar names but different character encodings
|
||||
3. Rename conflicting files to have unique names
|
||||
4. Run sync again after resolving conflicts
|
||||
|
||||
### "File path conflict detected during move"
|
||||
|
||||
**Cause:** Enhanced conflict detection preventing potential database integrity violations.
|
||||
|
||||
**What this means:** The system detected that moving a file would create a conflict before attempting the database operation.
|
||||
|
||||
**Resolution:** Follow the specific guidance in the error message, which will indicate the type of conflict detected.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. File Naming Conventions
|
||||
|
||||
**Recommended patterns:**
|
||||
- Use consistent casing (prefer lowercase)
|
||||
- Use hyphens instead of spaces for multi-word files
|
||||
- Avoid special characters that could conflict with path separators
|
||||
- Be consistent with directory structure casing
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
✅ Good:
|
||||
- finance/investment-strategy.md
|
||||
- projects/basic-memory-features.md
|
||||
- docs/api-reference.md
|
||||
|
||||
❌ Problematic:
|
||||
- Finance/Investment Strategy.md (mixed case, spaces)
|
||||
- finance/Investment Strategy.md (inconsistent case)
|
||||
- docs/API/Reference.md (mixed case directories)
|
||||
```
|
||||
|
||||
### 2. Permalink Management
|
||||
|
||||
**Custom permalinks in frontmatter:**
|
||||
```yaml
|
||||
---
|
||||
type: knowledge
|
||||
permalink: custom-permalink-name
|
||||
---
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
- Use lowercase permalinks
|
||||
- Use hyphens for word separation
|
||||
- Avoid path separators unless creating sub-paths
|
||||
- Ensure uniqueness within your project
|
||||
|
||||
### 3. Directory Structure
|
||||
|
||||
**Consistent casing:**
|
||||
```
|
||||
✅ Good:
|
||||
finance/
|
||||
investment-strategies.md
|
||||
portfolio-management.md
|
||||
|
||||
❌ Problematic:
|
||||
Finance/ (capital F)
|
||||
investment-strategies.md
|
||||
finance/ (lowercase f)
|
||||
portfolio-management.md
|
||||
```
|
||||
|
||||
## Migration and Cleanup
|
||||
|
||||
### Identifying Conflicts
|
||||
|
||||
Use Basic Memory's built-in conflict detection:
|
||||
|
||||
```bash
|
||||
# Sync will report conflicts
|
||||
basic-memory sync
|
||||
|
||||
# Check sync status for warnings
|
||||
basic-memory status
|
||||
```
|
||||
|
||||
### Resolving Existing Conflicts
|
||||
|
||||
1. **Identify conflicting files** from sync error messages
|
||||
2. **Choose consistent naming convention** for your project
|
||||
3. **Rename files** to follow the convention
|
||||
4. **Re-run sync** to verify resolution
|
||||
|
||||
### Bulk Renaming Strategy
|
||||
|
||||
For projects with many conflicts:
|
||||
|
||||
1. **Backup your project** before making changes
|
||||
2. **Standardize on lowercase** file and directory names
|
||||
3. **Replace spaces with hyphens** in file names
|
||||
4. **Use consistent character encoding** (UTF-8)
|
||||
5. **Test sync after each batch** of changes
|
||||
|
||||
## System Enhancements
|
||||
|
||||
### Recent Improvements (v0.13+)
|
||||
|
||||
1. **Enhanced conflict detection** before database operations
|
||||
2. **Improved error messages** with specific resolution guidance
|
||||
3. **Character normalization utilities** for consistent handling
|
||||
4. **File swap detection** for complex move scenarios
|
||||
5. **Proactive conflict warnings** during permalink resolution
|
||||
|
||||
### Monitoring and Logging
|
||||
|
||||
The system now provides detailed logging for conflict resolution:
|
||||
|
||||
```
|
||||
DEBUG: Detected potential file path conflicts for 'Finance/Investment.md': ['finance/investment.md']
|
||||
WARNING: File path conflict detected during move: entity_id=123 trying to move from 'old.md' to 'new.md'
|
||||
```
|
||||
|
||||
These logs help identify and resolve conflicts before they cause sync failures.
|
||||
|
||||
## Support and Resources
|
||||
|
||||
If you encounter character-related conflicts not covered in this guide:
|
||||
|
||||
1. **Check the logs** for specific conflict details
|
||||
2. **Review error messages** for resolution guidance
|
||||
3. **Report issues** with examples of the conflicting files
|
||||
4. **Consider the file naming best practices** outlined above
|
||||
|
||||
The Basic Memory system is designed to handle most character conflicts automatically while providing clear guidance for manual resolution when needed.
|
||||
@@ -3,6 +3,9 @@
|
||||
# Install dependencies
|
||||
install:
|
||||
pip install -e ".[dev]"
|
||||
uv sync
|
||||
@echo ""
|
||||
@echo "💡 Remember to activate the virtual environment by running: source .venv/bin/activate"
|
||||
|
||||
# Run unit tests in parallel
|
||||
test-unit:
|
||||
@@ -17,7 +20,7 @@ test: test-unit test-int
|
||||
|
||||
# Lint and fix code
|
||||
lint:
|
||||
ruff check . --fix
|
||||
uv run ruff check . --fix
|
||||
|
||||
# Type check code
|
||||
type-check:
|
||||
@@ -179,4 +182,4 @@ beta version:
|
||||
|
||||
# List all available recipes
|
||||
default:
|
||||
@just --list
|
||||
@just --list
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
file_path,file_name,category,size_bytes,modified_date,primary_author,all_contributors,sha256_hash
|
||||
.github/workflows/release.yml,release.yml,configuration,2479,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez,163307a99e2f3d317af6ae8927e1537654d88bf574a6958ac0bcd2d4de266b2a
|
||||
CLAUDE.md,CLAUDE.md,legal,12310,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; bm-claudeai (AI Assistant); Ikko Eltociear Ashimine,751f10c1a9a9c0ba2054d9c9cd36cf0989d6a347491fd505bb6504ee74b12900
|
||||
README.md,README.md,documentation,16261,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; Jason Noble; Marc Baiza; Matias Forbord; bm-claudeai (AI Assistant),2c4b82b30f49ab465617034fda999157450ed168f8e097fe8e9ca7ac65a197a7
|
||||
justfile,justfile,other,5496,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain,a8df6c73ee8cf3eaa48e4cba193dffbc2821b843a8cc0c0cd58b8517f2252907
|
||||
pyproject.toml,pyproject.toml,configuration,3279,2025-07-30,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant); semantic-release,428a5d64d4555dc7cd2a81b89a3444bdb1d2f3017257fbc9160bd0ae982718e1
|
||||
src/basic_memory/__init__.py,__init__.py,source_code,256,2025-07-06,Paul Hernandez,Paul Hernandez; Drew Cain; semantic-release,c946490ab9d64957d25df37f721392024916f8a81ed838f70a8e938b1c2637db
|
||||
src/basic_memory/alembic/env.py,env.py,source_code,2838,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),e241d987eadfcd5011cbd9ddbecb833d306de87937759d81c08d37d04a6906b0
|
||||
src/basic_memory/api/routers/utils.py,utils.py,source_code,5159,2025-07-03,Drew Cain,Drew Cain; bm-claudeai (AI Assistant),9e60f57da24e1dc9d64236c26a7a2353003dc618a77fbeb853c494aa3357a57c
|
||||
src/basic_memory/cli/commands/mcp.py,mcp.py,source_code,2593,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),3e1be3d27b18ef4743204b7cf8a37fbc2a1a91fc1636c468e08cefcd8597bac6
|
||||
src/basic_memory/config.py,config.py,source_code,12487,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant); github-actions[bot] (AI Assistant),86341330a041b94d38f5e4f1b94f3509d74351c67cc826af1a4ff57313e5f726
|
||||
src/basic_memory/db.py,db.py,source_code,7428,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),25e053b0b3f9e8fa823b05fc099001efe8a3a4f6e0f80728e5079ba45aa406d8
|
||||
src/basic_memory/mcp/server.py,server.py,source_code,1248,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),0a910cae27d61ea923e90fbe2d4b5c7cacf3b29d3e156f0b7f77872ab973c761
|
||||
src/basic_memory/mcp/tools/__init__.py,__init__.py,source_code,1619,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez,872b771dd52ec3b763645a2baf6e10a70f049ce3106830a6d3f053b3e09a5fe6
|
||||
src/basic_memory/mcp/tools/project_management.py,project_management.py,source_code,12944,2025-07-06,Drew Cain,Drew Cain; Paul Hernandez,cdac736d65129f689a85c6b82f8e043bc84a31657ead7a8331771171eeaa860f
|
||||
src/basic_memory/mcp/tools/search.py,search.py,source_code,15883,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; bm-claudeai (AI Assistant); github-actions[bot] (AI Assistant),8519b0057468c4450eb543a2f561bcd0d7cb96e1ce1b95e948e02b309ba6b7ca
|
||||
src/basic_memory/repository/entity_repository.py,entity_repository.py,source_code,10405,2025-07-03,Drew Cain,Drew Cain; bm-claudeai (AI Assistant); Paul Hernandez,e2a8d1eba6c8d64bc61d7168df0fe8c29a670858bf9daeac464a0f440273fae0
|
||||
src/basic_memory/schemas/memory.py,memory.py,source_code,5833,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; bm-claudeai (AI Assistant),acb4a953a553feca672c4895798a7d948ec51f922f75b66dd7d55716e3bebed3
|
||||
src/basic_memory/services/entity_service.py,entity_service.py,source_code,30440,2025-07-03,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant); github-actions[bot] (AI Assistant),7cd5163eca6b8a0772e838c8c869e478166763cd6a2d745b0b9aa5c25a65baee
|
||||
src/basic_memory/services/initialization.py,initialization.py,source_code,6715,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),4dde58b799cf4ac90631e59949b55b3355a93bdf99df0d9c2408c0cea67e10c4
|
||||
src/basic_memory/templates/prompts/continue_conversation.hbs,continue_conversation.hbs,templates,3076,2025-07-01,Drew Cain,Drew Cain; bm-claudeai (AI Assistant),b6bac31d25c0e52d0909b2273285092f4e31bc219107f92ed511cd407b65e992
|
||||
src/basic_memory/utils.py,utils.py,source_code,8654,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain; andyxinweiminicloud; github-actions[bot] (AI Assistant),111f343e3367339730e081c08acf4613667be574f946126513b5d82da3086bed
|
||||
tests/mcp/test_tool_edit_note.py,test_tool_edit_note.py,source_code,13503,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez,dd441969aa54b3739f2d9202d3feaf46aee807ec51e01a13ef41eda52743944f
|
||||
tests/mcp/test_tool_search.py,test_tool_search.py,source_code,10959,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; github-actions[bot] (AI Assistant),07839cf8e89d74ba05ec1dc55fa842444c1f0cc3d1587823b0ac3458ad89e87c
|
||||
tests/mcp/test_tool_write_note.py,test_tool_write_note.py,source_code,35136,2025-07-30,jope-bm,jope-bm; Drew Cain; Paul Hernandez,45172de20cfc4f4436287573b4f5a94b8b616a360647af67ba1c6c343ccd41b8
|
||||
tests/repository/test_entity_repository_upsert.py,test_entity_repository_upsert.py,source_code,16983,2025-07-03,Paul Hernandez,Paul Hernandez; Drew Cain,ee0ecd6f4ff8b31838bd6de6fb1018af437e02813d8315c06f4e20a6b78a0572
|
||||
tests/schemas/test_schemas.py,test_schemas.py,source_code,16880,2025-07-03,Drew Cain,Drew Cain; Paul Hernandez; bm-claudeai (AI Assistant),2c746a00c340f063c160ace67569955a300a294040949695c79fd4ae124fb611
|
||||
tests/services/test_entity_service.py,test_entity_service.py,source_code,59278,2025-07-03,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),d7d495756faa10b415d705cf26b69d2efbc5f190a834b6767386ff296629ea71
|
||||
tests/test_config.py,test_config.py,source_code,3680,2025-07-03,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),6a246431cff739274efda895dfa28f38710289a73e645312f5fa05fce0885ec8
|
||||
tests/test_db_migration_deduplication.py,test_db_migration_deduplication.py,source_code,6166,2025-07-08,Paul Hernandez,Paul Hernandez; Drew Cain,ab768e061531ca2edcd598c101b5d884e549f90c7a4b305a23713a199d263fbb
|
||||
uv.lock,uv.lock,other,232710,2025-07-30,Paul Hernandez,Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant),caa1e3f6e66464b884a78cab77df2fe303fa3c0dc573be0196474b6d3c150581
|
||||
|
@@ -0,0 +1,28 @@
|
||||
file_path,file_name,category,size_bytes,modified_date,primary_author,all_contributors,sha256_hash
|
||||
CONTRIBUTING.md,CONTRIBUTING.md,documentation,7329,2025-07-30,jope-bm,jope-bm; Paul Hernandez,ca822fe39cc529bfc3822d317ab8a20fd141ec3ecb8c20de9e2eca3edce1c282
|
||||
justfile,justfile,other,5496,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain,a8df6c73ee8cf3eaa48e4cba193dffbc2821b843a8cc0c0cd58b8517f2252907
|
||||
src/basic_memory/api/routers/project_router.py,project_router.py,source_code,8444,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),6539c69f1d506c65d14cc2c151876b19057f37d84d51a9e6f464ed47a4692648
|
||||
src/basic_memory/cli/commands/import_memory_json.py,import_memory_json.py,source_code,3013,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),7ea193f6720b2672290ad16c79a143a31d88a1d567838f12b9ebf9110a7c0338
|
||||
src/basic_memory/cli/commands/project.py,project.py,source_code,13836,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),af3e6f30410ab48355f4b1a46aaa3aa5423cb0a0490f51447db1d25e09f39916
|
||||
src/basic_memory/config.py,config.py,source_code,12487,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain; bm-claudeai (AI Assistant); github-actions[bot] (AI Assistant),86341330a041b94d38f5e4f1b94f3509d74351c67cc826af1a4ff57313e5f726
|
||||
src/basic_memory/importers/memory_json_importer.py,memory_json_importer.py,source_code,4631,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),bc7d045299f17ed9ad5cebff7b1423250ec28a11130e47a0ac48d3ab82bdeafc
|
||||
src/basic_memory/mcp/async_client.py,async_client.py,source_code,925,2025-07-30,jope-bm,jope-bm; Paul Hernandez,6531f4387f30a30a322197ff51d6eacae4b25a58c648095931b3d1145d1da303
|
||||
src/basic_memory/mcp/tools/move_note.py,move_note.py,source_code,17541,2025-07-30,jope-bm,jope-bm; Paul Hernandez,5ef27d9baaebc2c5656fc49c4798a4e4e40a70b6cd9d1e761dbcae021bad558c
|
||||
src/basic_memory/mcp/tools/read_content.py,read_content.py,source_code,9125,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),96a13addac507fd874394dfa2e1e5ed71af5eb89d0872c1d68cacf9cac3f8724
|
||||
src/basic_memory/mcp/tools/read_note.py,read_note.py,source_code,8132,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant); Amadeusz Wieczorek; github-actions[bot] (AI Assistant),dff53ce02b16d9c5fca315b3f09bb70867a89d614b0ef3f0548dce87978048ee
|
||||
src/basic_memory/mcp/tools/write_note.py,write_note.py,source_code,6612,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),771396fb49d397feb5f7e36321d4d233c22c4d1b53414912c849957c6e96fc83
|
||||
src/basic_memory/repository/project_repository.py,project_repository.py,source_code,3803,2025-07-30,jope-bm,jope-bm; bm-claudeai (AI Assistant),9083ba214b2bcaaf98164d9c33cda7add52bc77f7269ecb6e43cdd5088728b7c
|
||||
src/basic_memory/schemas/importer.py,importer.py,source_code,693,2025-07-30,jope-bm,jope-bm; bm-claudeai (AI Assistant),ac33df423ca32b28ce7b6ea9c29d541f8783a86c0c29f78db35163bf93f139cd
|
||||
src/basic_memory/services/project_service.py,project_service.py,source_code,30125,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),bce95c00aa9a1da0a8c8090ee34b5b1494207f8c99e6c25de4ec43fddc38f6be
|
||||
src/basic_memory/utils.py,utils.py,source_code,8654,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Drew Cain; andyxinweiminicloud; github-actions[bot] (AI Assistant),111f343e3367339730e081c08acf4613667be574f946126513b5d82da3086bed
|
||||
tests/api/test_async_client.py,test_async_client.py,source_code,1382,2025-07-30,jope-bm,jope-bm; Paul Hernandez,a2f96226a75e5cff2326ca48ddccea4915964d83e529f7b99958bc233ccdc31a
|
||||
tests/api/test_project_router.py,test_project_router.py,source_code,13288,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),af85d0870069d3831a9214372853994e5d12dc910e37298eb9958f72f6a8bb31
|
||||
tests/cli/test_import_memory_json.py,test_import_memory_json.py,source_code,4891,2025-07-30,jope-bm,jope-bm; bm-claudeai (AI Assistant); Paul Hernandez,d6734ce059dacf97bc764d848b2e2a6ba52fa96871bc331ec9d2ddc60b766bb6
|
||||
tests/cli/test_project_commands.py,test_project_commands.py,source_code,6578,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),51e2ac7ff44b0d1a1db6da406c5c2e6aebceb1b63a1e8d5ab51bbaa834507157
|
||||
tests/mcp/test_tool_move_note.py,test_tool_move_note.py,source_code,25851,2025-07-30,jope-bm,jope-bm; Paul Hernandez,c9f95158503fa42e37532aaa35027538fa49cea718d059d570cf547d31f01043
|
||||
tests/mcp/test_tool_read_content.py,test_tool_read_content.py,source_code,19442,2025-07-30,jope-bm,jope-bm,1817e32f997b06dbdabf02864d0c70584c1cd80bf1fae8dfbef649de79434bc5
|
||||
tests/mcp/test_tool_read_note.py,test_tool_read_note.py,source_code,22991,2025-07-30,jope-bm,jope-bm; Paul Hernandez; Amadeusz Wieczorek; github-actions[bot] (AI Assistant),4e13ea9ff977525aa62b6a8c22505ffe989329dc3e7d07b71e044cf43ef3ddfd
|
||||
tests/mcp/test_tool_write_note.py,test_tool_write_note.py,source_code,35136,2025-07-30,jope-bm,jope-bm; Drew Cain; Paul Hernandez,45172de20cfc4f4436287573b4f5a94b8b616a360647af67ba1c6c343ccd41b8
|
||||
tests/repository/test_project_repository.py,test_project_repository.py,source_code,10511,2025-07-30,jope-bm,jope-bm; bm-claudeai (AI Assistant),279a3324635b1b515c18003bd1875cf230fef1192cadef9ca426846783717e75
|
||||
tests/services/test_project_service.py,test_project_service.py,source_code,28152,2025-07-30,jope-bm,jope-bm; Paul Hernandez; bm-claudeai (AI Assistant),430868ed796518937a3b0bed4d8c33be53264a6100d61d54b0745bb2b58bf29a
|
||||
tests/utils/test_validate_project_path.py,test_validate_project_path.py,source_code,15346,2025-07-30,jope-bm,jope-bm,27a4bf2a9d277e6cd773619e5241b20175d211ec60c284361bec404fd99143f3
|
||||
|
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Legal File Inventory Generator for Basic Memory
|
||||
|
||||
This script generates a comprehensive file inventory for legal documentation purposes,
|
||||
including copyright assignments and company agreement exhibits.
|
||||
|
||||
The inventory includes:
|
||||
- All source code files and their contributors
|
||||
- Documentation and configuration files
|
||||
- License and legal files
|
||||
- Excludes generated files, dependencies, and temporary files
|
||||
|
||||
Usage:
|
||||
python legal_file_inventory.py [--output inventory.csv] [--format csv|json|markdown]
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import csv
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import argparse
|
||||
import hashlib
|
||||
|
||||
class FileInventoryGenerator:
|
||||
def __init__(self, repo_path: str = "."):
|
||||
self.repo_path = Path(repo_path).resolve()
|
||||
self.inventory = []
|
||||
|
||||
# File patterns to exclude from legal inventory
|
||||
self.exclude_patterns = {
|
||||
# Version control and git
|
||||
'.git', '.gitignore', '.gitmodules',
|
||||
|
||||
# Python compiled and cache files
|
||||
'__pycache__', '*.pyc', '*.pyo', '*.pyd', '.pytest_cache',
|
||||
|
||||
# Virtual environments and dependencies
|
||||
'.venv', 'venv', '.env', 'env', 'ENV',
|
||||
'*.dist-info', 'site-packages',
|
||||
|
||||
# IDE and editor files
|
||||
'.idea', '.vscode', '*.swp', '*.swo', '.DS_Store',
|
||||
|
||||
# Build and distribution artifacts
|
||||
'build', 'dist', 'htmlcov', '.coverage', '.coverage.*',
|
||||
'*.egg-info', '.eggs', 'wheels',
|
||||
|
||||
# Cache and temporary files
|
||||
'.ruff_cache', '.mypy_cache', '.tox',
|
||||
'node_modules', '.npm',
|
||||
|
||||
# Documentation build artifacts (but keep source docs)
|
||||
'.obsidian',
|
||||
|
||||
# Lock files (these are generated)
|
||||
'uv.lock', 'Pipfile.lock', 'poetry.lock', 'package-lock.json'
|
||||
}
|
||||
|
||||
# File extensions that are definitely source/authored content
|
||||
self.source_extensions = {
|
||||
'.py', '.md', '.rst', '.txt', '.toml', '.yaml', '.yml',
|
||||
'.json', '.cfg', '.ini', '.conf', '.sh', '.sql',
|
||||
'.js', '.ts', '.jsx', '.tsx', '.css', '.scss', '.sass',
|
||||
'.html', '.htm', '.xml', '.svg', '.dockerfile', '.Dockerfile'
|
||||
}
|
||||
|
||||
# License file patterns
|
||||
self.license_patterns = {
|
||||
'LICENSE', 'LICENCE', 'COPYING', 'COPYRIGHT',
|
||||
'license.txt', 'LICENSE.txt', 'LICENSE.md',
|
||||
'CITATION.cff', 'CLA.md'
|
||||
}
|
||||
|
||||
def run_git_command(self, command: List[str]) -> str:
|
||||
"""Run a git command and return the output."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git'] + command,
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return ""
|
||||
|
||||
def get_file_contributors(self, file_path: str) -> Dict[str, int]:
|
||||
"""Get contributors and their line contributions for a file."""
|
||||
try:
|
||||
blame_output = self.run_git_command(['blame', '--line-porcelain', file_path])
|
||||
contributors = {}
|
||||
|
||||
for line in blame_output.split('\n'):
|
||||
if line.startswith('author '):
|
||||
author = line[7:] # Remove 'author ' prefix
|
||||
contributors[author] = contributors.get(author, 0) + 1
|
||||
|
||||
return contributors
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def get_file_history(self, file_path: str) -> Dict[str, any]:
|
||||
"""Get file creation date, last modification, and total commits."""
|
||||
try:
|
||||
# Get creation date (first commit)
|
||||
first_commit = self.run_git_command([
|
||||
'log', '--follow', '--format=%ad', '--date=iso',
|
||||
'--reverse', file_path
|
||||
]).split('\n')[0] if self.run_git_command([
|
||||
'log', '--follow', '--format=%ad', '--date=iso',
|
||||
'--reverse', file_path
|
||||
]) else None
|
||||
|
||||
# Get last modification date
|
||||
last_commit = self.run_git_command([
|
||||
'log', '-1', '--format=%ad', '--date=iso', file_path
|
||||
])
|
||||
|
||||
# Get total commits for this file
|
||||
commit_count = len(self.run_git_command([
|
||||
'log', '--follow', '--oneline', file_path
|
||||
]).split('\n')) if self.run_git_command([
|
||||
'log', '--follow', '--oneline', file_path
|
||||
]) else 0
|
||||
|
||||
return {
|
||||
'created': first_commit or 'Unknown',
|
||||
'last_modified': last_commit or 'Unknown',
|
||||
'commit_count': commit_count
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
'created': 'Unknown',
|
||||
'last_modified': 'Unknown',
|
||||
'commit_count': 0
|
||||
}
|
||||
|
||||
def should_exclude_file(self, file_path: Path) -> bool:
|
||||
"""Determine if a file should be excluded from the inventory."""
|
||||
str_path = str(file_path)
|
||||
|
||||
# Check if any part of the path matches exclude patterns
|
||||
for pattern in self.exclude_patterns:
|
||||
if pattern in str_path or file_path.match(pattern):
|
||||
return True
|
||||
|
||||
# Exclude files in virtual environment paths
|
||||
if '/.venv/' in str_path or '/venv/' in str_path:
|
||||
return True
|
||||
|
||||
# Exclude binary files that are likely dependencies
|
||||
if file_path.suffix in {'.so', '.dylib', '.dll', '.pyd'}:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def calculate_file_hash(self, file_path: Path) -> str:
|
||||
"""Calculate SHA-256 hash of file content."""
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def categorize_file(self, file_path: Path) -> str:
|
||||
"""Categorize the file based on its path and extension."""
|
||||
str_path = str(file_path).lower()
|
||||
|
||||
# License and legal files
|
||||
if any(pattern.lower() in file_path.name.lower() for pattern in self.license_patterns):
|
||||
return "Legal/License"
|
||||
|
||||
# Documentation
|
||||
if file_path.suffix.lower() in {'.md', '.rst', '.txt'} and any(
|
||||
doc_dir in str_path for doc_dir in ['doc', 'readme', 'changelog', 'contributing']
|
||||
):
|
||||
return "Documentation"
|
||||
|
||||
# Configuration files
|
||||
if file_path.suffix.lower() in {'.toml', '.yaml', '.yml', '.json', '.cfg', '.ini', '.conf'}:
|
||||
return "Configuration"
|
||||
|
||||
# Source code
|
||||
if file_path.suffix.lower() in {'.py', '.js', '.ts', '.jsx', '.tsx'}:
|
||||
return "Source Code"
|
||||
|
||||
# Tests
|
||||
if 'test' in str_path and file_path.suffix.lower() == '.py':
|
||||
return "Test Code"
|
||||
|
||||
# Build and deployment
|
||||
if file_path.name.lower() in {'dockerfile', 'justfile', 'makefile'} or file_path.suffix.lower() in {'.sh'}:
|
||||
return "Build/Deployment"
|
||||
|
||||
# Database and migrations
|
||||
if 'migration' in str_path or 'alembic' in str_path or file_path.suffix.lower() == '.sql':
|
||||
return "Database/Migration"
|
||||
|
||||
# Templates and resources
|
||||
if file_path.suffix.lower() in {'.hbs', '.j2', '.jinja', '.template'}:
|
||||
return "Templates/Resources"
|
||||
|
||||
return "Other"
|
||||
|
||||
def scan_repository(self):
|
||||
"""Scan the repository and build the file inventory."""
|
||||
print(f"Scanning repository: {self.repo_path}")
|
||||
|
||||
for root, dirs, files in os.walk(self.repo_path):
|
||||
# Skip excluded directories
|
||||
dirs[:] = [d for d in dirs if not any(pattern in d for pattern in self.exclude_patterns)]
|
||||
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
relative_path = file_path.relative_to(self.repo_path)
|
||||
|
||||
# Skip excluded files
|
||||
if self.should_exclude_file(relative_path):
|
||||
continue
|
||||
|
||||
# Get file stats
|
||||
try:
|
||||
stat_info = file_path.stat()
|
||||
file_size = stat_info.st_size
|
||||
modified_time = datetime.fromtimestamp(stat_info.st_mtime)
|
||||
except Exception:
|
||||
file_size = 0
|
||||
modified_time = datetime.now()
|
||||
|
||||
# Get git information
|
||||
contributors = self.get_file_contributors(str(relative_path))
|
||||
history = self.get_file_history(str(relative_path))
|
||||
|
||||
# Calculate file hash for integrity verification
|
||||
file_hash = self.calculate_file_hash(file_path)
|
||||
|
||||
# Build inventory entry
|
||||
entry = {
|
||||
'file_path': str(relative_path),
|
||||
'full_path': str(file_path),
|
||||
'file_name': file_path.name,
|
||||
'file_extension': file_path.suffix,
|
||||
'file_size_bytes': file_size,
|
||||
'category': self.categorize_file(relative_path),
|
||||
'fs_modified_date': modified_time.isoformat(),
|
||||
'git_created_date': history['created'],
|
||||
'git_last_modified': history['last_modified'],
|
||||
'git_commit_count': history['commit_count'],
|
||||
'contributors': contributors,
|
||||
'primary_author': max(contributors.items(), key=lambda x: x[1])[0] if contributors else 'Unknown',
|
||||
'contributor_count': len(contributors),
|
||||
'total_author_lines': sum(contributors.values()) if contributors else 0,
|
||||
'sha256_hash': file_hash,
|
||||
'scan_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.inventory.append(entry)
|
||||
|
||||
print(f"Scanned {len(self.inventory)} files")
|
||||
|
||||
def get_summary_statistics(self) -> Dict:
|
||||
"""Generate summary statistics for the inventory."""
|
||||
if not self.inventory:
|
||||
return {}
|
||||
|
||||
# Collect all contributors
|
||||
all_contributors = set()
|
||||
for entry in self.inventory:
|
||||
all_contributors.update(entry['contributors'].keys())
|
||||
|
||||
# Category breakdown
|
||||
categories = {}
|
||||
for entry in self.inventory:
|
||||
cat = entry['category']
|
||||
categories[cat] = categories.get(cat, 0) + 1
|
||||
|
||||
# File extension breakdown
|
||||
extensions = {}
|
||||
for entry in self.inventory:
|
||||
ext = entry['file_extension'] or 'no_extension'
|
||||
extensions[ext] = extensions.get(ext, 0) + 1
|
||||
|
||||
# Contributor statistics
|
||||
contributor_files = {}
|
||||
contributor_lines = {}
|
||||
for entry in self.inventory:
|
||||
for contributor, lines in entry['contributors'].items():
|
||||
contributor_files[contributor] = contributor_files.get(contributor, 0) + 1
|
||||
contributor_lines[contributor] = contributor_lines.get(contributor, 0) + lines
|
||||
|
||||
return {
|
||||
'total_files': len(self.inventory),
|
||||
'total_contributors': len(all_contributors),
|
||||
'categories': categories,
|
||||
'file_extensions': extensions,
|
||||
'contributor_file_counts': contributor_files,
|
||||
'contributor_line_counts': contributor_lines,
|
||||
'scan_date': datetime.now().isoformat(),
|
||||
'repository_path': str(self.repo_path)
|
||||
}
|
||||
|
||||
def export_csv(self, output_file: str):
|
||||
"""Export inventory to CSV format."""
|
||||
with open(output_file, 'w', newline='', encoding='utf-8') as f:
|
||||
if not self.inventory:
|
||||
return
|
||||
|
||||
fieldnames = list(self.inventory[0].keys())
|
||||
# Convert complex fields to strings for CSV
|
||||
fieldnames = [f for f in fieldnames if f != 'contributors']
|
||||
fieldnames.append('contributors_json')
|
||||
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for entry in self.inventory:
|
||||
row = {k: v for k, v in entry.items() if k != 'contributors'}
|
||||
row['contributors_json'] = json.dumps(entry['contributors'])
|
||||
writer.writerow(row)
|
||||
|
||||
def export_json(self, output_file: str):
|
||||
"""Export inventory to JSON format."""
|
||||
export_data = {
|
||||
'metadata': self.get_summary_statistics(),
|
||||
'files': self.inventory
|
||||
}
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def export_markdown(self, output_file: str):
|
||||
"""Export inventory to Markdown format."""
|
||||
stats = self.get_summary_statistics()
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Basic Memory - Legal File Inventory\n\n")
|
||||
f.write(f"**Generated:** {datetime.now().isoformat()}\n\n")
|
||||
f.write(f"**Repository:** {stats.get('repository_path', 'Unknown')}\n\n")
|
||||
|
||||
# Summary statistics
|
||||
f.write("## Summary Statistics\n\n")
|
||||
f.write(f"- **Total Files:** {stats.get('total_files', 0)}\n")
|
||||
f.write(f"- **Total Contributors:** {stats.get('total_contributors', 0)}\n\n")
|
||||
|
||||
# Categories
|
||||
if 'categories' in stats:
|
||||
f.write("### Files by Category\n\n")
|
||||
for category, count in sorted(stats['categories'].items()):
|
||||
f.write(f"- **{category}:** {count} files\n")
|
||||
f.write("\n")
|
||||
|
||||
# Top contributors
|
||||
if 'contributor_file_counts' in stats:
|
||||
f.write("### Top Contributors by Files Modified\n\n")
|
||||
sorted_contributors = sorted(
|
||||
stats['contributor_file_counts'].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)[:10]
|
||||
for contributor, count in sorted_contributors:
|
||||
f.write(f"- **{contributor}:** {count} files\n")
|
||||
f.write("\n")
|
||||
|
||||
# Detailed file listing
|
||||
f.write("## Detailed File Inventory\n\n")
|
||||
f.write("| File Path | Category | Size (bytes) | Primary Author | Contributors |\n")
|
||||
f.write("|-----------|----------|--------------|----------------|-------------|\n")
|
||||
|
||||
for entry in sorted(self.inventory, key=lambda x: x['file_path']):
|
||||
contributors_str = ', '.join(entry['contributors'].keys())[:50]
|
||||
if len(contributors_str) == 50:
|
||||
contributors_str += "..."
|
||||
|
||||
f.write(f"| {entry['file_path']} | {entry['category']} | "
|
||||
f"{entry['file_size_bytes']} | {entry['primary_author']} | "
|
||||
f"{contributors_str} |\n")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate legal file inventory for Basic Memory repository"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
default='basic_memory_legal_inventory.csv',
|
||||
help='Output file path (default: basic_memory_legal_inventory.csv)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--format', '-f',
|
||||
choices=['csv', 'json', 'markdown'],
|
||||
default='csv',
|
||||
help='Output format (default: csv)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo-path', '-r',
|
||||
default='.',
|
||||
help='Path to repository (default: current directory)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize and run the inventory generator
|
||||
generator = FileInventoryGenerator(args.repo_path)
|
||||
generator.scan_repository()
|
||||
|
||||
# Export in requested format
|
||||
if args.format == 'csv':
|
||||
generator.export_csv(args.output)
|
||||
elif args.format == 'json':
|
||||
generator.export_json(args.output)
|
||||
elif args.format == 'markdown':
|
||||
generator.export_markdown(args.output)
|
||||
|
||||
# Print summary
|
||||
stats = generator.get_summary_statistics()
|
||||
print("\n=== Legal File Inventory Complete ===")
|
||||
print(f"Repository: {stats.get('repository_path', 'Unknown')}")
|
||||
print(f"Total files inventoried: {stats.get('total_files', 0)}")
|
||||
print(f"Total contributors identified: {stats.get('total_contributors', 0)}")
|
||||
print(f"Output written to: {args.output}")
|
||||
|
||||
# Show top contributors
|
||||
if 'contributor_file_counts' in stats:
|
||||
print("\nTop 5 contributors by files modified:")
|
||||
sorted_contributors = sorted(
|
||||
stats['contributor_file_counts'].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)[:5]
|
||||
for i, (contributor, count) in enumerate(sorted_contributors, 1):
|
||||
print(f" {i}. {contributor}: {count} files")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,272 @@
|
||||
path,name,category,size_bytes,modified_time,primary_author,contributor_count,contributors_list,sha256_hash
|
||||
.claude/commands/release/beta.md,beta.md,documentation,3103,2025-07-01T08:33:22.037438,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),345f06e3f304470a8892e96979355919c8e5d8f4212ebbe543c3272fd15a5cbe
|
||||
.claude/commands/release/changelog.md,changelog.md,documentation,4648,2025-07-03T18:43:10.075727,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ea2484352f4f881aa71f546dcf84b0882d60c96673a88406aa2f7e12a282a1f4
|
||||
.claude/commands/release/release-check.md,release-check.md,documentation,3344,2025-07-01T08:33:22.040060,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7dd41a40e95c89153936c4ea60a3c989eccd5973fff9f6caef156de82f12bd04
|
||||
.claude/commands/release/release.md,release.md,documentation,2925,2025-07-01T08:33:22.043674,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),0e4afd788fcf85fbe46094452e9b92a4290c3e23702283e72ee56aa9288476e5
|
||||
.claude/commands/test-live.md,test-live.md,documentation,18720,2025-07-03T18:43:10.076403,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),21a9edeefb3e5673d833151a2fd0464087c0f0a9d056fdd7fca474c1d177d6fc
|
||||
.dockerignore,.dockerignore,other,623,2025-07-01T08:33:22.048301,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b3079bffad89d07186c9166725075524ffc07ceea6dc1d53fdbea6dca9585684
|
||||
.github/ISSUE_TEMPLATE/bug_report.md,bug_report.md,documentation,967,2025-03-01T14:22:50.968994,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),0d34443ddda84a5e9726c8268083c9646d2cc64ff9c5ebf56c89587cef4c7706
|
||||
.github/ISSUE_TEMPLATE/config.yml,config.yml,configuration,374,2025-03-01T14:23:26.936836,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),f4e21bb9b7f053509b6c08548a2649a5d62f44f0106810aa8c915d2f29acc623
|
||||
.github/ISSUE_TEMPLATE/documentation.md,documentation.md,documentation,557,2025-03-01T14:23:40.020117,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),78f80a6b9a2b954695f74a847a068e0b780da6020572401c13c931555a4a9db5
|
||||
.github/ISSUE_TEMPLATE/feature_request.md,feature_request.md,documentation,786,2025-03-01T14:23:06.344081,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),cee834de442221d53f3d2eeb85b81f3a641b3a7cb587ef8a169452305007439d
|
||||
.github/dependabot.yml,dependabot.yml,configuration,529,2025-03-15T10:40:16.936258,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),8c6c2ef9a1417da63adb49c70f45524129bd5c46bbd7452b6b97cfe65205f89d
|
||||
.github/workflows/claude.yml,claude.yml,legal,4628,2025-07-03T18:43:10.076958,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),d026f1756310781b85abc491b454cc31b52dfce4b951cdf8a4a555b6b047604c
|
||||
.github/workflows/dev-release.yml,dev-release.yml,configuration,1405,2025-06-03T08:31:42.915367,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),58bcfc9e11e210e02dee76aabe7d1378da4edafb7f61857049ee6ccee9d45ffa
|
||||
.github/workflows/docker.yml,docker.yml,configuration,1621,2025-07-01T08:33:22.049870,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c918a86f2b16ebb45182a73c45272bcd6cea7022cc6e0eaeb2b2a24d8eebfe4d
|
||||
.github/workflows/pr-title.yml,pr-title.yml,configuration,887,2025-02-17T16:29:35.733646,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),b6df62a20235496feba4d9419c85d6e8446c4d447ad69e7f57b51cc8953f68f4
|
||||
.github/workflows/release.yml,release.yml,configuration,2479,2025-07-03T18:43:10.077261,Drew Cain,2,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co),163307a99e2f3d317af6ae8927e1537654d88bf574a6958ac0bcd2d4de266b2a
|
||||
.github/workflows/test.yml,test.yml,configuration,1426,2025-07-01T08:33:22.054430,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ab09084a11be8adfc83fc6285c652fbe44a4584aa7d7d3658fcf0c57112d1640
|
||||
.gitignore,.gitignore,other,566,2025-07-01T08:33:22.055006,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),358658b0faa8c3145e539421deba4ad0bac4a8ea2e608b2c2f867a822c67b36f
|
||||
.python-version,.python-version,other,5,2024-12-02T16:41:37.012830,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),7b55f8e67b5623c4bef3fa691288da9437d79d3aba156de48d481db32ac7d16d
|
||||
CHANGELOG.md,CHANGELOG.md,documentation,53647,2025-07-06T10:34:45.550847,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); semantic-release (semantic-release),0b165eb18975ff253cde08f4687b7808ac6ce63b6d8c10912c495a76c9058d1a
|
||||
CITATION.cff,CITATION.cff,other,302,2025-02-17T16:29:35.735146,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),bc4878cf6f80bb64655f3f93a5821ffe0924ad8311c784b5f81a36093fcffdda
|
||||
CLA.md,CLA.md,legal,1342,2025-03-01T14:12:38.435132,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),c238c7ff4f2b8451c6bba1bbacd10b04fb7d41d9a9cd76a62644d8193791d169
|
||||
CLAUDE.md,CLAUDE.md,legal,12310,2025-07-03T18:43:10.078147,Drew Cain,4,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); Ikko Eltociear Ashimine (eltociear@gmail.com),751f10c1a9a9c0ba2054d9c9cd36cf0989d6a347491fd505bb6504ee74b12900
|
||||
CODE_OF_CONDUCT.md,CODE_OF_CONDUCT.md,documentation,509,2025-02-17T16:29:35.735238,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),b276b464e85eb1e5d9f09d90bba9573508a7bf81d2eeb38d0f25bd9f2fa66198
|
||||
CONTRIBUTING.md,CONTRIBUTING.md,documentation,7329,2025-07-30T09:39:36.607158,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co),ca822fe39cc529bfc3822d317ab8a20fd141ec3ecb8c20de9e2eca3edce1c282
|
||||
Dockerfile,Dockerfile,other,856,2025-07-01T08:33:22.056908,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),c45501905423990e31ffdb8ae4e976eca6423c6784905c1036bcc7795b70feef
|
||||
LICENSE,LICENSE,legal,34523,2024-12-02T16:41:20.739392,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef
|
||||
README.md,README.md,documentation,16261,2025-07-03T18:43:10.078281,Drew Cain,6,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Jason Noble (github+jasonn@jasonnoble.org); Marc Baiza (43151891+mbaiza27@users.noreply.github.com); Matias Forbord (codeluggage@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),2c4b82b30f49ab465617034fda999157450ed168f8e097fe8e9ca7ac65a197a7
|
||||
SECURITY.md,SECURITY.md,documentation,303,2025-03-12T16:04:01.989927,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),96932268b8fe737577941b49e9dff5ff8ad59c11cc3f9a016989589ef5c98c1f
|
||||
docker-compose.yml,docker-compose.yml,configuration,2534,2025-07-01T08:33:22.059281,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),4a9fdfd2f0d42b148fe021eee77fd5e772c7ba9c2fe5d0cc310c8c7a64257417
|
||||
docs/AI Assistant Guide.md,AI Assistant Guide.md,documentation,16423,2025-07-01T08:33:22.060491,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c717869a3d356852077026591364ecb0589a6d53f9764af027c57d142ea7ff55
|
||||
docs/Docker.md,Docker.md,documentation,9088,2025-07-01T08:33:22.060976,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),e13d3f50148353046b52d9d493986880f76f2914ef4efe1a759b2cc9d10913a4
|
||||
justfile,justfile,other,5496,2025-07-30T09:39:36.608008,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com),a8df6c73ee8cf3eaa48e4cba193dffbc2821b843a8cc0c0cd58b8517f2252907
|
||||
llms-install.md,llms-install.md,documentation,2307,2025-06-29T14:19:28.826375,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),674105eb85c8f1d59fc2ddc6840f2a2a3e293358687a305c37dffc8144304f97
|
||||
memory.json,memory.json,configuration,90075,2025-06-29T14:19:28.827172,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),22d5eee995ca0548b7d9dd152f9a3690c42a514cbf38fea750b3ed2a17dd99c5
|
||||
pyproject.toml,pyproject.toml,configuration,3279,2025-07-30T09:39:36.608791,Paul Hernandez,4,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); semantic-release (semantic-release),428a5d64d4555dc7cd2a81b89a3444bdb1d2f3017257fbc9160bd0ae982718e1
|
||||
smithery.yaml,smithery.yaml,configuration,433,2025-03-11T16:36:03.389716,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),63f13499cf4fee0751b5005f5bd85597dc1c7f9a881f4ec8147cc23e95e75c2d
|
||||
src/basic_memory/__init__.py,__init__.py,source_code,256,2025-07-06T10:34:45.551291,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@gmail.com); semantic-release (semantic-release),c946490ab9d64957d25df37f721392024916f8a81ed838f70a8e938b1c2637db
|
||||
src/basic_memory/alembic/alembic.ini,alembic.ini,configuration,3733,2025-02-23T17:31:23.198433,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),20466c9c5f026db66793006bebb2f328a3686c7bb34da40d52f33c3eca29e717
|
||||
src/basic_memory/alembic/env.py,env.py,source_code,2838,2025-07-08T20:39:18.336650,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),e241d987eadfcd5011cbd9ddbecb833d306de87937759d81c08d37d04a6906b0
|
||||
src/basic_memory/alembic/migrations.py,migrations.py,source_code,718,2025-06-21T00:01:59.613806,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),96b8873d70dd04b48d5c45b7413a54d1224ab9577557ef0dad5929377a9fb144
|
||||
src/basic_memory/alembic/script.py.mako,script.py.mako,other,635,2025-02-17T16:29:35.739659,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),304a8bfb6a804e5493f5300e79882c70c9f5bb2e87512f4d16f0e097dddd323f
|
||||
src/basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py,3dae7c7b1564_initial_schema.py,source_code,4363,2025-06-21T00:01:59.595762,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),9536d69409ddd5eb3bc54f7d0e82607da45ed7f2ad7bc4cbf7cae2a9e28657cd
|
||||
src/basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py,502b60eaa905_remove_required_from_entity_permalink.py,source_code,1840,2025-06-21T00:01:59.604717,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),93ac584e660f33d468b3eec203b070641298c282bf82655d0beda7f050237681
|
||||
src/basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py,5fe1ab1ccebe_add_projects_table.py,source_code,4379,2025-06-21T00:01:59.601299,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),d82098f5aca3cdbada60c7229ea4ac262f527349eed9ef9e864509bacfaccf7e
|
||||
src/basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py,647e7a75e2cd_project_constraint_fix.py,source_code,4210,2025-07-01T08:33:22.063463,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),604ac5908a5976272f52297866d13abb14a99390590935d9fd37cf13e32049fa
|
||||
src/basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py,b3c3938bacdb_relation_to_name_unique_index.py,source_code,1428,2025-06-21T00:01:59.598551,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),46c1b2990cdf4575752d234a8b28b494c8a54f15b5360c12f6347aef27b66a92
|
||||
src/basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py,cc7172b46608_update_search_index_schema.py,source_code,3679,2025-06-21T00:01:59.610909,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),9036af47d42ac7dc12bb93f7a9de1266aff0688b031b550c4e0d92983a24b4c5
|
||||
src/basic_memory/api/__init__.py,__init__.py,source_code,72,2025-06-21T00:01:59.579981,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),c02a63fb6d63d43d0acca97d425eae9cb055158d0ad6e1a9fc57926512adaa99
|
||||
src/basic_memory/api/app.py,app.py,source_code,2897,2025-07-08T20:39:18.337081,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ad673ac2c09410af5830f6afdffaa28b0040480dacd52ff01750b8b2c066e8cf
|
||||
src/basic_memory/api/routers/__init__.py,__init__.py,source_code,398,2025-06-21T00:01:59.549416,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),4443b908a43dea8e6fb4658009cb08c48a56c9b21449e29773cdd159b59cf014
|
||||
src/basic_memory/api/routers/directory_router.py,directory_router.py,source_code,2054,2025-06-21T00:01:59.567371,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac1407beec1f7d43a4d222af704b3642559c960bebf2eaf6e1ffe91f7fac5514
|
||||
src/basic_memory/api/routers/importer_router.py,importer_router.py,source_code,4513,2025-06-21T00:01:59.561600,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),c45502a2b90f5a8f00174c9ad14adc2e65cd7fc0a33d9740a9d750207f0f3bea
|
||||
src/basic_memory/api/routers/knowledge_router.py,knowledge_router.py,source_code,9738,2025-07-01T08:33:22.064119,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e1d0ffb4fa5c246582ba844a11b417092de1a173567be55b7060bbc55fbc5681
|
||||
src/basic_memory/api/routers/management_router.py,management_router.py,source_code,2730,2025-07-08T20:39:18.337438,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),cdbce294dcec6146c56c4dae15744cdf77039fd21b23eef7cbc0b5f9bfb5f4ee
|
||||
src/basic_memory/api/routers/memory_router.py,memory_router.py,source_code,3003,2025-07-01T08:33:22.064510,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6bd0a7c775e0c1290efb6001173237c0cdcfa0c1b1bb27c9145a7b35f1596a97
|
||||
src/basic_memory/api/routers/project_router.py,project_router.py,source_code,8444,2025-07-30T09:39:45.721369,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6539c69f1d506c65d14cc2c151876b19057f37d84d51a9e6f464ed47a4692648
|
||||
src/basic_memory/api/routers/prompt_router.py,prompt_router.py,source_code,9948,2025-07-01T08:33:22.065221,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e30c6aebe35112054933c37d0b462c3750005030fc9e44c23b35b2cd2a93485c
|
||||
src/basic_memory/api/routers/resource_router.py,resource_router.py,source_code,8058,2025-06-21T00:01:59.570399,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),584244a84698ff24ca8f9f94fab5b891740a51c25f972920c08ebf83f478d5c9
|
||||
src/basic_memory/api/routers/search_router.py,search_router.py,source_code,1226,2025-06-21T00:01:59.543020,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),183eb68e50904e22ff54db1d89b8be6f57fa1f8d0a096a3d497d8297b607e105
|
||||
src/basic_memory/api/routers/utils.py,utils.py,source_code,5159,2025-07-03T18:43:10.079143,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),9e60f57da24e1dc9d64236c26a7a2353003dc618a77fbeb853c494aa3357a57c
|
||||
src/basic_memory/api/template_loader.py,template_loader.py,source_code,8607,2025-06-21T00:01:59.576132,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),7b16d3797c89460c8b1664a335eae8c634fb5f60d6166d97e6a5156b776b6d83
|
||||
src/basic_memory/cli/__init__.py,__init__.py,source_code,33,2025-06-21T00:01:59.456735,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),6ab70a2c05910e13c3ef1e6df34325be265e633c076741998a0cb2dcd295a069
|
||||
src/basic_memory/cli/app.py,app.py,source_code,2268,2025-07-08T20:39:18.337708,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),f710670380d707c4458ad881f7a25bb3c5cdb2bcff59f139f37421264fe8f2f8
|
||||
src/basic_memory/cli/commands/__init__.py,__init__.py,source_code,393,2025-07-08T20:39:18.338070,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),de8a23702f98fb844fa9f7fdbedc16ce24ff4f8baf05589c2f4a521cd8a55645
|
||||
src/basic_memory/cli/commands/db.py,db.py,source_code,1480,2025-07-08T20:39:18.338326,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),704a1096d80ab9d12e247d029fe6223e935a0d0ceee7e615c020f46a720828e0
|
||||
src/basic_memory/cli/commands/import_chatgpt.py,import_chatgpt.py,source_code,2856,2025-07-08T20:39:18.338787,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),8957cca3acab6351335624a518bdc19d956487e93d86dd1b6c2309e9d5850ae5
|
||||
src/basic_memory/cli/commands/import_claude_conversations.py,import_claude_conversations.py,legal,2946,2025-07-08T20:39:18.339053,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7bc97838732bf00f4fb4a80e3ba4fdfbce8971ae85cc2ac902c3b3a3e110ae57
|
||||
src/basic_memory/cli/commands/import_claude_projects.py,import_claude_projects.py,legal,2891,2025-07-08T20:39:18.339310,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),6321577075801c99ad47a0cdc13b5aa3c9ca102a05ca8f06ae2a51125a8c43bc
|
||||
src/basic_memory/cli/commands/import_memory_json.py,import_memory_json.py,source_code,3013,2025-07-30T09:39:45.721637,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7ea193f6720b2672290ad16c79a143a31d88a1d567838f12b9ebf9110a7c0338
|
||||
src/basic_memory/cli/commands/mcp.py,mcp.py,source_code,2593,2025-07-08T20:39:18.340025,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),3e1be3d27b18ef4743204b7cf8a37fbc2a1a91fc1636c468e08cefcd8597bac6
|
||||
src/basic_memory/cli/commands/project.py,project.py,source_code,13836,2025-07-30T09:39:45.721838,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),af3e6f30410ab48355f4b1a46aaa3aa5423cb0a0490f51447db1d25e09f39916
|
||||
src/basic_memory/cli/commands/status.py,status.py,source_code,5809,2025-07-08T20:39:18.340324,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b6d0ea237d714d054f69b24a2058beba74e224a2fa4a80a525d62a2aa2ea2d70
|
||||
src/basic_memory/cli/commands/sync.py,sync.py,source_code,8213,2025-07-08T20:39:18.340627,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),703b3e73c4c33850c27f6f4c07abbf5d4ce5dc43f706bf0c43264bbb45f8b213
|
||||
src/basic_memory/cli/commands/tool.py,tool.py,source_code,9540,2025-07-01T08:33:22.070903,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),9b2fa400b9f7921bf55b602f8bbdfa36b1ec7e4a5bc8fe7b9838b92e31f0a9ed
|
||||
src/basic_memory/cli/main.py,main.py,source_code,465,2025-07-08T20:39:18.340868,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7a405b12bff98e319968526238ae15859df0362bba5a3857562fecca47b5c70d
|
||||
src/basic_memory/config.py,config.py,source_code,12487,2025-07-30T09:39:45.722031,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),86341330a041b94d38f5e4f1b94f3509d74351c67cc826af1a4ff57313e5f726
|
||||
src/basic_memory/db.py,db.py,source_code,7428,2025-07-08T20:39:18.341550,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),25e053b0b3f9e8fa823b05fc099001efe8a3a4f6e0f80728e5079ba45aa406d8
|
||||
src/basic_memory/deps.py,deps.py,source_code,12144,2025-07-08T20:39:18.341870,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bce28cd9ca549b8afce0bc689cc1fb2dc80cd5b929f85114aa682bb8ae82411d
|
||||
src/basic_memory/file_utils.py,file_utils.py,source_code,6700,2025-06-21T00:01:59.587200,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),79ac5328b2c46d3232fcc6ff22ffc39ade085c0252ad91958be2a7ae9c9c8b71
|
||||
src/basic_memory/importers/__init__.py,__init__.py,source_code,795,2025-06-21T00:01:59.295471,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),0537015bdecfded85cb166b9c3db50b2f3aef329c70e0c36fbcb4f824099a21f
|
||||
src/basic_memory/importers/base.py,base.py,source_code,2531,2025-06-21T00:01:59.305335,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),6b0c1efd4f827f348834aa0cea2ae8eebbb842a2e5b1f5f3747ced0ef79b9f13
|
||||
src/basic_memory/importers/chatgpt_importer.py,chatgpt_importer.py,source_code,7372,2025-06-21T00:01:59.308956,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),dfa540b196ab23b5c4e6bfcac4da567873351cf7c78fa35f4c3b87e84c58e218
|
||||
src/basic_memory/importers/claude_conversations_importer.py,claude_conversations_importer.py,legal,5725,2025-06-21T00:01:59.288540,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),a7bc9e872f4f81ce5f79fe9dd1a6fd0f00a6f020a2cb2664184a97f14eeb1dbc
|
||||
src/basic_memory/importers/claude_projects_importer.py,claude_projects_importer.py,legal,5389,2025-06-21T00:01:59.291737,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),a452675fd9bb18ebf64eb4bd7f69ccd7e993b617931015a3c4ab700d67493863
|
||||
src/basic_memory/importers/memory_json_importer.py,memory_json_importer.py,source_code,4631,2025-07-30T09:39:45.722316,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bc7d045299f17ed9ad5cebff7b1423250ec28a11130e47a0ac48d3ab82bdeafc
|
||||
src/basic_memory/importers/utils.py,utils.py,source_code,1792,2025-06-21T00:01:59.298428,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),ee7e22fd4812362394f62ed8944abe80738c1e3ab88131faf639f4a8ccfd6c89
|
||||
src/basic_memory/markdown/__init__.py,__init__.py,source_code,501,2025-06-21T00:01:59.316337,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),0ddce2a025ad0e729aab4e411d82e02ffefc15ac041cba4b5e7a7e90f4957c87
|
||||
src/basic_memory/markdown/entity_parser.py,entity_parser.py,source_code,4516,2025-06-21T00:01:59.321461,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),1b0d1177358645ccbadd170b3f62760dc9bd838d38c741978ab0da5836234d68
|
||||
src/basic_memory/markdown/markdown_processor.py,markdown_processor.py,source_code,4968,2025-06-21T00:01:59.325241,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4b99f2ebdceed9d96a3bbb562682eba4b4b383c7a64080eaec3bbba25a4952c9
|
||||
src/basic_memory/markdown/plugins.py,plugins.py,source_code,6639,2025-06-21T00:01:59.312733,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),82d2332918e866ccaf06a2e954d9eb9b397f71b4d9e591a7f2472e5f1423464a
|
||||
src/basic_memory/markdown/schemas.py,schemas.py,source_code,1896,2025-06-21T00:01:59.329982,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7b2c580abd615725a62267257b46ac13922dfc30fa01192a681658bb528ae67e
|
||||
src/basic_memory/markdown/utils.py,utils.py,source_code,3410,2025-07-03T18:43:10.079889,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),1b757f0d06e60e3ea276c0b2ea44fe1a156a895e093c1e607d6286e702bf4ae4
|
||||
src/basic_memory/mcp/__init__.py,__init__.py,source_code,35,2025-06-21T00:01:59.404757,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),76c0ce84aaa361e21b0942db1c8c5f708b536eab9d12e120d4da7ce9eab41844
|
||||
src/basic_memory/mcp/async_client.py,async_client.py,source_code,925,2025-07-30T09:39:45.722547,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),6531f4387f30a30a322197ff51d6eacae4b25a58c648095931b3d1145d1da303
|
||||
src/basic_memory/mcp/project_session.py,project_session.py,source_code,4203,2025-07-08T20:39:18.342906,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4da403ec93dec9063eeb8b05246e3501740d162cc77a067d3ce54ba43d690819
|
||||
src/basic_memory/mcp/prompts/__init__.py,__init__.py,source_code,615,2025-07-08T20:39:18.343263,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),f8197d0e08f64c3f4f50b8f38203ea5eebcf123582472ed2f58834de1dbe53b0
|
||||
src/basic_memory/mcp/prompts/ai_assistant_guide.py,ai_assistant_guide.py,source_code,821,2025-06-21T00:13:53.262832,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),f13239c4e6e2455730bfac3d6f2d71407957d3086fc84efe2c6b22a833114c58
|
||||
src/basic_memory/mcp/prompts/continue_conversation.py,continue_conversation.py,source_code,1998,2025-06-21T00:13:53.263309,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),aec9a50b657b7bb1ba0c02b42bcdb9bc5b0f2a0b1143bd361c5ce7ea59076833
|
||||
src/basic_memory/mcp/prompts/recent_activity.py,recent_activity.py,source_code,3311,2025-07-01T08:33:22.073986,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),d2fd5cddbd927430f15d5b85f1e3a3368a18cb4e2e4587a5d29749d2b9e0830e
|
||||
src/basic_memory/mcp/prompts/search.py,search.py,source_code,1692,2025-06-21T00:13:53.264093,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),9dbf3c319cbdb5d5bf32608b51522d8ae92b2dd6f7c441d62efd0954b525738a
|
||||
src/basic_memory/mcp/prompts/utils.py,utils.py,source_code,5431,2025-06-21T00:01:59.425042,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),55a72b6eac18b724a9225608aca1e8e6cea3b684ccb1c609aab1511f7ce90ba4
|
||||
src/basic_memory/mcp/resources/ai_assistant_guide.md,ai_assistant_guide.md,documentation,14777,2025-03-28T19:12:38.653627,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),aa76160e46256fe2662ae3a86799659916acfede1d5835c1fe2f0b136efb2e0b
|
||||
src/basic_memory/mcp/resources/project_info.py,project_info.py,source_code,1906,2025-06-21T00:13:53.264918,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),2dc5244f1e225c17d4e8ba784d5721efc3aa2e8a5b3b2e258f22a77ebe155eca
|
||||
src/basic_memory/mcp/server.py,server.py,source_code,1248,2025-07-08T20:39:18.343419,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),0a910cae27d61ea923e90fbe2d4b5c7cacf3b29d3e156f0b7f77872ab973c761
|
||||
src/basic_memory/mcp/tools/__init__.py,__init__.py,source_code,1619,2025-07-03T18:43:10.080029,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co),872b771dd52ec3b763645a2baf6e10a70f049ce3106830a6d3f053b3e09a5fe6
|
||||
src/basic_memory/mcp/tools/build_context.py,build_context.py,source_code,4611,2025-07-03T18:43:10.080426,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),724280b77b8f5f3e61cd3fdeebc3ee66e2bcfedaaea3638e6a2e2e33fcb1978e
|
||||
src/basic_memory/mcp/tools/canvas.py,canvas.py,source_code,3738,2025-07-01T08:33:22.076204,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),db617d1bd81f3dbfa5f22d41e6b6b825afe1f73618f37acf63d9830390b98246
|
||||
src/basic_memory/mcp/tools/delete_note.py,delete_note.py,source_code,7346,2025-07-01T08:33:22.076726,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),b52c9173f5600662f255e7a70a5c17d5293ef8b29c19a8403334d7da62b65c8b
|
||||
src/basic_memory/mcp/tools/edit_note.py,edit_note.py,source_code,13570,2025-07-01T08:33:22.077181,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ab8c7e7fbfa3fe5fb0ce6d7bf805454f5fd6182a340aae25237b1e6127b6d5a6
|
||||
src/basic_memory/mcp/tools/list_directory.py,list_directory.py,source_code,5236,2025-06-21T00:13:53.267453,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),f85c43b02aeee580f4d8ce2a9100c0bab1095b245a0bb608e1847ace0d1ab51f
|
||||
src/basic_memory/mcp/tools/move_note.py,move_note.py,source_code,17541,2025-07-30T09:39:45.722919,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),5ef27d9baaebc2c5656fc49c4798a4e4e40a70b6cd9d1e761dbcae021bad558c
|
||||
src/basic_memory/mcp/tools/project_management.py,project_management.py,source_code,12944,2025-07-06T10:34:45.552403,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co),cdac736d65129f689a85c6b82f8e043bc84a31657ead7a8331771171eeaa860f
|
||||
src/basic_memory/mcp/tools/read_content.py,read_content.py,source_code,9125,2025-07-30T09:39:45.723469,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),96a13addac507fd874394dfa2e1e5ed71af5eb89d0872c1d68cacf9cac3f8724
|
||||
src/basic_memory/mcp/tools/read_note.py,read_note.py,source_code,8132,2025-07-30T09:39:45.723828,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Amadeusz Wieczorek (git@amadeusw.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),dff53ce02b16d9c5fca315b3f09bb70867a89d614b0ef3f0548dce87978048ee
|
||||
src/basic_memory/mcp/tools/recent_activity.py,recent_activity.py,source_code,4833,2025-06-21T00:13:53.269120,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),5d58cd2402679b1bf3c7dfcbb3503e40e776c9347ba499524d3b91c528af98de
|
||||
src/basic_memory/mcp/tools/search.py,search.py,source_code,15883,2025-07-03T18:43:10.081557,Drew Cain,4,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),8519b0057468c4450eb543a2f561bcd0d7cb96e1ce1b95e948e02b309ba6b7ca
|
||||
src/basic_memory/mcp/tools/sync_status.py,sync_status.py,source_code,10517,2025-07-08T20:39:18.344231,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),73dd73b186eaec8b82e6b17e39a67c27082d24e0653dc4e4b35712bd83b80a22
|
||||
src/basic_memory/mcp/tools/utils.py,utils.py,source_code,20892,2025-07-30T09:39:36.616253,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),a95004911e276822eba88a3fef15c5b9ba861b1ca9a2ecfe0c1e3fb2f4ef0116
|
||||
src/basic_memory/mcp/tools/view_note.py,view_note.py,source_code,2499,2025-07-01T08:33:22.080024,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),75d357c72113b1d0394987e521a4158ff09b77b23b08b56cddab5144331b1a68
|
||||
src/basic_memory/mcp/tools/write_note.py,write_note.py,source_code,6612,2025-07-30T09:39:45.723985,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),771396fb49d397feb5f7e36321d4d233c22c4d1b53414912c849957c6e96fc83
|
||||
src/basic_memory/models/__init__.py,__init__.py,source_code,333,2025-06-21T00:01:59.437212,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),8f40b876d162f85384690291f1d416106f9d26d750d793414e2260e276c85cd5
|
||||
src/basic_memory/models/base.py,base.py,source_code,225,2025-06-21T00:01:59.447249,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),e2101727c084d519e32a16f6de53ddf9033b1bf15721d4e8c0c27d6d18b1b295
|
||||
src/basic_memory/models/knowledge.py,knowledge.py,source_code,7087,2025-06-21T00:01:59.444376,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),005c5f292f1f45ae372aadc48c9080b9fa6d7b854d0bb7ecf587ec843ac1e28d
|
||||
src/basic_memory/models/project.py,project.py,source_code,2773,2025-07-01T08:33:22.080907,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),a14ad06943aeeff9ae4a5fa2dfc0e1d07ce6085acc02dc20c402c3513b7d9397
|
||||
src/basic_memory/models/search.py,search.py,source_code,1300,2025-06-21T00:01:59.440923,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),3e143cc38b5a0294af8e1d43a4f841e1c1fd193b76136a68f83159ce19e86646
|
||||
src/basic_memory/repository/__init__.py,__init__.py,source_code,327,2025-06-21T00:01:59.256363,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),3162bea3c42292accea5ee52c8f6ca4368a8079056034529cfae6d820f84d035
|
||||
src/basic_memory/repository/entity_repository.py,entity_repository.py,source_code,10405,2025-07-03T18:43:10.082570,Drew Cain,3,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),e2a8d1eba6c8d64bc61d7168df0fe8c29a670858bf9daeac464a0f440273fae0
|
||||
src/basic_memory/repository/observation_repository.py,observation_repository.py,source_code,2943,2025-06-21T00:01:59.269646,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),aa132f1cb4a36a84f715afdc40e2ac4f98d83e3eba1974b2b4404cc0b020ca04
|
||||
src/basic_memory/repository/project_info_repository.py,project_info_repository.py,source_code,338,2025-06-21T00:01:59.252582,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),f172d50182a405643a19b2a3d62a80f4e2b41463c775394eb3b66d7de51ff6df
|
||||
src/basic_memory/repository/project_repository.py,project_repository.py,source_code,3803,2025-07-30T09:39:45.724344,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),9083ba214b2bcaaf98164d9c33cda7add52bc77f7269ecb6e43cdd5088728b7c
|
||||
src/basic_memory/repository/relation_repository.py,relation_repository.py,source_code,3179,2025-06-21T00:01:59.246745,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),cfb3a8e59cff27e063e91bd00e949647bdd92e4d9fc46edeee3acc6de15e26f4
|
||||
src/basic_memory/repository/repository.py,repository.py,source_code,15224,2025-06-21T00:01:59.273355,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),3096fe71bf106506cbf86aeafe2aafe0aabbe5a5f6c90a212c8aa1e53e1f171c
|
||||
src/basic_memory/repository/search_repository.py,search_repository.py,source_code,21936,2025-07-03T18:43:10.082844,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),a972f73d1b71dac5770e8eb3793c6c9ac44e4e792f9dab528f8c4e6c6a80bcaa
|
||||
src/basic_memory/schemas/__init__.py,__init__.py,source_code,1674,2025-06-21T00:01:59.514555,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),98480815c75379bfafe32d2090e87fa40e73caa19b664fbd5db5ea9528ba58cb
|
||||
src/basic_memory/schemas/base.py,base.py,source_code,6738,2025-07-01T08:33:22.082193,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),171f7b0c4ab33abef2f7379eb1e3bda950586ce7edff83907fd12255f84e267e
|
||||
src/basic_memory/schemas/delete.py,delete.py,source_code,1180,2025-06-21T00:01:59.502563,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),50047624af7d58c8f780ffb2a065a51c3dde64492f9534917fc42860813e59fc
|
||||
src/basic_memory/schemas/directory.py,directory.py,source_code,881,2025-06-21T00:01:59.529023,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),17dfcbac9a91a9bfe43b4f060ca2735cb6f69e16d81b63dd554a397830ff29fe
|
||||
src/basic_memory/schemas/importer.py,importer.py,source_code,693,2025-07-30T09:39:45.724633,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac33df423ca32b28ce7b6ea9c29d541f8783a86c0c29f78db35163bf93f139cd
|
||||
src/basic_memory/schemas/memory.py,memory.py,source_code,5833,2025-07-03T18:43:10.083167,Drew Cain,3,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),acb4a953a553feca672c4895798a7d948ec51f922f75b66dd7d55716e3bebed3
|
||||
src/basic_memory/schemas/project_info.py,project_info.py,source_code,7047,2025-07-01T08:33:22.083248,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),7dc3635297b6e7fe6e3262b2d78da26f7a79a846a4cecd5624f2e4828979cf2c
|
||||
src/basic_memory/schemas/prompt.py,prompt.py,source_code,3648,2025-06-21T00:01:59.525753,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),4a92157d9a6b413f04e6e3f8d23dc2a417369c729f9703a8de2643ec114f2071
|
||||
src/basic_memory/schemas/request.py,request.py,source_code,3760,2025-06-21T00:01:59.511789,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),32fe44beb2d99452223ebf1d3a3a3fe105ef92c79885023b71dfd7db30ecc503
|
||||
src/basic_memory/schemas/response.py,response.py,source_code,6450,2025-07-03T18:43:10.083478,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),5eea4660a2abe48d83ed083d1c2483fdcfb403e0b504f03c14d22f1cae866abb
|
||||
src/basic_memory/schemas/search.py,search.py,source_code,3657,2025-06-21T00:01:59.522804,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),cb032c0c60102b6b0ed934f995cf9d6bf93aece296d71d537a75e8ae61c75afe
|
||||
src/basic_memory/services/__init__.py,__init__.py,source_code,259,2025-06-21T00:01:59.655059,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),5c6b7c597ddf5ffd0af4bdfb32ccbc1c5f2794c658206dee43a9945fafe226d8
|
||||
src/basic_memory/services/context_service.py,context_service.py,source_code,14462,2025-06-21T00:01:59.672086,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (paulmh@gmail.com),e1178b005e6a89f03d6b239e3c6b152aeb30d535a3f280734440a3aec16228f2
|
||||
src/basic_memory/services/directory_service.py,directory_service.py,source_code,6017,2025-06-21T00:01:59.648501,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),fd838f5ec79033892777b3c8140868f4b57f13e1658d513959524a434b9f959b
|
||||
src/basic_memory/services/entity_service.py,entity_service.py,source_code,30440,2025-07-03T18:43:10.083956,Paul Hernandez,4,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),7cd5163eca6b8a0772e838c8c869e478166763cd6a2d745b0b9aa5c25a65baee
|
||||
src/basic_memory/services/exceptions.py,exceptions.py,source_code,390,2025-06-21T00:01:59.668908,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),a158d0af9d1742a9c5ab5f8c34a062948d9286d1c3c72a5abf20e4d547b21e1c
|
||||
src/basic_memory/services/file_service.py,file_service.py,source_code,10059,2025-06-21T00:01:59.641785,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),8c2ae69c4913438b7d1c5ecbfcce812fbb5d0ea8e3cedcbd8694e8f40cf086f3
|
||||
src/basic_memory/services/initialization.py,initialization.py,source_code,6715,2025-07-08T20:39:18.344482,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),4dde58b799cf4ac90631e59949b55b3355a93bdf99df0d9c2408c0cea67e10c4
|
||||
src/basic_memory/services/link_resolver.py,link_resolver.py,source_code,4594,2025-07-01T08:33:22.084908,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),d7efd516cbea753e6b5411def09aeaeb7539f5743487119ecf163c736e138a8c
|
||||
src/basic_memory/services/project_service.py,project_service.py,source_code,30125,2025-07-30T09:39:45.725079,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bce95c00aa9a1da0a8c8090ee34b5b1494207f8c99e6c25de4ec43fddc38f6be
|
||||
src/basic_memory/services/search_service.py,search_service.py,source_code,12782,2025-06-21T00:01:59.665439,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),7392b2d2e7f3ed83c5807855ccd450e0e79c17f254aedee700bce9323a1b5b83
|
||||
src/basic_memory/services/service.py,service.py,source_code,336,2025-06-21T00:01:59.636167,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),57e77ff20395d3bcc62100e92fe2acaacdd937d977a9fdc764e2b57ff60d4da8
|
||||
src/basic_memory/services/sync_status_service.py,sync_status_service.py,source_code,7504,2025-07-03T18:43:10.084229,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),0a025d689e8e16f1632872104952105fc904537f3d32bcff592eb1f1dc76fbb7
|
||||
src/basic_memory/sync/__init__.py,__init__.py,source_code,156,2025-06-21T00:01:59.621137,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),0951e0b981f8e7b876bb6c6833c2af3a29490bbd5726567ea9487c947723623e
|
||||
src/basic_memory/sync/background_sync.py,background_sync.py,source_code,726,2025-07-08T20:39:18.345062,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),549af64ae91128b76c6df07ef517b82de85ca4ad796be44b5c0141fac01d4513
|
||||
src/basic_memory/sync/sync_service.py,sync_service.py,source_code,23387,2025-07-01T08:33:22.086507,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),0310b927561370f593980d07773bce641b618b8fbf2d9e3890d174290a0344fc
|
||||
src/basic_memory/sync/watch_service.py,watch_service.py,source_code,15316,2025-06-26T09:28:51.859209,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),240ba6ac7523575945f4db442b7da3820d26c9605f2d7a2d357c4e35e215f523
|
||||
src/basic_memory/templates/prompts/continue_conversation.hbs,continue_conversation.hbs,templates,3076,2025-07-01T08:33:22.086792,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b6bac31d25c0e52d0909b2273285092f4e31bc219107f92ed511cd407b65e992
|
||||
src/basic_memory/templates/prompts/search.hbs,search.hbs,templates,3098,2025-05-25T10:07:54.520119,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),1f570222c1caa78542d46ac7d8a79407ca467b9bd714fa9bd953e8b72a667820
|
||||
src/basic_memory/utils.py,utils.py,source_code,8654,2025-07-30T09:39:45.725428,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); andyxinweiminicloud (andyxinweimin263@icloud.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),111f343e3367339730e081c08acf4613667be574f946126513b5d82da3086bed
|
||||
test-int/conftest.py,conftest.py,source_code,8203,2025-07-08T20:39:18.345735,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),7231e1bc76cddc7c94433e853825dc0165b103bedf996c2c24eda6ae32fff5ad
|
||||
test-int/mcp/test_build_context_validation.py,test_build_context_validation.py,source_code,6469,2025-07-08T20:39:18.346021,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),ee61d6b15cee0194fd74c1bcf9361d5a839f6081c7d571ef081da8fcefe1bbf2
|
||||
test-int/mcp/test_delete_note_integration.py,test_delete_note_integration.py,source_code,13175,2025-07-08T20:39:18.346366,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),497495249b8bcf0af177d8705c6899875460707697980434e560cfe56f86feb3
|
||||
test-int/mcp/test_edit_note_integration.py,test_edit_note_integration.py,source_code,19822,2025-07-08T20:39:18.346730,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),9ba704badd05808aee0b5a686b7e250e1c865229ec92dab561cba5287b6f6fc6
|
||||
test-int/mcp/test_list_directory_integration.py,test_list_directory_integration.py,source_code,14744,2025-07-08T20:39:18.347106,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),2c4336a59de63f17ed252bb52d1330ebca901ad83bfebc9e5aa67f132a3c77c6
|
||||
test-int/mcp/test_move_note_integration.py,test_move_note_integration.py,source_code,19825,2025-07-08T20:39:18.347344,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),cbb17688d0f9551306a7520b059ace7f7dfba70fc20bf0ec98b1148000d7ad7d
|
||||
test-int/mcp/test_project_management_integration.py,test_project_management_integration.py,source_code,33175,2025-07-08T20:39:18.347658,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),93a43cd699453a7602a5def839740ba5027ab914eba3e7b66a026860ed6bd567
|
||||
test-int/mcp/test_project_state_sync_integration.py,test_project_state_sync_integration.py,source_code,7728,2025-07-08T20:39:18.347907,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),80b0f5b5cd359498dcbd3af7dcb6194afa4582d8efb3faa38d28675ee45e0748
|
||||
test-int/mcp/test_read_content_integration.py,test_read_content_integration.py,source_code,11187,2025-07-08T20:39:18.348233,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),6955dd343b40df088ac79175e52723501d83796335a16e43b7f58e16e757e1b8
|
||||
test-int/mcp/test_read_note_integration.py,test_read_note_integration.py,source_code,1381,2025-07-08T20:39:18.348463,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b93050da68d0e9d47d46879a386567f688399e28390f4eb31ee37353dc27514b
|
||||
test-int/mcp/test_search_integration.py,test_search_integration.py,source_code,14690,2025-07-08T20:39:18.348959,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),e0957ada9dd9b12eae5843a37d241ec9293ca929f18281e31197dae36bff589d
|
||||
test-int/mcp/test_write_note_integration.py,test_write_note_integration.py,source_code,9472,2025-07-08T20:39:18.349398,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b2b56fee8351168d0c65640c295d969dc4575d9bea87927b2dc07e591cabdff3
|
||||
tests/Non-MarkdownFileSupport.pdf,Non-MarkdownFileSupport.pdf,other,106751,2025-02-23T17:31:23.206428,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),4aa96f9d0ee830ef6a1ec92243f302bbe2fa8971b84945f4eb5e276bf1ffde96
|
||||
tests/Screenshot.png,Screenshot.png,other,190076,2025-02-23T17:31:23.207771,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),dd82612fe013c39810d28ae0f2bbcca6ab2c9925b035d7720bb02493ddf5d0a9
|
||||
tests/__init__.py,__init__.py,source_code,141,2025-06-21T00:01:46.216456,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),2ac5605e69ad8829e96b3689632536539f1cdd3149a29c1903f25af0793a5d65
|
||||
tests/api/conftest.py,conftest.py,source_code,1404,2025-06-21T00:01:46.359728,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),ea46cde47a37c1f43fe079e48f5d0de8007155a0187f4445abc4f2ffde9c1791
|
||||
tests/api/test_async_client.py,test_async_client.py,source_code,1382,2025-07-30T09:39:45.725666,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),a2f96226a75e5cff2326ca48ddccea4915964d83e529f7b99958bc233ccdc31a
|
||||
tests/api/test_continue_conversation_template.py,test_continue_conversation_template.py,source_code,5152,2025-06-21T00:01:46.390901,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),cca08b0291db0ad693c60d8dc203660021feab46d9eb530be7f67e0ebb52c1d3
|
||||
tests/api/test_directory_router.py,test_directory_router.py,source_code,9376,2025-06-21T00:01:46.356659,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),428626589f0010512481ba495368b0bf2b9617b22764e588eaa3562e86da4d37
|
||||
tests/api/test_importer_router.py,test_importer_router.py,source_code,16578,2025-06-21T00:01:46.368070,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),03edf6e7404c7350afe76e54ea2bf0d66f668bcdb49f59702067a67b3694309d
|
||||
tests/api/test_knowledge_router.py,test_knowledge_router.py,source_code,45771,2025-07-01T08:33:22.090490,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),43a5f91ae1fb9038e65c09701336fbdc4dbf40c8319c94727e6312fda9d6d56e
|
||||
tests/api/test_management_router.py,test_management_router.py,source_code,6220,2025-06-21T00:01:46.402764,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),f6a3f41f263f10c08281525571e2e39fc9ae818ebb7e9895e3f3f3115ea05677
|
||||
tests/api/test_memory_router.py,test_memory_router.py,source_code,5555,2025-06-21T00:01:46.399667,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),2b8782015c893081fc67b7f13a7255d4e3dc45b27705a524f94918a339e2d486
|
||||
tests/api/test_project_router.py,test_project_router.py,source_code,13288,2025-07-30T09:39:45.725847,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),af85d0870069d3831a9214372853994e5d12dc910e37298eb9958f72f6a8bb31
|
||||
tests/api/test_project_router_operations.py,test_project_router_operations.py,source_code,1812,2025-06-21T00:01:46.381705,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),afd40879de3d7c508cca0feeae3cb7a8f3afcfbb06f59a6104189b1e3291376a
|
||||
tests/api/test_prompt_router.py,test_prompt_router.py,source_code,5046,2025-06-21T00:01:46.396588,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),9115995b7dd8b44d8a9c06c5e1bbb177618286675a769889e35ac16f26fd9897
|
||||
tests/api/test_resource_router.py,test_resource_router.py,source_code,13960,2025-06-21T00:01:46.384919,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ad3cb2cabaeab82a7e64253ceede7c1791e2bd70c92f60bc5f3be939292ca22d
|
||||
tests/api/test_search_router.py,test_search_router.py,source_code,6409,2025-06-21T00:01:46.375427,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (phernandez@basicmachines.co),ba3e3809795e72db9dd9c8c729048bdbbbdb897c2a5aa8b175112b5d4b56d152
|
||||
tests/api/test_search_template.py,test_search_template.py,source_code,5258,2025-06-21T00:01:46.378721,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),8f0d16a1de947d0466d96afed02e695f7648adac70b9ba99753250068207a686
|
||||
tests/api/test_template_loader.py,test_template_loader.py,source_code,8035,2025-06-21T00:01:46.406694,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),4fd7c6c4862cc0d877d29244ecdc0b52a0693fc4574a9667663bee36bfd36b62
|
||||
tests/api/test_template_loader_helpers.py,test_template_loader_helpers.py,source_code,7445,2025-06-21T00:01:46.372114,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),8dd35f11c9b3f2d96ac36263edf4430308728c04c9da543b1cb845b96ab20aa7
|
||||
tests/cli/conftest.py,conftest.py,source_code,1185,2025-07-01T08:33:22.090848,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c3c73da7ad73224931e1fba4ebd70792221be607bfb1488bb5c9420bba03bdb8
|
||||
tests/cli/test_cli_tools.py,test_cli_tools.py,source_code,13528,2025-06-21T00:01:46.318964,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),befb8cab50132e26a1b13b82ce8e93a3258282cf157a4230214bfcefeae65102
|
||||
tests/cli/test_import_chatgpt.py,test_import_chatgpt.py,source_code,6719,2025-07-08T20:39:18.350234,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),df447335545248e2fb2417af33f8cc2f0143bd413a1783e41f89605de305b7c8
|
||||
tests/cli/test_import_claude_conversations.py,test_import_claude_conversations.py,legal,5028,2025-07-08T20:39:18.350607,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac4b3b9dfc331fa7fa32a2941c027ff8b6585a94a5ccfaaf0a4affe5cf19c00c
|
||||
tests/cli/test_import_claude_projects.py,test_import_claude_projects.py,legal,4585,2025-07-08T20:39:18.350885,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7eaab3d491d50ca59c8ba015ecd5740a5feae94d970ee17559c6df9d8558c2cc
|
||||
tests/cli/test_import_memory_json.py,test_import_memory_json.py,source_code,4891,2025-07-30T09:39:45.726290,jope-bm,3,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),d6734ce059dacf97bc764d848b2e2a6ba52fa96871bc331ec9d2ddc60b766bb6
|
||||
tests/cli/test_project_commands.py,test_project_commands.py,source_code,6578,2025-07-30T09:39:45.726703,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),51e2ac7ff44b0d1a1db6da406c5c2e6aebceb1b63a1e8d5ab51bbaa834507157
|
||||
tests/cli/test_project_info.py,test_project_info.py,source_code,3874,2025-07-01T08:33:22.091600,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),0b08950e95a5f276f14c56f6fe99af4d41b8f04548e9b7746f807c5ac4155578
|
||||
tests/cli/test_status.py,test_status.py,source_code,4160,2025-07-01T08:33:22.091978,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),a1ff89fdac4bf57ba03be92ac79b8ca3210af0b1132d5a2bd0fc0c253fcbf988
|
||||
tests/cli/test_sync.py,test_sync.py,source_code,3864,2025-07-08T20:43:47.816788,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),77e0f6b42b584a5acfda3c2ff194ff5e71ca57a40cb4484d563f5482bbbb4c0f
|
||||
tests/cli/test_version.py,test_version.py,source_code,289,2025-06-21T00:01:46.305368,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),db95665178a32d391bd83011d073db269cefba9da8a4dc4d3e392d47bc0e0a8f
|
||||
tests/conftest.py,conftest.py,source_code,15286,2025-07-08T20:39:18.351368,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),c6f38f103ce58144ffe4562f0c47dad849974e96327f02805e854447e19bc411
|
||||
tests/importers/test_importer_base.py,test_importer_base.py,source_code,4329,2025-07-08T20:39:00.549594,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),da872a073f2b90fda3c98c9f44cf45884f4f682d91ec1b1f931d42c34eee1939
|
||||
tests/importers/test_importer_utils.py,test_importer_utils.py,source_code,1827,2025-06-21T00:01:46.184343,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),cbee630c855ceb1de764ac0cdb1d5d781c16cd3a934d67166adc235c98f363d7
|
||||
tests/markdown/__init__.py,__init__.py,source_code,0,2025-06-21T00:01:46.196570,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
tests/markdown/test_entity_parser.py,test_entity_parser.py,source_code,8669,2025-06-21T00:01:46.206579,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),496fdab6fe164634f8223f827585a4c0dedd30ccea7b0b69dc79004ad0996de2
|
||||
tests/markdown/test_markdown_plugins.py,test_markdown_plugins.py,source_code,5139,2025-06-21T00:01:46.200759,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),9624d73462aadcd47fb0edd0d87880959a8982eecfa39529f21e3b292c34cb83
|
||||
tests/markdown/test_markdown_processor.py,test_markdown_processor.py,source_code,5728,2025-06-21T00:01:46.194175,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),35042aa57dd2aeed848c624aa7f69decfb18e83c3b217eca1fdacfc4a10831db
|
||||
tests/markdown/test_observation_edge_cases.py,test_observation_edge_cases.py,source_code,4295,2025-06-21T00:01:46.209598,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),6f1ab7684bfa8b02af0e0c42b3d7aed91fcd0b1c6288cbc9b6d6943c166979fa
|
||||
tests/markdown/test_parser_edge_cases.py,test_parser_edge_cases.py,source_code,5142,2025-06-21T00:01:46.213291,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),d511d7d1187e3c1d3a8d3e0ab9efc9e68e84115c46811e7a9dc21eca9a87d520
|
||||
tests/markdown/test_relation_edge_cases.py,test_relation_edge_cases.py,source_code,3952,2025-06-21T00:01:46.203846,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),f8e2016308f84139342cc37ec6e389ffe52d7eb46f998ee327c7f0d5bba3ad9b
|
||||
tests/markdown/test_task_detection.py,test_task_detection.py,source_code,401,2025-06-21T00:01:46.191444,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),37fe91a689742033a0eb76723557e31c2a85e591133383b3887572cb714db2a5
|
||||
tests/mcp/conftest.py,conftest.py,source_code,1761,2025-07-08T20:39:18.351554,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),350b2ca42c52ef76e002deddb65fc0eccb9f8989bd26078ea1c100e3b93bae3f
|
||||
tests/mcp/test_prompts.py,test_prompts.py,source_code,5989,2025-07-01T08:33:22.093691,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e3b2ee5b4bc120b5043225ebdbf73eadd71be9c3d9d8e1fdd66a24a8ff11a780
|
||||
tests/mcp/test_resource_project_info.py,test_resource_project_info.py,source_code,4885,2025-07-01T08:33:22.094085,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6a043736789b239c8d4ae00d1a5332a0fc3bed4038fe3388970ba97dc7431a93
|
||||
tests/mcp/test_resources.py,test_resources.py,source_code,549,2025-07-01T08:33:22.094481,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),77a6396b9b8a9d3b3d202e438243ca6178d87fd7680c9fc063ee013f2042aa4c
|
||||
tests/mcp/test_tool_build_context.py,test_tool_build_context.py,source_code,3714,2025-07-01T08:33:22.094661,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),5f0a39fdb652605335ed54aa91fe994262441bb7ba46f381c46ebca882fc23f2
|
||||
tests/mcp/test_tool_canvas.py,test_tool_canvas.py,source_code,7997,2025-07-01T08:33:22.094982,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c517bb1f9edc77b76fbeb483dfcb2dd57de9f0b80a10738708bc90cbe388dc9e
|
||||
tests/mcp/test_tool_delete_note.py,test_tool_delete_note.py,source_code,4208,2025-07-01T08:33:22.095578,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),4cbf3ba4313438651b0608d5b25faf1400f7bd6a78af630351aecd4457800895
|
||||
tests/mcp/test_tool_edit_note.py,test_tool_edit_note.py,source_code,13503,2025-07-03T18:43:10.084890,Drew Cain,2,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),dd441969aa54b3739f2d9202d3feaf46aee807ec51e01a13ef41eda52743944f
|
||||
tests/mcp/test_tool_list_directory.py,test_tool_list_directory.py,source_code,7730,2025-07-01T08:33:22.096737,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),cfbb5c06bcfe74ea60f31051eb9b03ac80b86232b053c188bf3b172132f7c0f5
|
||||
tests/mcp/test_tool_move_note.py,test_tool_move_note.py,source_code,25851,2025-07-30T09:39:45.727347,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co),c9f95158503fa42e37532aaa35027538fa49cea718d059d570cf547d31f01043
|
||||
tests/mcp/test_tool_read_content.py,test_tool_read_content.py,source_code,19442,2025-07-30T09:39:45.727526,jope-bm,1,jope-bm (joe@basicmemory.com),1817e32f997b06dbdabf02864d0c70584c1cd80bf1fae8dfbef649de79434bc5
|
||||
tests/mcp/test_tool_read_note.py,test_tool_read_note.py,source_code,22991,2025-07-30T09:39:45.727746,jope-bm,4,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co); Amadeusz Wieczorek (git@amadeusw.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),4e13ea9ff977525aa62b6a8c22505ffe989329dc3e7d07b71e044cf43ef3ddfd
|
||||
tests/mcp/test_tool_recent_activity.py,test_tool_recent_activity.py,source_code,3983,2025-07-01T08:33:22.098561,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),45414152cd45334b84df56f653146b7bd2939907548e7245a37207fdccfc4c87
|
||||
tests/mcp/test_tool_resource.py,test_tool_resource.py,source_code,6782,2025-07-01T08:33:22.098948,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),3916ae1c4d458eee53d2c570b9bbb708ecd617de5a145c3a7552389954bc9195
|
||||
tests/mcp/test_tool_search.py,test_tool_search.py,source_code,10959,2025-07-03T18:43:10.085177,Drew Cain,3,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),07839cf8e89d74ba05ec1dc55fa842444c1f0cc3d1587823b0ac3458ad89e87c
|
||||
tests/mcp/test_tool_sync_status.py,test_tool_sync_status.py,source_code,6331,2025-07-01T08:33:22.100021,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),3f3566df5b9f3c66bb9b3bd77d4c05825480d0e85015305cb1b7ab9429c7a7ec
|
||||
tests/mcp/test_tool_utils.py,test_tool_utils.py,source_code,9228,2025-07-01T08:33:22.100561,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),b50789f7a78b0169f52dc8bd16e262510ed4a0ac130fce698d4be84af1fe2d31
|
||||
tests/mcp/test_tool_view_note.py,test_tool_view_note.py,source_code,10004,2025-07-01T08:33:22.101164,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),385535ce791c3ab55db827cbcf27ea25ae85faf3a027f52df4eea052cede7f36
|
||||
tests/mcp/test_tool_write_note.py,test_tool_write_note.py,source_code,35136,2025-07-30T09:39:45.728137,jope-bm,3,jope-bm (joe@basicmemory.com); Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),45172de20cfc4f4436287573b4f5a94b8b616a360647af67ba1c6c343ccd41b8
|
||||
tests/repository/test_entity_repository.py,test_entity_repository.py,source_code,15498,2025-06-21T00:01:46.176265,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),8f5d1ff6cff7b640993ff73f463b1e7801a4dd69640fc55fa23dc58b865e9943
|
||||
tests/repository/test_entity_repository_upsert.py,test_entity_repository_upsert.py,source_code,16983,2025-07-03T18:43:10.085482,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com),ee0ecd6f4ff8b31838bd6de6fb1018af437e02813d8315c06f4e20a6b78a0572
|
||||
tests/repository/test_observation_repository.py,test_observation_repository.py,source_code,11786,2025-06-21T00:01:46.158862,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),3006d392ad94dcc9b3ae031d63a1af6935624534389dd025de70d4c9e112aa0b
|
||||
tests/repository/test_project_info_repository.py,test_project_info_repository.py,source_code,1186,2025-06-21T00:01:46.161876,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),dbce7ae7350403cbc37343c2e3e68e4d2255e247006aaa6c495a7ce8bfddea7d
|
||||
tests/repository/test_project_repository.py,test_project_repository.py,source_code,10511,2025-07-30T09:39:45.728536,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),279a3324635b1b515c18003bd1875cf230fef1192cadef9ca426846783717e75
|
||||
tests/repository/test_relation_repository.py,test_relation_repository.py,source_code,11980,2025-06-21T00:01:46.167567,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),fcdbdf40f7b56145b98eedde5ead64ef6187e859adde48cc09f5a97754757428
|
||||
tests/repository/test_repository.py,test_repository.py,source_code,5996,2025-06-21T00:01:46.152691,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),3a6d4ca0b7a438a30ae504f5eac22874d4e1a937a4246e90efad46c90f0e1c95
|
||||
tests/repository/test_search_repository.py,test_search_repository.py,source_code,25471,2025-07-03T18:43:10.086041,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),805b5cc55a53ae9d6e86c0de735c7f38ac9fd92c23194b0150fa3e7f5507a146
|
||||
tests/schemas/test_memory_url.py,test_memory_url.py,source_code,2000,2025-06-21T00:01:46.346515,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),946fa6c48534f41c8b9cc27bbad164d3e3673a79afa84fbb439fae4cd26c504b
|
||||
tests/schemas/test_memory_url_validation.py,test_memory_url_validation.py,source_code,9338,2025-07-01T08:33:22.103247,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7054c3a3cdb8e05d2ffd254ed298908ff3d48a0a3927d960975eae1ab9beeb38
|
||||
tests/schemas/test_schemas.py,test_schemas.py,source_code,16880,2025-07-03T18:43:10.086337,Drew Cain,3,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),2c746a00c340f063c160ace67569955a300a294040949695c79fd4ae124fb611
|
||||
tests/schemas/test_search.py,test_search.py,source_code,3264,2025-06-21T00:01:46.343685,github-actions[bot] (AI Assistant),2,github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),7437775d1b4a61a438cbabe3dbb1becd2adc4dbcc0c24595830c0843fa7a3166
|
||||
tests/services/test_context_service.py,test_context_service.py,source_code,8833,2025-06-21T00:01:46.446200,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),ab49d21a05d5364e7e8a2f00b07aabe1109b7417698e0acd92792e2cb5f22732
|
||||
tests/services/test_directory_service.py,test_directory_service.py,source_code,6854,2025-06-21T00:01:46.457258,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),a0c765fb6bd1a75b6ba4592b1df344c48273b6ab03ba6bd15d2b5e840eb2d521
|
||||
tests/services/test_entity_service.py,test_entity_service.py,source_code,59278,2025-07-03T18:43:10.086674,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),d7d495756faa10b415d705cf26b69d2efbc5f190a834b6767386ff296629ea71
|
||||
tests/services/test_file_service.py,test_file_service.py,source_code,5243,2025-06-21T00:01:46.460141,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),83e6e9a7584b7baeccbc84865d8c1d3e2a0223e7d2b9c066d3799c289196ab7d
|
||||
tests/services/test_initialization.py,test_initialization.py,source_code,6939,2025-07-08T20:39:18.351946,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ebb8d11ff37dcb36c4844c68f873c8e0e8aa6ab1b82abb22ee96c4233e6ab287
|
||||
tests/services/test_link_resolver.py,test_link_resolver.py,source_code,13618,2025-07-01T08:33:22.105766,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),c9fc56d0c3572770e95ca6175c213ed8b44c2a6ddddc298b5662cc2d8f41014e
|
||||
tests/services/test_project_service.py,test_project_service.py,source_code,28152,2025-07-30T09:39:45.728920,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),430868ed796518937a3b0bed4d8c33be53264a6100d61d54b0745bb2b58bf29a
|
||||
tests/services/test_project_service_operations.py,test_project_service_operations.py,source_code,4679,2025-06-21T00:01:46.442141,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),d4103b6c5fff7e74ba84b86033537b8522aa60fc75aec4974d52e1dfec68b1b4
|
||||
tests/services/test_search_service.py,test_search_service.py,source_code,25299,2025-06-21T00:01:46.439133,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),b3e58258db2b727ab8a1792e52fcb038d3a0779bcd1b70f2d295a9d06e5e2513
|
||||
tests/services/test_sync_status_service.py,test_sync_status_service.py,source_code,9926,2025-07-03T18:43:10.087266,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),00245ca356e33fc7afcd2d42cb5ec082038c95383d738067821cf1e8d23c9a4b
|
||||
tests/sync/test_sync_service.py,test_sync_service.py,source_code,39659,2025-07-01T08:33:22.107088,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),0609aee9906549f27fc4289b5ac2d46a50e53e12a0615a2701f17fbe9a74e169
|
||||
tests/sync/test_sync_wikilink_issue.py,test_sync_wikilink_issue.py,source_code,1959,2025-06-21T00:01:46.420370,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),db4b98dd7008a1a26b0fd02b9a2d03bca950b281efc19d98a91b017c500b23a5
|
||||
tests/sync/test_tmp_files.py,test_tmp_files.py,source_code,5930,2025-06-21T00:01:46.424277,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),cead95a07b29f6c1ab933abe67f9648122f5f33d33753c8e4ab4934af96cbc25
|
||||
tests/sync/test_watch_service.py,test_watch_service.py,source_code,14076,2025-06-26T09:28:51.860679,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),e324a4127f8bfe59798f3a08125488c701e88239b7482d87a0394b91c55b893e
|
||||
tests/sync/test_watch_service_edge_cases.py,test_watch_service_edge_cases.py,source_code,2169,2025-06-21T00:01:46.409830,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b87c5015330e9b551630aab7ef95ac1e7663f0ce46e912d8155b319e139559aa
|
||||
tests/test_config.py,test_config.py,source_code,3680,2025-07-03T18:43:10.087558,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),6a246431cff739274efda895dfa28f38710289a73e645312f5fa05fce0885ec8
|
||||
tests/test_db_migration_deduplication.py,test_db_migration_deduplication.py,source_code,6166,2025-07-08T20:39:18.352690,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com),ab768e061531ca2edcd598c101b5d884e549f90c7a4b305a23713a199d263fbb
|
||||
tests/utils/test_file_utils.py,test_file_utils.py,source_code,6199,2025-06-21T00:01:46.219673,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4615bb20b92b8a79d9e086301f852b34c3360f17ecc209920d68528268ddeed4
|
||||
tests/utils/test_parse_tags.py,test_parse_tags.py,source_code,1691,2025-06-21T00:01:46.223400,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),b83afdf18dac8b3411bd0fab1301683217d3d1abceaf2d22c09e79c709efd8eb
|
||||
tests/utils/test_permalink_formatting.py,test_permalink_formatting.py,source_code,4193,2025-06-21T00:01:46.225738,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),d998a4ca846c78679728a918b6f71df67c30b6961ede3cffa609f2cf6ad160eb
|
||||
tests/utils/test_utf8_handling.py,test_utf8_handling.py,source_code,5791,2025-06-21T00:01:46.228483,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),50ec792e572af51cd2d996d4ddacbcfd3bbd79aa928818a90c42117451ea829f
|
||||
tests/utils/test_validate_project_path.py,test_validate_project_path.py,source_code,15346,2025-07-30T09:39:45.729224,jope-bm,1,jope-bm (joe@basicmemory.com),27a4bf2a9d277e6cd773619e5241b20175d211ec60c284361bec404fd99143f3
|
||||
uv.lock,uv.lock,other,232710,2025-07-30T09:39:36.619155,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),caa1e3f6e66464b884a78cab77df2fe303fa3c0dc573be0196474b6d3c150581
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
path,name,category,size_bytes,modified_time,primary_author,contributor_count,contributors_list,sha256_hash
|
||||
.claude/commands/release/beta.md,beta.md,documentation,3103,2025-07-01T08:33:22.037438,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),345f06e3f304470a8892e96979355919c8e5d8f4212ebbe543c3272fd15a5cbe
|
||||
.claude/commands/release/changelog.md,changelog.md,documentation,4648,2025-07-03T18:43:10.075727,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ea2484352f4f881aa71f546dcf84b0882d60c96673a88406aa2f7e12a282a1f4
|
||||
.claude/commands/release/release-check.md,release-check.md,documentation,3344,2025-07-01T08:33:22.040060,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7dd41a40e95c89153936c4ea60a3c989eccd5973fff9f6caef156de82f12bd04
|
||||
.claude/commands/release/release.md,release.md,documentation,2925,2025-07-01T08:33:22.043674,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),0e4afd788fcf85fbe46094452e9b92a4290c3e23702283e72ee56aa9288476e5
|
||||
.claude/commands/test-live.md,test-live.md,documentation,18720,2025-07-03T18:43:10.076403,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),21a9edeefb3e5673d833151a2fd0464087c0f0a9d056fdd7fca474c1d177d6fc
|
||||
.dockerignore,.dockerignore,other,623,2025-07-01T08:33:22.048301,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b3079bffad89d07186c9166725075524ffc07ceea6dc1d53fdbea6dca9585684
|
||||
.github/ISSUE_TEMPLATE/bug_report.md,bug_report.md,documentation,967,2025-03-01T14:22:50.968994,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),0d34443ddda84a5e9726c8268083c9646d2cc64ff9c5ebf56c89587cef4c7706
|
||||
.github/ISSUE_TEMPLATE/config.yml,config.yml,configuration,374,2025-03-01T14:23:26.936836,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),f4e21bb9b7f053509b6c08548a2649a5d62f44f0106810aa8c915d2f29acc623
|
||||
.github/ISSUE_TEMPLATE/documentation.md,documentation.md,documentation,557,2025-03-01T14:23:40.020117,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),78f80a6b9a2b954695f74a847a068e0b780da6020572401c13c931555a4a9db5
|
||||
.github/ISSUE_TEMPLATE/feature_request.md,feature_request.md,documentation,786,2025-03-01T14:23:06.344081,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),cee834de442221d53f3d2eeb85b81f3a641b3a7cb587ef8a169452305007439d
|
||||
.github/dependabot.yml,dependabot.yml,configuration,529,2025-03-15T10:40:16.936258,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),8c6c2ef9a1417da63adb49c70f45524129bd5c46bbd7452b6b97cfe65205f89d
|
||||
.github/workflows/claude.yml,claude.yml,legal,4628,2025-07-03T18:43:10.076958,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),d026f1756310781b85abc491b454cc31b52dfce4b951cdf8a4a555b6b047604c
|
||||
.github/workflows/dev-release.yml,dev-release.yml,configuration,1405,2025-06-03T08:31:42.915367,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),58bcfc9e11e210e02dee76aabe7d1378da4edafb7f61857049ee6ccee9d45ffa
|
||||
.github/workflows/docker.yml,docker.yml,configuration,1621,2025-07-01T08:33:22.049870,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c918a86f2b16ebb45182a73c45272bcd6cea7022cc6e0eaeb2b2a24d8eebfe4d
|
||||
.github/workflows/pr-title.yml,pr-title.yml,configuration,887,2025-02-17T16:29:35.733646,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),b6df62a20235496feba4d9419c85d6e8446c4d447ad69e7f57b51cc8953f68f4
|
||||
.github/workflows/release.yml,release.yml,configuration,2479,2025-07-03T18:43:10.077261,Drew Cain,2,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co),163307a99e2f3d317af6ae8927e1537654d88bf574a6958ac0bcd2d4de266b2a
|
||||
.github/workflows/test.yml,test.yml,configuration,1426,2025-07-01T08:33:22.054430,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ab09084a11be8adfc83fc6285c652fbe44a4584aa7d7d3658fcf0c57112d1640
|
||||
.gitignore,.gitignore,other,566,2025-07-01T08:33:22.055006,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),358658b0faa8c3145e539421deba4ad0bac4a8ea2e608b2c2f867a822c67b36f
|
||||
.python-version,.python-version,other,5,2024-12-02T16:41:37.012830,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),7b55f8e67b5623c4bef3fa691288da9437d79d3aba156de48d481db32ac7d16d
|
||||
CHANGELOG.md,CHANGELOG.md,documentation,53647,2025-07-06T10:34:45.550847,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); semantic-release (semantic-release),0b165eb18975ff253cde08f4687b7808ac6ce63b6d8c10912c495a76c9058d1a
|
||||
CITATION.cff,CITATION.cff,other,302,2025-02-17T16:29:35.735146,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),bc4878cf6f80bb64655f3f93a5821ffe0924ad8311c784b5f81a36093fcffdda
|
||||
CLA.md,CLA.md,legal,1342,2025-03-01T14:12:38.435132,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),c238c7ff4f2b8451c6bba1bbacd10b04fb7d41d9a9cd76a62644d8193791d169
|
||||
CLAUDE.md,CLAUDE.md,legal,12310,2025-07-03T18:43:10.078147,Drew Cain,4,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); Ikko Eltociear Ashimine (eltociear@gmail.com),751f10c1a9a9c0ba2054d9c9cd36cf0989d6a347491fd505bb6504ee74b12900
|
||||
CODE_OF_CONDUCT.md,CODE_OF_CONDUCT.md,documentation,509,2025-02-17T16:29:35.735238,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),b276b464e85eb1e5d9f09d90bba9573508a7bf81d2eeb38d0f25bd9f2fa66198
|
||||
CONTRIBUTING.md,CONTRIBUTING.md,documentation,7329,2025-07-30T09:39:36.607158,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co),ca822fe39cc529bfc3822d317ab8a20fd141ec3ecb8c20de9e2eca3edce1c282
|
||||
Dockerfile,Dockerfile,other,856,2025-07-01T08:33:22.056908,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),c45501905423990e31ffdb8ae4e976eca6423c6784905c1036bcc7795b70feef
|
||||
LICENSE,LICENSE,legal,34523,2024-12-02T16:41:20.739392,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef
|
||||
README.md,README.md,documentation,16261,2025-07-03T18:43:10.078281,Drew Cain,6,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Jason Noble (github+jasonn@jasonnoble.org); Marc Baiza (43151891+mbaiza27@users.noreply.github.com); Matias Forbord (codeluggage@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),2c4b82b30f49ab465617034fda999157450ed168f8e097fe8e9ca7ac65a197a7
|
||||
SECURITY.md,SECURITY.md,documentation,303,2025-03-12T16:04:01.989927,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),96932268b8fe737577941b49e9dff5ff8ad59c11cc3f9a016989589ef5c98c1f
|
||||
docker-compose.yml,docker-compose.yml,configuration,2534,2025-07-01T08:33:22.059281,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),4a9fdfd2f0d42b148fe021eee77fd5e772c7ba9c2fe5d0cc310c8c7a64257417
|
||||
docs/AI Assistant Guide.md,AI Assistant Guide.md,documentation,16423,2025-07-01T08:33:22.060491,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c717869a3d356852077026591364ecb0589a6d53f9764af027c57d142ea7ff55
|
||||
docs/Docker.md,Docker.md,documentation,9088,2025-07-01T08:33:22.060976,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),e13d3f50148353046b52d9d493986880f76f2914ef4efe1a759b2cc9d10913a4
|
||||
justfile,justfile,other,5496,2025-07-30T09:39:36.608008,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com),a8df6c73ee8cf3eaa48e4cba193dffbc2821b843a8cc0c0cd58b8517f2252907
|
||||
llms-install.md,llms-install.md,documentation,2307,2025-06-29T14:19:28.826375,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),674105eb85c8f1d59fc2ddc6840f2a2a3e293358687a305c37dffc8144304f97
|
||||
memory.json,memory.json,configuration,90075,2025-06-29T14:19:28.827172,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),22d5eee995ca0548b7d9dd152f9a3690c42a514cbf38fea750b3ed2a17dd99c5
|
||||
pyproject.toml,pyproject.toml,configuration,3279,2025-07-30T09:39:36.608791,Paul Hernandez,4,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); semantic-release (semantic-release),428a5d64d4555dc7cd2a81b89a3444bdb1d2f3017257fbc9160bd0ae982718e1
|
||||
smithery.yaml,smithery.yaml,configuration,433,2025-03-11T16:36:03.389716,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),63f13499cf4fee0751b5005f5bd85597dc1c7f9a881f4ec8147cc23e95e75c2d
|
||||
src/basic_memory/__init__.py,__init__.py,source_code,256,2025-07-06T10:34:45.551291,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@gmail.com); semantic-release (semantic-release),c946490ab9d64957d25df37f721392024916f8a81ed838f70a8e938b1c2637db
|
||||
src/basic_memory/alembic/alembic.ini,alembic.ini,configuration,3733,2025-02-23T17:31:23.198433,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),20466c9c5f026db66793006bebb2f328a3686c7bb34da40d52f33c3eca29e717
|
||||
src/basic_memory/alembic/env.py,env.py,source_code,2838,2025-07-08T20:39:18.336650,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),e241d987eadfcd5011cbd9ddbecb833d306de87937759d81c08d37d04a6906b0
|
||||
src/basic_memory/alembic/migrations.py,migrations.py,source_code,718,2025-06-21T00:01:59.613806,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),96b8873d70dd04b48d5c45b7413a54d1224ab9577557ef0dad5929377a9fb144
|
||||
src/basic_memory/alembic/script.py.mako,script.py.mako,other,635,2025-02-17T16:29:35.739659,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),304a8bfb6a804e5493f5300e79882c70c9f5bb2e87512f4d16f0e097dddd323f
|
||||
src/basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py,3dae7c7b1564_initial_schema.py,source_code,4363,2025-06-21T00:01:59.595762,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),9536d69409ddd5eb3bc54f7d0e82607da45ed7f2ad7bc4cbf7cae2a9e28657cd
|
||||
src/basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py,502b60eaa905_remove_required_from_entity_permalink.py,source_code,1840,2025-06-21T00:01:59.604717,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),93ac584e660f33d468b3eec203b070641298c282bf82655d0beda7f050237681
|
||||
src/basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py,5fe1ab1ccebe_add_projects_table.py,source_code,4379,2025-06-21T00:01:59.601299,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),d82098f5aca3cdbada60c7229ea4ac262f527349eed9ef9e864509bacfaccf7e
|
||||
src/basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py,647e7a75e2cd_project_constraint_fix.py,source_code,4210,2025-07-01T08:33:22.063463,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),604ac5908a5976272f52297866d13abb14a99390590935d9fd37cf13e32049fa
|
||||
src/basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py,b3c3938bacdb_relation_to_name_unique_index.py,source_code,1428,2025-06-21T00:01:59.598551,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),46c1b2990cdf4575752d234a8b28b494c8a54f15b5360c12f6347aef27b66a92
|
||||
src/basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py,cc7172b46608_update_search_index_schema.py,source_code,3679,2025-06-21T00:01:59.610909,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),9036af47d42ac7dc12bb93f7a9de1266aff0688b031b550c4e0d92983a24b4c5
|
||||
src/basic_memory/api/__init__.py,__init__.py,source_code,72,2025-06-21T00:01:59.579981,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),c02a63fb6d63d43d0acca97d425eae9cb055158d0ad6e1a9fc57926512adaa99
|
||||
src/basic_memory/api/app.py,app.py,source_code,2897,2025-07-08T20:39:18.337081,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ad673ac2c09410af5830f6afdffaa28b0040480dacd52ff01750b8b2c066e8cf
|
||||
src/basic_memory/api/routers/__init__.py,__init__.py,source_code,398,2025-06-21T00:01:59.549416,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),4443b908a43dea8e6fb4658009cb08c48a56c9b21449e29773cdd159b59cf014
|
||||
src/basic_memory/api/routers/directory_router.py,directory_router.py,source_code,2054,2025-06-21T00:01:59.567371,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac1407beec1f7d43a4d222af704b3642559c960bebf2eaf6e1ffe91f7fac5514
|
||||
src/basic_memory/api/routers/importer_router.py,importer_router.py,source_code,4513,2025-06-21T00:01:59.561600,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),c45502a2b90f5a8f00174c9ad14adc2e65cd7fc0a33d9740a9d750207f0f3bea
|
||||
src/basic_memory/api/routers/knowledge_router.py,knowledge_router.py,source_code,9738,2025-07-01T08:33:22.064119,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e1d0ffb4fa5c246582ba844a11b417092de1a173567be55b7060bbc55fbc5681
|
||||
src/basic_memory/api/routers/management_router.py,management_router.py,source_code,2730,2025-07-08T20:39:18.337438,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),cdbce294dcec6146c56c4dae15744cdf77039fd21b23eef7cbc0b5f9bfb5f4ee
|
||||
src/basic_memory/api/routers/memory_router.py,memory_router.py,source_code,3003,2025-07-01T08:33:22.064510,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6bd0a7c775e0c1290efb6001173237c0cdcfa0c1b1bb27c9145a7b35f1596a97
|
||||
src/basic_memory/api/routers/project_router.py,project_router.py,source_code,8444,2025-07-30T09:39:45.721369,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6539c69f1d506c65d14cc2c151876b19057f37d84d51a9e6f464ed47a4692648
|
||||
src/basic_memory/api/routers/prompt_router.py,prompt_router.py,source_code,9948,2025-07-01T08:33:22.065221,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e30c6aebe35112054933c37d0b462c3750005030fc9e44c23b35b2cd2a93485c
|
||||
src/basic_memory/api/routers/resource_router.py,resource_router.py,source_code,8058,2025-06-21T00:01:59.570399,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),584244a84698ff24ca8f9f94fab5b891740a51c25f972920c08ebf83f478d5c9
|
||||
src/basic_memory/api/routers/search_router.py,search_router.py,source_code,1226,2025-06-21T00:01:59.543020,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),183eb68e50904e22ff54db1d89b8be6f57fa1f8d0a096a3d497d8297b607e105
|
||||
src/basic_memory/api/routers/utils.py,utils.py,source_code,5159,2025-07-03T18:43:10.079143,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),9e60f57da24e1dc9d64236c26a7a2353003dc618a77fbeb853c494aa3357a57c
|
||||
src/basic_memory/api/template_loader.py,template_loader.py,source_code,8607,2025-06-21T00:01:59.576132,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),7b16d3797c89460c8b1664a335eae8c634fb5f60d6166d97e6a5156b776b6d83
|
||||
src/basic_memory/cli/__init__.py,__init__.py,source_code,33,2025-06-21T00:01:59.456735,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),6ab70a2c05910e13c3ef1e6df34325be265e633c076741998a0cb2dcd295a069
|
||||
src/basic_memory/cli/app.py,app.py,source_code,2268,2025-07-08T20:39:18.337708,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),f710670380d707c4458ad881f7a25bb3c5cdb2bcff59f139f37421264fe8f2f8
|
||||
src/basic_memory/cli/commands/__init__.py,__init__.py,source_code,393,2025-07-08T20:39:18.338070,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),de8a23702f98fb844fa9f7fdbedc16ce24ff4f8baf05589c2f4a521cd8a55645
|
||||
src/basic_memory/cli/commands/db.py,db.py,source_code,1480,2025-07-08T20:39:18.338326,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),704a1096d80ab9d12e247d029fe6223e935a0d0ceee7e615c020f46a720828e0
|
||||
src/basic_memory/cli/commands/import_chatgpt.py,import_chatgpt.py,source_code,2856,2025-07-08T20:39:18.338787,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),8957cca3acab6351335624a518bdc19d956487e93d86dd1b6c2309e9d5850ae5
|
||||
src/basic_memory/cli/commands/import_claude_conversations.py,import_claude_conversations.py,legal,2946,2025-07-08T20:39:18.339053,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7bc97838732bf00f4fb4a80e3ba4fdfbce8971ae85cc2ac902c3b3a3e110ae57
|
||||
src/basic_memory/cli/commands/import_claude_projects.py,import_claude_projects.py,legal,2891,2025-07-08T20:39:18.339310,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),6321577075801c99ad47a0cdc13b5aa3c9ca102a05ca8f06ae2a51125a8c43bc
|
||||
src/basic_memory/cli/commands/import_memory_json.py,import_memory_json.py,source_code,3013,2025-07-30T09:39:45.721637,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7ea193f6720b2672290ad16c79a143a31d88a1d567838f12b9ebf9110a7c0338
|
||||
src/basic_memory/cli/commands/mcp.py,mcp.py,source_code,2593,2025-07-08T20:39:18.340025,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),3e1be3d27b18ef4743204b7cf8a37fbc2a1a91fc1636c468e08cefcd8597bac6
|
||||
src/basic_memory/cli/commands/project.py,project.py,source_code,13836,2025-07-30T09:39:45.721838,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),af3e6f30410ab48355f4b1a46aaa3aa5423cb0a0490f51447db1d25e09f39916
|
||||
src/basic_memory/cli/commands/status.py,status.py,source_code,5809,2025-07-08T20:39:18.340324,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b6d0ea237d714d054f69b24a2058beba74e224a2fa4a80a525d62a2aa2ea2d70
|
||||
src/basic_memory/cli/commands/sync.py,sync.py,source_code,8213,2025-07-08T20:39:18.340627,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),703b3e73c4c33850c27f6f4c07abbf5d4ce5dc43f706bf0c43264bbb45f8b213
|
||||
src/basic_memory/cli/commands/tool.py,tool.py,source_code,9540,2025-07-01T08:33:22.070903,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),9b2fa400b9f7921bf55b602f8bbdfa36b1ec7e4a5bc8fe7b9838b92e31f0a9ed
|
||||
src/basic_memory/cli/main.py,main.py,source_code,465,2025-07-08T20:39:18.340868,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7a405b12bff98e319968526238ae15859df0362bba5a3857562fecca47b5c70d
|
||||
src/basic_memory/config.py,config.py,source_code,12487,2025-07-30T09:39:45.722031,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),86341330a041b94d38f5e4f1b94f3509d74351c67cc826af1a4ff57313e5f726
|
||||
src/basic_memory/db.py,db.py,source_code,7428,2025-07-08T20:39:18.341550,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),25e053b0b3f9e8fa823b05fc099001efe8a3a4f6e0f80728e5079ba45aa406d8
|
||||
src/basic_memory/deps.py,deps.py,source_code,12144,2025-07-08T20:39:18.341870,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bce28cd9ca549b8afce0bc689cc1fb2dc80cd5b929f85114aa682bb8ae82411d
|
||||
src/basic_memory/file_utils.py,file_utils.py,source_code,6700,2025-06-21T00:01:59.587200,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),79ac5328b2c46d3232fcc6ff22ffc39ade085c0252ad91958be2a7ae9c9c8b71
|
||||
src/basic_memory/importers/__init__.py,__init__.py,source_code,795,2025-06-21T00:01:59.295471,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),0537015bdecfded85cb166b9c3db50b2f3aef329c70e0c36fbcb4f824099a21f
|
||||
src/basic_memory/importers/base.py,base.py,source_code,2531,2025-06-21T00:01:59.305335,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),6b0c1efd4f827f348834aa0cea2ae8eebbb842a2e5b1f5f3747ced0ef79b9f13
|
||||
src/basic_memory/importers/chatgpt_importer.py,chatgpt_importer.py,source_code,7372,2025-06-21T00:01:59.308956,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),dfa540b196ab23b5c4e6bfcac4da567873351cf7c78fa35f4c3b87e84c58e218
|
||||
src/basic_memory/importers/claude_conversations_importer.py,claude_conversations_importer.py,legal,5725,2025-06-21T00:01:59.288540,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),a7bc9e872f4f81ce5f79fe9dd1a6fd0f00a6f020a2cb2664184a97f14eeb1dbc
|
||||
src/basic_memory/importers/claude_projects_importer.py,claude_projects_importer.py,legal,5389,2025-06-21T00:01:59.291737,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),a452675fd9bb18ebf64eb4bd7f69ccd7e993b617931015a3c4ab700d67493863
|
||||
src/basic_memory/importers/memory_json_importer.py,memory_json_importer.py,source_code,4631,2025-07-30T09:39:45.722316,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bc7d045299f17ed9ad5cebff7b1423250ec28a11130e47a0ac48d3ab82bdeafc
|
||||
src/basic_memory/importers/utils.py,utils.py,source_code,1792,2025-06-21T00:01:59.298428,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),ee7e22fd4812362394f62ed8944abe80738c1e3ab88131faf639f4a8ccfd6c89
|
||||
src/basic_memory/markdown/__init__.py,__init__.py,source_code,501,2025-06-21T00:01:59.316337,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),0ddce2a025ad0e729aab4e411d82e02ffefc15ac041cba4b5e7a7e90f4957c87
|
||||
src/basic_memory/markdown/entity_parser.py,entity_parser.py,source_code,4516,2025-06-21T00:01:59.321461,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),1b0d1177358645ccbadd170b3f62760dc9bd838d38c741978ab0da5836234d68
|
||||
src/basic_memory/markdown/markdown_processor.py,markdown_processor.py,source_code,4968,2025-06-21T00:01:59.325241,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4b99f2ebdceed9d96a3bbb562682eba4b4b383c7a64080eaec3bbba25a4952c9
|
||||
src/basic_memory/markdown/plugins.py,plugins.py,source_code,6639,2025-06-21T00:01:59.312733,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),82d2332918e866ccaf06a2e954d9eb9b397f71b4d9e591a7f2472e5f1423464a
|
||||
src/basic_memory/markdown/schemas.py,schemas.py,source_code,1896,2025-06-21T00:01:59.329982,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7b2c580abd615725a62267257b46ac13922dfc30fa01192a681658bb528ae67e
|
||||
src/basic_memory/markdown/utils.py,utils.py,source_code,3410,2025-07-03T18:43:10.079889,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),1b757f0d06e60e3ea276c0b2ea44fe1a156a895e093c1e607d6286e702bf4ae4
|
||||
src/basic_memory/mcp/__init__.py,__init__.py,source_code,35,2025-06-21T00:01:59.404757,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),76c0ce84aaa361e21b0942db1c8c5f708b536eab9d12e120d4da7ce9eab41844
|
||||
src/basic_memory/mcp/async_client.py,async_client.py,source_code,925,2025-07-30T09:39:45.722547,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),6531f4387f30a30a322197ff51d6eacae4b25a58c648095931b3d1145d1da303
|
||||
src/basic_memory/mcp/project_session.py,project_session.py,source_code,4203,2025-07-08T20:39:18.342906,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4da403ec93dec9063eeb8b05246e3501740d162cc77a067d3ce54ba43d690819
|
||||
src/basic_memory/mcp/prompts/__init__.py,__init__.py,source_code,615,2025-07-08T20:39:18.343263,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),f8197d0e08f64c3f4f50b8f38203ea5eebcf123582472ed2f58834de1dbe53b0
|
||||
src/basic_memory/mcp/prompts/ai_assistant_guide.py,ai_assistant_guide.py,source_code,821,2025-06-21T00:13:53.262832,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),f13239c4e6e2455730bfac3d6f2d71407957d3086fc84efe2c6b22a833114c58
|
||||
src/basic_memory/mcp/prompts/continue_conversation.py,continue_conversation.py,source_code,1998,2025-06-21T00:13:53.263309,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),aec9a50b657b7bb1ba0c02b42bcdb9bc5b0f2a0b1143bd361c5ce7ea59076833
|
||||
src/basic_memory/mcp/prompts/recent_activity.py,recent_activity.py,source_code,3311,2025-07-01T08:33:22.073986,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),d2fd5cddbd927430f15d5b85f1e3a3368a18cb4e2e4587a5d29749d2b9e0830e
|
||||
src/basic_memory/mcp/prompts/search.py,search.py,source_code,1692,2025-06-21T00:13:53.264093,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),9dbf3c319cbdb5d5bf32608b51522d8ae92b2dd6f7c441d62efd0954b525738a
|
||||
src/basic_memory/mcp/prompts/utils.py,utils.py,source_code,5431,2025-06-21T00:01:59.425042,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),55a72b6eac18b724a9225608aca1e8e6cea3b684ccb1c609aab1511f7ce90ba4
|
||||
src/basic_memory/mcp/resources/ai_assistant_guide.md,ai_assistant_guide.md,documentation,14777,2025-03-28T19:12:38.653627,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),aa76160e46256fe2662ae3a86799659916acfede1d5835c1fe2f0b136efb2e0b
|
||||
src/basic_memory/mcp/resources/project_info.py,project_info.py,source_code,1906,2025-06-21T00:13:53.264918,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),2dc5244f1e225c17d4e8ba784d5721efc3aa2e8a5b3b2e258f22a77ebe155eca
|
||||
src/basic_memory/mcp/server.py,server.py,source_code,1248,2025-07-08T20:39:18.343419,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),0a910cae27d61ea923e90fbe2d4b5c7cacf3b29d3e156f0b7f77872ab973c761
|
||||
src/basic_memory/mcp/tools/__init__.py,__init__.py,source_code,1619,2025-07-03T18:43:10.080029,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co),872b771dd52ec3b763645a2baf6e10a70f049ce3106830a6d3f053b3e09a5fe6
|
||||
src/basic_memory/mcp/tools/build_context.py,build_context.py,source_code,4611,2025-07-03T18:43:10.080426,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),724280b77b8f5f3e61cd3fdeebc3ee66e2bcfedaaea3638e6a2e2e33fcb1978e
|
||||
src/basic_memory/mcp/tools/canvas.py,canvas.py,source_code,3738,2025-07-01T08:33:22.076204,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),db617d1bd81f3dbfa5f22d41e6b6b825afe1f73618f37acf63d9830390b98246
|
||||
src/basic_memory/mcp/tools/delete_note.py,delete_note.py,source_code,7346,2025-07-01T08:33:22.076726,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),b52c9173f5600662f255e7a70a5c17d5293ef8b29c19a8403334d7da62b65c8b
|
||||
src/basic_memory/mcp/tools/edit_note.py,edit_note.py,source_code,13570,2025-07-01T08:33:22.077181,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),ab8c7e7fbfa3fe5fb0ce6d7bf805454f5fd6182a340aae25237b1e6127b6d5a6
|
||||
src/basic_memory/mcp/tools/list_directory.py,list_directory.py,source_code,5236,2025-06-21T00:13:53.267453,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),f85c43b02aeee580f4d8ce2a9100c0bab1095b245a0bb608e1847ace0d1ab51f
|
||||
src/basic_memory/mcp/tools/move_note.py,move_note.py,source_code,17541,2025-07-30T09:39:45.722919,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),5ef27d9baaebc2c5656fc49c4798a4e4e40a70b6cd9d1e761dbcae021bad558c
|
||||
src/basic_memory/mcp/tools/project_management.py,project_management.py,source_code,12944,2025-07-06T10:34:45.552403,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co),cdac736d65129f689a85c6b82f8e043bc84a31657ead7a8331771171eeaa860f
|
||||
src/basic_memory/mcp/tools/read_content.py,read_content.py,source_code,9125,2025-07-30T09:39:45.723469,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),96a13addac507fd874394dfa2e1e5ed71af5eb89d0872c1d68cacf9cac3f8724
|
||||
src/basic_memory/mcp/tools/read_note.py,read_note.py,source_code,8132,2025-07-30T09:39:45.723828,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Amadeusz Wieczorek (git@amadeusw.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),dff53ce02b16d9c5fca315b3f09bb70867a89d614b0ef3f0548dce87978048ee
|
||||
src/basic_memory/mcp/tools/recent_activity.py,recent_activity.py,source_code,4833,2025-06-21T00:13:53.269120,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),5d58cd2402679b1bf3c7dfcbb3503e40e776c9347ba499524d3b91c528af98de
|
||||
src/basic_memory/mcp/tools/search.py,search.py,source_code,15883,2025-07-03T18:43:10.081557,Drew Cain,4,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),8519b0057468c4450eb543a2f561bcd0d7cb96e1ce1b95e948e02b309ba6b7ca
|
||||
src/basic_memory/mcp/tools/sync_status.py,sync_status.py,source_code,10517,2025-07-08T20:39:18.344231,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),73dd73b186eaec8b82e6b17e39a67c27082d24e0653dc4e4b35712bd83b80a22
|
||||
src/basic_memory/mcp/tools/utils.py,utils.py,source_code,20892,2025-07-30T09:39:36.616253,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),a95004911e276822eba88a3fef15c5b9ba861b1ca9a2ecfe0c1e3fb2f4ef0116
|
||||
src/basic_memory/mcp/tools/view_note.py,view_note.py,source_code,2499,2025-07-01T08:33:22.080024,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),75d357c72113b1d0394987e521a4158ff09b77b23b08b56cddab5144331b1a68
|
||||
src/basic_memory/mcp/tools/write_note.py,write_note.py,source_code,6612,2025-07-30T09:39:45.723985,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),771396fb49d397feb5f7e36321d4d233c22c4d1b53414912c849957c6e96fc83
|
||||
src/basic_memory/models/__init__.py,__init__.py,source_code,333,2025-06-21T00:01:59.437212,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),8f40b876d162f85384690291f1d416106f9d26d750d793414e2260e276c85cd5
|
||||
src/basic_memory/models/base.py,base.py,source_code,225,2025-06-21T00:01:59.447249,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),e2101727c084d519e32a16f6de53ddf9033b1bf15721d4e8c0c27d6d18b1b295
|
||||
src/basic_memory/models/knowledge.py,knowledge.py,source_code,7087,2025-06-21T00:01:59.444376,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),005c5f292f1f45ae372aadc48c9080b9fa6d7b854d0bb7ecf587ec843ac1e28d
|
||||
src/basic_memory/models/project.py,project.py,source_code,2773,2025-07-01T08:33:22.080907,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),a14ad06943aeeff9ae4a5fa2dfc0e1d07ce6085acc02dc20c402c3513b7d9397
|
||||
src/basic_memory/models/search.py,search.py,source_code,1300,2025-06-21T00:01:59.440923,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),3e143cc38b5a0294af8e1d43a4f841e1c1fd193b76136a68f83159ce19e86646
|
||||
src/basic_memory/repository/__init__.py,__init__.py,source_code,327,2025-06-21T00:01:59.256363,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),3162bea3c42292accea5ee52c8f6ca4368a8079056034529cfae6d820f84d035
|
||||
src/basic_memory/repository/entity_repository.py,entity_repository.py,source_code,10405,2025-07-03T18:43:10.082570,Drew Cain,3,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),e2a8d1eba6c8d64bc61d7168df0fe8c29a670858bf9daeac464a0f440273fae0
|
||||
src/basic_memory/repository/observation_repository.py,observation_repository.py,source_code,2943,2025-06-21T00:01:59.269646,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),aa132f1cb4a36a84f715afdc40e2ac4f98d83e3eba1974b2b4404cc0b020ca04
|
||||
src/basic_memory/repository/project_info_repository.py,project_info_repository.py,source_code,338,2025-06-21T00:01:59.252582,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),f172d50182a405643a19b2a3d62a80f4e2b41463c775394eb3b66d7de51ff6df
|
||||
src/basic_memory/repository/project_repository.py,project_repository.py,source_code,3803,2025-07-30T09:39:45.724344,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),9083ba214b2bcaaf98164d9c33cda7add52bc77f7269ecb6e43cdd5088728b7c
|
||||
src/basic_memory/repository/relation_repository.py,relation_repository.py,source_code,3179,2025-06-21T00:01:59.246745,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),cfb3a8e59cff27e063e91bd00e949647bdd92e4d9fc46edeee3acc6de15e26f4
|
||||
src/basic_memory/repository/repository.py,repository.py,source_code,15224,2025-06-21T00:01:59.273355,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),3096fe71bf106506cbf86aeafe2aafe0aabbe5a5f6c90a212c8aa1e53e1f171c
|
||||
src/basic_memory/repository/search_repository.py,search_repository.py,source_code,21936,2025-07-03T18:43:10.082844,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),a972f73d1b71dac5770e8eb3793c6c9ac44e4e792f9dab528f8c4e6c6a80bcaa
|
||||
src/basic_memory/schemas/__init__.py,__init__.py,source_code,1674,2025-06-21T00:01:59.514555,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),98480815c75379bfafe32d2090e87fa40e73caa19b664fbd5db5ea9528ba58cb
|
||||
src/basic_memory/schemas/base.py,base.py,source_code,6738,2025-07-01T08:33:22.082193,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),171f7b0c4ab33abef2f7379eb1e3bda950586ce7edff83907fd12255f84e267e
|
||||
src/basic_memory/schemas/delete.py,delete.py,source_code,1180,2025-06-21T00:01:59.502563,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),50047624af7d58c8f780ffb2a065a51c3dde64492f9534917fc42860813e59fc
|
||||
src/basic_memory/schemas/directory.py,directory.py,source_code,881,2025-06-21T00:01:59.529023,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),17dfcbac9a91a9bfe43b4f060ca2735cb6f69e16d81b63dd554a397830ff29fe
|
||||
src/basic_memory/schemas/importer.py,importer.py,source_code,693,2025-07-30T09:39:45.724633,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac33df423ca32b28ce7b6ea9c29d541f8783a86c0c29f78db35163bf93f139cd
|
||||
src/basic_memory/schemas/memory.py,memory.py,source_code,5833,2025-07-03T18:43:10.083167,Drew Cain,3,Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),acb4a953a553feca672c4895798a7d948ec51f922f75b66dd7d55716e3bebed3
|
||||
src/basic_memory/schemas/project_info.py,project_info.py,source_code,7047,2025-07-01T08:33:22.083248,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),7dc3635297b6e7fe6e3262b2d78da26f7a79a846a4cecd5624f2e4828979cf2c
|
||||
src/basic_memory/schemas/prompt.py,prompt.py,source_code,3648,2025-06-21T00:01:59.525753,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),4a92157d9a6b413f04e6e3f8d23dc2a417369c729f9703a8de2643ec114f2071
|
||||
src/basic_memory/schemas/request.py,request.py,source_code,3760,2025-06-21T00:01:59.511789,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),32fe44beb2d99452223ebf1d3a3a3fe105ef92c79885023b71dfd7db30ecc503
|
||||
src/basic_memory/schemas/response.py,response.py,source_code,6450,2025-07-03T18:43:10.083478,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),5eea4660a2abe48d83ed083d1c2483fdcfb403e0b504f03c14d22f1cae866abb
|
||||
src/basic_memory/schemas/search.py,search.py,source_code,3657,2025-06-21T00:01:59.522804,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),cb032c0c60102b6b0ed934f995cf9d6bf93aece296d71d537a75e8ae61c75afe
|
||||
src/basic_memory/services/__init__.py,__init__.py,source_code,259,2025-06-21T00:01:59.655059,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),5c6b7c597ddf5ffd0af4bdfb32ccbc1c5f2794c658206dee43a9945fafe226d8
|
||||
src/basic_memory/services/context_service.py,context_service.py,source_code,14462,2025-06-21T00:01:59.672086,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (paulmh@gmail.com),e1178b005e6a89f03d6b239e3c6b152aeb30d535a3f280734440a3aec16228f2
|
||||
src/basic_memory/services/directory_service.py,directory_service.py,source_code,6017,2025-06-21T00:01:59.648501,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),fd838f5ec79033892777b3c8140868f4b57f13e1658d513959524a434b9f959b
|
||||
src/basic_memory/services/entity_service.py,entity_service.py,source_code,30440,2025-07-03T18:43:10.083956,Paul Hernandez,4,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),7cd5163eca6b8a0772e838c8c869e478166763cd6a2d745b0b9aa5c25a65baee
|
||||
src/basic_memory/services/exceptions.py,exceptions.py,source_code,390,2025-06-21T00:01:59.668908,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),a158d0af9d1742a9c5ab5f8c34a062948d9286d1c3c72a5abf20e4d547b21e1c
|
||||
src/basic_memory/services/file_service.py,file_service.py,source_code,10059,2025-06-21T00:01:59.641785,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),8c2ae69c4913438b7d1c5ecbfcce812fbb5d0ea8e3cedcbd8694e8f40cf086f3
|
||||
src/basic_memory/services/initialization.py,initialization.py,source_code,6715,2025-07-08T20:39:18.344482,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),4dde58b799cf4ac90631e59949b55b3355a93bdf99df0d9c2408c0cea67e10c4
|
||||
src/basic_memory/services/link_resolver.py,link_resolver.py,source_code,4594,2025-07-01T08:33:22.084908,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),d7efd516cbea753e6b5411def09aeaeb7539f5743487119ecf163c736e138a8c
|
||||
src/basic_memory/services/project_service.py,project_service.py,source_code,30125,2025-07-30T09:39:45.725079,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),bce95c00aa9a1da0a8c8090ee34b5b1494207f8c99e6c25de4ec43fddc38f6be
|
||||
src/basic_memory/services/search_service.py,search_service.py,source_code,12782,2025-06-21T00:01:59.665439,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),7392b2d2e7f3ed83c5807855ccd450e0e79c17f254aedee700bce9323a1b5b83
|
||||
src/basic_memory/services/service.py,service.py,source_code,336,2025-06-21T00:01:59.636167,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),57e77ff20395d3bcc62100e92fe2acaacdd937d977a9fdc764e2b57ff60d4da8
|
||||
src/basic_memory/services/sync_status_service.py,sync_status_service.py,source_code,7504,2025-07-03T18:43:10.084229,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),0a025d689e8e16f1632872104952105fc904537f3d32bcff592eb1f1dc76fbb7
|
||||
src/basic_memory/sync/__init__.py,__init__.py,source_code,156,2025-06-21T00:01:59.621137,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),0951e0b981f8e7b876bb6c6833c2af3a29490bbd5726567ea9487c947723623e
|
||||
src/basic_memory/sync/background_sync.py,background_sync.py,source_code,726,2025-07-08T20:39:18.345062,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),549af64ae91128b76c6df07ef517b82de85ca4ad796be44b5c0141fac01d4513
|
||||
src/basic_memory/sync/sync_service.py,sync_service.py,source_code,23387,2025-07-01T08:33:22.086507,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),0310b927561370f593980d07773bce641b618b8fbf2d9e3890d174290a0344fc
|
||||
src/basic_memory/sync/watch_service.py,watch_service.py,source_code,15316,2025-06-26T09:28:51.859209,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),240ba6ac7523575945f4db442b7da3820d26c9605f2d7a2d357c4e35e215f523
|
||||
src/basic_memory/templates/prompts/continue_conversation.hbs,continue_conversation.hbs,templates,3076,2025-07-01T08:33:22.086792,Drew Cain,2,Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b6bac31d25c0e52d0909b2273285092f4e31bc219107f92ed511cd407b65e992
|
||||
src/basic_memory/templates/prompts/search.hbs,search.hbs,templates,3098,2025-05-25T10:07:54.520119,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),1f570222c1caa78542d46ac7d8a79407ca467b9bd714fa9bd953e8b72a667820
|
||||
src/basic_memory/utils.py,utils.py,source_code,8654,2025-07-30T09:39:45.725428,jope-bm,5,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); andyxinweiminicloud (andyxinweimin263@icloud.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),111f343e3367339730e081c08acf4613667be574f946126513b5d82da3086bed
|
||||
test-int/conftest.py,conftest.py,source_code,8203,2025-07-08T20:39:18.345735,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),7231e1bc76cddc7c94433e853825dc0165b103bedf996c2c24eda6ae32fff5ad
|
||||
test-int/mcp/test_build_context_validation.py,test_build_context_validation.py,source_code,6469,2025-07-08T20:39:18.346021,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),ee61d6b15cee0194fd74c1bcf9361d5a839f6081c7d571ef081da8fcefe1bbf2
|
||||
test-int/mcp/test_delete_note_integration.py,test_delete_note_integration.py,source_code,13175,2025-07-08T20:39:18.346366,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),497495249b8bcf0af177d8705c6899875460707697980434e560cfe56f86feb3
|
||||
test-int/mcp/test_edit_note_integration.py,test_edit_note_integration.py,source_code,19822,2025-07-08T20:39:18.346730,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),9ba704badd05808aee0b5a686b7e250e1c865229ec92dab561cba5287b6f6fc6
|
||||
test-int/mcp/test_list_directory_integration.py,test_list_directory_integration.py,source_code,14744,2025-07-08T20:39:18.347106,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),2c4336a59de63f17ed252bb52d1330ebca901ad83bfebc9e5aa67f132a3c77c6
|
||||
test-int/mcp/test_move_note_integration.py,test_move_note_integration.py,source_code,19825,2025-07-08T20:39:18.347344,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),cbb17688d0f9551306a7520b059ace7f7dfba70fc20bf0ec98b1148000d7ad7d
|
||||
test-int/mcp/test_project_management_integration.py,test_project_management_integration.py,source_code,33175,2025-07-08T20:39:18.347658,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),93a43cd699453a7602a5def839740ba5027ab914eba3e7b66a026860ed6bd567
|
||||
test-int/mcp/test_project_state_sync_integration.py,test_project_state_sync_integration.py,source_code,7728,2025-07-08T20:39:18.347907,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),80b0f5b5cd359498dcbd3af7dcb6194afa4582d8efb3faa38d28675ee45e0748
|
||||
test-int/mcp/test_read_content_integration.py,test_read_content_integration.py,source_code,11187,2025-07-08T20:39:18.348233,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),6955dd343b40df088ac79175e52723501d83796335a16e43b7f58e16e757e1b8
|
||||
test-int/mcp/test_read_note_integration.py,test_read_note_integration.py,source_code,1381,2025-07-08T20:39:18.348463,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b93050da68d0e9d47d46879a386567f688399e28390f4eb31ee37353dc27514b
|
||||
test-int/mcp/test_search_integration.py,test_search_integration.py,source_code,14690,2025-07-08T20:39:18.348959,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),e0957ada9dd9b12eae5843a37d241ec9293ca929f18281e31197dae36bff589d
|
||||
test-int/mcp/test_write_note_integration.py,test_write_note_integration.py,source_code,9472,2025-07-08T20:39:18.349398,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),b2b56fee8351168d0c65640c295d969dc4575d9bea87927b2dc07e591cabdff3
|
||||
tests/Non-MarkdownFileSupport.pdf,Non-MarkdownFileSupport.pdf,other,106751,2025-02-23T17:31:23.206428,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),4aa96f9d0ee830ef6a1ec92243f302bbe2fa8971b84945f4eb5e276bf1ffde96
|
||||
tests/Screenshot.png,Screenshot.png,other,190076,2025-02-23T17:31:23.207771,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),dd82612fe013c39810d28ae0f2bbcca6ab2c9925b035d7720bb02493ddf5d0a9
|
||||
tests/__init__.py,__init__.py,source_code,141,2025-06-21T00:01:46.216456,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),2ac5605e69ad8829e96b3689632536539f1cdd3149a29c1903f25af0793a5d65
|
||||
tests/api/conftest.py,conftest.py,source_code,1404,2025-06-21T00:01:46.359728,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),ea46cde47a37c1f43fe079e48f5d0de8007155a0187f4445abc4f2ffde9c1791
|
||||
tests/api/test_async_client.py,test_async_client.py,source_code,1382,2025-07-30T09:39:45.725666,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),a2f96226a75e5cff2326ca48ddccea4915964d83e529f7b99958bc233ccdc31a
|
||||
tests/api/test_continue_conversation_template.py,test_continue_conversation_template.py,source_code,5152,2025-06-21T00:01:46.390901,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),cca08b0291db0ad693c60d8dc203660021feab46d9eb530be7f67e0ebb52c1d3
|
||||
tests/api/test_directory_router.py,test_directory_router.py,source_code,9376,2025-06-21T00:01:46.356659,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),428626589f0010512481ba495368b0bf2b9617b22764e588eaa3562e86da4d37
|
||||
tests/api/test_importer_router.py,test_importer_router.py,source_code,16578,2025-06-21T00:01:46.368070,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),03edf6e7404c7350afe76e54ea2bf0d66f668bcdb49f59702067a67b3694309d
|
||||
tests/api/test_knowledge_router.py,test_knowledge_router.py,source_code,45771,2025-07-01T08:33:22.090490,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),43a5f91ae1fb9038e65c09701336fbdc4dbf40c8319c94727e6312fda9d6d56e
|
||||
tests/api/test_management_router.py,test_management_router.py,source_code,6220,2025-06-21T00:01:46.402764,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),f6a3f41f263f10c08281525571e2e39fc9ae818ebb7e9895e3f3f3115ea05677
|
||||
tests/api/test_memory_router.py,test_memory_router.py,source_code,5555,2025-06-21T00:01:46.399667,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),2b8782015c893081fc67b7f13a7255d4e3dc45b27705a524f94918a339e2d486
|
||||
tests/api/test_project_router.py,test_project_router.py,source_code,13288,2025-07-30T09:39:45.725847,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),af85d0870069d3831a9214372853994e5d12dc910e37298eb9958f72f6a8bb31
|
||||
tests/api/test_project_router_operations.py,test_project_router_operations.py,source_code,1812,2025-06-21T00:01:46.381705,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),afd40879de3d7c508cca0feeae3cb7a8f3afcfbb06f59a6104189b1e3291376a
|
||||
tests/api/test_prompt_router.py,test_prompt_router.py,source_code,5046,2025-06-21T00:01:46.396588,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),9115995b7dd8b44d8a9c06c5e1bbb177618286675a769889e35ac16f26fd9897
|
||||
tests/api/test_resource_router.py,test_resource_router.py,source_code,13960,2025-06-21T00:01:46.384919,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ad3cb2cabaeab82a7e64253ceede7c1791e2bd70c92f60bc5f3be939292ca22d
|
||||
tests/api/test_search_router.py,test_search_router.py,source_code,6409,2025-06-21T00:01:46.375427,bm-claudeai (AI Assistant),3,bm-claudeai (AI Assistant) (claude@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (phernandez@basicmachines.co),ba3e3809795e72db9dd9c8c729048bdbbbdb897c2a5aa8b175112b5d4b56d152
|
||||
tests/api/test_search_template.py,test_search_template.py,source_code,5258,2025-06-21T00:01:46.378721,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),8f0d16a1de947d0466d96afed02e695f7648adac70b9ba99753250068207a686
|
||||
tests/api/test_template_loader.py,test_template_loader.py,source_code,8035,2025-06-21T00:01:46.406694,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),4fd7c6c4862cc0d877d29244ecdc0b52a0693fc4574a9667663bee36bfd36b62
|
||||
tests/api/test_template_loader_helpers.py,test_template_loader_helpers.py,source_code,7445,2025-06-21T00:01:46.372114,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),8dd35f11c9b3f2d96ac36263edf4430308728c04c9da543b1cb845b96ab20aa7
|
||||
tests/cli/conftest.py,conftest.py,source_code,1185,2025-07-01T08:33:22.090848,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c3c73da7ad73224931e1fba4ebd70792221be607bfb1488bb5c9420bba03bdb8
|
||||
tests/cli/test_cli_tools.py,test_cli_tools.py,source_code,13528,2025-06-21T00:01:46.318964,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),befb8cab50132e26a1b13b82ce8e93a3258282cf157a4230214bfcefeae65102
|
||||
tests/cli/test_import_chatgpt.py,test_import_chatgpt.py,source_code,6719,2025-07-08T20:39:18.350234,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),df447335545248e2fb2417af33f8cc2f0143bd413a1783e41f89605de305b7c8
|
||||
tests/cli/test_import_claude_conversations.py,test_import_claude_conversations.py,legal,5028,2025-07-08T20:39:18.350607,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ac4b3b9dfc331fa7fa32a2941c027ff8b6585a94a5ccfaaf0a4affe5cf19c00c
|
||||
tests/cli/test_import_claude_projects.py,test_import_claude_projects.py,legal,4585,2025-07-08T20:39:18.350885,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),7eaab3d491d50ca59c8ba015ecd5740a5feae94d970ee17559c6df9d8558c2cc
|
||||
tests/cli/test_import_memory_json.py,test_import_memory_json.py,source_code,4891,2025-07-30T09:39:45.726290,jope-bm,3,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (60959+phernandez@users.noreply.github.com),d6734ce059dacf97bc764d848b2e2a6ba52fa96871bc331ec9d2ddc60b766bb6
|
||||
tests/cli/test_project_commands.py,test_project_commands.py,source_code,6578,2025-07-30T09:39:45.726703,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),51e2ac7ff44b0d1a1db6da406c5c2e6aebceb1b63a1e8d5ab51bbaa834507157
|
||||
tests/cli/test_project_info.py,test_project_info.py,source_code,3874,2025-07-01T08:33:22.091600,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),0b08950e95a5f276f14c56f6fe99af4d41b8f04548e9b7746f807c5ac4155578
|
||||
tests/cli/test_status.py,test_status.py,source_code,4160,2025-07-01T08:33:22.091978,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),a1ff89fdac4bf57ba03be92ac79b8ca3210af0b1132d5a2bd0fc0c253fcbf988
|
||||
tests/cli/test_sync.py,test_sync.py,source_code,3864,2025-07-08T20:43:47.816788,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),77e0f6b42b584a5acfda3c2ff194ff5e71ca57a40cb4484d563f5482bbbb4c0f
|
||||
tests/cli/test_version.py,test_version.py,source_code,289,2025-06-21T00:01:46.305368,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),db95665178a32d391bd83011d073db269cefba9da8a4dc4d3e392d47bc0e0a8f
|
||||
tests/conftest.py,conftest.py,source_code,15286,2025-07-08T20:39:18.351368,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),c6f38f103ce58144ffe4562f0c47dad849974e96327f02805e854447e19bc411
|
||||
tests/importers/test_importer_base.py,test_importer_base.py,source_code,4329,2025-07-08T20:39:00.549594,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),da872a073f2b90fda3c98c9f44cf45884f4f682d91ec1b1f931d42c34eee1939
|
||||
tests/importers/test_importer_utils.py,test_importer_utils.py,source_code,1827,2025-06-21T00:01:46.184343,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),cbee630c855ceb1de764ac0cdb1d5d781c16cd3a934d67166adc235c98f363d7
|
||||
tests/markdown/__init__.py,__init__.py,source_code,0,2025-06-21T00:01:46.196570,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
tests/markdown/test_entity_parser.py,test_entity_parser.py,source_code,8669,2025-06-21T00:01:46.206579,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),496fdab6fe164634f8223f827585a4c0dedd30ccea7b0b69dc79004ad0996de2
|
||||
tests/markdown/test_markdown_plugins.py,test_markdown_plugins.py,source_code,5139,2025-06-21T00:01:46.200759,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),9624d73462aadcd47fb0edd0d87880959a8982eecfa39529f21e3b292c34cb83
|
||||
tests/markdown/test_markdown_processor.py,test_markdown_processor.py,source_code,5728,2025-06-21T00:01:46.194175,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),35042aa57dd2aeed848c624aa7f69decfb18e83c3b217eca1fdacfc4a10831db
|
||||
tests/markdown/test_observation_edge_cases.py,test_observation_edge_cases.py,source_code,4295,2025-06-21T00:01:46.209598,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),6f1ab7684bfa8b02af0e0c42b3d7aed91fcd0b1c6288cbc9b6d6943c166979fa
|
||||
tests/markdown/test_parser_edge_cases.py,test_parser_edge_cases.py,source_code,5142,2025-06-21T00:01:46.213291,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),d511d7d1187e3c1d3a8d3e0ab9efc9e68e84115c46811e7a9dc21eca9a87d520
|
||||
tests/markdown/test_relation_edge_cases.py,test_relation_edge_cases.py,source_code,3952,2025-06-21T00:01:46.203846,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),f8e2016308f84139342cc37ec6e389ffe52d7eb46f998ee327c7f0d5bba3ad9b
|
||||
tests/markdown/test_task_detection.py,test_task_detection.py,source_code,401,2025-06-21T00:01:46.191444,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),37fe91a689742033a0eb76723557e31c2a85e591133383b3887572cb714db2a5
|
||||
tests/mcp/conftest.py,conftest.py,source_code,1761,2025-07-08T20:39:18.351554,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),350b2ca42c52ef76e002deddb65fc0eccb9f8989bd26078ea1c100e3b93bae3f
|
||||
tests/mcp/test_prompts.py,test_prompts.py,source_code,5989,2025-07-01T08:33:22.093691,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),e3b2ee5b4bc120b5043225ebdbf73eadd71be9c3d9d8e1fdd66a24a8ff11a780
|
||||
tests/mcp/test_resource_project_info.py,test_resource_project_info.py,source_code,4885,2025-07-01T08:33:22.094085,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),6a043736789b239c8d4ae00d1a5332a0fc3bed4038fe3388970ba97dc7431a93
|
||||
tests/mcp/test_resources.py,test_resources.py,source_code,549,2025-07-01T08:33:22.094481,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),77a6396b9b8a9d3b3d202e438243ca6178d87fd7680c9fc063ee013f2042aa4c
|
||||
tests/mcp/test_tool_build_context.py,test_tool_build_context.py,source_code,3714,2025-07-01T08:33:22.094661,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),5f0a39fdb652605335ed54aa91fe994262441bb7ba46f381c46ebca882fc23f2
|
||||
tests/mcp/test_tool_canvas.py,test_tool_canvas.py,source_code,7997,2025-07-01T08:33:22.094982,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),c517bb1f9edc77b76fbeb483dfcb2dd57de9f0b80a10738708bc90cbe388dc9e
|
||||
tests/mcp/test_tool_delete_note.py,test_tool_delete_note.py,source_code,4208,2025-07-01T08:33:22.095578,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),4cbf3ba4313438651b0608d5b25faf1400f7bd6a78af630351aecd4457800895
|
||||
tests/mcp/test_tool_edit_note.py,test_tool_edit_note.py,source_code,13503,2025-07-03T18:43:10.084890,Drew Cain,2,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),dd441969aa54b3739f2d9202d3feaf46aee807ec51e01a13ef41eda52743944f
|
||||
tests/mcp/test_tool_list_directory.py,test_tool_list_directory.py,source_code,7730,2025-07-01T08:33:22.096737,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),cfbb5c06bcfe74ea60f31051eb9b03ac80b86232b053c188bf3b172132f7c0f5
|
||||
tests/mcp/test_tool_move_note.py,test_tool_move_note.py,source_code,25851,2025-07-30T09:39:45.727347,jope-bm,2,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co),c9f95158503fa42e37532aaa35027538fa49cea718d059d570cf547d31f01043
|
||||
tests/mcp/test_tool_read_content.py,test_tool_read_content.py,source_code,19442,2025-07-30T09:39:45.727526,jope-bm,1,jope-bm (joe@basicmemory.com),1817e32f997b06dbdabf02864d0c70584c1cd80bf1fae8dfbef649de79434bc5
|
||||
tests/mcp/test_tool_read_note.py,test_tool_read_note.py,source_code,22991,2025-07-30T09:39:45.727746,jope-bm,4,jope-bm (joe@basicmemory.com); Paul Hernandez (paul@basicmachines.co); Amadeusz Wieczorek (git@amadeusw.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),4e13ea9ff977525aa62b6a8c22505ffe989329dc3e7d07b71e044cf43ef3ddfd
|
||||
tests/mcp/test_tool_recent_activity.py,test_tool_recent_activity.py,source_code,3983,2025-07-01T08:33:22.098561,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),45414152cd45334b84df56f653146b7bd2939907548e7245a37207fdccfc4c87
|
||||
tests/mcp/test_tool_resource.py,test_tool_resource.py,source_code,6782,2025-07-01T08:33:22.098948,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),3916ae1c4d458eee53d2c570b9bbb708ecd617de5a145c3a7552389954bc9195
|
||||
tests/mcp/test_tool_search.py,test_tool_search.py,source_code,10959,2025-07-03T18:43:10.085177,Drew Cain,3,Drew Cain (groksrc@gmail.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),07839cf8e89d74ba05ec1dc55fa842444c1f0cc3d1587823b0ac3458ad89e87c
|
||||
tests/mcp/test_tool_sync_status.py,test_tool_sync_status.py,source_code,6331,2025-07-01T08:33:22.100021,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),3f3566df5b9f3c66bb9b3bd77d4c05825480d0e85015305cb1b7ab9429c7a7ec
|
||||
tests/mcp/test_tool_utils.py,test_tool_utils.py,source_code,9228,2025-07-01T08:33:22.100561,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),b50789f7a78b0169f52dc8bd16e262510ed4a0ac130fce698d4be84af1fe2d31
|
||||
tests/mcp/test_tool_view_note.py,test_tool_view_note.py,source_code,10004,2025-07-01T08:33:22.101164,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),385535ce791c3ab55db827cbcf27ea25ae85faf3a027f52df4eea052cede7f36
|
||||
tests/mcp/test_tool_write_note.py,test_tool_write_note.py,source_code,35136,2025-07-30T09:39:45.728137,jope-bm,3,jope-bm (joe@basicmemory.com); Drew Cain (groksrc@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),45172de20cfc4f4436287573b4f5a94b8b616a360647af67ba1c6c343ccd41b8
|
||||
tests/repository/test_entity_repository.py,test_entity_repository.py,source_code,15498,2025-06-21T00:01:46.176265,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),8f5d1ff6cff7b640993ff73f463b1e7801a4dd69640fc55fa23dc58b865e9943
|
||||
tests/repository/test_entity_repository_upsert.py,test_entity_repository_upsert.py,source_code,16983,2025-07-03T18:43:10.085482,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com),ee0ecd6f4ff8b31838bd6de6fb1018af437e02813d8315c06f4e20a6b78a0572
|
||||
tests/repository/test_observation_repository.py,test_observation_repository.py,source_code,11786,2025-06-21T00:01:46.158862,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),3006d392ad94dcc9b3ae031d63a1af6935624534389dd025de70d4c9e112aa0b
|
||||
tests/repository/test_project_info_repository.py,test_project_info_repository.py,source_code,1186,2025-06-21T00:01:46.161876,bm-claudeai (AI Assistant),1,bm-claudeai (AI Assistant) (claude@basicmachines.co),dbce7ae7350403cbc37343c2e3e68e4d2255e247006aaa6c495a7ce8bfddea7d
|
||||
tests/repository/test_project_repository.py,test_project_repository.py,source_code,10511,2025-07-30T09:39:45.728536,jope-bm,2,jope-bm (joe@basicmemory.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),279a3324635b1b515c18003bd1875cf230fef1192cadef9ca426846783717e75
|
||||
tests/repository/test_relation_repository.py,test_relation_repository.py,source_code,11980,2025-06-21T00:01:46.167567,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (phernandez@basicmachines.co),fcdbdf40f7b56145b98eedde5ead64ef6187e859adde48cc09f5a97754757428
|
||||
tests/repository/test_repository.py,test_repository.py,source_code,5996,2025-06-21T00:01:46.152691,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),3a6d4ca0b7a438a30ae504f5eac22874d4e1a937a4246e90efad46c90f0e1c95
|
||||
tests/repository/test_search_repository.py,test_search_repository.py,source_code,25471,2025-07-03T18:43:10.086041,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),805b5cc55a53ae9d6e86c0de735c7f38ac9fd92c23194b0150fa3e7f5507a146
|
||||
tests/schemas/test_memory_url.py,test_memory_url.py,source_code,2000,2025-06-21T00:01:46.346515,Paul Hernandez,1,Paul Hernandez (paulmh@gmail.com),946fa6c48534f41c8b9cc27bbad164d3e3673a79afa84fbb439fae4cd26c504b
|
||||
tests/schemas/test_memory_url_validation.py,test_memory_url_validation.py,source_code,9338,2025-07-01T08:33:22.103247,Paul Hernandez,1,Paul Hernandez (paul@basicmachines.co),7054c3a3cdb8e05d2ffd254ed298908ff3d48a0a3927d960975eae1ab9beeb38
|
||||
tests/schemas/test_schemas.py,test_schemas.py,source_code,16880,2025-07-03T18:43:10.086337,Drew Cain,3,Drew Cain (groksrc@gmail.com); Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),2c746a00c340f063c160ace67569955a300a294040949695c79fd4ae124fb611
|
||||
tests/schemas/test_search.py,test_search.py,source_code,3264,2025-06-21T00:01:46.343685,github-actions[bot] (AI Assistant),2,github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com); Paul Hernandez (60959+phernandez@users.noreply.github.com),7437775d1b4a61a438cbabe3dbb1becd2adc4dbcc0c24595830c0843fa7a3166
|
||||
tests/services/test_context_service.py,test_context_service.py,source_code,8833,2025-06-21T00:01:46.446200,bm-claudeai (AI Assistant),2,bm-claudeai (AI Assistant) (claude@basicmachines.co); Paul Hernandez (paulmh@gmail.com),ab49d21a05d5364e7e8a2f00b07aabe1109b7417698e0acd92792e2cb5f22732
|
||||
tests/services/test_directory_service.py,test_directory_service.py,source_code,6854,2025-06-21T00:01:46.457258,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),a0c765fb6bd1a75b6ba4592b1df344c48273b6ab03ba6bd15d2b5e840eb2d521
|
||||
tests/services/test_entity_service.py,test_entity_service.py,source_code,59278,2025-07-03T18:43:10.086674,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),d7d495756faa10b415d705cf26b69d2efbc5f190a834b6767386ff296629ea71
|
||||
tests/services/test_file_service.py,test_file_service.py,source_code,5243,2025-06-21T00:01:46.460141,Paul Hernandez,1,Paul Hernandez (phernandez@basicmachines.co),83e6e9a7584b7baeccbc84865d8c1d3e2a0223e7d2b9c066d3799c289196ab7d
|
||||
tests/services/test_initialization.py,test_initialization.py,source_code,6939,2025-07-08T20:39:18.351946,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),ebb8d11ff37dcb36c4844c68f873c8e0e8aa6ab1b82abb22ee96c4233e6ab287
|
||||
tests/services/test_link_resolver.py,test_link_resolver.py,source_code,13618,2025-07-01T08:33:22.105766,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),c9fc56d0c3572770e95ca6175c213ed8b44c2a6ddddc298b5662cc2d8f41014e
|
||||
tests/services/test_project_service.py,test_project_service.py,source_code,28152,2025-07-30T09:39:45.728920,jope-bm,3,jope-bm (joe@basicmemory.com); Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),430868ed796518937a3b0bed4d8c33be53264a6100d61d54b0745bb2b58bf29a
|
||||
tests/services/test_project_service_operations.py,test_project_service_operations.py,source_code,4679,2025-06-21T00:01:46.442141,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),d4103b6c5fff7e74ba84b86033537b8522aa60fc75aec4974d52e1dfec68b1b4
|
||||
tests/services/test_search_service.py,test_search_service.py,source_code,25299,2025-06-21T00:01:46.439133,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),b3e58258db2b727ab8a1792e52fcb038d3a0779bcd1b70f2d295a9d06e5e2513
|
||||
tests/services/test_sync_status_service.py,test_sync_status_service.py,source_code,9926,2025-07-03T18:43:10.087266,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),00245ca356e33fc7afcd2d42cb5ec082038c95383d738067821cf1e8d23c9a4b
|
||||
tests/sync/test_sync_service.py,test_sync_service.py,source_code,39659,2025-07-01T08:33:22.107088,Paul Hernandez,2,Paul Hernandez (paul@basicmachines.co); bm-claudeai (AI Assistant) (claude@basicmachines.co),0609aee9906549f27fc4289b5ac2d46a50e53e12a0615a2701f17fbe9a74e169
|
||||
tests/sync/test_sync_wikilink_issue.py,test_sync_wikilink_issue.py,source_code,1959,2025-06-21T00:01:46.420370,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),db4b98dd7008a1a26b0fd02b9a2d03bca950b281efc19d98a91b017c500b23a5
|
||||
tests/sync/test_tmp_files.py,test_tmp_files.py,source_code,5930,2025-06-21T00:01:46.424277,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),cead95a07b29f6c1ab933abe67f9648122f5f33d33753c8e4ab4934af96cbc25
|
||||
tests/sync/test_watch_service.py,test_watch_service.py,source_code,14076,2025-06-26T09:28:51.860679,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),e324a4127f8bfe59798f3a08125488c701e88239b7482d87a0394b91c55b893e
|
||||
tests/sync/test_watch_service_edge_cases.py,test_watch_service_edge_cases.py,source_code,2169,2025-06-21T00:01:46.409830,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),b87c5015330e9b551630aab7ef95ac1e7663f0ce46e912d8155b319e139559aa
|
||||
tests/test_config.py,test_config.py,source_code,3680,2025-07-03T18:43:10.087558,Paul Hernandez,3,Paul Hernandez (paul@basicmachines.co); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),6a246431cff739274efda895dfa28f38710289a73e645312f5fa05fce0885ec8
|
||||
tests/test_db_migration_deduplication.py,test_db_migration_deduplication.py,source_code,6166,2025-07-08T20:39:18.352690,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com),ab768e061531ca2edcd598c101b5d884e549f90c7a4b305a23713a199d263fbb
|
||||
tests/utils/test_file_utils.py,test_file_utils.py,source_code,6199,2025-06-21T00:01:46.219673,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),4615bb20b92b8a79d9e086301f852b34c3360f17ecc209920d68528268ddeed4
|
||||
tests/utils/test_parse_tags.py,test_parse_tags.py,source_code,1691,2025-06-21T00:01:46.223400,Paul Hernandez,2,Paul Hernandez (60959+phernandez@users.noreply.github.com); github-actions[bot] (AI Assistant) (41898282+github-actions[bot]@users.noreply.github.com),b83afdf18dac8b3411bd0fab1301683217d3d1abceaf2d22c09e79c709efd8eb
|
||||
tests/utils/test_permalink_formatting.py,test_permalink_formatting.py,source_code,4193,2025-06-21T00:01:46.225738,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),d998a4ca846c78679728a918b6f71df67c30b6961ede3cffa609f2cf6ad160eb
|
||||
tests/utils/test_utf8_handling.py,test_utf8_handling.py,source_code,5791,2025-06-21T00:01:46.228483,Paul Hernandez,1,Paul Hernandez (60959+phernandez@users.noreply.github.com),50ec792e572af51cd2d996d4ddacbcfd3bbd79aa928818a90c42117451ea829f
|
||||
tests/utils/test_validate_project_path.py,test_validate_project_path.py,source_code,15346,2025-07-30T09:39:45.729224,jope-bm,1,jope-bm (joe@basicmemory.com),27a4bf2a9d277e6cd773619e5241b20175d211ec60c284361bec404fd99143f3
|
||||
uv.lock,uv.lock,other,232710,2025-07-30T09:39:36.619155,Paul Hernandez,3,Paul Hernandez (60959+phernandez@users.noreply.github.com); Drew Cain (groksrc@users.noreply.github.com); bm-claudeai (AI Assistant) (claude@basicmachines.co),caa1e3f6e66464b884a78cab77df2fe303fa3c0dc573be0196474b6d3c150581
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,294 @@
|
||||
# Basic Memory - Legal File Inventory
|
||||
|
||||
**Generated:** 2025-07-29T16:10:54.745774
|
||||
|
||||
**Repository:** /Users/phernandez/dev/basicmachines/basic-memory
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Total Files:** 255
|
||||
- **Total Contributors:** 12
|
||||
|
||||
### Files by Category
|
||||
|
||||
- **Build/Deployment:** 2 files
|
||||
- **Configuration:** 6 files
|
||||
- **Database/Migration:** 1 files
|
||||
- **Documentation:** 6 files
|
||||
- **Legal/License:** 3 files
|
||||
- **Other:** 13 files
|
||||
- **Source Code:** 222 files
|
||||
- **Templates/Resources:** 2 files
|
||||
|
||||
### Top Contributors by Files Modified
|
||||
|
||||
- **phernandez:** 190 files
|
||||
- **Paul Hernandez:** 181 files
|
||||
- **bm-claudeai:** 135 files
|
||||
- **Drew Cain:** 24 files
|
||||
- **github-actions[bot]:** 20 files
|
||||
- **Amadeusz Wieczorek:** 2 files
|
||||
- **semantic-release:** 1 files
|
||||
- **Jason Noble:** 1 files
|
||||
- **Marc Baiza:** 1 files
|
||||
- **Matias Forbord:** 1 files
|
||||
|
||||
## Detailed File Inventory
|
||||
|
||||
| File Path | Category | Size (bytes) | Primary Author | Contributors |
|
||||
|-----------|----------|--------------|----------------|-------------|
|
||||
| .claude/commands/release/beta.md | Other | 3103 | phernandez | phernandez |
|
||||
| .claude/commands/release/changelog.md | Documentation | 4648 | phernandez | phernandez |
|
||||
| .claude/commands/release/release-check.md | Other | 3344 | phernandez | phernandez |
|
||||
| .claude/commands/release/release.md | Other | 2925 | phernandez | phernandez |
|
||||
| .claude/commands/test-live.md | Other | 18720 | phernandez | phernandez |
|
||||
| .claude/settings.local.json | Configuration | 3122 | Unknown | |
|
||||
| .dockerignore | Other | 623 | Paul Hernandez | Paul Hernandez |
|
||||
| .python-version | Other | 5 | phernandez | phernandez |
|
||||
| CHANGELOG.md | Documentation | 53647 | semantic-release | semantic-release, phernandez |
|
||||
| CITATION.cff | Legal/License | 302 | phernandez | phernandez |
|
||||
| CLA.md | Legal/License | 1342 | phernandez | phernandez |
|
||||
| CLAUDE.md | Other | 12310 | Paul Hernandez | Paul Hernandez, phernandez, Ikko Eltociear Ashimin... |
|
||||
| CODE_OF_CONDUCT.md | Other | 509 | phernandez | phernandez |
|
||||
| CONTRIBUTING.md | Documentation | 7241 | phernandez | phernandez, Paul Hernandez |
|
||||
| Dockerfile | Build/Deployment | 856 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| LICENSE | Legal/License | 34523 | Paul Hernandez | Paul Hernandez |
|
||||
| README.md | Documentation | 16261 | phernandez | phernandez, bm-claudeai, Paul Hernandez, Jason Nob... |
|
||||
| SECURITY.md | Other | 303 | Paul Hernandez | Paul Hernandez |
|
||||
| docker-compose.yml | Configuration | 2534 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| docs/AI Assistant Guide.md | Documentation | 16423 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| docs/Docker.md | Documentation | 9088 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| justfile | Build/Deployment | 5371 | phernandez | phernandez, Drew Cain |
|
||||
| legal_file_inventory.py | Source Code | 17407 | Unknown | |
|
||||
| llms-install.md | Other | 2307 | phernandez | phernandez |
|
||||
| memory.json | Configuration | 90075 | phernandez | phernandez |
|
||||
| pyproject.toml | Configuration | 3279 | phernandez | phernandez, Paul Hernandez, bm-claudeai, Drew Cain... |
|
||||
| smithery.yaml | Configuration | 433 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/__init__.py | Source Code | 256 | phernandez | phernandez |
|
||||
| src/basic_memory/alembic/alembic.ini | Configuration | 3733 | phernandez | phernandez |
|
||||
| src/basic_memory/alembic/migrations.py | Source Code | 718 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/alembic/script.py.mako | Database/Migration | 635 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py | Source Code | 4363 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py | Source Code | 1840 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py | Source Code | 4379 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py | Source Code | 4210 | phernandez | phernandez |
|
||||
| src/basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py | Source Code | 1428 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py | Source Code | 3679 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/api/__init__.py | Source Code | 72 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/api/app.py | Source Code | 2897 | bm-claudeai | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/api/routers/__init__.py | Source Code | 398 | phernandez | phernandez, bm-claudeai |
|
||||
| src/basic_memory/api/routers/directory_router.py | Source Code | 2054 | Paul Hernandez | bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/api/routers/importer_router.py | Source Code | 4513 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/api/routers/knowledge_router.py | Source Code | 9738 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/api/routers/management_router.py | Source Code | 2730 | bm-claudeai | bm-claudeai, phernandez |
|
||||
| src/basic_memory/api/routers/memory_router.py | Source Code | 3003 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/api/routers/project_router.py | Source Code | 8239 | bm-claudeai | bm-claudeai, phernandez, Paul Hernandez |
|
||||
| src/basic_memory/api/routers/prompt_router.py | Source Code | 9948 | bm-claudeai | bm-claudeai, phernandez |
|
||||
| src/basic_memory/api/routers/resource_router.py | Source Code | 8058 | Paul Hernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/api/routers/search_router.py | Source Code | 1226 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/api/routers/utils.py | Source Code | 5159 | bm-claudeai | bm-claudeai, Drew Cain |
|
||||
| src/basic_memory/api/template_loader.py | Source Code | 8607 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/cli/__init__.py | Source Code | 33 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/cli/app.py | Source Code | 2268 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/cli/commands/__init__.py | Source Code | 393 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/cli/commands/db.py | Source Code | 1480 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/cli/commands/import_chatgpt.py | Source Code | 2856 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| src/basic_memory/cli/commands/import_claude_conversations.py | Source Code | 2946 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| src/basic_memory/cli/commands/import_claude_projects.py | Source Code | 2891 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| src/basic_memory/cli/commands/import_memory_json.py | Source Code | 2947 | Paul Hernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/cli/commands/mcp.py | Source Code | 2593 | phernandez | bm-claudeai, phernandez, Paul Hernandez |
|
||||
| src/basic_memory/cli/commands/project.py | Source Code | 12252 | phernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/cli/commands/status.py | Source Code | 5809 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/cli/commands/sync.py | Source Code | 8213 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/cli/commands/tool.py | Source Code | 9540 | phernandez | Paul Hernandez, phernandez, github-actions[bot] |
|
||||
| src/basic_memory/cli/main.py | Source Code | 465 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/config.py | Source Code | 12620 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai, Drew Cain... |
|
||||
| src/basic_memory/db.py | Source Code | 7428 | phernandez | phernandez, Paul Hernandez, Drew Cain, bm-claudeai... |
|
||||
| src/basic_memory/deps.py | Source Code | 12144 | bm-claudeai | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/file_utils.py | Source Code | 6700 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/importers/__init__.py | Source Code | 795 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/importers/base.py | Source Code | 2531 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/importers/chatgpt_importer.py | Source Code | 7372 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/importers/claude_conversations_importer.py | Source Code | 5725 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/importers/claude_projects_importer.py | Source Code | 5389 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/importers/memory_json_importer.py | Source Code | 3986 | bm-claudeai | bm-claudeai, phernandez |
|
||||
| src/basic_memory/importers/utils.py | Source Code | 1792 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/markdown/__init__.py | Source Code | 501 | phernandez | phernandez |
|
||||
| src/basic_memory/markdown/entity_parser.py | Source Code | 4516 | phernandez | phernandez, Paul Hernandez, github-actions[bot] |
|
||||
| src/basic_memory/markdown/markdown_processor.py | Source Code | 4968 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/markdown/plugins.py | Source Code | 6639 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/markdown/schemas.py | Source Code | 1896 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/markdown/utils.py | Source Code | 3410 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/mcp/__init__.py | Source Code | 35 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/mcp/async_client.py | Source Code | 925 | Paul Hernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/mcp/project_session.py | Source Code | 4203 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/mcp/prompts/__init__.py | Source Code | 615 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/mcp/prompts/ai_assistant_guide.py | Source Code | 821 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/mcp/prompts/continue_conversation.py | Source Code | 1998 | Paul Hernandez | Paul Hernandez, github-actions[bot], bm-claudeai |
|
||||
| src/basic_memory/mcp/prompts/recent_activity.py | Source Code | 3311 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/prompts/search.py | Source Code | 1692 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/prompts/utils.py | Source Code | 5431 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/resources/ai_assistant_guide.md | Other | 14777 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/mcp/resources/project_info.py | Source Code | 1906 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/server.py | Source Code | 1248 | Paul Hernandez | bm-claudeai, phernandez, Paul Hernandez |
|
||||
| src/basic_memory/mcp/tools/__init__.py | Source Code | 1619 | Paul Hernandez | phernandez, Paul Hernandez, Drew Cain |
|
||||
| src/basic_memory/mcp/tools/auth.py | Source Code | 1231 | phernandez | phernandez |
|
||||
| src/basic_memory/mcp/tools/canvas.py | Source Code | 3738 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/tools/delete_note.py | Source Code | 7346 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/tools/edit_note.py | Source Code | 13570 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/mcp/tools/list_directory.py | Source Code | 5236 | Paul Hernandez | Paul Hernandez |
|
||||
| src/basic_memory/mcp/tools/move_note.py | Source Code | 16708 | phernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/mcp/tools/project_management.py | Source Code | 12944 | Paul Hernandez | Paul Hernandez, phernandez, Drew Cain |
|
||||
| src/basic_memory/mcp/tools/read_content.py | Source Code | 8596 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/tools/read_note.py | Source Code | 7614 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez, Amadeusz ... |
|
||||
| src/basic_memory/mcp/tools/recent_activity.py | Source Code | 4833 | phernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/tools/search.py | Source Code | 15883 | phernandez | phernandez, github-actions[bot], Paul Hernandez, D... |
|
||||
| src/basic_memory/mcp/tools/sync_status.py | Source Code | 10517 | phernandez | phernandez |
|
||||
| src/basic_memory/mcp/tools/utils.py | Source Code | 21426 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/mcp/tools/view_note.py | Source Code | 2499 | phernandez | phernandez |
|
||||
| src/basic_memory/mcp/tools/write_note.py | Source Code | 6178 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| src/basic_memory/models/__init__.py | Source Code | 333 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/models/base.py | Source Code | 225 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/models/knowledge.py | Source Code | 7087 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/models/project.py | Source Code | 2773 | bm-claudeai | bm-claudeai, phernandez |
|
||||
| src/basic_memory/models/search.py | Source Code | 1300 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/repository/__init__.py | Source Code | 327 | Paul Hernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/repository/entity_repository.py | Source Code | 10405 | Drew Cain | phernandez, Paul Hernandez, Drew Cain, bm-claudeai... |
|
||||
| src/basic_memory/repository/observation_repository.py | Source Code | 2943 | phernandez | phernandez, bm-claudeai |
|
||||
| src/basic_memory/repository/project_info_repository.py | Source Code | 338 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/repository/project_repository.py | Source Code | 3159 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/repository/relation_repository.py | Source Code | 3179 | phernandez | phernandez, bm-claudeai |
|
||||
| src/basic_memory/repository/repository.py | Source Code | 15224 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/repository/search_repository.py | Source Code | 21936 | phernandez | phernandez, Paul Hernandez, github-actions[bot], b... |
|
||||
| src/basic_memory/schemas/__init__.py | Source Code | 1674 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/schemas/base.py | Source Code | 6738 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/schemas/delete.py | Source Code | 1180 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/schemas/directory.py | Source Code | 881 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/schemas/importer.py | Source Code | 663 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/schemas/memory.py | Source Code | 5833 | phernandez | phernandez, Paul Hernandez, Drew Cain, bm-claudeai... |
|
||||
| src/basic_memory/schemas/project_info.py | Source Code | 7047 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| src/basic_memory/schemas/prompt.py | Source Code | 3648 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/schemas/request.py | Source Code | 3760 | Paul Hernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/schemas/response.py | Source Code | 6450 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/schemas/search.py | Source Code | 3657 | phernandez | phernandez, Paul Hernandez, github-actions[bot], b... |
|
||||
| src/basic_memory/services/__init__.py | Source Code | 259 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/services/context_service.py | Source Code | 14462 | bm-claudeai | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/services/directory_service.py | Source Code | 6017 | bm-claudeai | bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/services/entity_service.py | Source Code | 30440 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai, Drew Cain... |
|
||||
| src/basic_memory/services/exceptions.py | Source Code | 390 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/services/file_service.py | Source Code | 10059 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/services/initialization.py | Source Code | 6715 | bm-claudeai | Paul Hernandez, bm-claudeai, Drew Cain, phernandez... |
|
||||
| src/basic_memory/services/link_resolver.py | Source Code | 4594 | phernandez | phernandez, Paul Hernandez, bm-claudeai, github-ac... |
|
||||
| src/basic_memory/services/project_service.py | Source Code | 28386 | bm-claudeai | bm-claudeai, Paul Hernandez, phernandez |
|
||||
| src/basic_memory/services/search_service.py | Source Code | 12782 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| src/basic_memory/services/service.py | Source Code | 336 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/services/sync_status_service.py | Source Code | 7504 | phernandez | phernandez, Paul Hernandez |
|
||||
| src/basic_memory/sync/__init__.py | Source Code | 156 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| src/basic_memory/sync/background_sync.py | Source Code | 726 | bm-claudeai | bm-claudeai, phernandez |
|
||||
| src/basic_memory/sync/sync_service.py | Source Code | 23387 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai, github-ac... |
|
||||
| src/basic_memory/sync/watch_service.py | Source Code | 15316 | Paul Hernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| src/basic_memory/templates/prompts/continue_conversation.hbs | Templates/Resources | 3076 | bm-claudeai | bm-claudeai, Drew Cain |
|
||||
| src/basic_memory/templates/prompts/search.hbs | Templates/Resources | 3098 | bm-claudeai | bm-claudeai |
|
||||
| src/basic_memory/utils.py | Source Code | 7657 | phernandez | phernandez, Paul Hernandez, andyxinweiminicloud, D... |
|
||||
| test-int/conftest.py | Source Code | 8203 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_delete_note_integration.py | Source Code | 13175 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_edit_note_integration.py | Source Code | 19822 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_list_directory_integration.py | Source Code | 14744 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_move_note_integration.py | Source Code | 19825 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_project_management_integration.py | Source Code | 33175 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_project_state_sync_integration.py | Source Code | 7728 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_read_content_integration.py | Source Code | 11187 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_read_note_integration.py | Source Code | 1381 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_search_integration.py | Source Code | 14690 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| test-int/mcp/test_write_note_integration.py | Source Code | 9472 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/Non-MarkdownFileSupport.pdf | Other | 106751 | Unknown | |
|
||||
| tests/Screenshot.png | Other | 190076 | Unknown | |
|
||||
| tests/__init__.py | Source Code | 141 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/api/conftest.py | Source Code | 1404 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| tests/api/test_async_client.py | Source Code | 1382 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/api/test_continue_conversation_template.py | Source Code | 5152 | bm-claudeai | bm-claudeai |
|
||||
| tests/api/test_directory_router.py | Source Code | 9376 | Paul Hernandez | bm-claudeai, Paul Hernandez |
|
||||
| tests/api/test_importer_router.py | Source Code | 16578 | bm-claudeai | bm-claudeai, Paul Hernandez |
|
||||
| tests/api/test_knowledge_router.py | Source Code | 45771 | Paul Hernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| tests/api/test_management_router.py | Source Code | 6220 | bm-claudeai | bm-claudeai |
|
||||
| tests/api/test_memory_router.py | Source Code | 5555 | phernandez | phernandez, bm-claudeai |
|
||||
| tests/api/test_project_router.py | Source Code | 5201 | Paul Hernandez | bm-claudeai, Paul Hernandez, phernandez |
|
||||
| tests/api/test_project_router_operations.py | Source Code | 1812 | bm-claudeai | bm-claudeai, Paul Hernandez |
|
||||
| tests/api/test_prompt_router.py | Source Code | 5046 | bm-claudeai | bm-claudeai |
|
||||
| tests/api/test_resource_router.py | Source Code | 13960 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| tests/api/test_search_router.py | Source Code | 6409 | phernandez | phernandez, bm-claudeai, Paul Hernandez, github-ac... |
|
||||
| tests/api/test_search_template.py | Source Code | 5258 | bm-claudeai | bm-claudeai |
|
||||
| tests/api/test_template_loader.py | Source Code | 8035 | bm-claudeai | bm-claudeai |
|
||||
| tests/api/test_template_loader_helpers.py | Source Code | 7445 | bm-claudeai | bm-claudeai |
|
||||
| tests/cli/conftest.py | Source Code | 1185 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/cli/test_cli_tools.py | Source Code | 13528 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/cli/test_import_chatgpt.py | Source Code | 6719 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/cli/test_import_claude_conversations.py | Source Code | 5028 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/cli/test_import_claude_projects.py | Source Code | 4585 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/cli/test_import_memory_json.py | Source Code | 3532 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/cli/test_project_commands.py | Source Code | 5143 | bm-claudeai | bm-claudeai, Paul Hernandez, phernandez |
|
||||
| tests/cli/test_project_info.py | Source Code | 3874 | phernandez | Paul Hernandez, phernandez |
|
||||
| tests/cli/test_status.py | Source Code | 4160 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/cli/test_sync.py | Source Code | 3864 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/cli/test_version.py | Source Code | 289 | Paul Hernandez | Paul Hernandez |
|
||||
| tests/conftest.py | Source Code | 15286 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| tests/importers/test_importer_base.py | Source Code | 4329 | bm-claudeai | bm-claudeai |
|
||||
| tests/importers/test_importer_utils.py | Source Code | 1827 | bm-claudeai | bm-claudeai |
|
||||
| tests/markdown/__init__.py | Source Code | 0 | Unknown | |
|
||||
| tests/markdown/test_entity_parser.py | Source Code | 8669 | phernandez | phernandez, Paul Hernandez, github-actions[bot] |
|
||||
| tests/markdown/test_markdown_plugins.py | Source Code | 5139 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/markdown/test_markdown_processor.py | Source Code | 5728 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/markdown/test_observation_edge_cases.py | Source Code | 4295 | phernandez | phernandez |
|
||||
| tests/markdown/test_parser_edge_cases.py | Source Code | 5142 | phernandez | phernandez |
|
||||
| tests/markdown/test_relation_edge_cases.py | Source Code | 3952 | phernandez | phernandez |
|
||||
| tests/markdown/test_task_detection.py | Source Code | 401 | Paul Hernandez | Paul Hernandez |
|
||||
| tests/mcp/conftest.py | Source Code | 1761 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| tests/mcp/test_prompts.py | Source Code | 5989 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/mcp/test_resource_project_info.py | Source Code | 4885 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/mcp/test_resources.py | Source Code | 549 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/mcp/test_tool_canvas.py | Source Code | 7997 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/mcp/test_tool_delete_note.py | Source Code | 4208 | phernandez | phernandez |
|
||||
| tests/mcp/test_tool_edit_note.py | Source Code | 13503 | Paul Hernandez | Paul Hernandez, phernandez, Drew Cain |
|
||||
| tests/mcp/test_tool_list_directory.py | Source Code | 7730 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/mcp/test_tool_move_note.py | Source Code | 16346 | Paul Hernandez | Paul Hernandez, phernandez |
|
||||
| tests/mcp/test_tool_read_note.py | Source Code | 9741 | Paul Hernandez | Paul Hernandez, phernandez, github-actions[bot], A... |
|
||||
| tests/mcp/test_tool_recent_activity.py | Source Code | 3983 | phernandez | phernandez, bm-claudeai |
|
||||
| tests/mcp/test_tool_resource.py | Source Code | 6782 | Paul Hernandez | Paul Hernandez, phernandez, github-actions[bot] |
|
||||
| tests/mcp/test_tool_search.py | Source Code | 10959 | phernandez | Paul Hernandez, phernandez, github-actions[bot], D... |
|
||||
| tests/mcp/test_tool_sync_status.py | Source Code | 6331 | phernandez | phernandez |
|
||||
| tests/mcp/test_tool_utils.py | Source Code | 9228 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/mcp/test_tool_view_note.py | Source Code | 10004 | phernandez | phernandez |
|
||||
| tests/mcp/test_tool_write_note.py | Source Code | 19537 | Paul Hernandez | phernandez, Paul Hernandez, Drew Cain |
|
||||
| tests/repository/test_entity_repository.py | Source Code | 15498 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| tests/repository/test_entity_repository_upsert.py | Source Code | 16983 | Drew Cain | Drew Cain, phernandez |
|
||||
| tests/repository/test_observation_repository.py | Source Code | 11786 | phernandez | phernandez, bm-claudeai |
|
||||
| tests/repository/test_project_info_repository.py | Source Code | 1186 | bm-claudeai | bm-claudeai |
|
||||
| tests/repository/test_project_repository.py | Source Code | 9455 | bm-claudeai | bm-claudeai |
|
||||
| tests/repository/test_relation_repository.py | Source Code | 11980 | phernandez | phernandez, bm-claudeai |
|
||||
| tests/repository/test_repository.py | Source Code | 5996 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/repository/test_search_repository.py | Source Code | 25471 | bm-claudeai | bm-claudeai, phernandez, Paul Hernandez |
|
||||
| tests/schemas/test_memory_url.py | Source Code | 2000 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/schemas/test_memory_url_validation.py | Source Code | 9338 | phernandez | phernandez |
|
||||
| tests/schemas/test_schemas.py | Source Code | 16880 | phernandez | phernandez, Paul Hernandez, bm-claudeai, Drew Cain... |
|
||||
| tests/schemas/test_search.py | Source Code | 3264 | phernandez | phernandez, Paul Hernandez, github-actions[bot] |
|
||||
| tests/services/test_context_service.py | Source Code | 8833 | phernandez | phernandez, bm-claudeai, Paul Hernandez |
|
||||
| tests/services/test_directory_service.py | Source Code | 6854 | Paul Hernandez | bm-claudeai, Paul Hernandez |
|
||||
| tests/services/test_entity_service.py | Source Code | 59278 | Paul Hernandez | phernandez, Paul Hernandez, bm-claudeai, Drew Cain... |
|
||||
| tests/services/test_file_service.py | Source Code | 5243 | phernandez | phernandez |
|
||||
| tests/services/test_initialization.py | Source Code | 6939 | bm-claudeai | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/services/test_link_resolver.py | Source Code | 13618 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| tests/services/test_project_service.py | Source Code | 23576 | phernandez | bm-claudeai, Paul Hernandez, phernandez |
|
||||
| tests/services/test_project_service_operations.py | Source Code | 4679 | bm-claudeai | bm-claudeai, Paul Hernandez |
|
||||
| tests/services/test_search_service.py | Source Code | 25299 | Paul Hernandez | phernandez, Paul Hernandez, github-actions[bot] |
|
||||
| tests/services/test_sync_status_service.py | Source Code | 9926 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/sync/test_sync_service.py | Source Code | 39659 | phernandez | phernandez, Paul Hernandez, bm-claudeai |
|
||||
| tests/sync/test_sync_wikilink_issue.py | Source Code | 1959 | github-actions[bot] | github-actions[bot], Paul Hernandez |
|
||||
| tests/sync/test_tmp_files.py | Source Code | 5930 | Paul Hernandez | Paul Hernandez, bm-claudeai, phernandez |
|
||||
| tests/sync/test_watch_service.py | Source Code | 14076 | Paul Hernandez | Paul Hernandez, phernandez, bm-claudeai |
|
||||
| tests/sync/test_watch_service_edge_cases.py | Source Code | 2169 | Paul Hernandez | Paul Hernandez, bm-claudeai |
|
||||
| tests/test_config.py | Source Code | 3680 | Drew Cain | Drew Cain |
|
||||
| tests/test_db_migration_deduplication.py | Source Code | 6166 | Paul Hernandez | Paul Hernandez, Drew Cain, phernandez |
|
||||
| tests/utils/test_file_utils.py | Source Code | 6199 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/utils/test_parse_tags.py | Source Code | 1691 | github-actions[bot] | github-actions[bot], Paul Hernandez |
|
||||
| tests/utils/test_permalink_formatting.py | Source Code | 4193 | phernandez | phernandez, Paul Hernandez |
|
||||
| tests/utils/test_utf8_handling.py | Source Code | 5791 | phernandez | phernandez, Paul Hernandez |
|
||||
+3
-3
@@ -30,7 +30,7 @@ dependencies = [
|
||||
"alembic>=1.14.1",
|
||||
"pillow>=11.1.0",
|
||||
"pybars3>=0.9.7",
|
||||
"fastmcp>=2.3.4",
|
||||
"fastmcp==2.10.2",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
@@ -52,7 +52,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["src", "tests"]
|
||||
addopts = "--cov=basic_memory --cov-report term-missing -ra -q"
|
||||
addopts = "--cov=basic_memory --cov-report term-missing"
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "strict"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
@@ -123,4 +123,4 @@ omit = [
|
||||
]
|
||||
|
||||
[tool.logfire]
|
||||
ignore_no_config = true
|
||||
ignore_no_config = true
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Legal File Inventory Generator for Basic Memory
|
||||
|
||||
Generates comprehensive file inventory with contributor information
|
||||
for legal documentation, copyright assignments, and due diligence.
|
||||
|
||||
Usage:
|
||||
python scripts/generate_legal_inventory.py [options]
|
||||
|
||||
Output formats:
|
||||
- CSV: Detailed spreadsheet for analysis
|
||||
- JSON: Structured data for integration
|
||||
- Markdown: Human-readable legal report
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class LegalInventoryGenerator:
|
||||
"""Generate comprehensive file inventory for legal documentation."""
|
||||
|
||||
# Files to exclude from legal inventory
|
||||
EXCLUDED_PATTERNS = {
|
||||
# Generated/compiled files
|
||||
'*.pyc', '*.pyo', '*.pyd', '__pycache__',
|
||||
'*.so', '*.dylib', '*.dll',
|
||||
|
||||
# Build/cache directories
|
||||
'build/', 'dist/', '.eggs/', '*.egg-info/',
|
||||
'.coverage', '.pytest_cache/', '.mypy_cache/',
|
||||
'.ruff_cache/', '.tox/', 'venv/', '.venv/', 'env/', '.env/',
|
||||
'node_modules/', '.npm/', '.yarn/',
|
||||
|
||||
# IDE and editor files
|
||||
'.vscode/', '.idea/', '*.swp', '*.swo', '*~',
|
||||
'.DS_Store', 'Thumbs.db',
|
||||
|
||||
# Version control
|
||||
'.git/', '.gitignore',
|
||||
|
||||
# OS generated
|
||||
'desktop.ini', '*.tmp', '*.temp'
|
||||
}
|
||||
|
||||
# File categories for legal classification
|
||||
FILE_CATEGORIES = {
|
||||
'source_code': ['.py', '.pyx', '.pyi'],
|
||||
'documentation': ['.md', '.rst', '.txt'],
|
||||
'configuration': ['.toml', '.yaml', '.yml', '.json', '.ini', '.cfg'],
|
||||
'legal': ['LICENSE', 'COPYING', 'COPYRIGHT', '.md'],
|
||||
'build_deployment': ['Dockerfile', 'Makefile', 'justfile', '.sh'],
|
||||
'database': ['.sql', '.sqlite', '.db'],
|
||||
'templates': ['.j2', '.jinja2', '.hbs', '.handlebars'],
|
||||
'data': ['.csv', '.json', '.xml'],
|
||||
'other': [] # Catch-all for uncategorized files
|
||||
}
|
||||
|
||||
def __init__(self, repo_path: str = "."):
|
||||
"""Initialize generator with repository path."""
|
||||
self.repo_path = Path(repo_path).resolve()
|
||||
self.file_inventory: List[Dict] = []
|
||||
self.contributors: Dict[str, Dict] = defaultdict(lambda: {
|
||||
'email': '', 'commits': 0, 'lines_added': 0, 'files': set()
|
||||
})
|
||||
|
||||
def should_exclude_file(self, file_path: Path) -> bool:
|
||||
"""Check if file should be excluded from inventory."""
|
||||
# Check if file is tracked by git (more efficient than check-ignore)
|
||||
try:
|
||||
rel_path = str(file_path.relative_to(self.repo_path))
|
||||
result = subprocess.run([
|
||||
'git', 'ls-files', '--error-unmatch', rel_path
|
||||
], capture_output=True, cwd=self.repo_path)
|
||||
|
||||
# If git ls-files returns non-zero, file is not tracked (likely ignored)
|
||||
if result.returncode != 0:
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
# Fallback to manual exclusion patterns if git fails
|
||||
pass
|
||||
|
||||
# Additional manual exclusions for safety
|
||||
file_str = str(file_path.relative_to(self.repo_path))
|
||||
|
||||
for pattern in self.EXCLUDED_PATTERNS:
|
||||
if pattern.endswith('/'):
|
||||
if any(part == pattern[:-1] for part in file_path.parts):
|
||||
return True
|
||||
elif '*' in pattern:
|
||||
import fnmatch
|
||||
if fnmatch.fnmatch(file_str, pattern):
|
||||
return True
|
||||
else:
|
||||
if file_path.name == pattern or file_str == pattern:
|
||||
return True
|
||||
return False
|
||||
|
||||
def categorize_file(self, file_path: Path) -> str:
|
||||
"""Categorize file based on extension and name."""
|
||||
suffix = file_path.suffix.lower()
|
||||
name = file_path.name.upper()
|
||||
|
||||
# Check legal files by name first
|
||||
if any(legal in name for legal in ['LICENSE', 'COPYING', 'COPYRIGHT', 'CLA']):
|
||||
return 'legal'
|
||||
|
||||
# Check by extension
|
||||
for category, extensions in self.FILE_CATEGORIES.items():
|
||||
if suffix in extensions:
|
||||
return category
|
||||
|
||||
return 'other'
|
||||
|
||||
def get_file_hash(self, file_path: Path) -> str:
|
||||
"""Generate SHA-256 hash of file content."""
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()
|
||||
except (IOError, OSError):
|
||||
return "ERROR_READING_FILE"
|
||||
|
||||
def get_git_contributors(self, file_path: Path) -> List[Dict]:
|
||||
"""Get contributor information for a specific file."""
|
||||
try:
|
||||
rel_path = file_path.relative_to(self.repo_path)
|
||||
|
||||
# Get contributors with line counts
|
||||
result = subprocess.run([
|
||||
'git', 'log', '--follow', '--pretty=format:%an|%ae|%ad|%H',
|
||||
'--date=short', '--', str(rel_path)
|
||||
], capture_output=True, text=True, cwd=self.repo_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
|
||||
contributors = []
|
||||
seen = set()
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split('|')
|
||||
if len(parts) >= 4:
|
||||
name, email, date, commit_hash = parts[:4]
|
||||
|
||||
# Normalize author names/emails
|
||||
normalized_name = self.normalize_author_name(name, email)
|
||||
|
||||
if normalized_name not in seen:
|
||||
contributors.append({
|
||||
'name': normalized_name,
|
||||
'email': email,
|
||||
'first_contribution': date,
|
||||
'commit_hash': commit_hash
|
||||
})
|
||||
seen.add(normalized_name)
|
||||
|
||||
return contributors
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get git info for {file_path}: {e}")
|
||||
return []
|
||||
|
||||
def normalize_author_name(self, name: str, email: str) -> str:
|
||||
"""Normalize author names to handle multiple emails for same person."""
|
||||
# Known mappings for Basic Memory team
|
||||
name_mappings = {
|
||||
'phernandez': 'Paul Hernandez',
|
||||
'Paul Hernandez': 'Paul Hernandez',
|
||||
'drew-cain': 'Drew Cain',
|
||||
'Drew Cain': 'Drew Cain'
|
||||
}
|
||||
|
||||
# Handle GitHub bot accounts
|
||||
if 'bot' in name.lower() or 'claude' in name.lower():
|
||||
return f"{name} (AI Assistant)"
|
||||
|
||||
return name_mappings.get(name, name)
|
||||
|
||||
def get_file_stats(self, file_path: Path) -> Dict:
|
||||
"""Get comprehensive file statistics."""
|
||||
try:
|
||||
stat = file_path.stat()
|
||||
rel_path = file_path.relative_to(self.repo_path)
|
||||
|
||||
# Basic file info
|
||||
file_info = {
|
||||
'path': str(rel_path),
|
||||
'name': file_path.name,
|
||||
'size_bytes': stat.st_size,
|
||||
'modified_time': datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
||||
'category': self.categorize_file(file_path),
|
||||
'sha256_hash': self.get_file_hash(file_path)
|
||||
}
|
||||
|
||||
# Git information
|
||||
contributors = self.get_git_contributors(file_path)
|
||||
file_info['contributors'] = contributors
|
||||
file_info['primary_author'] = contributors[0]['name'] if contributors else 'Unknown'
|
||||
file_info['contributor_count'] = len(contributors)
|
||||
|
||||
# Update global contributor stats
|
||||
for contrib in contributors:
|
||||
name = contrib['name']
|
||||
self.contributors[name]['email'] = contrib['email']
|
||||
self.contributors[name]['files'].add(str(rel_path))
|
||||
|
||||
return file_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def scan_repository(self) -> None:
|
||||
"""Scan repository and build file inventory."""
|
||||
print(f"Scanning repository: {self.repo_path}")
|
||||
|
||||
# Get all git-tracked files first (much more efficient)
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'git', 'ls-files'
|
||||
], capture_output=True, text=True, cwd=self.repo_path)
|
||||
|
||||
if result.returncode == 0:
|
||||
tracked_files = [self.repo_path / f for f in result.stdout.strip().split('\n') if f]
|
||||
print(f"Found {len(tracked_files)} git-tracked files")
|
||||
|
||||
for file_path in tracked_files:
|
||||
if file_path.is_file():
|
||||
file_info = self.get_file_stats(file_path)
|
||||
if file_info:
|
||||
self.file_inventory.append(file_info)
|
||||
else:
|
||||
print("Warning: Could not get git tracked files, falling back to directory scan")
|
||||
self._fallback_scan()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Git command failed ({e}), falling back to directory scan")
|
||||
self._fallback_scan()
|
||||
|
||||
# Get global git stats
|
||||
self._get_global_git_stats()
|
||||
|
||||
print(f"Processed {len(self.file_inventory)} files")
|
||||
print(f"Found {len(self.contributors)} contributors")
|
||||
|
||||
def _fallback_scan(self) -> None:
|
||||
"""Fallback directory scan if git commands fail."""
|
||||
for file_path in self.repo_path.rglob('*'):
|
||||
if file_path.is_file() and not self.should_exclude_file(file_path):
|
||||
file_info = self.get_file_stats(file_path)
|
||||
if file_info:
|
||||
self.file_inventory.append(file_info)
|
||||
|
||||
def _get_global_git_stats(self) -> None:
|
||||
"""Get global contributor statistics from git."""
|
||||
try:
|
||||
# Get commit counts per author
|
||||
result = subprocess.run([
|
||||
'git', 'shortlog', '-sn', '--all'
|
||||
], capture_output=True, text=True, cwd=self.repo_path)
|
||||
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
parts = line.strip().split('\t', 1)
|
||||
if len(parts) == 2:
|
||||
count, name = parts
|
||||
normalized_name = self.normalize_author_name(name, '')
|
||||
self.contributors[normalized_name]['commits'] = int(count)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get global git stats: {e}")
|
||||
|
||||
def generate_summary(self) -> Dict:
|
||||
"""Generate inventory summary statistics."""
|
||||
total_files = len(self.file_inventory)
|
||||
total_size = sum(f['size_bytes'] for f in self.file_inventory)
|
||||
|
||||
# Category breakdown
|
||||
categories = defaultdict(int)
|
||||
for file_info in self.file_inventory:
|
||||
categories[file_info['category']] += 1
|
||||
|
||||
# Top contributors
|
||||
top_contributors = sorted(
|
||||
self.contributors.items(),
|
||||
key=lambda x: len(x[1]['files']),
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
return {
|
||||
'scan_date': datetime.now().isoformat(),
|
||||
'repository_path': str(self.repo_path),
|
||||
'total_files': total_files,
|
||||
'total_size_bytes': total_size,
|
||||
'categories': dict(categories),
|
||||
'contributor_count': len(self.contributors),
|
||||
'top_contributors': [
|
||||
{
|
||||
'name': name,
|
||||
'file_count': len(stats['files']),
|
||||
'commit_count': stats['commits'],
|
||||
'email': stats['email']
|
||||
}
|
||||
for name, stats in top_contributors
|
||||
]
|
||||
}
|
||||
|
||||
def export_csv(self, output_path: str) -> None:
|
||||
"""Export inventory to CSV format."""
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
||||
fieldnames = [
|
||||
'path', 'name', 'category', 'size_bytes', 'modified_time',
|
||||
'primary_author', 'contributor_count', 'contributors_list',
|
||||
'sha256_hash'
|
||||
]
|
||||
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for file_info in sorted(self.file_inventory, key=lambda x: x['path']):
|
||||
contributors_list = '; '.join([
|
||||
f"{c['name']} ({c['email']})" for c in file_info['contributors']
|
||||
])
|
||||
|
||||
writer.writerow({
|
||||
'path': file_info['path'],
|
||||
'name': file_info['name'],
|
||||
'category': file_info['category'],
|
||||
'size_bytes': file_info['size_bytes'],
|
||||
'modified_time': file_info['modified_time'],
|
||||
'primary_author': file_info['primary_author'],
|
||||
'contributor_count': file_info['contributor_count'],
|
||||
'contributors_list': contributors_list,
|
||||
'sha256_hash': file_info['sha256_hash']
|
||||
})
|
||||
|
||||
print(f"CSV export saved to: {output_path}")
|
||||
|
||||
def export_json(self, output_path: str) -> None:
|
||||
"""Export inventory to JSON format."""
|
||||
# Convert sets to lists for JSON serialization
|
||||
contributors_serializable = {}
|
||||
for name, stats in self.contributors.items():
|
||||
contributors_serializable[name] = {
|
||||
'email': stats['email'],
|
||||
'commits': stats['commits'],
|
||||
'lines_added': stats['lines_added'],
|
||||
'files': list(stats['files'])
|
||||
}
|
||||
|
||||
data = {
|
||||
'summary': self.generate_summary(),
|
||||
'files': self.file_inventory,
|
||||
'contributors': contributors_serializable
|
||||
}
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as jsonfile:
|
||||
json.dump(data, jsonfile, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"JSON export saved to: {output_path}")
|
||||
|
||||
def export_markdown(self, output_path: str) -> None:
|
||||
"""Export inventory to Markdown format for legal documentation."""
|
||||
summary = self.generate_summary()
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as mdfile:
|
||||
mdfile.write("# Basic Memory - Legal File Inventory\n\n")
|
||||
|
||||
# Summary section
|
||||
mdfile.write("## Summary\n\n")
|
||||
mdfile.write(f"**Scan Date:** {summary['scan_date']}\n")
|
||||
mdfile.write(f"**Repository:** {summary['repository_path']}\n")
|
||||
mdfile.write(f"**Total Files:** {summary['total_files']:,}\n")
|
||||
mdfile.write(f"**Total Size:** {summary['total_size_bytes']:,} bytes\n")
|
||||
mdfile.write(f"**Contributors:** {summary['contributor_count']}\n\n")
|
||||
|
||||
# Category breakdown
|
||||
mdfile.write("## File Categories\n\n")
|
||||
for category, count in sorted(summary['categories'].items()):
|
||||
mdfile.write(f"- **{category.replace('_', ' ').title()}:** {count} files\n")
|
||||
mdfile.write("\n")
|
||||
|
||||
# Top contributors
|
||||
mdfile.write("## Contributors\n\n")
|
||||
for contrib in summary['top_contributors']:
|
||||
mdfile.write(f"- **{contrib['name']}** ({contrib['email']}): ")
|
||||
mdfile.write(f"{contrib['file_count']} files, {contrib['commit_count']} commits\n")
|
||||
mdfile.write("\n")
|
||||
|
||||
# Detailed file listing by category
|
||||
mdfile.write("## Detailed File Inventory\n\n")
|
||||
|
||||
for category in sorted(summary['categories'].keys()):
|
||||
category_files = [f for f in self.file_inventory if f['category'] == category]
|
||||
if not category_files:
|
||||
continue
|
||||
|
||||
mdfile.write(f"### {category.replace('_', ' ').title()}\n\n")
|
||||
|
||||
for file_info in sorted(category_files, key=lambda x: x['path']):
|
||||
mdfile.write(f"**{file_info['path']}**\n")
|
||||
mdfile.write(f"- Primary Author: {file_info['primary_author']}\n")
|
||||
mdfile.write(f"- Contributors: {file_info['contributor_count']}\n")
|
||||
mdfile.write(f"- Size: {file_info['size_bytes']:,} bytes\n")
|
||||
|
||||
if file_info['contributors']:
|
||||
contributors_str = ', '.join([c['name'] for c in file_info['contributors']])
|
||||
mdfile.write(f"- All Contributors: {contributors_str}\n")
|
||||
|
||||
mdfile.write(f"- SHA-256: `{file_info['sha256_hash']}`\n\n")
|
||||
|
||||
print(f"Markdown export saved to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate legal file inventory for Basic Memory repository"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo-path', '-r',
|
||||
default='.',
|
||||
help='Path to repository (default: current directory)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output-dir', '-o',
|
||||
default='./legal_inventory',
|
||||
help='Output directory for reports (default: ./legal_inventory)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--formats', '-f',
|
||||
nargs='+',
|
||||
choices=['csv', 'json', 'markdown', 'all'],
|
||||
default=['all'],
|
||||
help='Output formats to generate (default: all)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate inventory
|
||||
generator = LegalInventoryGenerator(args.repo_path)
|
||||
generator.scan_repository()
|
||||
|
||||
# Determine formats to export
|
||||
formats = args.formats
|
||||
if 'all' in formats:
|
||||
formats = ['csv', 'json', 'markdown']
|
||||
|
||||
# Export in requested formats
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
if 'csv' in formats:
|
||||
generator.export_csv(output_dir / f'basic_memory_inventory_{timestamp}.csv')
|
||||
|
||||
if 'json' in formats:
|
||||
generator.export_json(output_dir / f'basic_memory_inventory_{timestamp}.json')
|
||||
|
||||
if 'markdown' in formats:
|
||||
generator.export_markdown(output_dir / f'basic_memory_inventory_{timestamp}.md')
|
||||
|
||||
print("\nLegal inventory generation complete!")
|
||||
print(f"Output saved to: {output_dir}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.14.0"
|
||||
__version__ = "0.14.4"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -8,17 +8,19 @@ from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
|
||||
os.environ["BASIC_MEMORY_ENV"] = "test"
|
||||
|
||||
# Import after setting environment variable # noqa: E402
|
||||
from basic_memory.config import app_config # noqa: E402
|
||||
from basic_memory.models import Base # noqa: E402
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Set the SQLAlchemy URL from our app config
|
||||
sqlalchemy_url = f"sqlite:///{app_config.database_path}"
|
||||
config.set_main_option("sqlalchemy.url", sqlalchemy_url)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""fix project foreign keys
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises: 647e7a75e2cd
|
||||
Create Date: 2025-08-19 22:06:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a1b2c3d4e5f6"
|
||||
down_revision: Union[str, None] = "647e7a75e2cd"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Re-establish foreign key constraints that were lost during project table recreation.
|
||||
|
||||
The migration 647e7a75e2cd recreated the project table but did not re-establish
|
||||
the foreign key constraint from entity.project_id to project.id, causing
|
||||
foreign key constraint failures when trying to delete projects with related entities.
|
||||
"""
|
||||
# SQLite doesn't allow adding foreign key constraints to existing tables easily
|
||||
# We need to be careful and handle the case where the constraint might already exist
|
||||
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
# Try to drop existing foreign key constraint (may not exist)
|
||||
try:
|
||||
batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
|
||||
except Exception:
|
||||
# Constraint may not exist, which is fine - we'll create it next
|
||||
pass
|
||||
|
||||
# Add the foreign key constraint with CASCADE DELETE
|
||||
# This ensures that when a project is deleted, all related entities are also deleted
|
||||
batch_op.create_foreign_key(
|
||||
"fk_entity_project_id",
|
||||
"project",
|
||||
["project_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove the foreign key constraint."""
|
||||
with op.batch_alter_table("entity", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
|
||||
@@ -20,15 +20,18 @@ from basic_memory.api.routers import (
|
||||
search,
|
||||
prompt_router,
|
||||
)
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.services.initialization import initialize_app, initialize_file_sync
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI): # pragma: no cover
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Initialize app and database
|
||||
logger.info("Starting Basic Memory API")
|
||||
print(f"fastapi {app_config.projects}")
|
||||
await initialize_app(app_config)
|
||||
|
||||
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.deps import SyncServiceDep, ProjectRepositoryDep
|
||||
|
||||
router = APIRouter(prefix="/management", tags=["management"])
|
||||
@@ -41,6 +41,8 @@ async def start_watch_service(
|
||||
# Watch service is already running
|
||||
return WatchStatusResponse(running=True)
|
||||
|
||||
app_config = ConfigManager().config
|
||||
|
||||
# Create and start a new watch service
|
||||
logger.info("Starting watch service via management API")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Router for project management."""
|
||||
|
||||
import os
|
||||
from fastapi import APIRouter, HTTPException, Path, Body
|
||||
from typing import Optional
|
||||
|
||||
@@ -32,40 +33,47 @@ async def get_project_info(
|
||||
@project_router.patch("/{name}", response_model=ProjectStatusResponse)
|
||||
async def update_project(
|
||||
project_service: ProjectServiceDep,
|
||||
project_name: str = Path(..., description="Name of the project to update"),
|
||||
path: Optional[str] = Body(None, description="New path for the project"),
|
||||
name: str = Path(..., description="Name of the project to update"),
|
||||
path: Optional[str] = Body(None, description="New absolute path for the project"),
|
||||
is_active: Optional[bool] = Body(None, description="Status of the project (active/inactive)"),
|
||||
) -> ProjectStatusResponse:
|
||||
"""Update a project's information in configuration and database.
|
||||
|
||||
Args:
|
||||
project_name: The name of the project to update
|
||||
path: Optional new path for the project
|
||||
name: The name of the project to update
|
||||
path: Optional new absolute path for the project
|
||||
is_active: Optional status update for the project
|
||||
|
||||
Returns:
|
||||
Response confirming the project was updated
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
try:
|
||||
# Validate that path is absolute if provided
|
||||
if path and not os.path.isabs(path):
|
||||
raise HTTPException(status_code=400, detail="Path must be absolute")
|
||||
|
||||
# Get original project info for the response
|
||||
old_project_info = ProjectItem(
|
||||
name=project_name,
|
||||
path=project_service.projects.get(project_name, ""),
|
||||
name=name,
|
||||
path=project_service.projects.get(name, ""),
|
||||
)
|
||||
|
||||
await project_service.update_project(project_name, updated_path=path, is_active=is_active)
|
||||
if path:
|
||||
await project_service.move_project(name, path)
|
||||
elif is_active is not None:
|
||||
await project_service.update_project(name, is_active=is_active)
|
||||
|
||||
# Get updated project info
|
||||
updated_path = path if path else project_service.projects.get(project_name, "")
|
||||
updated_path = path if path else project_service.projects.get(name, "")
|
||||
|
||||
return ProjectStatusResponse(
|
||||
message=f"Project '{project_name}' updated successfully",
|
||||
message=f"Project '{name}' updated successfully",
|
||||
status="success",
|
||||
default=(project_name == project_service.default_project),
|
||||
default=(name == project_service.default_project),
|
||||
old_project=old_project_info,
|
||||
new_project=ProjectItem(name=project_name, path=updated_path),
|
||||
new_project=ProjectItem(name=name, path=updated_path),
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ async def write_resource(
|
||||
"content_type": content_type,
|
||||
"file_path": file_path,
|
||||
"checksum": checksum,
|
||||
"updated_at": datetime.fromtimestamp(file_stats.st_mtime),
|
||||
"updated_at": datetime.fromtimestamp(file_stats.st_mtime).astimezone(),
|
||||
},
|
||||
)
|
||||
status_code = 200
|
||||
@@ -200,8 +200,8 @@ async def write_resource(
|
||||
content_type=content_type,
|
||||
file_path=file_path,
|
||||
checksum=checksum,
|
||||
created_at=datetime.fromtimestamp(file_stats.st_ctime),
|
||||
updated_at=datetime.fromtimestamp(file_stats.st_mtime),
|
||||
created_at=datetime.fromtimestamp(file_stats.st_ctime).astimezone(),
|
||||
updated_at=datetime.fromtimestamp(file_stats.st_mtime).astimezone(),
|
||||
)
|
||||
entity = await entity_repository.add(entity)
|
||||
status_code = 201
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.config import get_project_config, ConfigManager
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ def version_callback(value: bool) -> None:
|
||||
"""Show version and exit."""
|
||||
if value: # pragma: no cover
|
||||
import basic_memory
|
||||
from basic_memory.config import config
|
||||
|
||||
config = get_project_config()
|
||||
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
|
||||
typer.echo(f"Current project: {config.project}")
|
||||
typer.echo(f"Project path: {config.home}")
|
||||
@@ -44,9 +44,9 @@ def app_callback(
|
||||
|
||||
# Run initialization for every command unless --version was specified
|
||||
if not version and ctx.invoked_subcommand is not None:
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.services.initialization import ensure_initialization
|
||||
|
||||
app_config = ConfigManager().config
|
||||
ensure_initialization(app_config)
|
||||
|
||||
# Initialize MCP session with the specified project or default
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""CLI commands for basic-memory."""
|
||||
|
||||
from . import auth, status, sync, db, import_memory_json, mcp, import_claude_conversations
|
||||
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
|
||||
from . import import_claude_projects, import_chatgpt, tool, project
|
||||
|
||||
__all__ = [
|
||||
"auth",
|
||||
"status",
|
||||
"sync",
|
||||
"db",
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""OAuth management commands."""
|
||||
|
||||
import typer
|
||||
from typing import Optional
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
|
||||
|
||||
auth_app = typer.Typer(help="OAuth client management commands")
|
||||
app.add_typer(auth_app, name="auth")
|
||||
|
||||
|
||||
@auth_app.command()
|
||||
def register_client(
|
||||
client_id: Optional[str] = typer.Option(
|
||||
None, help="Client ID (auto-generated if not provided)"
|
||||
),
|
||||
client_secret: Optional[str] = typer.Option(
|
||||
None, help="Client secret (auto-generated if not provided)"
|
||||
),
|
||||
issuer_url: str = typer.Option("http://localhost:8000", help="OAuth issuer URL"),
|
||||
):
|
||||
"""Register a new OAuth client for Basic Memory MCP server."""
|
||||
|
||||
# Create provider instance
|
||||
provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
# Create client info with required redirect_uris
|
||||
client_info = OAuthClientInformationFull(
|
||||
client_id=client_id or "", # Provider will generate if empty
|
||||
client_secret=client_secret or "", # Provider will generate if empty
|
||||
redirect_uris=[AnyHttpUrl("http://localhost:8000/callback")], # Default redirect URI
|
||||
client_name="Basic Memory OAuth Client",
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
)
|
||||
|
||||
# Register the client
|
||||
import asyncio
|
||||
|
||||
asyncio.run(provider.register_client(client_info))
|
||||
|
||||
typer.echo("Client registered successfully!")
|
||||
typer.echo(f"Client ID: {client_info.client_id}")
|
||||
typer.echo(f"Client Secret: {client_info.client_secret}")
|
||||
typer.echo("\nSave these credentials securely - the client secret cannot be retrieved later.")
|
||||
|
||||
|
||||
@auth_app.command()
|
||||
def test_auth(
|
||||
issuer_url: str = typer.Option("http://localhost:8000", help="OAuth issuer URL"),
|
||||
):
|
||||
"""Test OAuth authentication flow.
|
||||
|
||||
IMPORTANT: Use the same FASTMCP_AUTH_SECRET_KEY environment variable
|
||||
as your MCP server for tokens to validate correctly.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import secrets
|
||||
from mcp.server.auth.provider import AuthorizationParams
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
async def test_flow():
|
||||
# Create provider with same secret key as server
|
||||
provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
# Register a test client
|
||||
client_info = OAuthClientInformationFull(
|
||||
client_id=secrets.token_urlsafe(16),
|
||||
client_secret=secrets.token_urlsafe(32),
|
||||
redirect_uris=[AnyHttpUrl("http://localhost:8000/callback")],
|
||||
client_name="Test OAuth Client",
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
)
|
||||
await provider.register_client(client_info)
|
||||
typer.echo(f"Registered test client: {client_info.client_id}")
|
||||
|
||||
# Get the client
|
||||
client = await provider.get_client(client_info.client_id)
|
||||
if not client:
|
||||
typer.echo("Error: Client not found after registration", err=True)
|
||||
return
|
||||
|
||||
# Create authorization request
|
||||
auth_params = AuthorizationParams(
|
||||
state="test-state",
|
||||
scopes=["read", "write"],
|
||||
code_challenge="test-challenge",
|
||||
redirect_uri=AnyHttpUrl("http://localhost:8000/callback"),
|
||||
redirect_uri_provided_explicitly=True,
|
||||
)
|
||||
|
||||
# Get authorization URL
|
||||
auth_url = await provider.authorize(client, auth_params)
|
||||
typer.echo(f"Authorization URL: {auth_url}")
|
||||
|
||||
# Extract auth code from URL
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
parsed = urlparse(auth_url)
|
||||
params = parse_qs(parsed.query)
|
||||
auth_code = params.get("code", [None])[0]
|
||||
|
||||
if not auth_code:
|
||||
typer.echo("Error: No authorization code in URL", err=True)
|
||||
return
|
||||
|
||||
# Load the authorization code
|
||||
code_obj = await provider.load_authorization_code(client, auth_code)
|
||||
if not code_obj:
|
||||
typer.echo("Error: Invalid authorization code", err=True)
|
||||
return
|
||||
|
||||
# Exchange for tokens
|
||||
token = await provider.exchange_authorization_code(client, code_obj)
|
||||
typer.echo(f"Access token: {token.access_token}")
|
||||
typer.echo(f"Refresh token: {token.refresh_token}")
|
||||
typer.echo(f"Expires in: {token.expires_in} seconds")
|
||||
|
||||
# Validate access token
|
||||
access_token_obj = await provider.load_access_token(token.access_token)
|
||||
if access_token_obj:
|
||||
typer.echo("Access token validated successfully!")
|
||||
typer.echo(f"Client ID: {access_token_obj.client_id}")
|
||||
typer.echo(f"Scopes: {access_token_obj.scopes}")
|
||||
else:
|
||||
typer.echo("Error: Invalid access token", err=True)
|
||||
|
||||
asyncio.run(test_flow())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auth_app()
|
||||
@@ -1,14 +1,13 @@
|
||||
"""Database management commands."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import app_config, config_manager
|
||||
from basic_memory.config import ConfigManager, BasicMemoryConfig, save_basic_memory_config
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -18,6 +17,8 @@ def reset(
|
||||
"""Reset database (drop all tables and recreate)."""
|
||||
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
||||
logger.info("Resetting database...")
|
||||
config_manager = ConfigManager()
|
||||
app_config = config_manager.config
|
||||
# Get database path
|
||||
db_path = app_config.app_database_path
|
||||
|
||||
@@ -27,9 +28,8 @@ def reset(
|
||||
logger.info(f"Database file deleted: {db_path}")
|
||||
|
||||
# Reset project configuration
|
||||
config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
|
||||
config_manager.config.default_project = "main"
|
||||
config_manager.save_config(config_manager.config)
|
||||
config = BasicMemoryConfig()
|
||||
save_basic_memory_config(config_manager.config_file, config)
|
||||
logger.info("Project configuration reset to default")
|
||||
|
||||
# Create a new empty database
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers import ChatGPTImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -49,7 +50,7 @@ def import_chatgpt(
|
||||
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
config = get_project_config()
|
||||
# Process the file
|
||||
base_path = config.home / folder
|
||||
console.print(f"\nImporting chats from {conversations_json}...writing to {base_path}")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.claude_conversations_importer import ClaudeConversationsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -42,6 +43,7 @@ def import_claude(
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
config = get_project_config()
|
||||
try:
|
||||
if not conversations_json.exists():
|
||||
typer.echo(f"Error: File not found: {conversations_json}", err=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import claude_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.claude_projects_importer import ClaudeProjectsImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -41,6 +42,7 @@ def import_projects(
|
||||
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
config = get_project_config()
|
||||
try:
|
||||
if not projects_json.exists():
|
||||
typer.echo(f"Error: File not found: {projects_json}", err=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
from basic_memory.cli.app import import_app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.importers.memory_json_importer import MemoryJsonImporter
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ console = Console()
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
config = get_project_config()
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
@@ -46,6 +47,7 @@ def memory_json(
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
config = get_project_config()
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
@@ -74,7 +76,8 @@ def memory_json(
|
||||
Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {result.entities} entities\n"
|
||||
f"Added {result.relations} relations",
|
||||
f"Added {result.relations} relations\n"
|
||||
f"Skipped {result.skipped_entities} entities\n",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import typer
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
# Import mcp instance
|
||||
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
||||
@@ -34,26 +35,13 @@ def mcp(
|
||||
- sse: Server-Sent Events (for compatibility with existing clients)
|
||||
"""
|
||||
|
||||
# Check if OAuth is enabled
|
||||
import os
|
||||
|
||||
auth_enabled = os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true"
|
||||
if auth_enabled:
|
||||
logger.info("OAuth authentication is ENABLED")
|
||||
logger.info(f"Issuer URL: {os.getenv('FASTMCP_AUTH_ISSUER_URL', 'http://localhost:8000')}")
|
||||
if os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES"):
|
||||
logger.info(f"Required scopes: {os.getenv('FASTMCP_AUTH_REQUIRED_SCOPES')}")
|
||||
else:
|
||||
logger.info("OAuth authentication is DISABLED")
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.services.initialization import initialize_file_sync
|
||||
|
||||
# Start the MCP server with the specified transport
|
||||
|
||||
# Use unified thread-based sync approach for both transports
|
||||
import threading
|
||||
|
||||
app_config = ConfigManager().config
|
||||
|
||||
def run_file_sync():
|
||||
"""Run file sync in a separate thread with its own event loop."""
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
@@ -24,6 +24,7 @@ from basic_memory.schemas.project_info import ProjectStatusResponse
|
||||
from basic_memory.mcp.tools.utils import call_delete
|
||||
from basic_memory.mcp.tools.utils import call_put
|
||||
from basic_memory.utils import generate_permalink
|
||||
from basic_memory.mcp.tools.utils import call_patch
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -73,7 +74,7 @@ def add_project(
|
||||
) -> None:
|
||||
"""Add a new project."""
|
||||
# Resolve to absolute path
|
||||
resolved_path = os.path.abspath(os.path.expanduser(path))
|
||||
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
|
||||
|
||||
try:
|
||||
data = {"name": name, "path": resolved_path, "set_default": set_default}
|
||||
@@ -99,8 +100,8 @@ def remove_project(
|
||||
) -> None:
|
||||
"""Remove a project from configuration."""
|
||||
try:
|
||||
project_name = generate_permalink(name)
|
||||
response = asyncio.run(call_delete(client, f"/projects/{project_name}"))
|
||||
project_permalink = generate_permalink(name)
|
||||
response = asyncio.run(call_delete(client, f"/projects/{project_permalink}"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
@@ -118,9 +119,8 @@ def set_default_project(
|
||||
) -> None:
|
||||
"""Set the default project and activate it for the current session."""
|
||||
try:
|
||||
project_name = generate_permalink(name)
|
||||
|
||||
response = asyncio.run(call_put(client, f"/projects/{project_name}/default"))
|
||||
project_permalink = generate_permalink(name)
|
||||
response = asyncio.run(call_put(client, f"/projects/{project_permalink}/default"))
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
@@ -148,6 +148,46 @@ def synchronize_projects() -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@project_app.command("move")
|
||||
def move_project(
|
||||
name: str = typer.Argument(..., help="Name of the project to move"),
|
||||
new_path: str = typer.Argument(..., help="New absolute path for the project"),
|
||||
) -> None:
|
||||
"""Move a project to a new location."""
|
||||
# Resolve to absolute path
|
||||
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
|
||||
|
||||
try:
|
||||
data = {"path": resolved_path}
|
||||
|
||||
project_permalink = generate_permalink(name)
|
||||
current_project = session.get_current_project()
|
||||
response = asyncio.run(
|
||||
call_patch(client, f"/{current_project}/project/{project_permalink}", json=data)
|
||||
)
|
||||
result = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
console.print(f"[green]{result.message}[/green]")
|
||||
|
||||
# Show important file movement reminder
|
||||
console.print() # Empty line for spacing
|
||||
console.print(
|
||||
Panel(
|
||||
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
|
||||
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
|
||||
f"[cyan]{resolved_path}[/cyan]\n\n"
|
||||
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
|
||||
title="⚠️ Manual File Movement Required",
|
||||
border_style="yellow",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error moving project: {str(e)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@project_app.command("info")
|
||||
def display_project_info(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
||||
|
||||
@@ -12,7 +12,7 @@ from rich.tree import Tree
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.cli.commands.sync import get_sync_service
|
||||
from basic_memory.config import config, app_config
|
||||
from basic_memory.config import ConfigManager, get_project_config
|
||||
from basic_memory.repository import ProjectRepository
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
|
||||
@@ -126,6 +126,9 @@ async def run_status(verbose: bool = False): # pragma: no cover
|
||||
"""Check sync status of files vs database."""
|
||||
# Check knowledge/ directory
|
||||
|
||||
app_config = ConfigManager().config
|
||||
config = get_project_config()
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.tree import Tree
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import ConfigManager, get_project_config
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.models import Project
|
||||
@@ -29,7 +29,6 @@ from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import SyncService
|
||||
from basic_memory.sync.sync_service import SyncReport
|
||||
from basic_memory.config import app_config
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -42,6 +41,8 @@ class ValidationIssue:
|
||||
|
||||
async def get_sync_service(project: Project) -> SyncService: # pragma: no cover
|
||||
"""Get sync service instance with all dependencies."""
|
||||
|
||||
app_config = ConfigManager().config
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
@@ -96,6 +97,7 @@ def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[V
|
||||
|
||||
def display_sync_summary(knowledge: SyncReport):
|
||||
"""Display a one-line summary of sync changes."""
|
||||
config = get_project_config()
|
||||
total_changes = knowledge.total
|
||||
project_name = config.project
|
||||
|
||||
@@ -124,6 +126,7 @@ def display_sync_summary(knowledge: SyncReport):
|
||||
|
||||
def display_detailed_sync_results(knowledge: SyncReport):
|
||||
"""Display detailed sync results with trees."""
|
||||
config = get_project_config()
|
||||
project_name = config.project
|
||||
|
||||
if knowledge.total == 0:
|
||||
@@ -158,6 +161,9 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
||||
|
||||
async def run_sync(verbose: bool = False):
|
||||
"""Run sync operation."""
|
||||
app_config = ConfigManager().config
|
||||
config = get_project_config()
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
@@ -212,6 +218,8 @@ def sync(
|
||||
),
|
||||
) -> None:
|
||||
"""Sync knowledge files with the database."""
|
||||
config = get_project_config()
|
||||
|
||||
try:
|
||||
# Show which project we're syncing
|
||||
typer.echo(f"Syncing project: {config.project}")
|
||||
|
||||
@@ -4,7 +4,6 @@ from basic_memory.cli.app import app # pragma: no cover
|
||||
|
||||
# Register commands
|
||||
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
||||
auth,
|
||||
db,
|
||||
import_chatgpt,
|
||||
import_claude_conversations,
|
||||
|
||||
+61
-34
@@ -46,7 +46,7 @@ class BasicMemoryConfig(BaseSettings):
|
||||
|
||||
projects: Dict[str, str] = Field(
|
||||
default_factory=lambda: {
|
||||
"main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
|
||||
"main": Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")).as_posix()
|
||||
},
|
||||
description="Mapping of project names to their filesystem paths",
|
||||
)
|
||||
@@ -74,6 +74,17 @@ class BasicMemoryConfig(BaseSettings):
|
||||
description="Whether to sync changes in real time. default (True)",
|
||||
)
|
||||
|
||||
kebab_filenames: bool = Field(
|
||||
default=False,
|
||||
description="Format for generated filenames. False preserves spaces and special chars, True converts them to hyphens for consistency with permalinks",
|
||||
)
|
||||
|
||||
# API connection configuration
|
||||
api_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="URL of remote Basic Memory API. If set, MCP will connect to this API instead of using local ASGI transport.",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BASIC_MEMORY_",
|
||||
extra="ignore",
|
||||
@@ -94,9 +105,9 @@ class BasicMemoryConfig(BaseSettings):
|
||||
"""Ensure configuration is valid after initialization."""
|
||||
# Ensure main project exists
|
||||
if "main" not in self.projects: # pragma: no cover
|
||||
self.projects["main"] = str(
|
||||
self.projects["main"] = (
|
||||
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
|
||||
)
|
||||
).as_posix()
|
||||
|
||||
# Ensure default project is valid
|
||||
if self.default_project not in self.projects: # pragma: no cover
|
||||
@@ -124,6 +135,7 @@ class BasicMemoryConfig(BaseSettings):
|
||||
"""
|
||||
|
||||
# Load the app-level database path from the global config
|
||||
config_manager = ConfigManager()
|
||||
config = config_manager.load_config() # pragma: no cover
|
||||
return config.app_database_path # pragma: no cover
|
||||
|
||||
@@ -162,20 +174,21 @@ class ConfigManager:
|
||||
# Ensure config directory exists
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load or create configuration
|
||||
self.config = self.load_config()
|
||||
@property
|
||||
def config(self) -> BasicMemoryConfig:
|
||||
"""Get configuration, loading it lazily if needed."""
|
||||
return self.load_config()
|
||||
|
||||
def load_config(self) -> BasicMemoryConfig:
|
||||
"""Load configuration from file or create default."""
|
||||
|
||||
if self.config_file.exists():
|
||||
try:
|
||||
data = json.loads(self.config_file.read_text(encoding="utf-8"))
|
||||
return BasicMemoryConfig(**data)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
config = BasicMemoryConfig()
|
||||
self.save_config(config)
|
||||
return config
|
||||
logger.exception(f"Failed to load config: {e}")
|
||||
raise e
|
||||
else:
|
||||
config = BasicMemoryConfig()
|
||||
self.save_config(config)
|
||||
@@ -183,10 +196,7 @@ class ConfigManager:
|
||||
|
||||
def save_config(self, config: BasicMemoryConfig) -> None:
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
self.config_file.write_text(json.dumps(config.model_dump(), indent=2))
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
save_basic_memory_config(self.config_file, config)
|
||||
|
||||
@property
|
||||
def projects(self) -> Dict[str, str]:
|
||||
@@ -208,8 +218,10 @@ class ConfigManager:
|
||||
project_path = Path(path)
|
||||
project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
||||
|
||||
self.config.projects[name] = str(project_path)
|
||||
self.save_config(self.config)
|
||||
# Load config, modify it, and save it
|
||||
config = self.load_config()
|
||||
config.projects[name] = project_path.as_posix()
|
||||
self.save_config(config)
|
||||
return ProjectConfig(name=name, home=project_path)
|
||||
|
||||
def remove_project(self, name: str) -> None:
|
||||
@@ -219,11 +231,13 @@ class ConfigManager:
|
||||
if not project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' not found")
|
||||
|
||||
if project_name == self.config.default_project: # pragma: no cover
|
||||
# Load config, check, modify, and save
|
||||
config = self.load_config()
|
||||
if project_name == config.default_project: # pragma: no cover
|
||||
raise ValueError(f"Cannot remove the default project '{name}'")
|
||||
|
||||
del self.config.projects[name]
|
||||
self.save_config(self.config)
|
||||
del config.projects[name]
|
||||
self.save_config(config)
|
||||
|
||||
def set_default_project(self, name: str) -> None:
|
||||
"""Set the default project."""
|
||||
@@ -231,15 +245,18 @@ class ConfigManager:
|
||||
if not project_name: # pragma: no cover
|
||||
raise ValueError(f"Project '{name}' not found")
|
||||
|
||||
self.config.default_project = name
|
||||
self.save_config(self.config)
|
||||
# Load config, modify, and save
|
||||
config = self.load_config()
|
||||
config.default_project = project_name
|
||||
self.save_config(config)
|
||||
|
||||
def get_project(self, name: str) -> Tuple[str, str] | Tuple[None, None]:
|
||||
"""Look up a project from the configuration by name or permalink"""
|
||||
project_permalink = generate_permalink(name)
|
||||
for name, path in app_config.projects.items():
|
||||
if project_permalink == generate_permalink(name):
|
||||
return name, path
|
||||
app_config = self.config
|
||||
for project_name, path in app_config.projects.items():
|
||||
if project_permalink == generate_permalink(project_name):
|
||||
return project_name, path
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -252,7 +269,7 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
actual_project_name = None
|
||||
|
||||
# load the config from file
|
||||
global app_config
|
||||
config_manager = ConfigManager()
|
||||
app_config = config_manager.load_config()
|
||||
|
||||
# Get project name from environment variable
|
||||
@@ -282,14 +299,12 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
||||
raise ValueError(f"Project '{actual_project_name}' not found") # pragma: no cover
|
||||
|
||||
|
||||
# Create config manager
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Export the app-level config
|
||||
app_config: BasicMemoryConfig = config_manager.config
|
||||
|
||||
# Load project config for the default project (backward compatibility)
|
||||
config: ProjectConfig = get_project_config()
|
||||
def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None:
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
file_path.write_text(json.dumps(config.model_dump(), indent=2))
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
|
||||
|
||||
def update_current_project(project_name: str) -> None:
|
||||
@@ -341,12 +356,24 @@ def setup_basic_memory_logging(): # pragma: no cover
|
||||
# print("Skipping duplicate logging setup")
|
||||
return
|
||||
|
||||
# Check for console logging environment variable - accept more truthy values
|
||||
console_logging_env = os.getenv("BASIC_MEMORY_CONSOLE_LOGGING", "false").lower()
|
||||
console_logging = console_logging_env in ("true", "1", "yes", "on")
|
||||
|
||||
# Check for log level environment variable first, fall back to config
|
||||
log_level = os.getenv("BASIC_MEMORY_LOG_LEVEL")
|
||||
if not log_level:
|
||||
config_manager = ConfigManager()
|
||||
log_level = config_manager.config.log_level
|
||||
|
||||
config_manager = ConfigManager()
|
||||
config = get_project_config()
|
||||
setup_logging(
|
||||
env=config_manager.config.env,
|
||||
home_dir=user_home, # Use user home for logs
|
||||
log_level=config_manager.load_config().log_level,
|
||||
log_level=log_level,
|
||||
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
||||
console=False,
|
||||
console=console_logging,
|
||||
)
|
||||
|
||||
logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
@@ -88,7 +88,6 @@ async def get_or_create_db(
|
||||
db_path: Path,
|
||||
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
||||
ensure_migrations: bool = True,
|
||||
app_config: Optional["BasicMemoryConfig"] = None,
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
||||
"""Get or create database engine and session maker."""
|
||||
global _engine, _session_maker
|
||||
@@ -98,10 +97,7 @@ async def get_or_create_db(
|
||||
|
||||
# Run migrations automatically unless explicitly disabled
|
||||
if ensure_migrations:
|
||||
if app_config is None:
|
||||
from basic_memory.config import app_config as global_app_config
|
||||
|
||||
app_config = global_app_config
|
||||
app_config = ConfigManager().config
|
||||
await run_migrations(app_config, db_type)
|
||||
|
||||
# These checks should never fail since we just created the engine and session maker
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
import pathlib
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig, ConfigManager
|
||||
from basic_memory.importers import (
|
||||
ChatGPTImporter,
|
||||
ClaudeConversationsImporter,
|
||||
@@ -33,10 +33,10 @@ from basic_memory.services.file_service import FileService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import SyncService
|
||||
from basic_memory.config import app_config
|
||||
|
||||
|
||||
def get_app_config() -> BasicMemoryConfig: # pragma: no cover
|
||||
app_config = ConfigManager().config
|
||||
return app_config
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
|
||||
|
||||
|
||||
async def get_sync_service(
|
||||
app_config: AppConfigDep,
|
||||
entity_service: EntityServiceDep,
|
||||
entity_parser: EntityParserDep,
|
||||
entity_repository: EntityRepositoryDep,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import yaml
|
||||
import frontmatter
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.utils import FilePath
|
||||
@@ -233,3 +235,66 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
||||
error=str(e),
|
||||
)
|
||||
raise FileError(f"Failed to update frontmatter: {e}")
|
||||
|
||||
|
||||
def dump_frontmatter(post: frontmatter.Post) -> str:
|
||||
"""
|
||||
Serialize frontmatter.Post to markdown with Obsidian-compatible YAML format.
|
||||
|
||||
This function ensures that tags are formatted as YAML lists instead of JSON arrays:
|
||||
|
||||
Good (Obsidian compatible):
|
||||
---
|
||||
tags:
|
||||
- system
|
||||
- overview
|
||||
- reference
|
||||
---
|
||||
|
||||
Bad (current behavior):
|
||||
---
|
||||
tags: ["system", "overview", "reference"]
|
||||
---
|
||||
|
||||
Args:
|
||||
post: frontmatter.Post object to serialize
|
||||
|
||||
Returns:
|
||||
String containing markdown with properly formatted YAML frontmatter
|
||||
"""
|
||||
if not post.metadata:
|
||||
# No frontmatter, just return content
|
||||
return post.content
|
||||
|
||||
# Serialize YAML with block style for lists
|
||||
yaml_str = yaml.dump(
|
||||
post.metadata,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
default_flow_style=False
|
||||
)
|
||||
|
||||
# Construct the final markdown with frontmatter
|
||||
if post.content:
|
||||
return f"---\n{yaml_str}---\n\n{post.content}"
|
||||
else:
|
||||
return f"---\n{yaml_str}---\n"
|
||||
|
||||
|
||||
def sanitize_for_filename(text: str, replacement: str = "-") -> str:
|
||||
"""
|
||||
Sanitize string to be safe for use as a note title
|
||||
Replaces path separators and other problematic characters
|
||||
with hyphens.
|
||||
"""
|
||||
# replace both POSIX and Windows path separators
|
||||
text = re.sub(r"[/\\]", replacement, text)
|
||||
|
||||
# replace some other problematic chars
|
||||
text = re.sub(r'[<>:"|?*]', replacement, text)
|
||||
|
||||
# compress multiple, repeated replacements
|
||||
text = re.sub(f"{re.escape(replacement)}+", replacement, text)
|
||||
|
||||
return text.strip(replacement)
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
break
|
||||
|
||||
# Generate permalink
|
||||
date_prefix = datetime.fromtimestamp(created_at).strftime("%Y%m%d")
|
||||
date_prefix = datetime.fromtimestamp(created_at).astimezone().strftime("%Y%m%d")
|
||||
clean_title = clean_filename(conversation["title"])
|
||||
|
||||
# Format content
|
||||
@@ -193,7 +193,7 @@ class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
def _traverse_messages(
|
||||
self, mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
|
||||
) -> List[Dict[str, Any]]: # pragma: no cover
|
||||
"""Traverse message tree and return messages in order.
|
||||
"""Traverse message tree iteratively to handle deep conversations.
|
||||
|
||||
Args:
|
||||
mapping: Message mapping.
|
||||
@@ -204,19 +204,29 @@ class ChatGPTImporter(Importer[ChatImportResult]):
|
||||
List of message data.
|
||||
"""
|
||||
messages = []
|
||||
node = mapping.get(root_id) if root_id else None
|
||||
if not root_id:
|
||||
return messages
|
||||
|
||||
while node:
|
||||
# Use iterative approach with stack to avoid recursion depth issues
|
||||
stack = [root_id]
|
||||
|
||||
while stack:
|
||||
node_id = stack.pop()
|
||||
if not node_id:
|
||||
continue
|
||||
|
||||
node = mapping.get(node_id)
|
||||
if not node:
|
||||
continue
|
||||
|
||||
# Process current node if it has a message and hasn't been seen
|
||||
if node["id"] not in seen and node.get("message"):
|
||||
seen.add(node["id"])
|
||||
messages.append(node["message"])
|
||||
|
||||
# Follow children
|
||||
# Add children to stack in reverse order to maintain conversation flow
|
||||
children = node.get("children", [])
|
||||
for child_id in children:
|
||||
child_msgs = self._traverse_messages(mapping, child_id, seen)
|
||||
messages.extend(child_msgs)
|
||||
|
||||
break # Don't follow siblings
|
||||
for child_id in reversed(children):
|
||||
stack.append(child_id)
|
||||
|
||||
return messages
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from basic_memory.config import config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
|
||||
from basic_memory.importers.base import Importer
|
||||
from basic_memory.schemas.importer import EntityImportResult
|
||||
@@ -27,10 +27,12 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
Returns:
|
||||
EntityImportResult containing statistics and status of the import.
|
||||
"""
|
||||
config = get_project_config()
|
||||
try:
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
skipped_entities: int = 0
|
||||
|
||||
# Ensure the base path exists
|
||||
base_path = config.home # pragma: no cover
|
||||
@@ -41,7 +43,13 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
for line in source_data:
|
||||
data = line
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
# Handle different possible name keys
|
||||
entity_name = data.get("name") or data.get("entityName") or data.get("id")
|
||||
if not entity_name:
|
||||
logger.warning(f"Entity missing name field: {data}")
|
||||
skipped_entities += 1
|
||||
continue
|
||||
entities[entity_name] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data.get("from") or data.get("from_id")
|
||||
@@ -57,25 +65,31 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
# Second pass - create and write entities
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
# Get entity type with fallback
|
||||
entity_type = entity_data.get("entityType") or entity_data.get("type") or "entity"
|
||||
|
||||
# Ensure entity type directory exists
|
||||
entity_type_dir = base_path / entity_data["entityType"]
|
||||
entity_type_dir = base_path / entity_type
|
||||
entity_type_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get observations with fallback to empty list
|
||||
observations = entity_data.get("observations", [])
|
||||
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"type": entity_type,
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}",
|
||||
"permalink": f"{entity_type}/{name}",
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[Observation(content=obs) for obs in entity_data["observations"]],
|
||||
observations=[Observation(content=obs) for obs in observations],
|
||||
relations=entity_relations.get(name, []),
|
||||
)
|
||||
|
||||
# Write entity file
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
file_path = base_path / f"{entity_type}/{name}.md"
|
||||
await self.write_entity(entity, file_path)
|
||||
entities_created += 1
|
||||
|
||||
@@ -86,6 +100,7 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
success=True,
|
||||
entities=entities_created,
|
||||
relations=relations_count,
|
||||
skipped_entities=skipped_entities,
|
||||
)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
|
||||
@@ -43,13 +43,13 @@ def format_timestamp(timestamp: Any) -> str: # pragma: no cover
|
||||
except ValueError:
|
||||
try:
|
||||
# Try unix timestamp as string
|
||||
timestamp = datetime.fromtimestamp(float(timestamp))
|
||||
timestamp = datetime.fromtimestamp(float(timestamp)).astimezone()
|
||||
except ValueError:
|
||||
# Return as is if we can't parse it
|
||||
return timestamp
|
||||
elif isinstance(timestamp, (int, float)):
|
||||
# Unix timestamp
|
||||
timestamp = datetime.fromtimestamp(timestamp)
|
||||
timestamp = datetime.fromtimestamp(timestamp).astimezone()
|
||||
|
||||
if isinstance(timestamp, datetime):
|
||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@@ -130,6 +130,6 @@ class EntityParser:
|
||||
content=post.content,
|
||||
observations=entity_content.observations,
|
||||
relations=entity_content.relations,
|
||||
created=datetime.fromtimestamp(file_stats.st_ctime),
|
||||
modified=datetime.fromtimestamp(file_stats.st_mtime),
|
||||
created=datetime.fromtimestamp(file_stats.st_ctime).astimezone(),
|
||||
modified=datetime.fromtimestamp(file_stats.st_mtime).astimezone(),
|
||||
)
|
||||
|
||||
@@ -2,11 +2,11 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
from collections import OrderedDict
|
||||
|
||||
import frontmatter
|
||||
from frontmatter import Post
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import file_utils
|
||||
from basic_memory.file_utils import dump_frontmatter
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, Observation, Relation
|
||||
|
||||
@@ -115,7 +115,7 @@ class MarkdownProcessor:
|
||||
|
||||
# Create Post object for frontmatter
|
||||
post = Post(content, **frontmatter_dict)
|
||||
final_content = frontmatter.dumps(post, sort_keys=False)
|
||||
final_content = dump_frontmatter(post)
|
||||
|
||||
logger.debug(f"writing file {path} with content:\n{final_content}")
|
||||
|
||||
|
||||
@@ -8,35 +8,49 @@ from markdown_it.token import Token
|
||||
# Observation handling functions
|
||||
def is_observation(token: Token) -> bool:
|
||||
"""Check if token looks like our observation format."""
|
||||
import re
|
||||
if token.type != "inline": # pragma: no cover
|
||||
return False
|
||||
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
if not content: # pragma: no cover
|
||||
return False
|
||||
|
||||
# if it's a markdown_task, return false
|
||||
if content.startswith("[ ]") or content.startswith("[x]") or content.startswith("[-]"):
|
||||
return False
|
||||
|
||||
has_category = content.startswith("[") and "]" in content
|
||||
|
||||
# Exclude markdown links: [text](url)
|
||||
if re.match(r"^\[.*?\]\(.*?\)$", content):
|
||||
return False
|
||||
|
||||
# Exclude wiki links: [[text]]
|
||||
if re.match(r"^\[\[.*?\]\]$", content):
|
||||
return False
|
||||
|
||||
# Check for proper observation format: [category] content
|
||||
match = re.match(r"^\[([^\[\]()]+)\]\s+(.+)", content)
|
||||
has_tags = "#" in content
|
||||
return has_category or has_tags
|
||||
return bool(match) or has_tags
|
||||
|
||||
|
||||
def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
"""Extract observation parts from token."""
|
||||
# Strip bullet point if present
|
||||
content = token.content.strip()
|
||||
|
||||
# Parse [category]
|
||||
import re
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
|
||||
# Parse [category] with regex
|
||||
match = re.match(r"^\[([^\[\]()]+)\]\s+(.+)", content)
|
||||
category = None
|
||||
if content.startswith("["):
|
||||
end = content.find("]")
|
||||
if end != -1:
|
||||
category = content[1:end].strip() or None # Convert empty to None
|
||||
content = content[end + 1 :].strip()
|
||||
|
||||
if match:
|
||||
category = match.group(1).strip()
|
||||
content = match.group(2).strip()
|
||||
else:
|
||||
# Handle empty brackets [] followed by content
|
||||
empty_match = re.match(r"^\[\]\s+(.+)", content)
|
||||
if empty_match:
|
||||
content = empty_match.group(1).strip()
|
||||
|
||||
# Parse (context)
|
||||
context = None
|
||||
if content.endswith(")"):
|
||||
@@ -44,20 +58,18 @@ def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
if start != -1:
|
||||
context = content[start + 1 : -1].strip()
|
||||
content = content[:start].strip()
|
||||
|
||||
|
||||
# Extract tags and keep original content
|
||||
tags = []
|
||||
parts = content.split()
|
||||
for part in parts:
|
||||
if part.startswith("#"):
|
||||
# Handle multiple #tags stuck together
|
||||
if "#" in part[1:]:
|
||||
# Split on # but keep non-empty tags
|
||||
subtags = [t for t in part.split("#") if t]
|
||||
tags.extend(subtags)
|
||||
else:
|
||||
tags.append(part[1:])
|
||||
|
||||
|
||||
return {
|
||||
"category": category,
|
||||
"content": content,
|
||||
@@ -72,14 +84,16 @@ def is_explicit_relation(token: Token) -> bool:
|
||||
if token.type != "inline": # pragma: no cover
|
||||
return False
|
||||
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
return "[[" in content and "]]" in content
|
||||
|
||||
|
||||
def parse_relation(token: Token) -> Dict[str, Any] | None:
|
||||
"""Extract relation parts from token."""
|
||||
# Remove bullet point if present
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
|
||||
# Extract [[target]]
|
||||
target = None
|
||||
@@ -213,10 +227,12 @@ def relation_plugin(md: MarkdownIt) -> None:
|
||||
token.meta["relations"] = [rel]
|
||||
|
||||
# Always check for inline links in any text
|
||||
elif "[[" in token.content:
|
||||
rels = parse_inline_relations(token.content)
|
||||
if rels:
|
||||
token.meta["relations"] = token.meta.get("relations", []) + rels
|
||||
else:
|
||||
content = token.tag or token.content
|
||||
if "[[" in content:
|
||||
rels = parse_inline_relations(content)
|
||||
if rels:
|
||||
token.meta["relations"] = token.meta.get("relations", []) + rels
|
||||
|
||||
# Add the rule after inline processing
|
||||
md.core.ruler.after("inline", "relations", relation_rule)
|
||||
|
||||
@@ -41,7 +41,7 @@ def entity_model_from_markdown(
|
||||
# Only update permalink if it exists in frontmatter, otherwise preserve existing
|
||||
if markdown.frontmatter.permalink is not None:
|
||||
model.permalink = markdown.frontmatter.permalink
|
||||
model.file_path = str(file_path)
|
||||
model.file_path = file_path.as_posix()
|
||||
model.content_type = "text/markdown"
|
||||
model.created_at = markdown.created
|
||||
model.updated_at = markdown.modified
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
from basic_memory.config import ConfigManager
|
||||
|
||||
|
||||
def create_client() -> AsyncClient:
|
||||
"""Create an HTTP client based on configuration.
|
||||
|
||||
Returns:
|
||||
AsyncClient configured for either local ASGI or remote HTTP transport
|
||||
"""
|
||||
config_manager = ConfigManager()
|
||||
config = config_manager.load_config()
|
||||
|
||||
if config.api_url:
|
||||
# Use HTTP transport for remote API
|
||||
logger.info(f"Creating HTTP client for remote Basic Memory API: {config.api_url}")
|
||||
return AsyncClient(base_url=config.api_url)
|
||||
else:
|
||||
# Use ASGI transport for local API
|
||||
logger.debug("Creating ASGI client for local Basic Memory API")
|
||||
return AsyncClient(transport=ASGITransport(app=fastapi_app), base_url="http://test")
|
||||
|
||||
BASE_URL = "http://test"
|
||||
|
||||
# Create shared async client
|
||||
client = AsyncClient(transport=ASGITransport(app=fastapi_app), base_url=BASE_URL)
|
||||
client = create_client()
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
"""OAuth authentication provider for Basic Memory MCP server."""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional
|
||||
|
||||
import jwt
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class BasicMemoryAuthorizationCode(AuthorizationCode):
|
||||
"""Extended authorization code with additional metadata."""
|
||||
|
||||
issuer_state: Optional[str] = None
|
||||
|
||||
|
||||
class BasicMemoryRefreshToken(RefreshToken):
|
||||
"""Extended refresh token with additional metadata."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BasicMemoryAccessToken(AccessToken):
|
||||
"""Extended access token with additional metadata."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BasicMemoryOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
BasicMemoryAuthorizationCode, BasicMemoryRefreshToken, BasicMemoryAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider for Basic Memory MCP server.
|
||||
|
||||
This is a simple in-memory implementation that can be extended
|
||||
to integrate with external OAuth providers or use persistent storage.
|
||||
"""
|
||||
|
||||
def __init__(self, issuer_url: str = "http://localhost:8000", secret_key: Optional[str] = None):
|
||||
self.issuer_url = issuer_url
|
||||
# Use environment variable for secret key if available, otherwise generate
|
||||
import os
|
||||
|
||||
self.secret_key = (
|
||||
secret_key or os.getenv("FASTMCP_AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
|
||||
)
|
||||
|
||||
# In-memory storage - in production, use a proper database
|
||||
self.clients: Dict[str, OAuthClientInformationFull] = {}
|
||||
self.authorization_codes: Dict[str, BasicMemoryAuthorizationCode] = {}
|
||||
self.refresh_tokens: Dict[str, BasicMemoryRefreshToken] = {}
|
||||
self.access_tokens: Dict[str, BasicMemoryAccessToken] = {}
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client by ID."""
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client."""
|
||||
# Generate client ID if not provided
|
||||
if not client_info.client_id:
|
||||
client_info.client_id = secrets.token_urlsafe(16)
|
||||
|
||||
# Generate client secret if not provided
|
||||
if not client_info.client_secret:
|
||||
client_info.client_secret = secrets.token_urlsafe(32)
|
||||
|
||||
self.clients[client_info.client_id] = client_info
|
||||
logger.info(f"Registered OAuth client: {client_info.client_id}")
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create an authorization URL for the OAuth flow.
|
||||
|
||||
For basic-memory, we'll implement a simple authorization flow.
|
||||
In production, this might redirect to an external provider.
|
||||
"""
|
||||
# Generate authorization code
|
||||
auth_code = secrets.token_urlsafe(32)
|
||||
|
||||
# Store authorization code with metadata
|
||||
self.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
|
||||
code=auth_code,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
issuer_state=params.state,
|
||||
)
|
||||
|
||||
# In a real implementation, we'd redirect to an authorization page
|
||||
# For now, we'll just return the redirect URL with the code
|
||||
redirect_uri = str(params.redirect_uri)
|
||||
separator = "&" if "?" in redirect_uri else "?"
|
||||
|
||||
auth_url = f"{redirect_uri}{separator}code={auth_code}"
|
||||
if params.state:
|
||||
auth_url += f"&state={params.state}"
|
||||
|
||||
return auth_url
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[BasicMemoryAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.authorization_codes.get(authorization_code)
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check if expired
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
del self.authorization_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: BasicMemoryAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange an authorization code for tokens."""
|
||||
# Generate tokens
|
||||
access_token = self._generate_access_token(client.client_id, authorization_code.scopes)
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[access_token] = BasicMemoryAccessToken(
|
||||
token=access_token,
|
||||
client_id=client.client_id,
|
||||
scopes=authorization_code.scopes,
|
||||
expires_at=int(expires_at),
|
||||
)
|
||||
|
||||
self.refresh_tokens[refresh_token] = BasicMemoryRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=authorization_code.scopes,
|
||||
)
|
||||
|
||||
# Remove used authorization code
|
||||
del self.authorization_codes[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=3600, # 1 hour
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[BasicMemoryRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token = self.refresh_tokens.get(refresh_token)
|
||||
|
||||
if token and token.client_id == client.client_id:
|
||||
return token
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: BasicMemoryRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange a refresh token for new tokens."""
|
||||
# Use requested scopes or original scopes
|
||||
token_scopes = scopes if scopes else refresh_token.scopes
|
||||
|
||||
# Generate new tokens
|
||||
new_access_token = self._generate_access_token(client.client_id, token_scopes)
|
||||
new_refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store new tokens
|
||||
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
|
||||
|
||||
self.access_tokens[new_access_token] = BasicMemoryAccessToken(
|
||||
token=new_access_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_scopes,
|
||||
expires_at=int(expires_at),
|
||||
)
|
||||
|
||||
self.refresh_tokens[new_refresh_token] = BasicMemoryRefreshToken(
|
||||
token=new_refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_scopes,
|
||||
)
|
||||
|
||||
# Remove old tokens
|
||||
del self.refresh_tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=3600, # 1 hour
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(token_scopes) if token_scopes else None,
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[BasicMemoryAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
logger.debug("Loading access token, checking in-memory store first")
|
||||
access_token = self.access_tokens.get(token)
|
||||
|
||||
if access_token:
|
||||
# Check if expired
|
||||
if access_token.expires_at and datetime.utcnow().timestamp() > access_token.expires_at:
|
||||
logger.debug("Token found in memory but expired, removing")
|
||||
del self.access_tokens[token]
|
||||
return None
|
||||
logger.debug("Token found in memory and valid")
|
||||
return access_token
|
||||
|
||||
# Try to decode as JWT
|
||||
logger.debug("Token not in memory, attempting JWT decode with secret key")
|
||||
try:
|
||||
# Decode with audience verification - PyJWT expects the audience to match
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self.secret_key,
|
||||
algorithms=["HS256"],
|
||||
audience="basic-memory", # Expecting this audience
|
||||
issuer=self.issuer_url, # And this issuer
|
||||
)
|
||||
logger.debug(f"JWT decoded successfully: {payload}")
|
||||
return BasicMemoryAccessToken(
|
||||
token=token,
|
||||
client_id=payload.get("sub", ""),
|
||||
scopes=payload.get("scopes", []),
|
||||
expires_at=payload.get("exp"),
|
||||
)
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.error(f"JWT decode failed: {e}")
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: BasicMemoryAccessToken | BasicMemoryRefreshToken) -> None:
|
||||
"""Revoke an access or refresh token."""
|
||||
if isinstance(token, BasicMemoryAccessToken):
|
||||
self.access_tokens.pop(token.token, None)
|
||||
else:
|
||||
self.refresh_tokens.pop(token.token, None)
|
||||
|
||||
def _generate_access_token(self, client_id: str, scopes: list[str]) -> str:
|
||||
"""Generate a JWT access token."""
|
||||
payload = {
|
||||
"iss": self.issuer_url,
|
||||
"sub": client_id,
|
||||
"aud": "basic-memory",
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
"scopes": scopes,
|
||||
}
|
||||
|
||||
return jwt.encode(payload, self.secret_key, algorithm="HS256")
|
||||
@@ -1,321 +0,0 @@
|
||||
"""External OAuth provider integration for Basic Memory MCP server."""
|
||||
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
construct_redirect_uri,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalAuthorizationCode(AuthorizationCode):
|
||||
"""Authorization code with external provider metadata."""
|
||||
|
||||
external_code: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalRefreshToken(RefreshToken):
|
||||
"""Refresh token with external provider metadata."""
|
||||
|
||||
external_token: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalAccessToken(AccessToken):
|
||||
"""Access token with external provider metadata."""
|
||||
|
||||
external_token: Optional[str] = None
|
||||
|
||||
|
||||
class ExternalOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
ExternalAuthorizationCode, ExternalRefreshToken, ExternalAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider that delegates to external OAuth providers.
|
||||
|
||||
This provider can integrate with services like:
|
||||
- GitHub OAuth
|
||||
- Google OAuth
|
||||
- Auth0
|
||||
- Okta
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
issuer_url: str,
|
||||
external_provider: str,
|
||||
external_client_id: str,
|
||||
external_client_secret: str,
|
||||
external_authorize_url: str,
|
||||
external_token_url: str,
|
||||
external_userinfo_url: Optional[str] = None,
|
||||
):
|
||||
self.issuer_url = issuer_url
|
||||
self.external_provider = external_provider
|
||||
self.external_client_id = external_client_id
|
||||
self.external_client_secret = external_client_secret
|
||||
self.external_authorize_url = external_authorize_url
|
||||
self.external_token_url = external_token_url
|
||||
self.external_userinfo_url = external_userinfo_url
|
||||
|
||||
# In-memory storage - in production, use a database
|
||||
self.clients: Dict[str, OAuthClientInformationFull] = {}
|
||||
self.codes: Dict[str, ExternalAuthorizationCode] = {}
|
||||
self.tokens: Dict[str, Any] = {}
|
||||
|
||||
self.http_client = httpx.AsyncClient()
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client by ID."""
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client."""
|
||||
self.clients[client_info.client_id] = client_info
|
||||
logger.info(f"Registered external OAuth client: {client_info.client_id}")
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create authorization URL redirecting to external provider."""
|
||||
# Store authorization request
|
||||
import secrets
|
||||
|
||||
state = secrets.token_urlsafe(32)
|
||||
|
||||
self.codes[state] = ExternalAuthorizationCode(
|
||||
code=state,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=0, # Will be set by external provider
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
state=params.state,
|
||||
)
|
||||
|
||||
# Build external provider URL
|
||||
external_params = {
|
||||
"client_id": self.external_client_id,
|
||||
"redirect_uri": f"{self.issuer_url}/callback",
|
||||
"response_type": "code",
|
||||
"state": state,
|
||||
"scope": " ".join(params.scopes or []),
|
||||
}
|
||||
|
||||
return construct_redirect_uri(self.external_authorize_url, **external_params)
|
||||
|
||||
async def handle_callback(self, code: str, state: str) -> str:
|
||||
"""Handle callback from external provider."""
|
||||
# Get original authorization request
|
||||
auth_code = self.codes.get(state)
|
||||
if not auth_code:
|
||||
raise ValueError("Invalid state parameter")
|
||||
|
||||
# Exchange code with external provider
|
||||
token_data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": f"{self.issuer_url}/callback",
|
||||
"client_id": self.external_client_id,
|
||||
"client_secret": self.external_client_secret,
|
||||
}
|
||||
|
||||
response = await self.http_client.post(
|
||||
self.external_token_url,
|
||||
data=token_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
external_tokens = response.json()
|
||||
|
||||
# Store external tokens
|
||||
import secrets
|
||||
|
||||
internal_code = secrets.token_urlsafe(32)
|
||||
|
||||
self.codes[internal_code] = ExternalAuthorizationCode(
|
||||
code=internal_code,
|
||||
scopes=auth_code.scopes,
|
||||
expires_at=0,
|
||||
client_id=auth_code.client_id,
|
||||
code_challenge=auth_code.code_challenge,
|
||||
redirect_uri=auth_code.redirect_uri,
|
||||
redirect_uri_provided_explicitly=auth_code.redirect_uri_provided_explicitly,
|
||||
external_code=code,
|
||||
state=auth_code.state,
|
||||
)
|
||||
|
||||
self.tokens[internal_code] = external_tokens
|
||||
|
||||
# Redirect to original client
|
||||
return construct_redirect_uri(
|
||||
str(auth_code.redirect_uri),
|
||||
code=internal_code,
|
||||
state=auth_code.state,
|
||||
)
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[ExternalAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.codes.get(authorization_code)
|
||||
if code and code.client_id == client.client_id:
|
||||
return code
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: ExternalAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange authorization code for tokens."""
|
||||
# Get stored external tokens
|
||||
external_tokens = self.tokens.get(authorization_code.code)
|
||||
if not external_tokens:
|
||||
raise ValueError("No tokens found for authorization code")
|
||||
|
||||
# Map external tokens to MCP tokens
|
||||
access_token = external_tokens.get("access_token")
|
||||
refresh_token = external_tokens.get("refresh_token")
|
||||
expires_in = external_tokens.get("expires_in", 3600)
|
||||
|
||||
# Store the mapping
|
||||
self.tokens[access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": access_token,
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
if refresh_token:
|
||||
self.tokens[refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": refresh_token,
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Clean up authorization code
|
||||
del self.codes[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=expires_in,
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[ExternalRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token_info = self.tokens.get(refresh_token)
|
||||
if token_info and token_info["client_id"] == client.client_id:
|
||||
return ExternalRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_info["scopes"],
|
||||
external_token=token_info.get("external_token"),
|
||||
)
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: ExternalRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange refresh token for new tokens."""
|
||||
# Exchange with external provider
|
||||
token_data = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token.external_token or refresh_token.token,
|
||||
"client_id": self.external_client_id,
|
||||
"client_secret": self.external_client_secret,
|
||||
}
|
||||
|
||||
response = await self.http_client.post(
|
||||
self.external_token_url,
|
||||
data=token_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
external_tokens = response.json()
|
||||
|
||||
# Update stored tokens
|
||||
new_access_token = external_tokens.get("access_token")
|
||||
new_refresh_token = external_tokens.get("refresh_token", refresh_token.token)
|
||||
expires_in = external_tokens.get("expires_in", 3600)
|
||||
|
||||
self.tokens[new_access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": new_access_token,
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
if new_refresh_token != refresh_token.token:
|
||||
self.tokens[new_refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"external_token": new_refresh_token,
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
del self.tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=expires_in,
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(scopes or refresh_token.scopes),
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[ExternalAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
token_info = self.tokens.get(token)
|
||||
if token_info:
|
||||
return ExternalAccessToken(
|
||||
token=token,
|
||||
client_id=token_info["client_id"],
|
||||
scopes=token_info["scopes"],
|
||||
external_token=token_info.get("external_token"),
|
||||
)
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: ExternalAccessToken | ExternalRefreshToken) -> None:
|
||||
"""Revoke a token."""
|
||||
self.tokens.pop(token.token, None)
|
||||
|
||||
|
||||
def create_github_provider() -> ExternalOAuthProvider:
|
||||
"""Create an OAuth provider for GitHub integration."""
|
||||
return ExternalOAuthProvider(
|
||||
issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
|
||||
external_provider="github",
|
||||
external_client_id=os.getenv("GITHUB_CLIENT_ID", ""),
|
||||
external_client_secret=os.getenv("GITHUB_CLIENT_SECRET", ""),
|
||||
external_authorize_url="https://github.com/login/oauth/authorize",
|
||||
external_token_url="https://github.com/login/oauth/access_token",
|
||||
external_userinfo_url="https://api.github.com/user",
|
||||
)
|
||||
|
||||
|
||||
def create_google_provider() -> ExternalOAuthProvider:
|
||||
"""Create an OAuth provider for Google integration."""
|
||||
return ExternalOAuthProvider(
|
||||
issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
|
||||
external_provider="google",
|
||||
external_client_id=os.getenv("GOOGLE_CLIENT_ID", ""),
|
||||
external_client_secret=os.getenv("GOOGLE_CLIENT_SECRET", ""),
|
||||
external_authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
||||
external_token_url="https://oauth2.googleapis.com/token",
|
||||
external_userinfo_url="https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
)
|
||||
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import ProjectConfig, get_project_config, config_manager
|
||||
from basic_memory.config import ProjectConfig, get_project_config, ConfigManager
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,7 +23,7 @@ class ProjectSession:
|
||||
current_project: Optional[str] = None
|
||||
default_project: Optional[str] = None
|
||||
|
||||
def initialize(self, default_project: str) -> None:
|
||||
def initialize(self, default_project: str) -> "ProjectSession":
|
||||
"""Set the default project from config on startup.
|
||||
|
||||
Args:
|
||||
@@ -32,6 +32,7 @@ class ProjectSession:
|
||||
self.default_project = default_project
|
||||
self.current_project = default_project
|
||||
logger.info(f"Initialized project session with default project: {default_project}")
|
||||
return self
|
||||
|
||||
def get_current_project(self) -> str:
|
||||
"""Get the currently active project name.
|
||||
@@ -72,7 +73,7 @@ class ProjectSession:
|
||||
via CLI or API to ensure MCP session stays in sync.
|
||||
"""
|
||||
# Reload config to get latest default project
|
||||
current_config = config_manager.load_config()
|
||||
current_config = ConfigManager().config
|
||||
new_default = current_config.default_project
|
||||
|
||||
# Reinitialize with new default
|
||||
@@ -102,7 +103,8 @@ def get_active_project(project_override: Optional[str] = None) -> ProjectConfig:
|
||||
return project
|
||||
|
||||
current_project = session.get_current_project()
|
||||
return get_project_config(current_project)
|
||||
active_project = get_project_config(current_project)
|
||||
return active_project
|
||||
|
||||
|
||||
def add_project_metadata(result: str, project_name: str) -> str:
|
||||
|
||||
@@ -10,12 +10,10 @@ from basic_memory.mcp.prompts import continue_conversation
|
||||
from basic_memory.mcp.prompts import recent_activity
|
||||
from basic_memory.mcp.prompts import search
|
||||
from basic_memory.mcp.prompts import ai_assistant_guide
|
||||
from basic_memory.mcp.prompts import sync_status
|
||||
|
||||
__all__ = [
|
||||
"ai_assistant_guide",
|
||||
"continue_conversation",
|
||||
"recent_activity",
|
||||
"search",
|
||||
"sync_status",
|
||||
]
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"""Sync status prompt for Basic Memory MCP server."""
|
||||
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@mcp.prompt(
|
||||
description="""Get sync status with recommendations for AI assistants.
|
||||
|
||||
This prompt provides both current sync status and guidance on how
|
||||
AI assistants should respond when sync operations are in progress or completed.
|
||||
""",
|
||||
)
|
||||
async def sync_status_prompt() -> str:
|
||||
"""Get sync status with AI assistant guidance.
|
||||
Returns:
|
||||
Formatted sync status with AI assistant guidance
|
||||
"""
|
||||
try: # pragma: no cover
|
||||
from basic_memory.services.migration_service import migration_manager
|
||||
|
||||
state = migration_manager.state
|
||||
|
||||
# Build status report
|
||||
lines = [
|
||||
"# Basic Memory Sync Status",
|
||||
"",
|
||||
f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
|
||||
f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
|
||||
"",
|
||||
]
|
||||
|
||||
if migration_manager.is_ready:
|
||||
lines.extend(
|
||||
[
|
||||
"✅ **All sync operations completed** - System is fully operational",
|
||||
"",
|
||||
"All Basic Memory tools are available and functioning normally.",
|
||||
"File indexing is complete and knowledge graphs are up to date.",
|
||||
"You can proceed with any knowledge management tasks.",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.append(f"**Status Message**: {state.message}")
|
||||
|
||||
if state.status.value == "in_progress":
|
||||
if state.projects_total > 0:
|
||||
progress = f" ({state.projects_migrated}/{state.projects_total})"
|
||||
lines.append(f"**Progress**: {progress}")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
|
||||
"",
|
||||
"**Impact**: Some tools may show status messages instead of normal responses",
|
||||
"until sync completes (usually 1-3 minutes).",
|
||||
]
|
||||
)
|
||||
|
||||
elif state.status.value == "failed":
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"❌ **Sync failed**: {state.error or 'Unknown error'}",
|
||||
"",
|
||||
"**Impact**: System may have limited functionality until issue is resolved.",
|
||||
]
|
||||
)
|
||||
|
||||
# Add AI assistant recommendations
|
||||
if not migration_manager.is_ready:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## AI Assistant Recommendations",
|
||||
"",
|
||||
"**When sync is in progress:**",
|
||||
"- Inform the user about the background file processing",
|
||||
"- Suggest using `sync_status()` tool to check progress",
|
||||
"- Explain that tools will work normally once sync completes",
|
||||
"- Avoid creating complex workflows until sync is done",
|
||||
"",
|
||||
"**What to tell users:**",
|
||||
"- 'Basic Memory is processing your files and building knowledge graphs'",
|
||||
"- 'This usually takes 1-3 minutes depending on your content size'",
|
||||
"- 'You can check progress anytime with the sync_status tool'",
|
||||
"- 'Full functionality will be available once processing completes'",
|
||||
"",
|
||||
"**User-friendly language:**",
|
||||
"- Say 'processing files' instead of 'migration' or 'sync'",
|
||||
"- Say 'building knowledge graphs' instead of 'indexing'",
|
||||
"- Say 'setting up your knowledge base' instead of 'running migrations'",
|
||||
]
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
return f"""# Sync Status - Error
|
||||
|
||||
❌ **Unable to check sync status**: {str(e)}
|
||||
|
||||
## AI Assistant Recommendations
|
||||
|
||||
**When status is unavailable:**
|
||||
- Assume the system is likely working normally
|
||||
- Try proceeding with normal operations
|
||||
- If users report issues, suggest checking logs or restarting
|
||||
- Use user-friendly language about 'setting up the knowledge base'
|
||||
"""
|
||||
@@ -7,25 +7,10 @@ from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncIterator, Optional, Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.logging import configure_logging as mcp_configure_logging
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.services.initialization import initialize_app
|
||||
from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
|
||||
from basic_memory.mcp.project_session import session
|
||||
from basic_memory.mcp.external_auth_provider import (
|
||||
create_github_provider,
|
||||
create_google_provider,
|
||||
)
|
||||
from basic_memory.mcp.supabase_auth_provider import SupabaseOAuthProvider
|
||||
|
||||
# mcp console logging
|
||||
mcp_configure_logging(level="ERROR")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,7 +21,11 @@ class AppContext:
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
|
||||
"""Manage application lifecycle with type-safe context"""
|
||||
""" """
|
||||
# defer import so tests can monkeypatch
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Initialize on startup (now returns migration_manager)
|
||||
migration_manager = await initialize_app(app_config)
|
||||
|
||||
@@ -50,60 +39,8 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma:
|
||||
pass
|
||||
|
||||
|
||||
# OAuth configuration function
|
||||
def create_auth_config() -> tuple[AuthSettings | None, Any | None]:
|
||||
"""Create OAuth configuration if enabled."""
|
||||
# Check if OAuth is enabled via environment variable
|
||||
import os
|
||||
|
||||
if os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true":
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
# Configure OAuth settings
|
||||
issuer_url = os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000")
|
||||
required_scopes = os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES", "read,write")
|
||||
docs_url = os.getenv("FASTMCP_AUTH_DOCS_URL") or "http://localhost:8000/docs/oauth"
|
||||
|
||||
auth_settings = AuthSettings(
|
||||
issuer_url=AnyHttpUrl(issuer_url),
|
||||
service_documentation_url=AnyHttpUrl(docs_url),
|
||||
required_scopes=required_scopes.split(",") if required_scopes else ["read", "write"],
|
||||
)
|
||||
|
||||
# Create OAuth provider based on type
|
||||
provider_type = os.getenv("FASTMCP_AUTH_PROVIDER", "basic").lower()
|
||||
|
||||
if provider_type == "github":
|
||||
auth_provider = create_github_provider()
|
||||
elif provider_type == "google":
|
||||
auth_provider = create_google_provider()
|
||||
elif provider_type == "supabase":
|
||||
supabase_url = os.getenv("SUPABASE_URL")
|
||||
supabase_anon_key = os.getenv("SUPABASE_ANON_KEY")
|
||||
supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY")
|
||||
|
||||
if not supabase_url or not supabase_anon_key:
|
||||
raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set for Supabase auth")
|
||||
|
||||
auth_provider = SupabaseOAuthProvider(
|
||||
supabase_url=supabase_url,
|
||||
supabase_anon_key=supabase_anon_key,
|
||||
supabase_service_key=supabase_service_key,
|
||||
issuer_url=issuer_url,
|
||||
)
|
||||
else: # default to "basic"
|
||||
auth_provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
||||
|
||||
return auth_settings, auth_provider
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
# Create auth configuration
|
||||
auth_settings, auth_provider = create_auth_config()
|
||||
|
||||
# Create the shared server instance
|
||||
# Create the shared server instance with custom Stytch auth
|
||||
mcp = FastMCP(
|
||||
name="Basic Memory",
|
||||
auth=auth_provider,
|
||||
lifespan=app_lifespan,
|
||||
)
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
"""Supabase OAuth provider for Basic Memory MCP server."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
from loguru import logger
|
||||
from mcp.server.auth.provider import (
|
||||
OAuthAuthorizationServerProvider,
|
||||
AuthorizationParams,
|
||||
AuthorizationCode,
|
||||
RefreshToken,
|
||||
AccessToken,
|
||||
TokenError,
|
||||
AuthorizeError,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseAuthorizationCode(AuthorizationCode):
|
||||
"""Authorization code with Supabase metadata."""
|
||||
|
||||
user_id: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseRefreshToken(RefreshToken):
|
||||
"""Refresh token with Supabase metadata."""
|
||||
|
||||
supabase_refresh_token: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupabaseAccessToken(AccessToken):
|
||||
"""Access token with Supabase metadata."""
|
||||
|
||||
supabase_access_token: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class SupabaseOAuthProvider(
|
||||
OAuthAuthorizationServerProvider[
|
||||
SupabaseAuthorizationCode, SupabaseRefreshToken, SupabaseAccessToken
|
||||
]
|
||||
):
|
||||
"""OAuth provider that integrates with Supabase Auth.
|
||||
|
||||
This provider uses Supabase as the authentication backend while
|
||||
maintaining compatibility with MCP's OAuth requirements.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
supabase_url: str,
|
||||
supabase_anon_key: str,
|
||||
supabase_service_key: Optional[str] = None,
|
||||
issuer_url: str = "http://localhost:8000",
|
||||
):
|
||||
self.supabase_url = supabase_url.rstrip("/")
|
||||
self.supabase_anon_key = supabase_anon_key
|
||||
self.supabase_service_key = supabase_service_key or supabase_anon_key
|
||||
self.issuer_url = issuer_url
|
||||
|
||||
# HTTP client for Supabase API calls
|
||||
self.http_client = httpx.AsyncClient()
|
||||
|
||||
# Temporary storage for auth flows (in production, use Supabase DB)
|
||||
self.pending_auth_codes: Dict[str, SupabaseAuthorizationCode] = {}
|
||||
self.mcp_to_supabase_tokens: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
|
||||
"""Get a client from Supabase.
|
||||
|
||||
In production, this would query a clients table in Supabase.
|
||||
"""
|
||||
# For now, we'll validate against a configured list of allowed clients
|
||||
# In production, query Supabase DB for client info
|
||||
allowed_clients = os.getenv("SUPABASE_ALLOWED_CLIENTS", "").split(",")
|
||||
|
||||
if client_id in allowed_clients:
|
||||
return OAuthClientInformationFull(
|
||||
client_id=client_id,
|
||||
client_secret="", # Supabase handles secrets
|
||||
redirect_uris=[], # Supabase handles redirect URIs
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Register a new OAuth client in Supabase.
|
||||
|
||||
In production, this would insert into a clients table.
|
||||
"""
|
||||
# For development, we just log the registration
|
||||
logger.info(f"Would register client {client_info.client_id} in Supabase")
|
||||
|
||||
# In production:
|
||||
# await self.supabase.table('oauth_clients').insert({
|
||||
# 'client_id': client_info.client_id,
|
||||
# 'client_secret': client_info.client_secret,
|
||||
# 'metadata': client_info.client_metadata,
|
||||
# }).execute()
|
||||
|
||||
async def authorize(
|
||||
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
||||
) -> str:
|
||||
"""Create authorization URL redirecting to Supabase Auth.
|
||||
|
||||
This initiates the OAuth flow with Supabase as the identity provider.
|
||||
"""
|
||||
# Generate state for this auth request
|
||||
state = secrets.token_urlsafe(32)
|
||||
|
||||
# Store the authorization request
|
||||
self.pending_auth_codes[state] = SupabaseAuthorizationCode(
|
||||
code=state,
|
||||
scopes=params.scopes or [],
|
||||
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
|
||||
client_id=client.client_id,
|
||||
code_challenge=params.code_challenge,
|
||||
redirect_uri=params.redirect_uri,
|
||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||
)
|
||||
|
||||
# Build Supabase auth URL
|
||||
auth_params = {
|
||||
"redirect_to": f"{self.issuer_url}/auth/callback",
|
||||
"scopes": " ".join(params.scopes or ["openid", "email"]),
|
||||
"state": state,
|
||||
}
|
||||
|
||||
# Use Supabase's OAuth endpoint
|
||||
auth_url = f"{self.supabase_url}/auth/v1/authorize"
|
||||
query_string = "&".join(f"{k}={v}" for k, v in auth_params.items())
|
||||
|
||||
return f"{auth_url}?{query_string}"
|
||||
|
||||
async def handle_supabase_callback(self, code: str, state: str) -> str:
|
||||
"""Handle callback from Supabase after user authentication."""
|
||||
# Get the original auth request
|
||||
auth_request = self.pending_auth_codes.get(state)
|
||||
if not auth_request:
|
||||
raise AuthorizeError(
|
||||
error="invalid_request",
|
||||
error_description="Invalid state parameter",
|
||||
)
|
||||
|
||||
# Exchange code with Supabase for tokens
|
||||
token_response = await self.http_client.post(
|
||||
f"{self.supabase_url}/auth/v1/token",
|
||||
json={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": f"{self.issuer_url}/auth/callback",
|
||||
},
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {self.supabase_anon_key}",
|
||||
},
|
||||
)
|
||||
|
||||
if not token_response.is_success:
|
||||
raise AuthorizeError(
|
||||
error="server_error",
|
||||
error_description="Failed to exchange code with Supabase",
|
||||
)
|
||||
|
||||
supabase_tokens = token_response.json()
|
||||
|
||||
# Get user info from Supabase
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {supabase_tokens['access_token']}",
|
||||
},
|
||||
)
|
||||
|
||||
user_data = user_response.json() if user_response.is_success else {}
|
||||
|
||||
# Generate MCP authorization code
|
||||
mcp_code = secrets.token_urlsafe(32)
|
||||
|
||||
# Update auth request with user info
|
||||
auth_request.code = mcp_code
|
||||
auth_request.user_id = user_data.get("id")
|
||||
auth_request.email = user_data.get("email")
|
||||
|
||||
# Store mapping
|
||||
self.pending_auth_codes[mcp_code] = auth_request
|
||||
self.mcp_to_supabase_tokens[mcp_code] = {
|
||||
"supabase_tokens": supabase_tokens,
|
||||
"user": user_data,
|
||||
}
|
||||
|
||||
# Clean up old state
|
||||
del self.pending_auth_codes[state]
|
||||
|
||||
# Redirect back to client
|
||||
redirect_uri = str(auth_request.redirect_uri)
|
||||
separator = "&" if "?" in redirect_uri else "?"
|
||||
|
||||
return f"{redirect_uri}{separator}code={mcp_code}&state={state}"
|
||||
|
||||
async def load_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: str
|
||||
) -> Optional[SupabaseAuthorizationCode]:
|
||||
"""Load an authorization code."""
|
||||
code = self.pending_auth_codes.get(authorization_code)
|
||||
|
||||
if code and code.client_id == client.client_id:
|
||||
# Check expiration
|
||||
if datetime.utcnow().timestamp() > code.expires_at:
|
||||
del self.pending_auth_codes[authorization_code]
|
||||
return None
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_authorization_code(
|
||||
self, client: OAuthClientInformationFull, authorization_code: SupabaseAuthorizationCode
|
||||
) -> OAuthToken:
|
||||
"""Exchange authorization code for tokens."""
|
||||
# Get stored Supabase tokens
|
||||
token_data = self.mcp_to_supabase_tokens.get(authorization_code.code)
|
||||
if not token_data:
|
||||
raise TokenError(error="invalid_grant", error_description="Invalid authorization code")
|
||||
|
||||
supabase_tokens = token_data["supabase_tokens"]
|
||||
user = token_data["user"]
|
||||
|
||||
# Generate MCP tokens that wrap Supabase tokens
|
||||
access_token = self._generate_mcp_token(
|
||||
client_id=client.client_id,
|
||||
user_id=user.get("id", ""),
|
||||
email=user.get("email", ""),
|
||||
scopes=authorization_code.scopes,
|
||||
supabase_access_token=supabase_tokens["access_token"],
|
||||
)
|
||||
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Store the token mapping
|
||||
self.mcp_to_supabase_tokens[access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user.get("id"),
|
||||
"email": user.get("email"),
|
||||
"supabase_access_token": supabase_tokens["access_token"],
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Store refresh token mapping
|
||||
self.mcp_to_supabase_tokens[refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user.get("id"),
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": authorization_code.scopes,
|
||||
}
|
||||
|
||||
# Clean up authorization code
|
||||
del self.pending_auth_codes[authorization_code.code]
|
||||
del self.mcp_to_supabase_tokens[authorization_code.code]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=supabase_tokens.get("expires_in", 3600),
|
||||
refresh_token=refresh_token,
|
||||
scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
|
||||
)
|
||||
|
||||
async def load_refresh_token(
|
||||
self, client: OAuthClientInformationFull, refresh_token: str
|
||||
) -> Optional[SupabaseRefreshToken]:
|
||||
"""Load a refresh token."""
|
||||
token_data = self.mcp_to_supabase_tokens.get(refresh_token)
|
||||
|
||||
if token_data and token_data["client_id"] == client.client_id:
|
||||
return SupabaseRefreshToken(
|
||||
token=refresh_token,
|
||||
client_id=client.client_id,
|
||||
scopes=token_data["scopes"],
|
||||
supabase_refresh_token=token_data["supabase_refresh_token"],
|
||||
user_id=token_data.get("user_id"),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
refresh_token: SupabaseRefreshToken,
|
||||
scopes: list[str],
|
||||
) -> OAuthToken:
|
||||
"""Exchange refresh token for new tokens using Supabase."""
|
||||
# Refresh with Supabase
|
||||
token_response = await self.http_client.post(
|
||||
f"{self.supabase_url}/auth/v1/token",
|
||||
json={
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token.supabase_refresh_token,
|
||||
},
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {self.supabase_anon_key}",
|
||||
},
|
||||
)
|
||||
|
||||
if not token_response.is_success:
|
||||
raise TokenError(
|
||||
error="invalid_grant",
|
||||
error_description="Failed to refresh with Supabase",
|
||||
)
|
||||
|
||||
supabase_tokens = token_response.json()
|
||||
|
||||
# Get updated user info
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {supabase_tokens['access_token']}",
|
||||
},
|
||||
)
|
||||
|
||||
user_data = user_response.json() if user_response.is_success else {}
|
||||
|
||||
# Generate new MCP tokens
|
||||
new_access_token = self._generate_mcp_token(
|
||||
client_id=client.client_id,
|
||||
user_id=user_data.get("id", ""),
|
||||
email=user_data.get("email", ""),
|
||||
scopes=scopes or refresh_token.scopes,
|
||||
supabase_access_token=supabase_tokens["access_token"],
|
||||
)
|
||||
|
||||
new_refresh_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Update token mappings
|
||||
self.mcp_to_supabase_tokens[new_access_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user_data.get("id"),
|
||||
"email": user_data.get("email"),
|
||||
"supabase_access_token": supabase_tokens["access_token"],
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
self.mcp_to_supabase_tokens[new_refresh_token] = {
|
||||
"client_id": client.client_id,
|
||||
"user_id": user_data.get("id"),
|
||||
"supabase_refresh_token": supabase_tokens["refresh_token"],
|
||||
"scopes": scopes or refresh_token.scopes,
|
||||
}
|
||||
|
||||
# Clean up old tokens
|
||||
del self.mcp_to_supabase_tokens[refresh_token.token]
|
||||
|
||||
return OAuthToken(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=supabase_tokens.get("expires_in", 3600),
|
||||
refresh_token=new_refresh_token,
|
||||
scope=" ".join(scopes or refresh_token.scopes),
|
||||
)
|
||||
|
||||
async def load_access_token(self, token: str) -> Optional[SupabaseAccessToken]:
|
||||
"""Load and validate an access token."""
|
||||
# First check our mapping
|
||||
token_data = self.mcp_to_supabase_tokens.get(token)
|
||||
if token_data:
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id=token_data["client_id"],
|
||||
scopes=token_data["scopes"],
|
||||
supabase_access_token=token_data.get("supabase_access_token"),
|
||||
user_id=token_data.get("user_id"),
|
||||
email=token_data.get("email"),
|
||||
)
|
||||
|
||||
# Try to decode as JWT
|
||||
try:
|
||||
# Verify with Supabase's JWT secret
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
os.getenv("SUPABASE_JWT_SECRET", ""),
|
||||
algorithms=["HS256"],
|
||||
audience="authenticated",
|
||||
)
|
||||
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id=payload.get("client_id", ""),
|
||||
scopes=payload.get("scopes", []),
|
||||
user_id=payload.get("sub"),
|
||||
email=payload.get("email"),
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
pass
|
||||
|
||||
# Validate with Supabase
|
||||
user_response = await self.http_client.get(
|
||||
f"{self.supabase_url}/auth/v1/user",
|
||||
headers={
|
||||
"apikey": self.supabase_anon_key,
|
||||
"Authorization": f"Bearer {token}",
|
||||
},
|
||||
)
|
||||
|
||||
if user_response.is_success:
|
||||
user_data = user_response.json()
|
||||
return SupabaseAccessToken(
|
||||
token=token,
|
||||
client_id="", # Unknown client for direct Supabase tokens
|
||||
scopes=[],
|
||||
supabase_access_token=token,
|
||||
user_id=user_data.get("id"),
|
||||
email=user_data.get("email"),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def revoke_token(self, token: SupabaseAccessToken | SupabaseRefreshToken) -> None:
|
||||
"""Revoke a token."""
|
||||
# Remove from our mapping
|
||||
self.mcp_to_supabase_tokens.pop(token.token, None)
|
||||
|
||||
# In production, also revoke in Supabase:
|
||||
# await self.supabase.auth.admin.sign_out(token.user_id)
|
||||
|
||||
def _generate_mcp_token(
|
||||
self,
|
||||
client_id: str,
|
||||
user_id: str,
|
||||
email: str,
|
||||
scopes: list[str],
|
||||
supabase_access_token: str,
|
||||
) -> str:
|
||||
"""Generate an MCP token that wraps Supabase authentication."""
|
||||
payload = {
|
||||
"iss": self.issuer_url,
|
||||
"sub": user_id,
|
||||
"client_id": client_id,
|
||||
"email": email,
|
||||
"scopes": scopes,
|
||||
"supabase_token": supabase_access_token[:10] + "...", # Reference only
|
||||
"exp": datetime.utcnow() + timedelta(hours=1),
|
||||
"iat": datetime.utcnow(),
|
||||
}
|
||||
|
||||
# Use Supabase JWT secret if available
|
||||
secret = os.getenv("SUPABASE_JWT_SECRET", secrets.token_urlsafe(32))
|
||||
|
||||
return jwt.encode(payload, secret, algorithm="HS256")
|
||||
@@ -20,24 +20,24 @@ from basic_memory.mcp.tools.edit_note import edit_note
|
||||
from basic_memory.mcp.tools.move_note import move_note
|
||||
from basic_memory.mcp.tools.sync_status import sync_status
|
||||
from basic_memory.mcp.tools.project_management import (
|
||||
list_projects,
|
||||
list_memory_projects,
|
||||
switch_project,
|
||||
get_current_project,
|
||||
set_default_project,
|
||||
create_project,
|
||||
create_memory_project,
|
||||
delete_project,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"build_context",
|
||||
"canvas",
|
||||
"create_project",
|
||||
"create_memory_project",
|
||||
"delete_note",
|
||||
"delete_project",
|
||||
"edit_note",
|
||||
"get_current_project",
|
||||
"list_directory",
|
||||
"list_projects",
|
||||
"list_memory_projects",
|
||||
"move_note",
|
||||
"read_content",
|
||||
"read_note",
|
||||
|
||||
@@ -15,6 +15,7 @@ from basic_memory.schemas.memory import (
|
||||
memory_url_path,
|
||||
)
|
||||
|
||||
type StringOrInt = str | int
|
||||
|
||||
@mcp.tool(
|
||||
description="""Build context from a memory:// URI to continue conversations naturally.
|
||||
@@ -35,7 +36,7 @@ from basic_memory.schemas.memory import (
|
||||
)
|
||||
async def build_context(
|
||||
url: MemoryUrl,
|
||||
depth: Optional[int] = 1,
|
||||
depth: Optional[StringOrInt] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
@@ -80,6 +81,15 @@ async def build_context(
|
||||
build_context("memory://specs/search", project="work-project")
|
||||
"""
|
||||
logger.info(f"Building context from {url}")
|
||||
|
||||
# Convert string depth to integer if needed
|
||||
if isinstance(depth, str):
|
||||
try:
|
||||
depth = int(depth)
|
||||
except ValueError:
|
||||
from mcp.server.fastmcp.exceptions import ToolError
|
||||
raise ToolError(f"Invalid depth parameter: '{depth}' is not a valid integer")
|
||||
|
||||
# URL is already validated and normalized by MemoryUrl type annotation
|
||||
|
||||
# Get the active project first to check project-specific sync status
|
||||
@@ -101,7 +111,7 @@ async def build_context(
|
||||
metadata=MemoryMetadata(
|
||||
depth=depth or 1,
|
||||
timeframe=timeframe,
|
||||
generated_at=datetime.now(),
|
||||
generated_at=datetime.now().astimezone(),
|
||||
primary_count=0,
|
||||
related_count=0,
|
||||
uri=migration_status, # Include status in metadata
|
||||
|
||||
@@ -11,6 +11,7 @@ from basic_memory.mcp.tools.utils import call_post, call_get
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.schemas import EntityResponse
|
||||
from basic_memory.schemas.project_info import ProjectList
|
||||
from basic_memory.utils import validate_project_path
|
||||
|
||||
|
||||
async def _detect_cross_project_move_attempt(
|
||||
@@ -47,18 +48,7 @@ async def _detect_cross_project_move_attempt(
|
||||
identifier, destination_path, current_project, matching_project
|
||||
)
|
||||
|
||||
# Check if the destination path looks like it might be trying to reference another project
|
||||
# (e.g., contains common project-like patterns)
|
||||
if any(keyword in dest_lower for keyword in ["project", "workspace", "repo"]):
|
||||
# This might be a cross-project attempt, but we can't be sure
|
||||
# Return a general guidance message
|
||||
available_projects = [
|
||||
p.name for p in project_list.projects if p.name != current_project
|
||||
]
|
||||
if available_projects:
|
||||
return _format_potential_cross_project_guidance(
|
||||
identifier, destination_path, current_project, available_projects
|
||||
)
|
||||
# No other cross-project patterns detected
|
||||
|
||||
except Exception as e:
|
||||
# If we can't detect, don't interfere with normal error handling
|
||||
@@ -404,6 +394,28 @@ async def move_note(
|
||||
active_project = get_active_project(project)
|
||||
project_url = active_project.project_url
|
||||
|
||||
# Validate destination path to prevent path traversal attacks
|
||||
project_path = active_project.home
|
||||
if not validate_project_path(destination_path, project_path):
|
||||
logger.warning(
|
||||
"Attempted path traversal attack blocked",
|
||||
destination_path=destination_path,
|
||||
project=active_project.name,
|
||||
)
|
||||
return f"""# Move Failed - Security Validation Error
|
||||
|
||||
The destination path '{destination_path}' is not allowed - paths must stay within project boundaries.
|
||||
|
||||
## Valid path examples:
|
||||
- `notes/my-file.md`
|
||||
- `projects/2025/meeting-notes.md`
|
||||
- `archive/old-notes.md`
|
||||
|
||||
## Try again with a safe path:
|
||||
```
|
||||
move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
|
||||
```"""
|
||||
|
||||
# Check for potential cross-project move attempts
|
||||
cross_project_error = await _detect_cross_project_move_attempt(
|
||||
identifier, destination_path, active_project.name
|
||||
|
||||
@@ -5,6 +5,7 @@ and manage project context during conversations.
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from loguru import logger
|
||||
@@ -19,7 +20,9 @@ from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@mcp.tool("list_memory_projects")
|
||||
async def list_projects(ctx: Context | None = None) -> str:
|
||||
async def list_memory_projects(
|
||||
ctx: Context | None = None, _compatibility: Optional[str] = None
|
||||
) -> str:
|
||||
"""List all available projects with their status.
|
||||
|
||||
Shows all Basic Memory projects that are available, indicating which one
|
||||
@@ -29,7 +32,7 @@ async def list_projects(ctx: Context | None = None) -> str:
|
||||
Formatted list of projects with status indicators
|
||||
|
||||
Example:
|
||||
list_projects()
|
||||
list_memory_projects()
|
||||
"""
|
||||
if ctx: # pragma: no cover
|
||||
await ctx.info("Listing all available projects")
|
||||
@@ -144,13 +147,13 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
Your session remains on the previous project.
|
||||
|
||||
## Troubleshooting:
|
||||
1. **Check available projects**: Use `list_projects()` to see valid project names
|
||||
1. **Check available projects**: Use `list_memory_projects()` to see valid project names
|
||||
2. **Verify spelling**: Ensure the project name is spelled correctly
|
||||
3. **Check permissions**: Verify you have access to the requested project
|
||||
4. **Try again**: The error might be temporary
|
||||
|
||||
## Available options:
|
||||
- See all projects: `list_projects()`
|
||||
- See all projects: `list_memory_projects()`
|
||||
- Stay on current project: `get_current_project()`
|
||||
- Try different project: `switch_project("correct-project-name")`
|
||||
|
||||
@@ -159,7 +162,9 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_current_project(ctx: Context | None = None) -> str:
|
||||
async def get_current_project(
|
||||
ctx: Context | None = None, _compatibility: Optional[str] = None
|
||||
) -> str:
|
||||
"""Show the currently active project and basic stats.
|
||||
|
||||
Displays which project is currently active and provides basic information
|
||||
@@ -216,8 +221,10 @@ async def set_default_project(project_name: str, ctx: Context | None = None) ->
|
||||
if ctx: # pragma: no cover
|
||||
await ctx.info(f"Setting default project to: {project_name}")
|
||||
|
||||
# Call API to set default project
|
||||
response = await call_put(client, f"/projects/{project_name}/default")
|
||||
# Call API to set default project using URL encoding for special characters
|
||||
from urllib.parse import quote
|
||||
encoded_name = quote(project_name, safe='')
|
||||
response = await call_put(client, f"/projects/{encoded_name}/default")
|
||||
status_response = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
result = f"✓ {status_response.message}\n\n"
|
||||
@@ -231,7 +238,7 @@ async def set_default_project(project_name: str, ctx: Context | None = None) ->
|
||||
|
||||
|
||||
@mcp.tool("create_memory_project")
|
||||
async def create_project(
|
||||
async def create_memory_project(
|
||||
project_name: str, project_path: str, set_default: bool = False, ctx: Context | None = None
|
||||
) -> str:
|
||||
"""Create a new Basic Memory project.
|
||||
@@ -248,8 +255,8 @@ async def create_project(
|
||||
Confirmation message with project details
|
||||
|
||||
Example:
|
||||
create_project("my-research", "~/Documents/research")
|
||||
create_project("work-notes", "/home/user/work", set_default=True)
|
||||
create_memory_project("my-research", "~/Documents/research")
|
||||
create_memory_project("work-notes", "/home/user/work", set_default=True)
|
||||
"""
|
||||
if ctx: # pragma: no cover
|
||||
await ctx.info(f"Creating project: {project_name} at {project_path}")
|
||||
@@ -318,16 +325,29 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
|
||||
response = await call_get(client, "/projects/projects")
|
||||
project_list = ProjectList.model_validate(response.json())
|
||||
|
||||
# Check if project exists
|
||||
project_exists = any(p.name == project_name for p in project_list.projects)
|
||||
if not project_exists:
|
||||
# Find the project by name (case-insensitive) or permalink - same logic as switch_project
|
||||
project_permalink = generate_permalink(project_name)
|
||||
target_project = None
|
||||
for p in project_list.projects:
|
||||
# Match by permalink (handles case-insensitive input)
|
||||
if p.permalink == project_permalink:
|
||||
target_project = p
|
||||
break
|
||||
# Also match by name comparison (case-insensitive)
|
||||
if p.name.lower() == project_name.lower():
|
||||
target_project = p
|
||||
break
|
||||
|
||||
if not target_project:
|
||||
available_projects = [p.name for p in project_list.projects]
|
||||
raise ValueError(
|
||||
f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
||||
)
|
||||
|
||||
# Call API to delete project
|
||||
response = await call_delete(client, f"/projects/{project_name}")
|
||||
# Call API to delete project using URL encoding for special characters
|
||||
from urllib.parse import quote
|
||||
encoded_name = quote(target_project.name, safe='')
|
||||
response = await call_delete(client, f"/projects/{encoded_name}")
|
||||
status_response = ProjectStatusResponse.model_validate(response.json())
|
||||
|
||||
result = f"✓ {status_response.message}\n\n"
|
||||
|
||||
@@ -17,6 +17,7 @@ from basic_memory.mcp.async_client import client
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.schemas.memory import memory_url_path
|
||||
from basic_memory.utils import validate_project_path
|
||||
|
||||
|
||||
def calculate_target_params(content_length):
|
||||
@@ -188,6 +189,21 @@ async def read_content(path: str, project: Optional[str] = None) -> dict:
|
||||
project_url = active_project.project_url
|
||||
|
||||
url = memory_url_path(path)
|
||||
|
||||
# Validate path to prevent path traversal attacks
|
||||
project_path = active_project.home
|
||||
if not validate_project_path(url, project_path):
|
||||
logger.warning(
|
||||
"Attempted path traversal attack blocked",
|
||||
path=path,
|
||||
url=url,
|
||||
project=active_project.name,
|
||||
)
|
||||
return {
|
||||
"type": "error",
|
||||
"error": f"Path '{path}' is not allowed - paths must stay within project boundaries",
|
||||
}
|
||||
|
||||
response = await call_get(client, f"{project_url}/resource/{url}")
|
||||
content_type = response.headers.get("content-type", "application/octet-stream")
|
||||
content_length = int(response.headers.get("content-length", 0))
|
||||
|
||||
@@ -11,6 +11,7 @@ from basic_memory.mcp.tools.search import search_notes
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.schemas.memory import memory_url_path
|
||||
from basic_memory.utils import validate_project_path
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
@@ -55,6 +56,20 @@ async def read_note(
|
||||
# Get the active project first to check project-specific sync status
|
||||
active_project = get_active_project(project)
|
||||
|
||||
# Validate identifier to prevent path traversal attacks
|
||||
# We need to check both the raw identifier and the processed path
|
||||
processed_path = memory_url_path(identifier)
|
||||
project_path = active_project.home
|
||||
|
||||
if not validate_project_path(identifier, project_path) or not validate_project_path(processed_path, project_path):
|
||||
logger.warning(
|
||||
"Attempted path traversal attack blocked",
|
||||
identifier=identifier,
|
||||
processed_path=processed_path,
|
||||
project=active_project.name,
|
||||
)
|
||||
return f"# Error\n\nIdentifier '{identifier}' is not allowed - paths must stay within project boundaries"
|
||||
|
||||
# Check migration status and wait briefly if needed
|
||||
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
||||
|
||||
@@ -124,7 +139,7 @@ def format_not_found_message(identifier: str) -> str:
|
||||
return dedent(f"""
|
||||
# Note Not Found: "{identifier}"
|
||||
|
||||
I searched for "{identifier}" using multiple methods (direct lookup, title search, and text search) but couldn't find any matching notes. Here are some suggestions:
|
||||
I couldn't find any notes matching "{identifier}". Here are some suggestions:
|
||||
|
||||
## Check Identifier Type
|
||||
- If you provided a title, try using the exact permalink instead
|
||||
@@ -170,7 +185,7 @@ def format_related_results(identifier: str, results) -> str:
|
||||
message = dedent(f"""
|
||||
# Note Not Found: "{identifier}"
|
||||
|
||||
I searched for "{identifier}" using direct lookup and title search but couldn't find an exact match. However, I found some related notes through text search:
|
||||
I couldn't find an exact match for "{identifier}", but I found some related notes:
|
||||
|
||||
""")
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
||||
```
|
||||
|
||||
## Alternative search strategies:
|
||||
- Break into simpler terms: `search_notes("{' '.join(clean_query.split()[:2])}")`
|
||||
- Break into simpler terms: `search_notes("{" ".join(clean_query.split()[:2])}")`
|
||||
- Try different search types: `search_notes("{clean_query}", search_type="title")`
|
||||
- Use filtering: `search_notes("{clean_query}", types=["entity"])`
|
||||
""").strip()
|
||||
@@ -105,7 +105,7 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
||||
- **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
|
||||
|
||||
4. **Try boolean operators for broader results**:
|
||||
- OR search: `search_notes("{' OR '.join(query.split()[:3])}")`
|
||||
- OR search: `search_notes("{" OR ".join(query.split()[:3])}")`
|
||||
- Remove restrictive terms: Focus on the most important keywords
|
||||
|
||||
5. **Use filtering to narrow scope**:
|
||||
@@ -183,7 +183,7 @@ Error searching for '{query}': {error_message}
|
||||
- Permalink patterns: `search_notes("{query}*", search_type="permalink")`
|
||||
- **With filters**: `search_notes("{query}", types=["entity"])`
|
||||
- **Recent content**: `search_notes("{query}", after_date="1 week")`
|
||||
- **Boolean variations**: `search_notes("{' OR '.join(query.split()[:2])}")`
|
||||
- **Boolean variations**: `search_notes("{" OR ".join(query.split()[:2])}")`
|
||||
|
||||
## Explore your content:
|
||||
- **Browse files**: `list_directory("/")` - See all available content
|
||||
@@ -224,7 +224,7 @@ async def search_notes(
|
||||
- `search_notes("keyword")` - Find any content containing "keyword"
|
||||
- `search_notes("exact phrase")` - Search for exact phrase match
|
||||
|
||||
### Advanced Boolean Searches
|
||||
### Advanced Boolean Searches
|
||||
- `search_notes("term1 term2")` - Find content with both terms (implicit AND)
|
||||
- `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
|
||||
- `search_notes("term1 OR term2")` - Either term can be present
|
||||
|
||||
@@ -4,8 +4,10 @@ from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.services.sync_status_service import sync_status_tracker
|
||||
|
||||
|
||||
def _get_all_projects_status() -> list[str]:
|
||||
@@ -13,8 +15,7 @@ def _get_all_projects_status() -> list[str]:
|
||||
status_lines = []
|
||||
|
||||
try:
|
||||
from basic_memory.config import app_config
|
||||
from basic_memory.services.sync_status_service import sync_status_tracker
|
||||
app_config = ConfigManager().config
|
||||
|
||||
if app_config.projects:
|
||||
status_lines.extend(["", "---", "", "**All Projects Status:**"])
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.mcp.tools.utils import call_put
|
||||
from basic_memory.mcp.project_session import get_active_project
|
||||
from basic_memory.schemas import EntityResponse
|
||||
from basic_memory.schemas.base import Entity
|
||||
from basic_memory.utils import parse_tags
|
||||
from basic_memory.utils import parse_tags, validate_project_path
|
||||
|
||||
# Define TagType as a Union that can accept either a string or a list of strings or None
|
||||
TagType = Union[List[str], str, None]
|
||||
@@ -75,6 +75,14 @@ async def write_note(
|
||||
# Get the active project first to check project-specific sync status
|
||||
active_project = get_active_project(project)
|
||||
|
||||
# Validate folder path to prevent path traversal attacks
|
||||
project_path = active_project.home
|
||||
if folder and not validate_project_path(folder, project_path):
|
||||
logger.warning(
|
||||
"Attempted path traversal attack blocked", folder=folder, project=active_project.name
|
||||
)
|
||||
return f"# Error\n\nFolder path '{folder}' is not allowed - paths must stay within project boundaries"
|
||||
|
||||
# Check migration status and wait briefly if needed
|
||||
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Knowledge graph models."""
|
||||
|
||||
from datetime import datetime
|
||||
from basic_memory.utils import ensure_timezone_aware
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -73,8 +74,8 @@ class Entity(Base):
|
||||
checksum: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
||||
|
||||
# Metadata and tracking
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now().astimezone())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now().astimezone(), onupdate=lambda: datetime.now().astimezone())
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", back_populates="entities")
|
||||
@@ -103,6 +104,16 @@ class Entity(Base):
|
||||
def is_markdown(self):
|
||||
"""Check if the entity is a markdown file."""
|
||||
return self.content_type == "text/markdown"
|
||||
|
||||
def __getattribute__(self, name):
|
||||
"""Override attribute access to ensure datetime fields are timezone-aware."""
|
||||
value = super().__getattribute__(name)
|
||||
|
||||
# Ensure datetime fields are timezone-aware
|
||||
if name in ('created_at', 'updated_at') and isinstance(value, datetime):
|
||||
return ensure_timezone_aware(value)
|
||||
|
||||
return value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Entity(id={self.id}, name='{self.title}', type='{self.entity_type}'"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Project model for Basic Memory."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -52,9 +52,9 @@ class Project(Base):
|
||||
is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
|
||||
)
|
||||
|
||||
# Define relationships to entities, observations, and relations
|
||||
|
||||
@@ -57,7 +57,7 @@ class EntityRepository(Repository[Entity]):
|
||||
"""
|
||||
query = (
|
||||
self.select()
|
||||
.where(Entity.file_path == str(file_path))
|
||||
.where(Entity.file_path == Path(file_path).as_posix())
|
||||
.options(*self.get_load_options())
|
||||
)
|
||||
return await self.find_one(query)
|
||||
@@ -68,7 +68,7 @@ class EntityRepository(Repository[Entity]):
|
||||
Args:
|
||||
file_path: Path to the entity file (will be converted to string internally)
|
||||
"""
|
||||
return await self.delete_by_fields(file_path=str(file_path))
|
||||
return await self.delete_by_fields(file_path=Path(file_path).as_posix())
|
||||
|
||||
def get_load_options(self) -> List[LoaderOption]:
|
||||
"""Get SQLAlchemy loader options for eager loading relationships."""
|
||||
|
||||
@@ -46,7 +46,7 @@ class ProjectRepository(Repository[Project]):
|
||||
Args:
|
||||
path: Path to the project directory (will be converted to string internally)
|
||||
"""
|
||||
query = self.select().where(Project.path == str(path))
|
||||
query = self.select().where(Project.path == Path(path).as_posix())
|
||||
return await self.find_one(query)
|
||||
|
||||
async def get_default_project(self) -> Optional[Project]:
|
||||
@@ -83,3 +83,21 @@ class ProjectRepository(Repository[Project]):
|
||||
await session.flush()
|
||||
return target_project
|
||||
return None # pragma: no cover
|
||||
|
||||
async def update_path(self, project_id: int, new_path: str) -> Optional[Project]:
|
||||
"""Update project path.
|
||||
|
||||
Args:
|
||||
project_id: ID of the project to update
|
||||
new_path: New filesystem path for the project
|
||||
|
||||
Returns:
|
||||
The updated project if found, None otherwise
|
||||
"""
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
project = await self.select_by_id(session, project_id)
|
||||
if project:
|
||||
project.path = new_path
|
||||
await session.flush()
|
||||
return project
|
||||
return None
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy import Executable, Result, text
|
||||
@@ -59,8 +60,11 @@ class SearchIndexRow:
|
||||
if not self.type == SearchItemType.ENTITY.value and not self.file_path:
|
||||
return ""
|
||||
|
||||
# Normalize path separators to handle both Windows (\) and Unix (/) paths
|
||||
normalized_path = Path(self.file_path).as_posix()
|
||||
|
||||
# Split the path by slashes
|
||||
parts = self.file_path.split("/")
|
||||
parts = normalized_path.split("/")
|
||||
|
||||
# If there's only one part (e.g., "README.md"), it's at the root
|
||||
if len(parts) <= 1:
|
||||
@@ -523,8 +527,8 @@ class SearchRepository:
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
# Delete existing record if any
|
||||
await session.execute(
|
||||
text("DELETE FROM search_index WHERE permalink = :permalink"),
|
||||
{"permalink": search_index_row.permalink},
|
||||
text("DELETE FROM search_index WHERE permalink = :permalink AND project_id = :project_id"),
|
||||
{"permalink": search_index_row.permalink, "project_id": self.project_id},
|
||||
)
|
||||
|
||||
# Prepare data for insert with project_id
|
||||
|
||||
@@ -22,6 +22,8 @@ from dateparser import parse
|
||||
|
||||
from pydantic import BaseModel, BeforeValidator, Field, model_validator
|
||||
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.file_utils import sanitize_for_filename
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@@ -53,22 +55,28 @@ def parse_timeframe(timeframe: str) -> datetime:
|
||||
timeframe: Natural language timeframe like 'today', '1d', '1 week ago', etc.
|
||||
|
||||
Returns:
|
||||
datetime: The parsed datetime for the start of the timeframe
|
||||
datetime: The parsed datetime for the start of the timeframe, timezone-aware in local system timezone
|
||||
|
||||
Examples:
|
||||
parse_timeframe('today') -> 2025-06-05 00:00:00 (start of today)
|
||||
parse_timeframe('1d') -> 2025-06-04 14:50:00 (24 hours ago)
|
||||
parse_timeframe('1 week ago') -> 2025-05-29 14:50:00 (1 week ago)
|
||||
parse_timeframe('today') -> 2025-06-05 00:00:00-07:00 (start of today with local timezone)
|
||||
parse_timeframe('1d') -> 2025-06-04 14:50:00-07:00 (24 hours ago with local timezone)
|
||||
parse_timeframe('1 week ago') -> 2025-05-29 14:50:00-07:00 (1 week ago with local timezone)
|
||||
"""
|
||||
if timeframe.lower() == "today":
|
||||
# Return start of today (00:00:00)
|
||||
return datetime.combine(datetime.now().date(), time.min)
|
||||
# Return start of today (00:00:00) in local timezone
|
||||
naive_dt = datetime.combine(datetime.now().date(), time.min)
|
||||
return naive_dt.astimezone()
|
||||
else:
|
||||
# Use dateparser for other formats
|
||||
parsed = parse(timeframe)
|
||||
if not parsed:
|
||||
raise ValueError(f"Could not parse timeframe: {timeframe}")
|
||||
return parsed
|
||||
|
||||
# If the parsed datetime is naive, make it timezone-aware in local system timezone
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.astimezone()
|
||||
else:
|
||||
return parsed
|
||||
|
||||
|
||||
def validate_timeframe(timeframe: str) -> str:
|
||||
@@ -85,7 +93,7 @@ def validate_timeframe(timeframe: str) -> str:
|
||||
parsed = parse_timeframe(timeframe)
|
||||
|
||||
# Convert to duration
|
||||
now = datetime.now()
|
||||
now = datetime.now().astimezone()
|
||||
if parsed > now:
|
||||
raise ValueError("Timeframe cannot be in the future")
|
||||
|
||||
@@ -184,13 +192,35 @@ class Entity(BaseModel):
|
||||
default="text/markdown",
|
||||
)
|
||||
|
||||
@property
|
||||
def safe_title(self) -> str:
|
||||
"""
|
||||
A sanitized version of the title, which is safe for use on the filesystem. For example,
|
||||
a title of "Coupon Enable/Disable Feature" should create a the file as "Coupon Enable-Disable Feature.md"
|
||||
instead of creating a file named "Disable Feature.md" beneath the "Coupon Enable" directory.
|
||||
|
||||
Replaces POSIX and/or Windows style slashes as well as a few other characters that are not safe for filenames.
|
||||
If kebab_filenames is True, then behavior is consistent with transformation used when generating permalink
|
||||
strings (e.g. "Coupon Enable/Disable Feature" -> "coupon-enable-disable-feature").
|
||||
"""
|
||||
fixed_title = sanitize_for_filename(self.title)
|
||||
|
||||
app_config = ConfigManager().config
|
||||
use_kebab_case = app_config.kebab_filenames
|
||||
|
||||
if use_kebab_case:
|
||||
fixed_title = generate_permalink(file_path=fixed_title, split_extension=False)
|
||||
|
||||
return fixed_title
|
||||
|
||||
@property
|
||||
def file_path(self):
|
||||
"""Get the file path for this entity based on its permalink."""
|
||||
safe_title = self.safe_title
|
||||
if self.content_type == "text/markdown":
|
||||
return f"{self.folder}/{self.title}.md" if self.folder else f"{self.title}.md"
|
||||
return f"{self.folder}/{safe_title}.md" if self.folder else f"{safe_title}.md"
|
||||
else:
|
||||
return f"{self.folder}/{self.title}" if self.folder else self.title
|
||||
return f"{self.folder}/{safe_title}" if self.folder else safe_title
|
||||
|
||||
@property
|
||||
def permalink(self) -> Permalink:
|
||||
|
||||
@@ -32,3 +32,4 @@ class EntityImportResult(ImportResult):
|
||||
|
||||
entities: int = 0
|
||||
relations: int = 0
|
||||
skipped_entities: int = 0
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Schemas for memory context."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Annotated, Sequence
|
||||
from typing import List, Optional, Annotated, Sequence, Literal, Union
|
||||
|
||||
from annotated_types import MinLen, MaxLen
|
||||
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
|
||||
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter, ConfigDict
|
||||
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
|
||||
@@ -117,8 +117,10 @@ def memory_url_path(url: memory_url) -> str: # pyright: ignore
|
||||
|
||||
class EntitySummary(BaseModel):
|
||||
"""Simplified entity representation."""
|
||||
|
||||
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
|
||||
|
||||
type: str = "entity"
|
||||
type: Literal["entity"] = "entity"
|
||||
permalink: Optional[str]
|
||||
title: str
|
||||
content: Optional[str] = None
|
||||
@@ -128,8 +130,10 @@ class EntitySummary(BaseModel):
|
||||
|
||||
class RelationSummary(BaseModel):
|
||||
"""Simplified relation representation."""
|
||||
|
||||
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
|
||||
|
||||
type: str = "relation"
|
||||
type: Literal["relation"] = "relation"
|
||||
title: str
|
||||
file_path: str
|
||||
permalink: str
|
||||
@@ -141,8 +145,10 @@ class RelationSummary(BaseModel):
|
||||
|
||||
class ObservationSummary(BaseModel):
|
||||
"""Simplified observation representation."""
|
||||
|
||||
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
|
||||
|
||||
type: str = "observation"
|
||||
type: Literal["observation"] = "observation"
|
||||
title: str
|
||||
file_path: str
|
||||
permalink: str
|
||||
@@ -153,6 +159,8 @@ class ObservationSummary(BaseModel):
|
||||
|
||||
class MemoryMetadata(BaseModel):
|
||||
"""Simplified response metadata."""
|
||||
|
||||
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
|
||||
|
||||
uri: Optional[str] = None
|
||||
types: Optional[List[SearchItemType]] = None
|
||||
@@ -169,17 +177,21 @@ class MemoryMetadata(BaseModel):
|
||||
class ContextResult(BaseModel):
|
||||
"""Context result containing a primary item with its observations and related items."""
|
||||
|
||||
primary_result: EntitySummary | RelationSummary | ObservationSummary = Field(
|
||||
description="Primary item"
|
||||
)
|
||||
primary_result: Annotated[
|
||||
Union[EntitySummary, RelationSummary, ObservationSummary],
|
||||
Field(discriminator="type", description="Primary item")
|
||||
]
|
||||
|
||||
observations: Sequence[ObservationSummary] = Field(
|
||||
description="Observations belonging to this entity", default_factory=list
|
||||
)
|
||||
|
||||
related_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
|
||||
description="Related items", default_factory=list
|
||||
)
|
||||
related_results: Sequence[
|
||||
Annotated[
|
||||
Union[EntitySummary, RelationSummary, ObservationSummary],
|
||||
Field(discriminator="type")
|
||||
]
|
||||
] = Field(description="Related items", default_factory=list)
|
||||
|
||||
|
||||
class GraphContext(BaseModel):
|
||||
|
||||
@@ -245,8 +245,8 @@ class ContextService:
|
||||
# For compatibility with the old query, we still need this for filtering
|
||||
values = ", ".join([f"('{t}', {i})" for t, i in type_id_pairs])
|
||||
|
||||
# Parameters for bindings
|
||||
params = {"max_depth": max_depth, "max_results": max_results}
|
||||
# Parameters for bindings - include project_id for security filtering
|
||||
params = {"max_depth": max_depth, "max_results": max_results, "project_id": self.search_repository.project_id}
|
||||
|
||||
# Build date and timeframe filters conditionally based on since parameter
|
||||
if since:
|
||||
@@ -258,6 +258,10 @@ class ContextService:
|
||||
date_filter = ""
|
||||
relation_date_filter = ""
|
||||
timeframe_condition = ""
|
||||
|
||||
# Add project filtering for security - ensure all entities and relations belong to the same project
|
||||
project_filter = "AND e.project_id = :project_id"
|
||||
relation_project_filter = "AND e_from.project_id = :project_id"
|
||||
|
||||
# Use a CTE that operates directly on entity and relation tables
|
||||
# This avoids the overhead of the search_index virtual table
|
||||
@@ -284,6 +288,7 @@ class ContextService:
|
||||
FROM entity e
|
||||
WHERE e.id IN ({entity_id_values})
|
||||
{date_filter}
|
||||
{project_filter}
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -314,8 +319,12 @@ class ContextService:
|
||||
JOIN entity e_from ON (
|
||||
r.from_id = e_from.id
|
||||
{relation_date_filter}
|
||||
{relation_project_filter}
|
||||
)
|
||||
LEFT JOIN entity e_to ON (r.to_id = e_to.id)
|
||||
WHERE eg.depth < :max_depth
|
||||
-- Ensure to_entity (if exists) also belongs to same project
|
||||
AND (r.to_id IS NULL OR e_to.project_id = :project_id)
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -348,6 +357,7 @@ class ContextService:
|
||||
ELSE eg.from_id
|
||||
END
|
||||
{date_filter}
|
||||
{project_filter}
|
||||
)
|
||||
WHERE eg.depth < :max_depth
|
||||
-- Only include entities connected by relations within timeframe if specified
|
||||
|
||||
@@ -106,8 +106,15 @@ class DirectoryService:
|
||||
List of DirectoryNode objects matching the criteria
|
||||
"""
|
||||
# Normalize directory path
|
||||
# Strip ./ prefix if present (handles relative path notation)
|
||||
if dir_name.startswith("./"):
|
||||
dir_name = dir_name[2:] # Remove "./" prefix
|
||||
|
||||
# Ensure path starts with "/"
|
||||
if not dir_name.startswith("/"):
|
||||
dir_name = f"/{dir_name}"
|
||||
|
||||
# Remove trailing slashes except for root
|
||||
if dir_name != "/" and dir_name.endswith("/"):
|
||||
dir_name = dir_name.rstrip("/")
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ from loguru import logger
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig
|
||||
from basic_memory.file_utils import has_frontmatter, parse_frontmatter, remove_frontmatter
|
||||
from basic_memory.file_utils import has_frontmatter, parse_frontmatter, remove_frontmatter, dump_frontmatter
|
||||
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
|
||||
from basic_memory.models import Observation, Relation
|
||||
from basic_memory.models.knowledge import Entity
|
||||
from basic_memory.repository import ObservationRepository, RelationRepository
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
@@ -44,6 +45,39 @@ class EntityService(BaseService[EntityModel]):
|
||||
self.file_service = file_service
|
||||
self.link_resolver = link_resolver
|
||||
|
||||
async def detect_file_path_conflicts(self, file_path: str) -> List[Entity]:
|
||||
"""Detect potential file path conflicts for a given file path.
|
||||
|
||||
This checks for entities with similar file paths that might cause conflicts:
|
||||
- Case sensitivity differences (Finance/file.md vs finance/file.md)
|
||||
- Character encoding differences
|
||||
- Hyphen vs space differences
|
||||
- Unicode normalization differences
|
||||
|
||||
Args:
|
||||
file_path: The file path to check for conflicts
|
||||
|
||||
Returns:
|
||||
List of entities that might conflict with the given file path
|
||||
"""
|
||||
from basic_memory.utils import detect_potential_file_conflicts
|
||||
|
||||
conflicts = []
|
||||
|
||||
# Get all existing file paths
|
||||
all_entities = await self.repository.find_all()
|
||||
existing_paths = [entity.file_path for entity in all_entities]
|
||||
|
||||
# Use the enhanced conflict detection utility
|
||||
conflicting_paths = detect_potential_file_conflicts(file_path, existing_paths)
|
||||
|
||||
# Find the entities corresponding to conflicting paths
|
||||
for entity in all_entities:
|
||||
if entity.file_path in conflicting_paths:
|
||||
conflicts.append(entity)
|
||||
|
||||
return conflicts
|
||||
|
||||
async def resolve_permalink(
|
||||
self, file_path: Permalink | Path, markdown: Optional[EntityMarkdown] = None
|
||||
) -> str:
|
||||
@@ -54,18 +88,30 @@ class EntityService(BaseService[EntityModel]):
|
||||
2. If markdown has permalink but it's used by another file -> make unique
|
||||
3. For existing files, keep current permalink from db
|
||||
4. Generate new unique permalink from file path
|
||||
|
||||
Enhanced to detect and handle character-related conflicts.
|
||||
"""
|
||||
file_path_str = Path(file_path).as_posix()
|
||||
|
||||
# Check for potential file path conflicts before resolving permalink
|
||||
conflicts = await self.detect_file_path_conflicts(file_path_str)
|
||||
if conflicts:
|
||||
logger.warning(
|
||||
f"Detected potential file path conflicts for '{file_path_str}': "
|
||||
f"{[entity.file_path for entity in conflicts]}"
|
||||
)
|
||||
|
||||
# If markdown has explicit permalink, try to validate it
|
||||
if markdown and markdown.frontmatter.permalink:
|
||||
desired_permalink = markdown.frontmatter.permalink
|
||||
existing = await self.repository.get_by_permalink(desired_permalink)
|
||||
|
||||
# If no conflict or it's our own file, use as is
|
||||
if not existing or existing.file_path == str(file_path):
|
||||
if not existing or existing.file_path == file_path_str:
|
||||
return desired_permalink
|
||||
|
||||
# For existing files, try to find current permalink
|
||||
existing = await self.repository.get_by_file_path(str(file_path))
|
||||
existing = await self.repository.get_by_file_path(file_path_str)
|
||||
if existing:
|
||||
return existing.permalink
|
||||
|
||||
@@ -73,9 +119,9 @@ class EntityService(BaseService[EntityModel]):
|
||||
if markdown and markdown.frontmatter.permalink:
|
||||
desired_permalink = markdown.frontmatter.permalink
|
||||
else:
|
||||
desired_permalink = generate_permalink(file_path)
|
||||
desired_permalink = generate_permalink(file_path_str)
|
||||
|
||||
# Make unique if needed
|
||||
# Make unique if needed - enhanced to handle character conflicts
|
||||
permalink = desired_permalink
|
||||
suffix = 1
|
||||
while await self.repository.get_by_permalink(permalink):
|
||||
@@ -150,7 +196,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
post = await schema_to_markdown(schema)
|
||||
|
||||
# write file
|
||||
final_content = frontmatter.dumps(post, sort_keys=False)
|
||||
final_content = dump_frontmatter(post)
|
||||
checksum = await self.file_service.write_file(file_path, final_content)
|
||||
|
||||
# parse entity from file
|
||||
@@ -227,7 +273,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata)
|
||||
|
||||
# write file
|
||||
final_content = frontmatter.dumps(merged_post, sort_keys=False)
|
||||
final_content = dump_frontmatter(merged_post)
|
||||
checksum = await self.file_service.write_file(file_path, final_content)
|
||||
|
||||
# parse entity from file
|
||||
@@ -237,7 +283,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
entity = await self.update_entity_and_observations(file_path, entity_markdown)
|
||||
|
||||
# add relations
|
||||
await self.update_entity_relations(str(file_path), entity_markdown)
|
||||
await self.update_entity_relations(file_path.as_posix(), entity_markdown)
|
||||
|
||||
# Set final checksum to match file
|
||||
entity = await self.repository.update(entity.id, {"checksum": checksum})
|
||||
@@ -328,7 +374,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
"""
|
||||
logger.debug(f"Updating entity and observations: {file_path}")
|
||||
|
||||
db_entity = await self.repository.get_by_file_path(str(file_path))
|
||||
db_entity = await self.repository.get_by_file_path(file_path.as_posix())
|
||||
|
||||
# Clear observations for entity
|
||||
await self.observation_repository.delete_by_fields(entity_id=db_entity.id)
|
||||
@@ -452,7 +498,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
# Update entity and its relationships
|
||||
entity = await self.update_entity_and_observations(file_path, entity_markdown)
|
||||
await self.update_entity_relations(str(file_path), entity_markdown)
|
||||
await self.update_entity_relations(file_path.as_posix(), entity_markdown)
|
||||
|
||||
# Set final checksum to match file
|
||||
entity = await self.repository.update(entity.id, {"checksum": checksum})
|
||||
|
||||
@@ -5,14 +5,12 @@ to ensure consistent application startup across all entry points.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
from basic_memory.models import Project
|
||||
from basic_memory.repository import ProjectRepository
|
||||
|
||||
|
||||
@@ -70,63 +68,6 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
||||
logger.info("Continuing with initialization despite synchronization error")
|
||||
|
||||
|
||||
async def migrate_legacy_projects(app_config: BasicMemoryConfig):
|
||||
# Get database session - migrations handled centrally
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path,
|
||||
db_type=db.DatabaseType.FILESYSTEM,
|
||||
ensure_migrations=False,
|
||||
)
|
||||
logger.info("Migrating legacy projects...")
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
|
||||
# For each project in config.json, check if it has a .basic-memory dir
|
||||
for project_name, project_path in app_config.projects.items():
|
||||
legacy_dir = Path(project_path) / ".basic-memory"
|
||||
if not legacy_dir.exists():
|
||||
continue
|
||||
logger.info(f"Detected legacy project directory: {legacy_dir}")
|
||||
project = await project_repository.get_by_name(project_name)
|
||||
if not project: # pragma: no cover
|
||||
logger.error(f"Project {project_name} not found in database, skipping migration")
|
||||
continue
|
||||
|
||||
logger.info(f"Starting migration for project: {project_name} (id: {project.id})")
|
||||
await migrate_legacy_project_data(project, legacy_dir)
|
||||
logger.info(f"Completed migration for project: {project_name}")
|
||||
logger.info("Legacy projects successfully migrated")
|
||||
|
||||
|
||||
async def migrate_legacy_project_data(project: Project, legacy_dir: Path) -> bool:
|
||||
"""Check if project has legacy .basic-memory dir and migrate if needed.
|
||||
|
||||
Args:
|
||||
project: The project to check and potentially migrate
|
||||
|
||||
Returns:
|
||||
True if migration occurred, False otherwise
|
||||
"""
|
||||
|
||||
# avoid circular imports
|
||||
from basic_memory.cli.commands.sync import get_sync_service
|
||||
|
||||
sync_service = await get_sync_service(project)
|
||||
sync_dir = Path(project.path)
|
||||
|
||||
logger.info(f"Sync starting project: {project.name}")
|
||||
await sync_service.sync(sync_dir, project_name=project.name)
|
||||
logger.info(f"Sync completed successfully for project: {project.name}")
|
||||
|
||||
# After successful sync, remove the legacy directory
|
||||
try:
|
||||
logger.info(f"Removing legacy directory: {legacy_dir}")
|
||||
shutil.rmtree(legacy_dir)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing legacy directory: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def initialize_file_sync(
|
||||
app_config: BasicMemoryConfig,
|
||||
):
|
||||
@@ -186,16 +127,6 @@ async def initialize_file_sync(
|
||||
sync_status_tracker.fail_project_sync(project.name, str(e))
|
||||
# Continue with other projects even if one fails
|
||||
|
||||
# Mark migration complete if it was in progress
|
||||
try:
|
||||
from basic_memory.services.migration_service import migration_manager
|
||||
|
||||
if not migration_manager.is_ready: # pragma: no cover
|
||||
migration_manager.mark_completed("Migration completed with file sync")
|
||||
logger.info("Marked migration as completed after file sync")
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.warning(f"Could not update migration status: {e}")
|
||||
|
||||
# Then start the watch service in the background
|
||||
logger.info("Starting watch service for all projects")
|
||||
# run the watch service
|
||||
@@ -229,13 +160,7 @@ async def initialize_app(
|
||||
# Reconcile projects from config.json with projects table
|
||||
await reconcile_projects_with_config(app_config)
|
||||
|
||||
# Start background migration for legacy project data (non-blocking)
|
||||
from basic_memory.services.migration_service import migration_manager
|
||||
|
||||
await migration_manager.start_background_migration(app_config)
|
||||
|
||||
logger.info("App initialization completed (migration running in background if needed)")
|
||||
return migration_manager
|
||||
|
||||
|
||||
def ensure_initialization(app_config: BasicMemoryConfig) -> None:
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
"""Migration service for handling background migrations and status tracking."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
|
||||
|
||||
class MigrationStatus(Enum):
|
||||
"""Status of migration operations."""
|
||||
|
||||
NOT_NEEDED = "not_needed"
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MigrationState:
|
||||
"""Current state of migration operations."""
|
||||
|
||||
status: MigrationStatus
|
||||
message: str
|
||||
progress: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
projects_migrated: int = 0
|
||||
projects_total: int = 0
|
||||
|
||||
|
||||
class MigrationManager:
|
||||
"""Manages background migration operations and status tracking."""
|
||||
|
||||
def __init__(self):
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.NOT_NEEDED, message="No migration required"
|
||||
)
|
||||
self._migration_task: Optional[asyncio.Task] = None
|
||||
|
||||
@property
|
||||
def state(self) -> MigrationState:
|
||||
"""Get current migration state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if the system is ready for normal operations."""
|
||||
return self._state.status in (MigrationStatus.NOT_NEEDED, MigrationStatus.COMPLETED)
|
||||
|
||||
@property
|
||||
def status_message(self) -> str:
|
||||
"""Get a user-friendly status message."""
|
||||
if self._state.status == MigrationStatus.IN_PROGRESS:
|
||||
progress = (
|
||||
f" ({self._state.projects_migrated}/{self._state.projects_total})"
|
||||
if self._state.projects_total > 0
|
||||
else ""
|
||||
)
|
||||
return f"🔄 File sync in progress{progress}: {self._state.message}. Use sync_status() tool for details."
|
||||
elif self._state.status == MigrationStatus.FAILED:
|
||||
return f"❌ File sync failed: {self._state.error or 'Unknown error'}. Use sync_status() tool for details."
|
||||
elif self._state.status == MigrationStatus.COMPLETED:
|
||||
return "✅ File sync completed successfully"
|
||||
else:
|
||||
return "✅ System ready"
|
||||
|
||||
async def check_migration_needed(self, app_config: BasicMemoryConfig) -> bool:
|
||||
"""Check if migration is needed without performing it."""
|
||||
from basic_memory import db
|
||||
from basic_memory.repository import ProjectRepository
|
||||
|
||||
try:
|
||||
# Get database session
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
|
||||
# Check for legacy projects
|
||||
legacy_projects = []
|
||||
for project_name, project_path in app_config.projects.items():
|
||||
legacy_dir = Path(project_path) / ".basic-memory"
|
||||
if legacy_dir.exists():
|
||||
project = await project_repository.get_by_name(project_name)
|
||||
if project:
|
||||
legacy_projects.append(project)
|
||||
|
||||
if legacy_projects:
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.PENDING,
|
||||
message="Legacy projects detected",
|
||||
projects_total=len(legacy_projects),
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.NOT_NEEDED, message="No migration required"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking migration status: {e}")
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.FAILED, message="Migration check failed", error=str(e)
|
||||
)
|
||||
return False
|
||||
|
||||
async def start_background_migration(self, app_config: BasicMemoryConfig) -> None:
|
||||
"""Start migration in background if needed."""
|
||||
if not await self.check_migration_needed(app_config):
|
||||
return
|
||||
|
||||
if self._migration_task and not self._migration_task.done():
|
||||
logger.info("Migration already in progress")
|
||||
return
|
||||
|
||||
logger.info("Starting background migration")
|
||||
self._migration_task = asyncio.create_task(self._run_migration(app_config))
|
||||
|
||||
async def _run_migration(self, app_config: BasicMemoryConfig) -> None:
|
||||
"""Run the actual migration process."""
|
||||
try:
|
||||
self._state.status = MigrationStatus.IN_PROGRESS
|
||||
self._state.message = "Migrating legacy projects"
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from basic_memory.services.initialization import migrate_legacy_projects
|
||||
|
||||
# Run the migration
|
||||
await migrate_legacy_projects(app_config)
|
||||
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.COMPLETED, message="Migration completed successfully"
|
||||
)
|
||||
logger.info("Background migration completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Background migration failed: {e}")
|
||||
self._state = MigrationState(
|
||||
status=MigrationStatus.FAILED, message="Migration failed", error=str(e)
|
||||
)
|
||||
|
||||
async def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
|
||||
"""Wait for migration to complete."""
|
||||
if self.is_ready:
|
||||
return True
|
||||
|
||||
if not self._migration_task:
|
||||
return False
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self._migration_task, timeout=timeout)
|
||||
return self.is_ready
|
||||
except asyncio.TimeoutError:
|
||||
return False
|
||||
|
||||
def mark_completed(self, message: str = "Migration completed") -> None:
|
||||
"""Mark migration as completed externally."""
|
||||
self._state = MigrationState(status=MigrationStatus.COMPLETED, message=message)
|
||||
|
||||
|
||||
# Global migration manager instance
|
||||
migration_manager = MigrationManager()
|
||||
@@ -9,7 +9,6 @@ from typing import Dict, Optional, Sequence
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
|
||||
from basic_memory.config import config, app_config
|
||||
from basic_memory.models import Project
|
||||
from basic_memory.repository.project_repository import ProjectRepository
|
||||
from basic_memory.schemas import (
|
||||
@@ -18,9 +17,8 @@ from basic_memory.schemas import (
|
||||
ProjectStatistics,
|
||||
SystemStatus,
|
||||
)
|
||||
from basic_memory.config import WATCH_STATUS_JSON
|
||||
from basic_memory.config import WATCH_STATUS_JSON, ConfigManager, get_project_config, ProjectConfig
|
||||
from basic_memory.utils import generate_permalink
|
||||
from basic_memory.config import config_manager
|
||||
|
||||
|
||||
class ProjectService:
|
||||
@@ -33,6 +31,24 @@ class ProjectService:
|
||||
super().__init__()
|
||||
self.repository = repository
|
||||
|
||||
@property
|
||||
def config_manager(self) -> ConfigManager:
|
||||
"""Get a ConfigManager instance.
|
||||
|
||||
Returns:
|
||||
Fresh ConfigManager instance for each access
|
||||
"""
|
||||
return ConfigManager()
|
||||
|
||||
@property
|
||||
def config(self) -> ProjectConfig:
|
||||
"""Get the current project configuration.
|
||||
|
||||
Returns:
|
||||
Current project configuration
|
||||
"""
|
||||
return get_project_config()
|
||||
|
||||
@property
|
||||
def projects(self) -> Dict[str, str]:
|
||||
"""Get all configured projects.
|
||||
@@ -40,7 +56,7 @@ class ProjectService:
|
||||
Returns:
|
||||
Dict mapping project names to their file paths
|
||||
"""
|
||||
return config_manager.projects
|
||||
return self.config_manager.projects
|
||||
|
||||
@property
|
||||
def default_project(self) -> str:
|
||||
@@ -49,7 +65,7 @@ class ProjectService:
|
||||
Returns:
|
||||
The name of the default project
|
||||
"""
|
||||
return config_manager.default_project
|
||||
return self.config_manager.default_project
|
||||
|
||||
@property
|
||||
def current_project(self) -> str:
|
||||
@@ -58,7 +74,7 @@ class ProjectService:
|
||||
Returns:
|
||||
The name of the current project
|
||||
"""
|
||||
return os.environ.get("BASIC_MEMORY_PROJECT", config_manager.default_project)
|
||||
return os.environ.get("BASIC_MEMORY_PROJECT", self.config_manager.default_project)
|
||||
|
||||
async def list_projects(self) -> Sequence[Project]:
|
||||
return await self.repository.find_all()
|
||||
@@ -84,10 +100,10 @@ class ProjectService:
|
||||
raise ValueError("Repository is required for add_project")
|
||||
|
||||
# Resolve to absolute path
|
||||
resolved_path = os.path.abspath(os.path.expanduser(path))
|
||||
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
|
||||
|
||||
# First add to config file (this will validate the project doesn't exist)
|
||||
project_config = config_manager.add_project(name, resolved_path)
|
||||
project_config = self.config_manager.add_project(name, resolved_path)
|
||||
|
||||
# Then add to database
|
||||
project_data = {
|
||||
@@ -103,7 +119,7 @@ class ProjectService:
|
||||
# If this should be the default project, ensure only one default exists
|
||||
if set_default:
|
||||
await self.repository.set_as_default(created_project.id)
|
||||
config_manager.set_default_project(name)
|
||||
self.config_manager.set_default_project(name)
|
||||
logger.info(f"Project '{name}' set as default")
|
||||
|
||||
logger.info(f"Project '{name}' added at {resolved_path}")
|
||||
@@ -121,10 +137,10 @@ class ProjectService:
|
||||
raise ValueError("Repository is required for remove_project")
|
||||
|
||||
# First remove from config (this will validate the project exists and is not default)
|
||||
config_manager.remove_project(name)
|
||||
self.config_manager.remove_project(name)
|
||||
|
||||
# Then remove from database
|
||||
project = await self.repository.get_by_name(name)
|
||||
# Then remove from database using robust lookup
|
||||
project = await self.get_project(name)
|
||||
if project:
|
||||
await self.repository.delete(project.id)
|
||||
|
||||
@@ -143,10 +159,10 @@ class ProjectService:
|
||||
raise ValueError("Repository is required for set_default_project")
|
||||
|
||||
# First update config file (this will validate the project exists)
|
||||
config_manager.set_default_project(name)
|
||||
self.config_manager.set_default_project(name)
|
||||
|
||||
# Then update database
|
||||
project = await self.repository.get_by_name(name)
|
||||
# Then update database using the same lookup logic as get_project
|
||||
project = await self.get_project(name)
|
||||
if project:
|
||||
await self.repository.set_as_default(project.id)
|
||||
else:
|
||||
@@ -196,7 +212,7 @@ class ProjectService:
|
||||
elif len(default_projects) == 0: # pragma: no cover
|
||||
# No default project - set the config default as default
|
||||
# This is defensive code for edge cases where no default exists
|
||||
config_default = config_manager.default_project # pragma: no cover
|
||||
config_default = self.config_manager.default_project # pragma: no cover
|
||||
config_project = await self.repository.get_by_name(config_default) # pragma: no cover
|
||||
if config_project: # pragma: no cover
|
||||
await self.repository.set_as_default(config_project.id) # pragma: no cover
|
||||
@@ -221,7 +237,7 @@ class ProjectService:
|
||||
db_projects_by_permalink = {p.permalink: p for p in db_projects}
|
||||
|
||||
# Get all projects from configuration and normalize names if needed
|
||||
config_projects = config_manager.projects.copy()
|
||||
config_projects = self.config_manager.projects.copy()
|
||||
updated_config = {}
|
||||
config_updated = False
|
||||
|
||||
@@ -237,8 +253,9 @@ class ProjectService:
|
||||
|
||||
# Update the configuration if any changes were made
|
||||
if config_updated:
|
||||
config_manager.config.projects = updated_config
|
||||
config_manager.save_config(config_manager.config)
|
||||
config = self.config_manager.load_config()
|
||||
config.projects = updated_config
|
||||
self.config_manager.save_config(config)
|
||||
logger.info("Config updated with normalized project names")
|
||||
|
||||
# Use the normalized config for further processing
|
||||
@@ -261,19 +278,19 @@ class ProjectService:
|
||||
for name, project in db_projects_by_permalink.items():
|
||||
if name not in config_projects:
|
||||
logger.info(f"Adding project '{name}' to configuration")
|
||||
config_manager.add_project(name, project.path)
|
||||
self.config_manager.add_project(name, project.path)
|
||||
|
||||
# Ensure database default project state is consistent
|
||||
await self._ensure_single_default_project()
|
||||
|
||||
# Make sure default project is synchronized between config and database
|
||||
db_default = await self.repository.get_default_project()
|
||||
config_default = config_manager.default_project
|
||||
config_default = self.config_manager.default_project
|
||||
|
||||
if db_default and db_default.name != config_default:
|
||||
# Update config to match DB default
|
||||
logger.info(f"Updating default project in config to '{db_default.name}'")
|
||||
config_manager.set_default_project(db_default.name)
|
||||
self.config_manager.set_default_project(db_default.name)
|
||||
elif not db_default and config_default:
|
||||
# Update DB to match config default (if the project exists)
|
||||
project = await self.repository.get_by_name(config_default)
|
||||
@@ -292,6 +309,47 @@ class ProjectService:
|
||||
# MCP components might not be available in all contexts
|
||||
logger.debug("MCP session not available, skipping session refresh")
|
||||
|
||||
async def move_project(self, name: str, new_path: str) -> None:
|
||||
"""Move a project to a new location.
|
||||
|
||||
Args:
|
||||
name: The name of the project to move
|
||||
new_path: The new absolute path for the project
|
||||
|
||||
Raises:
|
||||
ValueError: If the project doesn't exist or repository isn't initialized
|
||||
"""
|
||||
if not self.repository:
|
||||
raise ValueError("Repository is required for move_project")
|
||||
|
||||
# Resolve to absolute path
|
||||
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
|
||||
|
||||
# Validate project exists in config
|
||||
if name not in self.config_manager.projects:
|
||||
raise ValueError(f"Project '{name}' not found in configuration")
|
||||
|
||||
# Create the new directory if it doesn't exist
|
||||
Path(resolved_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Update in configuration
|
||||
config = self.config_manager.load_config()
|
||||
old_path = config.projects[name]
|
||||
config.projects[name] = resolved_path
|
||||
self.config_manager.save_config(config)
|
||||
|
||||
# Update in database using robust lookup
|
||||
project = await self.get_project(name)
|
||||
if project:
|
||||
await self.repository.update_path(project.id, resolved_path)
|
||||
logger.info(f"Moved project '{name}' from {old_path} to {resolved_path}")
|
||||
else:
|
||||
logger.error(f"Project '{name}' exists in config but not in database")
|
||||
# Restore the old path in config since DB update failed
|
||||
config.projects[name] = old_path
|
||||
self.config_manager.save_config(config)
|
||||
raise ValueError(f"Project '{name}' not found in database")
|
||||
|
||||
async def update_project( # pragma: no cover
|
||||
self, name: str, updated_path: Optional[str] = None, is_active: Optional[bool] = None
|
||||
) -> None:
|
||||
@@ -309,24 +367,23 @@ class ProjectService:
|
||||
raise ValueError("Repository is required for update_project")
|
||||
|
||||
# Validate project exists in config
|
||||
if name not in config_manager.projects:
|
||||
if name not in self.config_manager.projects:
|
||||
raise ValueError(f"Project '{name}' not found in configuration")
|
||||
|
||||
# Get project from database
|
||||
project = await self.repository.get_by_name(name)
|
||||
# Get project from database using robust lookup
|
||||
project = await self.get_project(name)
|
||||
if not project:
|
||||
logger.error(f"Project '{name}' exists in config but not in database")
|
||||
return
|
||||
|
||||
# Update path if provided
|
||||
if updated_path:
|
||||
resolved_path = os.path.abspath(os.path.expanduser(updated_path))
|
||||
resolved_path = Path(os.path.abspath(os.path.expanduser(updated_path))).as_posix()
|
||||
|
||||
# Update in config
|
||||
projects = config_manager.config.projects.copy()
|
||||
projects[name] = resolved_path
|
||||
config_manager.config.projects = projects
|
||||
config_manager.save_config(config_manager.config)
|
||||
config = self.config_manager.load_config()
|
||||
config.projects[name] = resolved_path
|
||||
self.config_manager.save_config(config)
|
||||
|
||||
# Update in database
|
||||
project.path = resolved_path
|
||||
@@ -347,7 +404,7 @@ class ProjectService:
|
||||
if active_projects:
|
||||
new_default = active_projects[0]
|
||||
await self.repository.set_as_default(new_default.id)
|
||||
config_manager.set_default_project(new_default.name)
|
||||
self.config_manager.set_default_project(new_default.name)
|
||||
logger.info(
|
||||
f"Changed default project to '{new_default.name}' as '{name}' was deactivated"
|
||||
)
|
||||
@@ -365,9 +422,9 @@ class ProjectService:
|
||||
raise ValueError("Repository is required for get_project_info")
|
||||
|
||||
# Use specified project or fall back to config project
|
||||
project_name = project_name or config.project
|
||||
project_name = project_name or self.config.project
|
||||
# Get project path from configuration
|
||||
name, project_path = config_manager.get_project(project_name)
|
||||
name, project_path = self.config_manager.get_project(project_name)
|
||||
if not name: # pragma: no cover
|
||||
raise ValueError(f"Project '{project_name}' not found in configuration")
|
||||
|
||||
@@ -393,11 +450,11 @@ class ProjectService:
|
||||
db_projects_by_permalink = {p.permalink: p for p in db_projects}
|
||||
|
||||
# Get default project info
|
||||
default_project = config_manager.default_project
|
||||
default_project = self.config_manager.default_project
|
||||
|
||||
# Convert config projects to include database info
|
||||
enhanced_projects = {}
|
||||
for name, path in config_manager.projects.items():
|
||||
for name, path in self.config_manager.projects.items():
|
||||
config_permalink = generate_permalink(name)
|
||||
db_project = db_projects_by_permalink.get(config_permalink)
|
||||
enhanced_projects[name] = {
|
||||
@@ -673,7 +730,7 @@ class ProjectService:
|
||||
import basic_memory
|
||||
|
||||
# Get database information
|
||||
db_path = app_config.database_path
|
||||
db_path = self.config_manager.config.database_path
|
||||
db_size = db_path.stat().st_size if db_path.exists() else 0
|
||||
db_size_readable = f"{db_size / (1024 * 1024):.2f} MB"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.config import config as project_config
|
||||
from basic_memory.config import get_project_config
|
||||
from basic_memory.sync import SyncService, WatchService
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ async def sync_and_watch(
|
||||
): # pragma: no cover
|
||||
"""Run sync and watch service."""
|
||||
|
||||
logger.info(f"Starting watch service to sync file changes in dir: {project_config.home}")
|
||||
config = get_project_config()
|
||||
logger.info(f"Starting watch service to sync file changes in dir: {config.home}")
|
||||
# full sync
|
||||
await sync_service.sync(project_config.home)
|
||||
await sync_service.sync(config.home)
|
||||
|
||||
# watch changes
|
||||
await watch_service.run()
|
||||
|
||||
@@ -357,8 +357,8 @@ class SyncService:
|
||||
|
||||
# get file timestamps
|
||||
file_stats = self.file_service.file_stats(path)
|
||||
created = datetime.fromtimestamp(file_stats.st_ctime)
|
||||
modified = datetime.fromtimestamp(file_stats.st_mtime)
|
||||
created = datetime.fromtimestamp(file_stats.st_ctime).astimezone()
|
||||
modified = datetime.fromtimestamp(file_stats.st_mtime).astimezone()
|
||||
|
||||
# get mime type
|
||||
content_type = self.file_service.content_type(path)
|
||||
@@ -453,6 +453,36 @@ class SyncService:
|
||||
|
||||
entity = await self.entity_repository.get_by_file_path(old_path)
|
||||
if entity:
|
||||
# Check if destination path is already occupied by another entity
|
||||
existing_at_destination = await self.entity_repository.get_by_file_path(new_path)
|
||||
if existing_at_destination and existing_at_destination.id != entity.id:
|
||||
# Handle the conflict - this could be a file swap or replacement scenario
|
||||
logger.warning(
|
||||
f"File path conflict detected during move: "
|
||||
f"entity_id={entity.id} trying to move from '{old_path}' to '{new_path}', "
|
||||
f"but entity_id={existing_at_destination.id} already occupies '{new_path}'"
|
||||
)
|
||||
|
||||
# Check if this is a file swap (the destination entity is being moved to our old path)
|
||||
# This would indicate a simultaneous move operation
|
||||
old_path_after_swap = await self.entity_repository.get_by_file_path(old_path)
|
||||
if old_path_after_swap and old_path_after_swap.id == existing_at_destination.id:
|
||||
logger.info(f"Detected file swap between '{old_path}' and '{new_path}'")
|
||||
# This is a swap scenario - both moves should succeed
|
||||
# We'll allow this to proceed since the other file has moved out
|
||||
else:
|
||||
# This is a conflict where the destination is occupied
|
||||
raise ValueError(
|
||||
f"Cannot move entity from '{old_path}' to '{new_path}': "
|
||||
f"destination path is already occupied by another file. "
|
||||
f"This may be caused by: "
|
||||
f"1. Conflicting file names with different character encodings, "
|
||||
f"2. Case sensitivity differences (e.g., 'Finance/' vs 'finance/'), "
|
||||
f"3. Character conflicts between hyphens in filenames and generated permalinks, "
|
||||
f"4. Files with similar names containing special characters. "
|
||||
f"Try renaming one of the conflicting files to resolve this issue."
|
||||
)
|
||||
|
||||
# Update file_path in all cases
|
||||
updates = {"file_path": new_path}
|
||||
|
||||
@@ -477,7 +507,26 @@ class SyncService:
|
||||
f"new_checksum={new_checksum}"
|
||||
)
|
||||
|
||||
updated = await self.entity_repository.update(entity.id, updates)
|
||||
try:
|
||||
updated = await self.entity_repository.update(entity.id, updates)
|
||||
except Exception as e:
|
||||
# Catch any database integrity errors and provide helpful context
|
||||
if "UNIQUE constraint failed" in str(e):
|
||||
logger.error(
|
||||
f"Database constraint violation during move: "
|
||||
f"entity_id={entity.id}, old_path='{old_path}', new_path='{new_path}'"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Cannot complete move from '{old_path}' to '{new_path}': "
|
||||
f"a database constraint was violated. This usually indicates "
|
||||
f"a file path or permalink conflict. Please check for: "
|
||||
f"1. Duplicate file names, "
|
||||
f"2. Case sensitivity issues (e.g., 'File.md' vs 'file.md'), "
|
||||
f"3. Character encoding conflicts in file names."
|
||||
) from e
|
||||
else:
|
||||
# Re-raise other exceptions as-is
|
||||
raise
|
||||
|
||||
if updated is None: # pragma: no cover
|
||||
logger.error(
|
||||
@@ -570,7 +619,7 @@ class SyncService:
|
||||
continue
|
||||
|
||||
path = Path(root) / filename
|
||||
rel_path = str(path.relative_to(directory))
|
||||
rel_path = path.relative_to(directory).as_posix()
|
||||
checksum = await self.file_service.compute_checksum(rel_path)
|
||||
result.files[rel_path] = checksum
|
||||
result.checksums[checksum] = rel_path
|
||||
|
||||
@@ -197,7 +197,7 @@ class WatchService:
|
||||
|
||||
for change, path in changes:
|
||||
# convert to relative path
|
||||
relative_path = str(Path(path).relative_to(directory))
|
||||
relative_path = Path(path).relative_to(directory).as_posix()
|
||||
|
||||
# Skip .tmp files - they're temporary and shouldn't be synced
|
||||
if relative_path.endswith(".tmp"):
|
||||
@@ -284,13 +284,36 @@ class WatchService:
|
||||
# Process deletes
|
||||
for path in deletes:
|
||||
if path not in processed:
|
||||
logger.debug("Processing deleted file", path=path)
|
||||
await sync_service.handle_delete(path)
|
||||
self.state.add_event(path=path, action="deleted", status="success")
|
||||
self.console.print(f"[red]✕[/red] {path}")
|
||||
logger.info(f"deleted: {path}")
|
||||
processed.add(path)
|
||||
delete_count += 1
|
||||
# Check if file still exists on disk (vim atomic write edge case)
|
||||
full_path = directory / path
|
||||
if full_path.exists() and full_path.is_file():
|
||||
# File still exists despite DELETE event - treat as modification
|
||||
logger.debug("File exists despite DELETE event, treating as modification", path=path)
|
||||
entity, checksum = await sync_service.sync_file(path, new=False)
|
||||
self.state.add_event(path=path, action="modified", status="success", checksum=checksum)
|
||||
self.console.print(f"[yellow]✎[/yellow] {path} (atomic write)")
|
||||
logger.info(f"atomic write detected: {path}")
|
||||
processed.add(path)
|
||||
modify_count += 1
|
||||
else:
|
||||
# Check if this was a directory - skip if so
|
||||
# (we can't tell if the deleted path was a directory since it no longer exists,
|
||||
# so we check if there's an entity in the database for it)
|
||||
entity = await sync_service.entity_repository.get_by_file_path(path)
|
||||
if entity is None:
|
||||
# No entity means this was likely a directory - skip it
|
||||
logger.debug(f"Skipping deleted path with no entity (likely directory), path={path}")
|
||||
processed.add(path)
|
||||
continue
|
||||
|
||||
# File truly deleted
|
||||
logger.debug("Processing deleted file", path=path)
|
||||
await sync_service.handle_delete(path)
|
||||
self.state.add_event(path=path, action="deleted", status="success")
|
||||
self.console.print(f"[red]✕[/red] {path}")
|
||||
logger.info(f"deleted: {path}")
|
||||
processed.add(path)
|
||||
delete_count += 1
|
||||
|
||||
# Process adds
|
||||
for path in adds:
|
||||
|
||||
+239
-74
@@ -5,11 +5,12 @@ import os
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Protocol, Union, runtime_checkable, List, Any
|
||||
from typing import Optional, Protocol, Union, runtime_checkable, List
|
||||
|
||||
from loguru import logger
|
||||
from unidecode import unidecode
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -27,9 +28,11 @@ FilePath = Union[Path, str]
|
||||
logging.getLogger("opentelemetry.sdk.metrics._internal.instrument").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def generate_permalink(file_path: Union[Path, str, Any]) -> str:
|
||||
"""
|
||||
Generate a permalink from a file path.
|
||||
def generate_permalink(file_path: Union[Path, str, PathLike], split_extension: bool = True) -> str:
|
||||
"""Generate a stable permalink from a file path.
|
||||
|
||||
Args:
|
||||
file_path: Original file path (str, Path, or PathLike)
|
||||
|
||||
Returns:
|
||||
Normalized permalink that matches validation rules. Converts spaces and underscores
|
||||
@@ -38,7 +41,7 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
|
||||
Examples:
|
||||
>>> generate_permalink("docs/My Feature.md")
|
||||
'docs/my-feature'
|
||||
>>> generate_permalink("specs/API_v2.md")
|
||||
>>> generate_permalink("specs/API (v2).md")
|
||||
'specs/api-v2'
|
||||
>>> generate_permalink("design/unified_model_refactor.md")
|
||||
'design/unified-model-refactor'
|
||||
@@ -46,84 +49,99 @@ def generate_permalink(file_path: Union[Path, str, Any]) -> str:
|
||||
'中文/测试文档'
|
||||
"""
|
||||
# Convert Path to string if needed
|
||||
path_str = str(file_path)
|
||||
path_str = Path(str(file_path)).as_posix()
|
||||
|
||||
# Remove extension
|
||||
base = os.path.splitext(path_str)[0]
|
||||
# Remove extension (for now, possibly)
|
||||
(base, extension) = os.path.splitext(path_str)
|
||||
|
||||
# Create a transliteration mapping for specific characters
|
||||
transliteration_map = {
|
||||
"ø": "o", # Handle Søren -> soren
|
||||
"å": "a", # Handle Kierkegård -> kierkegard
|
||||
"ü": "u", # Handle Müller -> muller
|
||||
"é": "e", # Handle Café -> cafe
|
||||
"è": "e", # Handle Mère -> mere
|
||||
"ê": "e", # Handle Fête -> fete
|
||||
"à": "a", # Handle À la mode -> a la mode
|
||||
"ç": "c", # Handle Façade -> facade
|
||||
"ñ": "n", # Handle Niño -> nino
|
||||
"ö": "o", # Handle Björk -> bjork
|
||||
"ä": "a", # Handle Häagen -> haagen
|
||||
# Add more mappings as needed
|
||||
}
|
||||
|
||||
# Process character by character, transliterating Latin characters with diacritics
|
||||
result = ""
|
||||
for char in base:
|
||||
# Direct mapping for known characters
|
||||
if char.lower() in transliteration_map:
|
||||
result += transliteration_map[char.lower()]
|
||||
# General case using Unicode normalization
|
||||
elif unicodedata.category(char).startswith("L") and ord(char) > 127:
|
||||
# Decompose the character (e.g., ü -> u + combining diaeresis)
|
||||
decomposed = unicodedata.normalize("NFD", char)
|
||||
# If decomposition produced multiple characters and first one is ASCII
|
||||
if len(decomposed) > 1 and ord(decomposed[0]) < 128:
|
||||
# Keep only the base character
|
||||
result += decomposed[0].lower()
|
||||
else:
|
||||
# For non-Latin scripts like Chinese, preserve the character
|
||||
result += char
|
||||
else:
|
||||
# Add the character as is
|
||||
result += char
|
||||
|
||||
# Handle special punctuation cases for apostrophes
|
||||
result = result.replace("'", "")
|
||||
|
||||
# Insert dash between camelCase
|
||||
# This regex finds boundaries between lowercase and uppercase letters
|
||||
result = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", result)
|
||||
|
||||
# Insert dash between Chinese and Latin character boundaries
|
||||
# This is needed for cases like "中文English" -> "中文-english"
|
||||
result = re.sub(r"([\u4e00-\u9fff])([a-zA-Z])", r"\1-\2", result)
|
||||
result = re.sub(r"([a-zA-Z])([\u4e00-\u9fff])", r"\1-\2", result)
|
||||
|
||||
# Convert ASCII letters to lowercase, preserve non-ASCII characters
|
||||
lower_text = "".join(c.lower() if c.isascii() and c.isalpha() else c for c in result)
|
||||
|
||||
# Replace underscores with hyphens
|
||||
text_with_hyphens = lower_text.replace("_", "-")
|
||||
|
||||
# Replace spaces and unsafe ASCII characters with hyphens, but preserve non-ASCII characters
|
||||
# Include common Chinese character ranges and other non-ASCII characters
|
||||
clean_text = re.sub(
|
||||
r"[^a-z0-9\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf/\-]", "-", text_with_hyphens
|
||||
# Check if we have CJK characters that should be preserved
|
||||
# CJK ranges: \u4e00-\u9fff (CJK Unified Ideographs), \u3000-\u303f (CJK symbols),
|
||||
# \u3400-\u4dbf (CJK Extension A), \uff00-\uffef (Fullwidth forms)
|
||||
has_cjk_chars = any(
|
||||
"\u4e00" <= char <= "\u9fff"
|
||||
or "\u3000" <= char <= "\u303f"
|
||||
or "\u3400" <= char <= "\u4dbf"
|
||||
or "\uff00" <= char <= "\uffef"
|
||||
for char in base
|
||||
)
|
||||
|
||||
if has_cjk_chars:
|
||||
# For text with CJK characters, selectively transliterate only Latin accented chars
|
||||
result = ""
|
||||
for char in base:
|
||||
if (
|
||||
"\u4e00" <= char <= "\u9fff"
|
||||
or "\u3000" <= char <= "\u303f"
|
||||
or "\u3400" <= char <= "\u4dbf"
|
||||
):
|
||||
# Preserve CJK ideographs and symbols
|
||||
result += char
|
||||
elif "\uff00" <= char <= "\uffef":
|
||||
# Remove Chinese fullwidth punctuation entirely (like ,!?)
|
||||
continue
|
||||
else:
|
||||
# Transliterate Latin accented characters to ASCII
|
||||
result += unidecode(char)
|
||||
|
||||
# Insert hyphens between CJK and Latin character transitions
|
||||
# Match: CJK followed by Latin letter/digit, or Latin letter/digit followed by CJK
|
||||
result = re.sub(
|
||||
r"([\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf])([a-zA-Z0-9])", r"\1-\2", result
|
||||
)
|
||||
result = re.sub(
|
||||
r"([a-zA-Z0-9])([\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf])", r"\1-\2", result
|
||||
)
|
||||
|
||||
# Insert dash between camelCase
|
||||
result = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", result)
|
||||
|
||||
# Convert ASCII letters to lowercase, preserve CJK
|
||||
lower_text = "".join(c.lower() if c.isascii() and c.isalpha() else c for c in result)
|
||||
|
||||
# Replace underscores with hyphens
|
||||
text_with_hyphens = lower_text.replace("_", "-")
|
||||
|
||||
# Remove apostrophes entirely (don't replace with hyphens)
|
||||
text_no_apostrophes = text_with_hyphens.replace("'", "")
|
||||
|
||||
# Replace unsafe chars with hyphens, but preserve CJK characters
|
||||
clean_text = re.sub(
|
||||
r"[^a-z0-9\u4e00-\u9fff\u3000-\u303f\u3400-\u4dbf/\-]", "-", text_no_apostrophes
|
||||
)
|
||||
else:
|
||||
# Original ASCII-only processing for backward compatibility
|
||||
# Transliterate unicode to ascii
|
||||
ascii_text = unidecode(base)
|
||||
|
||||
# Insert dash between camelCase
|
||||
ascii_text = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", ascii_text)
|
||||
|
||||
# Convert to lowercase
|
||||
lower_text = ascii_text.lower()
|
||||
|
||||
# replace underscores with hyphens
|
||||
text_with_hyphens = lower_text.replace("_", "-")
|
||||
|
||||
# Remove apostrophes entirely (don't replace with hyphens)
|
||||
text_no_apostrophes = text_with_hyphens.replace("'", "")
|
||||
|
||||
# Replace remaining invalid chars with hyphens
|
||||
clean_text = re.sub(r"[^a-z0-9/\-]", "-", text_no_apostrophes)
|
||||
|
||||
# Collapse multiple hyphens
|
||||
clean_text = re.sub(r"-+", "-", clean_text)
|
||||
|
||||
# Remove hyphens between adjacent Chinese characters only
|
||||
# This handles cases like "你好-世界" -> "你好世界"
|
||||
clean_text = re.sub(r"([\u4e00-\u9fff])-([\u4e00-\u9fff])", r"\1\2", clean_text)
|
||||
|
||||
# Clean each path segment
|
||||
segments = clean_text.split("/")
|
||||
clean_segments = [s.strip("-") for s in segments]
|
||||
|
||||
return "/".join(clean_segments)
|
||||
return_val = "/".join(clean_segments)
|
||||
|
||||
# Append file extension back, if necessary
|
||||
if not split_extension and extension:
|
||||
return_val += extension
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
def setup_logging(
|
||||
@@ -201,8 +219,21 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
||||
# 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
|
||||
# Process string input
|
||||
if isinstance(tags, str):
|
||||
# Check if it's a JSON array string (common issue from AI assistants)
|
||||
import json
|
||||
if tags.strip().startswith('[') and tags.strip().endswith(']'):
|
||||
try:
|
||||
# Try to parse as JSON array
|
||||
parsed_json = json.loads(tags)
|
||||
if isinstance(parsed_json, list):
|
||||
# Recursively parse the JSON array as a list
|
||||
return parse_tags(parsed_json)
|
||||
except json.JSONDecodeError:
|
||||
# Not valid JSON, fall through to comma-separated parsing
|
||||
pass
|
||||
|
||||
# Split by comma, strip whitespace, then strip leading '#' characters
|
||||
return [tag.strip().lstrip("#") for tag in tags.split(",") if tag and tag.strip()]
|
||||
|
||||
@@ -212,3 +243,137 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
||||
except (ValueError, TypeError): # pragma: no cover
|
||||
logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
|
||||
return []
|
||||
|
||||
|
||||
def normalize_newlines(multiline: str) -> str:
|
||||
"""Replace any \r\n, \r, or \n with the native newline.
|
||||
|
||||
Args:
|
||||
multiline: String containing any mixture of newlines.
|
||||
|
||||
Returns:
|
||||
A string with normalized newlines native to the platform.
|
||||
"""
|
||||
return re.sub(r"\r\n?|\n", os.linesep, multiline)
|
||||
|
||||
|
||||
def normalize_file_path_for_comparison(file_path: str) -> str:
|
||||
"""Normalize a file path for conflict detection.
|
||||
|
||||
This function normalizes file paths to help detect potential conflicts:
|
||||
- Converts to lowercase for case-insensitive comparison
|
||||
- Normalizes Unicode characters
|
||||
- Handles path separators consistently
|
||||
|
||||
Args:
|
||||
file_path: The file path to normalize
|
||||
|
||||
Returns:
|
||||
Normalized file path for comparison purposes
|
||||
"""
|
||||
import unicodedata
|
||||
|
||||
# Convert to lowercase for case-insensitive comparison
|
||||
normalized = file_path.lower()
|
||||
|
||||
# Normalize Unicode characters (NFD normalization)
|
||||
normalized = unicodedata.normalize("NFD", normalized)
|
||||
|
||||
# Replace path separators with forward slashes
|
||||
normalized = normalized.replace("\\", "/")
|
||||
|
||||
# Remove multiple slashes
|
||||
normalized = re.sub(r"/+", "/", normalized)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def detect_potential_file_conflicts(file_path: str, existing_paths: List[str]) -> List[str]:
|
||||
"""Detect potential conflicts between a file path and existing paths.
|
||||
|
||||
This function checks for various types of conflicts:
|
||||
- Case sensitivity differences
|
||||
- Unicode normalization differences
|
||||
- Path separator differences
|
||||
- Permalink generation conflicts
|
||||
|
||||
Args:
|
||||
file_path: The file path to check
|
||||
existing_paths: List of existing file paths to check against
|
||||
|
||||
Returns:
|
||||
List of existing paths that might conflict with the given file path
|
||||
"""
|
||||
conflicts = []
|
||||
|
||||
# Normalize the input file path
|
||||
normalized_input = normalize_file_path_for_comparison(file_path)
|
||||
input_permalink = generate_permalink(file_path)
|
||||
|
||||
for existing_path in existing_paths:
|
||||
# Skip identical paths
|
||||
if existing_path == file_path:
|
||||
continue
|
||||
|
||||
# Check for case-insensitive path conflicts
|
||||
normalized_existing = normalize_file_path_for_comparison(existing_path)
|
||||
if normalized_input == normalized_existing:
|
||||
conflicts.append(existing_path)
|
||||
continue
|
||||
|
||||
# Check for permalink conflicts
|
||||
existing_permalink = generate_permalink(existing_path)
|
||||
if input_permalink == existing_permalink:
|
||||
conflicts.append(existing_path)
|
||||
continue
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def validate_project_path(path: str, project_path: Path) -> bool:
|
||||
"""Ensure path stays within project boundaries."""
|
||||
# Allow empty strings as they resolve to the project root
|
||||
if not path:
|
||||
return True
|
||||
|
||||
# Check for obvious path traversal patterns first
|
||||
if ".." in path or "~" in path:
|
||||
return False
|
||||
|
||||
# Check for Windows-style path traversal (even on Unix systems)
|
||||
if "\\.." in path or path.startswith("\\"):
|
||||
return False
|
||||
|
||||
# Block absolute paths (Unix-style starting with / or Windows-style with drive letters)
|
||||
if path.startswith("/") or (len(path) >= 2 and path[1] == ":"):
|
||||
return False
|
||||
|
||||
# Block paths with control characters (but allow whitespace that will be stripped)
|
||||
if path.strip() and any(ord(c) < 32 and c not in [" ", "\t"] for c in path):
|
||||
return False
|
||||
|
||||
try:
|
||||
resolved = (project_path / path).resolve()
|
||||
return resolved.is_relative_to(project_path.resolve())
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def ensure_timezone_aware(dt: datetime) -> datetime:
|
||||
"""Ensure a datetime is timezone-aware using system timezone.
|
||||
|
||||
If the datetime is naive, convert it to timezone-aware using the system's local timezone.
|
||||
If it's already timezone-aware, return it unchanged.
|
||||
|
||||
Args:
|
||||
dt: The datetime to ensure is timezone-aware
|
||||
|
||||
Returns:
|
||||
A timezone-aware datetime
|
||||
"""
|
||||
if dt.tzinfo is None:
|
||||
# Naive datetime - assume it's in local time and add timezone
|
||||
return dt.astimezone()
|
||||
else:
|
||||
# Already timezone-aware
|
||||
return dt
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
"""Integration test for database reset command.
|
||||
|
||||
This test validates the fix for GitHub issue #151 where the reset command
|
||||
was only removing the SQLite database but leaving project configuration
|
||||
intact in ~/.basic-memory/config.json.
|
||||
|
||||
The test verifies that the reset command now:
|
||||
1. Removes the SQLite database
|
||||
2. Resets project configuration to default state (main project only)
|
||||
3. Recreates empty database
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_config_file_behavior(config_manager):
|
||||
"""Test that reset command properly updates the config.json file."""
|
||||
|
||||
# Step 1: Set up initial state with multiple projects in config
|
||||
original_projects = {
|
||||
"project1": "/path/to/project1",
|
||||
"project2": "/path/to/project2",
|
||||
"user-project": "/home/user/documents",
|
||||
}
|
||||
config_manager.config.projects = original_projects.copy()
|
||||
config_manager.config.default_project = "user-project"
|
||||
|
||||
# Step 2: Save the config to a temporary file to simulate the real config file
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_config_file = Path(temp_dir) / "config.json"
|
||||
config_manager.config_file = temp_config_file
|
||||
config_manager.save_config(config_manager.config)
|
||||
|
||||
# Step 3: Verify the config file contains the multiple projects
|
||||
config_json = json.loads(temp_config_file.read_text())
|
||||
assert len(config_json["projects"]) == 3
|
||||
assert config_json["default_project"] == "user-project"
|
||||
assert "project1" in config_json["projects"]
|
||||
assert "project2" in config_json["projects"]
|
||||
assert "user-project" in config_json["projects"]
|
||||
|
||||
# Step 4: Simulate the reset command's configuration reset behavior
|
||||
# This is the exact fix for issue #151
|
||||
with patch("pathlib.Path.home") as mock_home:
|
||||
mock_home.return_value = Path("/home/testuser")
|
||||
|
||||
# Apply the reset logic from the reset command
|
||||
config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
|
||||
config_manager.config.default_project = "main"
|
||||
config_manager.save_config(config_manager.config)
|
||||
|
||||
# Step 5: Read the config file and verify it was properly reset
|
||||
updated_config_json = json.loads(temp_config_file.read_text())
|
||||
|
||||
# Should now only have the main project
|
||||
assert len(updated_config_json["projects"]) == 1
|
||||
assert "main" in updated_config_json["projects"]
|
||||
assert updated_config_json["projects"]["main"] == "/home/testuser/basic-memory"
|
||||
assert updated_config_json["default_project"] == "main"
|
||||
|
||||
# All original projects should be gone from the file
|
||||
assert "project1" not in updated_config_json["projects"]
|
||||
assert "project2" not in updated_config_json["projects"]
|
||||
assert "user-project" not in updated_config_json["projects"]
|
||||
|
||||
# This validates that issue #151 is fixed:
|
||||
# Before the fix, these projects would persist in config.json after reset
|
||||
# After the fix, only the default "main" project remains
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_command_source_code_validation():
|
||||
"""Validate that the reset command source contains the required fix."""
|
||||
# This test ensures the fix for issue #151 is present in the source code
|
||||
reset_source_path = (
|
||||
Path(__file__).parent.parent.parent / "src" / "basic_memory" / "cli" / "commands" / "db.py"
|
||||
)
|
||||
reset_source = reset_source_path.read_text()
|
||||
|
||||
# Verify the key components of the fix are present
|
||||
required_lines = [
|
||||
"# Reset project configuration",
|
||||
'config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}',
|
||||
'config_manager.config.default_project = "main"',
|
||||
"config_manager.save_config(config_manager.config)",
|
||||
'logger.info("Project configuration reset to default")',
|
||||
]
|
||||
|
||||
for line in required_lines:
|
||||
assert line in reset_source, f"Required fix line not found: {line}"
|
||||
|
||||
# Verify the fix is in the correct location (after database deletion, before recreation)
|
||||
lines = reset_source.split("\n")
|
||||
|
||||
# Find key markers
|
||||
db_deletion_line = None
|
||||
config_reset_line = None
|
||||
db_recreation_line = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if "db_path.unlink()" in line:
|
||||
db_deletion_line = i
|
||||
elif "config_manager.config.projects = {" in line:
|
||||
config_reset_line = i
|
||||
elif "asyncio.run(db.run_migrations" in line:
|
||||
db_recreation_line = i
|
||||
|
||||
# Verify the order is correct
|
||||
assert db_deletion_line is not None, "Database deletion code not found"
|
||||
assert config_reset_line is not None, "Config reset code not found"
|
||||
assert db_recreation_line is not None, "Database recreation code not found"
|
||||
|
||||
# Config reset should be after db deletion and before db recreation
|
||||
assert db_deletion_line < config_reset_line < db_recreation_line, (
|
||||
"Config reset is not in the correct order in the reset command"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_reset_behavior_simulation(config_manager):
|
||||
"""Test the specific configuration reset behavior that fixes issue #151."""
|
||||
|
||||
# Step 1: Set up the problem state (multiple projects in config)
|
||||
original_projects = {
|
||||
"project1": "/path/to/project1",
|
||||
"project2": "/path/to/project2",
|
||||
"user-project": "/home/user/documents",
|
||||
}
|
||||
config_manager.config.projects = original_projects.copy()
|
||||
config_manager.config.default_project = "user-project"
|
||||
|
||||
# Verify the problem state
|
||||
assert len(config_manager.config.projects) == 3
|
||||
assert config_manager.config.default_project == "user-project"
|
||||
|
||||
# Step 2: Apply the reset fix (simulate what reset command does)
|
||||
with patch("pathlib.Path.home") as mock_home:
|
||||
mock_home.return_value = Path("/home/testuser")
|
||||
|
||||
# This is the exact code from the reset command that fixes issue #151
|
||||
config_manager.config.projects = {"main": str(Path.home() / "basic-memory")}
|
||||
config_manager.config.default_project = "main"
|
||||
# Note: We don't call save_config in test to avoid file operations
|
||||
|
||||
# Step 3: Verify the fix worked
|
||||
assert len(config_manager.config.projects) == 1
|
||||
assert "main" in config_manager.config.projects
|
||||
assert config_manager.config.projects["main"] == "/home/testuser/basic-memory"
|
||||
assert config_manager.config.default_project == "main"
|
||||
|
||||
# Step 4: Verify original projects are gone
|
||||
for project_name in original_projects:
|
||||
assert project_name not in config_manager.config.projects
|
||||
+27
-40
@@ -58,16 +58,12 @@ from pathlib import Path
|
||||
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
import basic_memory.config
|
||||
import basic_memory.mcp.project_session
|
||||
|
||||
from basic_memory.config import BasicMemoryConfig, ProjectConfig, ConfigManager
|
||||
from basic_memory.db import engine_session_factory, DatabaseType
|
||||
from basic_memory.models import Project
|
||||
from basic_memory.repository.project_repository import ProjectRepository
|
||||
from fastapi import FastAPI
|
||||
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
from basic_memory.deps import get_project_config, get_engine_factory, get_app_config
|
||||
|
||||
|
||||
@@ -93,12 +89,12 @@ async def engine_factory(tmp_path):
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def test_project(tmp_path, engine_factory) -> Project:
|
||||
async def test_project(config_home, engine_factory) -> Project:
|
||||
"""Create a test project."""
|
||||
project_data = {
|
||||
"name": "test-project",
|
||||
"description": "Project used for integration tests",
|
||||
"path": str(tmp_path),
|
||||
"path": str(config_home),
|
||||
"is_active": True,
|
||||
"is_default": True,
|
||||
}
|
||||
@@ -112,55 +108,50 @@ async def test_project(tmp_path, engine_factory) -> Project:
|
||||
@pytest.fixture
|
||||
def config_home(tmp_path, monkeypatch) -> Path:
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
# Set BASIC_MEMORY_HOME to the test directory
|
||||
monkeypatch.setenv("BASIC_MEMORY_HOME", str(tmp_path / "basic-memory"))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def app_config(config_home, test_project, tmp_path, monkeypatch) -> BasicMemoryConfig:
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def app_config(config_home, tmp_path, monkeypatch) -> BasicMemoryConfig:
|
||||
"""Create test app configuration."""
|
||||
projects = {test_project.name: str(test_project.path)}
|
||||
# Create a basic config with test-project like unit tests do
|
||||
projects = {"test-project": str(config_home)}
|
||||
app_config = BasicMemoryConfig(
|
||||
env="test",
|
||||
projects=projects,
|
||||
default_project=test_project.name,
|
||||
default_project="test-project",
|
||||
update_permalinks_on_move=True,
|
||||
)
|
||||
|
||||
# Set the module app_config instance project list (like regular tests)
|
||||
monkeypatch.setattr("basic_memory.config.app_config", app_config)
|
||||
return app_config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_manager(app_config: BasicMemoryConfig, config_home, monkeypatch) -> ConfigManager:
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def config_manager(app_config: BasicMemoryConfig, config_home) -> ConfigManager:
|
||||
config_manager = ConfigManager()
|
||||
# Update its paths to use the test directory
|
||||
config_manager.config_dir = config_home / ".basic-memory"
|
||||
config_manager.config_file = config_manager.config_dir / "config.json"
|
||||
config_manager.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Override the config directly instead of relying on disk load
|
||||
config_manager.config = app_config
|
||||
|
||||
# Ensure the config file is written to disk
|
||||
config_manager.save_config(app_config)
|
||||
|
||||
# Patch the config_manager in all locations where it's imported
|
||||
monkeypatch.setattr("basic_memory.config.config_manager", config_manager)
|
||||
monkeypatch.setattr("basic_memory.services.project_service.config_manager", config_manager)
|
||||
monkeypatch.setattr("basic_memory.mcp.project_session.config_manager", config_manager)
|
||||
|
||||
return config_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_session(test_project: Project):
|
||||
# initialize the project session with the test project
|
||||
basic_memory.mcp.project_session.session.initialize(test_project.name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def project_config(test_project, monkeypatch):
|
||||
def project_session(test_project: Project, config_manager):
|
||||
# Initialize the global session directly with the test project
|
||||
# This ensures all MCP tools use the test project context
|
||||
import basic_memory.mcp.project_session
|
||||
|
||||
basic_memory.mcp.project_session.session.initialize(test_project.name)
|
||||
return basic_memory.mcp.project_session.session
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def project_config(test_project):
|
||||
"""Create test project configuration."""
|
||||
|
||||
project_config = ProjectConfig(
|
||||
@@ -168,9 +159,6 @@ def project_config(test_project, monkeypatch):
|
||||
home=Path(test_project.path),
|
||||
)
|
||||
|
||||
# override config module project config
|
||||
monkeypatch.setattr("basic_memory.config.config", project_config)
|
||||
|
||||
return project_config
|
||||
|
||||
|
||||
@@ -180,6 +168,10 @@ def app(
|
||||
) -> FastAPI:
|
||||
"""Create test FastAPI application with single project."""
|
||||
|
||||
# Import the FastAPI app AFTER the config_manager has written the test config to disk
|
||||
# This ensures that when the app's lifespan manager runs, it reads the correct test config
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
|
||||
app = fastapi_app
|
||||
app.dependency_overrides[get_project_config] = lambda: project_config
|
||||
app.dependency_overrides[get_engine_factory] = lambda: engine_factory
|
||||
@@ -215,7 +207,7 @@ async def search_service(engine_factory, test_project):
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mcp_server(app_config, search_service):
|
||||
def mcp_server(config_manager, search_service, project_session):
|
||||
# Import mcp instance
|
||||
from basic_memory.mcp.server import mcp as server
|
||||
|
||||
@@ -225,11 +217,6 @@ def mcp_server(app_config, search_service):
|
||||
# Import prompts to register them
|
||||
import basic_memory.mcp.prompts # noqa: F401
|
||||
|
||||
# Initialize project session with test project
|
||||
from basic_memory.mcp.project_session import session
|
||||
|
||||
session.initialize(app_config.default_project)
|
||||
|
||||
return server
|
||||
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ async def test_build_context_valid_urls(mcp_server, app):
|
||||
result = await client.call_tool("build_context", {"url": url})
|
||||
|
||||
# Should return a valid GraphContext response
|
||||
assert len(result) == 1
|
||||
response = result[0].text
|
||||
assert len(result.content) == 1
|
||||
response = result.content[0].text
|
||||
assert '"results"' in response # Should contain results structure
|
||||
assert '"metadata"' in response # Should contain metadata
|
||||
|
||||
@@ -82,6 +82,7 @@ async def test_build_context_empty_urls_fail_validation(mcp_server, app):
|
||||
or "too_short" in error_message
|
||||
or "empty or whitespace" in error_message
|
||||
or "value_error" in error_message
|
||||
or "should be non-empty" in error_message
|
||||
)
|
||||
|
||||
|
||||
@@ -101,8 +102,8 @@ async def test_build_context_nonexistent_urls_return_empty_results(mcp_server, a
|
||||
result = await client.call_tool("build_context", {"url": url})
|
||||
|
||||
# Should return valid response with empty results
|
||||
assert len(result) == 1
|
||||
response = result[0].text
|
||||
assert len(result.content) == 1
|
||||
response = result.content[0].text
|
||||
assert '"results": []' in response # Empty results
|
||||
assert '"total_results": 0' in response # Zero count
|
||||
assert '"metadata"' in response # But should have metadata
|
||||
@@ -163,8 +164,8 @@ async def test_build_context_pattern_matching_works(mcp_server, app):
|
||||
# Test pattern matching
|
||||
result = await client.call_tool("build_context", {"url": "patterns/*"})
|
||||
|
||||
assert len(result) == 1
|
||||
response = result[0].text
|
||||
assert len(result.content) == 1
|
||||
response = result.content[0].text
|
||||
|
||||
# Should find the pattern matches but not the other note
|
||||
assert '"total_results": 2' in response or '"primary_count": 2' in response
|
||||
|
||||
@@ -31,8 +31,8 @@ async def test_delete_note_by_title(mcp_server, app):
|
||||
"identifier": "Note to Delete",
|
||||
},
|
||||
)
|
||||
assert len(read_result) == 1
|
||||
assert "Note to Delete" in read_result[0].text
|
||||
assert len(read_result.content) == 1
|
||||
assert "Note to Delete" in read_result.content[0].text
|
||||
|
||||
# Delete the note by title
|
||||
delete_result = await client.call_tool(
|
||||
@@ -43,9 +43,9 @@ async def test_delete_note_by_title(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert len(delete_result) == 1
|
||||
assert delete_result[0].type == "text"
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert len(delete_result.content) == 1
|
||||
assert delete_result.content[0].type == "text"
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
# Verify the note no longer exists
|
||||
read_after_delete = await client.call_tool(
|
||||
@@ -56,8 +56,8 @@ async def test_delete_note_by_title(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return helpful "Note Not Found" message instead of the actual note
|
||||
assert len(read_after_delete) == 1
|
||||
result_text = read_after_delete[0].text
|
||||
assert len(read_after_delete.content) == 1
|
||||
result_text = read_after_delete.content[0].text
|
||||
assert "Note Not Found" in result_text
|
||||
assert "Note to Delete" in result_text
|
||||
|
||||
@@ -87,8 +87,8 @@ async def test_delete_note_by_permalink(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert len(delete_result) == 1
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert len(delete_result.content) == 1
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
# Verify the note no longer exists by searching
|
||||
search_result = await client.call_tool(
|
||||
@@ -99,7 +99,10 @@ async def test_delete_note_by_permalink(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should have no results
|
||||
assert '"results": []' in search_result[0].text or '"results":[]' in search_result[0].text
|
||||
assert (
|
||||
'"results": []' in search_result.content[0].text
|
||||
or '"results":[]' in search_result.content[0].text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -142,8 +145,8 @@ The system handles multiple projects and users."""
|
||||
"identifier": "Project Management System",
|
||||
},
|
||||
)
|
||||
assert len(read_result) == 1
|
||||
result_text = read_result[0].text
|
||||
assert len(read_result.content) == 1
|
||||
result_text = read_result.content[0].text
|
||||
assert "Task tracking functionality" in result_text
|
||||
assert "depends_on" in result_text
|
||||
|
||||
@@ -156,7 +159,7 @@ The system handles multiple projects and users."""
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
# Verify the note and all its components are deleted
|
||||
read_after_delete_2 = await client.call_tool(
|
||||
@@ -167,8 +170,8 @@ The system handles multiple projects and users."""
|
||||
)
|
||||
|
||||
# Should return "Note Not Found" message
|
||||
assert len(read_after_delete_2) == 1
|
||||
result_text = read_after_delete_2[0].text
|
||||
assert len(read_after_delete_2.content) == 1
|
||||
result_text = read_after_delete_2.content[0].text
|
||||
assert "Note Not Found" in result_text
|
||||
assert "Project Management System" in result_text
|
||||
|
||||
@@ -209,7 +212,9 @@ async def test_delete_note_special_characters_in_title(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert "true" in delete_result[0].text.lower(), f"Failed to delete note: {title}"
|
||||
assert "true" in delete_result.content[0].text.lower(), (
|
||||
f"Failed to delete note: {title}"
|
||||
)
|
||||
|
||||
# Verify the note is deleted
|
||||
read_after_delete = await client.call_tool(
|
||||
@@ -220,8 +225,8 @@ async def test_delete_note_special_characters_in_title(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return "Note Not Found" message
|
||||
assert len(read_after_delete) == 1
|
||||
result_text = read_after_delete[0].text
|
||||
assert len(read_after_delete.content) == 1
|
||||
result_text = read_after_delete.content[0].text
|
||||
assert "Note Not Found" in result_text
|
||||
assert title in result_text
|
||||
|
||||
@@ -240,8 +245,8 @@ async def test_delete_nonexistent_note(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return False for unsuccessful deletion
|
||||
assert len(delete_result) == 1
|
||||
assert "false" in delete_result[0].text.lower()
|
||||
assert len(delete_result.content) == 1
|
||||
assert "false" in delete_result.content[0].text.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -269,7 +274,7 @@ async def test_delete_note_by_file_path(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
# Verify deletion
|
||||
read_after_delete = await client.call_tool(
|
||||
@@ -280,8 +285,8 @@ async def test_delete_note_by_file_path(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return "Note Not Found" message
|
||||
assert len(read_after_delete) == 1
|
||||
result_text = read_after_delete[0].text
|
||||
assert len(read_after_delete.content) == 1
|
||||
result_text = read_after_delete.content[0].text
|
||||
assert "Note Not Found" in result_text
|
||||
assert "File Path Delete" in result_text
|
||||
|
||||
@@ -311,7 +316,7 @@ async def test_delete_note_case_insensitive(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -349,7 +354,7 @@ async def test_delete_multiple_notes_sequentially(mcp_server, app):
|
||||
)
|
||||
|
||||
# Each deletion should be successful
|
||||
assert "true" in delete_result[0].text.lower(), f"Failed to delete {title}"
|
||||
assert "true" in delete_result.content[0].text.lower(), f"Failed to delete {title}"
|
||||
|
||||
# Verify all notes are deleted by searching
|
||||
search_result = await client.call_tool(
|
||||
@@ -360,7 +365,10 @@ async def test_delete_multiple_notes_sequentially(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should have no results
|
||||
assert '"results": []' in search_result[0].text or '"results":[]' in search_result[0].text
|
||||
assert (
|
||||
'"results": []' in search_result.content[0].text
|
||||
or '"results":[]' in search_result.content[0].text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -404,7 +412,7 @@ This note contains various Unicode characters:
|
||||
)
|
||||
|
||||
# Should return True for successful deletion
|
||||
assert "true" in delete_result[0].text.lower()
|
||||
assert "true" in delete_result.content[0].text.lower()
|
||||
|
||||
# Verify deletion
|
||||
read_after_delete = await client.call_tool(
|
||||
@@ -415,7 +423,7 @@ This note contains various Unicode characters:
|
||||
)
|
||||
|
||||
# Should return "Note Not Found" message
|
||||
assert len(read_after_delete) == 1
|
||||
result_text = read_after_delete[0].text
|
||||
assert len(read_after_delete.content) == 1
|
||||
result_text = read_after_delete.content[0].text
|
||||
assert "Note Not Found" in result_text
|
||||
assert "Unicode Test Note" in result_text
|
||||
|
||||
@@ -35,8 +35,8 @@ async def test_edit_note_append_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return successful edit summary
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (append)" in edit_text
|
||||
assert "Added 5 lines to end of note" in edit_text
|
||||
assert "test/append-test-note" in edit_text
|
||||
@@ -49,7 +49,7 @@ async def test_edit_note_append_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "Original content here." in content
|
||||
assert "## New Section" in content
|
||||
assert "This content was appended." in content
|
||||
@@ -82,8 +82,8 @@ async def test_edit_note_prepend_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return successful edit summary
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (prepend)" in edit_text
|
||||
assert "Added 5 lines to beginning of note" in edit_text
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_edit_note_prepend_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "## Important Update" in content
|
||||
assert "This was added at the top." in content
|
||||
assert "Existing content." in content
|
||||
@@ -143,8 +143,8 @@ v1.0.0 introduces new features.""",
|
||||
)
|
||||
|
||||
# Should return successful edit summary
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (find_replace)" in edit_text
|
||||
assert "Find and replace operation completed" in edit_text
|
||||
|
||||
@@ -156,7 +156,7 @@ v1.0.0 introduces new features.""",
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "v1.2.0" in content
|
||||
assert "v1.0.0" not in content # Should be completely replaced
|
||||
assert content.count("v1.2.0") == 3 # Should have exactly 3 occurrences
|
||||
@@ -206,8 +206,8 @@ All services communicate via message queues.""",
|
||||
)
|
||||
|
||||
# Should return successful edit summary
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (replace_section)" in edit_text
|
||||
assert "Replaced content under section '## Implementation'" in edit_text
|
||||
|
||||
@@ -219,7 +219,7 @@ All services communicate via message queues.""",
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "New implementation approach using microservices" in content
|
||||
assert "Old implementation details here" not in content
|
||||
assert "Service A handles authentication" in content
|
||||
@@ -282,8 +282,8 @@ Current endpoints include user management."""
|
||||
)
|
||||
|
||||
# Should return edit summary with observation and relation counts
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (append)" in edit_text
|
||||
assert "## Observations" in edit_text
|
||||
assert "## Relations" in edit_text
|
||||
@@ -301,7 +301,7 @@ Current endpoints include user management."""
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "Added payment processing endpoints" in content
|
||||
assert "integrates_with [[Payment Gateway]]" in content
|
||||
|
||||
@@ -322,8 +322,8 @@ async def test_edit_note_error_handling_note_not_found(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return helpful error message
|
||||
assert len(edit_result) == 1
|
||||
error_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
error_text = edit_result.content[0].text
|
||||
assert "Edit Failed" in error_text
|
||||
assert "Non-existent Note" in error_text
|
||||
assert "search_notes(" in error_text
|
||||
@@ -357,8 +357,8 @@ async def test_edit_note_error_handling_text_not_found(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return helpful error message
|
||||
assert len(edit_result) == 1
|
||||
error_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
error_text = edit_result.content[0].text
|
||||
assert "Edit Failed - Text Not Found" in error_text
|
||||
assert "non-existent text" in error_text
|
||||
assert "Error Test Note" in error_text
|
||||
@@ -398,8 +398,8 @@ Final test of the content.""",
|
||||
)
|
||||
|
||||
# Should return helpful error message about count mismatch
|
||||
assert len(edit_result) == 1
|
||||
error_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
error_text = edit_result.content[0].text
|
||||
assert "Edit Failed - Wrong Replacement Count" in error_text
|
||||
assert "Expected 5 occurrences" in error_text
|
||||
assert "test" in error_text
|
||||
@@ -521,8 +521,8 @@ def test_function():
|
||||
)
|
||||
|
||||
# Should successfully handle special characters
|
||||
assert len(edit_result) == 1
|
||||
edit_text = edit_result[0].text
|
||||
assert len(edit_result.content) == 1
|
||||
edit_text = edit_result.content[0].text
|
||||
assert "Edited note (append)" in edit_text
|
||||
assert "## Observations" in edit_text
|
||||
assert "unicode:" in edit_text
|
||||
@@ -536,7 +536,7 @@ def test_function():
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "🚀" in content
|
||||
assert "测试中文" in content
|
||||
assert "∑∏∂∇∆Ω" in content
|
||||
@@ -569,7 +569,7 @@ async def test_edit_note_using_different_identifiers(mcp_server, app):
|
||||
"content": "\n\nEdited by title.",
|
||||
},
|
||||
)
|
||||
assert "Edited note (append)" in edit_result1[0].text
|
||||
assert "Edited note (append)" in edit_result1.content[0].text
|
||||
|
||||
# Test editing by permalink
|
||||
edit_result2 = await client.call_tool(
|
||||
@@ -580,7 +580,7 @@ async def test_edit_note_using_different_identifiers(mcp_server, app):
|
||||
"content": "\n\nEdited by permalink.",
|
||||
},
|
||||
)
|
||||
assert "Edited note (append)" in edit_result2[0].text
|
||||
assert "Edited note (append)" in edit_result2.content[0].text
|
||||
|
||||
# Test editing by folder/title format
|
||||
edit_result3 = await client.call_tool(
|
||||
@@ -591,7 +591,7 @@ async def test_edit_note_using_different_identifiers(mcp_server, app):
|
||||
"content": "\n\nEdited by folder/title.",
|
||||
},
|
||||
)
|
||||
assert "Edited note (append)" in edit_result3[0].text
|
||||
assert "Edited note (append)" in edit_result3.content[0].text
|
||||
|
||||
# Verify all edits were applied
|
||||
read_result = await client.call_tool(
|
||||
@@ -601,7 +601,7 @@ async def test_edit_note_using_different_identifiers(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "Edited by title." in content
|
||||
assert "Edited by permalink." in content
|
||||
assert "Edited by folder/title." in content
|
||||
|
||||
@@ -54,8 +54,8 @@ async def test_list_directory_basic_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return formatted directory listing
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show the structure
|
||||
assert "Contents of '/' (depth 1):" in list_text
|
||||
@@ -113,8 +113,8 @@ async def test_list_directory_specific_folder(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show work folder contents
|
||||
assert "Contents of '/work' (depth 1):" in list_text
|
||||
@@ -169,8 +169,8 @@ async def test_list_directory_with_depth(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show nested structure within depth=3
|
||||
assert "Contents of '/research' (depth 3):" in list_text
|
||||
@@ -226,8 +226,8 @@ async def test_list_directory_with_glob_pattern(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show only matching files
|
||||
assert "Files in '/meetings' matching 'Meeting*' (depth 1):" in list_text
|
||||
@@ -250,8 +250,8 @@ async def test_list_directory_empty_directory(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should indicate no files found
|
||||
assert "No files found in directory '/empty'" in list_text
|
||||
@@ -283,8 +283,8 @@ async def test_list_directory_glob_no_matches(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should indicate no matches for the pattern
|
||||
assert "No files found in directory '/docs' matching '*.py'" in list_text
|
||||
@@ -325,8 +325,8 @@ async def test_list_directory_various_file_types(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show file names, paths, and titles
|
||||
assert "📄 Simple Note.md" in list_text
|
||||
@@ -358,8 +358,8 @@ async def test_list_directory_default_parameters(mcp_server, app):
|
||||
{}, # Use all defaults
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show root directory with depth 1
|
||||
assert "Contents of '/' (depth 1):" in list_text
|
||||
@@ -402,8 +402,8 @@ async def test_list_directory_deep_recursion(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show deep structure
|
||||
assert "Contents of '/level1' (depth 10):" in list_text
|
||||
@@ -457,11 +457,87 @@ async def test_list_directory_complex_glob_patterns(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show only Project files
|
||||
assert "Project Alpha Plan.md" in list_text
|
||||
assert "Project Beta Plan.md" in list_text
|
||||
assert "Meeting Minutes" not in list_text
|
||||
assert "matching 'Project*'" in list_text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_dot_slash_prefix_paths(mcp_server, app):
|
||||
"""Test directory listing with ./ prefix paths (reproduces bug report issue)."""
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
# Create test files in a subdirectory
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"title": "Artifact One",
|
||||
"folder": "artifacts",
|
||||
"content": "# Artifact One\n\nFirst artifact document.",
|
||||
"tags": "artifact,test",
|
||||
},
|
||||
)
|
||||
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"title": "Artifact Two",
|
||||
"folder": "artifacts",
|
||||
"content": "# Artifact Two\n\nSecond artifact document.",
|
||||
"tags": "artifact,test",
|
||||
},
|
||||
)
|
||||
|
||||
# Test normal path without ./ prefix (should work)
|
||||
normal_result = await client.call_tool(
|
||||
"list_directory",
|
||||
{
|
||||
"dir_name": "artifacts",
|
||||
"depth": 1,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(normal_result.content) == 1
|
||||
normal_text = normal_result.content[0].text
|
||||
assert "Artifact One.md" in normal_text
|
||||
assert "Artifact Two.md" in normal_text
|
||||
assert "2 files" in normal_text
|
||||
|
||||
# Test with ./ prefix (this was the failing case in the bug report)
|
||||
dot_slash_result = await client.call_tool(
|
||||
"list_directory",
|
||||
{
|
||||
"dir_name": "./artifacts",
|
||||
"depth": 1,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(dot_slash_result.content) == 1
|
||||
dot_slash_text = dot_slash_result.content[0].text
|
||||
|
||||
# Should show the same files as normal path
|
||||
assert "Artifact One.md" in dot_slash_text
|
||||
assert "Artifact Two.md" in dot_slash_text
|
||||
assert "2 files" in dot_slash_text
|
||||
|
||||
# Test with trailing slash after ./ prefix
|
||||
dot_slash_trailing_result = await client.call_tool(
|
||||
"list_directory",
|
||||
{
|
||||
"dir_name": "./artifacts/",
|
||||
"depth": 1,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(dot_slash_trailing_result.content) == 1
|
||||
dot_slash_trailing_text = dot_slash_trailing_result.content[0].text
|
||||
|
||||
# Should show the same files
|
||||
assert "Artifact One.md" in dot_slash_trailing_text
|
||||
assert "Artifact Two.md" in dot_slash_trailing_text
|
||||
assert "2 files" in dot_slash_trailing_text
|
||||
|
||||
@@ -34,8 +34,8 @@ async def test_move_note_basic_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return successful move message
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "Move Test Note" in move_text
|
||||
assert "destination/moved-note.md" in move_text
|
||||
@@ -49,7 +49,7 @@ async def test_move_note_basic_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "This note will be moved to a new location" in content
|
||||
|
||||
# Verify the original location no longer works
|
||||
@@ -61,7 +61,7 @@ async def test_move_note_basic_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return "Note Not Found" message
|
||||
assert "Note Not Found" in read_original[0].text
|
||||
assert "Note Not Found" in read_original.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -90,8 +90,8 @@ async def test_move_note_using_permalink(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should successfully move
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "test/permalink-move-test" in move_text
|
||||
assert "archive/permalink-moved.md" in move_text
|
||||
@@ -104,7 +104,7 @@ async def test_move_note_using_permalink(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert "Moving by permalink" in read_result[0].text
|
||||
assert "Moving by permalink" in read_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -150,8 +150,8 @@ This note demonstrates moving complex content."""
|
||||
)
|
||||
|
||||
# Should successfully move
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "Complex Note" in move_text
|
||||
assert "moved/complex-note.md" in move_text
|
||||
@@ -164,7 +164,7 @@ This note demonstrates moving complex content."""
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "Has structured observations" in content
|
||||
assert "implements [[Auth System]]" in content
|
||||
assert "## Observations" in content
|
||||
@@ -198,8 +198,8 @@ async def test_move_note_to_nested_directory(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should successfully create directory structure and move
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "Nested Move Test" in move_text
|
||||
assert "projects/2025/q2/work/nested-note.md" in move_text
|
||||
@@ -212,7 +212,7 @@ async def test_move_note_to_nested_directory(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert "This will be moved deep" in read_result[0].text
|
||||
assert "This will be moved deep" in read_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -241,8 +241,8 @@ async def test_move_note_with_special_characters(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should handle special characters properly
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "archive/special-chars-note.md" in move_text
|
||||
|
||||
@@ -254,7 +254,7 @@ async def test_move_note_with_special_characters(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert "Testing special characters in move" in read_result[0].text
|
||||
assert "Testing special characters in move" in read_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -272,8 +272,8 @@ async def test_move_note_error_handling_note_not_found(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should contain error message about the failed operation
|
||||
assert len(move_result) == 1
|
||||
error_message = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
error_message = move_result.content[0].text
|
||||
assert "# Move Failed" in error_message
|
||||
assert "Non-existent Note" in error_message
|
||||
|
||||
@@ -304,8 +304,8 @@ async def test_move_note_error_handling_invalid_destination(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should contain error message about the failed operation
|
||||
assert len(move_result) == 1
|
||||
error_message = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
error_message = move_result.content[0].text
|
||||
assert "# Move Failed" in error_message
|
||||
assert "/absolute/path/note.md" in error_message
|
||||
|
||||
@@ -347,8 +347,8 @@ async def test_move_note_error_handling_destination_exists(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should contain error message about the failed operation
|
||||
assert len(move_result) == 1
|
||||
error_message = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
error_message = move_result.content[0].text
|
||||
assert "# Move Failed" in error_message
|
||||
assert "already exists" in error_message
|
||||
|
||||
@@ -389,8 +389,8 @@ This note contains unique search terms:
|
||||
},
|
||||
)
|
||||
|
||||
assert len(search_before) > 0
|
||||
assert "Searchable Note" in search_before[0].text
|
||||
assert len(search_before.content) > 0
|
||||
assert "Searchable Note" in search_before.content[0].text
|
||||
|
||||
# Move the note
|
||||
move_result = await client.call_tool(
|
||||
@@ -401,8 +401,8 @@ This note contains unique search terms:
|
||||
},
|
||||
)
|
||||
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
|
||||
# Verify note is still searchable after move
|
||||
@@ -413,8 +413,8 @@ This note contains unique search terms:
|
||||
},
|
||||
)
|
||||
|
||||
assert len(search_after) > 0
|
||||
search_text = search_after[0].text
|
||||
assert len(search_after.content) > 0
|
||||
search_text = search_after.content[0].text
|
||||
assert "quantum mechanics" in search_text
|
||||
assert "research/quantum-ai-note.md" in search_text or "quantum-ai-note" in search_text
|
||||
|
||||
@@ -426,7 +426,7 @@ This note contains unique search terms:
|
||||
},
|
||||
)
|
||||
|
||||
assert len(search_by_path) > 0
|
||||
assert len(search_by_path.content) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -473,8 +473,8 @@ async def test_move_note_using_different_identifier_formats(mcp_server, app):
|
||||
"destination_path": "moved/title-moved.md",
|
||||
},
|
||||
)
|
||||
assert len(move1) == 1
|
||||
assert "✅ Note moved successfully" in move1[0].text
|
||||
assert len(move1.content) == 1
|
||||
assert "✅ Note moved successfully" in move1.content[0].text
|
||||
|
||||
# Test moving by permalink
|
||||
move2 = await client.call_tool(
|
||||
@@ -484,8 +484,8 @@ async def test_move_note_using_different_identifier_formats(mcp_server, app):
|
||||
"destination_path": "moved/permalink-moved.md",
|
||||
},
|
||||
)
|
||||
assert len(move2) == 1
|
||||
assert "✅ Note moved successfully" in move2[0].text
|
||||
assert len(move2.content) == 1
|
||||
assert "✅ Note moved successfully" in move2.content[0].text
|
||||
|
||||
# Test moving by folder/title format
|
||||
move3 = await client.call_tool(
|
||||
@@ -495,18 +495,18 @@ async def test_move_note_using_different_identifier_formats(mcp_server, app):
|
||||
"destination_path": "moved/folder-title-moved.md",
|
||||
},
|
||||
)
|
||||
assert len(move3) == 1
|
||||
assert "✅ Note moved successfully" in move3[0].text
|
||||
assert len(move3.content) == 1
|
||||
assert "✅ Note moved successfully" in move3.content[0].text
|
||||
|
||||
# Verify all notes can be accessed at their new locations
|
||||
read1 = await client.call_tool("read_note", {"identifier": "moved/title-moved.md"})
|
||||
assert "Move by title" in read1[0].text
|
||||
assert "Move by title" in read1.content[0].text
|
||||
|
||||
read2 = await client.call_tool("read_note", {"identifier": "moved/permalink-moved.md"})
|
||||
assert "Move by permalink" in read2[0].text
|
||||
assert "Move by permalink" in read2.content[0].text
|
||||
|
||||
read3 = await client.call_tool("read_note", {"identifier": "moved/folder-title-moved.md"})
|
||||
assert "Move by folder/title" in read3[0].text
|
||||
assert "Move by folder/title" in read3.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -545,8 +545,8 @@ async def test_move_note_cross_project_detection(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should detect cross-project attempt and provide helpful guidance
|
||||
assert len(move_result) == 1
|
||||
error_message = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
error_message = move_result.content[0].text
|
||||
assert "Cross-Project Move Not Supported" in error_message
|
||||
assert "test-project-b" in error_message
|
||||
assert "switch_project" in error_message
|
||||
@@ -554,50 +554,6 @@ async def test_move_note_cross_project_detection(mcp_server, app):
|
||||
assert "write_note" in error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_potential_cross_project_guidance(mcp_server, app):
|
||||
"""Test guidance for potentially cross-project moves with project-like keywords."""
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
# Create another test project
|
||||
await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": "workspace-docs",
|
||||
"project_path": "/tmp/workspace-docs",
|
||||
"set_default": False,
|
||||
},
|
||||
)
|
||||
|
||||
# Create a note in the default project
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"title": "Potential Cross Project Note",
|
||||
"folder": "source",
|
||||
"content": "# Potential Cross Project Note\n\nThis might be moved cross-project.",
|
||||
"tags": "test,potential-cross-project",
|
||||
},
|
||||
)
|
||||
|
||||
# Try to move to a path that contains project-like keywords but not exact project names
|
||||
move_result = await client.call_tool(
|
||||
"move_note",
|
||||
{
|
||||
"identifier": "Potential Cross Project Note",
|
||||
"destination_path": "project-archive/moved-note.md",
|
||||
},
|
||||
)
|
||||
|
||||
# Should provide guidance for potential cross-project moves
|
||||
assert len(move_result) == 1
|
||||
error_message = move_result[0].text
|
||||
assert "Check Project Context" in error_message
|
||||
assert "workspace-docs" in error_message # Should mention other available projects
|
||||
assert "list_projects" in error_message
|
||||
assert "switch_project" in error_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_normal_moves_still_work(mcp_server, app):
|
||||
"""Test that normal within-project moves still work after cross-project detection."""
|
||||
@@ -624,8 +580,8 @@ async def test_move_note_normal_moves_still_work(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should work normally
|
||||
assert len(move_result) == 1
|
||||
move_text = move_result[0].text
|
||||
assert len(move_result.content) == 1
|
||||
move_text = move_result.content[0].text
|
||||
assert "✅ Note moved successfully" in move_text
|
||||
assert "Normal Move Note" in move_text
|
||||
assert "destination/normal-moved.md" in move_text
|
||||
@@ -638,5 +594,5 @@ async def test_move_note_normal_moves_still_work(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
content = read_result[0].text
|
||||
content = read_result.content[0].text
|
||||
assert "This should move normally" in content
|
||||
|
||||
@@ -20,8 +20,8 @@ async def test_list_projects_basic_operation(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should return formatted project list
|
||||
assert len(list_result) == 1
|
||||
list_text = list_result[0].text
|
||||
assert len(list_result.content) == 1
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
# Should show available projects with status indicators
|
||||
assert "Available projects:" in list_text
|
||||
@@ -52,8 +52,8 @@ async def test_get_current_project_operation(mcp_server, app):
|
||||
{},
|
||||
)
|
||||
|
||||
assert len(current_result) == 1
|
||||
current_text = current_result[0].text
|
||||
assert len(current_result.content) == 1
|
||||
current_text = current_result.content[0].text
|
||||
|
||||
# Should show current project and stats
|
||||
assert "Current project: test-project" in current_text
|
||||
@@ -114,8 +114,8 @@ This is the second entity.
|
||||
{},
|
||||
)
|
||||
|
||||
assert len(current_result) == 1
|
||||
current_text = current_result[0].text
|
||||
assert len(current_result.content) == 1
|
||||
current_text = current_result.content[0].text
|
||||
|
||||
# Should show entity and observation counts
|
||||
assert "Current project: test-project" in current_text
|
||||
@@ -144,8 +144,8 @@ async def test_switch_project_not_found(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(switch_result) == 1
|
||||
switch_text = switch_result[0].text
|
||||
assert len(switch_result.content) == 1
|
||||
switch_text = switch_result.content[0].text
|
||||
|
||||
# Should show error message with available projects
|
||||
assert "Error: Project 'non-existent-project' not found" in switch_text
|
||||
@@ -166,8 +166,8 @@ async def test_switch_project_to_test_project(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(switch_result) == 1
|
||||
switch_text = switch_result[0].text
|
||||
assert len(switch_result.content) == 1
|
||||
switch_text = switch_result.content[0].text
|
||||
|
||||
# Should show successful switch
|
||||
assert "✓ Switched to test-project project" in switch_text
|
||||
@@ -189,8 +189,8 @@ async def test_set_default_project_operation(mcp_server, app):
|
||||
{},
|
||||
)
|
||||
|
||||
assert len(current_result) == 1
|
||||
current_text = current_result[0].text
|
||||
assert len(current_result.content) == 1
|
||||
current_text = current_result.content[0].text
|
||||
|
||||
# Should show current project and stats
|
||||
assert "Current project: test-project" in current_text
|
||||
@@ -203,8 +203,8 @@ async def test_set_default_project_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(default_result) == 1
|
||||
default_text = default_result[0].text
|
||||
assert len(default_result.content) == 1
|
||||
default_text = default_result.content[0].text
|
||||
|
||||
# Should show success message and restart instructions
|
||||
assert "✓" in default_text # Success indicator
|
||||
@@ -245,20 +245,20 @@ async def test_project_management_workflow(mcp_server, app):
|
||||
async with Client(mcp_server) as client:
|
||||
# 1. Check current project
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
assert "test-project" in current_result[0].text
|
||||
assert "test-project" in current_result.content[0].text
|
||||
|
||||
# 2. List all projects
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert "Available projects:" in list_result[0].text
|
||||
assert "test-project" in list_result[0].text
|
||||
assert "Available projects:" in list_result.content[0].text
|
||||
assert "test-project" in list_result.content[0].text
|
||||
|
||||
# 3. Switch to same project (should work)
|
||||
switch_result = await client.call_tool("switch_project", {"project_name": "test-project"})
|
||||
assert "✓ Switched to test-project project" in switch_result[0].text
|
||||
assert "✓ Switched to test-project project" in switch_result.content[0].text
|
||||
|
||||
# 4. Verify we're still on the same project
|
||||
current_result2 = await client.call_tool("get_current_project", {})
|
||||
assert "Current project: test-project" in current_result2[0].text
|
||||
assert "Current project: test-project" in current_result2.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -270,22 +270,22 @@ async def test_project_metadata_consistency(mcp_server, app):
|
||||
|
||||
# list_projects
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert "Project: test-project" in list_result[0].text
|
||||
assert "Project: test-project" in list_result.content[0].text
|
||||
|
||||
# get_current_project
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
assert "Project: test-project" in current_result[0].text
|
||||
assert "Project: test-project" in current_result.content[0].text
|
||||
|
||||
# switch_project
|
||||
switch_result = await client.call_tool("switch_project", {"project_name": "test-project"})
|
||||
assert "Project: test-project" in switch_result[0].text
|
||||
assert "Project: test-project" in switch_result.content[0].text
|
||||
|
||||
# set_default_project (skip since API not working in test env)
|
||||
# default_result = await client.call_tool(
|
||||
# "set_default_project",
|
||||
# {"project_name": "test-project"}
|
||||
# )
|
||||
# assert "Project: test-project" in default_result[0].text
|
||||
# assert "Project: test-project" in default_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -295,7 +295,7 @@ async def test_project_statistics_accuracy(mcp_server, app):
|
||||
async with Client(mcp_server) as client:
|
||||
# Get initial stats
|
||||
initial_result = await client.call_tool("get_current_project", {})
|
||||
initial_text = initial_result[0].text
|
||||
initial_text = initial_result.content[0].text
|
||||
assert initial_text is not None
|
||||
|
||||
# Create a new entity
|
||||
@@ -320,7 +320,7 @@ Testing statistics accuracy.
|
||||
|
||||
# Get updated stats
|
||||
updated_result = await client.call_tool("get_current_project", {})
|
||||
updated_text = updated_result[0].text
|
||||
updated_text = updated_result.content[0].text
|
||||
|
||||
# Should show project info with stats
|
||||
assert "Current project: test-project" in updated_text
|
||||
@@ -357,8 +357,8 @@ async def test_create_project_basic_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(create_result) == 1
|
||||
create_text = create_result[0].text
|
||||
assert len(create_result.content) == 1
|
||||
create_text = create_result.content[0].text
|
||||
|
||||
# Should show success message and project details
|
||||
assert "✓" in create_text # Success indicator
|
||||
@@ -371,7 +371,7 @@ async def test_create_project_basic_operation(mcp_server, app):
|
||||
|
||||
# Verify project appears in project list
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
list_text = list_result[0].text
|
||||
list_text = list_result.content[0].text
|
||||
assert "test-new-project" in list_text
|
||||
|
||||
|
||||
@@ -390,8 +390,8 @@ async def test_create_project_with_default_flag(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(create_result) == 1
|
||||
create_text = create_result[0].text
|
||||
assert len(create_result.content) == 1
|
||||
create_text = create_result.content[0].text
|
||||
|
||||
# Should show success and default flag
|
||||
assert "✓" in create_text
|
||||
@@ -401,7 +401,7 @@ async def test_create_project_with_default_flag(mcp_server, app):
|
||||
|
||||
# Verify we switched to the new project
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
current_text = current_result[0].text
|
||||
current_text = current_result.content[0].text
|
||||
assert "Current project: test-default-project" in current_text
|
||||
|
||||
|
||||
@@ -455,7 +455,7 @@ async def test_delete_project_basic_operation(mcp_server, app):
|
||||
|
||||
# Verify it exists
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert "to-be-deleted" in list_result[0].text
|
||||
assert "to-be-deleted" in list_result.content[0].text
|
||||
|
||||
# Delete the project
|
||||
delete_result = await client.call_tool(
|
||||
@@ -465,8 +465,8 @@ async def test_delete_project_basic_operation(mcp_server, app):
|
||||
},
|
||||
)
|
||||
|
||||
assert len(delete_result) == 1
|
||||
delete_text = delete_result[0].text
|
||||
assert len(delete_result.content) == 1
|
||||
delete_text = delete_result.content[0].text
|
||||
|
||||
# Should show success message
|
||||
assert "✓" in delete_text
|
||||
@@ -479,7 +479,7 @@ async def test_delete_project_basic_operation(mcp_server, app):
|
||||
|
||||
# Verify project no longer appears in list
|
||||
list_result_after = await client.call_tool("list_memory_projects", {})
|
||||
assert "to-be-deleted" not in list_result_after[0].text
|
||||
assert "to-be-deleted" not in list_result_after.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -546,8 +546,8 @@ async def test_project_lifecycle_workflow(mcp_server, app):
|
||||
"project_path": project_path,
|
||||
},
|
||||
)
|
||||
assert "✓" in create_result[0].text
|
||||
assert project_name in create_result[0].text
|
||||
assert "✓" in create_result.content[0].text
|
||||
assert project_name in create_result.content[0].text
|
||||
|
||||
# 2. Switch to the new project
|
||||
switch_result = await client.call_tool(
|
||||
@@ -556,7 +556,7 @@ async def test_project_lifecycle_workflow(mcp_server, app):
|
||||
"project_name": project_name,
|
||||
},
|
||||
)
|
||||
assert f"✓ Switched to {project_name} project" in switch_result[0].text
|
||||
assert f"✓ Switched to {project_name} project" in switch_result.content[0].text
|
||||
|
||||
# 3. Create content in the new project
|
||||
await client.call_tool(
|
||||
@@ -571,7 +571,7 @@ async def test_project_lifecycle_workflow(mcp_server, app):
|
||||
|
||||
# 4. Verify project stats show our content
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
current_text = current_result[0].text
|
||||
current_text = current_result.content[0].text
|
||||
assert f"Current project: {project_name}" in current_text
|
||||
assert "entities" in current_text
|
||||
|
||||
@@ -590,13 +590,13 @@ async def test_project_lifecycle_workflow(mcp_server, app):
|
||||
"project_name": project_name,
|
||||
},
|
||||
)
|
||||
assert "✓" in delete_result[0].text
|
||||
assert f"{project_name}" in delete_result[0].text
|
||||
assert "removed successfully" in delete_result[0].text
|
||||
assert "✓" in delete_result.content[0].text
|
||||
assert f"{project_name}" in delete_result.content[0].text
|
||||
assert "removed successfully" in delete_result.content[0].text
|
||||
|
||||
# 7. Verify project is gone from list
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert project_name not in list_result[0].text
|
||||
assert project_name not in list_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -604,23 +604,23 @@ async def test_create_delete_project_edge_cases(mcp_server, app):
|
||||
"""Test edge cases for create and delete project operations."""
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
# Test with special characters in project name (should be handled gracefully)
|
||||
special_name = "test-project-with-dashes"
|
||||
# Test with special characters and spaces in project name (should be handled gracefully)
|
||||
special_name = "test project with spaces & symbols!"
|
||||
|
||||
# Create project with special characters
|
||||
create_result = await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": special_name,
|
||||
"project_path": f"/tmp/{special_name}",
|
||||
"project_path": "/tmp/test-project-with-special-chars",
|
||||
},
|
||||
)
|
||||
assert "✓" in create_result[0].text
|
||||
assert special_name in create_result[0].text
|
||||
assert "✓" in create_result.content[0].text
|
||||
assert special_name in create_result.content[0].text
|
||||
|
||||
# Verify it appears in list
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert special_name in list_result[0].text
|
||||
assert special_name in list_result.content[0].text
|
||||
|
||||
# Delete it
|
||||
delete_result = await client.call_tool(
|
||||
@@ -629,12 +629,12 @@ async def test_create_delete_project_edge_cases(mcp_server, app):
|
||||
"project_name": special_name,
|
||||
},
|
||||
)
|
||||
assert "✓" in delete_result[0].text
|
||||
assert special_name in delete_result[0].text
|
||||
assert "✓" in delete_result.content[0].text
|
||||
assert special_name in delete_result.content[0].text
|
||||
|
||||
# Verify it's gone
|
||||
list_result_after = await client.call_tool("list_memory_projects", {})
|
||||
assert special_name not in list_result_after[0].text
|
||||
assert special_name not in list_result_after.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -651,12 +651,12 @@ async def test_case_insensitive_project_switching(mcp_server, app):
|
||||
"project_path": f"/tmp/{project_name}",
|
||||
},
|
||||
)
|
||||
assert "✓" in create_result[0].text
|
||||
assert project_name in create_result[0].text
|
||||
assert "✓" in create_result.content[0].text
|
||||
assert project_name in create_result.content[0].text
|
||||
|
||||
# Verify project was created with canonical name
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
assert project_name in list_result[0].text
|
||||
assert project_name in list_result.content[0].text
|
||||
|
||||
# Test switching with different case variations
|
||||
test_cases = [
|
||||
@@ -674,18 +674,18 @@ async def test_case_insensitive_project_switching(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should succeed and show canonical name in response
|
||||
assert "✓ Switched to" in switch_result[0].text
|
||||
assert project_name in switch_result[0].text # Canonical name should appear
|
||||
assert "✓ Switched to" in switch_result.content[0].text
|
||||
assert project_name in switch_result.content[0].text # Canonical name should appear
|
||||
# Project summary may be unavailable in test environment
|
||||
assert (
|
||||
"Project Summary:" in switch_result[0].text
|
||||
or "Project summary unavailable" in switch_result[0].text
|
||||
"Project Summary:" in switch_result.content[0].text
|
||||
or "Project summary unavailable" in switch_result.content[0].text
|
||||
)
|
||||
|
||||
# Verify get_current_project works after case-insensitive switch
|
||||
try:
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
current_text = current_result[0].text
|
||||
current_text = current_result.content[0].text
|
||||
|
||||
# Should show canonical project name, not the input case
|
||||
assert f"Current project: {project_name}" in current_text
|
||||
@@ -715,15 +715,15 @@ async def test_case_insensitive_project_operations(mcp_server, app):
|
||||
"project_path": f"/tmp/{project_name}",
|
||||
},
|
||||
)
|
||||
assert "✓" in create_result[0].text
|
||||
assert "✓" in create_result.content[0].text
|
||||
|
||||
# Switch to project using lowercase input
|
||||
switch_result = await client.call_tool(
|
||||
"switch_project",
|
||||
{"project_name": "camel-case-project"}, # lowercase input
|
||||
)
|
||||
assert "✓ Switched to" in switch_result[0].text
|
||||
assert project_name in switch_result[0].text # Should show canonical name
|
||||
assert "✓ Switched to" in switch_result.content[0].text
|
||||
assert project_name in switch_result.content[0].text # Should show canonical name
|
||||
|
||||
# Test that MCP operations work correctly after case-insensitive switch
|
||||
|
||||
@@ -737,12 +737,12 @@ async def test_case_insensitive_project_operations(mcp_server, app):
|
||||
"tags": "case,test",
|
||||
},
|
||||
)
|
||||
assert len(write_result) == 1
|
||||
assert "Case Test Note" in write_result[0].text
|
||||
assert len(write_result.content) == 1
|
||||
assert "Case Test Note" in write_result.content[0].text
|
||||
|
||||
# 2. Verify get_current_project shows stats correctly
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
current_text = current_result[0].text
|
||||
current_text = current_result.content[0].text
|
||||
assert f"Current project: {project_name}" in current_text
|
||||
assert "1 entities" in current_text or "entities" in current_text
|
||||
|
||||
@@ -751,17 +751,17 @@ async def test_case_insensitive_project_operations(mcp_server, app):
|
||||
"search_notes",
|
||||
{"query": "case insensitive"},
|
||||
)
|
||||
assert len(search_result) == 1
|
||||
assert "Case Test Note" in search_result[0].text
|
||||
assert len(search_result.content) == 1
|
||||
assert "Case Test Note" in search_result.content[0].text
|
||||
|
||||
# 4. Test read_note works
|
||||
read_result = await client.call_tool(
|
||||
"read_note",
|
||||
{"identifier": "Case Test Note"},
|
||||
)
|
||||
assert len(read_result) == 1
|
||||
assert "Case Test Note" in read_result[0].text
|
||||
assert "case insensitive" in read_result[0].text.lower()
|
||||
assert len(read_result.content) == 1
|
||||
assert "Case Test Note" in read_result.content[0].text
|
||||
assert "case insensitive" in read_result.content[0].text.lower()
|
||||
|
||||
# Clean up
|
||||
await client.call_tool("switch_project", {"project_name": "test-project"})
|
||||
@@ -788,9 +788,9 @@ async def test_case_insensitive_error_handling(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should show error for all case variations
|
||||
assert f"Error: Project '{test_case}' not found" in switch_result[0].text
|
||||
assert "Available projects:" in switch_result[0].text
|
||||
assert "test-project" in switch_result[0].text
|
||||
assert f"Error: Project '{test_case}' not found" in switch_result.content[0].text
|
||||
assert "Available projects:" in switch_result.content[0].text
|
||||
assert "test-project" in switch_result.content[0].text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -818,7 +818,7 @@ async def test_case_preservation_in_project_list(mcp_server, app):
|
||||
|
||||
# List projects and verify each appears with its original case
|
||||
list_result = await client.call_tool("list_memory_projects", {})
|
||||
list_text = list_result[0].text
|
||||
list_text = list_result.content[0].text
|
||||
|
||||
for project_name in test_projects:
|
||||
assert project_name in list_text, f"Project {project_name} not found in list"
|
||||
@@ -833,12 +833,12 @@ async def test_case_preservation_in_project_list(mcp_server, app):
|
||||
)
|
||||
|
||||
# Should succeed and show original case in response
|
||||
assert "✓ Switched to" in switch_result[0].text
|
||||
assert project_name in switch_result[0].text # Original case preserved
|
||||
assert "✓ Switched to" in switch_result.content[0].text
|
||||
assert project_name in switch_result.content[0].text # Original case preserved
|
||||
|
||||
# Verify current project shows original case
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
assert f"Current project: {project_name}" in current_result[0].text
|
||||
assert f"Current project: {project_name}" in current_result.content[0].text
|
||||
|
||||
# Clean up - switch back and delete test projects
|
||||
await client.call_tool("switch_project", {"project_name": "test-project"})
|
||||
@@ -888,14 +888,17 @@ async def test_session_state_consistency_after_case_switch(mcp_server, app):
|
||||
|
||||
# All operations should work and reference the canonical project name
|
||||
if op_name == "get_current_project":
|
||||
assert f"Current project: {project_name}" in result[0].text
|
||||
assert f"Current project: {project_name}" in result.content[0].text
|
||||
elif op_name == "list_memory_projects":
|
||||
assert project_name in result[0].text
|
||||
assert "(current)" in result[0].text or "current" in result[0].text.lower()
|
||||
assert project_name in result.content[0].text
|
||||
assert (
|
||||
"(current)" in result.content[0].text
|
||||
or "current" in result.content[0].text.lower()
|
||||
)
|
||||
|
||||
# All operations should include project metadata with canonical name
|
||||
# FIXME
|
||||
# assert f"Project: {project_name}" in result[0].text
|
||||
# assert f"Project: {project_name}" in result.content[0].text
|
||||
|
||||
# Clean up
|
||||
await client.call_tool("switch_project", {"project_name": "test-project"})
|
||||
|
||||
@@ -21,8 +21,8 @@ async def test_project_state_sync_after_default_change(mcp_server, app, config_m
|
||||
async with Client(mcp_server) as client:
|
||||
# Step 1: Verify initial state - MCP should show test-project as current
|
||||
initial_result = await client.call_tool("get_current_project", {})
|
||||
assert len(initial_result) == 1
|
||||
assert "Current project: test-project" in initial_result[0].text
|
||||
assert len(initial_result.content) == 1
|
||||
assert "Current project: test-project" in initial_result.content[0].text
|
||||
|
||||
# Step 2: Create a second project that we can switch to
|
||||
create_result = await client.call_tool(
|
||||
@@ -33,26 +33,26 @@ async def test_project_state_sync_after_default_change(mcp_server, app, config_m
|
||||
"set_default": False, # Don't set as default yet
|
||||
},
|
||||
)
|
||||
assert len(create_result) == 1
|
||||
assert "✓" in create_result[0].text
|
||||
assert "minerva" in create_result[0].text
|
||||
assert len(create_result.content) == 1
|
||||
assert "✓" in create_result.content[0].text
|
||||
assert "minerva" in create_result.content[0].text
|
||||
|
||||
# Step 3: Change default project to minerva via set_default_project tool
|
||||
# This simulates the CLI command `basic-memory project default minerva`
|
||||
set_default_result = await client.call_tool(
|
||||
"set_default_project", {"project_name": "minerva"}
|
||||
)
|
||||
assert len(set_default_result) == 1
|
||||
assert "✓" in set_default_result[0].text
|
||||
assert "minerva" in set_default_result[0].text
|
||||
assert len(set_default_result.content) == 1
|
||||
assert "✓" in set_default_result.content[0].text
|
||||
assert "minerva" in set_default_result.content[0].text
|
||||
|
||||
# Step 4: Verify MCP session immediately reflects the change (no restart needed)
|
||||
# This tests the fix - session.refresh_from_config() should have been called
|
||||
updated_result = await client.call_tool("get_current_project", {})
|
||||
assert len(updated_result) == 1
|
||||
assert len(updated_result.content) == 1
|
||||
|
||||
# The fix should ensure these are consistent now:
|
||||
updated_text = updated_result[0].text
|
||||
updated_text = updated_result.content[0].text
|
||||
assert "Current project: minerva" in updated_text
|
||||
|
||||
# Step 5: Verify config manager also shows the new default
|
||||
@@ -69,14 +69,14 @@ async def test_project_state_sync_after_default_change(mcp_server, app, config_m
|
||||
"tags": "test,consistency",
|
||||
},
|
||||
)
|
||||
assert len(write_result) == 1
|
||||
assert "Test Consistency Note" in write_result[0].text
|
||||
assert len(write_result.content) == 1
|
||||
assert "Test Consistency Note" in write_result.content[0].text
|
||||
|
||||
# Step 7: Test that we can read the note we just created
|
||||
read_result = await client.call_tool("read_note", {"identifier": "Test Consistency Note"})
|
||||
assert len(read_result) == 1
|
||||
assert "Test Consistency Note" in read_result[0].text
|
||||
assert "project state sync working" in read_result[0].text.lower()
|
||||
assert len(read_result.content) == 1
|
||||
assert "Test Consistency Note" in read_result.content[0].text
|
||||
assert "project state sync working" in read_result.content[0].text.lower()
|
||||
|
||||
# Step 8: Test that edit operations work (this was failing in the original issue)
|
||||
edit_result = await client.call_tool(
|
||||
@@ -87,15 +87,18 @@ async def test_project_state_sync_after_default_change(mcp_server, app, config_m
|
||||
"content": "\n\n## Update\n\nEdit operation successful after project switch!",
|
||||
},
|
||||
)
|
||||
assert len(edit_result) == 1
|
||||
assert "added" in edit_result[0].text.lower() and "lines" in edit_result[0].text.lower()
|
||||
assert len(edit_result.content) == 1
|
||||
assert (
|
||||
"added" in edit_result.content[0].text.lower()
|
||||
and "lines" in edit_result.content[0].text.lower()
|
||||
)
|
||||
|
||||
# Step 9: Verify the edit was applied
|
||||
final_read_result = await client.call_tool(
|
||||
"read_note", {"identifier": "Test Consistency Note"}
|
||||
)
|
||||
assert len(final_read_result) == 1
|
||||
final_content = final_read_result[0].text
|
||||
assert len(final_read_result.content) == 1
|
||||
final_content = final_read_result.content[0].text
|
||||
assert "Edit operation successful" in final_content
|
||||
|
||||
# Clean up - switch back to test-project
|
||||
@@ -124,11 +127,11 @@ async def test_multiple_project_switches_maintain_consistency(mcp_server, app, c
|
||||
set_result = await client.call_tool(
|
||||
"set_default_project", {"project_name": project_name}
|
||||
)
|
||||
assert "✓" in set_result[0].text
|
||||
assert "✓" in set_result.content[0].text
|
||||
|
||||
# Verify MCP session immediately reflects the change
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
assert f"Current project: {project_name}" in current_result[0].text
|
||||
assert f"Current project: {project_name}" in current_result.content[0].text
|
||||
|
||||
# Verify config is also updated
|
||||
assert config_manager.default_project == project_name
|
||||
@@ -144,7 +147,7 @@ async def test_multiple_project_switches_maintain_consistency(mcp_server, app, c
|
||||
"tags": "test",
|
||||
},
|
||||
)
|
||||
assert note_title in write_result[0].text
|
||||
assert note_title in write_result.content[0].text
|
||||
|
||||
# Clean up - switch back to test-project
|
||||
await client.call_tool("set_default_project", {"project_name": "test-project"})
|
||||
@@ -159,8 +162,8 @@ async def test_session_handles_nonexistent_project_gracefully(mcp_server, app):
|
||||
switch_result = await client.call_tool(
|
||||
"switch_project", {"project_name": "nonexistent-project"}
|
||||
)
|
||||
assert len(switch_result) == 1
|
||||
result_text = switch_result[0].text
|
||||
assert len(switch_result.content) == 1
|
||||
result_text = switch_result.content[0].text
|
||||
|
||||
# Should show an error message
|
||||
assert "Error:" in result_text
|
||||
@@ -170,4 +173,4 @@ async def test_session_handles_nonexistent_project_gracefully(mcp_server, app):
|
||||
|
||||
# Verify the session stays on the original project
|
||||
current_result = await client.call_tool("get_current_project", {})
|
||||
assert "Current project: test-project" in current_result[0].text
|
||||
assert "Current project: test-project" in current_result.content[0].text
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user