contextual-engine 0.8.0__tar.gz → 0.8.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/PKG-INFO +1 -1
- contextual_engine-0.8.5/contextual/cli/commands/client.py +680 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/initialisation.py +7 -21
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/mcp.py +20 -8
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/core/output.py +30 -33
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/global_/mcp_config.py +9 -6
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/indexing.py +8 -3
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/incremental/daemon_embed.py +15 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/pipeline.py +153 -44
- contextual_engine-0.8.5/contextual/integrations/__init__.py +1 -0
- contextual_engine-0.8.5/contextual/integrations/client_content.py +149 -0
- contextual_engine-0.8.5/contextual/integrations/client_writer.py +126 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/daemon.py +52 -12
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/router.py +8 -20
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/server.py +53 -10
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/session_manager.py +28 -3
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/shim.py +15 -2
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/__init__.py +68 -34
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/reranker_model.py +43 -1
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/unified_embedder.py +52 -14
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/runner.py +15 -2
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m012_workspace_metadata.py +1 -1
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/blame_pipeline.py +16 -18
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/pyproject.toml +1 -1
- contextual_engine-0.8.0/contextual/cli/commands/client.py +0 -316
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/.contextualignore +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/.github/workflows/ci.yml +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/.github/workflows/release.yml +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/.gitignore +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/.mcp.json +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/LICENSE +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/README.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/__main__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/auth.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/doctor.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/fetch.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/index.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/install.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/stats.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/commands/workspace.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/core/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/core/context.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/core/decorators.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/cli/core/editor.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/base.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/global_/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/global_/security_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/global_/storage_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/paths.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/cache_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/indexing_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/observability_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/retrieval_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/config/workspace/workspace_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/core/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/core/errors.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/core/models.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/git/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/git/blame.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/git/hooks.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/git/incremental_git.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/dedup.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/extractor.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/integrator.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/resolver.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/staleness.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/store.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/graph/traversal.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/incremental/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/incremental/file_watcher.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/incremental/incremental_indexer.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/_config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/chunker.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/docs_pipeline.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/index_writer.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/merkle_tree.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/processor.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/indexing/symbol_extractor.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/__main__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/_platform.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/access.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/audit.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/auth.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/ratelimit.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/warmup.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/middleware/workspace.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/graph.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/nexus.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/semantic.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/system.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/mcp/tools/temporal.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/_inference.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/allowlist.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/models/base_embedder.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/config.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/exporters.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/logging.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/retention.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/observability/tracer.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/context_assembler.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/mmr.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/pipeline.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/ranker.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/retrieval/tokenizer.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/security/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/security/paths.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/security/sanitize.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/security/workspace.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/cache.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/connection.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/fts.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/lancedb_provider.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me001_orgs.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me002_org_members.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me003_teams.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me004_team_members.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me005_org_workspaces.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me006_workspace_permissions.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me007_idp_sync_log.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me008_org_policies.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me009_api_keys.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me010_org_audit_log.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/enterprise/me011_workspace_traceability.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m001_global.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m002_embeddings.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m003_temporal.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m004_graph.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m005_cache.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m006_logs.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m007_terminal_sessions.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m008_queue.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m009_swarm.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m010_task_force.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m011_reminders.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m013_temporal_l2.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/m014_nexus_foundation.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m001_global.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m002_embeddings.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m003_temporal.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m004_graph.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m005_cache.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m006_logs.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m007_terminal_sessions.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m008_queue.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m009_swarm.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m010_task_force.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m011_reminders.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m013_temporal_l2.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/solo/m014_nexus_foundation.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt001_orgs.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt002_org_members.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt003_teams.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt004_team_members.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt005_team_workspaces.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt006_workspace_permissions.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt007_team_audit_log.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/migrations/teams/mt008_workspace_traceability.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/schema.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/schema_base.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/schema_ent.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/storage/schema_team.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/adr.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/context.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/query.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/contextual/temporal/velocity.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ADRs/ADR-001-lancedb-over-vec0.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ADRs/ADR-002-retry-exponential-backoff.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ADRs/ADR-003-blake3-deduplication.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ADRs/ADR-004-async-first-architecture.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ADRs/ADR-005-model-lineup.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ARCHITECTURE.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/CHANGELOG.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DATABASE.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DEPLOYMENT.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DISTRIBUTIONS.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CHATGPT-DR-mcp-cli-security-testing-distribution.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CHATGPT-DR-web-ui-experience.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CLAUDE-DR-ai-agents-teams.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CLAUDE-DR-competitive-landscape-market-analysis.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CLAUDE-DR-core-architecture.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CLAUDE-DR-mvp-phase0-technical-plan.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/CLAUDE-DR-strategic-briefing.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-graph-implementation.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-hybrid-retrieval-local-optimization.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-infrastructure-security.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-lance-specs.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-nexus-graph-architecture.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-nexus-graph-engineering.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GEMINI-DR-vec0-vs-lancedb.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/GRAPH_LAYER_OPEN_QUESTIONS_RESOLVED.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/KIMI-DR-tech-stack-optimization.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-L2-temporal.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-client-integration.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-indexing-optimisation.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-lancedb-implementation.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-local-data-infrastructure.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-mcp-config.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-mcp-security-hardening.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-models-pipelines.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-python-async-ai-pipelines.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/DRs/PERPLEXITY-DR-semantic-indexing.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/FRONTEND.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/GITHUB.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/INTEGRATIONS.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/MCP_TOOLS.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/MEMORY.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/PROJECT_MANAGEMENT.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/PROJECT_PHASES.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/RESOURCES.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/ROADMAP.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/SECURITY.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/TEAMS.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/TECHNICAL_SPEC.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/TESTING.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/docs/VERSION.MD +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/pytest.ini +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/README.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/conftest.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/fixtures/__init__.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/fixtures/sample_code.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/fixtures/sample_docs.md +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_accuracy.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_architecture.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_async.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_behaviour.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_cache_stats.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_cli.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_flow.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_git.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_graph.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_graph_resolution_order.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_incremental.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_indexing.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_latency.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_mcp.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_mcp_hardening.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_observability.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_performance.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_query_cache_roundtrip.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_retrieval.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_retry.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_security.py +0 -0
- {contextual_engine-0.8.0 → contextual_engine-0.8.5}/tests/test_storage.py +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Client configuration commands for MCP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from contextual.cli.core import register_command, require_config, open_in_editor
|
|
16
|
+
from contextual.cli.core.output import (
|
|
17
|
+
console,
|
|
18
|
+
print_client_configured,
|
|
19
|
+
print_error,
|
|
20
|
+
print_info,
|
|
21
|
+
print_md_injected,
|
|
22
|
+
print_agents_md_hint,
|
|
23
|
+
print_client_list,
|
|
24
|
+
)
|
|
25
|
+
from contextual.integrations.client_content import (
|
|
26
|
+
cursor_file_content,
|
|
27
|
+
antigravity_file_content,
|
|
28
|
+
marker_block,
|
|
29
|
+
)
|
|
30
|
+
from contextual.integrations.client_writer import write_type_a, write_type_b
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _find_uvx() -> str | None:
|
|
34
|
+
"""Locate a usable uvx executable."""
|
|
35
|
+
uvx_path = shutil.which("uvx")
|
|
36
|
+
if uvx_path:
|
|
37
|
+
return uvx_path
|
|
38
|
+
|
|
39
|
+
for candidate in [
|
|
40
|
+
Path.home() / ".local" / "bin" / "uvx",
|
|
41
|
+
Path("/usr/local/bin/uvx"),
|
|
42
|
+
Path("/opt/homebrew/bin/uvx"),
|
|
43
|
+
]:
|
|
44
|
+
if candidate.exists():
|
|
45
|
+
return str(candidate)
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _backup_config(config_path: Path) -> dict:
|
|
51
|
+
"""Load an existing config, backing it up first when present."""
|
|
52
|
+
existing: dict = {}
|
|
53
|
+
if config_path.exists():
|
|
54
|
+
backup_path = config_path.with_suffix(".json.bak")
|
|
55
|
+
try:
|
|
56
|
+
shutil.copy2(config_path, backup_path)
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
62
|
+
except (json.JSONDecodeError, OSError):
|
|
63
|
+
existing = {}
|
|
64
|
+
|
|
65
|
+
return existing
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_workspace_from_entry(entry: dict) -> str | None:
|
|
69
|
+
"""Return the resolved workspace path embedded in an MCP entry's args, or None."""
|
|
70
|
+
args = entry.get("args", [])
|
|
71
|
+
if not isinstance(args, list):
|
|
72
|
+
return None
|
|
73
|
+
# Standard entry: [..., "--workspace", "/path"]
|
|
74
|
+
try:
|
|
75
|
+
idx = args.index("--workspace")
|
|
76
|
+
if idx + 1 < len(args):
|
|
77
|
+
return str(Path(args[idx + 1]).resolve())
|
|
78
|
+
except ValueError:
|
|
79
|
+
pass
|
|
80
|
+
# Shell entry (Claude Desktop zsh -lc "... --workspace /path ..."):
|
|
81
|
+
# workspace path is the token immediately after --workspace in the shell string.
|
|
82
|
+
for arg in args:
|
|
83
|
+
if isinstance(arg, str) and "--workspace" in arg:
|
|
84
|
+
after = arg.split("--workspace", 1)[1].strip()
|
|
85
|
+
try:
|
|
86
|
+
tokens = shlex.split(after)
|
|
87
|
+
if tokens:
|
|
88
|
+
return str(Path(tokens[0]).resolve())
|
|
89
|
+
except ValueError:
|
|
90
|
+
pass
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _persist_client_config(config_path: Path, document: dict, mcp_servers: dict) -> None:
|
|
95
|
+
"""Atomically write the merged config back to disk."""
|
|
96
|
+
document["mcpServers"] = mcp_servers
|
|
97
|
+
try:
|
|
98
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
tmp = config_path.with_suffix(".json.tmp")
|
|
100
|
+
tmp.write_text(json.dumps(document, indent=2), encoding="utf-8")
|
|
101
|
+
os.chmod(tmp, 0o600)
|
|
102
|
+
os.replace(tmp, config_path)
|
|
103
|
+
except OSError as exc:
|
|
104
|
+
print_error(
|
|
105
|
+
f"Failed to write config: {exc}",
|
|
106
|
+
hint=f"Check permissions on {config_path.parent}",
|
|
107
|
+
)
|
|
108
|
+
raise typer.Exit(1) from exc
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_client_config(config_path: Path, mcp_entry: dict, workspace_root: Path) -> str:
|
|
112
|
+
"""Merge the Contextual entry into a client config. Returns the key written.
|
|
113
|
+
|
|
114
|
+
Smart workspace detection:
|
|
115
|
+
- If an existing contextual entry already points at this workspace → update it in-place
|
|
116
|
+
(preserves the user's key, avoids accumulating duplicate entries).
|
|
117
|
+
- If no contextual entry exists → add under the bare ``contextual`` key.
|
|
118
|
+
- If ``contextual`` is taken by a different workspace → add as ``contextual-<dirname>``,
|
|
119
|
+
with a short path-hash suffix if that key is also taken.
|
|
120
|
+
"""
|
|
121
|
+
import hashlib
|
|
122
|
+
|
|
123
|
+
existing = _backup_config(config_path)
|
|
124
|
+
mcp_servers = existing.get("mcpServers")
|
|
125
|
+
if not isinstance(mcp_servers, dict):
|
|
126
|
+
mcp_servers = {}
|
|
127
|
+
|
|
128
|
+
ws_str = str(workspace_root.resolve())
|
|
129
|
+
|
|
130
|
+
# 1. Look for an existing contextual entry that already owns this workspace.
|
|
131
|
+
for key, val in mcp_servers.items():
|
|
132
|
+
if isinstance(val, dict) and (key == "contextual" or key.startswith("contextual-")):
|
|
133
|
+
if _extract_workspace_from_entry(val) == ws_str:
|
|
134
|
+
mcp_servers[key] = mcp_entry
|
|
135
|
+
_persist_client_config(config_path, existing, mcp_servers)
|
|
136
|
+
return key
|
|
137
|
+
|
|
138
|
+
# 2. No match — pick the least-surprising new key.
|
|
139
|
+
if "contextual" not in mcp_servers:
|
|
140
|
+
key = "contextual"
|
|
141
|
+
else:
|
|
142
|
+
key = f"contextual-{workspace_root.name}"
|
|
143
|
+
if key in mcp_servers:
|
|
144
|
+
suffix = hashlib.sha256(ws_str.encode()).hexdigest()[:6]
|
|
145
|
+
key = f"contextual-{workspace_root.name}-{suffix}"
|
|
146
|
+
|
|
147
|
+
mcp_servers[key] = mcp_entry
|
|
148
|
+
_persist_client_config(config_path, existing, mcp_servers)
|
|
149
|
+
return key
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _build_mcp_entry(uvx_path: str, workspace_root: Path) -> dict:
|
|
153
|
+
"""Build the default MCP launcher entry for Contextual."""
|
|
154
|
+
return {
|
|
155
|
+
"command": uvx_path,
|
|
156
|
+
"args": ["--from", "contextual-engine", "contextual-mcp", "--workspace", str(workspace_root)],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _build_claude_mcp_entry(uvx_path: str, workspace_root: Path) -> dict:
|
|
161
|
+
"""Build the Claude Desktop MCP entry with an explicit workspace cd."""
|
|
162
|
+
return {
|
|
163
|
+
"command": "/bin/zsh",
|
|
164
|
+
"args": [
|
|
165
|
+
"-lc",
|
|
166
|
+
(
|
|
167
|
+
f"cd {shlex.quote(str(workspace_root))} "
|
|
168
|
+
f"&& exec {shlex.quote(uvx_path)} --from contextual-engine contextual-mcp "
|
|
169
|
+
f"--workspace {shlex.quote(str(workspace_root))}"
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_vscode_mcp_entry(uvx_path: str, workspace_root: Path) -> dict:
|
|
176
|
+
"""Build the VS Code / GitHub Copilot MCP entry.
|
|
177
|
+
|
|
178
|
+
VS Code requires an explicit ``"type": "stdio"`` field — other clients
|
|
179
|
+
accept entries without it.
|
|
180
|
+
"""
|
|
181
|
+
return {
|
|
182
|
+
"type": "stdio",
|
|
183
|
+
"command": uvx_path,
|
|
184
|
+
"args": ["--from", "contextual-engine", "contextual-mcp", "--workspace", str(workspace_root)],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ── VS Code / GitHub Copilot writer ──────────────────────────────────────────
|
|
189
|
+
# VS Code's mcp.json uses a "servers" root key (not "mcpServers").
|
|
190
|
+
|
|
191
|
+
def _persist_vscode_config(config_path: Path, document: dict, servers: dict) -> None:
|
|
192
|
+
"""Atomically write VS Code mcp.json using the 'servers' root key."""
|
|
193
|
+
document["servers"] = servers
|
|
194
|
+
try:
|
|
195
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
tmp = config_path.with_suffix(".json.tmp")
|
|
197
|
+
tmp.write_text(json.dumps(document, indent=2), encoding="utf-8")
|
|
198
|
+
os.chmod(tmp, 0o600)
|
|
199
|
+
os.replace(tmp, config_path)
|
|
200
|
+
except OSError as exc:
|
|
201
|
+
print_error(
|
|
202
|
+
f"Failed to write config: {exc}",
|
|
203
|
+
hint=f"Check permissions on {config_path.parent}",
|
|
204
|
+
)
|
|
205
|
+
raise typer.Exit(1) from exc
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _write_vscode_config(config_path: Path, mcp_entry: dict, workspace_root: Path) -> str:
|
|
209
|
+
"""Merge the Contextual entry into a VS Code mcp.json. Returns the key written."""
|
|
210
|
+
import hashlib
|
|
211
|
+
|
|
212
|
+
existing = _backup_config(config_path)
|
|
213
|
+
servers = existing.get("servers")
|
|
214
|
+
if not isinstance(servers, dict):
|
|
215
|
+
servers = {}
|
|
216
|
+
|
|
217
|
+
ws_str = str(workspace_root.resolve())
|
|
218
|
+
|
|
219
|
+
for key, val in servers.items():
|
|
220
|
+
if isinstance(val, dict) and (key == "contextual" or key.startswith("contextual-")):
|
|
221
|
+
if _extract_workspace_from_entry(val) == ws_str:
|
|
222
|
+
servers[key] = mcp_entry
|
|
223
|
+
_persist_vscode_config(config_path, existing, servers)
|
|
224
|
+
return key
|
|
225
|
+
|
|
226
|
+
if "contextual" not in servers:
|
|
227
|
+
key = "contextual"
|
|
228
|
+
else:
|
|
229
|
+
key = f"contextual-{workspace_root.name}"
|
|
230
|
+
if key in servers:
|
|
231
|
+
suffix = hashlib.sha256(ws_str.encode()).hexdigest()[:6]
|
|
232
|
+
key = f"contextual-{workspace_root.name}-{suffix}"
|
|
233
|
+
|
|
234
|
+
servers[key] = mcp_entry
|
|
235
|
+
_persist_vscode_config(config_path, existing, servers)
|
|
236
|
+
return key
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _configure_vscode_client(
|
|
240
|
+
config_path: Path,
|
|
241
|
+
mcp_entry: dict,
|
|
242
|
+
workspace_root: Path,
|
|
243
|
+
dry_run: bool = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Write Contextual MCP entry into a VS Code mcp.json."""
|
|
246
|
+
if dry_run:
|
|
247
|
+
from rich.syntax import Syntax
|
|
248
|
+
print_info(f"Dry run — would write to: {config_path}")
|
|
249
|
+
console.print()
|
|
250
|
+
console.print(
|
|
251
|
+
Syntax(
|
|
252
|
+
json.dumps({"servers": {"contextual": mcp_entry}}, indent=2),
|
|
253
|
+
"json",
|
|
254
|
+
theme="ansi_dark",
|
|
255
|
+
background_color="default",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
console.print()
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
key = _write_vscode_config(config_path, mcp_entry, workspace_root)
|
|
262
|
+
print_client_configured("GitHub Copilot", key, config_path)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Codex TOML writer ─────────────────────────────────────────────────────────
|
|
266
|
+
# Codex uses ~/.codex/config.toml with [mcp_servers.<name>] TOML tables.
|
|
267
|
+
|
|
268
|
+
def _persist_codex_toml(config_path: Path, document: dict) -> None:
|
|
269
|
+
"""Atomically write Codex config.toml."""
|
|
270
|
+
import tomli_w
|
|
271
|
+
try:
|
|
272
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
tmp = config_path.with_suffix(".toml.tmp")
|
|
274
|
+
tmp.write_bytes(tomli_w.dumps(document).encode("utf-8"))
|
|
275
|
+
os.chmod(tmp, 0o600)
|
|
276
|
+
os.replace(tmp, config_path)
|
|
277
|
+
except OSError as exc:
|
|
278
|
+
print_error(
|
|
279
|
+
f"Failed to write config: {exc}",
|
|
280
|
+
hint=f"Check permissions on {config_path.parent}",
|
|
281
|
+
)
|
|
282
|
+
raise typer.Exit(1) from exc
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _write_codex_toml_config(config_path: Path, uvx_path: str, workspace_root: Path) -> str:
|
|
286
|
+
"""Merge the Contextual entry into Codex config.toml. Returns the key written."""
|
|
287
|
+
import hashlib
|
|
288
|
+
import tomllib
|
|
289
|
+
|
|
290
|
+
existing: dict = {}
|
|
291
|
+
if config_path.exists():
|
|
292
|
+
backup_path = config_path.with_suffix(".toml.bak")
|
|
293
|
+
try:
|
|
294
|
+
shutil.copy2(config_path, backup_path)
|
|
295
|
+
except OSError:
|
|
296
|
+
pass
|
|
297
|
+
try:
|
|
298
|
+
with open(config_path, "rb") as fh:
|
|
299
|
+
existing = tomllib.load(fh)
|
|
300
|
+
except Exception:
|
|
301
|
+
existing = {}
|
|
302
|
+
|
|
303
|
+
mcp_servers = existing.setdefault("mcp_servers", {})
|
|
304
|
+
ws_str = str(workspace_root.resolve())
|
|
305
|
+
entry = {
|
|
306
|
+
"command": uvx_path,
|
|
307
|
+
"args": ["--from", "contextual-engine", "contextual-mcp", "--workspace", str(workspace_root)],
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for key, val in mcp_servers.items():
|
|
311
|
+
if isinstance(val, dict) and (key == "contextual" or key.startswith("contextual-")):
|
|
312
|
+
args = val.get("args", [])
|
|
313
|
+
try:
|
|
314
|
+
idx = args.index("--workspace")
|
|
315
|
+
if idx + 1 < len(args) and str(Path(args[idx + 1]).resolve()) == ws_str:
|
|
316
|
+
mcp_servers[key] = entry
|
|
317
|
+
_persist_codex_toml(config_path, existing)
|
|
318
|
+
return key
|
|
319
|
+
except (ValueError, IndexError):
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
if "contextual" not in mcp_servers:
|
|
323
|
+
key = "contextual"
|
|
324
|
+
else:
|
|
325
|
+
key = f"contextual-{workspace_root.name}"
|
|
326
|
+
if key in mcp_servers:
|
|
327
|
+
suffix = hashlib.sha256(ws_str.encode()).hexdigest()[:6]
|
|
328
|
+
key = f"contextual-{workspace_root.name}-{suffix}"
|
|
329
|
+
|
|
330
|
+
mcp_servers[key] = entry
|
|
331
|
+
_persist_codex_toml(config_path, existing)
|
|
332
|
+
return key
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _configure_codex_client(
|
|
336
|
+
config_path: Path,
|
|
337
|
+
uvx_path: str,
|
|
338
|
+
workspace_root: Path,
|
|
339
|
+
dry_run: bool = False,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Write Contextual MCP entry into Codex config.toml."""
|
|
342
|
+
if dry_run:
|
|
343
|
+
from rich.syntax import Syntax
|
|
344
|
+
print_info(f"Dry run — would write to: {config_path}")
|
|
345
|
+
console.print()
|
|
346
|
+
console.print(
|
|
347
|
+
Syntax(
|
|
348
|
+
f"[mcp_servers.contextual]\ncommand = {json.dumps(uvx_path)}\n"
|
|
349
|
+
f'args = ["--from", "contextual-engine", "contextual-mcp", '
|
|
350
|
+
f'"--workspace", {json.dumps(str(workspace_root))}]',
|
|
351
|
+
"toml",
|
|
352
|
+
theme="ansi_dark",
|
|
353
|
+
background_color="default",
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
console.print()
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
key = _write_codex_toml_config(config_path, uvx_path, workspace_root)
|
|
360
|
+
print_client_configured("Codex", key, config_path)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ── Manual / UI-based clients ─────────────────────────────────────────────────
|
|
364
|
+
# ChatGPT and Perplexity have no writable local config file — MCP servers are
|
|
365
|
+
# added through their desktop UI. Display what the user needs to paste.
|
|
366
|
+
|
|
367
|
+
def _display_manual_config(
|
|
368
|
+
client_name: str,
|
|
369
|
+
uvx_path: str,
|
|
370
|
+
workspace_root: Path,
|
|
371
|
+
ui_steps: list[str],
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Print manual setup instructions for UI-based MCP clients."""
|
|
374
|
+
from rich.syntax import Syntax
|
|
375
|
+
from rich.panel import Panel
|
|
376
|
+
from rich.text import Text
|
|
377
|
+
|
|
378
|
+
cmd = (
|
|
379
|
+
f"{uvx_path} --from contextual-engine contextual-mcp "
|
|
380
|
+
f"--workspace {shlex.quote(str(workspace_root))}"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
console.print()
|
|
384
|
+
console.print(Panel(
|
|
385
|
+
Text(
|
|
386
|
+
f"{client_name} does not support a local config file — "
|
|
387
|
+
"MCP servers must be added through the app UI.",
|
|
388
|
+
style="white",
|
|
389
|
+
),
|
|
390
|
+
title=f"[white bold]{client_name} — Manual Setup Required[/white bold]",
|
|
391
|
+
border_style="bright_black",
|
|
392
|
+
padding=(0, 1),
|
|
393
|
+
))
|
|
394
|
+
console.print()
|
|
395
|
+
|
|
396
|
+
for i, step in enumerate(ui_steps, 1):
|
|
397
|
+
console.print(f" [white bold]{i}.[/white bold] [#6b6b6b]{step}[/#6b6b6b]")
|
|
398
|
+
|
|
399
|
+
console.print()
|
|
400
|
+
console.print(" [white bold]Server name:[/white bold] [white]contextual[/white]")
|
|
401
|
+
console.print(" [white bold]Command:[/white bold]")
|
|
402
|
+
console.print()
|
|
403
|
+
console.print(
|
|
404
|
+
Syntax(cmd, "bash", theme="ansi_dark", background_color="default")
|
|
405
|
+
)
|
|
406
|
+
console.print()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _config_paths() -> dict[str, Path]:
|
|
410
|
+
"""Return per-client config file paths, platform-aware.
|
|
411
|
+
|
|
412
|
+
Paths verified against docs/DRs/PERPLEXITY-DR-mcp-config.md (June 2026).
|
|
413
|
+
chatgpt / perplexity are intentionally absent — both are UI-only; no local
|
|
414
|
+
config file exists for them. They are handled by _display_manual_config().
|
|
415
|
+
"""
|
|
416
|
+
home = Path.home()
|
|
417
|
+
is_mac = sys.platform == "darwin"
|
|
418
|
+
is_win = sys.platform == "win32"
|
|
419
|
+
app_support = (
|
|
420
|
+
home / "Library" / "Application Support"
|
|
421
|
+
if is_mac
|
|
422
|
+
else Path(os.environ.get("APPDATA", str(home)))
|
|
423
|
+
if is_win
|
|
424
|
+
else home / ".config"
|
|
425
|
+
)
|
|
426
|
+
return {
|
|
427
|
+
"claude": app_support / "Claude" / "claude_desktop_config.json",
|
|
428
|
+
"gemini-cli": home / ".gemini" / "settings.json",
|
|
429
|
+
"cursor": home / ".cursor" / "mcp.json",
|
|
430
|
+
"copilot": home / ".vscode" / "mcp.json",
|
|
431
|
+
"windsurf": home / ".codeium" / "windsurf" / "mcp_config.json",
|
|
432
|
+
"codex": home / ".codex" / "config.toml",
|
|
433
|
+
"antigravity": home / ".gemini" / "config" / "mcp_config.json",
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _configure_client(
|
|
438
|
+
client_name: str,
|
|
439
|
+
config_path: Path,
|
|
440
|
+
mcp_entry: dict,
|
|
441
|
+
workspace_root: Path,
|
|
442
|
+
dry_run: bool = False,
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Write Contextual MCP entry into a client config file."""
|
|
445
|
+
if dry_run:
|
|
446
|
+
from rich.syntax import Syntax
|
|
447
|
+
print_info(f"Dry run — would write to: {config_path}")
|
|
448
|
+
console.print()
|
|
449
|
+
console.print(
|
|
450
|
+
Syntax(
|
|
451
|
+
json.dumps({"contextual": mcp_entry}, indent=2),
|
|
452
|
+
"json",
|
|
453
|
+
theme="ansi_dark",
|
|
454
|
+
background_color="default",
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
console.print()
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
key = _write_client_config(config_path, mcp_entry, workspace_root)
|
|
461
|
+
print_client_configured(client_name, key, config_path)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@register_command(group="client")
|
|
465
|
+
@require_config
|
|
466
|
+
def client(
|
|
467
|
+
typer_ctx: typer.Context,
|
|
468
|
+
claude: Annotated[
|
|
469
|
+
bool,
|
|
470
|
+
typer.Option("--claude", help="Configure Claude Desktop to use Contextual via MCP."),
|
|
471
|
+
] = False,
|
|
472
|
+
claude_code: Annotated[
|
|
473
|
+
bool,
|
|
474
|
+
typer.Option("--claude-code", help="Configure Claude Code (CLI) to use Contextual via MCP."),
|
|
475
|
+
] = False,
|
|
476
|
+
chatgpt: Annotated[
|
|
477
|
+
bool,
|
|
478
|
+
typer.Option("--chatgpt", help="Configure ChatGPT to use Contextual via MCP."),
|
|
479
|
+
] = False,
|
|
480
|
+
perplexity: Annotated[
|
|
481
|
+
bool,
|
|
482
|
+
typer.Option("--perplexity", help="Configure Perplexity to use Contextual via MCP."),
|
|
483
|
+
] = False,
|
|
484
|
+
gemini_cli: Annotated[
|
|
485
|
+
bool,
|
|
486
|
+
typer.Option("--gemini-cli", help="Configure Gemini CLI to use Contextual via MCP."),
|
|
487
|
+
] = False,
|
|
488
|
+
cursor: Annotated[
|
|
489
|
+
bool,
|
|
490
|
+
typer.Option("--cursor", help="Configure Cursor IDE to use Contextual via MCP."),
|
|
491
|
+
] = False,
|
|
492
|
+
copilot: Annotated[
|
|
493
|
+
bool,
|
|
494
|
+
typer.Option("--copilot", help="Configure GitHub Copilot (VS Code) to use Contextual via MCP."),
|
|
495
|
+
] = False,
|
|
496
|
+
windsurf: Annotated[
|
|
497
|
+
bool,
|
|
498
|
+
typer.Option("--windsurf", help="Configure Windsurf to use Contextual via MCP."),
|
|
499
|
+
] = False,
|
|
500
|
+
codex: Annotated[
|
|
501
|
+
bool,
|
|
502
|
+
typer.Option("--codex", help="Configure Codex CLI to use Contextual via MCP."),
|
|
503
|
+
] = False,
|
|
504
|
+
antigravity: Annotated[
|
|
505
|
+
bool,
|
|
506
|
+
typer.Option("--antigravity", help="Configure Antigravity to use Contextual via MCP."),
|
|
507
|
+
] = False,
|
|
508
|
+
cline: Annotated[
|
|
509
|
+
bool,
|
|
510
|
+
typer.Option("--cline", help="Inject Contextual tool instructions into AGENTS.md for Cline."),
|
|
511
|
+
] = False,
|
|
512
|
+
dry_run: Annotated[
|
|
513
|
+
bool,
|
|
514
|
+
typer.Option("--dry-run", help="Show what would be written without writing."),
|
|
515
|
+
] = False,
|
|
516
|
+
) -> None:
|
|
517
|
+
"""Configure an MCP client to use Contextual."""
|
|
518
|
+
if typer_ctx.invoked_subcommand is not None:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
flags = [claude, claude_code, chatgpt, perplexity, gemini_cli, cursor, copilot, windsurf, codex, antigravity, cline]
|
|
522
|
+
selected = sum(flags)
|
|
523
|
+
|
|
524
|
+
if selected == 0:
|
|
525
|
+
print_error(
|
|
526
|
+
"Specify a client to configure.",
|
|
527
|
+
hint=(
|
|
528
|
+
"contextual client --claude\n"
|
|
529
|
+
" contextual client --claude-code\n"
|
|
530
|
+
" contextual client --cursor\n"
|
|
531
|
+
" contextual client --copilot\n"
|
|
532
|
+
" contextual client --windsurf\n"
|
|
533
|
+
" contextual client --antigravity\n"
|
|
534
|
+
" contextual client --cline\n"
|
|
535
|
+
" contextual client --gemini-cli\n"
|
|
536
|
+
" contextual client --codex\n"
|
|
537
|
+
" contextual client --chatgpt\n"
|
|
538
|
+
" contextual client --perplexity"
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
raise typer.Exit(1)
|
|
542
|
+
|
|
543
|
+
if selected > 1:
|
|
544
|
+
print_error(
|
|
545
|
+
"Only one client can be configured at a time.",
|
|
546
|
+
hint="Run the command separately for each client.",
|
|
547
|
+
)
|
|
548
|
+
raise typer.Exit(1)
|
|
549
|
+
|
|
550
|
+
uvx_path = _find_uvx()
|
|
551
|
+
if not uvx_path:
|
|
552
|
+
print_error(
|
|
553
|
+
"uvx not found on PATH.",
|
|
554
|
+
hint="Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
555
|
+
)
|
|
556
|
+
raise typer.Exit(1)
|
|
557
|
+
|
|
558
|
+
paths = _config_paths()
|
|
559
|
+
workspace_root = Path.cwd().resolve()
|
|
560
|
+
|
|
561
|
+
agents_md = workspace_root / ".contextual" / "AGENTS.md"
|
|
562
|
+
|
|
563
|
+
if claude:
|
|
564
|
+
entry = _build_claude_mcp_entry(uvx_path, workspace_root)
|
|
565
|
+
_configure_client("Claude Desktop", paths["claude"], entry, workspace_root, dry_run)
|
|
566
|
+
print_agents_md_hint(agents_md, "Claude → Project → Custom Instructions")
|
|
567
|
+
|
|
568
|
+
elif claude_code:
|
|
569
|
+
_configure_client("Claude Code", workspace_root / ".mcp.json", _build_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
570
|
+
if not dry_run:
|
|
571
|
+
result = write_type_b(workspace_root / "CLAUDE.md", marker_block("claude-code"))
|
|
572
|
+
print_md_injected(workspace_root / "CLAUDE.md", result)
|
|
573
|
+
|
|
574
|
+
elif chatgpt:
|
|
575
|
+
_display_manual_config(
|
|
576
|
+
"ChatGPT",
|
|
577
|
+
uvx_path,
|
|
578
|
+
workspace_root,
|
|
579
|
+
ui_steps=[
|
|
580
|
+
"Open ChatGPT Desktop and go to Settings",
|
|
581
|
+
"Navigate to Connectors → Advanced → Developer mode",
|
|
582
|
+
"Click 'Add server' and enter the name and command below",
|
|
583
|
+
"Toggle the server on and restart ChatGPT",
|
|
584
|
+
],
|
|
585
|
+
)
|
|
586
|
+
print_agents_md_hint(agents_md, "ChatGPT → Project → Custom Instructions")
|
|
587
|
+
|
|
588
|
+
elif perplexity:
|
|
589
|
+
_display_manual_config(
|
|
590
|
+
"Perplexity",
|
|
591
|
+
uvx_path,
|
|
592
|
+
workspace_root,
|
|
593
|
+
ui_steps=[
|
|
594
|
+
"Open Perplexity Desktop (macOS) and go to Settings",
|
|
595
|
+
"Navigate to Connectors → Add Connector",
|
|
596
|
+
"Enter the server name and command below",
|
|
597
|
+
"Toggle MCP on under Sources on the homepage",
|
|
598
|
+
],
|
|
599
|
+
)
|
|
600
|
+
print_agents_md_hint(agents_md, "Perplexity → Space → System Prompt")
|
|
601
|
+
|
|
602
|
+
elif gemini_cli:
|
|
603
|
+
_configure_client("Gemini CLI", paths["gemini-cli"], _build_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
604
|
+
|
|
605
|
+
elif cursor:
|
|
606
|
+
_configure_client("Cursor", paths["cursor"], _build_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
607
|
+
if not dry_run:
|
|
608
|
+
result = write_type_a(workspace_root / ".cursor" / "rules" / "contextual.mdc", cursor_file_content())
|
|
609
|
+
print_md_injected(workspace_root / ".cursor" / "rules" / "contextual.mdc", result)
|
|
610
|
+
|
|
611
|
+
elif copilot:
|
|
612
|
+
_configure_vscode_client(paths["copilot"], _build_vscode_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
613
|
+
if not dry_run:
|
|
614
|
+
result = write_type_b(workspace_root / ".github" / "copilot-instructions.md", marker_block("copilot"))
|
|
615
|
+
print_md_injected(workspace_root / ".github" / "copilot-instructions.md", result)
|
|
616
|
+
|
|
617
|
+
elif windsurf:
|
|
618
|
+
_configure_client("Windsurf", paths["windsurf"], _build_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
619
|
+
if not dry_run:
|
|
620
|
+
result = write_type_b(workspace_root / ".windsurfrules", marker_block("windsurf"))
|
|
621
|
+
print_md_injected(workspace_root / ".windsurfrules", result)
|
|
622
|
+
|
|
623
|
+
elif codex:
|
|
624
|
+
_configure_codex_client(paths["codex"], uvx_path, workspace_root, dry_run)
|
|
625
|
+
|
|
626
|
+
elif antigravity:
|
|
627
|
+
_configure_client("Antigravity", paths["antigravity"], _build_mcp_entry(uvx_path, workspace_root), workspace_root, dry_run)
|
|
628
|
+
if not dry_run:
|
|
629
|
+
result = write_type_a(workspace_root / ".agent" / "rules" / "contextual.md", antigravity_file_content())
|
|
630
|
+
print_md_injected(workspace_root / ".agent" / "rules" / "contextual.md", result)
|
|
631
|
+
|
|
632
|
+
elif cline:
|
|
633
|
+
# Cline: markdown-only — no separate MCP config file.
|
|
634
|
+
if not dry_run:
|
|
635
|
+
result = write_type_b(workspace_root / "AGENTS.md", marker_block("cline"))
|
|
636
|
+
print_md_injected(workspace_root / "AGENTS.md", result)
|
|
637
|
+
else:
|
|
638
|
+
from rich.syntax import Syntax
|
|
639
|
+
print_info("Dry run — would inject into: AGENTS.md")
|
|
640
|
+
console.print()
|
|
641
|
+
console.print(Syntax(marker_block("cline")[:300] + "\n...", "markdown", theme="ansi_dark", background_color="default"))
|
|
642
|
+
console.print()
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
@register_command(group="client", name="list")
|
|
646
|
+
def client_list() -> None:
|
|
647
|
+
"""List configured MCP clients."""
|
|
648
|
+
from contextual.mcp.access import registry
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
registry.load_from_disk()
|
|
652
|
+
except Exception:
|
|
653
|
+
pass
|
|
654
|
+
|
|
655
|
+
clients_list = []
|
|
656
|
+
for name, info in registry._by_name.items():
|
|
657
|
+
clients_list.append({
|
|
658
|
+
"name": name,
|
|
659
|
+
"access": info.get("access", "none"),
|
|
660
|
+
"workspaces": info.get("workspaces", ["*"])
|
|
661
|
+
})
|
|
662
|
+
print_client_list(clients_list)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@register_command(group="client", name="config")
|
|
666
|
+
def client_config() -> None:
|
|
667
|
+
"""Open global mcp.json client access policy configuration in editor."""
|
|
668
|
+
from contextual.config.paths import MCP_CONFIG_FILE as MCP_JSON_PATH
|
|
669
|
+
if not MCP_JSON_PATH.exists():
|
|
670
|
+
try:
|
|
671
|
+
MCP_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
672
|
+
MCP_JSON_PATH.write_text("{\n \"clients\": {}\n}\n")
|
|
673
|
+
except Exception as e:
|
|
674
|
+
print_error(f"Could not create mcp.json config: {e}")
|
|
675
|
+
raise typer.Exit(1)
|
|
676
|
+
try:
|
|
677
|
+
open_in_editor(MCP_JSON_PATH)
|
|
678
|
+
except Exception as e:
|
|
679
|
+
print_error(f"Failed to open config file: {e}")
|
|
680
|
+
raise typer.Exit(1)
|