This is page 32 of 47. Use http://codebase.md/doobidoo/mcp-memory-service?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ ├── amp-bridge.md
│ │ ├── amp-pr-automator.md
│ │ ├── code-quality-guard.md
│ │ ├── gemini-pr-automator.md
│ │ └── github-release-manager.md
│ ├── settings.local.json.backup
│ └── settings.local.json.local
├── .commit-message
├── .dockerignore
├── .env.example
├── .env.sqlite.backup
├── .envnn#
├── .gitattributes
├── .github
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── performance_issue.yml
│ ├── pull_request_template.md
│ └── workflows
│ ├── bridge-tests.yml
│ ├── CACHE_FIX.md
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── cleanup-images.yml.disabled
│ ├── dev-setup-validation.yml
│ ├── docker-publish.yml
│ ├── LATEST_FIXES.md
│ ├── main-optimized.yml.disabled
│ ├── main.yml
│ ├── publish-and-test.yml
│ ├── README_OPTIMIZATION.md
│ ├── release-tag.yml.disabled
│ ├── release.yml
│ ├── roadmap-review-reminder.yml
│ ├── SECRET_CONDITIONAL_FIX.md
│ └── WORKFLOW_FIXES.md
├── .gitignore
├── .mcp.json.backup
├── .mcp.json.template
├── .pyscn
│ ├── .gitignore
│ └── reports
│ └── analyze_20251123_214224.html
├── AGENTS.md
├── archive
│ ├── deployment
│ │ ├── deploy_fastmcp_fixed.sh
│ │ ├── deploy_http_with_mcp.sh
│ │ └── deploy_mcp_v4.sh
│ ├── deployment-configs
│ │ ├── empty_config.yml
│ │ └── smithery.yaml
│ ├── development
│ │ └── test_fastmcp.py
│ ├── docs-removed-2025-08-23
│ │ ├── authentication.md
│ │ ├── claude_integration.md
│ │ ├── claude-code-compatibility.md
│ │ ├── claude-code-integration.md
│ │ ├── claude-code-quickstart.md
│ │ ├── claude-desktop-setup.md
│ │ ├── complete-setup-guide.md
│ │ ├── database-synchronization.md
│ │ ├── development
│ │ │ ├── autonomous-memory-consolidation.md
│ │ │ ├── CLEANUP_PLAN.md
│ │ │ ├── CLEANUP_README.md
│ │ │ ├── CLEANUP_SUMMARY.md
│ │ │ ├── dream-inspired-memory-consolidation.md
│ │ │ ├── hybrid-slm-memory-consolidation.md
│ │ │ ├── mcp-milestone.md
│ │ │ ├── multi-client-architecture.md
│ │ │ ├── test-results.md
│ │ │ └── TIMESTAMP_FIX_SUMMARY.md
│ │ ├── distributed-sync.md
│ │ ├── invocation_guide.md
│ │ ├── macos-intel.md
│ │ ├── master-guide.md
│ │ ├── mcp-client-configuration.md
│ │ ├── multi-client-server.md
│ │ ├── service-installation.md
│ │ ├── sessions
│ │ │ └── MCP_ENHANCEMENT_SESSION_MEMORY_v4.1.0.md
│ │ ├── UBUNTU_SETUP.md
│ │ ├── ubuntu.md
│ │ ├── windows-setup.md
│ │ └── windows.md
│ ├── docs-root-cleanup-2025-08-23
│ │ ├── AWESOME_LIST_SUBMISSION.md
│ │ ├── CLOUDFLARE_IMPLEMENTATION.md
│ │ ├── DOCUMENTATION_ANALYSIS.md
│ │ ├── DOCUMENTATION_CLEANUP_PLAN.md
│ │ ├── DOCUMENTATION_CONSOLIDATION_COMPLETE.md
│ │ ├── LITESTREAM_SETUP_GUIDE.md
│ │ ├── lm_studio_system_prompt.md
│ │ ├── PYTORCH_DOWNLOAD_FIX.md
│ │ └── README-ORIGINAL-BACKUP.md
│ ├── investigations
│ │ └── MACOS_HOOKS_INVESTIGATION.md
│ ├── litestream-configs-v6.3.0
│ │ ├── install_service.sh
│ │ ├── litestream_master_config_fixed.yml
│ │ ├── litestream_master_config.yml
│ │ ├── litestream_replica_config_fixed.yml
│ │ ├── litestream_replica_config.yml
│ │ ├── litestream_replica_simple.yml
│ │ ├── litestream-http.service
│ │ ├── litestream.service
│ │ └── requirements-cloudflare.txt
│ ├── release-notes
│ │ └── release-notes-v7.1.4.md
│ └── setup-development
│ ├── README.md
│ ├── setup_consolidation_mdns.sh
│ ├── STARTUP_SETUP_GUIDE.md
│ └── test_service.sh
├── CHANGELOG-HISTORIC.md
├── CHANGELOG.md
├── claude_commands
│ ├── memory-context.md
│ ├── memory-health.md
│ ├── memory-ingest-dir.md
│ ├── memory-ingest.md
│ ├── memory-recall.md
│ ├── memory-search.md
│ ├── memory-store.md
│ ├── README.md
│ └── session-start.md
├── claude-hooks
│ ├── config.json
│ ├── config.template.json
│ ├── CONFIGURATION.md
│ ├── core
│ │ ├── memory-retrieval.js
│ │ ├── mid-conversation.js
│ │ ├── session-end.js
│ │ ├── session-start.js
│ │ └── topic-change.js
│ ├── debug-pattern-test.js
│ ├── install_claude_hooks_windows.ps1
│ ├── install_hooks.py
│ ├── memory-mode-controller.js
│ ├── MIGRATION.md
│ ├── README-NATURAL-TRIGGERS.md
│ ├── README-phase2.md
│ ├── README.md
│ ├── simple-test.js
│ ├── statusline.sh
│ ├── test-adaptive-weights.js
│ ├── test-dual-protocol-hook.js
│ ├── test-mcp-hook.js
│ ├── test-natural-triggers.js
│ ├── test-recency-scoring.js
│ ├── tests
│ │ ├── integration-test.js
│ │ ├── phase2-integration-test.js
│ │ ├── test-code-execution.js
│ │ ├── test-cross-session.json
│ │ ├── test-session-tracking.json
│ │ └── test-threading.json
│ ├── utilities
│ │ ├── adaptive-pattern-detector.js
│ │ ├── context-formatter.js
│ │ ├── context-shift-detector.js
│ │ ├── conversation-analyzer.js
│ │ ├── dynamic-context-updater.js
│ │ ├── git-analyzer.js
│ │ ├── mcp-client.js
│ │ ├── memory-client.js
│ │ ├── memory-scorer.js
│ │ ├── performance-manager.js
│ │ ├── project-detector.js
│ │ ├── session-tracker.js
│ │ ├── tiered-conversation-monitor.js
│ │ └── version-checker.js
│ └── WINDOWS-SESSIONSTART-BUG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Development-Sprint-November-2025.md
├── docs
│ ├── amp-cli-bridge.md
│ ├── api
│ │ ├── code-execution-interface.md
│ │ ├── memory-metadata-api.md
│ │ ├── PHASE1_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_REPORT.md
│ │ └── tag-standardization.md
│ ├── architecture
│ │ ├── search-enhancement-spec.md
│ │ └── search-examples.md
│ ├── architecture.md
│ ├── archive
│ │ └── obsolete-workflows
│ │ ├── load_memory_context.md
│ │ └── README.md
│ ├── assets
│ │ └── images
│ │ ├── dashboard-v3.3.0-preview.png
│ │ ├── memory-awareness-hooks-example.png
│ │ ├── project-infographic.svg
│ │ └── README.md
│ ├── CLAUDE_CODE_QUICK_REFERENCE.md
│ ├── cloudflare-setup.md
│ ├── deployment
│ │ ├── docker.md
│ │ ├── dual-service.md
│ │ ├── production-guide.md
│ │ └── systemd-service.md
│ ├── development
│ │ ├── ai-agent-instructions.md
│ │ ├── code-quality
│ │ │ ├── phase-2a-completion.md
│ │ │ ├── phase-2a-handle-get-prompt.md
│ │ │ ├── phase-2a-index.md
│ │ │ ├── phase-2a-install-package.md
│ │ │ └── phase-2b-session-summary.md
│ │ ├── code-quality-workflow.md
│ │ ├── dashboard-workflow.md
│ │ ├── issue-management.md
│ │ ├── pr-review-guide.md
│ │ ├── refactoring-notes.md
│ │ ├── release-checklist.md
│ │ └── todo-tracker.md
│ ├── docker-optimized-build.md
│ ├── document-ingestion.md
│ ├── DOCUMENTATION_AUDIT.md
│ ├── enhancement-roadmap-issue-14.md
│ ├── examples
│ │ ├── analysis-scripts.js
│ │ ├── maintenance-session-example.md
│ │ ├── memory-distribution-chart.jsx
│ │ └── tag-schema.json
│ ├── first-time-setup.md
│ ├── glama-deployment.md
│ ├── guides
│ │ ├── advanced-command-examples.md
│ │ ├── chromadb-migration.md
│ │ ├── commands-vs-mcp-server.md
│ │ ├── mcp-enhancements.md
│ │ ├── mdns-service-discovery.md
│ │ ├── memory-consolidation-guide.md
│ │ ├── migration.md
│ │ ├── scripts.md
│ │ └── STORAGE_BACKENDS.md
│ ├── HOOK_IMPROVEMENTS.md
│ ├── hooks
│ │ └── phase2-code-execution-migration.md
│ ├── http-server-management.md
│ ├── ide-compatability.md
│ ├── IMAGE_RETENTION_POLICY.md
│ ├── images
│ │ └── dashboard-placeholder.md
│ ├── implementation
│ │ ├── health_checks.md
│ │ └── performance.md
│ ├── IMPLEMENTATION_PLAN_HTTP_SSE.md
│ ├── integration
│ │ ├── homebrew.md
│ │ └── multi-client.md
│ ├── integrations
│ │ ├── gemini.md
│ │ ├── groq-bridge.md
│ │ ├── groq-integration-summary.md
│ │ └── groq-model-comparison.md
│ ├── integrations.md
│ ├── legacy
│ │ └── dual-protocol-hooks.md
│ ├── LM_STUDIO_COMPATIBILITY.md
│ ├── maintenance
│ │ └── memory-maintenance.md
│ ├── mastery
│ │ ├── api-reference.md
│ │ ├── architecture-overview.md
│ │ ├── configuration-guide.md
│ │ ├── local-setup-and-run.md
│ │ ├── testing-guide.md
│ │ └── troubleshooting.md
│ ├── migration
│ │ └── code-execution-api-quick-start.md
│ ├── natural-memory-triggers
│ │ ├── cli-reference.md
│ │ ├── installation-guide.md
│ │ └── performance-optimization.md
│ ├── oauth-setup.md
│ ├── pr-graphql-integration.md
│ ├── quick-setup-cloudflare-dual-environment.md
│ ├── README.md
│ ├── remote-configuration-wiki-section.md
│ ├── research
│ │ ├── code-execution-interface-implementation.md
│ │ └── code-execution-interface-summary.md
│ ├── ROADMAP.md
│ ├── sqlite-vec-backend.md
│ ├── statistics
│ │ ├── charts
│ │ │ ├── activity_patterns.png
│ │ │ ├── contributors.png
│ │ │ ├── growth_trajectory.png
│ │ │ ├── monthly_activity.png
│ │ │ └── october_sprint.png
│ │ ├── data
│ │ │ ├── activity_by_day.csv
│ │ │ ├── activity_by_hour.csv
│ │ │ ├── contributors.csv
│ │ │ └── monthly_activity.csv
│ │ ├── generate_charts.py
│ │ └── REPOSITORY_STATISTICS.md
│ ├── technical
│ │ ├── development.md
│ │ ├── memory-migration.md
│ │ ├── migration-log.md
│ │ ├── sqlite-vec-embedding-fixes.md
│ │ └── tag-storage.md
│ ├── testing
│ │ └── regression-tests.md
│ ├── testing-cloudflare-backend.md
│ ├── troubleshooting
│ │ ├── cloudflare-api-token-setup.md
│ │ ├── cloudflare-authentication.md
│ │ ├── general.md
│ │ ├── hooks-quick-reference.md
│ │ ├── pr162-schema-caching-issue.md
│ │ ├── session-end-hooks.md
│ │ └── sync-issues.md
│ └── tutorials
│ ├── advanced-techniques.md
│ ├── data-analysis.md
│ └── demo-session-walkthrough.md
├── examples
│ ├── claude_desktop_config_template.json
│ ├── claude_desktop_config_windows.json
│ ├── claude-desktop-http-config.json
│ ├── config
│ │ └── claude_desktop_config.json
│ ├── http-mcp-bridge.js
│ ├── memory_export_template.json
│ ├── README.md
│ ├── setup
│ │ └── setup_multi_client_complete.py
│ └── start_https_example.sh
├── install_service.py
├── install.py
├── LICENSE
├── NOTICE
├── pyproject.toml
├── pytest.ini
├── README.md
├── run_server.py
├── scripts
│ ├── .claude
│ │ └── settings.local.json
│ ├── archive
│ │ └── check_missing_timestamps.py
│ ├── backup
│ │ ├── backup_memories.py
│ │ ├── backup_sqlite_vec.sh
│ │ ├── export_distributable_memories.sh
│ │ └── restore_memories.py
│ ├── benchmarks
│ │ ├── benchmark_code_execution_api.py
│ │ ├── benchmark_hybrid_sync.py
│ │ └── benchmark_server_caching.py
│ ├── database
│ │ ├── analyze_sqlite_vec_db.py
│ │ ├── check_sqlite_vec_status.py
│ │ ├── db_health_check.py
│ │ └── simple_timestamp_check.py
│ ├── development
│ │ ├── debug_server_initialization.py
│ │ ├── find_orphaned_files.py
│ │ ├── fix_mdns.sh
│ │ ├── fix_sitecustomize.py
│ │ ├── remote_ingest.sh
│ │ ├── setup-git-merge-drivers.sh
│ │ ├── uv-lock-merge.sh
│ │ └── verify_hybrid_sync.py
│ ├── hooks
│ │ └── pre-commit
│ ├── installation
│ │ ├── install_linux_service.py
│ │ ├── install_macos_service.py
│ │ ├── install_uv.py
│ │ ├── install_windows_service.py
│ │ ├── install.py
│ │ ├── setup_backup_cron.sh
│ │ ├── setup_claude_mcp.sh
│ │ └── setup_cloudflare_resources.py
│ ├── linux
│ │ ├── service_status.sh
│ │ ├── start_service.sh
│ │ ├── stop_service.sh
│ │ ├── uninstall_service.sh
│ │ └── view_logs.sh
│ ├── maintenance
│ │ ├── assign_memory_types.py
│ │ ├── check_memory_types.py
│ │ ├── cleanup_corrupted_encoding.py
│ │ ├── cleanup_memories.py
│ │ ├── cleanup_organize.py
│ │ ├── consolidate_memory_types.py
│ │ ├── consolidation_mappings.json
│ │ ├── delete_orphaned_vectors_fixed.py
│ │ ├── fast_cleanup_duplicates_with_tracking.sh
│ │ ├── find_all_duplicates.py
│ │ ├── find_cloudflare_duplicates.py
│ │ ├── find_duplicates.py
│ │ ├── memory-types.md
│ │ ├── README.md
│ │ ├── recover_timestamps_from_cloudflare.py
│ │ ├── regenerate_embeddings.py
│ │ ├── repair_malformed_tags.py
│ │ ├── repair_memories.py
│ │ ├── repair_sqlite_vec_embeddings.py
│ │ ├── repair_zero_embeddings.py
│ │ ├── restore_from_json_export.py
│ │ └── scan_todos.sh
│ ├── migration
│ │ ├── cleanup_mcp_timestamps.py
│ │ ├── legacy
│ │ │ └── migrate_chroma_to_sqlite.py
│ │ ├── mcp-migration.py
│ │ ├── migrate_sqlite_vec_embeddings.py
│ │ ├── migrate_storage.py
│ │ ├── migrate_tags.py
│ │ ├── migrate_timestamps.py
│ │ ├── migrate_to_cloudflare.py
│ │ ├── migrate_to_sqlite_vec.py
│ │ ├── migrate_v5_enhanced.py
│ │ ├── TIMESTAMP_CLEANUP_README.md
│ │ └── verify_mcp_timestamps.py
│ ├── pr
│ │ ├── amp_collect_results.sh
│ │ ├── amp_detect_breaking_changes.sh
│ │ ├── amp_generate_tests.sh
│ │ ├── amp_pr_review.sh
│ │ ├── amp_quality_gate.sh
│ │ ├── amp_suggest_fixes.sh
│ │ ├── auto_review.sh
│ │ ├── detect_breaking_changes.sh
│ │ ├── generate_tests.sh
│ │ ├── lib
│ │ │ └── graphql_helpers.sh
│ │ ├── quality_gate.sh
│ │ ├── resolve_threads.sh
│ │ ├── run_pyscn_analysis.sh
│ │ ├── run_quality_checks.sh
│ │ ├── thread_status.sh
│ │ └── watch_reviews.sh
│ ├── quality
│ │ ├── fix_dead_code_install.sh
│ │ ├── phase1_dead_code_analysis.md
│ │ ├── phase2_complexity_analysis.md
│ │ ├── README_PHASE1.md
│ │ ├── README_PHASE2.md
│ │ ├── track_pyscn_metrics.sh
│ │ └── weekly_quality_review.sh
│ ├── README.md
│ ├── run
│ │ ├── run_mcp_memory.sh
│ │ ├── run-with-uv.sh
│ │ └── start_sqlite_vec.sh
│ ├── run_memory_server.py
│ ├── server
│ │ ├── check_http_server.py
│ │ ├── check_server_health.py
│ │ ├── memory_offline.py
│ │ ├── preload_models.py
│ │ ├── run_http_server.py
│ │ ├── run_memory_server.py
│ │ ├── start_http_server.bat
│ │ └── start_http_server.sh
│ ├── service
│ │ ├── deploy_dual_services.sh
│ │ ├── install_http_service.sh
│ │ ├── mcp-memory-http.service
│ │ ├── mcp-memory.service
│ │ ├── memory_service_manager.sh
│ │ ├── service_control.sh
│ │ ├── service_utils.py
│ │ └── update_service.sh
│ ├── sync
│ │ ├── check_drift.py
│ │ ├── claude_sync_commands.py
│ │ ├── export_memories.py
│ │ ├── import_memories.py
│ │ ├── litestream
│ │ │ ├── apply_local_changes.sh
│ │ │ ├── enhanced_memory_store.sh
│ │ │ ├── init_staging_db.sh
│ │ │ ├── io.litestream.replication.plist
│ │ │ ├── manual_sync.sh
│ │ │ ├── memory_sync.sh
│ │ │ ├── pull_remote_changes.sh
│ │ │ ├── push_to_remote.sh
│ │ │ ├── README.md
│ │ │ ├── resolve_conflicts.sh
│ │ │ ├── setup_local_litestream.sh
│ │ │ ├── setup_remote_litestream.sh
│ │ │ ├── staging_db_init.sql
│ │ │ ├── stash_local_changes.sh
│ │ │ ├── sync_from_remote_noconfig.sh
│ │ │ └── sync_from_remote.sh
│ │ ├── README.md
│ │ ├── safe_cloudflare_update.sh
│ │ ├── sync_memory_backends.py
│ │ └── sync_now.py
│ ├── testing
│ │ ├── run_complete_test.py
│ │ ├── run_memory_test.sh
│ │ ├── simple_test.py
│ │ ├── test_cleanup_logic.py
│ │ ├── test_cloudflare_backend.py
│ │ ├── test_docker_functionality.py
│ │ ├── test_installation.py
│ │ ├── test_mdns.py
│ │ ├── test_memory_api.py
│ │ ├── test_memory_simple.py
│ │ ├── test_migration.py
│ │ ├── test_search_api.py
│ │ ├── test_sqlite_vec_embeddings.py
│ │ ├── test_sse_events.py
│ │ ├── test-connection.py
│ │ └── test-hook.js
│ ├── utils
│ │ ├── claude_commands_utils.py
│ │ ├── generate_personalized_claude_md.sh
│ │ ├── groq
│ │ ├── groq_agent_bridge.py
│ │ ├── list-collections.py
│ │ ├── memory_wrapper_uv.py
│ │ ├── query_memories.py
│ │ ├── smithery_wrapper.py
│ │ ├── test_groq_bridge.sh
│ │ └── uv_wrapper.py
│ └── validation
│ ├── check_dev_setup.py
│ ├── check_documentation_links.py
│ ├── diagnose_backend_config.py
│ ├── validate_configuration_complete.py
│ ├── validate_memories.py
│ ├── validate_migration.py
│ ├── validate_timestamp_integrity.py
│ ├── verify_environment.py
│ ├── verify_pytorch_windows.py
│ └── verify_torch.py
├── SECURITY.md
├── selective_timestamp_recovery.py
├── SPONSORS.md
├── src
│ └── mcp_memory_service
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── operations.py
│ │ ├── sync_wrapper.py
│ │ └── types.py
│ ├── backup
│ │ ├── __init__.py
│ │ └── scheduler.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── ingestion.py
│ │ ├── main.py
│ │ └── utils.py
│ ├── config.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── associations.py
│ │ ├── base.py
│ │ ├── clustering.py
│ │ ├── compression.py
│ │ ├── consolidator.py
│ │ ├── decay.py
│ │ ├── forgetting.py
│ │ ├── health.py
│ │ └── scheduler.py
│ ├── dependency_check.py
│ ├── discovery
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── mdns_service.py
│ ├── embeddings
│ │ ├── __init__.py
│ │ └── onnx_embeddings.py
│ ├── ingestion
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── chunker.py
│ │ ├── csv_loader.py
│ │ ├── json_loader.py
│ │ ├── pdf_loader.py
│ │ ├── registry.py
│ │ ├── semtools_loader.py
│ │ └── text_loader.py
│ ├── lm_studio_compat.py
│ ├── mcp_server.py
│ ├── models
│ │ ├── __init__.py
│ │ └── memory.py
│ ├── server.py
│ ├── services
│ │ ├── __init__.py
│ │ └── memory_service.py
│ ├── storage
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── cloudflare.py
│ │ ├── factory.py
│ │ ├── http_client.py
│ │ ├── hybrid.py
│ │ └── sqlite_vec.py
│ ├── sync
│ │ ├── __init__.py
│ │ ├── exporter.py
│ │ ├── importer.py
│ │ └── litestream_config.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── cache_manager.py
│ │ ├── content_splitter.py
│ │ ├── db_utils.py
│ │ ├── debug.py
│ │ ├── document_processing.py
│ │ ├── gpu_detection.py
│ │ ├── hashing.py
│ │ ├── http_server_manager.py
│ │ ├── port_detection.py
│ │ ├── system_detection.py
│ │ └── time_parser.py
│ └── web
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── analytics.py
│ │ ├── backup.py
│ │ ├── consolidation.py
│ │ ├── documents.py
│ │ ├── events.py
│ │ ├── health.py
│ │ ├── manage.py
│ │ ├── mcp.py
│ │ ├── memories.py
│ │ ├── search.py
│ │ └── sync.py
│ ├── app.py
│ ├── dependencies.py
│ ├── oauth
│ │ ├── __init__.py
│ │ ├── authorization.py
│ │ ├── discovery.py
│ │ ├── middleware.py
│ │ ├── models.py
│ │ ├── registration.py
│ │ └── storage.py
│ ├── sse.py
│ └── static
│ ├── app.js
│ ├── index.html
│ ├── README.md
│ ├── sse_test.html
│ └── style.css
├── start_http_debug.bat
├── start_http_server.sh
├── test_document.txt
├── test_version_checker.js
├── tests
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── test_compact_types.py
│ │ └── test_operations.py
│ ├── bridge
│ │ ├── mock_responses.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── test_http_mcp_bridge.js
│ ├── conftest.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_associations.py
│ │ ├── test_clustering.py
│ │ ├── test_compression.py
│ │ ├── test_consolidator.py
│ │ ├── test_decay.py
│ │ └── test_forgetting.py
│ ├── contracts
│ │ └── api-specification.yml
│ ├── integration
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── test_api_key_fallback.py
│ │ ├── test_api_memories_chronological.py
│ │ ├── test_api_tag_time_search.py
│ │ ├── test_api_with_memory_service.py
│ │ ├── test_bridge_integration.js
│ │ ├── test_cli_interfaces.py
│ │ ├── test_cloudflare_connection.py
│ │ ├── test_concurrent_clients.py
│ │ ├── test_data_serialization_consistency.py
│ │ ├── test_http_server_startup.py
│ │ ├── test_mcp_memory.py
│ │ ├── test_mdns_integration.py
│ │ ├── test_oauth_basic_auth.py
│ │ ├── test_oauth_flow.py
│ │ ├── test_server_handlers.py
│ │ └── test_store_memory.py
│ ├── performance
│ │ ├── test_background_sync.py
│ │ └── test_hybrid_live.py
│ ├── README.md
│ ├── smithery
│ │ └── test_smithery.py
│ ├── sqlite
│ │ └── simple_sqlite_vec_test.py
│ ├── test_client.py
│ ├── test_content_splitting.py
│ ├── test_database.py
│ ├── test_hybrid_cloudflare_limits.py
│ ├── test_hybrid_storage.py
│ ├── test_memory_ops.py
│ ├── test_semantic_search.py
│ ├── test_sqlite_vec_storage.py
│ ├── test_time_parser.py
│ ├── test_timestamp_preservation.py
│ ├── timestamp
│ │ ├── test_hook_vs_manual_storage.py
│ │ ├── test_issue99_final_validation.py
│ │ ├── test_search_retrieval_inconsistency.py
│ │ ├── test_timestamp_issue.py
│ │ └── test_timestamp_simple.py
│ └── unit
│ ├── conftest.py
│ ├── test_cloudflare_storage.py
│ ├── test_csv_loader.py
│ ├── test_fastapi_dependencies.py
│ ├── test_import.py
│ ├── test_json_loader.py
│ ├── test_mdns_simple.py
│ ├── test_mdns.py
│ ├── test_memory_service.py
│ ├── test_memory.py
│ ├── test_semtools_loader.py
│ ├── test_storage_interface_compatibility.py
│ └── test_tag_time_filtering.py
├── tools
│ ├── docker
│ │ ├── DEPRECATED.md
│ │ ├── docker-compose.http.yml
│ │ ├── docker-compose.pythonpath.yml
│ │ ├── docker-compose.standalone.yml
│ │ ├── docker-compose.uv.yml
│ │ ├── docker-compose.yml
│ │ ├── docker-entrypoint-persistent.sh
│ │ ├── docker-entrypoint-unified.sh
│ │ ├── docker-entrypoint.sh
│ │ ├── Dockerfile
│ │ ├── Dockerfile.glama
│ │ ├── Dockerfile.slim
│ │ ├── README.md
│ │ └── test-docker-modes.sh
│ └── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/scripts/utils/claude_commands_utils.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # Copyright 2024 Heinrich Krupp
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """
17 | Utilities for installing and managing Claude Code commands for MCP Memory Service.
18 | """
19 |
20 | import os
21 | import sys
22 | import shutil
23 | import subprocess
24 | from pathlib import Path
25 | from datetime import datetime
26 | from typing import Optional, List, Dict, Tuple
27 |
28 |
29 | def print_info(text: str) -> None:
30 | """Print formatted info text."""
31 | print(f" -> {text}")
32 |
33 |
34 | def print_error(text: str) -> None:
35 | """Print formatted error text."""
36 | print(f" [ERROR] {text}")
37 |
38 |
39 | def print_success(text: str) -> None:
40 | """Print formatted success text."""
41 | print(f" [OK] {text}")
42 |
43 |
44 | def print_warning(text: str) -> None:
45 | """Print formatted warning text."""
46 | print(f" [WARNING] {text}")
47 |
48 |
49 | def check_claude_code_cli() -> Tuple[bool, Optional[str]]:
50 | """
51 | Check if Claude Code CLI is installed and available.
52 |
53 | Returns:
54 | Tuple of (is_available, version_or_error)
55 | """
56 | try:
57 | # Try to run claude --version
58 | result = subprocess.run(
59 | ['claude', '--version'],
60 | capture_output=True,
61 | text=True,
62 | timeout=10
63 | )
64 |
65 | if result.returncode == 0:
66 | version = result.stdout.strip()
67 | return True, version
68 | else:
69 | return False, f"claude command failed: {result.stderr.strip()}"
70 |
71 | except subprocess.TimeoutExpired:
72 | return False, "claude command timed out"
73 | except FileNotFoundError:
74 | return False, "claude command not found in PATH"
75 | except Exception as e:
76 | return False, f"Error checking claude CLI: {str(e)}"
77 |
78 |
79 | def get_claude_commands_directory() -> Path:
80 | """
81 | Get the Claude Code commands directory path.
82 |
83 | Returns:
84 | Path to ~/.claude/commands/
85 | """
86 | return Path.home() / ".claude" / "commands"
87 |
88 |
89 | def get_claude_hooks_directory() -> Path:
90 | """
91 | Get the Claude Code hooks directory path.
92 |
93 | Returns:
94 | Path to ~/.claude/hooks/
95 | """
96 | return Path.home() / ".claude" / "hooks"
97 |
98 |
99 | def check_for_legacy_claude_paths() -> Tuple[bool, List[str]]:
100 | """
101 | Check for legacy .claude-code directory structure and provide migration guidance.
102 |
103 | Returns:
104 | Tuple of (legacy_found, list_of_issues_and_recommendations)
105 | """
106 | issues = []
107 | legacy_found = False
108 |
109 | # Check for legacy .claude-code directories
110 | legacy_paths = [
111 | Path.home() / ".claude-code",
112 | Path.home() / ".claude-code" / "hooks",
113 | Path.home() / ".claude-code" / "commands"
114 | ]
115 |
116 | for legacy_path in legacy_paths:
117 | if legacy_path.exists():
118 | legacy_found = True
119 | issues.append(f"⚠ Found legacy directory: {legacy_path}")
120 |
121 | # Check what's in the legacy directory
122 | if legacy_path.name == "hooks" and any(legacy_path.glob("*.js")):
123 | issues.append(f" → Contains hook files that should be moved to ~/.claude/hooks/")
124 | elif legacy_path.name == "commands" and any(legacy_path.glob("*.md")):
125 | issues.append(f" → Contains command files that should be moved to ~/.claude/commands/")
126 |
127 | if legacy_found:
128 | issues.append("")
129 | issues.append("Migration steps:")
130 | issues.append("1. Create new directory: ~/.claude/")
131 | issues.append("2. Move hooks: ~/.claude-code/hooks/* → ~/.claude/hooks/")
132 | issues.append("3. Move commands: ~/.claude-code/commands/* → ~/.claude/commands/")
133 | issues.append("4. Update settings.json to reference new paths")
134 | issues.append("5. Remove old ~/.claude-code/ directory when satisfied")
135 |
136 | return legacy_found, issues
137 |
138 |
139 | def validate_claude_settings_paths() -> Tuple[bool, List[str]]:
140 | """
141 | Validate paths in Claude settings files and detect common Windows path issues.
142 |
143 | Returns:
144 | Tuple of (all_valid, list_of_issues_and_recommendations)
145 | """
146 | import json
147 | import platform
148 |
149 | issues = []
150 | all_valid = True
151 |
152 | # Common Claude settings locations
153 | settings_paths = [
154 | Path.home() / ".claude" / "settings.json",
155 | Path.home() / ".claude" / "settings.local.json"
156 | ]
157 |
158 | for settings_path in settings_paths:
159 | if not settings_path.exists():
160 | continue
161 |
162 | try:
163 | with open(settings_path, 'r', encoding='utf-8') as f:
164 | settings = json.load(f)
165 |
166 | # Check hooks configuration
167 | if 'hooks' in settings:
168 | for hook in settings.get('hooks', []):
169 | if 'command' in hook:
170 | command = hook['command']
171 |
172 | # Check for Windows path issues
173 | if platform.system() == "Windows":
174 | if '\\' in command and not command.startswith('"'):
175 | all_valid = False
176 | issues.append(f"⚠ Windows path with backslashes in {settings_path.name}:")
177 | issues.append(f" → {command}")
178 | issues.append(f" → Consider using forward slashes: {command.replace(chr(92), '/')}")
179 |
180 | # Check for legacy .claude-code references
181 | if '.claude-code' in command:
182 | all_valid = False
183 | issues.append(f"⚠ Legacy path reference in {settings_path.name}:")
184 | issues.append(f" → {command}")
185 | issues.append(f" → Update to use .claude instead of .claude-code")
186 |
187 | # Check for missing session-start-wrapper.bat
188 | if 'session-start-wrapper.bat' in command:
189 | all_valid = False
190 | issues.append(f"⚠ Reference to non-existent wrapper file in {settings_path.name}:")
191 | issues.append(f" → {command}")
192 | issues.append(f" → Use Node.js script directly: node path/to/session-start.js")
193 |
194 | # Check if referenced files exist
195 | if command.startswith('node '):
196 | script_path_str = command.replace('node ', '').strip()
197 | # Handle quoted paths
198 | if script_path_str.startswith('"') and script_path_str.endswith('"'):
199 | script_path_str = script_path_str[1:-1]
200 |
201 | script_path = Path(script_path_str)
202 | if not script_path.exists() and not script_path.is_absolute():
203 | # Try to resolve relative to home directory
204 | script_path = Path.home() / script_path_str
205 |
206 | if not script_path.exists():
207 | all_valid = False
208 | issues.append(f"⚠ Hook script not found: {script_path_str}")
209 | issues.append(f" → Check if hooks are properly installed")
210 |
211 | except json.JSONDecodeError as e:
212 | all_valid = False
213 | issues.append(f"⚠ JSON parsing error in {settings_path.name}: {str(e)}")
214 | except Exception as e:
215 | all_valid = False
216 | issues.append(f"⚠ Error reading {settings_path.name}: {str(e)}")
217 |
218 | return all_valid, issues
219 |
220 |
221 | def normalize_windows_path_for_json(path_str: str) -> str:
222 | """
223 | Normalize a Windows path for use in JSON configuration files.
224 |
225 | Args:
226 | path_str: Path string that may contain backslashes
227 |
228 | Returns:
229 | Path string with forward slashes suitable for JSON
230 | """
231 | import platform
232 |
233 | if platform.system() == "Windows":
234 | # Convert backslashes to forward slashes
235 | normalized = path_str.replace('\\', '/')
236 | # Handle double backslashes from escaped strings
237 | normalized = normalized.replace('//', '/')
238 | return normalized
239 |
240 | return path_str
241 |
242 |
243 | def check_commands_directory_access() -> Tuple[bool, str]:
244 | """
245 | Check if we can access and write to the Claude commands directory.
246 |
247 | Returns:
248 | Tuple of (can_access, status_message)
249 | """
250 | commands_dir = get_claude_commands_directory()
251 |
252 | try:
253 | # Check if directory exists
254 | if not commands_dir.exists():
255 | # Try to create it
256 | commands_dir.mkdir(parents=True, exist_ok=True)
257 | return True, f"Created commands directory: {commands_dir}"
258 |
259 | # Check if we can write to it
260 | test_file = commands_dir / ".test_write_access"
261 | try:
262 | test_file.write_text("test")
263 | test_file.unlink()
264 | return True, f"Commands directory accessible: {commands_dir}"
265 | except PermissionError:
266 | return False, f"No write permission to commands directory: {commands_dir}"
267 |
268 | except Exception as e:
269 | return False, f"Cannot access commands directory: {str(e)}"
270 |
271 |
272 | def get_source_commands_directory() -> Path:
273 | """
274 | Get the source directory containing the command markdown files.
275 |
276 | Returns:
277 | Path to the claude_commands directory in the project
278 | """
279 | # Get the directory containing this script
280 | script_dir = Path(__file__).parent
281 | # Go up one level to the project root and find claude_commands
282 | project_root = script_dir.parent
283 | return project_root / "claude_commands"
284 |
285 |
286 | def list_available_commands() -> List[Dict[str, str]]:
287 | """
288 | List all available command files in the source directory.
289 |
290 | Returns:
291 | List of command info dictionaries
292 | """
293 | source_dir = get_source_commands_directory()
294 | commands = []
295 |
296 | if not source_dir.exists():
297 | return commands
298 |
299 | for md_file in source_dir.glob("*.md"):
300 | # Extract command name from filename
301 | command_name = md_file.stem
302 |
303 | # Read the first line to get the description
304 | try:
305 | with open(md_file, 'r', encoding='utf-8') as f:
306 | first_line = f.readline().strip()
307 | # Remove markdown header formatting
308 | description = first_line.lstrip('# ').strip()
309 | except Exception:
310 | description = "Command description unavailable"
311 |
312 | commands.append({
313 | 'name': command_name,
314 | 'file': md_file.name,
315 | 'description': description,
316 | 'path': str(md_file)
317 | })
318 |
319 | return commands
320 |
321 |
322 | def backup_existing_commands() -> Optional[str]:
323 | """
324 | Create a backup of existing command files before installation.
325 |
326 | Returns:
327 | Path to backup directory if backup was created, None otherwise
328 | """
329 | commands_dir = get_claude_commands_directory()
330 |
331 | if not commands_dir.exists():
332 | return None
333 |
334 | # Check if there are any existing .md files
335 | existing_commands = list(commands_dir.glob("*.md"))
336 | if not existing_commands:
337 | return None
338 |
339 | # Create backup directory with timestamp
340 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
341 | backup_dir = commands_dir / f"backup_{timestamp}"
342 |
343 | try:
344 | backup_dir.mkdir(exist_ok=True)
345 |
346 | for cmd_file in existing_commands:
347 | shutil.copy2(cmd_file, backup_dir / cmd_file.name)
348 |
349 | print_info(f"Backed up {len(existing_commands)} existing commands to: {backup_dir}")
350 | return str(backup_dir)
351 |
352 | except Exception as e:
353 | print_error(f"Failed to create backup: {str(e)}")
354 | return None
355 |
356 |
357 | def install_command_files() -> Tuple[bool, List[str]]:
358 | """
359 | Install command markdown files to the Claude commands directory.
360 |
361 | Returns:
362 | Tuple of (success, list_of_installed_files)
363 | """
364 | source_dir = get_source_commands_directory()
365 | commands_dir = get_claude_commands_directory()
366 | installed_files = []
367 |
368 | if not source_dir.exists():
369 | print_error(f"Source commands directory not found: {source_dir}")
370 | return False, []
371 |
372 | try:
373 | # Ensure destination directory exists
374 | commands_dir.mkdir(parents=True, exist_ok=True)
375 |
376 | # Copy all .md files
377 | for md_file in source_dir.glob("*.md"):
378 | dest_file = commands_dir / md_file.name
379 | shutil.copy2(md_file, dest_file)
380 | installed_files.append(md_file.name)
381 | print_info(f"Installed: {md_file.name}")
382 |
383 | if installed_files:
384 | print_success(f"Successfully installed {len(installed_files)} Claude Code commands")
385 | return True, installed_files
386 | else:
387 | print_warning("No command files found to install")
388 | return False, []
389 |
390 | except Exception as e:
391 | print_error(f"Failed to install command files: {str(e)}")
392 | return False, []
393 |
394 |
395 | def verify_mcp_service_connectivity() -> Tuple[bool, str]:
396 | """
397 | Verify that the MCP Memory Service is accessible.
398 |
399 | Returns:
400 | Tuple of (is_accessible, status_message)
401 | """
402 | try:
403 | # Try to import the MCP service modules
404 | sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
405 |
406 | # Test basic connectivity
407 | from mcp_memory_service import config
408 |
409 | # Check if we can detect a running service
410 | # This is a basic check - in a real scenario, we'd try to connect
411 | return True, "MCP Memory Service modules available"
412 |
413 | except ImportError as e:
414 | return False, f"MCP Memory Service not properly installed: {str(e)}"
415 | except Exception as e:
416 | return False, f"Error checking MCP service: {str(e)}"
417 |
418 |
419 | def test_command_functionality() -> Tuple[bool, List[str]]:
420 | """
421 | Test that installed commands are accessible via Claude Code CLI.
422 |
423 | Returns:
424 | Tuple of (all_tests_passed, list_of_test_results)
425 | """
426 | commands_dir = get_claude_commands_directory()
427 | test_results = []
428 | all_passed = True
429 |
430 | # Check if command files exist and are readable
431 | for md_file in commands_dir.glob("memory-*.md"):
432 | try:
433 | with open(md_file, 'r', encoding='utf-8') as f:
434 | content = f.read()
435 | if len(content) > 0:
436 | test_results.append(f"✓ {md_file.name} - readable and non-empty")
437 | else:
438 | test_results.append(f"✗ {md_file.name} - file is empty")
439 | all_passed = False
440 | except Exception as e:
441 | test_results.append(f"✗ {md_file.name} - error reading: {str(e)}")
442 | all_passed = False
443 |
444 | # Try to run claude commands (if Claude CLI is available)
445 | claude_available, _ = check_claude_code_cli()
446 | if claude_available:
447 | try:
448 | # Test that claude can see our commands
449 | result = subprocess.run(
450 | ['claude', '--help'],
451 | capture_output=True,
452 | text=True,
453 | timeout=10
454 | )
455 | if result.returncode == 0:
456 | test_results.append("✓ Claude Code CLI is responsive")
457 | else:
458 | test_results.append("✗ Claude Code CLI returned error")
459 | all_passed = False
460 | except Exception as e:
461 | test_results.append(f"✗ Error testing Claude CLI: {str(e)}")
462 | all_passed = False
463 |
464 | return all_passed, test_results
465 |
466 |
467 | def uninstall_commands() -> Tuple[bool, List[str]]:
468 | """
469 | Uninstall MCP Memory Service commands from Claude Code.
470 |
471 | Returns:
472 | Tuple of (success, list_of_removed_files)
473 | """
474 | commands_dir = get_claude_commands_directory()
475 | removed_files = []
476 |
477 | if not commands_dir.exists():
478 | return True, [] # Nothing to remove
479 |
480 | try:
481 | # Remove all memory-*.md files
482 | for md_file in commands_dir.glob("memory-*.md"):
483 | md_file.unlink()
484 | removed_files.append(md_file.name)
485 | print_info(f"Removed: {md_file.name}")
486 |
487 | if removed_files:
488 | print_success(f"Successfully removed {len(removed_files)} commands")
489 | else:
490 | print_info("No MCP Memory Service commands found to remove")
491 |
492 | return True, removed_files
493 |
494 | except Exception as e:
495 | print_error(f"Failed to uninstall commands: {str(e)}")
496 | return False, []
497 |
498 |
499 | def install_claude_commands(verbose: bool = True) -> bool:
500 | """
501 | Main function to install Claude Code commands for MCP Memory Service.
502 |
503 | Args:
504 | verbose: Whether to print detailed progress information
505 |
506 | Returns:
507 | True if installation was successful, False otherwise
508 | """
509 | if verbose:
510 | print_info("Installing Claude Code commands for MCP Memory Service...")
511 |
512 | # Check for legacy paths and provide migration guidance
513 | legacy_found, legacy_issues = check_for_legacy_claude_paths()
514 | if legacy_found:
515 | print_warning("Legacy Claude Code directory structure detected:")
516 | for issue in legacy_issues:
517 | print_info(issue)
518 | print_info("")
519 |
520 | # Validate existing settings paths
521 | settings_valid, settings_issues = validate_claude_settings_paths()
522 | if not settings_valid:
523 | print_warning("Claude settings path issues detected:")
524 | for issue in settings_issues:
525 | print_info(issue)
526 | print_info("")
527 |
528 | # Check Claude Code CLI availability
529 | claude_available, claude_status = check_claude_code_cli()
530 | if not claude_available:
531 | print_error(f"Claude Code CLI not available: {claude_status}")
532 | print_info("Please install Claude Code CLI first: https://claude.ai/code")
533 | return False
534 |
535 | if verbose:
536 | print_success(f"Claude Code CLI detected: {claude_status}")
537 |
538 | # Check commands directory access
539 | can_access, access_status = check_commands_directory_access()
540 | if not can_access:
541 | print_error(access_status)
542 | return False
543 |
544 | if verbose:
545 | print_success(access_status)
546 |
547 | # Create backup of existing commands
548 | backup_path = backup_existing_commands()
549 |
550 | # Install command files
551 | install_success, installed_files = install_command_files()
552 | if not install_success:
553 | return False
554 |
555 | # Verify MCP service connectivity (optional - warn but don't fail)
556 | mcp_available, mcp_status = verify_mcp_service_connectivity()
557 | if mcp_available:
558 | if verbose:
559 | print_success(mcp_status)
560 | else:
561 | if verbose:
562 | print_warning(f"MCP service check: {mcp_status}")
563 | print_info("Commands installed but MCP service may need to be started")
564 |
565 | # Test command functionality
566 | if verbose:
567 | print_info("Testing installed commands...")
568 | tests_passed, test_results = test_command_functionality()
569 | for result in test_results:
570 | print_info(result)
571 |
572 | if tests_passed:
573 | print_success("All command tests passed")
574 | else:
575 | print_warning("Some command tests failed - commands may still work")
576 |
577 | # Show usage instructions
578 | if verbose:
579 | print_info("\nClaude Code commands installed successfully!")
580 | print_info("Available commands:")
581 | for cmd_file in installed_files:
582 | cmd_name = cmd_file.replace('.md', '')
583 | print_info(f" claude /{cmd_name}")
584 |
585 | print_info("\nExample usage:")
586 | print_info(' claude /memory-store "Important decision about architecture"')
587 | print_info(' claude /memory-recall "what did we decide last week?"')
588 | print_info(' claude /memory-search --tags "architecture,database"')
589 | print_info(' claude /memory-health')
590 |
591 | return True
592 |
593 |
594 | if __name__ == "__main__":
595 | # Allow running this script directly for testing
596 | import argparse
597 |
598 | parser = argparse.ArgumentParser(description="Install Claude Code commands for MCP Memory Service")
599 | parser.add_argument('--test', action='store_true', help='Test installation without installing')
600 | parser.add_argument('--uninstall', action='store_true', help='Uninstall commands')
601 | parser.add_argument('--validate', action='store_true', help='Validate Claude configuration paths')
602 | parser.add_argument('--quiet', action='store_true', help='Minimal output')
603 |
604 | args = parser.parse_args()
605 |
606 | if args.uninstall:
607 | success, removed = uninstall_commands()
608 | if success:
609 | sys.exit(0)
610 | else:
611 | sys.exit(1)
612 | elif args.validate:
613 | # Path validation mode
614 | print("Claude Code Configuration Validation")
615 | print("=" * 40)
616 |
617 | # Check for legacy paths
618 | legacy_found, legacy_issues = check_for_legacy_claude_paths()
619 | if legacy_found:
620 | print("❌ Legacy paths detected:")
621 | for issue in legacy_issues:
622 | print(f" {issue}")
623 | else:
624 | print("✅ No legacy paths found")
625 |
626 | print()
627 |
628 | # Validate settings
629 | settings_valid, settings_issues = validate_claude_settings_paths()
630 | if settings_valid:
631 | print("✅ Claude settings paths are valid")
632 | else:
633 | print("❌ Settings path issues detected:")
634 | for issue in settings_issues:
635 | print(f" {issue}")
636 |
637 | sys.exit(0 if settings_valid and not legacy_found else 1)
638 | elif args.test:
639 | # Test mode - check prerequisites but don't install
640 | claude_ok, claude_msg = check_claude_code_cli()
641 | access_ok, access_msg = check_commands_directory_access()
642 | mcp_ok, mcp_msg = verify_mcp_service_connectivity()
643 |
644 | print("Claude Code commands installation test:")
645 | print(f" Claude CLI: {'✓' if claude_ok else '✗'} {claude_msg}")
646 | print(f" Directory access: {'✓' if access_ok else '✗'} {access_msg}")
647 | print(f" MCP service: {'✓' if mcp_ok else '⚠'} {mcp_msg}")
648 |
649 | if claude_ok and access_ok:
650 | print("✓ Ready to install Claude Code commands")
651 | sys.exit(0)
652 | else:
653 | print("✗ Prerequisites not met")
654 | sys.exit(1)
655 | else:
656 | # Normal installation
657 | success = install_claude_commands(verbose=not args.quiet)
658 | sys.exit(0 if success else 1)
```
--------------------------------------------------------------------------------
/src/mcp_memory_service/consolidation/forgetting.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright 2024 Heinrich Krupp
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Controlled forgetting system with archival for memory management."""
16 |
17 | import os
18 | import json
19 | import shutil
20 | from typing import List, Dict, Any, Optional, Tuple
21 | from datetime import datetime
22 | from dataclasses import dataclass
23 | from pathlib import Path
24 | import hashlib
25 |
26 | from .base import ConsolidationBase, ConsolidationConfig
27 | from .decay import RelevanceScore
28 | from ..models.memory import Memory
29 |
30 | @dataclass
31 | class ForgettingCandidate:
32 | """A memory candidate for forgetting."""
33 | memory: Memory
34 | relevance_score: RelevanceScore
35 | forgetting_reasons: List[str]
36 | archive_priority: int # 1=high, 2=medium, 3=low
37 | can_be_deleted: bool
38 |
39 | @dataclass
40 | class ForgettingResult:
41 | """Result of forgetting operation."""
42 | memory_hash: str
43 | action_taken: str # 'archived', 'compressed', 'deleted', 'skipped'
44 | archive_path: Optional[str]
45 | compressed_version: Optional[Memory]
46 | metadata: Dict[str, Any]
47 |
48 | class ControlledForgettingEngine(ConsolidationBase):
49 | """
50 | Implements intelligent forgetting to maintain memory system health.
51 |
52 | Rather than deleting memories, this system compresses and archives
53 | low-value memories while maintaining audit trails and recovery options.
54 | """
55 |
56 | def __init__(self, config: ConsolidationConfig):
57 | super().__init__(config)
58 | self.relevance_threshold = config.relevance_threshold
59 | self.access_threshold_days = config.access_threshold_days
60 | self.archive_location = config.archive_location or "~/.mcp_memory_archive"
61 |
62 | # Ensure archive directory exists
63 | self.archive_path = Path(os.path.expanduser(self.archive_location))
64 | self.archive_path.mkdir(parents=True, exist_ok=True)
65 |
66 | # Create subdirectories for different archive types
67 | self.daily_archive = self.archive_path / "daily"
68 | self.compressed_archive = self.archive_path / "compressed"
69 | self.metadata_archive = self.archive_path / "metadata"
70 |
71 | for archive_dir in [self.daily_archive, self.compressed_archive, self.metadata_archive]:
72 | archive_dir.mkdir(exist_ok=True)
73 |
74 | async def process(self, memories: List[Memory], relevance_scores: List[RelevanceScore], **kwargs) -> List[ForgettingResult]:
75 | """Identify and process memories for controlled forgetting."""
76 | if not self._validate_memories(memories):
77 | return []
78 |
79 | # Create score lookup
80 | score_lookup = {score.memory_hash: score for score in relevance_scores}
81 |
82 | # Get access patterns from kwargs
83 | access_patterns = kwargs.get('access_patterns', {})
84 | time_horizon = kwargs.get('time_horizon', 'monthly')
85 |
86 | # Identify forgetting candidates
87 | candidates = await self._identify_forgetting_candidates(
88 | memories, score_lookup, access_patterns, time_horizon
89 | )
90 |
91 | if not candidates:
92 | self.logger.info("No memories identified for forgetting")
93 | return []
94 |
95 | # Process candidates
96 | results = []
97 | for candidate in candidates:
98 | result = await self._process_forgetting_candidate(candidate)
99 | results.append(result)
100 |
101 | # Log forgetting summary
102 | actions_summary = {}
103 | for result in results:
104 | action = result.action_taken
105 | actions_summary[action] = actions_summary.get(action, 0) + 1
106 |
107 | self.logger.info(f"Forgetting results: {actions_summary}")
108 | return results
109 |
110 | async def _identify_forgetting_candidates(
111 | self,
112 | memories: List[Memory],
113 | score_lookup: Dict[str, RelevanceScore],
114 | access_patterns: Dict[str, datetime],
115 | time_horizon: str
116 | ) -> List[ForgettingCandidate]:
117 | """Identify memories that are candidates for forgetting."""
118 | candidates = []
119 | current_time = datetime.now()
120 |
121 | for memory in memories:
122 | # Skip protected memories
123 | if self._is_protected_memory(memory):
124 | continue
125 |
126 | # Get relevance score
127 | relevance_score = score_lookup.get(memory.content_hash)
128 | if not relevance_score:
129 | continue
130 |
131 | # Check if memory meets forgetting criteria
132 | forgetting_reasons = []
133 | can_be_deleted = False
134 | archive_priority = 3 # Default to low priority
135 |
136 | # Low relevance check
137 | if relevance_score.total_score < self.relevance_threshold:
138 | forgetting_reasons.append("low_relevance")
139 | archive_priority = min(archive_priority, 2) # Medium priority
140 |
141 | # Access pattern check
142 | last_accessed = access_patterns.get(memory.content_hash)
143 | if not last_accessed and memory.updated_at:
144 | last_accessed = datetime.utcfromtimestamp(memory.updated_at)
145 |
146 | if last_accessed:
147 | days_since_access = (current_time - last_accessed).days
148 | if days_since_access > self.access_threshold_days:
149 | forgetting_reasons.append("old_access")
150 | if days_since_access > self.access_threshold_days * 2:
151 | archive_priority = min(archive_priority, 1) # High priority
152 | can_be_deleted = True # Can be deleted if very old
153 |
154 | # Memory type specific checks
155 | memory_type = self._extract_memory_type(memory)
156 | if memory_type == 'temporary':
157 | age_days = self._get_memory_age_days(memory, current_time)
158 | if age_days > 7: # Temporary memories older than a week
159 | forgetting_reasons.append("expired_temporary")
160 | can_be_deleted = True
161 | archive_priority = 1
162 |
163 | # Content quality checks
164 | if self._is_low_quality_content(memory):
165 | forgetting_reasons.append("low_quality")
166 | archive_priority = min(archive_priority, 2)
167 |
168 | # Duplicate content check
169 | if self._appears_to_be_duplicate(memory, memories):
170 | forgetting_reasons.append("potential_duplicate")
171 | can_be_deleted = True
172 | archive_priority = 1
173 |
174 | # Create candidate if reasons exist
175 | if forgetting_reasons:
176 | # Override time horizon restriction for certain types of deletable content
177 | can_delete_final = can_be_deleted
178 | if not (time_horizon in ['quarterly', 'yearly']):
179 | # Still allow deletion for expired temporary memories and duplicates
180 | if not ('expired_temporary' in forgetting_reasons or 'potential_duplicate' in forgetting_reasons):
181 | can_delete_final = False
182 |
183 | candidate = ForgettingCandidate(
184 | memory=memory,
185 | relevance_score=relevance_score,
186 | forgetting_reasons=forgetting_reasons,
187 | archive_priority=archive_priority,
188 | can_be_deleted=can_delete_final
189 | )
190 | candidates.append(candidate)
191 |
192 | # Sort by priority (higher priority = lower number = first in list)
193 | candidates.sort(key=lambda c: (c.archive_priority, -c.relevance_score.total_score))
194 |
195 | self.logger.info(f"Identified {len(candidates)} forgetting candidates")
196 | return candidates
197 |
198 | def _is_low_quality_content(self, memory: Memory) -> bool:
199 | """Check if memory content appears to be low quality."""
200 | content = memory.content.strip()
201 |
202 | # Very short content
203 | if len(content) < 10:
204 | return True
205 |
206 | # Mostly punctuation or special characters
207 | alpha_chars = sum(1 for c in content if c.isalpha())
208 | if alpha_chars / len(content) < 0.3: # Less than 30% alphabetic
209 | return True
210 |
211 | # Repetitive content patterns
212 | if len(set(content.split())) < len(content.split()) * 0.5: # Less than 50% unique words
213 | return True
214 |
215 | # Common low-value patterns
216 | low_value_patterns = [
217 | 'test', 'testing', 'hello world', 'lorem ipsum',
218 | 'asdf', 'qwerty', '1234', 'temp', 'temporary'
219 | ]
220 |
221 | content_lower = content.lower()
222 | for pattern in low_value_patterns:
223 | if pattern in content_lower and len(content) < 100:
224 | return True
225 |
226 | return False
227 |
228 | def _appears_to_be_duplicate(self, memory: Memory, all_memories: List[Memory]) -> bool:
229 | """Check if memory appears to be a duplicate of another memory."""
230 | content = memory.content.strip().lower()
231 |
232 | # Skip very short content for duplicate detection
233 | if len(content) < 20:
234 | return False
235 |
236 | for other_memory in all_memories:
237 | if other_memory.content_hash == memory.content_hash:
238 | continue
239 |
240 | other_content = other_memory.content.strip().lower()
241 |
242 | # Exact match
243 | if content == other_content:
244 | return True
245 |
246 | # Very similar content (simple check)
247 | if len(content) > 50 and len(other_content) > 50:
248 | # Check if one is a substring of the other with high overlap
249 | if content in other_content or other_content in content:
250 | return True
251 |
252 | # Check word overlap
253 | words1 = set(content.split())
254 | words2 = set(other_content.split())
255 |
256 | if len(words1) > 5 and len(words2) > 5:
257 | overlap = len(words1.intersection(words2))
258 | union = len(words1.union(words2))
259 |
260 | if overlap / union > 0.8: # 80% word overlap
261 | return True
262 |
263 | return False
264 |
265 | async def _process_forgetting_candidate(self, candidate: ForgettingCandidate) -> ForgettingResult:
266 | """Process a single forgetting candidate."""
267 | memory = candidate.memory
268 |
269 | try:
270 | # Determine action based on candidate properties
271 | if candidate.can_be_deleted and 'potential_duplicate' in candidate.forgetting_reasons:
272 | # Delete obvious duplicates or expired temporary content
273 | return await self._delete_memory(candidate)
274 |
275 | elif candidate.archive_priority <= 2:
276 | # Archive high and medium priority candidates
277 | return await self._archive_memory(candidate)
278 |
279 | else:
280 | # Compress low priority candidates
281 | return await self._compress_memory(candidate)
282 |
283 | except Exception as e:
284 | self.logger.error(f"Error processing forgetting candidate {memory.content_hash}: {e}")
285 | return ForgettingResult(
286 | memory_hash=memory.content_hash,
287 | action_taken='skipped',
288 | archive_path=None,
289 | compressed_version=None,
290 | metadata={'error': str(e)}
291 | )
292 |
293 | async def _archive_memory(self, candidate: ForgettingCandidate) -> ForgettingResult:
294 | """Archive a memory to the filesystem."""
295 | memory = candidate.memory
296 |
297 | # Create archive filename with timestamp and hash
298 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
299 | short_hash = memory.content_hash[:12]
300 | filename = f"{timestamp}_{short_hash}.json"
301 |
302 | # Choose archive directory based on priority
303 | if candidate.archive_priority == 1:
304 | archive_dir = self.daily_archive
305 | else:
306 | archive_dir = self.compressed_archive
307 |
308 | archive_file = archive_dir / filename
309 |
310 | # Create archive data
311 | archive_data = {
312 | 'memory': memory.to_dict(),
313 | 'relevance_score': {
314 | 'total_score': candidate.relevance_score.total_score,
315 | 'base_importance': candidate.relevance_score.base_importance,
316 | 'decay_factor': candidate.relevance_score.decay_factor,
317 | 'connection_boost': candidate.relevance_score.connection_boost,
318 | 'access_boost': candidate.relevance_score.access_boost,
319 | 'metadata': candidate.relevance_score.metadata
320 | },
321 | 'forgetting_metadata': {
322 | 'reasons': candidate.forgetting_reasons,
323 | 'archive_priority': candidate.archive_priority,
324 | 'archive_date': datetime.now().isoformat(),
325 | 'original_hash': memory.content_hash
326 | }
327 | }
328 |
329 | # Write to archive
330 | with open(archive_file, 'w', encoding='utf-8') as f:
331 | json.dump(archive_data, f, indent=2, ensure_ascii=False)
332 |
333 | # Create metadata entry
334 | await self._create_metadata_entry(memory, archive_file, 'archived')
335 |
336 | return ForgettingResult(
337 | memory_hash=memory.content_hash,
338 | action_taken='archived',
339 | archive_path=str(archive_file),
340 | compressed_version=None,
341 | metadata={
342 | 'archive_priority': candidate.archive_priority,
343 | 'reasons': candidate.forgetting_reasons,
344 | 'file_size': archive_file.stat().st_size
345 | }
346 | )
347 |
348 | async def _compress_memory(self, candidate: ForgettingCandidate) -> ForgettingResult:
349 | """Create a compressed version of the memory."""
350 | memory = candidate.memory
351 | original_content = memory.content
352 |
353 | # Simple compression: extract key information
354 | compressed_content = self._create_compressed_content(original_content)
355 |
356 | # Create compressed memory
357 | compressed_hash = hashlib.sha256(compressed_content.encode()).hexdigest()
358 | compressed_memory = Memory(
359 | content=compressed_content,
360 | content_hash=compressed_hash,
361 | tags=memory.tags + ['compressed'],
362 | memory_type='compressed',
363 | metadata={
364 | **memory.metadata,
365 | 'original_hash': memory.content_hash,
366 | 'original_length': len(original_content),
367 | 'compressed_length': len(compressed_content),
368 | 'compression_ratio': len(compressed_content) / len(original_content),
369 | 'compression_date': datetime.now().isoformat(),
370 | 'forgetting_reasons': candidate.forgetting_reasons
371 | },
372 | embedding=memory.embedding, # Preserve embedding
373 | created_at=memory.created_at,
374 | created_at_iso=memory.created_at_iso
375 | )
376 |
377 | # Archive original for recovery
378 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
379 | short_hash = memory.content_hash[:12]
380 | archive_file = self.compressed_archive / f"original_{timestamp}_{short_hash}.json"
381 |
382 | with open(archive_file, 'w', encoding='utf-8') as f:
383 | json.dump(memory.to_dict(), f, indent=2, ensure_ascii=False)
384 |
385 | # Create metadata entry
386 | await self._create_metadata_entry(memory, archive_file, 'compressed')
387 |
388 | return ForgettingResult(
389 | memory_hash=memory.content_hash,
390 | action_taken='compressed',
391 | archive_path=str(archive_file),
392 | compressed_version=compressed_memory,
393 | metadata={
394 | 'original_length': len(original_content),
395 | 'compressed_length': len(compressed_content),
396 | 'compression_ratio': len(compressed_content) / len(original_content),
397 | 'reasons': candidate.forgetting_reasons
398 | }
399 | )
400 |
401 | async def _delete_memory(self, candidate: ForgettingCandidate) -> ForgettingResult:
402 | """Delete a memory (with metadata backup)."""
403 | memory = candidate.memory
404 |
405 | # Always create a metadata backup before deletion
406 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
407 | short_hash = memory.content_hash[:12]
408 | backup_file = self.metadata_archive / f"deleted_{timestamp}_{short_hash}.json"
409 |
410 | backup_data = {
411 | 'memory': memory.to_dict(),
412 | 'deletion_metadata': {
413 | 'reasons': candidate.forgetting_reasons,
414 | 'deletion_date': datetime.now().isoformat(),
415 | 'relevance_score': candidate.relevance_score.total_score
416 | }
417 | }
418 |
419 | with open(backup_file, 'w', encoding='utf-8') as f:
420 | json.dump(backup_data, f, indent=2, ensure_ascii=False)
421 |
422 | return ForgettingResult(
423 | memory_hash=memory.content_hash,
424 | action_taken='deleted',
425 | archive_path=str(backup_file),
426 | compressed_version=None,
427 | metadata={
428 | 'reasons': candidate.forgetting_reasons,
429 | 'backup_location': str(backup_file)
430 | }
431 | )
432 |
433 | def _create_compressed_content(self, original_content: str) -> str:
434 | """Create compressed version of content preserving key information."""
435 | # Simple compression strategy: extract key sentences and important terms
436 | sentences = original_content.split('.')
437 |
438 | # Keep first and last sentences if content is long enough
439 | if len(sentences) > 3:
440 | key_sentences = [sentences[0].strip(), sentences[-1].strip()]
441 | middle_content = ' '.join(sentences[1:-1])
442 |
443 | # Extract important terms from middle content
444 | important_terms = self._extract_important_terms(middle_content)
445 | if important_terms:
446 | key_sentences.append(f"Key terms: {', '.join(important_terms[:5])}")
447 |
448 | compressed = '. '.join(key_sentences)
449 | else:
450 | # Content is already short, just clean it up
451 | compressed = original_content.strip()
452 |
453 | # Add compression indicator
454 | if len(compressed) < len(original_content) * 0.8:
455 | compressed += " [Compressed]"
456 |
457 | return compressed
458 |
459 | def _extract_important_terms(self, text: str) -> List[str]:
460 | """Extract important terms from text."""
461 | import re
462 |
463 | # Extract capitalized words, numbers, and technical terms
464 | terms = set()
465 |
466 | # Capitalized words (potential proper nouns)
467 | terms.update(re.findall(r'\b[A-Z][a-z]+\b', text))
468 |
469 | # Numbers and measurements
470 | terms.update(re.findall(r'\b\d+(?:\.\d+)?(?:\s*[a-zA-Z]+)?\b', text))
471 |
472 | # Words in quotes
473 | terms.update(re.findall(r'"([^"]*)"', text))
474 |
475 | # Technical-looking terms (CamelCase, with underscores, etc.)
476 | terms.update(re.findall(r'\b[a-z]+[A-Z][a-zA-Z]*\b', text))
477 | terms.update(re.findall(r'\b\w+_\w+\b', text))
478 |
479 | return list(terms)[:10] # Limit to 10 terms
480 |
481 | async def _create_metadata_entry(self, memory: Memory, archive_path: Path, action: str):
482 | """Create a metadata entry for tracking archived/compressed memories."""
483 | metadata_file = self.metadata_archive / "forgetting_log.jsonl"
484 |
485 | entry = {
486 | 'memory_hash': memory.content_hash,
487 | 'action': action,
488 | 'archive_path': str(archive_path),
489 | 'timestamp': datetime.now().isoformat(),
490 | 'memory_type': memory.memory_type,
491 | 'tags': memory.tags,
492 | 'content_length': len(memory.content)
493 | }
494 |
495 | # Append to log file
496 | with open(metadata_file, 'a', encoding='utf-8') as f:
497 | f.write(json.dumps(entry) + '\n')
498 |
499 | async def recover_memory(self, memory_hash: str) -> Optional[Memory]:
500 | """Recover a forgotten memory from archives."""
501 | # Search through archive directories
502 | for archive_dir in [self.daily_archive, self.compressed_archive]:
503 | for archive_file in archive_dir.glob("*.json"):
504 | try:
505 | with open(archive_file, 'r', encoding='utf-8') as f:
506 | data = json.load(f)
507 |
508 | # Check if this is the memory we're looking for
509 | if data.get('memory', {}).get('content_hash') == memory_hash:
510 | memory_data = data['memory']
511 | return Memory.from_dict(memory_data)
512 |
513 | except Exception as e:
514 | self.logger.warning(f"Error reading archive file {archive_file}: {e}")
515 |
516 | return None
517 |
518 | async def get_forgetting_statistics(self) -> Dict[str, Any]:
519 | """Get statistics about forgetting operations."""
520 | stats = {
521 | 'total_archived': 0,
522 | 'total_compressed': 0,
523 | 'total_deleted': 0,
524 | 'archive_size_bytes': 0,
525 | 'oldest_archive': None,
526 | 'newest_archive': None
527 | }
528 |
529 | # Count files in archive directories
530 | for archive_dir in [self.daily_archive, self.compressed_archive]:
531 | if archive_dir.exists():
532 | files = list(archive_dir.glob("*.json"))
533 | stats['total_archived'] += len(files)
534 |
535 | for file in files:
536 | stats['archive_size_bytes'] += file.stat().st_size
537 |
538 | # Read forgetting log for detailed stats
539 | log_file = self.metadata_archive / "forgetting_log.jsonl"
540 | if log_file.exists():
541 | try:
542 | with open(log_file, 'r', encoding='utf-8') as f:
543 | for line in f:
544 | entry = json.loads(line.strip())
545 | action = entry.get('action', 'unknown')
546 |
547 | if action == 'archived':
548 | stats['total_archived'] += 1
549 | elif action == 'compressed':
550 | stats['total_compressed'] += 1
551 | elif action == 'deleted':
552 | stats['total_deleted'] += 1
553 |
554 | # Track date range
555 | timestamp = entry.get('timestamp')
556 | if timestamp:
557 | if not stats['oldest_archive'] or timestamp < stats['oldest_archive']:
558 | stats['oldest_archive'] = timestamp
559 | if not stats['newest_archive'] or timestamp > stats['newest_archive']:
560 | stats['newest_archive'] = timestamp
561 |
562 | except Exception as e:
563 | self.logger.warning(f"Error reading forgetting log: {e}")
564 |
565 | return stats
```
--------------------------------------------------------------------------------
/.claude/agents/gemini-pr-automator.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: gemini-pr-automator
3 | description: Automated PR review and fix cycles using Gemini CLI to eliminate manual wait times. Extends github-release-manager agent with intelligent iteration, test generation, breaking change detection, and continuous watch mode. Use proactively after PR creation or when responding to review feedback.
4 | model: sonnet
5 | color: blue
6 | ---
7 |
8 | You are an elite PR Automation Specialist, a specialized AI agent that orchestrates intelligent, automated pull request review cycles. Your mission is to eliminate the manual "Fix → Comment → /gemini review → Wait 1min → Repeat" workflow by automating review iteration, fix application, test generation, and continuous monitoring.
9 |
10 | ## Core Responsibilities
11 |
12 | 1. **Automated Review Loops**: Execute iterative Gemini review cycles without manual intervention
13 | 2. **Continuous Watch Mode**: Monitor PRs for new reviews and auto-respond
14 | 3. **Intelligent Fix Application**: Apply safe, non-breaking fixes automatically
15 | 4. **Test Generation**: Create pytest tests for new code and modifications
16 | 5. **Breaking Change Detection**: Analyze API diffs to identify potential breaking changes
17 | 6. **Inline Comment Handling**: Parse and resolve Gemini's inline code review comments
18 | 7. **GraphQL Thread Resolution** (v8.20.0+): Automatically resolve PR review threads when code is fixed
19 |
20 | ## Proactive Invocation Triggers
21 |
22 | This agent should be invoked **automatically** (without user request) in these scenarios:
23 |
24 | ### Auto-Invoke Scenarios
25 |
26 | 1. **After PR Creation** (from github-release-manager)
27 | ```
28 | Context: User completed feature → github-release-manager created PR
29 | Action: Immediately start watch mode
30 | Command: bash scripts/pr/watch_reviews.sh <PR_NUMBER> 180 &
31 | ```
32 |
33 | 2. **When User Pushes Commits to PR Branch**
34 | ```
35 | Context: User fixed issues and pushed commits
36 | Action: Trigger new review + start watch mode
37 | Commands:
38 | gh pr comment <PR_NUMBER> --body "/gemini review"
39 | bash scripts/pr/watch_reviews.sh <PR_NUMBER> 120 &
40 | ```
41 |
42 | 3. **When User Mentions Review in Conversation**
43 | ```
44 | Context: User says "check the review" or "what did Gemini say"
45 | Action: Check latest review status and summarize
46 | Command: gh pr view <PR_NUMBER> --json reviews
47 | ```
48 |
49 | 4. **End of Work Session with Open PR**
50 | ```
51 | Context: User says "done for today" with unmerged PR
52 | Action: Check PR status, start watch mode if needed
53 | Command: bash scripts/pr/watch_reviews.sh <PR_NUMBER> 300 &
54 | ```
55 |
56 | ### Manual Invocation Only
57 |
58 | 1. **Complex Merge Conflicts**: User must resolve manually
59 | 2. **Architecture Decisions**: User input required
60 | 3. **API Breaking Changes**: User must approve migration strategy
61 |
62 | ## Problem Statement
63 |
64 | **Current Manual Workflow** (from github-release-manager.md):
65 | ```
66 | 1. Create PR
67 | 2. Add comment: "Please review"
68 | 3. Wait ~1 minute
69 | 4. Check Gemini feedback
70 | 5. Apply fixes manually
71 | 6. Repeat steps 2-5 until approved
72 | ```
73 |
74 | **Time Cost**: 5-10 iterations × 2-3 minutes per cycle = 10-30 minutes per PR
75 |
76 | **Automated Workflow** (this agent):
77 | ```
78 | 1. Create PR
79 | 2. Agent automatically:
80 | - Triggers Gemini review
81 | - Waits for feedback
82 | - Applies safe fixes
83 | - Commits changes
84 | - Re-triggers review
85 | - Repeats until approved or max iterations
86 | ```
87 |
88 | **Time Cost**: 5-10 iterations × automated = 0 minutes of manual work
89 |
90 | ## Gemini CLI Integration
91 |
92 | ### Basic PR Review Workflow
93 |
94 | ```bash
95 | #!/bin/bash
96 | # scripts/pr/auto_review.sh - Automated PR review loop
97 |
98 | PR_NUMBER=$1
99 | MAX_ITERATIONS=${2:-5}
100 | SAFE_FIX_MODE=${3:-true} # Auto-apply safe fixes
101 |
102 | if [ -z "$PR_NUMBER" ]; then
103 | echo "Usage: $0 <PR_NUMBER> [MAX_ITERATIONS] [SAFE_FIX_MODE]"
104 | exit 1
105 | fi
106 |
107 | iteration=1
108 | approved=false
109 |
110 | while [ $iteration -le $MAX_ITERATIONS ] && [ "$approved" = false ]; do
111 | echo "=== Iteration $iteration/$MAX_ITERATIONS ==="
112 |
113 | # Trigger Gemini review (comment on PR)
114 | gh pr comment $PR_NUMBER --body "Please review this PR for code quality, security, and best practices."
115 |
116 | # Wait for Gemini to process
117 | echo "Waiting for Gemini review..."
118 | sleep 90 # Gemini typically responds in 60-90 seconds
119 |
120 | # Fetch latest review comments
121 | review_comments=$(gh pr view $PR_NUMBER --json comments --jq '.comments[-1].body')
122 |
123 | echo "Review feedback:"
124 | echo "$review_comments"
125 |
126 | # Check if approved
127 | if echo "$review_comments" | grep -qi "looks good\|approved\|lgtm"; then
128 | echo "✅ PR approved by Gemini!"
129 | approved=true
130 | break
131 | fi
132 |
133 | # Extract issues and generate fixes
134 | if [ "$SAFE_FIX_MODE" = true ]; then
135 | echo "Generating fixes for review feedback..."
136 |
137 | # Use Gemini to analyze feedback and suggest code changes
138 | fixes=$(gemini "Based on this code review feedback, generate specific code fixes. Review feedback: $review_comments
139 |
140 | Changed files: $(gh pr diff $PR_NUMBER)
141 |
142 | Provide fixes in git diff format that can be applied with git apply. Focus only on safe, non-breaking changes.")
143 |
144 | # Apply fixes (would need more sophisticated parsing in production)
145 | echo "$fixes" > /tmp/pr_fixes_$PR_NUMBER.diff
146 |
147 | # Apply and commit
148 | if git apply --check /tmp/pr_fixes_$PR_NUMBER.diff 2>/dev/null; then
149 | git apply /tmp/pr_fixes_$PR_NUMBER.diff
150 | git add -A
151 | git commit -m "fix: apply Gemini review feedback (iteration $iteration)"
152 | git push
153 | echo "✅ Fixes applied and pushed"
154 | else
155 | echo "⚠️ Fixes could not be auto-applied, manual intervention needed"
156 | break
157 | fi
158 | else
159 | echo "Manual fix mode - review feedback above and apply manually"
160 | break
161 | fi
162 |
163 | iteration=$((iteration + 1))
164 | echo ""
165 | done
166 |
167 | if [ "$approved" = true ]; then
168 | echo "🎉 PR $PR_NUMBER is approved and ready to merge!"
169 | exit 0
170 | else
171 | echo "⚠️ Max iterations reached or manual intervention needed"
172 | exit 1
173 | fi
174 | ```
175 |
176 | ### Test Generation Workflow
177 |
178 | ```bash
179 | #!/bin/bash
180 | # scripts/pr/generate_tests.sh - Auto-generate tests for new code
181 |
182 | PR_NUMBER=$1
183 |
184 | if [ -z "$PR_NUMBER" ]; then
185 | echo "Usage: $0 <PR_NUMBER>"
186 | exit 1
187 | fi
188 |
189 | echo "Analyzing PR $PR_NUMBER for test coverage..."
190 |
191 | # Get changed Python files
192 | changed_files=$(gh pr diff $PR_NUMBER --name-only | grep '\.py$' | grep -v '^tests/')
193 |
194 | if [ -z "$changed_files" ]; then
195 | echo "No Python files changed (excluding tests)"
196 | exit 0
197 | fi
198 |
199 | for file in $changed_files; do
200 | echo "Generating tests for: $file"
201 |
202 | # Check if test file exists
203 | test_file="tests/test_$(basename $file)"
204 |
205 | if [ -f "$test_file" ]; then
206 | echo "Test file exists, suggesting additional test cases..."
207 | existing_tests=$(cat "$test_file")
208 | prompt="Existing test file: $existing_tests
209 |
210 | New/changed code: $(cat $file)
211 |
212 | Suggest additional pytest test cases to cover the new/changed code. Output only the new test functions to append to the existing file."
213 | else
214 | echo "Creating new test file..."
215 | prompt="Generate comprehensive pytest tests for this Python module: $(cat $file)
216 |
217 | Include:
218 | - Happy path tests
219 | - Edge cases
220 | - Error handling
221 | - Async test cases if applicable
222 |
223 | Output complete pytest test file."
224 | fi
225 |
226 | gemini "$prompt" > "/tmp/test_gen_$file.py"
227 |
228 | echo "Generated tests saved to /tmp/test_gen_$file.py"
229 | echo "Review and apply with: cat /tmp/test_gen_$file.py >> $test_file"
230 | echo ""
231 | done
232 | ```
233 |
234 | ### Breaking Change Detection
235 |
236 | ```bash
237 | #!/bin/bash
238 | # scripts/pr/detect_breaking_changes.sh - Analyze API changes for breaking changes
239 |
240 | BASE_BRANCH=${1:-main}
241 | HEAD_BRANCH=${2:-$(git branch --show-current)}
242 |
243 | echo "Detecting breaking changes: $BASE_BRANCH...$HEAD_BRANCH"
244 |
245 | # Get API-related file changes
246 | api_changes=$(git diff $BASE_BRANCH...$HEAD_BRANCH -- src/mcp_memory_service/tools.py src/mcp_memory_service/web/api/)
247 |
248 | if [ -z "$api_changes" ]; then
249 | echo "✅ No API changes detected"
250 | exit 0
251 | fi
252 |
253 | echo "Analyzing API changes for breaking changes..."
254 |
255 | result=$(gemini "Analyze these API changes for breaking changes. A breaking change is:
256 | - Removed function/method/endpoint
257 | - Changed function signature (parameters removed/reordered)
258 | - Changed return type
259 | - Renamed public API
260 | - Changed HTTP endpoint path/method
261 |
262 | Report ONLY breaking changes with severity (CRITICAL/HIGH/MEDIUM).
263 |
264 | Changes:
265 | $api_changes")
266 |
267 | if echo "$result" | grep -qi "breaking\|CRITICAL\|HIGH"; then
268 | echo "🔴 BREAKING CHANGES DETECTED:"
269 | echo "$result"
270 | exit 1
271 | else
272 | echo "✅ No breaking changes detected"
273 | exit 0
274 | fi
275 | ```
276 |
277 | ## Decision-Making Framework
278 |
279 | ### When to Use Auto-Iteration
280 |
281 | **Use automated iteration when**:
282 | - PR contains straightforward code quality fixes
283 | - Changes are non-critical (not release-blocking)
284 | - Reviewer feedback is typically formatting/style
285 | - Team has confidence in automated fix safety
286 |
287 | **Use manual iteration when**:
288 | - PR touches critical paths (authentication, storage backends)
289 | - Architectural changes requiring human judgment
290 | - Security-related modifications
291 | - Complex refactoring with cross-file dependencies
292 |
293 | ### Safe Fix Classification
294 |
295 | **Safe Fixes** (auto-apply):
296 | - Formatting changes (whitespace, line length)
297 | - Import organization
298 | - Type hint additions
299 | - Docstring improvements
300 | - Variable renaming for clarity
301 | - Simple refactoring (extract method with identical behavior)
302 |
303 | **Unsafe Fixes** (manual review required):
304 | - Logic changes
305 | - Error handling modifications
306 | - API signature changes
307 | - Database queries
308 | - Authentication/authorization code
309 | - Performance optimizations with side effects
310 |
311 | ### Iteration Limits
312 |
313 | - **Standard PRs**: Max 5 iterations
314 | - **Urgent fixes**: Max 3 iterations (faster manual intervention if needed)
315 | - **Experimental features**: Max 10 iterations (more tolerance for iteration)
316 | - **Release PRs**: Max 2 iterations (strict human oversight)
317 |
318 | ## Operational Workflows
319 |
320 | ### 1. Full Automated PR Review Cycle
321 |
322 | ```bash
323 | #!/bin/bash
324 | # scripts/pr/full_auto_review.sh - Complete automated PR workflow
325 |
326 | PR_NUMBER=$1
327 |
328 | echo "Starting automated PR review for #$PR_NUMBER"
329 |
330 | # Step 1: Run code quality checks
331 | echo "Step 1: Code quality analysis..."
332 | bash scripts/pr/quality_gate.sh $PR_NUMBER
333 | if [ $? -ne 0 ]; then
334 | echo "❌ Quality checks failed, fix issues first"
335 | exit 1
336 | fi
337 |
338 | # Step 2: Generate tests for new code
339 | echo "Step 2: Test generation..."
340 | bash scripts/pr/generate_tests.sh $PR_NUMBER
341 |
342 | # Step 3: Check for breaking changes
343 | echo "Step 3: Breaking change detection..."
344 | bash scripts/pr/detect_breaking_changes.sh main $(gh pr view $PR_NUMBER --json headRefName --jq '.headRefName')
345 | if [ $? -ne 0 ]; then
346 | echo "⚠️ Breaking changes detected, review carefully"
347 | fi
348 |
349 | # Step 4: Automated review loop
350 | echo "Step 4: Automated Gemini review iteration..."
351 | bash scripts/pr/auto_review.sh $PR_NUMBER 5 true
352 |
353 | # Step 5: Final status
354 | if [ $? -eq 0 ]; then
355 | echo "🎉 PR #$PR_NUMBER is ready for merge!"
356 | gh pr comment $PR_NUMBER --body "✅ Automated review completed successfully. All checks passed!"
357 | else
358 | echo "⚠️ Manual intervention needed for PR #$PR_NUMBER"
359 | gh pr comment $PR_NUMBER --body "⚠️ Automated review requires manual attention. Please review feedback above."
360 | fi
361 | ```
362 |
363 | ### 2. Intelligent Fix Application
364 |
365 | ```bash
366 | #!/bin/bash
367 | # scripts/pr/apply_review_fixes.sh - Parse and apply Gemini feedback
368 |
369 | PR_NUMBER=$1
370 | REVIEW_COMMENT_ID=$2
371 |
372 | if [ -z "$PR_NUMBER" ] || [ -z "$REVIEW_COMMENT_ID" ]; then
373 | echo "Usage: $0 <PR_NUMBER> <REVIEW_COMMENT_ID>"
374 | exit 1
375 | fi
376 |
377 | # Fetch specific review comment
378 | review_text=$(gh api "repos/:owner/:repo/pulls/$PR_NUMBER/comments/$REVIEW_COMMENT_ID" --jq '.body')
379 |
380 | echo "Analyzing review feedback..."
381 |
382 | # Use Gemini to categorize issues
383 | categorized=$(gemini "Categorize these code review comments into: SAFE (can auto-fix), UNSAFE (needs manual review), NON-CODE (documentation/discussion).
384 |
385 | Review comments:
386 | $review_text
387 |
388 | Output in JSON format:
389 | {
390 | \"safe\": [\"issue 1\", \"issue 2\"],
391 | \"unsafe\": [\"issue 3\"],
392 | \"non_code\": [\"comment 1\"]
393 | }")
394 |
395 | echo "$categorized" > /tmp/categorized_issues_$PR_NUMBER.json
396 |
397 | # Extract safe issues
398 | safe_issues=$(echo "$categorized" | jq -r '.safe[]')
399 |
400 | if [ -z "$safe_issues" ]; then
401 | echo "No safe auto-fixable issues found"
402 | exit 0
403 | fi
404 |
405 | echo "Safe issues to auto-fix:"
406 | echo "$safe_issues"
407 |
408 | # Generate fixes for safe issues
409 | fixes=$(gemini "Generate code fixes for these issues. Changed files: $(gh pr diff $PR_NUMBER)
410 |
411 | Issues to fix:
412 | $safe_issues
413 |
414 | Provide fixes as git diff patches.")
415 |
416 | echo "$fixes" > /tmp/fixes_$PR_NUMBER.patch
417 |
418 | # Apply fixes
419 | if git apply --check /tmp/fixes_$PR_NUMBER.patch 2>/dev/null; then
420 | git apply /tmp/fixes_$PR_NUMBER.patch
421 | git add -A
422 | git commit -m "fix: apply Gemini review feedback
423 |
424 | Addressed: $(echo \"$safe_issues\" | tr '\n' ', ')
425 |
426 | Co-Authored-By: Gemini Code Assist <[email protected]>"
427 | git push
428 | echo "✅ Fixes applied successfully"
429 |
430 | # Update PR with comment
431 | gh pr comment $PR_NUMBER --body "✅ Auto-applied fixes for: $(echo \"$safe_issues\" | tr '\n' ', ')"
432 | else
433 | echo "❌ Could not apply fixes automatically"
434 | exit 1
435 | fi
436 | ```
437 |
438 | ### 3. PR Quality Gate Integration
439 |
440 | ```bash
441 | #!/bin/bash
442 | # scripts/pr/quality_gate.sh - Run all quality checks before review
443 |
444 | PR_NUMBER=$1
445 |
446 | echo "Running PR quality gate checks for #$PR_NUMBER..."
447 |
448 | exit_code=0
449 |
450 | # Check 1: Code complexity
451 | echo "Check 1: Code complexity..."
452 | changed_files=$(gh pr diff $PR_NUMBER --name-only | grep '\.py$')
453 |
454 | for file in $changed_files; do
455 | result=$(gemini "Check complexity. Report ONLY if any function scores >7: $(cat $file)")
456 | if [ ! -z "$result" ]; then
457 | echo "⚠️ High complexity in $file: $result"
458 | exit_code=1
459 | fi
460 | done
461 |
462 | # Check 2: Security scan
463 | echo "Check 2: Security vulnerabilities..."
464 | for file in $changed_files; do
465 | result=$(gemini "Security scan. Report ONLY vulnerabilities: $(cat $file)")
466 | if [ ! -z "$result" ]; then
467 | echo "🔴 Security issue in $file: $result"
468 | exit_code=2 # Critical failure
469 | break
470 | fi
471 | done
472 |
473 | # Check 3: Test coverage
474 | echo "Check 3: Test coverage..."
475 | test_files=$(gh pr diff $PR_NUMBER --name-only | grep -c '^tests/.*\.py$' || echo "0")
476 | code_files=$(gh pr diff $PR_NUMBER --name-only | grep '\.py$' | grep -vc '^tests/' || echo "0")
477 |
478 | if [ $code_files -gt 0 ] && [ $test_files -eq 0 ]; then
479 | echo "⚠️ No test files added/modified despite code changes"
480 | exit_code=1
481 | fi
482 |
483 | # Check 4: Breaking changes
484 | echo "Check 4: Breaking changes..."
485 | bash scripts/pr/detect_breaking_changes.sh main $(gh pr view $PR_NUMBER --json headRefName --jq '.headRefName')
486 | if [ $? -ne 0 ]; then
487 | echo "⚠️ Potential breaking changes detected"
488 | exit_code=1
489 | fi
490 |
491 | # Report results
492 | if [ $exit_code -eq 0 ]; then
493 | echo "✅ All quality gate checks passed"
494 | gh pr comment $PR_NUMBER --body "✅ **Quality Gate PASSED**
495 |
496 | All automated checks completed successfully:
497 | - Code complexity: OK
498 | - Security scan: OK
499 | - Test coverage: OK
500 | - Breaking changes: None detected
501 |
502 | Ready for Gemini review."
503 | elif [ $exit_code -eq 2 ]; then
504 | echo "🔴 CRITICAL: Security issues found, blocking PR"
505 | gh pr comment $PR_NUMBER --body "🔴 **Quality Gate FAILED - CRITICAL**
506 |
507 | Security vulnerabilities detected. PR is blocked until issues are resolved.
508 |
509 | Please run: \`bash scripts/security/scan_vulnerabilities.sh\` locally and fix all issues."
510 | else
511 | echo "⚠️ Quality gate checks found issues (non-blocking)"
512 | gh pr comment $PR_NUMBER --body "⚠️ **Quality Gate WARNINGS**
513 |
514 | Some checks require attention (non-blocking):
515 | - See logs above for details
516 |
517 | Consider addressing these before requesting review."
518 | fi
519 |
520 | exit $exit_code
521 | ```
522 |
523 | ### 4. Continuous Watch Mode (Recommended)
524 |
525 | **NEW**: Automated monitoring for continuous PR review cycles.
526 |
527 | ```bash
528 | #!/bin/bash
529 | # scripts/pr/watch_reviews.sh - Monitor PR for Gemini reviews and auto-respond
530 |
531 | PR_NUMBER=$1
532 | CHECK_INTERVAL=${2:-180} # Default: 3 minutes
533 |
534 | echo "Starting PR watch mode for #$PR_NUMBER"
535 | echo "Checking every ${CHECK_INTERVAL}s for new reviews..."
536 |
537 | last_review_time=""
538 |
539 | while true; do
540 | # Get latest Gemini review timestamp
541 | repo=$(gh repo view --json nameWithOwner -q .nameWithOwner)
542 | current_review_time=$(gh api "repos/$repo/pulls/$PR_NUMBER/reviews" | \
543 | jq -r '[.[] | select(.user.login == "gemini-code-assist")] | last | .submitted_at')
544 |
545 | # Detect new review
546 | if [ -n "$current_review_time" ] && [ "$current_review_time" != "$last_review_time" ]; then
547 | echo "🔔 NEW REVIEW DETECTED!"
548 | last_review_time="$current_review_time"
549 |
550 | # Get review state
551 | review_state=$(gh pr view $PR_NUMBER --json reviews --jq \
552 | '[.reviews[] | select(.author.login == "gemini-code-assist")] | last | .state')
553 |
554 | # Get inline comments count
555 | comments_count=$(gh api "repos/$repo/pulls/$PR_NUMBER/comments" | \
556 | jq '[.[] | select(.user.login == "gemini-code-assist")] | length')
557 |
558 | echo " State: $review_state"
559 | echo " Inline Comments: $comments_count"
560 |
561 | # Handle review state
562 | if [ "$review_state" = "APPROVED" ]; then
563 | echo "✅ PR APPROVED!"
564 | echo " Ready to merge: gh pr merge $PR_NUMBER --squash"
565 | exit 0
566 |
567 | elif [ "$review_state" = "CHANGES_REQUESTED" ] || [ "$comments_count" -gt 0 ]; then
568 | echo "📝 Review feedback received"
569 |
570 | # Optionally auto-fix
571 | read -t 30 -p "Auto-run review cycle? (y/N): " response || response="n"
572 |
573 | if [[ "$response" =~ ^[Yy]$ ]]; then
574 | echo "🤖 Starting automated fix cycle..."
575 | bash scripts/pr/auto_review.sh $PR_NUMBER 3 true
576 | fi
577 | fi
578 | fi
579 |
580 | sleep $CHECK_INTERVAL
581 | done
582 | ```
583 |
584 | **Usage:**
585 |
586 | ```bash
587 | # Start watch mode (checks every 3 minutes)
588 | bash scripts/pr/watch_reviews.sh 212
589 |
590 | # Faster polling (every 2 minutes)
591 | bash scripts/pr/watch_reviews.sh 212 120
592 |
593 | # Run in background
594 | bash scripts/pr/watch_reviews.sh 212 180 &
595 | ```
596 |
597 | **When to Use Watch Mode vs Auto-Review:**
598 |
599 | | Scenario | Use | Command |
600 | |----------|-----|---------|
601 | | **Just created PR** | Auto-review (immediate) | `bash scripts/pr/auto_review.sh 212 5 true` |
602 | | **Pushed new commits** | Watch mode (continuous) | `bash scripts/pr/watch_reviews.sh 212` |
603 | | **Waiting for approval** | Watch mode (continuous) | `bash scripts/pr/watch_reviews.sh 212 180` |
604 | | **One-time fix cycle** | Auto-review (immediate) | `bash scripts/pr/auto_review.sh 212 3 true` |
605 |
606 | **Benefits:**
607 | - ✅ Auto-detects new reviews (no manual `/gemini review` needed)
608 | - ✅ Handles inline comments that auto-resolve when fixed
609 | - ✅ Offers optional auto-fix at each iteration
610 | - ✅ Exits automatically when approved
611 | - ✅ Runs indefinitely until approved or stopped
612 |
613 | ### 5. GraphQL Thread Resolution (v8.20.0+)
614 |
615 | **NEW**: Automatic PR review thread resolution using GitHub GraphQL API.
616 |
617 | **Problem:** GitHub's REST API cannot resolve PR review threads. Manual clicking "Resolve" 30+ times per PR is time-consuming and error-prone.
618 |
619 | **Solution:** GraphQL API provides `resolveReviewThread` mutation for programmatic thread resolution.
620 |
621 | **Key Components:**
622 |
623 | 1. **GraphQL Helpers Library** (`scripts/pr/lib/graphql_helpers.sh`)
624 | - `get_review_threads <PR_NUMBER>` - Fetch all threads with metadata
625 | - `resolve_review_thread <THREAD_ID> [COMMENT]` - Resolve with explanation
626 | - `get_thread_stats <PR_NUMBER>` - Get counts (total, resolved, unresolved)
627 | - `was_line_modified <FILE> <LINE> <COMMIT>` - Check if code changed
628 |
629 | 2. **Smart Resolution Tool** (`scripts/pr/resolve_threads.sh`)
630 | - Automatically resolves threads when referenced code is modified
631 | - Interactive or auto mode (--auto flag)
632 | - Adds explanatory comments with commit references
633 |
634 | 3. **Thread Status Tool** (`scripts/pr/thread_status.sh`)
635 | - Display all threads with filtering (--unresolved, --resolved, --outdated)
636 | - Comprehensive status including file paths, line numbers, authors
637 |
638 | **Usage:**
639 |
640 | ```bash
641 | # Check thread status
642 | bash scripts/pr/thread_status.sh 212
643 |
644 | # Auto-resolve threads after pushing fixes
645 | bash scripts/pr/resolve_threads.sh 212 HEAD --auto
646 |
647 | # Interactive resolution (prompts for each thread)
648 | bash scripts/pr/resolve_threads.sh 212 HEAD
649 | ```
650 |
651 | **Integration with Auto-Review:**
652 |
653 | The `auto_review.sh` script now **automatically resolves threads** after applying fixes:
654 |
655 | ```bash
656 | # After pushing fixes
657 | echo "Resolving review threads for fixed code..."
658 | latest_commit=$(git rev-parse HEAD)
659 | bash scripts/pr/resolve_threads.sh $PR_NUMBER $latest_commit --auto
660 |
661 | # Output:
662 | # Resolved: 8 threads
663 | # Skipped: 3 threads (no changes detected)
664 | # Failed: 0 threads
665 | ```
666 |
667 | **Integration with Watch Mode:**
668 |
669 | The `watch_reviews.sh` script now **displays thread status** during monitoring:
670 |
671 | ```bash
672 | # On each check cycle
673 | Review Threads: 45 total, 30 resolved, 15 unresolved
674 |
675 | # When new review detected
676 | Thread Status:
677 | Thread #1: scripts/pr/auto_review.sh:89 (UNRESOLVED)
678 | Thread #2: scripts/pr/quality_gate.sh:45 (UNRESOLVED)
679 | ...
680 |
681 | Options:
682 | 1. View detailed thread status:
683 | bash scripts/pr/thread_status.sh 212
684 | 2. Run auto-review (auto-resolves threads):
685 | bash scripts/pr/auto_review.sh 212 5 true
686 | 3. Manually resolve after fixes:
687 | bash scripts/pr/resolve_threads.sh 212 HEAD --auto
688 | ```
689 |
690 | **Decision Logic for Thread Resolution:**
691 |
692 | ```
693 | For each unresolved thread:
694 | ├─ Is the file modified in this commit?
695 | │ ├─ YES → Was the specific line changed?
696 | │ │ ├─ YES → ✅ Resolve with "Line X modified in commit ABC"
697 | │ │ └─ NO → ⏭️ Skip (file changed but not this line)
698 | │ └─ NO → Is thread marked "outdated" by GitHub?
699 | │ ├─ YES → ✅ Resolve with "Thread outdated by subsequent commits"
700 | │ └─ NO → ⏭️ Skip (file not modified)
701 | ```
702 |
703 | **Benefits:**
704 |
705 | - ✅ **Zero manual clicks** - Threads resolve automatically when code is fixed
706 | - ✅ **Accurate resolution** - Only resolves when actual code changes match thread location
707 | - ✅ **Audit trail** - Adds comments with commit references for transparency
708 | - ✅ **Safe defaults** - Skips threads when unsure (conservative approach)
709 | - ✅ **Graceful fallback** - Works without GraphQL (just disables auto-resolution)
710 |
711 | **Time Savings:**
712 |
713 | - **Before:** 30 threads × 5 seconds per click = 2.5 minutes of manual clicking
714 | - **After:** `bash scripts/pr/resolve_threads.sh 212 HEAD --auto` = 2 seconds
715 |
716 | **Complete Automated Workflow:**
717 |
718 | ```bash
719 | # 1. Create PR (github-release-manager)
720 | gh pr create --title "feat: new feature" --body "..."
721 |
722 | # 2. Start watch mode with GraphQL tracking
723 | bash scripts/pr/watch_reviews.sh 212 180 &
724 |
725 | # 3. When review arrives, auto-review handles everything:
726 | bash scripts/pr/auto_review.sh 212 5 true
727 | # - Fetches review feedback
728 | # - Categorizes issues
729 | # - Generates fixes
730 | # - Applies and commits
731 | # - Pushes to PR branch
732 | # - **Auto-resolves threads** ← NEW!
733 | # - Triggers new review
734 | # - Repeats until approved
735 |
736 | # 4. Merge when approved (github-release-manager)
737 | gh pr merge 212 --squash
738 | ```
739 |
740 | **Documentation:**
741 |
742 | See `docs/pr-graphql-integration.md` for:
743 | - Complete API reference
744 | - Troubleshooting guide
745 | - GraphQL query examples
746 | - Advanced usage patterns
747 | - Performance considerations
748 |
749 | ## Integration with github-release-manager
750 |
751 | This agent **extends** the github-release-manager workflow:
752 |
753 | **github-release-manager handles**:
754 | - Version bumping
755 | - CHANGELOG/README updates
756 | - PR creation
757 | - Issue tracking
758 | - Post-release actions
759 |
760 | **gemini-pr-automator adds**:
761 | - Automated review iteration
762 | - Fix application
763 | - Test generation
764 | - Quality gates
765 | - Breaking change detection
766 |
767 | **Combined Workflow**:
768 | 1. `github-release-manager` creates release PR
769 | 2. `gemini-pr-automator` runs quality gate
770 | 3. `gemini-pr-automator` triggers automated review loop
771 | 4. `github-release-manager` merges when approved
772 | 5. `github-release-manager` handles post-release tasks
773 |
774 | ## Project-Specific Patterns
775 |
776 | ### MCP Memory Service PR Standards
777 |
778 | **Required Checks**:
779 | - ✅ All tests pass (`pytest tests/`)
780 | - ✅ No security vulnerabilities
781 | - ✅ Code complexity ≤7 for new functions
782 | - ✅ Type hints on all new functions
783 | - ✅ Breaking changes documented in CHANGELOG
784 |
785 | **Review Focus Areas**:
786 | - Storage backend modifications (critical path)
787 | - MCP tool schema changes (protocol compliance)
788 | - Web API endpoints (security implications)
789 | - Hook system changes (user-facing)
790 | - Performance-critical code (5ms target)
791 |
792 | ### Gemini Review Iteration Pattern
793 |
794 | **Iteration 1**: Initial review (broad feedback)
795 | **Iteration 2**: Apply safe fixes, re-review specific areas
796 | **Iteration 3**: Address remaining issues, focus on edge cases
797 | **Iteration 4**: Final polish, documentation review
798 | **Iteration 5**: Approval or escalate to manual review
799 |
800 | ## Usage Examples
801 |
802 | ### Quick Automated Review
803 |
804 | ```bash
805 | # Standard automated review (5 iterations, safe fixes enabled)
806 | bash scripts/pr/auto_review.sh 123
807 |
808 | # Conservative mode (3 iterations, manual fixes)
809 | bash scripts/pr/auto_review.sh 123 3 false
810 |
811 | # Aggressive mode (10 iterations, auto-fix everything)
812 | bash scripts/pr/auto_review.sh 123 10 true
813 | ```
814 |
815 | ### Generate Tests Only
816 |
817 | ```bash
818 | # Generate tests for PR #123
819 | bash scripts/pr/generate_tests.sh 123
820 |
821 | # Review generated tests
822 | ls -la /tmp/test_gen_*.py
823 | ```
824 |
825 | ### Breaking Change Check
826 |
827 | ```bash
828 | # Check if PR introduces breaking changes
829 | bash scripts/pr/detect_breaking_changes.sh main feature/new-api
830 |
831 | # Exit code 0 = no breaking changes
832 | # Exit code 1 = breaking changes detected
833 | ```
834 |
835 | ## Best Practices
836 |
837 | 1. **Always run quality gate first**: Catch issues before review iteration
838 | 2. **Start with safe-fix mode off**: Observe behavior before trusting automation
839 | 3. **Review auto-applied commits**: Ensure changes make sense before merging
840 | 4. **Limit iterations**: Prevent infinite loops, escalate to humans at max
841 | 5. **Document breaking changes**: Always update CHANGELOG for API changes
842 | 6. **Test generated tests**: Verify generated tests actually work before committing
843 |
844 | ## Limitations
845 |
846 | - **Context limitations**: Gemini has context limits, very large PRs may need manual review
847 | - **Fix quality**: Auto-generated fixes may not always be optimal (human review recommended)
848 | - **False negatives**: Breaking change detection may miss subtle breaking changes
849 | - **API rate limits**: Gemini CLI subject to rate limits, add delays between iterations
850 | - **Complexity**: Multi-file refactoring with complex dependencies needs manual oversight
851 |
852 | ## Performance Considerations
853 |
854 | - Single review iteration: ~90-120 seconds (Gemini response time)
855 | - Full automated cycle (5 iterations): ~7-10 minutes
856 | - Test generation per file: ~30-60 seconds
857 | - Breaking change detection: ~15-30 seconds
858 |
859 | **Time Savings**: ~10-30 minutes saved per PR vs manual iteration
860 |
861 | ---
862 |
863 | **Quick Reference Card**:
864 |
865 | ```bash
866 | # Full automated review
867 | bash scripts/pr/full_auto_review.sh <PR_NUMBER>
868 |
869 | # Quality gate only
870 | bash scripts/pr/quality_gate.sh <PR_NUMBER>
871 |
872 | # Generate tests
873 | bash scripts/pr/generate_tests.sh <PR_NUMBER>
874 |
875 | # Breaking changes
876 | bash scripts/pr/detect_breaking_changes.sh main <BRANCH>
877 |
878 | # Auto-review with options
879 | bash scripts/pr/auto_review.sh <PR_NUMBER> <MAX_ITER> <SAFE_FIX:true/false>
880 | ```
881 |
```
--------------------------------------------------------------------------------
/scripts/migration/migrate_v5_enhanced.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # Copyright 2024 Heinrich Krupp
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """
17 | Enhanced ChromaDB to SQLite-vec Migration Script for v5.0.0+
18 |
19 | This script provides a robust migration path from ChromaDB to SQLite-vec with:
20 | - Custom data path support
21 | - Proper content hash generation
22 | - Tag format validation and correction
23 | - Progress indicators
24 | - Transaction-based migration with rollback
25 | - Dry-run mode for testing
26 | - Comprehensive error handling
27 | """
28 |
29 | import argparse
30 | import asyncio
31 | import hashlib
32 | import json
33 | import logging
34 | import os
35 | import sqlite3
36 | import sys
37 | import tempfile
38 | import time
39 | from datetime import datetime
40 | from pathlib import Path
41 | from typing import List, Dict, Any, Optional, Union, Tuple
42 |
43 | # Try importing with progress bar support
44 | try:
45 | from tqdm import tqdm
46 | TQDM_AVAILABLE = True
47 | except ImportError:
48 | TQDM_AVAILABLE = False
49 | print("Note: Install 'tqdm' for progress bars: pip install tqdm")
50 |
51 | # Add project root to path
52 | project_root = Path(__file__).parent.parent
53 | sys.path.insert(0, str(project_root / "src"))
54 |
55 | # Import storage modules
56 | try:
57 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage
58 | from mcp_memory_service.storage.sqlite_vec import SqliteVecMemoryStorage
59 | from mcp_memory_service.models.memory import Memory
60 | from mcp_memory_service.utils.hashing import generate_content_hash
61 | except ImportError as e:
62 | print(f"Error importing MCP modules: {e}")
63 | print("Make sure you're running this from the MCP Memory Service directory")
64 | sys.exit(1)
65 |
66 | # Setup logging
67 | logging.basicConfig(
68 | level=logging.INFO,
69 | format='%(asctime)s - %(levelname)s - %(message)s'
70 | )
71 | logger = logging.getLogger(__name__)
72 |
73 |
74 | class MigrationConfig:
75 | """Configuration for migration process."""
76 |
77 | def __init__(self):
78 | self.chroma_path: Optional[str] = None
79 | self.sqlite_path: Optional[str] = None
80 | self.batch_size: int = 50
81 | self.dry_run: bool = False
82 | self.skip_duplicates: bool = True
83 | self.backup_path: Optional[str] = None
84 | self.verbose: bool = False
85 | self.validate_only: bool = False
86 | self.force: bool = False
87 |
88 | @classmethod
89 | def from_args(cls, args) -> 'MigrationConfig':
90 | """Create config from command line arguments."""
91 | config = cls()
92 | config.chroma_path = args.chroma_path
93 | config.sqlite_path = args.sqlite_path
94 | config.batch_size = args.batch_size
95 | config.dry_run = args.dry_run
96 | config.skip_duplicates = not args.no_skip_duplicates
97 | config.backup_path = args.backup
98 | config.verbose = args.verbose
99 | config.validate_only = args.validate_only
100 | config.force = args.force
101 | return config
102 |
103 | def resolve_paths(self):
104 | """Resolve and validate data paths."""
105 | # Resolve ChromaDB path
106 | if not self.chroma_path:
107 | # Check environment variable first
108 | self.chroma_path = os.environ.get('MCP_MEMORY_CHROMA_PATH')
109 |
110 | if not self.chroma_path:
111 | # Use default locations based on platform
112 | home = Path.home()
113 | if sys.platform == 'darwin': # macOS
114 | default_base = home / 'Library' / 'Application Support' / 'mcp-memory'
115 | elif sys.platform == 'win32': # Windows
116 | default_base = Path(os.getenv('LOCALAPPDATA', '')) / 'mcp-memory'
117 | else: # Linux
118 | default_base = home / '.local' / 'share' / 'mcp-memory'
119 |
120 | # Try multiple possible locations
121 | possible_paths = [
122 | home / '.mcp_memory_chroma', # Legacy location
123 | default_base / 'chroma_db', # New standard location
124 | Path.cwd() / 'chroma_db', # Current directory
125 | ]
126 |
127 | for path in possible_paths:
128 | if path.exists():
129 | self.chroma_path = str(path)
130 | logger.info(f"Found ChromaDB at: {path}")
131 | break
132 |
133 | if not self.chroma_path:
134 | raise ValueError(
135 | "Could not find ChromaDB data. Please specify --chroma-path or "
136 | "set MCP_MEMORY_CHROMA_PATH environment variable"
137 | )
138 |
139 | # Resolve SQLite path
140 | if not self.sqlite_path:
141 | # Check environment variable first
142 | self.sqlite_path = os.environ.get('MCP_MEMORY_SQLITE_PATH')
143 |
144 | if not self.sqlite_path:
145 | # Default to same directory as ChromaDB with different name
146 | chroma_dir = Path(self.chroma_path).parent
147 | self.sqlite_path = str(chroma_dir / 'sqlite_vec.db')
148 | logger.info(f"Using default SQLite path: {self.sqlite_path}")
149 |
150 | # Resolve backup path if needed
151 | if self.backup_path is None and not self.dry_run and not self.validate_only:
152 | backup_dir = Path(self.sqlite_path).parent / 'backups'
153 | backup_dir.mkdir(exist_ok=True)
154 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
155 | self.backup_path = str(backup_dir / f'migration_backup_{timestamp}.json')
156 |
157 |
158 | class EnhancedMigrationTool:
159 | """Enhanced migration tool with proper error handling and progress tracking."""
160 |
161 | def __init__(self, config: MigrationConfig):
162 | self.config = config
163 | self.stats = {
164 | 'total': 0,
165 | 'migrated': 0,
166 | 'skipped': 0,
167 | 'failed': 0,
168 | 'errors': []
169 | }
170 | self.chroma_storage = None
171 | self.sqlite_storage = None
172 |
173 | def generate_proper_content_hash(self, content: str) -> str:
174 | """Generate a proper SHA256 content hash."""
175 | return hashlib.sha256(content.encode()).hexdigest()
176 |
177 | def validate_and_fix_tags(self, tags: Any) -> List[str]:
178 | """Validate and fix tag format."""
179 | if not tags:
180 | return []
181 |
182 | if isinstance(tags, str):
183 | # Handle comma-separated string
184 | if ',' in tags:
185 | return [tag.strip() for tag in tags.split(',') if tag.strip()]
186 | # Handle single tag
187 | return [tags.strip()] if tags.strip() else []
188 |
189 | if isinstance(tags, list):
190 | # Clean and validate list tags
191 | clean_tags = []
192 | for tag in tags:
193 | if isinstance(tag, str) and tag.strip():
194 | clean_tags.append(tag.strip())
195 | return clean_tags
196 |
197 | # Unknown format - log warning and return empty
198 | logger.warning(f"Unknown tag format: {type(tags)} - {tags}")
199 | return []
200 |
201 | def safe_timestamp_convert(self, timestamp: Any) -> float:
202 | """Safely convert various timestamp formats to float."""
203 | if timestamp is None:
204 | return datetime.now().timestamp()
205 |
206 | # Handle numeric timestamps
207 | if isinstance(timestamp, (int, float)):
208 | # Check if timestamp is reasonable (between 2000 and 2100)
209 | if 946684800 <= float(timestamp) <= 4102444800:
210 | return float(timestamp)
211 | else:
212 | logger.warning(f"Timestamp {timestamp} out of reasonable range, using current time")
213 | return datetime.now().timestamp()
214 |
215 | # Handle string timestamps
216 | if isinstance(timestamp, str):
217 | # Try ISO format
218 | for fmt in [
219 | '%Y-%m-%dT%H:%M:%S.%f',
220 | '%Y-%m-%dT%H:%M:%S',
221 | '%Y-%m-%d %H:%M:%S.%f',
222 | '%Y-%m-%d %H:%M:%S',
223 | ]:
224 | try:
225 | dt = datetime.strptime(timestamp.rstrip('Z'), fmt)
226 | return dt.timestamp()
227 | except ValueError:
228 | continue
229 |
230 | # Try parsing as float string
231 | try:
232 | ts = float(timestamp)
233 | if 946684800 <= ts <= 4102444800:
234 | return ts
235 | except ValueError:
236 | pass
237 |
238 | # Fallback to current time
239 | logger.warning(f"Could not parse timestamp '{timestamp}', using current time")
240 | return datetime.now().timestamp()
241 |
242 | async def extract_memories_from_chroma(self) -> List[Dict[str, Any]]:
243 | """Extract all memories from ChromaDB with proper error handling."""
244 | memories = []
245 |
246 | try:
247 | # Initialize ChromaDB storage
248 | logger.info("Connecting to ChromaDB...")
249 | self.chroma_storage = ChromaMemoryStorage(path=self.config.chroma_path)
250 |
251 | # Access the collection directly
252 | collection = self.chroma_storage.collection
253 | if not collection:
254 | raise ValueError("ChromaDB collection not initialized")
255 |
256 | # Get all data from collection
257 | logger.info("Extracting memories from ChromaDB...")
258 | results = collection.get(include=['documents', 'metadatas', 'embeddings'])
259 |
260 | if not results or not results.get('ids'):
261 | logger.warning("No memories found in ChromaDB")
262 | return memories
263 |
264 | total = len(results['ids'])
265 | logger.info(f"Found {total} memories to process")
266 |
267 | # Process each memory
268 | for i in range(total):
269 | try:
270 | # Extract data with defaults
271 | doc_id = results['ids'][i]
272 | content = results['documents'][i] if i < len(results.get('documents', [])) else ""
273 | metadata = results['metadatas'][i] if i < len(results.get('metadatas', [])) else {}
274 | embedding = results['embeddings'][i] if i < len(results.get('embeddings', [])) else None
275 |
276 | if not content:
277 | logger.warning(f"Skipping memory {doc_id}: empty content")
278 | continue
279 |
280 | # Generate proper content hash
281 | content_hash = self.generate_proper_content_hash(content)
282 |
283 | # Extract and validate tags
284 | raw_tags = metadata.get('tags', metadata.get('tags_str', []))
285 | tags = self.validate_and_fix_tags(raw_tags)
286 |
287 | # Extract timestamps
288 | created_at = self.safe_timestamp_convert(
289 | metadata.get('created_at', metadata.get('timestamp'))
290 | )
291 | updated_at = self.safe_timestamp_convert(
292 | metadata.get('updated_at', created_at)
293 | )
294 |
295 | # Extract memory type
296 | memory_type = metadata.get('memory_type', metadata.get('type', 'imported'))
297 |
298 | # Clean metadata (remove special fields)
299 | clean_metadata = {}
300 | exclude_keys = {
301 | 'tags', 'tags_str', 'created_at', 'updated_at',
302 | 'timestamp', 'timestamp_float', 'timestamp_str',
303 | 'memory_type', 'type', 'content_hash',
304 | 'created_at_iso', 'updated_at_iso'
305 | }
306 |
307 | for key, value in metadata.items():
308 | if key not in exclude_keys and value is not None:
309 | clean_metadata[key] = value
310 |
311 | # Create memory record
312 | memory_data = {
313 | 'content': content,
314 | 'content_hash': content_hash,
315 | 'tags': tags,
316 | 'memory_type': memory_type,
317 | 'metadata': clean_metadata,
318 | 'embedding': embedding,
319 | 'created_at': created_at,
320 | 'updated_at': updated_at,
321 | 'original_id': doc_id # Keep for reference
322 | }
323 |
324 | memories.append(memory_data)
325 |
326 | if self.config.verbose and (i + 1) % 100 == 0:
327 | logger.info(f"Processed {i + 1}/{total} memories")
328 |
329 | except Exception as e:
330 | logger.error(f"Failed to extract memory {i}: {e}")
331 | self.stats['errors'].append(f"Extract error at index {i}: {str(e)}")
332 | continue
333 |
334 | logger.info(f"Successfully extracted {len(memories)} memories")
335 | return memories
336 |
337 | except Exception as e:
338 | logger.error(f"Critical error extracting from ChromaDB: {e}")
339 | raise
340 |
341 | async def migrate_to_sqlite(self, memories: List[Dict[str, Any]]) -> bool:
342 | """Migrate memories to SQLite-vec with transaction support."""
343 | if not memories:
344 | logger.warning("No memories to migrate")
345 | return True
346 |
347 | try:
348 | # Initialize SQLite-vec storage
349 | logger.info(f"Initializing SQLite-vec at {self.config.sqlite_path}")
350 | self.sqlite_storage = SqliteVecMemoryStorage(
351 | db_path=self.config.sqlite_path,
352 | embedding_model=os.environ.get('MCP_MEMORY_EMBEDDING_MODEL', 'all-MiniLM-L6-v2')
353 | )
354 | await self.sqlite_storage.initialize()
355 |
356 | # Start transaction
357 | conn = self.sqlite_storage.conn
358 | conn.execute("BEGIN TRANSACTION")
359 |
360 | try:
361 | # Migrate memories in batches
362 | total = len(memories)
363 | batch_size = self.config.batch_size
364 |
365 | # Use progress bar if available
366 | if TQDM_AVAILABLE and not self.config.dry_run:
367 | progress_bar = tqdm(total=total, desc="Migrating memories")
368 | else:
369 | progress_bar = None
370 |
371 | for i in range(0, total, batch_size):
372 | batch = memories[i:i + batch_size]
373 |
374 | if not self.config.dry_run:
375 | for memory_data in batch:
376 | try:
377 | # Check for duplicates
378 | if self.config.skip_duplicates:
379 | existing = conn.execute(
380 | "SELECT 1 FROM memories WHERE content_hash = ? LIMIT 1",
381 | (memory_data['content_hash'],)
382 | ).fetchone()
383 |
384 | if existing:
385 | self.stats['skipped'] += 1
386 | if progress_bar:
387 | progress_bar.update(1)
388 | continue
389 |
390 | # Create Memory object
391 | memory = Memory(
392 | content=memory_data['content'],
393 | content_hash=memory_data['content_hash'],
394 | tags=memory_data['tags'],
395 | memory_type=memory_data.get('memory_type'),
396 | metadata=memory_data.get('metadata', {}),
397 | created_at=memory_data['created_at'],
398 | updated_at=memory_data['updated_at']
399 | )
400 |
401 | # Store memory
402 | success, message = await self.sqlite_storage.store(memory)
403 |
404 | if success:
405 | self.stats['migrated'] += 1
406 | else:
407 | raise Exception(f"Failed to store: {message}")
408 |
409 | if progress_bar:
410 | progress_bar.update(1)
411 |
412 | except Exception as e:
413 | self.stats['failed'] += 1
414 | self.stats['errors'].append(
415 | f"Migration error for {memory_data['content_hash'][:8]}: {str(e)}"
416 | )
417 | if progress_bar:
418 | progress_bar.update(1)
419 |
420 | else:
421 | # Dry run - just count
422 | self.stats['migrated'] += len(batch)
423 | if progress_bar:
424 | progress_bar.update(len(batch))
425 |
426 | if progress_bar:
427 | progress_bar.close()
428 |
429 | # Commit transaction
430 | if not self.config.dry_run:
431 | conn.execute("COMMIT")
432 | logger.info("Transaction committed successfully")
433 | else:
434 | conn.execute("ROLLBACK")
435 | logger.info("Dry run - transaction rolled back")
436 |
437 | return True
438 |
439 | except Exception as e:
440 | # Rollback on error
441 | conn.execute("ROLLBACK")
442 | logger.error(f"Migration failed, transaction rolled back: {e}")
443 | raise
444 |
445 | except Exception as e:
446 | logger.error(f"Critical error during migration: {e}")
447 | return False
448 |
449 | finally:
450 | # Clean up
451 | if self.sqlite_storage:
452 | self.sqlite_storage.close()
453 |
454 | async def create_backup(self, memories: List[Dict[str, Any]]):
455 | """Create a JSON backup of memories."""
456 | if not self.config.backup_path or self.config.dry_run:
457 | return
458 |
459 | logger.info(f"Creating backup at {self.config.backup_path}")
460 |
461 | backup_data = {
462 | 'version': '2.0',
463 | 'created_at': datetime.now().isoformat(),
464 | 'source': self.config.chroma_path,
465 | 'total_memories': len(memories),
466 | 'memories': memories
467 | }
468 |
469 | # Remove embeddings from backup to reduce size
470 | for memory in backup_data['memories']:
471 | memory.pop('embedding', None)
472 |
473 | with open(self.config.backup_path, 'w') as f:
474 | json.dump(backup_data, f, indent=2, default=str)
475 |
476 | logger.info(f"Backup created: {self.config.backup_path}")
477 |
478 | async def validate_migration(self) -> bool:
479 | """Validate the migrated data."""
480 | logger.info("Validating migration...")
481 |
482 | try:
483 | # Connect to SQLite database
484 | conn = sqlite3.connect(self.config.sqlite_path)
485 |
486 | # Check memory count
487 | count = conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
488 | logger.info(f"SQLite database contains {count} memories")
489 |
490 | # Check for required fields
491 | sample = conn.execute("""
492 | SELECT content_hash, content, tags, created_at
493 | FROM memories
494 | LIMIT 10
495 | """).fetchall()
496 |
497 | issues = []
498 | for row in sample:
499 | if not row[0]: # content_hash
500 | issues.append("Missing content_hash")
501 | if not row[1]: # content
502 | issues.append("Missing content")
503 | if row[3] is None: # created_at
504 | issues.append("Missing created_at")
505 |
506 | conn.close()
507 |
508 | if issues:
509 | logger.warning(f"Validation issues found: {', '.join(set(issues))}")
510 | return False
511 |
512 | logger.info("Validation passed!")
513 | return True
514 |
515 | except Exception as e:
516 | logger.error(f"Validation failed: {e}")
517 | return False
518 |
519 | async def run(self) -> bool:
520 | """Run the migration process."""
521 | try:
522 | # Resolve paths
523 | self.config.resolve_paths()
524 |
525 | # Print configuration
526 | print("\n" + "="*60)
527 | print("MCP Memory Service - Enhanced Migration Tool v2.0")
528 | print("="*60)
529 | print(f"ChromaDB source: {self.config.chroma_path}")
530 | print(f"SQLite-vec target: {self.config.sqlite_path}")
531 | if self.config.backup_path:
532 | print(f"Backup location: {self.config.backup_path}")
533 | print(f"Mode: {'DRY RUN' if self.config.dry_run else 'LIVE MIGRATION'}")
534 | print(f"Batch size: {self.config.batch_size}")
535 | print(f"Skip duplicates: {self.config.skip_duplicates}")
536 | print()
537 |
538 | # Check if validation only
539 | if self.config.validate_only:
540 | return await self.validate_migration()
541 |
542 | # Check if target exists
543 | if Path(self.config.sqlite_path).exists() and not self.config.force:
544 | if not self.config.dry_run:
545 | response = input(f"Target database exists. Overwrite? (y/N): ")
546 | if response.lower() != 'y':
547 | print("Migration cancelled")
548 | return False
549 |
550 | # Extract memories from ChromaDB
551 | memories = await self.extract_memories_from_chroma()
552 | self.stats['total'] = len(memories)
553 |
554 | if not memories:
555 | print("No memories found to migrate")
556 | return True
557 |
558 | # Create backup
559 | if self.config.backup_path and not self.config.dry_run:
560 | await self.create_backup(memories)
561 |
562 | # Confirm migration
563 | if not self.config.dry_run and not self.config.force:
564 | print(f"\nAbout to migrate {len(memories)} memories")
565 | response = input("Proceed? (y/N): ")
566 | if response.lower() != 'y':
567 | print("Migration cancelled")
568 | return False
569 |
570 | # Perform migration
571 | success = await self.migrate_to_sqlite(memories)
572 |
573 | # Print summary
574 | print("\n" + "="*60)
575 | print("MIGRATION SUMMARY")
576 | print("="*60)
577 | print(f"Total memories found: {self.stats['total']}")
578 | print(f"Successfully migrated: {self.stats['migrated']}")
579 | print(f"Duplicates skipped: {self.stats['skipped']}")
580 | print(f"Failed migrations: {self.stats['failed']}")
581 |
582 | if self.stats['errors'] and self.config.verbose:
583 | print("\nErrors encountered:")
584 | for i, error in enumerate(self.stats['errors'][:10], 1):
585 | print(f" {i}. {error}")
586 | if len(self.stats['errors']) > 10:
587 | print(f" ... and {len(self.stats['errors']) - 10} more")
588 |
589 | if success and not self.config.dry_run:
590 | # Validate migration
591 | if await self.validate_migration():
592 | print("\n✅ Migration completed successfully!")
593 | print("\nNext steps:")
594 | print("1. Set environment variable:")
595 | print(" export MCP_MEMORY_STORAGE_BACKEND=sqlite_vec")
596 | print(f"2. Set database path:")
597 | print(f" export MCP_MEMORY_SQLITE_PATH={self.config.sqlite_path}")
598 | print("3. Restart MCP Memory Service")
599 | print("4. Test that your memories are accessible")
600 | else:
601 | print("\n⚠️ Migration completed with validation warnings")
602 |
603 | return success
604 |
605 | except Exception as e:
606 | logger.error(f"Migration failed: {e}")
607 | if self.config.verbose:
608 | import traceback
609 | traceback.print_exc()
610 | return False
611 |
612 |
613 | def main():
614 | """Main entry point."""
615 | parser = argparse.ArgumentParser(
616 | description="Enhanced ChromaDB to SQLite-vec migration tool",
617 | formatter_class=argparse.RawDescriptionHelpFormatter
618 | )
619 |
620 | parser.add_argument(
621 | '--chroma-path',
622 | help='Path to ChromaDB data directory (default: auto-detect)'
623 | )
624 | parser.add_argument(
625 | '--sqlite-path',
626 | help='Path for SQLite-vec database (default: same dir as ChromaDB)'
627 | )
628 | parser.add_argument(
629 | '--batch-size',
630 | type=int,
631 | default=50,
632 | help='Number of memories to migrate per batch (default: 50)'
633 | )
634 | parser.add_argument(
635 | '--dry-run',
636 | action='store_true',
637 | help='Simulate migration without making changes'
638 | )
639 | parser.add_argument(
640 | '--no-skip-duplicates',
641 | action='store_true',
642 | help='Migrate all memories including duplicates'
643 | )
644 | parser.add_argument(
645 | '--backup',
646 | help='Path for JSON backup file (default: auto-generate)'
647 | )
648 | parser.add_argument(
649 | '--verbose',
650 | action='store_true',
651 | help='Enable verbose logging'
652 | )
653 | parser.add_argument(
654 | '--validate-only',
655 | action='store_true',
656 | help='Only validate existing SQLite database'
657 | )
658 | parser.add_argument(
659 | '--force',
660 | action='store_true',
661 | help='Skip confirmation prompts'
662 | )
663 |
664 | args = parser.parse_args()
665 |
666 | # Set logging level
667 | if args.verbose:
668 | logging.getLogger().setLevel(logging.DEBUG)
669 |
670 | # Create configuration
671 | config = MigrationConfig.from_args(args)
672 |
673 | # Run migration
674 | tool = EnhancedMigrationTool(config)
675 | success = asyncio.run(tool.run())
676 |
677 | sys.exit(0 if success else 1)
678 |
679 |
680 | if __name__ == "__main__":
681 | main()
```
--------------------------------------------------------------------------------
/tests/unit/test_mdns.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright 2024 Heinrich Krupp
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | Unit tests for mDNS service discovery functionality.
17 | """
18 |
19 | import pytest
20 | import asyncio
21 | import socket
22 | from unittest.mock import Mock, AsyncMock, patch, MagicMock
23 | from zeroconf import ServiceInfo, Zeroconf
24 |
25 | # Import the modules under test
26 | from mcp_memory_service.discovery.mdns_service import (
27 | ServiceAdvertiser,
28 | ServiceDiscovery,
29 | DiscoveryListener,
30 | ServiceDetails
31 | )
32 | from mcp_memory_service.discovery.client import DiscoveryClient, HealthStatus
33 |
34 |
35 | class TestServiceDetails:
36 | """Test ServiceDetails dataclass."""
37 |
38 | def test_service_details_creation(self):
39 | """Test ServiceDetails creation with basic parameters."""
40 | service_info = Mock()
41 | details = ServiceDetails(
42 | name="Test Service",
43 | host="192.168.1.100",
44 | port=8000,
45 | https=False,
46 | api_version="2.1.0",
47 | requires_auth=True,
48 | service_info=service_info
49 | )
50 |
51 | assert details.name == "Test Service"
52 | assert details.host == "192.168.1.100"
53 | assert details.port == 8000
54 | assert details.https is False
55 | assert details.api_version == "2.1.0"
56 | assert details.requires_auth is True
57 | assert details.service_info == service_info
58 |
59 | def test_service_details_url_http(self):
60 | """Test URL generation for HTTP service."""
61 | details = ServiceDetails(
62 | name="Test Service",
63 | host="192.168.1.100",
64 | port=8000,
65 | https=False,
66 | api_version="2.1.0",
67 | requires_auth=False,
68 | service_info=Mock()
69 | )
70 |
71 | assert details.url == "http://192.168.1.100:8000"
72 | assert details.api_url == "http://192.168.1.100:8000/api"
73 |
74 | def test_service_details_url_https(self):
75 | """Test URL generation for HTTPS service."""
76 | details = ServiceDetails(
77 | name="Test Service",
78 | host="192.168.1.100",
79 | port=8443,
80 | https=True,
81 | api_version="2.1.0",
82 | requires_auth=True,
83 | service_info=Mock()
84 | )
85 |
86 | assert details.url == "https://192.168.1.100:8443"
87 | assert details.api_url == "https://192.168.1.100:8443/api"
88 |
89 |
90 | class TestServiceAdvertiser:
91 | """Test ServiceAdvertiser class."""
92 |
93 | def test_init_default_parameters(self):
94 | """Test ServiceAdvertiser initialization with default parameters."""
95 | advertiser = ServiceAdvertiser()
96 |
97 | assert advertiser.service_name == "MCP Memory Service"
98 | assert advertiser.service_type == "_mcp-memory._tcp.local."
99 | assert advertiser.host == "0.0.0.0"
100 | assert advertiser.port == 8000
101 | assert advertiser.https_enabled is False
102 | assert advertiser.api_key_required is False
103 | assert advertiser._registered is False
104 |
105 | def test_init_custom_parameters(self):
106 | """Test ServiceAdvertiser initialization with custom parameters."""
107 | advertiser = ServiceAdvertiser(
108 | service_name="Custom Service",
109 | service_type="_custom._tcp.local.",
110 | host="192.168.1.100",
111 | port=8443,
112 | https_enabled=True,
113 | api_key_required=True
114 | )
115 |
116 | assert advertiser.service_name == "Custom Service"
117 | assert advertiser.service_type == "_custom._tcp.local."
118 | assert advertiser.host == "192.168.1.100"
119 | assert advertiser.port == 8443
120 | assert advertiser.https_enabled is True
121 | assert advertiser.api_key_required is True
122 |
123 | @patch('socket.socket')
124 | def test_get_local_ip(self, mock_socket):
125 | """Test local IP address detection."""
126 | mock_sock_instance = Mock()
127 | mock_sock_instance.getsockname.return_value = ("192.168.1.100", 12345)
128 | mock_socket.return_value.__enter__.return_value = mock_sock_instance
129 |
130 | advertiser = ServiceAdvertiser()
131 | ip = advertiser._get_local_ip()
132 |
133 | assert ip == "192.168.1.100"
134 | mock_sock_instance.connect.assert_called_once_with(("8.8.8.8", 80))
135 |
136 | @patch('socket.socket')
137 | def test_get_local_ip_fallback(self, mock_socket):
138 | """Test local IP address detection fallback."""
139 | mock_socket.side_effect = Exception("Network error")
140 |
141 | advertiser = ServiceAdvertiser()
142 | ip = advertiser._get_local_ip()
143 |
144 | assert ip == "127.0.0.1"
145 |
146 | @patch('socket.inet_aton')
147 | @patch.object(ServiceAdvertiser, '_get_local_ip')
148 | def test_create_service_info(self, mock_get_ip, mock_inet_aton):
149 | """Test ServiceInfo creation."""
150 | mock_get_ip.return_value = "192.168.1.100"
151 | mock_inet_aton.return_value = b'\xc0\xa8\x01\x64' # 192.168.1.100
152 |
153 | advertiser = ServiceAdvertiser(
154 | service_name="Test Service",
155 | https_enabled=True,
156 | api_key_required=True
157 | )
158 |
159 | service_info = advertiser._create_service_info()
160 |
161 | assert service_info.type == "_mcp-memory._tcp.local."
162 | assert service_info.name == "Test Service._mcp-memory._tcp.local."
163 | assert service_info.port == 8000
164 | assert service_info.server == "test-service.local."
165 |
166 | # Check properties
167 | properties = service_info.properties
168 | assert properties[b'https'] == b'True'
169 | assert properties[b'auth_required'] == b'True'
170 | assert properties[b'api_path'] == b'/api'
171 |
172 | @pytest.mark.asyncio
173 | async def test_start_success(self):
174 | """Test successful service advertisement start."""
175 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class:
176 | mock_zeroconf = AsyncMock()
177 | mock_zeroconf_class.return_value = mock_zeroconf
178 |
179 | advertiser = ServiceAdvertiser()
180 |
181 | with patch.object(advertiser, '_create_service_info') as mock_create_info:
182 | mock_service_info = Mock()
183 | mock_create_info.return_value = mock_service_info
184 |
185 | result = await advertiser.start()
186 |
187 | assert result is True
188 | assert advertiser._registered is True
189 | mock_zeroconf.async_register_service.assert_called_once_with(mock_service_info)
190 |
191 | @pytest.mark.asyncio
192 | async def test_start_already_registered(self):
193 | """Test starting advertisement when already registered."""
194 | advertiser = ServiceAdvertiser()
195 | advertiser._registered = True
196 |
197 | result = await advertiser.start()
198 |
199 | assert result is True # Should return True but log warning
200 |
201 | @pytest.mark.asyncio
202 | async def test_start_failure(self):
203 | """Test service advertisement start failure."""
204 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class:
205 | mock_zeroconf = AsyncMock()
206 | mock_zeroconf.async_register_service.side_effect = Exception("Registration failed")
207 | mock_zeroconf_class.return_value = mock_zeroconf
208 |
209 | advertiser = ServiceAdvertiser()
210 |
211 | with patch.object(advertiser, '_create_service_info'):
212 | result = await advertiser.start()
213 |
214 | assert result is False
215 | assert advertiser._registered is False
216 |
217 | @pytest.mark.asyncio
218 | async def test_stop_success(self):
219 | """Test successful service advertisement stop."""
220 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class:
221 | mock_zeroconf = AsyncMock()
222 | mock_zeroconf_class.return_value = mock_zeroconf
223 |
224 | advertiser = ServiceAdvertiser()
225 | advertiser._registered = True
226 | advertiser._zeroconf = mock_zeroconf
227 | advertiser._service_info = Mock()
228 |
229 | await advertiser.stop()
230 |
231 | assert advertiser._registered is False
232 | mock_zeroconf.async_unregister_service.assert_called_once()
233 | mock_zeroconf.async_close.assert_called_once()
234 |
235 | @pytest.mark.asyncio
236 | async def test_stop_not_registered(self):
237 | """Test stopping advertisement when not registered."""
238 | advertiser = ServiceAdvertiser()
239 |
240 | # Should not raise exception
241 | await advertiser.stop()
242 |
243 |
244 | class TestDiscoveryListener:
245 | """Test DiscoveryListener class."""
246 |
247 | def test_init_no_callback(self):
248 | """Test DiscoveryListener initialization without callback."""
249 | listener = DiscoveryListener()
250 |
251 | assert listener.callback is None
252 | assert len(listener.services) == 0
253 |
254 | def test_init_with_callback(self):
255 | """Test DiscoveryListener initialization with callback."""
256 | callback = Mock()
257 | listener = DiscoveryListener(callback)
258 |
259 | assert listener.callback == callback
260 |
261 | @patch('socket.inet_ntoa')
262 | def test_parse_service_info(self, mock_inet_ntoa):
263 | """Test parsing ServiceInfo into ServiceDetails."""
264 | mock_inet_ntoa.return_value = "192.168.1.100"
265 |
266 | # Create mock ServiceInfo
267 | service_info = Mock()
268 | service_info.name = "Test Service._mcp-memory._tcp.local."
269 | service_info.type = "_mcp-memory._tcp.local."
270 | service_info.port = 8000
271 | service_info.addresses = [b'\xc0\xa8\x01\x64'] # 192.168.1.100
272 | service_info.properties = {
273 | b'https': b'true',
274 | b'api_version': b'2.1.0',
275 | b'auth_required': b'false'
276 | }
277 |
278 | listener = DiscoveryListener()
279 | details = listener._parse_service_info(service_info)
280 |
281 | assert details.name == "Test Service"
282 | assert details.host == "192.168.1.100"
283 | assert details.port == 8000
284 | assert details.https is True
285 | assert details.api_version == "2.1.0"
286 | assert details.requires_auth is False
287 |
288 | @patch('socket.inet_ntoa')
289 | def test_parse_service_info_no_addresses(self, mock_inet_ntoa):
290 | """Test parsing ServiceInfo with no addresses."""
291 | service_info = Mock()
292 | service_info.name = "Test Service._mcp-memory._tcp.local."
293 | service_info.type = "_mcp-memory._tcp.local."
294 | service_info.port = 8000
295 | service_info.addresses = []
296 | service_info.properties = {}
297 |
298 | listener = DiscoveryListener()
299 | details = listener._parse_service_info(service_info)
300 |
301 | assert details.host == "localhost"
302 |
303 | def test_add_service_success(self):
304 | """Test successful service addition."""
305 | callback = Mock()
306 | listener = DiscoveryListener(callback)
307 |
308 | # Mock zeroconf and service info
309 | mock_zc = Mock()
310 | mock_service_info = Mock()
311 | mock_zc.get_service_info.return_value = mock_service_info
312 |
313 | with patch.object(listener, '_parse_service_info') as mock_parse:
314 | mock_details = Mock()
315 | mock_details.name = "Test Service"
316 | mock_parse.return_value = mock_details
317 |
318 | listener.add_service(mock_zc, "_mcp-memory._tcp.local.", "test-service")
319 |
320 | assert "test-service" in listener.services
321 | callback.assert_called_once_with(mock_details)
322 |
323 | def test_add_service_no_info(self):
324 | """Test service addition when no service info available."""
325 | listener = DiscoveryListener()
326 |
327 | mock_zc = Mock()
328 | mock_zc.get_service_info.return_value = None
329 |
330 | listener.add_service(mock_zc, "_mcp-memory._tcp.local.", "test-service")
331 |
332 | assert "test-service" not in listener.services
333 |
334 | def test_remove_service(self):
335 | """Test service removal."""
336 | listener = DiscoveryListener()
337 | mock_details = Mock()
338 | mock_details.name = "Test Service"
339 | listener.services["test-service"] = mock_details
340 |
341 | listener.remove_service(Mock(), "_mcp-memory._tcp.local.", "test-service")
342 |
343 | assert "test-service" not in listener.services
344 |
345 |
346 | class TestServiceDiscovery:
347 | """Test ServiceDiscovery class."""
348 |
349 | def test_init_default_parameters(self):
350 | """Test ServiceDiscovery initialization with defaults."""
351 | discovery = ServiceDiscovery()
352 |
353 | assert discovery.service_type == "_mcp-memory._tcp.local."
354 | assert discovery.discovery_timeout == 5
355 | assert discovery._discovering is False
356 |
357 | def test_init_custom_parameters(self):
358 | """Test ServiceDiscovery initialization with custom parameters."""
359 | discovery = ServiceDiscovery(
360 | service_type="_custom._tcp.local.",
361 | discovery_timeout=10
362 | )
363 |
364 | assert discovery.service_type == "_custom._tcp.local."
365 | assert discovery.discovery_timeout == 10
366 |
367 | @pytest.mark.asyncio
368 | async def test_discover_services_success(self):
369 | """Test successful service discovery."""
370 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class, \
371 | patch('mcp_memory_service.discovery.mdns_service.AsyncServiceBrowser') as mock_browser_class:
372 |
373 | mock_zeroconf = AsyncMock()
374 | mock_zeroconf_class.return_value = mock_zeroconf
375 |
376 | mock_browser = Mock()
377 | mock_browser_class.return_value = mock_browser
378 |
379 | discovery = ServiceDiscovery(discovery_timeout=1) # Short timeout for testing
380 |
381 | # Mock discovered services
382 | mock_listener = Mock()
383 | mock_service = Mock()
384 | mock_service.name = "Test Service"
385 | mock_listener.services = {"test-service": mock_service}
386 |
387 | with patch.object(discovery, '_listener', mock_listener):
388 | services = await discovery.discover_services()
389 |
390 | assert len(services) == 1
391 | assert services[0] == mock_service
392 |
393 | @pytest.mark.asyncio
394 | async def test_discover_services_already_discovering(self):
395 | """Test discovery when already in progress."""
396 | discovery = ServiceDiscovery()
397 | discovery._discovering = True
398 |
399 | # Mock existing services
400 | mock_listener = Mock()
401 | mock_service = Mock()
402 | mock_listener.services = {"test-service": mock_service}
403 | discovery._listener = mock_listener
404 |
405 | services = await discovery.discover_services()
406 |
407 | assert len(services) == 1
408 | assert services[0] == mock_service
409 |
410 | @pytest.mark.asyncio
411 | async def test_start_continuous_discovery(self):
412 | """Test starting continuous service discovery."""
413 | callback = Mock()
414 |
415 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class, \
416 | patch('mcp_memory_service.discovery.mdns_service.AsyncServiceBrowser') as mock_browser_class:
417 |
418 | mock_zeroconf = AsyncMock()
419 | mock_zeroconf_class.return_value = mock_zeroconf
420 |
421 | mock_browser = Mock()
422 | mock_browser_class.return_value = mock_browser
423 |
424 | discovery = ServiceDiscovery()
425 |
426 | result = await discovery.start_continuous_discovery(callback)
427 |
428 | assert result is True
429 | assert discovery._discovering is True
430 |
431 | @pytest.mark.asyncio
432 | async def test_start_continuous_discovery_already_started(self):
433 | """Test starting continuous discovery when already started."""
434 | discovery = ServiceDiscovery()
435 | discovery._discovering = True
436 |
437 | result = await discovery.start_continuous_discovery(Mock())
438 |
439 | assert result is False
440 |
441 | @pytest.mark.asyncio
442 | async def test_stop_discovery(self):
443 | """Test stopping service discovery."""
444 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class, \
445 | patch('mcp_memory_service.discovery.mdns_service.AsyncServiceBrowser') as mock_browser_class:
446 |
447 | mock_zeroconf = AsyncMock()
448 | mock_browser = AsyncMock()
449 |
450 | discovery = ServiceDiscovery()
451 | discovery._discovering = True
452 | discovery._zeroconf = mock_zeroconf
453 | discovery._browser = mock_browser
454 |
455 | await discovery.stop_discovery()
456 |
457 | assert discovery._discovering is False
458 | mock_browser.async_cancel.assert_called_once()
459 | mock_zeroconf.async_close.assert_called_once()
460 |
461 | def test_get_discovered_services(self):
462 | """Test getting discovered services."""
463 | discovery = ServiceDiscovery()
464 |
465 | # No listener
466 | services = discovery.get_discovered_services()
467 | assert len(services) == 0
468 |
469 | # With listener
470 | mock_listener = Mock()
471 | mock_service = Mock()
472 | mock_listener.services = {"test": mock_service}
473 | discovery._listener = mock_listener
474 |
475 | services = discovery.get_discovered_services()
476 | assert len(services) == 1
477 | assert services[0] == mock_service
478 |
479 |
480 | class TestDiscoveryClient:
481 | """Test DiscoveryClient class."""
482 |
483 | def test_init_default_timeout(self):
484 | """Test DiscoveryClient initialization with default timeout."""
485 | client = DiscoveryClient()
486 |
487 | assert client.discovery_timeout == 5
488 |
489 | def test_init_custom_timeout(self):
490 | """Test DiscoveryClient initialization with custom timeout."""
491 | client = DiscoveryClient(discovery_timeout=10)
492 |
493 | assert client.discovery_timeout == 10
494 |
495 | @pytest.mark.asyncio
496 | async def test_discover_services(self):
497 | """Test service discovery."""
498 | client = DiscoveryClient()
499 |
500 | mock_service = Mock()
501 | mock_service.name = "Test Service"
502 | mock_service.url = "http://192.168.1.100:8000"
503 | mock_service.requires_auth = False
504 |
505 | with patch.object(client._discovery, 'discover_services', return_value=[mock_service]):
506 | services = await client.discover_services()
507 |
508 | assert len(services) == 1
509 | assert services[0] == mock_service
510 |
511 | @pytest.mark.asyncio
512 | async def test_find_best_service_no_services(self):
513 | """Test finding best service when no services available."""
514 | client = DiscoveryClient()
515 |
516 | with patch.object(client, 'discover_services', return_value=[]):
517 | service = await client.find_best_service()
518 |
519 | assert service is None
520 |
521 | @pytest.mark.asyncio
522 | async def test_find_best_service_with_validation(self):
523 | """Test finding best service with health validation."""
524 | client = DiscoveryClient()
525 |
526 | # Create mock services
527 | http_service = Mock()
528 | http_service.https = False
529 | http_service.requires_auth = False
530 | http_service.port = 8000
531 | http_service.name = "HTTP Service"
532 | http_service.url = "http://192.168.1.100:8000"
533 |
534 | https_service = Mock()
535 | https_service.https = True
536 | https_service.requires_auth = False
537 | https_service.port = 8443
538 | https_service.name = "HTTPS Service"
539 | https_service.url = "https://192.168.1.100:8443"
540 |
541 | with patch.object(client, 'discover_services', return_value=[http_service, https_service]), \
542 | patch.object(client, 'check_service_health') as mock_health:
543 |
544 | # Mock health check results
545 | def health_side_effect(service):
546 | if service.https:
547 | return HealthStatus(
548 | healthy=True, status='ok', backend='sqlite_vec',
549 | statistics={}, response_time_ms=50.0
550 | )
551 | else:
552 | return HealthStatus(
553 | healthy=False, status='error', backend='unknown',
554 | statistics={}, response_time_ms=0, error='Connection failed'
555 | )
556 |
557 | mock_health.side_effect = health_side_effect
558 |
559 | service = await client.find_best_service(prefer_https=True)
560 |
561 | assert service == https_service
562 |
563 | @pytest.mark.asyncio
564 | async def test_check_service_health_success(self):
565 | """Test successful service health check."""
566 | client = DiscoveryClient()
567 |
568 | mock_service = Mock()
569 | mock_service.api_url = "http://192.168.1.100:8000/api"
570 |
571 | mock_response = Mock()
572 | mock_response.status = 200
573 | mock_response.json = AsyncMock(return_value={
574 | 'status': 'healthy',
575 | 'storage_type': 'sqlite_vec',
576 | 'statistics': {'memory_count': 100}
577 | })
578 |
579 | with patch('aiohttp.ClientSession') as mock_session_class:
580 | mock_session = AsyncMock()
581 | mock_session_class.return_value.__aenter__.return_value = mock_session
582 | mock_session.get.return_value.__aenter__.return_value = mock_response
583 |
584 | health = await client.check_service_health(mock_service)
585 |
586 | assert health is not None
587 | assert health.healthy is True
588 | assert health.status == 'healthy'
589 | assert health.backend == 'sqlite_vec'
590 | assert health.statistics == {'memory_count': 100}
591 |
592 | @pytest.mark.asyncio
593 | async def test_check_service_health_failure(self):
594 | """Test service health check failure."""
595 | client = DiscoveryClient()
596 |
597 | mock_service = Mock()
598 | mock_service.api_url = "http://192.168.1.100:8000/api"
599 |
600 | with patch('aiohttp.ClientSession') as mock_session_class:
601 | mock_session_class.side_effect = Exception("Connection failed")
602 |
603 | health = await client.check_service_health(mock_service)
604 |
605 | assert health is not None
606 | assert health.healthy is False
607 | assert health.error == "Connection failed"
608 |
609 | @pytest.mark.asyncio
610 | async def test_find_services_with_health(self):
611 | """Test finding services with health status."""
612 | client = DiscoveryClient()
613 |
614 | # Create mock services
615 | service1 = Mock()
616 | service1.https = True
617 | service1.requires_auth = False
618 |
619 | service2 = Mock()
620 | service2.https = False
621 | service2.requires_auth = False
622 |
623 | health1 = HealthStatus(
624 | healthy=True, status='ok', backend='sqlite_vec',
625 | statistics={}, response_time_ms=50.0
626 | )
627 |
628 | health2 = HealthStatus(
629 | healthy=False, status='error', backend='unknown',
630 | statistics={}, response_time_ms=0, error='Connection failed'
631 | )
632 |
633 | with patch.object(client, 'discover_services', return_value=[service1, service2]), \
634 | patch.object(client, 'check_service_health', side_effect=[health1, health2]):
635 |
636 | services_with_health = await client.find_services_with_health()
637 |
638 | assert len(services_with_health) == 2
639 | # Should be sorted with healthy services first
640 | assert services_with_health[0][1].healthy is True
641 | assert services_with_health[1][1].healthy is False
642 |
643 | @pytest.mark.asyncio
644 | async def test_stop(self):
645 | """Test stopping the discovery client."""
646 | client = DiscoveryClient()
647 |
648 | with patch.object(client._discovery, 'stop_discovery') as mock_stop:
649 | await client.stop()
650 | mock_stop.assert_called_once()
651 |
652 |
653 | class TestHealthStatus:
654 | """Test HealthStatus dataclass."""
655 |
656 | def test_health_status_creation(self):
657 | """Test HealthStatus creation."""
658 | health = HealthStatus(
659 | healthy=True,
660 | status='ok',
661 | backend='sqlite_vec',
662 | statistics={'memory_count': 100},
663 | response_time_ms=50.0,
664 | error=None
665 | )
666 |
667 | assert health.healthy is True
668 | assert health.status == 'ok'
669 | assert health.backend == 'sqlite_vec'
670 | assert health.statistics == {'memory_count': 100}
671 | assert health.response_time_ms == 50.0
672 | assert health.error is None
673 |
674 | def test_health_status_with_error(self):
675 | """Test HealthStatus creation with error."""
676 | health = HealthStatus(
677 | healthy=False,
678 | status='error',
679 | backend='unknown',
680 | statistics={},
681 | response_time_ms=0,
682 | error='Connection timeout'
683 | )
684 |
685 | assert health.healthy is False
686 | assert health.error == 'Connection timeout'
687 |
688 |
689 | # Integration tests that can run without actual network
690 | class TestMDNSIntegration:
691 | """Integration tests for mDNS functionality."""
692 |
693 | @pytest.mark.asyncio
694 | async def test_advertiser_discovery_integration(self):
695 | """Test integration between advertiser and discovery (mocked)."""
696 | # This test uses mocks to simulate the integration without actual network traffic
697 |
698 | with patch('mcp_memory_service.discovery.mdns_service.AsyncZeroconf') as mock_zeroconf_class, \
699 | patch('mcp_memory_service.discovery.mdns_service.AsyncServiceBrowser') as mock_browser_class:
700 |
701 | # Setup mocks
702 | mock_zeroconf = AsyncMock()
703 | mock_zeroconf_class.return_value = mock_zeroconf
704 |
705 | mock_browser = Mock()
706 | mock_browser_class.return_value = mock_browser
707 |
708 | # Start advertiser
709 | advertiser = ServiceAdvertiser(service_name="Test Service")
710 |
711 | with patch.object(advertiser, '_create_service_info'):
712 | await advertiser.start()
713 | assert advertiser._registered is True
714 |
715 | # Start discovery
716 | discovery = ServiceDiscovery(discovery_timeout=1)
717 |
718 | # Mock discovered service
719 | mock_service = ServiceDetails(
720 | name="Test Service",
721 | host="192.168.1.100",
722 | port=8000,
723 | https=False,
724 | api_version="2.1.0",
725 | requires_auth=False,
726 | service_info=Mock()
727 | )
728 |
729 | with patch.object(discovery, '_listener') as mock_listener:
730 | mock_listener.services = {"test-service": mock_service}
731 |
732 | services = await discovery.discover_services()
733 |
734 | assert len(services) == 1
735 | assert services[0].name == "Test Service"
736 |
737 | # Clean up
738 | await advertiser.stop()
739 | await discovery.stop_discovery()
740 |
741 |
742 | if __name__ == '__main__':
743 | pytest.main([__file__])
```
--------------------------------------------------------------------------------
/docs/examples/tag-schema.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "tag_schema": {
3 | "version": "2.0",
4 | "last_updated": "2025-06-07",
5 | "description": "Standardized tag schema for MCP Memory Service knowledge management",
6 | "total_categories": 6,
7 | "naming_convention": {
8 | "format": "lowercase-with-hyphens",
9 | "rules": [
10 | "Use lowercase letters only",
11 | "Replace spaces with hyphens",
12 | "Use descriptive but concise terms",
13 | "Avoid abbreviations unless widely understood",
14 | "Use singular form when possible"
15 | ],
16 | "examples": {
17 | "good": ["memory-service", "github-integration", "best-practices"],
18 | "bad": ["memoryservice", "GitHub_Integration", "bestPractices"]
19 | }
20 | },
21 | "categories": {
22 | "projects_and_repositories": {
23 | "description": "Primary projects, repositories, and major components",
24 | "color": "#3b82f6",
25 | "icon": "🚀",
26 | "tags": {
27 | "primary_projects": [
28 | {
29 | "tag": "mcp-memory-service",
30 | "description": "Core memory service development",
31 | "usage_count": 45,
32 | "examples": ["Core development", "Memory storage features", "Database operations"]
33 | },
34 | {
35 | "tag": "memory-dashboard",
36 | "description": "Dashboard application for memory management",
37 | "usage_count": 23,
38 | "examples": ["UI development", "Dashboard features", "User interface"]
39 | },
40 | {
41 | "tag": "github-integration",
42 | "description": "GitHub connectivity and automation",
43 | "usage_count": 15,
44 | "examples": ["Issue tracking", "Repository management", "CI/CD"]
45 | },
46 | {
47 | "tag": "mcp-protocol",
48 | "description": "Model Context Protocol development",
49 | "usage_count": 12,
50 | "examples": ["Protocol specifications", "Communication standards"]
51 | },
52 | {
53 | "tag": "cloudflare-workers",
54 | "description": "Edge computing integration",
55 | "usage_count": 8,
56 | "examples": ["Edge deployment", "Worker scripts", "Distributed systems"]
57 | }
58 | ],
59 | "project_components": [
60 | {
61 | "tag": "frontend",
62 | "description": "User interface components and client-side development",
63 | "usage_count": 18,
64 | "examples": ["React components", "UI/UX", "Client applications"]
65 | },
66 | {
67 | "tag": "backend",
68 | "description": "Server-side development and APIs",
69 | "usage_count": 32,
70 | "examples": ["Server logic", "Database operations", "API development"]
71 | },
72 | {
73 | "tag": "api",
74 | "description": "API design and implementation",
75 | "usage_count": 14,
76 | "examples": ["REST APIs", "Endpoints", "API documentation"]
77 | },
78 | {
79 | "tag": "database",
80 | "description": "Data storage and management",
81 | "usage_count": 22,
82 | "examples": ["ChromaDB", "Data models", "Database optimization"]
83 | },
84 | {
85 | "tag": "infrastructure",
86 | "description": "Deployment and DevOps",
87 | "usage_count": 16,
88 | "examples": ["Docker", "Cloud deployment", "System architecture"]
89 | }
90 | ]
91 | }
92 | },
93 | "technologies_and_tools": {
94 | "description": "Programming languages, frameworks, libraries, and development tools",
95 | "color": "#10b981",
96 | "icon": "⚙️",
97 | "tags": {
98 | "programming_languages": [
99 | {
100 | "tag": "python",
101 | "description": "Python development and scripts",
102 | "usage_count": 38,
103 | "examples": ["Backend development", "Scripts", "Data processing"]
104 | },
105 | {
106 | "tag": "typescript",
107 | "description": "TypeScript development",
108 | "usage_count": 25,
109 | "examples": ["Frontend development", "Type safety", "React applications"]
110 | },
111 | {
112 | "tag": "javascript",
113 | "description": "JavaScript development",
114 | "usage_count": 20,
115 | "examples": ["Client-side logic", "Node.js", "Web development"]
116 | },
117 | {
118 | "tag": "bash",
119 | "description": "Shell scripting and command-line operations",
120 | "usage_count": 12,
121 | "examples": ["Automation scripts", "System administration", "Build processes"]
122 | },
123 | {
124 | "tag": "sql",
125 | "description": "Database queries and operations",
126 | "usage_count": 8,
127 | "examples": ["Database queries", "Data analysis", "Schema design"]
128 | }
129 | ],
130 | "frameworks_and_libraries": [
131 | {
132 | "tag": "react",
133 | "description": "React framework development",
134 | "usage_count": 22,
135 | "examples": ["Component development", "UI frameworks", "Frontend applications"]
136 | },
137 | {
138 | "tag": "fastapi",
139 | "description": "FastAPI framework for Python APIs",
140 | "usage_count": 15,
141 | "examples": ["API development", "Web services", "Backend frameworks"]
142 | },
143 | {
144 | "tag": "chromadb",
145 | "description": "ChromaDB vector database",
146 | "usage_count": 28,
147 | "examples": ["Vector storage", "Embedding operations", "Similarity search"]
148 | },
149 | {
150 | "tag": "sentence-transformers",
151 | "description": "Sentence transformer models for embeddings",
152 | "usage_count": 18,
153 | "examples": ["Text embeddings", "Semantic search", "NLP models"]
154 | },
155 | {
156 | "tag": "pytest",
157 | "description": "Python testing framework",
158 | "usage_count": 10,
159 | "examples": ["Unit testing", "Test automation", "Quality assurance"]
160 | }
161 | ],
162 | "tools_and_platforms": [
163 | {
164 | "tag": "git",
165 | "description": "Version control and repository management",
166 | "usage_count": 24,
167 | "examples": ["Version control", "Collaboration", "Code management"]
168 | },
169 | {
170 | "tag": "docker",
171 | "description": "Containerization and deployment",
172 | "usage_count": 16,
173 | "examples": ["Container deployment", "Application packaging", "DevOps"]
174 | },
175 | {
176 | "tag": "github",
177 | "description": "GitHub platform and repository management",
178 | "usage_count": 20,
179 | "examples": ["Repository hosting", "Issue tracking", "Collaboration"]
180 | },
181 | {
182 | "tag": "aws",
183 | "description": "Amazon Web Services cloud platform",
184 | "usage_count": 12,
185 | "examples": ["Cloud infrastructure", "Deployment", "Scalability"]
186 | },
187 | {
188 | "tag": "npm",
189 | "description": "Node package management",
190 | "usage_count": 8,
191 | "examples": ["Package management", "Dependencies", "JavaScript ecosystem"]
192 | }
193 | ]
194 | }
195 | },
196 | "activities_and_processes": {
197 | "description": "Development activities, operational processes, and workflows",
198 | "color": "#f59e0b",
199 | "icon": "🔧",
200 | "tags": {
201 | "development_activities": [
202 | {
203 | "tag": "development",
204 | "description": "General development work and programming",
205 | "usage_count": 35,
206 | "examples": ["Feature development", "Code writing", "Implementation"]
207 | },
208 | {
209 | "tag": "implementation",
210 | "description": "Feature implementation and code realization",
211 | "usage_count": 28,
212 | "examples": ["Feature implementation", "Code realization", "System building"]
213 | },
214 | {
215 | "tag": "debugging",
216 | "description": "Bug investigation and problem solving",
217 | "usage_count": 22,
218 | "examples": ["Bug fixes", "Problem investigation", "Issue resolution"]
219 | },
220 | {
221 | "tag": "testing",
222 | "description": "Quality assurance and testing activities",
223 | "usage_count": 30,
224 | "examples": ["Unit testing", "Integration testing", "Quality assurance"]
225 | },
226 | {
227 | "tag": "refactoring",
228 | "description": "Code improvement and restructuring",
229 | "usage_count": 12,
230 | "examples": ["Code cleanup", "Architecture improvement", "Optimization"]
231 | },
232 | {
233 | "tag": "optimization",
234 | "description": "Performance enhancement and efficiency improvements",
235 | "usage_count": 15,
236 | "examples": ["Performance tuning", "Resource optimization", "Speed improvements"]
237 | }
238 | ],
239 | "documentation_activities": [
240 | {
241 | "tag": "documentation",
242 | "description": "Writing documentation and guides",
243 | "usage_count": 25,
244 | "examples": ["Technical documentation", "User guides", "API documentation"]
245 | },
246 | {
247 | "tag": "tutorial",
248 | "description": "Creating tutorials and learning materials",
249 | "usage_count": 8,
250 | "examples": ["Step-by-step guides", "Learning materials", "How-to documents"]
251 | },
252 | {
253 | "tag": "guide",
254 | "description": "Comprehensive guides and references",
255 | "usage_count": 12,
256 | "examples": ["Best practice guides", "Implementation guides", "Reference materials"]
257 | },
258 | {
259 | "tag": "reference",
260 | "description": "Reference materials and quick lookups",
261 | "usage_count": 18,
262 | "examples": ["Quick reference", "Lookup tables", "Technical specifications"]
263 | },
264 | {
265 | "tag": "examples",
266 | "description": "Code examples and practical demonstrations",
267 | "usage_count": 15,
268 | "examples": ["Code samples", "Usage examples", "Demonstrations"]
269 | }
270 | ],
271 | "operational_activities": [
272 | {
273 | "tag": "deployment",
274 | "description": "Application deployment and release management",
275 | "usage_count": 18,
276 | "examples": ["Production deployment", "Release management", "Environment setup"]
277 | },
278 | {
279 | "tag": "monitoring",
280 | "description": "System monitoring and observability",
281 | "usage_count": 10,
282 | "examples": ["Performance monitoring", "Health checks", "System observability"]
283 | },
284 | {
285 | "tag": "backup",
286 | "description": "Data backup and recovery processes",
287 | "usage_count": 8,
288 | "examples": ["Data backup", "Disaster recovery", "Data preservation"]
289 | },
290 | {
291 | "tag": "migration",
292 | "description": "Data or system migration processes",
293 | "usage_count": 12,
294 | "examples": ["Data migration", "System upgrades", "Platform transitions"]
295 | },
296 | {
297 | "tag": "maintenance",
298 | "description": "System maintenance and upkeep",
299 | "usage_count": 15,
300 | "examples": ["Regular maintenance", "System updates", "Preventive care"]
301 | },
302 | {
303 | "tag": "troubleshooting",
304 | "description": "Problem resolution and diagnostic work",
305 | "usage_count": 20,
306 | "examples": ["Issue diagnosis", "Problem solving", "System repair"]
307 | }
308 | ]
309 | }
310 | },
311 | "content_types_and_formats": {
312 | "description": "Types of knowledge content and documentation formats",
313 | "color": "#8b5cf6",
314 | "icon": "📚",
315 | "tags": {
316 | "knowledge_types": [
317 | {
318 | "tag": "concept",
319 | "description": "Conceptual information and theoretical content",
320 | "usage_count": 18,
321 | "examples": ["Design concepts", "Theoretical frameworks", "Ideas"]
322 | },
323 | {
324 | "tag": "architecture",
325 | "description": "System architecture and design patterns",
326 | "usage_count": 22,
327 | "examples": ["System design", "Architecture patterns", "Technical blueprints"]
328 | },
329 | {
330 | "tag": "design",
331 | "description": "Design decisions and design patterns",
332 | "usage_count": 16,
333 | "examples": ["Design decisions", "UI/UX design", "System design"]
334 | },
335 | {
336 | "tag": "best-practices",
337 | "description": "Proven methodologies and recommended approaches",
338 | "usage_count": 20,
339 | "examples": ["Industry standards", "Recommended practices", "Quality guidelines"]
340 | },
341 | {
342 | "tag": "methodology",
343 | "description": "Systematic approaches and methodologies",
344 | "usage_count": 12,
345 | "examples": ["Development methodologies", "Process frameworks", "Systematic approaches"]
346 | },
347 | {
348 | "tag": "workflow",
349 | "description": "Process workflows and operational procedures",
350 | "usage_count": 14,
351 | "examples": ["Business processes", "Development workflows", "Operational procedures"]
352 | }
353 | ],
354 | "documentation_formats": [
355 | {
356 | "tag": "tutorial",
357 | "description": "Step-by-step instructional content",
358 | "usage_count": 15,
359 | "examples": ["Learning tutorials", "How-to guides", "Educational content"]
360 | },
361 | {
362 | "tag": "reference",
363 | "description": "Quick reference materials and lookups",
364 | "usage_count": 18,
365 | "examples": ["API reference", "Command reference", "Quick lookups"]
366 | },
367 | {
368 | "tag": "example",
369 | "description": "Practical examples and demonstrations",
370 | "usage_count": 22,
371 | "examples": ["Code examples", "Use cases", "Practical demonstrations"]
372 | },
373 | {
374 | "tag": "template",
375 | "description": "Reusable templates and boilerplates",
376 | "usage_count": 10,
377 | "examples": ["Document templates", "Code templates", "Process templates"]
378 | },
379 | {
380 | "tag": "checklist",
381 | "description": "Verification checklists and task lists",
382 | "usage_count": 8,
383 | "examples": ["Quality checklists", "Process verification", "Task lists"]
384 | },
385 | {
386 | "tag": "summary",
387 | "description": "Condensed information and overviews",
388 | "usage_count": 12,
389 | "examples": ["Executive summaries", "Project overviews", "Condensed reports"]
390 | }
391 | ],
392 | "technical_content": [
393 | {
394 | "tag": "configuration",
395 | "description": "System configuration and setup information",
396 | "usage_count": 16,
397 | "examples": ["System setup", "Configuration files", "Environment setup"]
398 | },
399 | {
400 | "tag": "specification",
401 | "description": "Technical specifications and requirements",
402 | "usage_count": 14,
403 | "examples": ["Technical specs", "Requirements", "Standards"]
404 | },
405 | {
406 | "tag": "analysis",
407 | "description": "Technical analysis and research findings",
408 | "usage_count": 18,
409 | "examples": ["Performance analysis", "Technical research", "Data analysis"]
410 | },
411 | {
412 | "tag": "research",
413 | "description": "Research findings and investigations",
414 | "usage_count": 15,
415 | "examples": ["Research results", "Investigations", "Study findings"]
416 | },
417 | {
418 | "tag": "review",
419 | "description": "Code reviews and process evaluations",
420 | "usage_count": 10,
421 | "examples": ["Code reviews", "Process reviews", "Quality assessments"]
422 | }
423 | ]
424 | }
425 | },
426 | "status_and_progress": {
427 | "description": "Development status, progress indicators, and priority levels",
428 | "color": "#ef4444",
429 | "icon": "📊",
430 | "tags": {
431 | "development_status": [
432 | {
433 | "tag": "resolved",
434 | "description": "Completed and verified work",
435 | "usage_count": 25,
436 | "examples": ["Completed features", "Fixed bugs", "Resolved issues"]
437 | },
438 | {
439 | "tag": "in-progress",
440 | "description": "Currently being worked on",
441 | "usage_count": 18,
442 | "examples": ["Active development", "Ongoing work", "Current tasks"]
443 | },
444 | {
445 | "tag": "blocked",
446 | "description": "Waiting for external dependencies",
447 | "usage_count": 8,
448 | "examples": ["Dependency blocks", "External waiting", "Resource constraints"]
449 | },
450 | {
451 | "tag": "needs-investigation",
452 | "description": "Requires further analysis or research",
453 | "usage_count": 12,
454 | "examples": ["Research needed", "Analysis required", "Investigation pending"]
455 | },
456 | {
457 | "tag": "planned",
458 | "description": "Scheduled for future work",
459 | "usage_count": 15,
460 | "examples": ["Future work", "Roadmap items", "Planned features"]
461 | },
462 | {
463 | "tag": "cancelled",
464 | "description": "No longer being pursued",
465 | "usage_count": 5,
466 | "examples": ["Cancelled projects", "Deprecated features", "Abandoned work"]
467 | }
468 | ],
469 | "quality_status": [
470 | {
471 | "tag": "verified",
472 | "description": "Tested and confirmed working",
473 | "usage_count": 20,
474 | "examples": ["Verified functionality", "Confirmed working", "Quality assured"]
475 | },
476 | {
477 | "tag": "tested",
478 | "description": "Has undergone testing",
479 | "usage_count": 22,
480 | "examples": ["Tested code", "QA complete", "Testing done"]
481 | },
482 | {
483 | "tag": "reviewed",
484 | "description": "Has been peer reviewed",
485 | "usage_count": 15,
486 | "examples": ["Code reviewed", "Peer reviewed", "Quality checked"]
487 | },
488 | {
489 | "tag": "approved",
490 | "description": "Officially approved for use",
491 | "usage_count": 12,
492 | "examples": ["Management approved", "Officially sanctioned", "Authorized"]
493 | },
494 | {
495 | "tag": "experimental",
496 | "description": "Proof of concept or experimental stage",
497 | "usage_count": 8,
498 | "examples": ["Proof of concept", "Experimental features", "Research stage"]
499 | },
500 | {
501 | "tag": "deprecated",
502 | "description": "No longer recommended for use",
503 | "usage_count": 6,
504 | "examples": ["Legacy code", "Outdated practices", "Superseded methods"]
505 | }
506 | ],
507 | "priority_levels": [
508 | {
509 | "tag": "urgent",
510 | "description": "Immediate attention required",
511 | "usage_count": 8,
512 | "examples": ["Critical bugs", "Emergency fixes", "Immediate action"]
513 | },
514 | {
515 | "tag": "high-priority",
516 | "description": "Important, should be addressed soon",
517 | "usage_count": 15,
518 | "examples": ["Important features", "Key improvements", "High-impact work"]
519 | },
520 | {
521 | "tag": "normal-priority",
522 | "description": "Standard priority work",
523 | "usage_count": 25,
524 | "examples": ["Regular work", "Standard features", "Normal development"]
525 | },
526 | {
527 | "tag": "low-priority",
528 | "description": "Can be addressed when time allows",
529 | "usage_count": 18,
530 | "examples": ["Nice-to-have features", "Minor improvements", "Low-impact work"]
531 | },
532 | {
533 | "tag": "nice-to-have",
534 | "description": "Enhancement, not critical",
535 | "usage_count": 12,
536 | "examples": ["Optional features", "Enhancements", "Convenience improvements"]
537 | }
538 | ]
539 | }
540 | },
541 | "context_and_temporal": {
542 | "description": "Temporal markers, environmental context, and scope indicators",
543 | "color": "#06b6d4",
544 | "icon": "🕒",
545 | "tags": {
546 | "temporal_markers": [
547 | {
548 | "tag": "january-2025",
549 | "description": "Content from January 2025",
550 | "usage_count": 50,
551 | "examples": ["Project initialization", "Early development", "Planning phase"]
552 | },
553 | {
554 | "tag": "june-2025",
555 | "description": "Content from June 2025",
556 | "usage_count": 45,
557 | "examples": ["Recent development", "Current work", "Latest updates"]
558 | },
559 | {
560 | "tag": "q1-2025",
561 | "description": "First quarter 2025 content",
562 | "usage_count": 18,
563 | "examples": ["Quarterly planning", "Q1 objectives", "First quarter work"]
564 | },
565 | {
566 | "tag": "milestone-v1",
567 | "description": "Version 1 milestone content",
568 | "usage_count": 12,
569 | "examples": ["Version milestones", "Release markers", "Development phases"]
570 | },
571 | {
572 | "tag": "sprint-3",
573 | "description": "Development sprint markers",
574 | "usage_count": 8,
575 | "examples": ["Sprint work", "Iteration markers", "Development cycles"]
576 | }
577 | ],
578 | "environmental_context": [
579 | {
580 | "tag": "development",
581 | "description": "Development environment context",
582 | "usage_count": 30,
583 | "examples": ["Development work", "Local environment", "Dev testing"]
584 | },
585 | {
586 | "tag": "staging",
587 | "description": "Staging environment context",
588 | "usage_count": 12,
589 | "examples": ["Staging deployment", "Pre-production", "Staging testing"]
590 | },
591 | {
592 | "tag": "production",
593 | "description": "Production environment context",
594 | "usage_count": 20,
595 | "examples": ["Live systems", "Production deployment", "Production issues"]
596 | },
597 | {
598 | "tag": "testing",
599 | "description": "Testing environment context",
600 | "usage_count": 25,
601 | "examples": ["Test environment", "QA testing", "Testing infrastructure"]
602 | },
603 | {
604 | "tag": "local",
605 | "description": "Local development context",
606 | "usage_count": 15,
607 | "examples": ["Local development", "Local testing", "Local setup"]
608 | }
609 | ],
610 | "scope_and_impact": [
611 | {
612 | "tag": "breaking-change",
613 | "description": "Introduces breaking changes",
614 | "usage_count": 8,
615 | "examples": ["API changes", "Backwards incompatible", "Major updates"]
616 | },
617 | {
618 | "tag": "feature",
619 | "description": "New feature development",
620 | "usage_count": 28,
621 | "examples": ["New features", "Feature additions", "Functionality expansion"]
622 | },
623 | {
624 | "tag": "enhancement",
625 | "description": "Improvement to existing features",
626 | "usage_count": 22,
627 | "examples": ["Feature improvements", "Performance enhancements", "User experience"]
628 | },
629 | {
630 | "tag": "hotfix",
631 | "description": "Critical fix for production issues",
632 | "usage_count": 6,
633 | "examples": ["Emergency fixes", "Critical patches", "Production fixes"]
634 | },
635 | {
636 | "tag": "security",
637 | "description": "Security-related content",
638 | "usage_count": 10,
639 | "examples": ["Security fixes", "Security analysis", "Vulnerability patches"]
640 | },
641 | {
642 | "tag": "performance",
643 | "description": "Performance-related improvements",
644 | "usage_count": 15,
645 | "examples": ["Performance optimization", "Speed improvements", "Efficiency gains"]
646 | }
647 | ]
648 | }
649 | }
650 | },
651 | "tag_combination_patterns": {
652 | "description": "Common patterns for combining tags across categories",
653 | "examples": [
654 | {
655 | "pattern": "Project + Technology + Activity + Status",
656 | "example": ["mcp-memory-service", "python", "debugging", "resolved"],
657 | "usage": "Most comprehensive tagging for technical work"
658 | },
659 | {
660 | "pattern": "Content Type + Domain + Technology + Context",
661 | "example": ["documentation", "backend", "chromadb", "production"],
662 | "usage": "Documentation and reference materials"
663 | },
664 | {
665 | "pattern": "Activity + Status + Priority + Temporal",
666 | "example": ["testing", "in-progress", "high-priority", "june-2025"],
667 | "usage": "Active work items with clear status"
668 | },
669 | {
670 | "pattern": "Concept + Architecture + Research + Domain",
671 | "example": ["concept", "architecture", "research", "system-design"],
672 | "usage": "Conceptual and design-related content"
673 | }
674 | ]
675 | },
676 | "usage_guidelines": {
677 | "recommended_tag_count": {
678 | "minimum": 3,
679 | "maximum": 8,
680 | "optimal": "4-6 tags from different categories"
681 | },
682 | "category_distribution": {
683 | "required": ["Project context (1-2 tags)", "Content type or activity (1-2 tags)"],
684 | "recommended": ["Technology (1-2 tags)", "Status (1 tag)"],
685 | "optional": ["Context/Temporal (0-2 tags)", "Priority (0-1 tags)"]
686 | },
687 | "quality_indicators": [
688 | "Tags from multiple categories",
689 | "Specific rather than generic terms",
690 | "Consistent with established patterns",
691 | "Relevant to content and future retrieval"
692 | ]
693 | },
694 | "maintenance": {
695 | "review_schedule": {
696 | "weekly": "Check new tag usage patterns",
697 | "monthly": "Review tag frequency and consistency",
698 | "quarterly": "Update schema based on usage patterns"
699 | },
700 | "evolution_process": [
701 | "Identify new patterns in content",
702 | "Propose new tags or categories",
703 | "Test with sample content",
704 | "Update schema documentation",
705 | "Migrate existing content if needed"
706 | ]
707 | }
708 | }
709 | }
```