attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,1896 @@
|
|
|
1
|
+
"""Install/uninstall attocode-code-intel MCP server into coding assistants.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Claude Code: via `claude mcp add` CLI
|
|
5
|
+
- Cursor: via `.cursor/mcp.json`
|
|
6
|
+
- Windsurf: via `.windsurf/mcp.json`
|
|
7
|
+
- VS Code: via `.vscode/mcp.json`
|
|
8
|
+
- Codex: via `.codex/config.toml`
|
|
9
|
+
- Claude Desktop: via platform-specific `claude_desktop_config.json`
|
|
10
|
+
- Cline: via VS Code globalStorage `cline_mcp_settings.json`
|
|
11
|
+
- Zed: via `.zed/settings.json` or `~/.config/zed/settings.json`
|
|
12
|
+
- OpenCode: via `~/.config/opencode/config.json`
|
|
13
|
+
- Gemini CLI: via `.gemini/settings.json` or `~/.gemini/settings.json`
|
|
14
|
+
- Roo Code: via `.roo/mcp.json`
|
|
15
|
+
- Amazon Q: via `~/.aws/amazonq/mcp.json`
|
|
16
|
+
- GitHub Copilot CLI: via `~/.copilot/mcp-config.json`
|
|
17
|
+
- Junie: via `.junie/mcp/mcp.json` or `~/.junie/mcp/mcp.json`
|
|
18
|
+
- Kiro: via `.kiro/settings/mcp.json`
|
|
19
|
+
- Trae: via `.trae/mcp.json`
|
|
20
|
+
- Firebase Studio: via `.idx/mcp.json`
|
|
21
|
+
- Amp: via `.amp/settings.json` or `~/.config/amp/settings.json`
|
|
22
|
+
- Continue.dev: via `.continue/mcp.json`
|
|
23
|
+
- Hermes Agent: via `~/.hermes/config.yaml`
|
|
24
|
+
- Goose: via `~/.config/goose/config.yaml`
|
|
25
|
+
- GSD: via `.gsd/mcp.json` or `~/.gsd/mcp.json`
|
|
26
|
+
- IntelliJ: manual instructions
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import platform
|
|
34
|
+
import shutil
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
import tomllib
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
import tomli_w
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Target constants
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
#: Targets that can be auto-installed via config file manipulation or CLI.
|
|
49
|
+
AUTO_INSTALL_TARGETS: tuple[str, ...] = (
|
|
50
|
+
"claude", "cursor", "windsurf", "vscode", "codex",
|
|
51
|
+
"claude-desktop", "cline", "zed",
|
|
52
|
+
"opencode", "gemini-cli", "roo-code", "amazon-q",
|
|
53
|
+
"copilot-cli", "junie", "kiro", "trae", "firebase",
|
|
54
|
+
"amp", "continue", "hermes", "goose", "gsd",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
#: Targets that only support manual setup instructions.
|
|
58
|
+
MANUAL_TARGETS: tuple[str, ...] = ("intellij",)
|
|
59
|
+
|
|
60
|
+
#: All recognised target names.
|
|
61
|
+
ALL_TARGETS: tuple[str, ...] = AUTO_INSTALL_TARGETS + MANUAL_TARGETS
|
|
62
|
+
|
|
63
|
+
#: Friendly string for error messages.
|
|
64
|
+
ALL_TARGETS_STR: str = ", ".join(ALL_TARGETS)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ResolvedInstallSpec:
|
|
69
|
+
"""Normalized read-only view of an installed MCP target config."""
|
|
70
|
+
|
|
71
|
+
target: str
|
|
72
|
+
scope: str
|
|
73
|
+
config_path: str | None
|
|
74
|
+
command: str
|
|
75
|
+
args: list[str]
|
|
76
|
+
env: dict[str, str]
|
|
77
|
+
source_kind: str
|
|
78
|
+
unsupported_reason: str | None = None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_supported(self) -> bool:
|
|
82
|
+
return self.unsupported_reason is None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_json_file(path: Path) -> dict[str, Any] | None:
|
|
86
|
+
try:
|
|
87
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
+
except (json.JSONDecodeError, OSError):
|
|
89
|
+
return None
|
|
90
|
+
return raw if isinstance(raw, dict) else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_toml_file(path: Path) -> dict[str, Any] | None:
|
|
94
|
+
try:
|
|
95
|
+
raw = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
96
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
97
|
+
return None
|
|
98
|
+
return raw if isinstance(raw, dict) else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_yaml_file(path: Path) -> dict[str, Any] | None:
|
|
102
|
+
import yaml
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
106
|
+
except (yaml.YAMLError, OSError):
|
|
107
|
+
return None
|
|
108
|
+
return raw if isinstance(raw, dict) else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _normalize_standard_entry(
|
|
112
|
+
target: str,
|
|
113
|
+
scope: str,
|
|
114
|
+
config_path: Path,
|
|
115
|
+
entry: dict[str, Any],
|
|
116
|
+
*,
|
|
117
|
+
source_kind: str,
|
|
118
|
+
) -> ResolvedInstallSpec | None:
|
|
119
|
+
command = entry.get("command")
|
|
120
|
+
args = entry.get("args", [])
|
|
121
|
+
env = entry.get("env", {})
|
|
122
|
+
if not isinstance(command, str):
|
|
123
|
+
return None
|
|
124
|
+
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
|
|
125
|
+
return None
|
|
126
|
+
if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
|
|
127
|
+
return None
|
|
128
|
+
return ResolvedInstallSpec(
|
|
129
|
+
target=target,
|
|
130
|
+
scope=scope,
|
|
131
|
+
config_path=str(config_path),
|
|
132
|
+
command=command,
|
|
133
|
+
args=list(args),
|
|
134
|
+
env=dict(env),
|
|
135
|
+
source_kind=source_kind,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _normalize_zed_entry(
|
|
140
|
+
target: str,
|
|
141
|
+
scope: str,
|
|
142
|
+
config_path: Path,
|
|
143
|
+
entry: dict[str, Any],
|
|
144
|
+
) -> ResolvedInstallSpec | None:
|
|
145
|
+
command_block = entry.get("command")
|
|
146
|
+
if not isinstance(command_block, dict):
|
|
147
|
+
return None
|
|
148
|
+
command = command_block.get("path")
|
|
149
|
+
args = command_block.get("args", [])
|
|
150
|
+
if not isinstance(command, str):
|
|
151
|
+
return None
|
|
152
|
+
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
|
|
153
|
+
return None
|
|
154
|
+
return ResolvedInstallSpec(
|
|
155
|
+
target=target,
|
|
156
|
+
scope=scope,
|
|
157
|
+
config_path=str(config_path),
|
|
158
|
+
command=command,
|
|
159
|
+
args=list(args),
|
|
160
|
+
env={},
|
|
161
|
+
source_kind="custom-json",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _normalize_opencode_entry(
|
|
166
|
+
target: str,
|
|
167
|
+
scope: str,
|
|
168
|
+
config_path: Path,
|
|
169
|
+
entry: dict[str, Any],
|
|
170
|
+
) -> ResolvedInstallSpec | None:
|
|
171
|
+
command_parts = entry.get("command")
|
|
172
|
+
if not isinstance(command_parts, list) or not command_parts or not all(isinstance(part, str) for part in command_parts):
|
|
173
|
+
return None
|
|
174
|
+
env = entry.get("env", {})
|
|
175
|
+
if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
|
|
176
|
+
env = {}
|
|
177
|
+
return ResolvedInstallSpec(
|
|
178
|
+
target=target,
|
|
179
|
+
scope=scope,
|
|
180
|
+
config_path=str(config_path),
|
|
181
|
+
command=command_parts[0],
|
|
182
|
+
args=list(command_parts[1:]),
|
|
183
|
+
env=dict(env),
|
|
184
|
+
source_kind="custom-json",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _normalize_goose_entry(
|
|
189
|
+
target: str,
|
|
190
|
+
scope: str,
|
|
191
|
+
config_path: Path,
|
|
192
|
+
entry: dict[str, Any],
|
|
193
|
+
) -> ResolvedInstallSpec | None:
|
|
194
|
+
import shlex
|
|
195
|
+
|
|
196
|
+
cmd = entry.get("cmd")
|
|
197
|
+
if not isinstance(cmd, str) or not cmd.strip():
|
|
198
|
+
return None
|
|
199
|
+
parts = shlex.split(cmd)
|
|
200
|
+
if not parts:
|
|
201
|
+
return None
|
|
202
|
+
envs = entry.get("envs", {})
|
|
203
|
+
if not isinstance(envs, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in envs.items()):
|
|
204
|
+
envs = {}
|
|
205
|
+
return ResolvedInstallSpec(
|
|
206
|
+
target=target,
|
|
207
|
+
scope=scope,
|
|
208
|
+
config_path=str(config_path),
|
|
209
|
+
command=parts[0],
|
|
210
|
+
args=parts[1:],
|
|
211
|
+
env=dict(envs),
|
|
212
|
+
source_kind="yaml",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _unsupported_spec(target: str, scope: str, reason: str) -> ResolvedInstallSpec:
|
|
217
|
+
return ResolvedInstallSpec(
|
|
218
|
+
target=target,
|
|
219
|
+
scope=scope,
|
|
220
|
+
config_path=None,
|
|
221
|
+
command="",
|
|
222
|
+
args=[],
|
|
223
|
+
env={},
|
|
224
|
+
source_kind="unsupported",
|
|
225
|
+
unsupported_reason=reason,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def resolve_install_spec(target: str, project_dir: str = ".", scope: str = "local") -> ResolvedInstallSpec | None:
|
|
230
|
+
"""Resolve the installed config for a target into a normalized spec.
|
|
231
|
+
|
|
232
|
+
Returns ``None`` when the target is supported but not currently installed.
|
|
233
|
+
Returns a ``ResolvedInstallSpec`` with ``unsupported_reason`` for targets
|
|
234
|
+
that are intentionally excluded from probing in the non-breaking phase.
|
|
235
|
+
"""
|
|
236
|
+
project_root = Path(project_dir)
|
|
237
|
+
|
|
238
|
+
if target == "claude":
|
|
239
|
+
return _unsupported_spec(
|
|
240
|
+
target,
|
|
241
|
+
scope,
|
|
242
|
+
"claude uses CLI-managed installs; probe-install only supports file-based targets in v1.",
|
|
243
|
+
)
|
|
244
|
+
if target == "intellij":
|
|
245
|
+
return _unsupported_spec(
|
|
246
|
+
target,
|
|
247
|
+
scope,
|
|
248
|
+
"intellij is manual-only; probe-install only supports file-based targets in v1.",
|
|
249
|
+
)
|
|
250
|
+
if target not in ALL_TARGETS:
|
|
251
|
+
return _unsupported_spec(target, scope, f"Unknown target '{target}'.")
|
|
252
|
+
|
|
253
|
+
if target in _JSON_CONFIG_TARGETS:
|
|
254
|
+
config_dirs = {
|
|
255
|
+
"cursor": ".cursor",
|
|
256
|
+
"windsurf": ".windsurf",
|
|
257
|
+
"vscode": ".vscode",
|
|
258
|
+
"roo-code": ".roo",
|
|
259
|
+
"trae": ".trae",
|
|
260
|
+
"kiro": os.path.join(".kiro", "settings"),
|
|
261
|
+
"firebase": ".idx",
|
|
262
|
+
"continue": ".continue",
|
|
263
|
+
}
|
|
264
|
+
config_path = project_root / config_dirs[target] / "mcp.json"
|
|
265
|
+
data = _read_json_file(config_path)
|
|
266
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
267
|
+
if not isinstance(entry, dict):
|
|
268
|
+
return None
|
|
269
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
270
|
+
|
|
271
|
+
if target == "codex":
|
|
272
|
+
config_path = Path.home() / ".codex" / "config.toml" if scope == "user" else project_root / ".codex" / "config.toml"
|
|
273
|
+
data = _read_toml_file(config_path)
|
|
274
|
+
entry = data.get("mcp_servers", {}).get("attocode-code-intel") if data else None
|
|
275
|
+
if not isinstance(entry, dict):
|
|
276
|
+
return None
|
|
277
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="toml")
|
|
278
|
+
|
|
279
|
+
if target == "claude-desktop":
|
|
280
|
+
config_dir = _get_user_config_dir("claude-desktop")
|
|
281
|
+
if config_dir is None:
|
|
282
|
+
return None
|
|
283
|
+
config_path = config_dir / "claude_desktop_config.json"
|
|
284
|
+
data = _read_json_file(config_path)
|
|
285
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
286
|
+
if not isinstance(entry, dict):
|
|
287
|
+
return None
|
|
288
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
289
|
+
|
|
290
|
+
if target == "cline":
|
|
291
|
+
config_dir = _get_user_config_dir("cline")
|
|
292
|
+
if config_dir is None:
|
|
293
|
+
return None
|
|
294
|
+
config_path = config_dir / "cline_mcp_settings.json"
|
|
295
|
+
data = _read_json_file(config_path)
|
|
296
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
297
|
+
if not isinstance(entry, dict):
|
|
298
|
+
return None
|
|
299
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
300
|
+
|
|
301
|
+
if target == "zed":
|
|
302
|
+
config_path = (
|
|
303
|
+
Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "zed" / "settings.json"
|
|
304
|
+
if scope == "user"
|
|
305
|
+
else project_root / ".zed" / "settings.json"
|
|
306
|
+
)
|
|
307
|
+
data = _read_json_file(config_path)
|
|
308
|
+
entry = data.get("context_servers", {}).get("attocode-code-intel") if data else None
|
|
309
|
+
if not isinstance(entry, dict):
|
|
310
|
+
return None
|
|
311
|
+
return _normalize_zed_entry(target, scope, config_path, entry)
|
|
312
|
+
|
|
313
|
+
if target == "opencode":
|
|
314
|
+
config_path = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "opencode" / "config.json"
|
|
315
|
+
data = _read_json_file(config_path)
|
|
316
|
+
entry = data.get("mcp", {}).get("attocode-code-intel") if data else None
|
|
317
|
+
if not isinstance(entry, dict):
|
|
318
|
+
return None
|
|
319
|
+
return _normalize_opencode_entry(target, scope, config_path, entry)
|
|
320
|
+
|
|
321
|
+
if target == "gemini-cli":
|
|
322
|
+
config_path = Path.home() / ".gemini" / "settings.json" if scope == "user" else project_root / ".gemini" / "settings.json"
|
|
323
|
+
data = _read_json_file(config_path)
|
|
324
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
325
|
+
if not isinstance(entry, dict):
|
|
326
|
+
return None
|
|
327
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
328
|
+
|
|
329
|
+
if target == "amazon-q":
|
|
330
|
+
config_path = Path.home() / ".aws" / "amazonq" / "mcp.json"
|
|
331
|
+
data = _read_json_file(config_path)
|
|
332
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
333
|
+
if not isinstance(entry, dict):
|
|
334
|
+
return None
|
|
335
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
336
|
+
|
|
337
|
+
if target == "copilot-cli":
|
|
338
|
+
config_path = Path.home() / ".copilot" / "mcp-config.json"
|
|
339
|
+
data = _read_json_file(config_path)
|
|
340
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
341
|
+
if not isinstance(entry, dict):
|
|
342
|
+
return None
|
|
343
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
344
|
+
|
|
345
|
+
if target == "junie":
|
|
346
|
+
config_path = Path.home() / ".junie" / "mcp" / "mcp.json" if scope == "user" else project_root / ".junie" / "mcp" / "mcp.json"
|
|
347
|
+
data = _read_json_file(config_path)
|
|
348
|
+
entry = data.get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
349
|
+
if not isinstance(entry, dict):
|
|
350
|
+
return None
|
|
351
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="json")
|
|
352
|
+
|
|
353
|
+
if target == "amp":
|
|
354
|
+
config_path = (
|
|
355
|
+
Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "amp" / "settings.json"
|
|
356
|
+
if scope == "user"
|
|
357
|
+
else project_root / ".amp" / "settings.json"
|
|
358
|
+
)
|
|
359
|
+
data = _read_json_file(config_path)
|
|
360
|
+
entry = data.get("amp", {}).get("mcpServers", {}).get("attocode-code-intel") if data else None
|
|
361
|
+
if not isinstance(entry, dict):
|
|
362
|
+
return None
|
|
363
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="custom-json")
|
|
364
|
+
|
|
365
|
+
if target == "hermes":
|
|
366
|
+
config_path = Path.home() / ".hermes" / "config.yaml"
|
|
367
|
+
data = _read_yaml_file(config_path)
|
|
368
|
+
entry = data.get("mcp_servers", {}).get("attocode-code-intel") if data else None
|
|
369
|
+
if not isinstance(entry, dict):
|
|
370
|
+
return None
|
|
371
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="yaml")
|
|
372
|
+
|
|
373
|
+
if target == "goose":
|
|
374
|
+
config_path = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "goose" / "config.yaml"
|
|
375
|
+
data = _read_yaml_file(config_path)
|
|
376
|
+
extensions = data.get("extensions", []) if data else []
|
|
377
|
+
if not isinstance(extensions, list):
|
|
378
|
+
return None
|
|
379
|
+
entry = next(
|
|
380
|
+
(
|
|
381
|
+
ext for ext in extensions
|
|
382
|
+
if isinstance(ext, dict) and ext.get("name") == "attocode-code-intel"
|
|
383
|
+
),
|
|
384
|
+
None,
|
|
385
|
+
)
|
|
386
|
+
if not isinstance(entry, dict):
|
|
387
|
+
return None
|
|
388
|
+
return _normalize_goose_entry(target, scope, config_path, entry)
|
|
389
|
+
|
|
390
|
+
if target == "gsd":
|
|
391
|
+
config_path = Path.home() / ".gsd" / "mcp.json" if scope == "user" else project_root / ".gsd" / "mcp.json"
|
|
392
|
+
data = _read_json_file(config_path)
|
|
393
|
+
entry = data.get("servers", {}).get("attocode-code-intel") if data else None
|
|
394
|
+
if not isinstance(entry, dict):
|
|
395
|
+
return None
|
|
396
|
+
return _normalize_standard_entry(target, scope, config_path, entry, source_kind="custom-json")
|
|
397
|
+
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _find_command(project_dir: str | None = None) -> str:
|
|
402
|
+
"""Determine the right command to invoke the MCP server.
|
|
403
|
+
|
|
404
|
+
Resolution order:
|
|
405
|
+
1. ``uv run attocode-code-intel`` when ``uv`` is available and the target
|
|
406
|
+
project's ``pyproject.toml`` declares the script — MCP hosts often spawn
|
|
407
|
+
subprocesses with a minimal PATH, so this is preferred for dev clones.
|
|
408
|
+
2. ``attocode-code-intel`` on PATH (pipx / global install).
|
|
409
|
+
3. ``python -m attocode_intel.entrypoint`` using the current interpreter. The
|
|
410
|
+
older ``attocode_intel.server`` lists all tools and has no profiles.
|
|
411
|
+
"""
|
|
412
|
+
base = Path(project_dir or ".").resolve()
|
|
413
|
+
pyproject = base / "pyproject.toml"
|
|
414
|
+
if shutil.which("uv") and pyproject.is_file():
|
|
415
|
+
try:
|
|
416
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
417
|
+
except OSError:
|
|
418
|
+
text = ""
|
|
419
|
+
if "attocode-code-intel" in text:
|
|
420
|
+
return "uv run attocode-code-intel"
|
|
421
|
+
|
|
422
|
+
if shutil.which("attocode-code-intel"):
|
|
423
|
+
return "attocode-code-intel"
|
|
424
|
+
|
|
425
|
+
return f"{sys.executable} -m attocode_intel.entrypoint"
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _build_server_entry(
|
|
429
|
+
project_dir: str | None,
|
|
430
|
+
*,
|
|
431
|
+
local_only: bool = True,
|
|
432
|
+
vscode_cursor_stdio: bool = False,
|
|
433
|
+
) -> dict:
|
|
434
|
+
"""Build the MCP server config dict for JSON/TOML-based configs.
|
|
435
|
+
|
|
436
|
+
By default, ``--local-only`` is included so the stdio MCP process does not
|
|
437
|
+
auto-load ``.attocode/config.toml`` remote settings (which often point at
|
|
438
|
+
``http://127.0.0.1:8080`` and break tools with connection refused when no
|
|
439
|
+
HTTP server is running). Pass ``local_only=False`` only if you intentionally
|
|
440
|
+
proxy MCP tools through a reachable remote code-intel API.
|
|
441
|
+
|
|
442
|
+
When ``vscode_cursor_stdio`` is True (Cursor, VS Code family, etc.), the
|
|
443
|
+
entry matches `Cursor MCP stdio requirements`_:
|
|
444
|
+
|
|
445
|
+
- ``"type": "stdio"`` (required for correct routing in Cursor)
|
|
446
|
+
- ``--project`` set to ``${workspaceFolder}`` so the subprocess always
|
|
447
|
+
indexes the folder that contains ``.cursor/mcp.json``, even when the
|
|
448
|
+
MCP host's cwd differs from the workspace root
|
|
449
|
+
- On macOS, ``PATH`` is prefixed with Homebrew locations so ``uv`` is
|
|
450
|
+
found when the host spawns the server with a minimal environment
|
|
451
|
+
|
|
452
|
+
.. _Cursor MCP stdio requirements: https://cursor.com/docs/mcp
|
|
453
|
+
"""
|
|
454
|
+
resolve_dir = project_dir if project_dir is not None else "."
|
|
455
|
+
cmd = _find_command(resolve_dir)
|
|
456
|
+
parts = cmd.split()
|
|
457
|
+
command = parts[0]
|
|
458
|
+
args = parts[1:]
|
|
459
|
+
if local_only:
|
|
460
|
+
args.append("--local-only")
|
|
461
|
+
if project_dir is not None:
|
|
462
|
+
if vscode_cursor_stdio:
|
|
463
|
+
args += ["--project", "${workspaceFolder}"]
|
|
464
|
+
else:
|
|
465
|
+
args += ["--project", os.path.abspath(project_dir)]
|
|
466
|
+
|
|
467
|
+
entry: dict = {
|
|
468
|
+
"command": command,
|
|
469
|
+
"args": args,
|
|
470
|
+
}
|
|
471
|
+
if vscode_cursor_stdio:
|
|
472
|
+
entry["type"] = "stdio"
|
|
473
|
+
if platform.system() == "Darwin":
|
|
474
|
+
entry["env"] = {
|
|
475
|
+
"PATH": "/opt/homebrew/bin:/usr/local/bin:${env:PATH}",
|
|
476
|
+
}
|
|
477
|
+
elif platform.system() != "Windows":
|
|
478
|
+
entry["env"] = {
|
|
479
|
+
"PATH": "/usr/local/bin:/usr/bin:${env:PATH}",
|
|
480
|
+
}
|
|
481
|
+
return entry
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def install_claude(project_dir: str = ".", scope: str = "local") -> bool:
|
|
485
|
+
"""Install into Claude Code via `claude mcp add`.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
project_dir: Path to the project to index.
|
|
489
|
+
scope: "local" (project) or "user" (global).
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
True if installation succeeded.
|
|
493
|
+
"""
|
|
494
|
+
if not shutil.which("claude"):
|
|
495
|
+
print("Error: `claude` CLI not found. Install Claude Code first.", file=sys.stderr)
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
cmd = _find_command(project_dir)
|
|
499
|
+
parts = cmd.split()
|
|
500
|
+
command = parts[0]
|
|
501
|
+
server_args = parts[1:]
|
|
502
|
+
server_args.append("--local-only")
|
|
503
|
+
|
|
504
|
+
# For local installs (or explicit --project), bake in the absolute path.
|
|
505
|
+
# For global installs without explicit --project, omit it so the server
|
|
506
|
+
# dynamically uses whatever directory Claude Code runs it from.
|
|
507
|
+
if scope != "user" or project_dir != ".":
|
|
508
|
+
server_args += ["--project", os.path.abspath(project_dir)]
|
|
509
|
+
|
|
510
|
+
claude_cmd = [
|
|
511
|
+
"claude", "mcp", "add",
|
|
512
|
+
"--transport", "stdio",
|
|
513
|
+
"--scope", scope,
|
|
514
|
+
"attocode-code-intel",
|
|
515
|
+
"--",
|
|
516
|
+
command,
|
|
517
|
+
*server_args,
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
result = subprocess.run(claude_cmd, capture_output=True, text=True, timeout=30)
|
|
522
|
+
if result.returncode == 0:
|
|
523
|
+
print(f"Installed attocode-code-intel into Claude Code (scope={scope})")
|
|
524
|
+
print(f" command: {command}")
|
|
525
|
+
print(f" args: {server_args}")
|
|
526
|
+
return True
|
|
527
|
+
else:
|
|
528
|
+
print(f"Error installing: {result.stderr.strip()}", file=sys.stderr)
|
|
529
|
+
return False
|
|
530
|
+
except FileNotFoundError:
|
|
531
|
+
print("Error: `claude` CLI not found.", file=sys.stderr)
|
|
532
|
+
return False
|
|
533
|
+
except subprocess.TimeoutExpired:
|
|
534
|
+
print("Error: `claude mcp add` timed out.", file=sys.stderr)
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def install_json_config(
|
|
539
|
+
target: str,
|
|
540
|
+
project_dir: str = ".",
|
|
541
|
+
) -> bool:
|
|
542
|
+
"""Install into a target that uses a project-scoped ``mcp.json`` file.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
target: One of the supported project-scoped JSON targets.
|
|
546
|
+
project_dir: Path to the project to index.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
True if installation succeeded.
|
|
550
|
+
"""
|
|
551
|
+
config_dirs = {
|
|
552
|
+
"cursor": ".cursor",
|
|
553
|
+
"windsurf": ".windsurf",
|
|
554
|
+
"vscode": ".vscode",
|
|
555
|
+
"roo-code": ".roo",
|
|
556
|
+
"trae": ".trae",
|
|
557
|
+
"kiro": os.path.join(".kiro", "settings"),
|
|
558
|
+
"firebase": ".idx",
|
|
559
|
+
"continue": ".continue",
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
config_dir = config_dirs.get(target)
|
|
563
|
+
if not config_dir:
|
|
564
|
+
print(f"Error: Unknown target '{target}'", file=sys.stderr)
|
|
565
|
+
return False
|
|
566
|
+
|
|
567
|
+
config_path = Path(project_dir) / config_dir / "mcp.json"
|
|
568
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
|
|
570
|
+
# Load existing config or start fresh
|
|
571
|
+
existing: dict = {}
|
|
572
|
+
if config_path.exists():
|
|
573
|
+
import contextlib
|
|
574
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
575
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
576
|
+
|
|
577
|
+
servers = existing.setdefault("mcpServers", {})
|
|
578
|
+
servers["attocode-code-intel"] = _build_server_entry(
|
|
579
|
+
project_dir,
|
|
580
|
+
vscode_cursor_stdio=True,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
config_path.write_text(
|
|
584
|
+
json.dumps(existing, indent=2) + "\n",
|
|
585
|
+
encoding="utf-8",
|
|
586
|
+
)
|
|
587
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
588
|
+
return True
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def uninstall_claude(scope: str = "local") -> bool:
|
|
592
|
+
"""Uninstall from Claude Code."""
|
|
593
|
+
if not shutil.which("claude"):
|
|
594
|
+
print("Error: `claude` CLI not found.", file=sys.stderr)
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
result = subprocess.run(
|
|
599
|
+
["claude", "mcp", "remove", "--scope", scope, "attocode-code-intel"],
|
|
600
|
+
capture_output=True, text=True, timeout=30,
|
|
601
|
+
)
|
|
602
|
+
if result.returncode == 0:
|
|
603
|
+
print(f"Removed attocode-code-intel from Claude Code (scope={scope})")
|
|
604
|
+
return True
|
|
605
|
+
else:
|
|
606
|
+
print(f"Error removing: {result.stderr.strip()}", file=sys.stderr)
|
|
607
|
+
return False
|
|
608
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
609
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
610
|
+
return False
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def uninstall_json_config(target: str, project_dir: str = ".") -> bool:
|
|
614
|
+
"""Uninstall from a project-scoped ``mcp.json`` target."""
|
|
615
|
+
config_dirs = {
|
|
616
|
+
"cursor": ".cursor",
|
|
617
|
+
"windsurf": ".windsurf",
|
|
618
|
+
"vscode": ".vscode",
|
|
619
|
+
"roo-code": ".roo",
|
|
620
|
+
"trae": ".trae",
|
|
621
|
+
"kiro": os.path.join(".kiro", "settings"),
|
|
622
|
+
"firebase": ".idx",
|
|
623
|
+
"continue": ".continue",
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
config_dir = config_dirs.get(target)
|
|
627
|
+
if not config_dir:
|
|
628
|
+
print(f"Error: Unknown target '{target}'", file=sys.stderr)
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
config_path = Path(project_dir) / config_dir / "mcp.json"
|
|
632
|
+
if not config_path.exists():
|
|
633
|
+
print(f"No config found at {config_path}")
|
|
634
|
+
return True
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
638
|
+
except (json.JSONDecodeError, OSError):
|
|
639
|
+
return True
|
|
640
|
+
|
|
641
|
+
servers = existing.get("mcpServers", {})
|
|
642
|
+
if "attocode-code-intel" in servers:
|
|
643
|
+
del servers["attocode-code-intel"]
|
|
644
|
+
config_path.write_text(
|
|
645
|
+
json.dumps(existing, indent=2) + "\n",
|
|
646
|
+
encoding="utf-8",
|
|
647
|
+
)
|
|
648
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
649
|
+
|
|
650
|
+
return True
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def install_codex(project_dir: str = ".", scope: str = "local") -> bool:
|
|
654
|
+
"""Install into OpenAI Codex CLI via `.codex/config.toml`.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
project_dir: Path to the project to index.
|
|
658
|
+
scope: "local" (project) or "user" (global ~/.codex/).
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
True if installation succeeded.
|
|
662
|
+
"""
|
|
663
|
+
if scope == "user":
|
|
664
|
+
config_path = Path.home() / ".codex" / "config.toml"
|
|
665
|
+
else:
|
|
666
|
+
config_path = Path(project_dir) / ".codex" / "config.toml"
|
|
667
|
+
|
|
668
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
669
|
+
|
|
670
|
+
# Load existing config or start fresh
|
|
671
|
+
existing: dict = {}
|
|
672
|
+
if config_path.exists():
|
|
673
|
+
import contextlib
|
|
674
|
+
with contextlib.suppress(tomllib.TOMLDecodeError, OSError):
|
|
675
|
+
existing = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
676
|
+
|
|
677
|
+
omit_project = scope == "user" and project_dir == "."
|
|
678
|
+
entry = _build_server_entry(None if omit_project else project_dir)
|
|
679
|
+
servers = existing.setdefault("mcp_servers", {})
|
|
680
|
+
servers["attocode-code-intel"] = {
|
|
681
|
+
"command": entry["command"],
|
|
682
|
+
"args": entry["args"],
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
config_path.write_text(
|
|
686
|
+
tomli_w.dumps(existing),
|
|
687
|
+
encoding="utf-8",
|
|
688
|
+
)
|
|
689
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
690
|
+
return True
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def uninstall_codex(project_dir: str = ".", scope: str = "local") -> bool:
|
|
694
|
+
"""Uninstall from OpenAI Codex CLI."""
|
|
695
|
+
if scope == "user":
|
|
696
|
+
config_path = Path.home() / ".codex" / "config.toml"
|
|
697
|
+
else:
|
|
698
|
+
config_path = Path(project_dir) / ".codex" / "config.toml"
|
|
699
|
+
|
|
700
|
+
if not config_path.exists():
|
|
701
|
+
print(f"No config found at {config_path}")
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
existing = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
706
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
707
|
+
return True
|
|
708
|
+
|
|
709
|
+
servers = existing.get("mcp_servers", {})
|
|
710
|
+
if "attocode-code-intel" in servers:
|
|
711
|
+
del servers["attocode-code-intel"]
|
|
712
|
+
config_path.write_text(
|
|
713
|
+
tomli_w.dumps(existing),
|
|
714
|
+
encoding="utf-8",
|
|
715
|
+
)
|
|
716
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
717
|
+
|
|
718
|
+
return True
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
# Platform-specific config path resolver
|
|
723
|
+
# ---------------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _get_user_config_dir(app: str) -> Path | None:
|
|
727
|
+
"""Return the platform-specific user config directory for *app*.
|
|
728
|
+
|
|
729
|
+
Supported apps:
|
|
730
|
+
- ``"claude-desktop"`` — Claude Desktop's config dir
|
|
731
|
+
- ``"cline"`` — Cline extension globalStorage inside VS Code
|
|
732
|
+
|
|
733
|
+
Returns ``None`` for unsupported (app, platform) combinations.
|
|
734
|
+
"""
|
|
735
|
+
system = platform.system() # "Darwin", "Linux", "Windows"
|
|
736
|
+
|
|
737
|
+
if app == "claude-desktop":
|
|
738
|
+
if system == "Darwin":
|
|
739
|
+
return Path.home() / "Library" / "Application Support" / "Claude"
|
|
740
|
+
if system == "Linux":
|
|
741
|
+
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "Claude"
|
|
742
|
+
if system == "Windows":
|
|
743
|
+
appdata = os.environ.get("APPDATA", "")
|
|
744
|
+
if appdata:
|
|
745
|
+
return Path(appdata) / "Claude"
|
|
746
|
+
return None
|
|
747
|
+
|
|
748
|
+
if app == "cline":
|
|
749
|
+
if system == "Darwin":
|
|
750
|
+
return (
|
|
751
|
+
Path.home()
|
|
752
|
+
/ "Library"
|
|
753
|
+
/ "Application Support"
|
|
754
|
+
/ "Code"
|
|
755
|
+
/ "User"
|
|
756
|
+
/ "globalStorage"
|
|
757
|
+
/ "saoudrizwan.claude-dev"
|
|
758
|
+
)
|
|
759
|
+
if system == "Linux":
|
|
760
|
+
return (
|
|
761
|
+
Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
762
|
+
/ "Code"
|
|
763
|
+
/ "User"
|
|
764
|
+
/ "globalStorage"
|
|
765
|
+
/ "saoudrizwan.claude-dev"
|
|
766
|
+
)
|
|
767
|
+
if system == "Windows":
|
|
768
|
+
appdata = os.environ.get("APPDATA", "")
|
|
769
|
+
if appdata:
|
|
770
|
+
return (
|
|
771
|
+
Path(appdata)
|
|
772
|
+
/ "Code"
|
|
773
|
+
/ "User"
|
|
774
|
+
/ "globalStorage"
|
|
775
|
+
/ "saoudrizwan.claude-dev"
|
|
776
|
+
)
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
return None
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# ---------------------------------------------------------------------------
|
|
783
|
+
# Claude Desktop
|
|
784
|
+
# ---------------------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def install_claude_desktop(project_dir: str = ".") -> bool:
|
|
788
|
+
"""Install into Claude Desktop via ``claude_desktop_config.json``.
|
|
789
|
+
|
|
790
|
+
Returns True if installation succeeded.
|
|
791
|
+
"""
|
|
792
|
+
config_dir = _get_user_config_dir("claude-desktop")
|
|
793
|
+
if config_dir is None:
|
|
794
|
+
print(
|
|
795
|
+
f"Error: Claude Desktop config path not supported on {platform.system()}",
|
|
796
|
+
file=sys.stderr,
|
|
797
|
+
)
|
|
798
|
+
return False
|
|
799
|
+
|
|
800
|
+
config_path = config_dir / "claude_desktop_config.json"
|
|
801
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
802
|
+
|
|
803
|
+
existing: dict = {}
|
|
804
|
+
if config_path.exists():
|
|
805
|
+
import contextlib
|
|
806
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
807
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
808
|
+
|
|
809
|
+
# Omit --project when no explicit project was given, so the server
|
|
810
|
+
# dynamically uses whatever directory the host runs it from.
|
|
811
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
812
|
+
servers = existing.setdefault("mcpServers", {})
|
|
813
|
+
servers["attocode-code-intel"] = _build_server_entry(effective_dir)
|
|
814
|
+
|
|
815
|
+
config_path.write_text(
|
|
816
|
+
json.dumps(existing, indent=2) + "\n",
|
|
817
|
+
encoding="utf-8",
|
|
818
|
+
)
|
|
819
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
820
|
+
return True
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def uninstall_claude_desktop() -> bool:
|
|
824
|
+
"""Uninstall from Claude Desktop."""
|
|
825
|
+
config_dir = _get_user_config_dir("claude-desktop")
|
|
826
|
+
if config_dir is None:
|
|
827
|
+
print(
|
|
828
|
+
f"Error: Claude Desktop config path not supported on {platform.system()}",
|
|
829
|
+
file=sys.stderr,
|
|
830
|
+
)
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
config_path = config_dir / "claude_desktop_config.json"
|
|
834
|
+
if not config_path.exists():
|
|
835
|
+
print(f"No config found at {config_path}")
|
|
836
|
+
return True
|
|
837
|
+
|
|
838
|
+
try:
|
|
839
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
840
|
+
except (json.JSONDecodeError, OSError):
|
|
841
|
+
return True
|
|
842
|
+
|
|
843
|
+
servers = existing.get("mcpServers", {})
|
|
844
|
+
if "attocode-code-intel" in servers:
|
|
845
|
+
del servers["attocode-code-intel"]
|
|
846
|
+
config_path.write_text(
|
|
847
|
+
json.dumps(existing, indent=2) + "\n",
|
|
848
|
+
encoding="utf-8",
|
|
849
|
+
)
|
|
850
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
851
|
+
|
|
852
|
+
return True
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# ---------------------------------------------------------------------------
|
|
856
|
+
# Cline (VS Code extension)
|
|
857
|
+
# ---------------------------------------------------------------------------
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def install_cline(project_dir: str = ".") -> bool:
|
|
861
|
+
"""Install into Cline via ``cline_mcp_settings.json``.
|
|
862
|
+
|
|
863
|
+
Returns True if installation succeeded.
|
|
864
|
+
"""
|
|
865
|
+
config_dir = _get_user_config_dir("cline")
|
|
866
|
+
if config_dir is None:
|
|
867
|
+
print(
|
|
868
|
+
f"Error: Cline config path not supported on {platform.system()}",
|
|
869
|
+
file=sys.stderr,
|
|
870
|
+
)
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
config_path = config_dir / "cline_mcp_settings.json"
|
|
874
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
875
|
+
|
|
876
|
+
existing: dict = {}
|
|
877
|
+
if config_path.exists():
|
|
878
|
+
import contextlib
|
|
879
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
880
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
881
|
+
|
|
882
|
+
# Omit --project when no explicit project was given, so the server
|
|
883
|
+
# dynamically uses whatever directory the host runs it from.
|
|
884
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
885
|
+
servers = existing.setdefault("mcpServers", {})
|
|
886
|
+
servers["attocode-code-intel"] = _build_server_entry(effective_dir)
|
|
887
|
+
|
|
888
|
+
config_path.write_text(
|
|
889
|
+
json.dumps(existing, indent=2) + "\n",
|
|
890
|
+
encoding="utf-8",
|
|
891
|
+
)
|
|
892
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
893
|
+
return True
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def uninstall_cline() -> bool:
|
|
897
|
+
"""Uninstall from Cline."""
|
|
898
|
+
config_dir = _get_user_config_dir("cline")
|
|
899
|
+
if config_dir is None:
|
|
900
|
+
print(
|
|
901
|
+
f"Error: Cline config path not supported on {platform.system()}",
|
|
902
|
+
file=sys.stderr,
|
|
903
|
+
)
|
|
904
|
+
return False
|
|
905
|
+
|
|
906
|
+
config_path = config_dir / "cline_mcp_settings.json"
|
|
907
|
+
if not config_path.exists():
|
|
908
|
+
print(f"No config found at {config_path}")
|
|
909
|
+
return True
|
|
910
|
+
|
|
911
|
+
try:
|
|
912
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
913
|
+
except (json.JSONDecodeError, OSError):
|
|
914
|
+
return True
|
|
915
|
+
|
|
916
|
+
servers = existing.get("mcpServers", {})
|
|
917
|
+
if "attocode-code-intel" in servers:
|
|
918
|
+
del servers["attocode-code-intel"]
|
|
919
|
+
config_path.write_text(
|
|
920
|
+
json.dumps(existing, indent=2) + "\n",
|
|
921
|
+
encoding="utf-8",
|
|
922
|
+
)
|
|
923
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
924
|
+
|
|
925
|
+
return True
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
# ---------------------------------------------------------------------------
|
|
929
|
+
# Zed
|
|
930
|
+
# ---------------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _build_zed_server_entry(project_dir: str) -> dict:
|
|
934
|
+
"""Build the MCP server config dict for Zed's ``context_servers`` format.
|
|
935
|
+
|
|
936
|
+
Zed uses a nested ``{"command": {"path": ..., "args": [...]}}`` schema.
|
|
937
|
+
"""
|
|
938
|
+
cmd = _find_command(project_dir)
|
|
939
|
+
parts = cmd.split()
|
|
940
|
+
command = parts[0]
|
|
941
|
+
args = parts[1:] + [
|
|
942
|
+
"--local-only",
|
|
943
|
+
"--project",
|
|
944
|
+
os.path.abspath(project_dir),
|
|
945
|
+
]
|
|
946
|
+
return {
|
|
947
|
+
"command": {
|
|
948
|
+
"path": command,
|
|
949
|
+
"args": args,
|
|
950
|
+
},
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def install_zed(project_dir: str = ".", scope: str = "local") -> bool:
|
|
955
|
+
"""Install into Zed via ``settings.json``.
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
project_dir: Path to the project to index.
|
|
959
|
+
scope: "local" writes to ``.zed/settings.json`` (project),
|
|
960
|
+
"user" writes to ``~/.config/zed/settings.json``.
|
|
961
|
+
|
|
962
|
+
Returns True if installation succeeded.
|
|
963
|
+
"""
|
|
964
|
+
if scope == "user":
|
|
965
|
+
config_path = Path(
|
|
966
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
967
|
+
) / "zed" / "settings.json"
|
|
968
|
+
else:
|
|
969
|
+
config_path = Path(project_dir) / ".zed" / "settings.json"
|
|
970
|
+
|
|
971
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
972
|
+
|
|
973
|
+
existing: dict = {}
|
|
974
|
+
if config_path.exists():
|
|
975
|
+
import contextlib
|
|
976
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
977
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
978
|
+
|
|
979
|
+
servers = existing.setdefault("context_servers", {})
|
|
980
|
+
servers["attocode-code-intel"] = _build_zed_server_entry(project_dir)
|
|
981
|
+
|
|
982
|
+
config_path.write_text(
|
|
983
|
+
json.dumps(existing, indent=2) + "\n",
|
|
984
|
+
encoding="utf-8",
|
|
985
|
+
)
|
|
986
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
987
|
+
return True
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def uninstall_zed(project_dir: str = ".", scope: str = "local") -> bool:
|
|
991
|
+
"""Uninstall from Zed."""
|
|
992
|
+
if scope == "user":
|
|
993
|
+
config_path = Path(
|
|
994
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
995
|
+
) / "zed" / "settings.json"
|
|
996
|
+
else:
|
|
997
|
+
config_path = Path(project_dir) / ".zed" / "settings.json"
|
|
998
|
+
|
|
999
|
+
if not config_path.exists():
|
|
1000
|
+
print(f"No config found at {config_path}")
|
|
1001
|
+
return True
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1005
|
+
except (json.JSONDecodeError, OSError):
|
|
1006
|
+
return True
|
|
1007
|
+
|
|
1008
|
+
servers = existing.get("context_servers", {})
|
|
1009
|
+
if "attocode-code-intel" in servers:
|
|
1010
|
+
del servers["attocode-code-intel"]
|
|
1011
|
+
config_path.write_text(
|
|
1012
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1013
|
+
encoding="utf-8",
|
|
1014
|
+
)
|
|
1015
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1016
|
+
|
|
1017
|
+
return True
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
# ---------------------------------------------------------------------------
|
|
1021
|
+
# OpenCode — ~/.config/opencode/config.json
|
|
1022
|
+
# ---------------------------------------------------------------------------
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def install_opencode(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1026
|
+
"""Install into OpenCode via ``~/.config/opencode/config.json``.
|
|
1027
|
+
|
|
1028
|
+
OpenCode uses ``"mcp"`` as the top-level key (not ``"mcpServers"``),
|
|
1029
|
+
entries require ``"type": "local"`` and ``"command"`` as a single array.
|
|
1030
|
+
"""
|
|
1031
|
+
config_path = Path(
|
|
1032
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1033
|
+
) / "opencode" / "config.json"
|
|
1034
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1035
|
+
|
|
1036
|
+
existing: dict = {}
|
|
1037
|
+
if config_path.exists():
|
|
1038
|
+
import contextlib
|
|
1039
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1040
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1041
|
+
|
|
1042
|
+
# Omit --project when no explicit project was given, so the server
|
|
1043
|
+
# dynamically uses whatever directory the host runs it from.
|
|
1044
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
1045
|
+
entry = _build_server_entry(effective_dir)
|
|
1046
|
+
|
|
1047
|
+
# OpenCode format: "mcp" key, "type": "local", "command" as single array.
|
|
1048
|
+
servers = existing.setdefault("mcp", {})
|
|
1049
|
+
servers["attocode-code-intel"] = {
|
|
1050
|
+
"type": "local",
|
|
1051
|
+
"command": [entry["command"]] + entry["args"],
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
config_path.write_text(
|
|
1055
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1056
|
+
encoding="utf-8",
|
|
1057
|
+
)
|
|
1058
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1059
|
+
return True
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def uninstall_opencode() -> bool:
|
|
1063
|
+
"""Uninstall from OpenCode."""
|
|
1064
|
+
config_path = Path(
|
|
1065
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1066
|
+
) / "opencode" / "config.json"
|
|
1067
|
+
|
|
1068
|
+
if not config_path.exists():
|
|
1069
|
+
print(f"No config found at {config_path}")
|
|
1070
|
+
return True
|
|
1071
|
+
|
|
1072
|
+
try:
|
|
1073
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1074
|
+
except (json.JSONDecodeError, OSError):
|
|
1075
|
+
return True
|
|
1076
|
+
|
|
1077
|
+
servers = existing.get("mcp", {})
|
|
1078
|
+
if "attocode-code-intel" in servers:
|
|
1079
|
+
del servers["attocode-code-intel"]
|
|
1080
|
+
config_path.write_text(
|
|
1081
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1082
|
+
encoding="utf-8",
|
|
1083
|
+
)
|
|
1084
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1085
|
+
|
|
1086
|
+
return True
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
# ---------------------------------------------------------------------------
|
|
1090
|
+
# Gemini CLI — .gemini/settings.json (project) or ~/.gemini/settings.json
|
|
1091
|
+
# ---------------------------------------------------------------------------
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def install_gemini(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1095
|
+
"""Install into Gemini CLI via ``settings.json``.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
project_dir: Path to the project to index.
|
|
1099
|
+
scope: "local" (project) or "user" (global).
|
|
1100
|
+
"""
|
|
1101
|
+
if scope == "user":
|
|
1102
|
+
config_path = Path.home() / ".gemini" / "settings.json"
|
|
1103
|
+
else:
|
|
1104
|
+
config_path = Path(project_dir) / ".gemini" / "settings.json"
|
|
1105
|
+
|
|
1106
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1107
|
+
|
|
1108
|
+
existing: dict = {}
|
|
1109
|
+
if config_path.exists():
|
|
1110
|
+
import contextlib
|
|
1111
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1112
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1113
|
+
|
|
1114
|
+
servers = existing.setdefault("mcpServers", {})
|
|
1115
|
+
servers["attocode-code-intel"] = _build_server_entry(project_dir)
|
|
1116
|
+
|
|
1117
|
+
config_path.write_text(
|
|
1118
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1119
|
+
encoding="utf-8",
|
|
1120
|
+
)
|
|
1121
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1122
|
+
return True
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def uninstall_gemini(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1126
|
+
"""Uninstall from Gemini CLI."""
|
|
1127
|
+
if scope == "user":
|
|
1128
|
+
config_path = Path.home() / ".gemini" / "settings.json"
|
|
1129
|
+
else:
|
|
1130
|
+
config_path = Path(project_dir) / ".gemini" / "settings.json"
|
|
1131
|
+
|
|
1132
|
+
if not config_path.exists():
|
|
1133
|
+
print(f"No config found at {config_path}")
|
|
1134
|
+
return True
|
|
1135
|
+
|
|
1136
|
+
try:
|
|
1137
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1138
|
+
except (json.JSONDecodeError, OSError):
|
|
1139
|
+
return True
|
|
1140
|
+
|
|
1141
|
+
servers = existing.get("mcpServers", {})
|
|
1142
|
+
if "attocode-code-intel" in servers:
|
|
1143
|
+
del servers["attocode-code-intel"]
|
|
1144
|
+
config_path.write_text(
|
|
1145
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1146
|
+
encoding="utf-8",
|
|
1147
|
+
)
|
|
1148
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1149
|
+
|
|
1150
|
+
return True
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# ---------------------------------------------------------------------------
|
|
1154
|
+
# Amazon Q Developer — ~/.aws/amazonq/mcp.json
|
|
1155
|
+
# ---------------------------------------------------------------------------
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def install_amazonq(project_dir: str = ".") -> bool:
|
|
1159
|
+
"""Install into Amazon Q Developer via ``~/.aws/amazonq/mcp.json``."""
|
|
1160
|
+
config_path = Path.home() / ".aws" / "amazonq" / "mcp.json"
|
|
1161
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1162
|
+
|
|
1163
|
+
existing: dict = {}
|
|
1164
|
+
if config_path.exists():
|
|
1165
|
+
import contextlib
|
|
1166
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1167
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1168
|
+
|
|
1169
|
+
# Omit --project when no explicit project was given, so the server
|
|
1170
|
+
# dynamically uses whatever directory the host runs it from.
|
|
1171
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
1172
|
+
servers = existing.setdefault("mcpServers", {})
|
|
1173
|
+
servers["attocode-code-intel"] = _build_server_entry(effective_dir)
|
|
1174
|
+
|
|
1175
|
+
config_path.write_text(
|
|
1176
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1177
|
+
encoding="utf-8",
|
|
1178
|
+
)
|
|
1179
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1180
|
+
return True
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def uninstall_amazonq() -> bool:
|
|
1184
|
+
"""Uninstall from Amazon Q Developer."""
|
|
1185
|
+
config_path = Path.home() / ".aws" / "amazonq" / "mcp.json"
|
|
1186
|
+
|
|
1187
|
+
if not config_path.exists():
|
|
1188
|
+
print(f"No config found at {config_path}")
|
|
1189
|
+
return True
|
|
1190
|
+
|
|
1191
|
+
try:
|
|
1192
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1193
|
+
except (json.JSONDecodeError, OSError):
|
|
1194
|
+
return True
|
|
1195
|
+
|
|
1196
|
+
servers = existing.get("mcpServers", {})
|
|
1197
|
+
if "attocode-code-intel" in servers:
|
|
1198
|
+
del servers["attocode-code-intel"]
|
|
1199
|
+
config_path.write_text(
|
|
1200
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1201
|
+
encoding="utf-8",
|
|
1202
|
+
)
|
|
1203
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1204
|
+
|
|
1205
|
+
return True
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
# ---------------------------------------------------------------------------
|
|
1209
|
+
# GitHub Copilot CLI — ~/.copilot/mcp-config.json
|
|
1210
|
+
# ---------------------------------------------------------------------------
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def install_copilot_cli(project_dir: str = ".") -> bool:
|
|
1214
|
+
"""Install into GitHub Copilot CLI via ``~/.copilot/mcp-config.json``."""
|
|
1215
|
+
config_path = Path.home() / ".copilot" / "mcp-config.json"
|
|
1216
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1217
|
+
|
|
1218
|
+
existing: dict = {}
|
|
1219
|
+
if config_path.exists():
|
|
1220
|
+
import contextlib
|
|
1221
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1222
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1223
|
+
|
|
1224
|
+
# Omit --project when no explicit project was given, so the server
|
|
1225
|
+
# dynamically uses whatever directory the host runs it from.
|
|
1226
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
1227
|
+
servers = existing.setdefault("mcpServers", {})
|
|
1228
|
+
servers["attocode-code-intel"] = _build_server_entry(effective_dir)
|
|
1229
|
+
|
|
1230
|
+
config_path.write_text(
|
|
1231
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1232
|
+
encoding="utf-8",
|
|
1233
|
+
)
|
|
1234
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1235
|
+
return True
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def uninstall_copilot_cli() -> bool:
|
|
1239
|
+
"""Uninstall from GitHub Copilot CLI."""
|
|
1240
|
+
config_path = Path.home() / ".copilot" / "mcp-config.json"
|
|
1241
|
+
|
|
1242
|
+
if not config_path.exists():
|
|
1243
|
+
print(f"No config found at {config_path}")
|
|
1244
|
+
return True
|
|
1245
|
+
|
|
1246
|
+
try:
|
|
1247
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1248
|
+
except (json.JSONDecodeError, OSError):
|
|
1249
|
+
return True
|
|
1250
|
+
|
|
1251
|
+
servers = existing.get("mcpServers", {})
|
|
1252
|
+
if "attocode-code-intel" in servers:
|
|
1253
|
+
del servers["attocode-code-intel"]
|
|
1254
|
+
config_path.write_text(
|
|
1255
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1256
|
+
encoding="utf-8",
|
|
1257
|
+
)
|
|
1258
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1259
|
+
|
|
1260
|
+
return True
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
# ---------------------------------------------------------------------------
|
|
1264
|
+
# Junie (JetBrains) — .junie/mcp/mcp.json or ~/.junie/mcp/mcp.json
|
|
1265
|
+
# ---------------------------------------------------------------------------
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def install_junie(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1269
|
+
"""Install into Junie via ``mcp.json``.
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
project_dir: Path to the project to index.
|
|
1273
|
+
scope: "local" (project) or "user" (global).
|
|
1274
|
+
"""
|
|
1275
|
+
if scope == "user":
|
|
1276
|
+
config_path = Path.home() / ".junie" / "mcp" / "mcp.json"
|
|
1277
|
+
else:
|
|
1278
|
+
config_path = Path(project_dir) / ".junie" / "mcp" / "mcp.json"
|
|
1279
|
+
|
|
1280
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1281
|
+
|
|
1282
|
+
existing: dict = {}
|
|
1283
|
+
if config_path.exists():
|
|
1284
|
+
import contextlib
|
|
1285
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1286
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1287
|
+
|
|
1288
|
+
servers = existing.setdefault("mcpServers", {})
|
|
1289
|
+
servers["attocode-code-intel"] = _build_server_entry(project_dir)
|
|
1290
|
+
|
|
1291
|
+
config_path.write_text(
|
|
1292
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1293
|
+
encoding="utf-8",
|
|
1294
|
+
)
|
|
1295
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1296
|
+
return True
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def uninstall_junie(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1300
|
+
"""Uninstall from Junie."""
|
|
1301
|
+
if scope == "user":
|
|
1302
|
+
config_path = Path.home() / ".junie" / "mcp" / "mcp.json"
|
|
1303
|
+
else:
|
|
1304
|
+
config_path = Path(project_dir) / ".junie" / "mcp" / "mcp.json"
|
|
1305
|
+
|
|
1306
|
+
if not config_path.exists():
|
|
1307
|
+
print(f"No config found at {config_path}")
|
|
1308
|
+
return True
|
|
1309
|
+
|
|
1310
|
+
try:
|
|
1311
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1312
|
+
except (json.JSONDecodeError, OSError):
|
|
1313
|
+
return True
|
|
1314
|
+
|
|
1315
|
+
servers = existing.get("mcpServers", {})
|
|
1316
|
+
if "attocode-code-intel" in servers:
|
|
1317
|
+
del servers["attocode-code-intel"]
|
|
1318
|
+
config_path.write_text(
|
|
1319
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1320
|
+
encoding="utf-8",
|
|
1321
|
+
)
|
|
1322
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1323
|
+
|
|
1324
|
+
return True
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
# ---------------------------------------------------------------------------
|
|
1328
|
+
# Amp (Sourcegraph) — .amp/settings.json or ~/.config/amp/settings.json
|
|
1329
|
+
# Key is nested under "amp" → "mcpServers"
|
|
1330
|
+
# ---------------------------------------------------------------------------
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def install_amp(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1334
|
+
"""Install into Amp (Sourcegraph) via ``settings.json``.
|
|
1335
|
+
|
|
1336
|
+
Amp uses a nested ``amp.mcpServers`` key, not a top-level ``mcpServers``.
|
|
1337
|
+
|
|
1338
|
+
Args:
|
|
1339
|
+
project_dir: Path to the project to index.
|
|
1340
|
+
scope: "local" (project) or "user" (global).
|
|
1341
|
+
"""
|
|
1342
|
+
if scope == "user":
|
|
1343
|
+
config_path = Path(
|
|
1344
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1345
|
+
) / "amp" / "settings.json"
|
|
1346
|
+
else:
|
|
1347
|
+
config_path = Path(project_dir) / ".amp" / "settings.json"
|
|
1348
|
+
|
|
1349
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1350
|
+
|
|
1351
|
+
existing: dict = {}
|
|
1352
|
+
if config_path.exists():
|
|
1353
|
+
import contextlib
|
|
1354
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1355
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1356
|
+
|
|
1357
|
+
amp_section = existing.setdefault("amp", {})
|
|
1358
|
+
servers = amp_section.setdefault("mcpServers", {})
|
|
1359
|
+
servers["attocode-code-intel"] = _build_server_entry(project_dir)
|
|
1360
|
+
|
|
1361
|
+
config_path.write_text(
|
|
1362
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1363
|
+
encoding="utf-8",
|
|
1364
|
+
)
|
|
1365
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1366
|
+
return True
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def uninstall_amp(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1370
|
+
"""Uninstall from Amp."""
|
|
1371
|
+
if scope == "user":
|
|
1372
|
+
config_path = Path(
|
|
1373
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1374
|
+
) / "amp" / "settings.json"
|
|
1375
|
+
else:
|
|
1376
|
+
config_path = Path(project_dir) / ".amp" / "settings.json"
|
|
1377
|
+
|
|
1378
|
+
if not config_path.exists():
|
|
1379
|
+
print(f"No config found at {config_path}")
|
|
1380
|
+
return True
|
|
1381
|
+
|
|
1382
|
+
try:
|
|
1383
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1384
|
+
except (json.JSONDecodeError, OSError):
|
|
1385
|
+
return True
|
|
1386
|
+
|
|
1387
|
+
servers = existing.get("amp", {}).get("mcpServers", {})
|
|
1388
|
+
if "attocode-code-intel" in servers:
|
|
1389
|
+
del servers["attocode-code-intel"]
|
|
1390
|
+
config_path.write_text(
|
|
1391
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1392
|
+
encoding="utf-8",
|
|
1393
|
+
)
|
|
1394
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1395
|
+
|
|
1396
|
+
return True
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
# ---------------------------------------------------------------------------
|
|
1400
|
+
# Hermes Agent (NousResearch) — ~/.hermes/config.yaml, key "mcp_servers"
|
|
1401
|
+
# ---------------------------------------------------------------------------
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def install_hermes(project_dir: str = ".") -> bool:
|
|
1405
|
+
"""Install into Hermes Agent via ``~/.hermes/config.yaml``."""
|
|
1406
|
+
import yaml
|
|
1407
|
+
|
|
1408
|
+
config_path = Path.home() / ".hermes" / "config.yaml"
|
|
1409
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1410
|
+
|
|
1411
|
+
existing: dict = {}
|
|
1412
|
+
if config_path.exists():
|
|
1413
|
+
import contextlib
|
|
1414
|
+
with contextlib.suppress(yaml.YAMLError, OSError):
|
|
1415
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
1416
|
+
if isinstance(raw, dict):
|
|
1417
|
+
existing = raw
|
|
1418
|
+
|
|
1419
|
+
# Omit --project when no explicit project was given, so the server
|
|
1420
|
+
# dynamically uses whatever directory the host runs it from.
|
|
1421
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
1422
|
+
servers = existing.setdefault("mcp_servers", {})
|
|
1423
|
+
entry = _build_server_entry(effective_dir)
|
|
1424
|
+
servers["attocode-code-intel"] = {
|
|
1425
|
+
"command": entry["command"],
|
|
1426
|
+
"args": entry["args"],
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
config_path.write_text(
|
|
1430
|
+
yaml.safe_dump(existing, default_flow_style=False, sort_keys=False),
|
|
1431
|
+
encoding="utf-8",
|
|
1432
|
+
)
|
|
1433
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1434
|
+
return True
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def uninstall_hermes() -> bool:
|
|
1438
|
+
"""Uninstall from Hermes Agent."""
|
|
1439
|
+
import yaml
|
|
1440
|
+
|
|
1441
|
+
config_path = Path.home() / ".hermes" / "config.yaml"
|
|
1442
|
+
|
|
1443
|
+
if not config_path.exists():
|
|
1444
|
+
print(f"No config found at {config_path}")
|
|
1445
|
+
return True
|
|
1446
|
+
|
|
1447
|
+
try:
|
|
1448
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
1449
|
+
if not isinstance(raw, dict):
|
|
1450
|
+
return True
|
|
1451
|
+
except (yaml.YAMLError, OSError):
|
|
1452
|
+
return True
|
|
1453
|
+
|
|
1454
|
+
servers = raw.get("mcp_servers", {})
|
|
1455
|
+
if "attocode-code-intel" in servers:
|
|
1456
|
+
del servers["attocode-code-intel"]
|
|
1457
|
+
config_path.write_text(
|
|
1458
|
+
yaml.safe_dump(raw, default_flow_style=False, sort_keys=False),
|
|
1459
|
+
encoding="utf-8",
|
|
1460
|
+
)
|
|
1461
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1462
|
+
|
|
1463
|
+
return True
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
# ---------------------------------------------------------------------------
|
|
1467
|
+
# Goose (Block) — ~/.config/goose/config.yaml, key "extensions"
|
|
1468
|
+
# Uses different schema: cmd, envs, type, name, timeout
|
|
1469
|
+
# ---------------------------------------------------------------------------
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def _build_goose_extension_entry(project_dir: str | None) -> dict:
|
|
1473
|
+
"""Build a Goose extension entry (different schema from standard MCP)."""
|
|
1474
|
+
entry = _build_server_entry(project_dir)
|
|
1475
|
+
cmd_str = entry["command"]
|
|
1476
|
+
if entry["args"]:
|
|
1477
|
+
cmd_str += " " + " ".join(entry["args"])
|
|
1478
|
+
return {
|
|
1479
|
+
"name": "attocode-code-intel",
|
|
1480
|
+
"type": "stdio",
|
|
1481
|
+
"cmd": cmd_str,
|
|
1482
|
+
"envs": {},
|
|
1483
|
+
"timeout": 300,
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def install_goose(project_dir: str = ".") -> bool:
|
|
1488
|
+
"""Install into Goose (Block) via ``~/.config/goose/config.yaml``."""
|
|
1489
|
+
import yaml
|
|
1490
|
+
|
|
1491
|
+
config_path = Path(
|
|
1492
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1493
|
+
) / "goose" / "config.yaml"
|
|
1494
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1495
|
+
|
|
1496
|
+
existing: dict = {}
|
|
1497
|
+
if config_path.exists():
|
|
1498
|
+
import contextlib
|
|
1499
|
+
with contextlib.suppress(yaml.YAMLError, OSError):
|
|
1500
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
1501
|
+
if isinstance(raw, dict):
|
|
1502
|
+
existing = raw
|
|
1503
|
+
|
|
1504
|
+
extensions = existing.setdefault("extensions", [])
|
|
1505
|
+
|
|
1506
|
+
# Remove existing entry if present
|
|
1507
|
+
extensions[:] = [
|
|
1508
|
+
ext for ext in extensions
|
|
1509
|
+
if not (isinstance(ext, dict) and ext.get("name") == "attocode-code-intel")
|
|
1510
|
+
]
|
|
1511
|
+
# Omit --project when no explicit project was given, so the server
|
|
1512
|
+
# dynamically uses whatever directory the host runs it from.
|
|
1513
|
+
effective_dir = None if project_dir == "." else project_dir
|
|
1514
|
+
extensions.append(_build_goose_extension_entry(effective_dir))
|
|
1515
|
+
|
|
1516
|
+
config_path.write_text(
|
|
1517
|
+
yaml.safe_dump(existing, default_flow_style=False, sort_keys=False),
|
|
1518
|
+
encoding="utf-8",
|
|
1519
|
+
)
|
|
1520
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1521
|
+
return True
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def uninstall_goose() -> bool:
|
|
1525
|
+
"""Uninstall from Goose."""
|
|
1526
|
+
import yaml
|
|
1527
|
+
|
|
1528
|
+
config_path = Path(
|
|
1529
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
1530
|
+
) / "goose" / "config.yaml"
|
|
1531
|
+
|
|
1532
|
+
if not config_path.exists():
|
|
1533
|
+
print(f"No config found at {config_path}")
|
|
1534
|
+
return True
|
|
1535
|
+
|
|
1536
|
+
try:
|
|
1537
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
1538
|
+
if not isinstance(raw, dict):
|
|
1539
|
+
return True
|
|
1540
|
+
except (yaml.YAMLError, OSError):
|
|
1541
|
+
return True
|
|
1542
|
+
|
|
1543
|
+
extensions = raw.get("extensions", [])
|
|
1544
|
+
filtered = [
|
|
1545
|
+
ext for ext in extensions
|
|
1546
|
+
if not (isinstance(ext, dict) and ext.get("name") == "attocode-code-intel")
|
|
1547
|
+
]
|
|
1548
|
+
if len(filtered) < len(extensions):
|
|
1549
|
+
raw["extensions"] = filtered
|
|
1550
|
+
config_path.write_text(
|
|
1551
|
+
yaml.safe_dump(raw, default_flow_style=False, sort_keys=False),
|
|
1552
|
+
encoding="utf-8",
|
|
1553
|
+
)
|
|
1554
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1555
|
+
|
|
1556
|
+
return True
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
# ---------------------------------------------------------------------------
|
|
1560
|
+
# GSD — .gsd/mcp.json (project) or ~/.gsd/mcp.json (global)
|
|
1561
|
+
# ---------------------------------------------------------------------------
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
def install_gsd(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1565
|
+
"""Install into GSD via ``mcp.json``.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
project_dir: Path to the project to index.
|
|
1569
|
+
scope: "local" (project) or "user" (global ~/.gsd/).
|
|
1570
|
+
"""
|
|
1571
|
+
if scope == "user":
|
|
1572
|
+
config_path = Path.home() / ".gsd" / "mcp.json"
|
|
1573
|
+
else:
|
|
1574
|
+
config_path = Path(project_dir) / ".gsd" / "mcp.json"
|
|
1575
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1576
|
+
|
|
1577
|
+
existing: dict = {}
|
|
1578
|
+
if config_path.exists():
|
|
1579
|
+
import contextlib
|
|
1580
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1581
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1582
|
+
|
|
1583
|
+
effective_dir = None if project_dir == "." and scope == "user" else project_dir
|
|
1584
|
+
entry = _build_server_entry(effective_dir)
|
|
1585
|
+
|
|
1586
|
+
servers = existing.setdefault("servers", {})
|
|
1587
|
+
servers["attocode-code-intel"] = entry
|
|
1588
|
+
|
|
1589
|
+
config_path.write_text(
|
|
1590
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1591
|
+
encoding="utf-8",
|
|
1592
|
+
)
|
|
1593
|
+
print(f"Installed attocode-code-intel into {config_path}")
|
|
1594
|
+
return True
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def uninstall_gsd(project_dir: str = ".", scope: str = "local") -> bool:
|
|
1598
|
+
"""Uninstall from GSD."""
|
|
1599
|
+
if scope == "user":
|
|
1600
|
+
config_path = Path.home() / ".gsd" / "mcp.json"
|
|
1601
|
+
else:
|
|
1602
|
+
config_path = Path(project_dir) / ".gsd" / "mcp.json"
|
|
1603
|
+
|
|
1604
|
+
if not config_path.exists():
|
|
1605
|
+
print(f"No config found at {config_path}")
|
|
1606
|
+
return True
|
|
1607
|
+
|
|
1608
|
+
try:
|
|
1609
|
+
existing = json.loads(config_path.read_text(encoding="utf-8"))
|
|
1610
|
+
except (json.JSONDecodeError, OSError):
|
|
1611
|
+
return True
|
|
1612
|
+
|
|
1613
|
+
servers = existing.get("servers", {})
|
|
1614
|
+
if "attocode-code-intel" in servers:
|
|
1615
|
+
del servers["attocode-code-intel"]
|
|
1616
|
+
config_path.write_text(
|
|
1617
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1618
|
+
encoding="utf-8",
|
|
1619
|
+
)
|
|
1620
|
+
print(f"Removed attocode-code-intel from {config_path}")
|
|
1621
|
+
|
|
1622
|
+
return True
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
# ---------------------------------------------------------------------------
|
|
1626
|
+
# Manual instruction targets (IntelliJ only)
|
|
1627
|
+
# ---------------------------------------------------------------------------
|
|
1628
|
+
|
|
1629
|
+
_MANUAL_INSTRUCTIONS: dict[str, str] = {
|
|
1630
|
+
"intellij": (
|
|
1631
|
+
"IntelliJ IDEA does not support file-based MCP configuration.\n"
|
|
1632
|
+
"\n"
|
|
1633
|
+
"To set up attocode-code-intel in IntelliJ:\n"
|
|
1634
|
+
" 1. Open Settings > Tools > AI Assistant > MCP Servers\n"
|
|
1635
|
+
" 2. Click '+' to add a new server\n"
|
|
1636
|
+
" 3. Set transport to 'stdio'\n"
|
|
1637
|
+
" 4. Set command to: {command}\n"
|
|
1638
|
+
" 5. Set arguments to: {args}\n"
|
|
1639
|
+
" 6. Click OK and restart the AI Assistant\n"
|
|
1640
|
+
),
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def print_manual_instructions(target: str, project_dir: str = ".") -> bool:
|
|
1645
|
+
"""Print manual setup instructions for targets without file-based config.
|
|
1646
|
+
|
|
1647
|
+
Returns True (always succeeds — just prints instructions).
|
|
1648
|
+
"""
|
|
1649
|
+
template = _MANUAL_INSTRUCTIONS.get(target)
|
|
1650
|
+
if template is None:
|
|
1651
|
+
print(f"Error: No manual instructions for '{target}'", file=sys.stderr)
|
|
1652
|
+
return False
|
|
1653
|
+
|
|
1654
|
+
entry = _build_server_entry(project_dir)
|
|
1655
|
+
print(
|
|
1656
|
+
template.format(
|
|
1657
|
+
command=entry["command"],
|
|
1658
|
+
args=" ".join(entry["args"]),
|
|
1659
|
+
args_json=json.dumps(entry["args"]),
|
|
1660
|
+
)
|
|
1661
|
+
)
|
|
1662
|
+
return True
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
# ---------------------------------------------------------------------------
|
|
1666
|
+
# Dispatch
|
|
1667
|
+
# ---------------------------------------------------------------------------
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
#: Targets handled by the generic ``install_json_config()`` function.
|
|
1671
|
+
_JSON_CONFIG_TARGETS = ("cursor", "windsurf", "vscode", "roo-code", "trae", "kiro", "firebase", "continue")
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def install(target: str, project_dir: str = ".", scope: str = "local") -> bool:
|
|
1675
|
+
"""Install into the given target.
|
|
1676
|
+
|
|
1677
|
+
Args:
|
|
1678
|
+
target: One of :data:`ALL_TARGETS`.
|
|
1679
|
+
project_dir: Path to the project to index.
|
|
1680
|
+
scope: For Claude/Codex/Zed/Gemini/Junie/Amp/OpenCode: "local" or "user".
|
|
1681
|
+
Ignored for others.
|
|
1682
|
+
"""
|
|
1683
|
+
if target == "claude":
|
|
1684
|
+
return install_claude(project_dir, scope=scope)
|
|
1685
|
+
elif target in _JSON_CONFIG_TARGETS:
|
|
1686
|
+
return install_json_config(target, project_dir)
|
|
1687
|
+
elif target == "codex":
|
|
1688
|
+
return install_codex(project_dir, scope=scope)
|
|
1689
|
+
elif target == "claude-desktop":
|
|
1690
|
+
return install_claude_desktop(project_dir)
|
|
1691
|
+
elif target == "cline":
|
|
1692
|
+
return install_cline(project_dir)
|
|
1693
|
+
elif target == "zed":
|
|
1694
|
+
return install_zed(project_dir, scope=scope)
|
|
1695
|
+
elif target == "opencode":
|
|
1696
|
+
return install_opencode(project_dir, scope=scope)
|
|
1697
|
+
elif target == "gemini-cli":
|
|
1698
|
+
return install_gemini(project_dir, scope=scope)
|
|
1699
|
+
elif target == "amazon-q":
|
|
1700
|
+
return install_amazonq(project_dir)
|
|
1701
|
+
elif target == "copilot-cli":
|
|
1702
|
+
return install_copilot_cli(project_dir)
|
|
1703
|
+
elif target == "junie":
|
|
1704
|
+
return install_junie(project_dir, scope=scope)
|
|
1705
|
+
elif target == "amp":
|
|
1706
|
+
return install_amp(project_dir, scope=scope)
|
|
1707
|
+
elif target == "hermes":
|
|
1708
|
+
return install_hermes(project_dir)
|
|
1709
|
+
elif target == "goose":
|
|
1710
|
+
return install_goose(project_dir)
|
|
1711
|
+
elif target == "gsd":
|
|
1712
|
+
return install_gsd(project_dir, scope=scope)
|
|
1713
|
+
elif target in MANUAL_TARGETS:
|
|
1714
|
+
return print_manual_instructions(target, project_dir)
|
|
1715
|
+
else:
|
|
1716
|
+
print(
|
|
1717
|
+
f"Error: Unknown target '{target}'. Use: {ALL_TARGETS_STR}",
|
|
1718
|
+
file=sys.stderr,
|
|
1719
|
+
)
|
|
1720
|
+
return False
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def uninstall(target: str, project_dir: str = ".", scope: str = "local") -> bool:
|
|
1724
|
+
"""Uninstall from the given target."""
|
|
1725
|
+
if target == "claude":
|
|
1726
|
+
return uninstall_claude(scope=scope)
|
|
1727
|
+
elif target in _JSON_CONFIG_TARGETS:
|
|
1728
|
+
return uninstall_json_config(target, project_dir)
|
|
1729
|
+
elif target == "codex":
|
|
1730
|
+
return uninstall_codex(project_dir, scope=scope)
|
|
1731
|
+
elif target == "claude-desktop":
|
|
1732
|
+
return uninstall_claude_desktop()
|
|
1733
|
+
elif target == "cline":
|
|
1734
|
+
return uninstall_cline()
|
|
1735
|
+
elif target == "zed":
|
|
1736
|
+
return uninstall_zed(project_dir, scope=scope)
|
|
1737
|
+
elif target == "opencode":
|
|
1738
|
+
return uninstall_opencode()
|
|
1739
|
+
elif target == "gemini-cli":
|
|
1740
|
+
return uninstall_gemini(project_dir, scope=scope)
|
|
1741
|
+
elif target == "amazon-q":
|
|
1742
|
+
return uninstall_amazonq()
|
|
1743
|
+
elif target == "copilot-cli":
|
|
1744
|
+
return uninstall_copilot_cli()
|
|
1745
|
+
elif target == "junie":
|
|
1746
|
+
return uninstall_junie(project_dir, scope=scope)
|
|
1747
|
+
elif target == "amp":
|
|
1748
|
+
return uninstall_amp(project_dir, scope=scope)
|
|
1749
|
+
elif target == "hermes":
|
|
1750
|
+
return uninstall_hermes()
|
|
1751
|
+
elif target == "goose":
|
|
1752
|
+
return uninstall_goose()
|
|
1753
|
+
elif target == "gsd":
|
|
1754
|
+
return uninstall_gsd(project_dir, scope=scope)
|
|
1755
|
+
elif target in MANUAL_TARGETS:
|
|
1756
|
+
print(f"Nothing to uninstall — {target} uses manual configuration.")
|
|
1757
|
+
return True
|
|
1758
|
+
else:
|
|
1759
|
+
print(
|
|
1760
|
+
f"Error: Unknown target '{target}'. Use: {ALL_TARGETS_STR}",
|
|
1761
|
+
file=sys.stderr,
|
|
1762
|
+
)
|
|
1763
|
+
return False
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
# ---------------------------------------------------------------------------
|
|
1767
|
+
# Hooks installation (Claude Code PostToolUse)
|
|
1768
|
+
# ---------------------------------------------------------------------------
|
|
1769
|
+
|
|
1770
|
+
#: The hook matcher pattern — matches file-editing tools.
|
|
1771
|
+
_HOOK_MATCHER = "Edit|Write|NotebookEdit"
|
|
1772
|
+
|
|
1773
|
+
#: Tag used to identify our hooks in settings.local.json.
|
|
1774
|
+
_HOOK_TAG = "attocode-code-intel"
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
def _build_hook_config() -> dict:
|
|
1778
|
+
"""Build the Claude Code PostToolUse hook configuration."""
|
|
1779
|
+
return {
|
|
1780
|
+
"matcher": _HOOK_MATCHER,
|
|
1781
|
+
"hooks": [
|
|
1782
|
+
{
|
|
1783
|
+
"type": "command",
|
|
1784
|
+
"command": "attocode-code-intel notify --stdin",
|
|
1785
|
+
},
|
|
1786
|
+
],
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def install_hooks(target: str, project_dir: str = ".") -> bool:
|
|
1791
|
+
"""Install PostToolUse hooks for automatic index updates.
|
|
1792
|
+
|
|
1793
|
+
Currently only supported for Claude Code (``claude`` target).
|
|
1794
|
+
Writes hooks to ``.claude/settings.local.json``.
|
|
1795
|
+
|
|
1796
|
+
Args:
|
|
1797
|
+
target: Installation target (only "claude" is supported).
|
|
1798
|
+
project_dir: Project directory.
|
|
1799
|
+
|
|
1800
|
+
Returns:
|
|
1801
|
+
True if hooks were installed.
|
|
1802
|
+
"""
|
|
1803
|
+
if target != "claude":
|
|
1804
|
+
print(
|
|
1805
|
+
f"Hooks not supported for {target}. "
|
|
1806
|
+
"The file watcher handles automatic updates. "
|
|
1807
|
+
"Alternatively, have your agent call the `notify_file_changed` "
|
|
1808
|
+
"MCP tool after edits.",
|
|
1809
|
+
)
|
|
1810
|
+
return False
|
|
1811
|
+
|
|
1812
|
+
settings_path = Path(project_dir) / ".claude" / "settings.local.json"
|
|
1813
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1814
|
+
|
|
1815
|
+
existing: dict = {}
|
|
1816
|
+
if settings_path.exists():
|
|
1817
|
+
import contextlib
|
|
1818
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
1819
|
+
existing = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
1820
|
+
|
|
1821
|
+
hooks = existing.setdefault("hooks", {})
|
|
1822
|
+
post_tool_use = hooks.setdefault("PostToolUse", [])
|
|
1823
|
+
|
|
1824
|
+
# Remove any existing attocode hooks (handles old formats too)
|
|
1825
|
+
filtered = [
|
|
1826
|
+
entry for entry in post_tool_use
|
|
1827
|
+
if not any(_HOOK_TAG in h.get("command", "") for h in entry.get("hooks", []))
|
|
1828
|
+
]
|
|
1829
|
+
|
|
1830
|
+
expected = _build_hook_config()
|
|
1831
|
+
|
|
1832
|
+
# Already up-to-date?
|
|
1833
|
+
if len(filtered) < len(post_tool_use) and expected in post_tool_use:
|
|
1834
|
+
print("Hooks already installed in .claude/settings.local.json")
|
|
1835
|
+
return True
|
|
1836
|
+
|
|
1837
|
+
hooks["PostToolUse"] = filtered + [expected]
|
|
1838
|
+
|
|
1839
|
+
settings_path.write_text(
|
|
1840
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1841
|
+
encoding="utf-8",
|
|
1842
|
+
)
|
|
1843
|
+
print(f"Installed PostToolUse hooks in {settings_path}")
|
|
1844
|
+
return True
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
def uninstall_hooks(target: str, project_dir: str = ".") -> bool:
|
|
1848
|
+
"""Remove attocode PostToolUse hooks.
|
|
1849
|
+
|
|
1850
|
+
Args:
|
|
1851
|
+
target: Installation target (only "claude" is supported).
|
|
1852
|
+
project_dir: Project directory.
|
|
1853
|
+
|
|
1854
|
+
Returns:
|
|
1855
|
+
True if hooks were removed (or didn't exist).
|
|
1856
|
+
"""
|
|
1857
|
+
if target != "claude":
|
|
1858
|
+
return True
|
|
1859
|
+
|
|
1860
|
+
settings_path = Path(project_dir) / ".claude" / "settings.local.json"
|
|
1861
|
+
if not settings_path.exists():
|
|
1862
|
+
return True
|
|
1863
|
+
|
|
1864
|
+
try:
|
|
1865
|
+
existing = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
1866
|
+
except (json.JSONDecodeError, OSError):
|
|
1867
|
+
return True
|
|
1868
|
+
|
|
1869
|
+
hooks = existing.get("hooks", {})
|
|
1870
|
+
post_tool_use = hooks.get("PostToolUse", [])
|
|
1871
|
+
if not post_tool_use:
|
|
1872
|
+
return True
|
|
1873
|
+
|
|
1874
|
+
# Remove entries that reference attocode-code-intel
|
|
1875
|
+
filtered = []
|
|
1876
|
+
removed = False
|
|
1877
|
+
for entry in post_tool_use:
|
|
1878
|
+
is_ours = False
|
|
1879
|
+
for h in entry.get("hooks", []):
|
|
1880
|
+
if _HOOK_TAG in h.get("command", ""):
|
|
1881
|
+
is_ours = True
|
|
1882
|
+
break
|
|
1883
|
+
if is_ours:
|
|
1884
|
+
removed = True
|
|
1885
|
+
else:
|
|
1886
|
+
filtered.append(entry)
|
|
1887
|
+
|
|
1888
|
+
if removed:
|
|
1889
|
+
hooks["PostToolUse"] = filtered
|
|
1890
|
+
settings_path.write_text(
|
|
1891
|
+
json.dumps(existing, indent=2) + "\n",
|
|
1892
|
+
encoding="utf-8",
|
|
1893
|
+
)
|
|
1894
|
+
print(f"Removed attocode hooks from {settings_path}")
|
|
1895
|
+
|
|
1896
|
+
return True
|