#
tokens: 48592/50000 6/625 files (page 32/47)
lines: on (toggle) GitHub
raw markdown copy reset
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 | }
```
Page 32/47FirstPrevNextLast