# Basic Memory - Modern Command Runner PYTEST_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_FLAGS", "--import-mode=importlib") TESTMON_SELECT_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_SELECT_FLAGS", "--import-mode=importlib --testmon --testmon-forceselect") TESTMON_REFRESH_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_REFRESH_FLAGS", "--import-mode=importlib --testmon-noselect") # Optional file sharding, shared by local recipes and CI # (e.g. "--splits 3 --group 2"). Empty locally. PYTEST_SPLIT_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_SPLIT_FLAGS", "") # Full suites run on every core. Postgres workers each get their own database # (tests/postgres_test_server.py). Set to "" to run a suite serially. PYTEST_XDIST_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_XDIST_FLAGS", "-n auto") # Coverage is off for the test-* recipes. CI turns it on for its Python 3.14 jobs # with "--cov --cov-report= --cov-fail-under=0" and COVERAGE_CORE=sysmon, then combines every job's # data in one coverage job. PYTEST_COV_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_COV_FLAGS", "--no-cov") # Install dependencies install: uv sync @echo "" @echo "๐Ÿ’ก Remember to activate the virtual environment by running: source .venv/bin/activate" # ============================================================================== # DATABASE BACKEND TESTING # ============================================================================== # Basic Memory supports dual database backends (SQLite and Postgres). # By default, tests run against SQLite (fast, no dependencies). # Set BASIC_MEMORY_TEST_POSTGRES=1 to run against Postgres (uses testcontainers). # # Quick Start: # just check # Run static checks only (fix, format, typecheck) # just fast-check # Fast static check: fix, format, typecheck # just fast-test # Run pytest-testmon impacted tests # just test # Run all tests against SQLite and Postgres # just test-sqlite # Run all tests against SQLite # just test-postgres # Run all tests against Postgres (testcontainers) # just test-unit-sqlite # Run unit tests against SQLite # just test-unit-postgres # Run unit tests against Postgres # just test-int-sqlite # Run integration tests against SQLite # just test-int-postgres # Run integration tests against Postgres # # CI runs both in parallel for faster feedback. # ============================================================================== # Run all tests against SQLite and Postgres test: test-sqlite test-postgres # Run all tests against SQLite test-sqlite: test-unit-sqlite test-int-sqlite # Run all tests against Postgres (uses testcontainers) test-postgres: test-unit-postgres test-int-postgres # Run unit tests against SQLite [positional-arguments] test-unit-sqlite *args: BASIC_MEMORY_ENV=test uv run python scripts/run_tests.py tests {{PYTEST_SPLIT_FLAGS}} "$@" -- -p pytest_mock -v {{PYTEST_COV_FLAGS}} {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}} # MCP and CLI unit tests are left out on Postgres: no test there depends on the # backend, they reach the database through repositories and services that run # here, and they cost 26 of Postgres unit's 60 CI minutes. The SQLite suites run # them, and the Postgres integration suite covers MCP against Postgres end to end. POSTGRES_UNIT_EXCLUDES := "--exclude tests/mcp --exclude tests/cli" # Run unit tests against Postgres; e.g. just test-unit-postgres --splits 3 --group 1 [positional-arguments] test-unit-postgres *args: BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run python scripts/run_tests.py tests {{POSTGRES_UNIT_EXCLUDES}} {{PYTEST_SPLIT_FLAGS}} "$@" -- -p pytest_mock -v {{PYTEST_COV_FLAGS}} {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}} # Re-record Postgres unit test durations so CI's file shards stay balanced. # Without .test_durations the file splitter estimates work from file sizes. test-durations-postgres: BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -q --no-cov --store-durations --durations-path .test_durations --ignore=tests/mcp --ignore=tests/cli tests # Run integration tests against SQLite (excludes semantic tests and on-demand benchmarks โ€” # use just test-semantic / run benchmark files explicitly) [positional-arguments] test-int-sqlite *args: BASIC_MEMORY_ENV=test uv run python scripts/run_tests.py test-int {{PYTEST_SPLIT_FLAGS}} "$@" -- -p pytest_mock -v {{PYTEST_COV_FLAGS}} {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}} -m "not semantic and not benchmark" # Run integration tests against Postgres. Per-test and CI job timeouts fail normally; # a killed process does not prove that the selected suite finished successfully. [positional-arguments] test-int-postgres *args: BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run python scripts/run_tests.py test-int {{PYTEST_SPLIT_FLAGS}} "$@" -- -p pytest_mock -v {{PYTEST_COV_FLAGS}} {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}} -m "not semantic and not benchmark" # Fast test selection for local iteration; run targeted tests explicitly when possible. fast-test *args: testmon-seed BASIC_MEMORY_ENV=test COVERAGE_CORE=ctrace uv run pytest -p pytest_mock -v --no-cov {{TESTMON_SELECT_FLAGS}} --testmon-env=local-ctrace-v1 {{args}} # Run tests impacted by recent changes (requires pytest-testmon). # Backcompat alias for the fast-test recipe. testmon *args: just fast-test {{args}} # Seed pytest-testmon data into this worktree from the shared Git cache. testmon-seed: uv run python scripts/testmon_cache.py seed # Record the same collection and environment that fast-test selects from. # CI's sharded recipes run complete suites independently of this local cache. testmon-refresh: #!/usr/bin/env bash set -euo pipefail BASIC_MEMORY_TESTMON_SELECT_FLAGS="{{TESTMON_REFRESH_FLAGS}}" just fast-test uv run python scripts/testmon_cache.py refresh # Show local and shared pytest-testmon cache locations. testmon-status: uv run python scripts/testmon_cache.py status # Child-process dependencies are invisible to testmon. Always run this small # startup/exit gate, regardless of the ordinary suite's selection flags. [positional-arguments] test-cli-smoke *args: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -p no:pytest-testmon -q --no-cov --import-mode=importlib tests/cli/test_cli_tool_exit.py tests/cli/test_cli_exit.py tests/cli/test_db_reset_exit.py "$@" # Run MCP smoke test (fast end-to-end loop) test-smoke: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m smoke test-int/mcp/test_smoke_integration.py # Run the complete semantic read-cache suite against a real Redis server. # BASIC_MEMORY_TEST_REDIS_URL may select an existing server; otherwise testcontainers owns one. test-read-cache: BASIC_MEMORY_ENV=test LOGFIRE_IGNORE_NO_CONFIG=1 uv run pytest -p pytest_mock --no-cov -q test-int/read_cache # Fast static check: auto-fix lint, format, and typecheck, but do not run tests. fast-check: just fix just format just typecheck # Fast local loop with live OpenAI-backed checks disabled. fast-check-no-openai: OPENAI_API_KEY= just fast-check # ============================================================================== # Runtime / Event Indexing Refactor # ============================================================================== # Focused portable storage-event contract tests. storage-event-contract-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/test_runtime_storage_events.py \ tests/index/test_storage_event_operation_processor.py \ tests/index/test_storage_event_orchestration.py # Focused provider-neutral project-index orchestration surface tests. project-index-surface-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_project_index_surface.py # Focused provider-neutral project-index workflow tests. project-index-workflow-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_project_index_workflow.py # Focused provider-neutral project-index coordinator tests. project-index-runner-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_project_index_runner.py # Focused provider-neutral change-planning tests. change-planning-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_change_planning.py # Focused local project-index adapter tests. local-project-index-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py # Focused local project-index scan parity tests. local-project-index-scan-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_scan_parity.py # Focused local project-index directory delete parity test. local-project-index-directory-delete-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_directory_delete_removes_notes_and_repairs_survivors # Focused local project-index hidden-file parity test. local-project-index-hidden-file-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_skips_hidden_markdown_files # Focused local project-index null-checksum repair parity test. local-project-index-null-checksum-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_repairs_null_checksum_entities # Focused local project-index file timestamp parity tests. local-project-index-timestamp-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_uses_file_mtime_for_new_markdown_entities \ tests/index/test_local_project_index.py::test_local_project_index_updates_entity_mtime_on_file_modification # Focused local project-index regular-file parity tests. local-project-index-regular-file-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_indexes_regular_files \ tests/index/test_local_project_index.py::test_local_project_index_updates_regular_file_checksum \ tests/index/test_local_project_index.py::test_local_project_index_moves_and_deletes_regular_file_entities \ tests/index/test_local_project_index.py::test_local_project_index_resolves_regular_file_relations # Focused local project-index markdown move conflict parity test. local-project-index-markdown-move-conflict-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_moves_markdown_over_deleted_path_with_permalink_repair # Focused local project-index changed-during-index parity test. local-project-index-race-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_reads_current_file_when_file_changes_after_observation # Focused local project-index duplicate permalink parity test. local-project-index-permalink-conflict-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_resolves_duplicate_permalink_update # Focused local project-index new duplicate permalink parity test. local-project-index-new-permalink-conflict-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_resolves_new_duplicate_permalink # Focused local project-index path-derived permalink conflict parity test. local-project-index-path-conflict-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_assigns_unique_permalinks_for_path_conflicts # Focused local project-index frontmatter policy parity tests. local-project-index-frontmatter-policy-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_does_not_add_frontmatter_when_disabled \ tests/index/test_local_project_index.py::test_local_project_index_indexes_thematic_break_content_without_frontmatter \ tests/index/test_local_project_index.py::test_local_project_index_writes_frontmatter_when_enabled_even_if_permalinks_disabled # Focused local project-index thematic-break frontmatter parity test. local-project-index-thematic-break-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_indexes_thematic_break_content_without_frontmatter # Focused local project-index relation resolution parity test. local-project-index-relation-parity-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_resolves_order_dependent_relations_after_batches \ tests/index/test_local_project_index.py::test_local_project_index_deduplicates_relations_by_type # Focused local project-index observation category parity test. local-project-index-observation-category-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_preserves_loose_observation_categories # Focused local project-index wikilink stability parity test. local-project-index-wikilink-stability-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_project_index.py::test_local_project_index_keeps_wikilink_source_stable_when_target_appears # Focused per-file indexing runner/model tests. file-index-runner-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_index_file_runner.py \ tests/indexing/test_file_indexer.py \ tests/indexing/test_models.py # Focused file-batch indexing runner/payload tests. file-index-batch-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_file_batch_runner.py \ tests/indexing/test_job_payloads.py # Focused batch-index semantic dependency parity test. file-index-semantic-dependency-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_batch_indexer.py::test_batch_indexer_keeps_file_indexed_when_semantic_dependencies_are_missing # Focused startup wiring for local project-index fanout. local-project-index-startup-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/services/test_initialization.py::test_initialize_file_indexing_uses_project_index_runtime_for_initial_sync_by_default # Focused CLI project-index surface tests. project-index-cli-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/cli/test_db_reindex.py \ tests/cli/test_status_wait_timeout.py # Focused project-wide indexing orchestration surface tests. project-index-contract-test: project-index-surface-test project-index-workflow-test project-index-runner-test change-planning-test local-project-index-test local-project-index-scan-test local-project-index-markdown-move-conflict-test local-project-index-new-permalink-conflict-test local-project-index-path-conflict-test local-project-index-thematic-break-test local-project-index-observation-category-test local-project-index-wikilink-stability-test local-project-index-startup-test project-index-cli-test # Focused event-based indexing contract tests for the cloud/core extraction loop. local-event-index-regular-file-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_watch_regular_file_parity.py # Focused local event-index relation cleanup parity test. local-event-index-relation-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_watch_regular_file_parity.py::test_local_event_index_deletes_regular_file_relation_target_and_repairs_search # Focused local event-index atomic-write parity test. local-event-index-atomic-write-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_watch_stress_parity.py::test_local_event_index_handles_rapid_atomic_writes_to_same_file # Focused local filesystem event temp/backup filtering parity test. filesystem-event-temp-file-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_filesystem_events.py::test_editor_swap_and_backup_changes_are_filtered_before_indexing # Focused local event-index larger watcher batch parity tests. local-event-index-stress-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/index/test_local_watch_stress_parity.py # Focused event-based indexing contract tests for the cloud/core extraction loop. event-index-contract-test: storage-event-contract-test filesystem-event-temp-file-test local-event-index-atomic-write-test local-event-index-stress-test BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_external_file_delete_runner.py \ tests/index/test_filesystem_events.py \ tests/index/test_inline_storage_event_processor.py \ tests/index/test_local_watch_ignore_parity.py \ tests/index/test_local_watch_regular_file_parity.py \ tests/index/test_local_watch_orchestration.py \ tests/index/test_repository_storage_event_project_resolution.py \ tests/services/test_initialization.py::test_initialize_file_indexing_wires_event_index_runtime_by_default # Focused parity loop for local project scans and shared storage-event routing. event-index-parity-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/test_runtime.py::TestRuntimeContracts::test_runtime_storage_event_operation_plans_index_delete_and_skip_work \ tests/test_runtime_observed_index_files.py \ tests/index/test_local_project_index.py \ tests/index/test_filesystem_events.py \ tests/index/test_storage_event_operation_processor.py \ tests/index/test_storage_event_orchestration.py # Focused indexing contract suite for the cloud/core extraction loop. index-contract-test: file-index-runner-test file-index-batch-test file-index-semantic-dependency-test project-index-contract-test event-index-contract-test # Focused core contract suite used by the basic-memory-cloud runtime refactor loop. runtime-core-pytest *args: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov {{args}} # Focused PR #1002 Codex feedback regressions. pr-1002-feedback-test: BASIC_MEMORY_ENV=test LOGFIRE_IGNORE_NO_CONFIG=1 uv run pytest -p pytest_mock -q --no-cov \ tests/runtime/test_deleted_note_response.py \ tests/repository/test_accepted_note_search_repository.py \ tests/indexing/test_project_index_workflow.py \ tests/indexing/test_accepted_note_write_runner.py \ tests/indexing/test_directory_delete_runner.py runtime-core-fast-check-no-openai: OPENAI_API_KEY= just fast-check runtime-refactor-contract-test: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \ tests/indexing/test_accepted_note_write_runner.py \ tests/indexing/test_accepted_note_enqueue_runner.py \ tests/indexing/test_note_content_read_repair_runner.py \ tests/runtime/test_accepted_note_response_planning.py \ tests/runtime/test_deleted_note_response.py \ tests/runtime/test_pending_note_materialization.py \ tests/runtime/test_note_content_read_planning.py just index-contract-test # Reset Postgres test database (drops and recreates schema) # Useful when Alembic migration state gets out of sync during development # Uses credentials from docker-compose-postgres.yml postgres-reset: docker exec basic-memory-postgres psql -U ${POSTGRES_USER:-basic_memory_user} -d ${POSTGRES_TEST_DB:-basic_memory_test} -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" @echo "โœ… Postgres test database reset" # Run Alembic migrations manually against Postgres test database # Useful for debugging migration issues # Uses credentials from docker-compose-postgres.yml (can override with env vars) postgres-migrate: @cd src/basic_memory/alembic && \ BASIC_MEMORY_DATABASE_BACKEND=postgres \ BASIC_MEMORY_DATABASE_URL=${POSTGRES_TEST_URL:-postgresql+asyncpg://basic_memory_user:dev_password@localhost:5433/basic_memory_test} \ uv run alembic upgrade head @echo "โœ… Migrations applied to Postgres test database" # Run Windows-specific tests only (only works on Windows platform) # These tests verify Windows-specific database optimizations (locking mode, NullPool) # Will be skipped automatically on non-Windows platforms test-windows: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m windows tests test-int # Run benchmark tests only (performance testing) # These are slow tests that measure sync performance with various file counts # Excluded from default test runs to keep CI fast test-benchmark: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m benchmark tests test-int # Run semantic search quality benchmarks (all combos) [positional-arguments] test-semantic *args: BASIC_MEMORY_ENV=test uv run python scripts/run_tests.py test-int/semantic {{PYTEST_SPLIT_FLAGS}} "$@" -- -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m semantic # Run semantic benchmarks with JSON artifact output, then show report test-semantic-report: BASIC_MEMORY_ENV=test BASIC_MEMORY_BENCHMARK_OUTPUT=.benchmarks/semantic-quality.jsonl uv run pytest -p pytest_mock -v -s --no-cov -m semantic test-int/semantic/ uv run python test-int/semantic/report.py .benchmarks/semantic-quality.jsonl # Run opt-in live LiteLLM provider checks against configured external APIs test-litellm-live *args: BASIC_MEMORY_ENV=test BASIC_MEMORY_RUN_LITELLM_INTEGRATION=1 PYTHONPATH=test-int:src uv run --extra litellm python -m semantic.litellm_live_harness {{args}} # Run semantic benchmarks (Postgres combos only) test-semantic-postgres: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m semantic -k postgres test-int/semantic/ # Run one multilingual model/backend pair in its own process so cold-load and RSS # measurements are not contaminated by another FastEmbed model. benchmark-multilingual model="bge-small-en" backend="sqlite" mode="vector" threshold="0.55": UV_PYTHON=3.12 \ BASIC_MEMORY_ENV=test \ LOGFIRE_IGNORE_NO_CONFIG=1 \ BASIC_MEMORY_MULTILINGUAL_MODEL="{{model}}" \ BASIC_MEMORY_MULTILINGUAL_BACKEND="{{backend}}" \ BASIC_MEMORY_MULTILINGUAL_RETRIEVAL_MODE="{{mode}}" \ BASIC_MEMORY_MULTILINGUAL_THRESHOLD="{{threshold}}" \ BASIC_MEMORY_BENCHMARK_OUTPUT=".benchmarks/multilingual-{{model}}-{{backend}}-{{mode}}-{{threshold}}.jsonl" \ uv run --extra milvus pytest -p pytest_mock -q --no-cov {{PYTEST_FLAGS}} \ test-int/semantic/test_multilingual_embedding_benchmark.py # Screen the current model and the 384-dimensional multilingual candidate, then # compare the latest records from their JSONL artifacts. benchmark-multilingual-compare backend="sqlite" mode="vector" threshold="0.55": just benchmark-multilingual bge-small-en "{{backend}}" "{{mode}}" "{{threshold}}" just benchmark-multilingual multilingual-minilm "{{backend}}" "{{mode}}" "{{threshold}}" UV_PYTHON=3.12 uv run python test-int/compare_search_benchmarks.py \ ".benchmarks/multilingual-bge-small-en-{{backend}}-{{mode}}-{{threshold}}.jsonl" \ ".benchmarks/multilingual-multilingual-minilm-{{backend}}-{{mode}}-{{threshold}}.jsonl" \ --format table # View semantic benchmark results (rich formatted table) # Usage: just semantic-report [--filter-combo sqlite] [--filter-suite paraphrase] [--sort-by avg_latency_ms] semantic-report *args: uv run python test-int/semantic/report.py .benchmarks/semantic-quality.jsonl {{args}} # Compare two search benchmark JSONL outputs # Usage: # just benchmark-compare .benchmarks/search-baseline.jsonl .benchmarks/search-candidate.jsonl # just benchmark-compare .benchmarks/search-baseline.jsonl .benchmarks/search-candidate.jsonl --format markdown --show-missing benchmark-compare baseline candidate *args: uv run python test-int/compare_search_benchmarks.py "{{baseline}}" "{{candidate}}" --format table {{args}} # Check the standalone read-load driver in the root environment that supplies Redis/testcontainers. bench-read-check: uv run ruff check benchmarks/scripts/read_load_bench.py uv run pyright benchmarks/scripts/read_load_bench.py # Run one MCP read-load sweep. An empty Redis URL is the authoritative baseline. bench-read-load label="authoritative" redis_url="": uv run python benchmarks/scripts/read_load_bench.py \ --bm-command .venv/bin/basic-memory \ --label "{{label}}" \ --redis-url "{{redis_url}}" \ --scratch ".scratch/read-load-{{label}}" \ --output ".scratch/read-load-{{label}}/results.jsonl" \ --truncate # Exercise MCP startup, Redis pool capacity, warmup, result output, and provenance with a tiny corpus. bench-read-smoke redis_url="redis://127.0.0.1:6379/0": uv run python benchmarks/scripts/read_load_bench.py \ --bm-command .venv/bin/basic-memory \ --label "smoke" \ --redis-url "{{redis_url}}" \ --scratch ".scratch/read-load-smoke" \ --output ".scratch/read-load-smoke/results.jsonl" \ --truncate \ --sizes 1024 \ --notes-per-size 2 \ --reads 64 \ --concurrency 64 \ --seed-concurrency 1 \ --quiesce-seconds 0.1 # Run adjacent authoritative and warmed-Redis sweeps, then print their comparison. bench-read-cache redis_url="redis://127.0.0.1:6379/0" run_id="latest": just bench-read-load "authoritative-{{run_id}}" just bench-read-load "redis-warm-{{run_id}}" "{{redis_url}}" uv run python test-int/compare_search_benchmarks.py \ ".scratch/read-load-authoritative-{{run_id}}/results.jsonl" \ ".scratch/read-load-redis-warm-{{run_id}}/results.jsonl" \ --format markdown # Run all tests including Windows, Postgres, and Benchmarks (for CI/comprehensive testing) # Use this before releasing to ensure everything works across all backends and platforms test-all: BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} tests test-int # Generate an HTML coverage report from the same four suites `just test` runs: # unit and integration, SQLite and Postgres. Each suite is its own process, with # the same flags and markers as its test-* recipe, so tests/ and test-int/ fixtures # never share a process and on-demand semantic/benchmark tests stay out. # pytest-cov collects from xdist workers and --cov-append adds each suite to one # data file. sysmon keeps tracing overhead near zero (it doubled test time under # the old C tracer). Every suite runs on all cores. coverage: #!/usr/bin/env bash set -uo pipefail uv run coverage erase failed=() run_suite() { local label=$1; shift echo "๐Ÿ”Ž Coverage ($label)..." if ! "$@"; then failed+=("$label") fi } # Each suite covers part of the code; only the combined report below checks fail_under. pytest_args=(-p pytest_mock -q --cov --cov-append --cov-report= --cov-fail-under=0 {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}}) int_markers=(-m "not semantic and not benchmark") export BASIC_MEMORY_ENV=test COVERAGE_CORE=sysmon run_suite "unit, SQLite" \ uv run pytest "${pytest_args[@]}" tests run_suite "integration, SQLite" \ uv run pytest "${pytest_args[@]}" "${int_markers[@]}" test-int run_suite "unit, Postgres" env BASIC_MEMORY_TEST_POSTGRES=1 \ uv run pytest "${pytest_args[@]}" --ignore=tests/mcp --ignore=tests/cli tests run_suite "integration, Postgres" env BASIC_MEMORY_TEST_POSTGRES=1 \ uv run pytest "${pytest_args[@]}" "${int_markers[@]}" test-int # The recipe runs without -e so every suite reports; keep the report's status # (2 below fail_under) and return it after the HTML is written. report_status=0 uv run coverage report -m || report_status=$? uv run coverage html echo "Coverage report generated in htmlcov/index.html" # Trigger: a suite failed. # Why: the report still covers every suite that ran, but the run is not green. # Outcome: name the failed suites and exit non-zero. if (( ${#failed[@]} )); then echo "โŒ Failed suites: ${failed[*]}" exit 1 fi # Trigger: combined coverage is below fail_under in pyproject.toml. # Outcome: fail like the CI Coverage job does. if (( report_status )); then echo "โŒ Combined coverage is below the fail_under floor" exit "$report_status" fi # List test files that can be deleted without losing line coverage, e.g. # just test-redundancy --only test-int/mcp. Records which test ran each line across # the four suites (about 30 minutes), then picks files greedily. Uses the ctrace core: # sysmon credits a line only to the first test that runs it. Read each listed file # before deleting it; line coverage cannot see assertions. test-redundancy *args: #!/usr/bin/env bash set -euo pipefail export BASIC_MEMORY_ENV=test COVERAGE_CORE=ctrace COVERAGE_FILE=.coverage.contexts rm -f .coverage.contexts .coverage.junit-*.xml pytest_args=(-p pytest_mock -q --cov --cov-append --cov-report= --cov-fail-under=0 --cov-context=test {{PYTEST_XDIST_FLAGS}} {{PYTEST_FLAGS}}) int_markers=(-m "not semantic and not benchmark") # Each suite's JUnit report gives the picker file durations, so it removes the # slowest of two duplicates and reports the time saved. uv run pytest "${pytest_args[@]}" --junitxml=.coverage.junit-unit-sqlite.xml tests uv run pytest "${pytest_args[@]}" "${int_markers[@]}" --junitxml=.coverage.junit-int-sqlite.xml test-int BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest "${pytest_args[@]}" --ignore=tests/mcp --ignore=tests/cli --junitxml=.coverage.junit-unit-postgres.xml tests BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest "${pytest_args[@]}" "${int_markers[@]}" --junitxml=.coverage.junit-int-postgres.xml test-int uv run python scripts/test_redundancy.py .coverage.contexts --junit .coverage.junit-*.xml {{args}} # Lint and fix code (calls fix) lint: fix # Lint and fix code fix: uv run ruff check --fix --unsafe-fixes . # Type check code (ty) typecheck: uv run ty check src tests test-int # Type check code (pyright) typecheck-pyright: uv run pyright # Type check code (ty) typecheck-ty: just typecheck # Clean build artifacts and cache files clean: find . -type f -name '*.pyc' -delete find . -type d -name '__pycache__' -exec rm -r {} + rm -rf installer/build/ installer/dist/ dist/ rm -f rw.*.dmg .coverage.* # Format code with ruff format: uv run ruff format . # Regenerate the manual's registry-owned SYNOPSIS blocks from the MCP tool registry man-regen: uv run python scripts/update_man_pages.py # Run MCP inspector tool run-inspector: npx @modelcontextprotocol/inspector # Run doctor checks in an isolated temp home/config doctor: #!/usr/bin/env bash set -euo pipefail TMP_HOME=$(mktemp -d) TMP_CONFIG=$(mktemp -d) HOME="$TMP_HOME" \ BASIC_MEMORY_HOME="$TMP_HOME/basic-memory" \ BASIC_MEMORY_CONFIG_DIR="$TMP_CONFIG" \ ./.venv/bin/python -m basic_memory.cli.main doctor --local # Run an isolated Logfire smoke workflow for local trace inspection telemetry-smoke: #!/usr/bin/env bash set -euo pipefail TMP_HOME=$(mktemp -d) TMP_CONFIG=$(mktemp -d) TMP_PROJECT=$(mktemp -d) export HOME="$TMP_HOME" export BASIC_MEMORY_ENV="${BASIC_MEMORY_ENV:-dev}" export BASIC_MEMORY_HOME="$TMP_HOME/basic-memory" export BASIC_MEMORY_CONFIG_DIR="$TMP_CONFIG" export BASIC_MEMORY_NO_PROMOS=1 export BASIC_MEMORY_LOG_LEVEL="${BASIC_MEMORY_LOG_LEVEL:-INFO}" export BASIC_MEMORY_LOGFIRE_ENABLED="${BASIC_MEMORY_LOGFIRE_ENABLED:-true}" export BASIC_MEMORY_LOGFIRE_ENVIRONMENT="${BASIC_MEMORY_LOGFIRE_ENVIRONMENT:-telemetry-smoke}" if [[ -z "${BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE:-}" ]]; then if [[ -n "${LOGFIRE_TOKEN:-}" ]]; then export BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE=true else export BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE=false fi fi mkdir -p "$BASIC_MEMORY_HOME" echo "Telemetry smoke setup:" echo " logfire_enabled=$BASIC_MEMORY_LOGFIRE_ENABLED" echo " send_to_logfire=$BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE" echo " log_level=$BASIC_MEMORY_LOG_LEVEL" echo " logfire_environment=$BASIC_MEMORY_LOGFIRE_ENVIRONMENT" echo " project_path=$TMP_PROJECT" ./.venv/bin/python -m basic_memory.cli.main project add telemetry-smoke "$TMP_PROJECT" --default --local ./.venv/bin/python -m basic_memory.cli.main tool write --title "Telemetry Smoke" --folder notes --content "hello from smoke" --project telemetry-smoke --local ./.venv/bin/python -m basic_memory.cli.main tool read notes/telemetry-smoke --project telemetry-smoke --local ./.venv/bin/python -m basic_memory.cli.main tool edit notes/telemetry-smoke --operation append --content $'\n\nsmoke edit line' --project telemetry-smoke --local ./.venv/bin/python -m basic_memory.cli.main tool context notes/telemetry-smoke --project telemetry-smoke --local --page-size 5 --max-related 5 ./.venv/bin/python -m basic_memory.cli.main tool search telemetry --project telemetry-smoke --local ./.venv/bin/python -m basic_memory.cli.main doctor --local echo "" echo "Telemetry smoke complete." echo "Search Logfire for:" echo " service_name: basic-memory-cli" echo " environment: $BASIC_MEMORY_LOGFIRE_ENVIRONMENT" echo " span names: mcp.tool.write_note, mcp.tool.read_note, mcp.tool.edit_note, mcp.tool.build_context, mcp.tool.search_notes, sync.project.run" # Update all dependencies to latest versions update-deps: uv sync --upgrade # Run static code quality checks. Use `just test` for the actual test suites. check: lint format typecheck # Run all code quality checks and all test suites, including semantic benchmarks check-all: lint format typecheck test test-semantic # Validate every consolidated agent package (Claude Code, Codex, skills, Hermes, OpenClaw, Tau, Pi) package-check: package-check-claude-code package-check-codex package-check-skills package-check-hermes package-check-openclaw package-check-tau package-check-pi # Alias for plugin/package validation during consolidation work plugins-check: package-check # Validate the host-native agent harnesses agent-harness-check: package-check-claude-code package-check-hermes package-check-openclaw package-check-tau package-check-pi # Claude Code plugin: manifests, bundled skills, bundled agent, and strict plugin validation package-check-claude-code: just --justfile plugins/claude-code/justfile --working-directory plugins/claude-code check # Codex plugin: manifest, bundled skills, hooks, MCP config, and schemas package-check-codex: just --justfile plugins/codex/justfile --working-directory plugins/codex check # Shared top-level SKILL.md source package-check-skills: just --justfile skills/justfile --working-directory skills check # Hermes plugin: native manifest plus hermetic unit test suite package-check-hermes: just --justfile integrations/hermes/justfile --working-directory integrations/hermes check # OpenClaw plugin: install deps, copy skills, typecheck, lint, build, test, and npm pack dry-run package-check-openclaw: just --justfile integrations/openclaw/justfile --working-directory integrations/openclaw install just --justfile integrations/openclaw/justfile --working-directory integrations/openclaw release-check # Tau extension: real MCP transport, runtime loading, and lifecycle checks package-check-tau: just --justfile integrations/tau/justfile --working-directory integrations/tau check # Pi package: install deps, copy skills, typecheck, test, and npm pack dry-run package-check-pi: just --justfile integrations/pi/justfile --working-directory integrations/pi check # Generate Alembic migration with descriptive message. # The revision ID is the next schema version ("0040", "0041", ...), assigned by alembic/env.py. # If another PR lands the same ID first, renumber yours to the next ID and point its # down_revision at the new head. Never add a merge revision; the numbered tail must stay linear. migration message: cd src/basic_memory/alembic && alembic revision --autogenerate -m "{{message}}" # Set the Basic Memory version across release manifests (scope: all | core | packages) set-version version scope="all": python3 scripts/update_versions.py "{{version}}" --scope "{{scope}}" # Pin the Basic Memory Git ref used by all Codex uv hook scripts. # --frozen rewrites the inline metadata without resolving it: `just release` pins the # tag it has not pushed yet, and uv 0.13 resolves the ref on `add --script` otherwise. set-codex-hook-version ref: uv add --script plugins/codex/hooks/session_start.py --frozen --raw "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@{{ref}}" uv add --script plugins/codex/hooks/pre_compact.py --frozen --raw "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@{{ref}}" # Preview a version update without writing (scope: all | core | packages) set-version-dry-run version scope="all": python3 scripts/update_versions.py "{{version}}" --scope "{{scope}}" --dry-run # Set the version for just the plugin/agent artifacts (plugins, marketplaces, Hermes, OpenClaw) set-packages-version version: just set-version "{{version}}" packages # Preview a plugin/agent-artifact version update without writing set-packages-version-dry-run version: just set-version-dry-run "{{version}}" packages # Preview the consolidated manifest version update without changing files release-dry-run version: just set-version-dry-run "{{version}}" # Create a stable release (e.g., just release v0.13.2) release version: #!/usr/bin/env bash set -euo pipefail # Validate version format if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "โŒ Invalid version format. Use: v0.13.2" exit 1 fi # Extract version number without 'v' prefix VERSION_NUM=$(echo "{{version}}" | sed 's/^v//') echo "๐Ÿš€ Creating stable release {{version}}" # Pre-flight checks echo "๐Ÿ“‹ Running pre-flight checks..." if [[ -n $(git status --porcelain) ]]; then echo "โŒ Uncommitted changes found. Please commit or stash them first." exit 1 fi if [[ $(git branch --show-current) != "main" ]]; then echo "โŒ Not on main branch. Switch to main first." exit 1 fi # Check if tag already exists if git tag -l "{{version}}" | grep -q "{{version}}"; then echo "โŒ Tag {{version}} already exists" exit 1 fi # Changelog must already be on main (land it via a normal PR first) if ! grep -q "^## {{version}} " CHANGELOG.md; then echo "โŒ CHANGELOG.md has no entry for {{version}}. Land one via PR first." exit 1 fi # Run quality checks echo "๐Ÿ” Running lint checks..." just lint just typecheck # Update all package manifests to the one Basic Memory product version. echo "๐Ÿ“ Updating consolidated package versions..." just set-version "{{version}}" just set-codex-hook-version "{{version}}" # Trigger: main's ruleset rejects direct pushes ("Changes must be made # through a pull request"). # Why: the version bump must land on main before the tag is cut, so it # rides a release PR that is rebase-merged (the repo disallows merge # commits). # Outcome: the bump commit gets a new SHA on main; the tag is created on # that rebased commit, found by its commit subject. COMMIT_SUBJECT="chore: update version to $VERSION_NUM for {{version}} release" git checkout -b "release/{{version}}" git add \ src/basic_memory/__init__.py \ server.json \ .claude-plugin/marketplace.json \ plugins/claude-code/.claude-plugin/plugin.json \ plugins/claude-code/.claude-plugin/marketplace.json \ plugins/claude-code/hooks/session_start.py \ plugins/claude-code/hooks/pre_compact.py \ plugins/codex/.codex-plugin/plugin.json \ plugins/codex/hooks/session_start.py \ plugins/codex/hooks/pre_compact.py \ integrations/hermes/plugin.yaml \ integrations/hermes/__init__.py \ integrations/openclaw/package.json \ integrations/pi/package.json \ integrations/pi/package-lock.json git commit -s -m "$COMMIT_SUBJECT" echo "๐Ÿ“ค Opening release PR..." git push -u origin "release/{{version}}" gh pr create --title "chore(core): release {{version}}" \ --body "Version bump for {{version}}. See CHANGELOG.md for release notes." # Trigger: the PR may not be mergeable synchronously (merge gates, # required checks added later, or GitHub still computing mergeability). # Why: the tag must point at the bump commit on main, so the recipe # cannot tag until the merge has actually landed. # Outcome: try a direct rebase-merge, fall back to queueing auto-merge, # then poll main for the rebased bump commit before tagging. if ! gh pr merge "release/{{version}}" --rebase --delete-branch; then echo "โš ๏ธ Direct merge did not complete (merge gates pending?). Queueing auto-merge..." gh pr merge "release/{{version}}" --rebase --delete-branch --auto fi echo "โณ Waiting for the bump commit to land on main..." TAG_COMMIT="" for _ in $(seq 1 60); do git fetch origin main --quiet TAG_COMMIT=$(git log FETCH_HEAD --fixed-strings --grep "$COMMIT_SUBJECT" --format='%H' -1) [[ -n "$TAG_COMMIT" ]] && break sleep 5 done if [[ -z "$TAG_COMMIT" ]]; then echo "โŒ Bump commit not on main after 5 minutes (merge still pending?)." echo " Once the release PR merges, finish the release manually:" echo " git fetch origin main" echo " git tag {{version}} \$(git log FETCH_HEAD --fixed-strings --grep \"$COMMIT_SUBJECT\" --format='%H' -1)" echo " git push origin {{version}}" exit 1 fi git checkout main git pull --ff-only origin main git branch -D "release/{{version}}" 2>/dev/null || true echo "๐Ÿท๏ธ Creating tag {{version}} at $TAG_COMMIT..." git tag "{{version}}" "$TAG_COMMIT" git push origin "{{version}}" echo "โœ… Release {{version}} created successfully!" echo "๐Ÿ“ฆ GitHub Actions will build and publish to PyPI" echo "๐Ÿ”— Monitor at: https://github.com/basicmachines-co/basic-memory/actions" echo "" echo "๐Ÿ“ REMINDER: Post-release tasks:" echo " 1. docs.basicmemory.com - Add a What's New page under content/2.whats-new/" echo " and bump the badge in content/index.md (see that repo's CLAUDE.md)" echo " 2. basicmemory.com - No version number in the site UI; for a significant" echo " release optionally add a post under src/content/blog/. Skip for patches." echo " 3. MCP Registry - once PyPI is live: gh workflow run mcp-registry-publish.yml" echo " See: .claude/commands/release/release.md for detailed instructions" # Create a beta release (e.g., just beta v0.13.2b1) beta version: #!/usr/bin/env bash set -euo pipefail # Validate version format (allow beta/rc suffixes) if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(b[0-9]+|rc[0-9]+)$ ]]; then echo "โŒ Invalid beta version format. Use: v0.13.2b1 or v0.13.2rc1" exit 1 fi # Extract version number without 'v' prefix VERSION_NUM=$(echo "{{version}}" | sed 's/^v//') echo "๐Ÿงช Creating beta release {{version}}" # Pre-flight checks echo "๐Ÿ“‹ Running pre-flight checks..." if [[ -n $(git status --porcelain) ]]; then echo "โŒ Uncommitted changes found. Please commit or stash them first." exit 1 fi if [[ $(git branch --show-current) != "main" ]]; then echo "โŒ Not on main branch. Switch to main first." exit 1 fi # Trigger: pyproject pins a FastMCP pre-release (fastmcp==X.Y.Zb1). # Why: while that pin exists every documented install/upgrade passes # --prerelease=allow, and uv applies it to basic-memory itself, so a # beta on PyPI would become the default install for stable users (#1338). # Outcome: refuse the beta unless explicitly overridden. if grep -Eq '"fastmcp==[0-9.]+(a|b|rc)[0-9]+"' pyproject.toml \ && [[ "${BASIC_MEMORY_ALLOW_BETA_WITH_PRERELEASE_DEPS:-}" != "1" ]]; then echo "โŒ pyproject.toml pins a FastMCP pre-release. Stable installs run with" echo " --prerelease=allow, so this beta would be picked up by every fresh install." echo " Set BASIC_MEMORY_ALLOW_BETA_WITH_PRERELEASE_DEPS=1 to publish anyway." exit 1 fi # Check if tag already exists if git tag -l "{{version}}" | grep -q "{{version}}"; then echo "โŒ Tag {{version}} already exists" exit 1 fi # Run quality checks echo "๐Ÿ” Running lint checks..." just lint just typecheck # Update all package manifests to the one Basic Memory product version. echo "๐Ÿ“ Updating consolidated package versions..." just set-version "{{version}}" just set-codex-hook-version "{{version}}" # Trigger: main's ruleset rejects direct pushes ("Changes must be made # through a pull request"). # Why: the version bump must land on main before the tag is cut, so it # rides a release PR that is rebase-merged (the repo disallows merge # commits). # Outcome: the bump commit gets a new SHA on main; the tag is created on # that rebased commit, found by its commit subject. COMMIT_SUBJECT="chore: update version to $VERSION_NUM for {{version}} beta release" git checkout -b "release/{{version}}" git add \ src/basic_memory/__init__.py \ server.json \ .claude-plugin/marketplace.json \ plugins/claude-code/.claude-plugin/plugin.json \ plugins/claude-code/.claude-plugin/marketplace.json \ plugins/claude-code/hooks/session_start.py \ plugins/claude-code/hooks/pre_compact.py \ plugins/codex/.codex-plugin/plugin.json \ plugins/codex/hooks/session_start.py \ plugins/codex/hooks/pre_compact.py \ integrations/hermes/plugin.yaml \ integrations/hermes/__init__.py \ integrations/openclaw/package.json \ integrations/pi/package.json \ integrations/pi/package-lock.json git commit -s -m "$COMMIT_SUBJECT" echo "๐Ÿ“ค Opening release PR..." git push -u origin "release/{{version}}" gh pr create --title "chore(core): release {{version}}" \ --body "Version bump for {{version}} beta." # Trigger: the PR may not be mergeable synchronously (merge gates, # required checks added later, or GitHub still computing mergeability). # Why: the tag must point at the bump commit on main, so the recipe # cannot tag until the merge has actually landed. # Outcome: try a direct rebase-merge, fall back to queueing auto-merge, # then poll main for the rebased bump commit before tagging. if ! gh pr merge "release/{{version}}" --rebase --delete-branch; then echo "โš ๏ธ Direct merge did not complete (merge gates pending?). Queueing auto-merge..." gh pr merge "release/{{version}}" --rebase --delete-branch --auto fi echo "โณ Waiting for the bump commit to land on main..." TAG_COMMIT="" for _ in $(seq 1 60); do git fetch origin main --quiet TAG_COMMIT=$(git log FETCH_HEAD --fixed-strings --grep "$COMMIT_SUBJECT" --format='%H' -1) [[ -n "$TAG_COMMIT" ]] && break sleep 5 done if [[ -z "$TAG_COMMIT" ]]; then echo "โŒ Bump commit not on main after 5 minutes (merge still pending?)." echo " Once the release PR merges, finish the release manually:" echo " git fetch origin main" echo " git tag {{version}} \$(git log FETCH_HEAD --fixed-strings --grep \"$COMMIT_SUBJECT\" --format='%H' -1)" echo " git push origin {{version}}" exit 1 fi git checkout main git pull --ff-only origin main git branch -D "release/{{version}}" 2>/dev/null || true echo "๐Ÿท๏ธ Creating tag {{version}} at $TAG_COMMIT..." git tag "{{version}}" "$TAG_COMMIT" git push origin "{{version}}" echo "โœ… Beta release {{version}} created successfully!" echo "๐Ÿ“ฆ GitHub Actions will build and publish to PyPI as pre-release" echo "๐Ÿ”— Monitor at: https://github.com/basicmachines-co/basic-memory/actions" echo "๐Ÿ“ฅ Install with: uv tool install basic-memory --pre" echo "" echo "๐Ÿ“ REMINDER: For stable releases, update documentation sites:" echo " 1. docs.basicmemory.com - Add a What's New page under content/2.whats-new/" echo " and bump the badge in content/index.md (see that repo's CLAUDE.md)" echo " 2. basicmemory.com - No version number in the site UI; for a significant" echo " release optionally add a post under src/content/blog/. Skip for patches." echo " See: .claude/commands/release/release.md for detailed instructions" # List all available recipes default: @just --list # Package latest combined skills using an existing ChatGPT plugin export. # Preserves plugin identity, MCP connections, metadata, and unrelated files. [positional-arguments] chatgpt-plugin *args: python3 scripts/build_chatgpt_plugin.py "$@"