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,258 @@
|
|
|
1
|
+
"""Reversible client configuration and end-to-end installation diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import tomlkit
|
|
13
|
+
|
|
14
|
+
from attocode_intel.catalog import INSTRUCTIONS
|
|
15
|
+
from attocode_intel.project_dir import cache_folder
|
|
16
|
+
|
|
17
|
+
NAME = "attocode-code-intel"
|
|
18
|
+
REMOTE_NAME = NAME + "-remote"
|
|
19
|
+
START = "<!-- attocode-code-intel:start -->"
|
|
20
|
+
END = "<!-- attocode-code-intel:end -->"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def client_paths(client: str, project: Path, global_scope: bool):
|
|
24
|
+
home = Path.home()
|
|
25
|
+
if client == "codex":
|
|
26
|
+
base = home / ".codex" if global_scope else project / ".codex"
|
|
27
|
+
return (
|
|
28
|
+
base / "config.toml",
|
|
29
|
+
(base / "AGENTS.md" if global_scope else project / "AGENTS.md"),
|
|
30
|
+
"mcp_servers",
|
|
31
|
+
)
|
|
32
|
+
if client == "claude":
|
|
33
|
+
return (
|
|
34
|
+
(home / ".claude.json" if global_scope else project / ".mcp.json"),
|
|
35
|
+
(home / ".claude/CLAUDE.md" if global_scope else project / "CLAUDE.md"),
|
|
36
|
+
"mcpServers",
|
|
37
|
+
)
|
|
38
|
+
if client == "cursor":
|
|
39
|
+
return (
|
|
40
|
+
(home if global_scope else project) / ".cursor/mcp.json",
|
|
41
|
+
project / ".cursor/rules/attocode-code-intel.mdc",
|
|
42
|
+
"mcpServers",
|
|
43
|
+
)
|
|
44
|
+
raise ValueError(f"Unsupported client: {client}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def atomic_write(path: Path, text: str):
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
mode = path.stat().st_mode & 0o777 if path.exists() else 0o600
|
|
50
|
+
fd, temporary = tempfile.mkstemp(prefix=".intel-", dir=path.parent)
|
|
51
|
+
try:
|
|
52
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
53
|
+
stream.write(text)
|
|
54
|
+
stream.flush()
|
|
55
|
+
os.fsync(stream.fileno())
|
|
56
|
+
os.chmod(temporary, mode)
|
|
57
|
+
os.replace(temporary, path)
|
|
58
|
+
finally:
|
|
59
|
+
if os.path.exists(temporary):
|
|
60
|
+
os.unlink(temporary)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _instruction_text(path: Path, client: str, remove=False):
|
|
64
|
+
text = path.read_text() if path.exists() else ""
|
|
65
|
+
if (START in text) != (END in text):
|
|
66
|
+
raise ValueError(f"Incomplete managed instruction block in {path}")
|
|
67
|
+
block = f"{START}\n{INSTRUCTIONS}\n{END}"
|
|
68
|
+
if START in text:
|
|
69
|
+
first = text.index(START)
|
|
70
|
+
last = text.index(END, first) + len(END)
|
|
71
|
+
text = text[:first] + ("" if remove else block) + text[last:]
|
|
72
|
+
elif not remove:
|
|
73
|
+
if not text and client == "cursor":
|
|
74
|
+
text = "---\nalwaysApply: true\n---\n"
|
|
75
|
+
text = text.rstrip() + "\n\n" + block + "\n"
|
|
76
|
+
return text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def configure_client(
|
|
80
|
+
client,
|
|
81
|
+
project=".",
|
|
82
|
+
*,
|
|
83
|
+
global_scope=False,
|
|
84
|
+
profile="daily",
|
|
85
|
+
dry_run=False,
|
|
86
|
+
remove=False,
|
|
87
|
+
server="",
|
|
88
|
+
token_env="ATTOCODE_API_KEY",
|
|
89
|
+
repo="",
|
|
90
|
+
):
|
|
91
|
+
if repo and not server and not remove:
|
|
92
|
+
raise ValueError("Remote installation requires --server; use --remote only with doctor or --remove")
|
|
93
|
+
root = Path(project).resolve()
|
|
94
|
+
config, guidance, key = client_paths(client, root, global_scope)
|
|
95
|
+
entry_name = REMOTE_NAME if server or repo else NAME
|
|
96
|
+
state_path = config.parent / ".attocode-intelligence-install.json"
|
|
97
|
+
# The Claude project config sits in the project root, where the record would show in git status.
|
|
98
|
+
old_state = None
|
|
99
|
+
if config.parent == root:
|
|
100
|
+
old_state, state_path = state_path, root / ".attocode" / "claude-install.json"
|
|
101
|
+
source = state_path if state_path.exists() or not (old_state and old_state.exists()) else old_state
|
|
102
|
+
state = json.loads(source.read_text()) if source.exists() else {}
|
|
103
|
+
is_toml = config.suffix == ".toml"
|
|
104
|
+
if config.exists():
|
|
105
|
+
raw = config.read_text()
|
|
106
|
+
document = tomlkit.parse(raw) if is_toml else json.loads(raw)
|
|
107
|
+
if not isinstance(document, dict) and not is_toml:
|
|
108
|
+
raise ValueError(f"Expected an object in {config}")
|
|
109
|
+
else:
|
|
110
|
+
document = tomlkit.document() if is_toml else {}
|
|
111
|
+
servers = document.setdefault(key, {})
|
|
112
|
+
if remove:
|
|
113
|
+
saved = state.get(entry_name)
|
|
114
|
+
if saved and servers.get(entry_name) != saved["installed"]:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"{entry_name} was edited after installation; preserve those edits before removing it"
|
|
117
|
+
)
|
|
118
|
+
if saved and saved["previous"] is not None:
|
|
119
|
+
servers[entry_name] = saved["previous"]
|
|
120
|
+
else:
|
|
121
|
+
servers.pop(entry_name, None)
|
|
122
|
+
state.pop(entry_name, None)
|
|
123
|
+
elif server:
|
|
124
|
+
from urllib.parse import urlsplit
|
|
125
|
+
|
|
126
|
+
url = urlsplit(server)
|
|
127
|
+
if url.scheme not in ("http", "https") or not url.netloc or url.username or url.password:
|
|
128
|
+
raise ValueError("Server must be an HTTP(S) URL without embedded credentials")
|
|
129
|
+
endpoint = server.rstrip("/")
|
|
130
|
+
entry = {"url": endpoint + ("/" if endpoint.endswith("/mcp") else "/mcp/")}
|
|
131
|
+
if client == "claude":
|
|
132
|
+
entry["type"] = "http"
|
|
133
|
+
if client == "codex":
|
|
134
|
+
entry["bearer_token_env_var"] = token_env
|
|
135
|
+
else:
|
|
136
|
+
env_ref = "${" + token_env + "}" if client == "claude" else "${env:" + token_env + "}"
|
|
137
|
+
entry["headers"] = {"Authorization": "Bearer " + env_ref}
|
|
138
|
+
if repo:
|
|
139
|
+
entry.setdefault("http_headers" if client == "codex" else "headers", {})[
|
|
140
|
+
"X-Attocode-Workspace"
|
|
141
|
+
] = repo
|
|
142
|
+
entry.setdefault("http_headers" if client == "codex" else "headers", {})[
|
|
143
|
+
"X-Attocode-Profile"
|
|
144
|
+
] = profile
|
|
145
|
+
previous = state.get(entry_name, {}).get("previous", servers.get(entry_name))
|
|
146
|
+
state[entry_name] = {"previous": previous, "installed": entry}
|
|
147
|
+
servers[entry_name] = entry
|
|
148
|
+
else:
|
|
149
|
+
executable = Path(sys.executable).parent / NAME
|
|
150
|
+
command = str(executable) if executable.is_file() else None
|
|
151
|
+
entry = {
|
|
152
|
+
"command": command or sys.executable,
|
|
153
|
+
"args": ([] if command else ["-m", "attocode_intel.entrypoint"])
|
|
154
|
+
+ ["--profile", profile],
|
|
155
|
+
}
|
|
156
|
+
if not global_scope:
|
|
157
|
+
entry["args"] += ["--project", str(root)]
|
|
158
|
+
if client == "cursor":
|
|
159
|
+
entry["type"] = "stdio"
|
|
160
|
+
previous = state.get(entry_name, {}).get("previous", servers.get(entry_name))
|
|
161
|
+
state[entry_name] = {"previous": previous, "installed": entry}
|
|
162
|
+
servers[entry_name] = entry
|
|
163
|
+
output = tomlkit.dumps(document) if is_toml else json.dumps(document, indent=2) + "\n"
|
|
164
|
+
# Claude Code shows the server instructions to the model, so a CLAUDE.md copy is a duplicate
|
|
165
|
+
# in every request. Claude gets no managed block, and an old block is removed.
|
|
166
|
+
drop = client == "claude" or (remove and not any(name in servers for name in (NAME, REMOTE_NAME)))
|
|
167
|
+
instruction = _instruction_text(guidance, client, remove=drop)
|
|
168
|
+
# Prepare everything before writing either file; previews contain no credential values.
|
|
169
|
+
if dry_run:
|
|
170
|
+
return {
|
|
171
|
+
"config": str(config),
|
|
172
|
+
"entry": entry_name,
|
|
173
|
+
"configuration": servers.get(entry_name),
|
|
174
|
+
"guidance": str(guidance),
|
|
175
|
+
"instructions": instruction,
|
|
176
|
+
}
|
|
177
|
+
atomic_write(config, output)
|
|
178
|
+
if guidance.exists() or not (remove or drop):
|
|
179
|
+
atomic_write(guidance, instruction)
|
|
180
|
+
if old_state:
|
|
181
|
+
cache_folder(root)
|
|
182
|
+
atomic_write(state_path, json.dumps(state, indent=2) + "\n")
|
|
183
|
+
if old_state and old_state.exists():
|
|
184
|
+
old_state.unlink()
|
|
185
|
+
return {"client": client, "config": str(config), "guidance": str(guidance), "removed": remove}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def diagnose(client, project=".", *, global_scope=False, remote=False):
|
|
189
|
+
import httpx
|
|
190
|
+
from mcp import ClientSession, StdioServerParameters
|
|
191
|
+
from mcp.client.stdio import stdio_client
|
|
192
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
193
|
+
|
|
194
|
+
root = Path(project).resolve()
|
|
195
|
+
config, _, key = client_paths(client, root, global_scope)
|
|
196
|
+
name = REMOTE_NAME if remote else NAME
|
|
197
|
+
text = config.read_text() if config.exists() else ""
|
|
198
|
+
doc = (tomlkit.parse(text) if config.suffix == ".toml" else json.loads(text)) if text else {}
|
|
199
|
+
entry = doc.get(key, {}).get(name)
|
|
200
|
+
if not entry:
|
|
201
|
+
return {"client": client, "status": "not configured", "config": str(config),
|
|
202
|
+
"hint": f"Run: attocode-code-intel init --client {client}"}
|
|
203
|
+
|
|
204
|
+
async def probe(streams):
|
|
205
|
+
async with ClientSession(streams[0], streams[1]) as session:
|
|
206
|
+
await session.initialize()
|
|
207
|
+
catalog = await session.list_tools()
|
|
208
|
+
result = await session.call_tool(
|
|
209
|
+
"capabilities", {"max_tokens": 16000, **({"workspace": str(root)} if "url" not in entry else {})}
|
|
210
|
+
)
|
|
211
|
+
if result.isError:
|
|
212
|
+
raise RuntimeError(result.content)
|
|
213
|
+
payload = result.structuredContent
|
|
214
|
+
if payload is None:
|
|
215
|
+
payload = json.loads(next(item.text for item in result.content if item.type == "text"))
|
|
216
|
+
metadata = payload.get("metadata", {})
|
|
217
|
+
if "url" not in entry and metadata.get("workspace") != str(root):
|
|
218
|
+
raise RuntimeError("Server selected a different workspace")
|
|
219
|
+
query = await session.call_tool(
|
|
220
|
+
"project_summary", {"workspace": metadata["workspace"], "max_tokens": 512}
|
|
221
|
+
)
|
|
222
|
+
if query.isError:
|
|
223
|
+
raise RuntimeError("Bounded project query failed")
|
|
224
|
+
return {
|
|
225
|
+
"client": client,
|
|
226
|
+
"tools": len(catalog.tools),
|
|
227
|
+
"metadata": metadata,
|
|
228
|
+
"status": "ok",
|
|
229
|
+
"languages": payload.get("capabilities", payload.get("data", {})).get("languages", {}),
|
|
230
|
+
"precision_hint": "Optional installed language servers improve reference lookup. Restart the agent after setup; ATTOCODE_INTEL_PRECISION=off disables enrichment.",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async with asyncio.timeout(30):
|
|
234
|
+
if "url" in entry:
|
|
235
|
+
headers = dict(entry.get("http_headers", entry.get("headers", {})))
|
|
236
|
+
for name, value in headers.items():
|
|
237
|
+
for env_name, env_value in os.environ.items():
|
|
238
|
+
value = value.replace("${env:" + env_name + "}", env_value).replace(
|
|
239
|
+
"${" + env_name + "}", env_value
|
|
240
|
+
)
|
|
241
|
+
headers[name] = value
|
|
242
|
+
if entry.get("bearer_token_env_var"):
|
|
243
|
+
token = os.environ.get(entry["bearer_token_env_var"], "")
|
|
244
|
+
headers["Authorization"] = "Bearer " + token
|
|
245
|
+
async with (
|
|
246
|
+
httpx.AsyncClient(headers=headers) as http,
|
|
247
|
+
streamable_http_client(entry["url"], http_client=http) as streams,
|
|
248
|
+
):
|
|
249
|
+
return await probe(streams)
|
|
250
|
+
else:
|
|
251
|
+
params = StdioServerParameters(
|
|
252
|
+
command=entry["command"],
|
|
253
|
+
args=list(entry.get("args", [])),
|
|
254
|
+
env={**os.environ, **dict(entry.get("env", {}))},
|
|
255
|
+
cwd=str(root),
|
|
256
|
+
)
|
|
257
|
+
async with stdio_client(params) as streams:
|
|
258
|
+
return await probe(streams)
|
attocode_intel/output.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""A single output budget, including all composed sections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
from mcp import types
|
|
9
|
+
|
|
10
|
+
from attocode_intel._internal.integrations.utilities.token_estimate import count_tokens
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def bounded_text(text: str, max_tokens: int) -> tuple[str, bool]:
|
|
14
|
+
if max_tokens < 1:
|
|
15
|
+
raise ValueError("max_tokens must be positive")
|
|
16
|
+
if count_tokens(text) <= max_tokens:
|
|
17
|
+
return text, False
|
|
18
|
+
suffix = "\n[Truncated; narrow the query or increase max_tokens.]"
|
|
19
|
+
if count_tokens(suffix) >= max_tokens:
|
|
20
|
+
suffix = ""
|
|
21
|
+
lo, hi = 0, len(text)
|
|
22
|
+
while lo < hi:
|
|
23
|
+
mid = (lo + hi + 1) // 2
|
|
24
|
+
if count_tokens(text[:mid] + suffix) <= max_tokens:
|
|
25
|
+
lo = mid
|
|
26
|
+
else:
|
|
27
|
+
hi = mid - 1
|
|
28
|
+
return text[:lo] + suffix, True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compact_metadata(metadata: dict) -> dict:
|
|
32
|
+
"""Provenance is mandatory; detailed diagnostics live in capabilities."""
|
|
33
|
+
result = {key: metadata[key] for key in
|
|
34
|
+
("workspace", "workspaces", "warming_workspaces", "source", "revision",
|
|
35
|
+
"freshness", "truncated", "ranking")
|
|
36
|
+
if key in metadata}
|
|
37
|
+
coverage = metadata.get("coverage", {})
|
|
38
|
+
analysis = metadata.get("analysis", {})
|
|
39
|
+
if coverage:
|
|
40
|
+
result["index"] = {key: coverage[key] for key in
|
|
41
|
+
("phase", "parsed_files", "total_files", "discovery_truncated") if key in coverage}
|
|
42
|
+
if coverage.get("excluded_checkout_roots"):
|
|
43
|
+
result["index"]["excluded_checkout_roots_found"] = len(coverage["excluded_checkout_roots"])
|
|
44
|
+
if coverage.get("excluded_checkout_roots_truncated"):
|
|
45
|
+
result["index"]["excluded_checkout_roots_truncated"] = True
|
|
46
|
+
if analysis:
|
|
47
|
+
result["analysis"] = analysis.get("status", "partial")
|
|
48
|
+
result["absence_proven"] = False
|
|
49
|
+
result["precision"] = analysis.get("precision", {}).get("status", "not_requested")
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def compact_result(metadata: dict, data) -> types.CallToolResult:
|
|
54
|
+
# Text-only MCP works in clients that ignore structuredContent and avoids duplication
|
|
55
|
+
# in clients that include both representations in the model's context.
|
|
56
|
+
text = json.dumps({"metadata": metadata, "data": data}, ensure_ascii=False, separators=(",", ":"))
|
|
57
|
+
return types.CallToolResult(content=[types.TextContent(type="text", text=text)])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def response_tokens(result: types.CallToolResult) -> int:
|
|
61
|
+
return count_tokens(result.model_dump_json(exclude_none=True))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def bounded_bootstrap(metadata: dict, body: str, max_tokens: int) -> types.CallToolResult:
|
|
65
|
+
"""Fit whole bootstrap sections into the serialized MCP budget."""
|
|
66
|
+
if not body.startswith("## "):
|
|
67
|
+
return bounded_compact(metadata, {"result": body}, max_tokens)
|
|
68
|
+
sections = ["## " + section for section in body.removeprefix("## ").split("\n\n## ")]
|
|
69
|
+
compact = compact_metadata(metadata)
|
|
70
|
+
probe_metadata = {**compact, "truncated": True}
|
|
71
|
+
selected: list[str] = []
|
|
72
|
+
omitted_search = False
|
|
73
|
+
for section in sections:
|
|
74
|
+
candidate = "\n\n".join([*selected, section])
|
|
75
|
+
if response_tokens(compact_result(probe_metadata, {"result": candidate})) <= max_tokens:
|
|
76
|
+
selected.append(section)
|
|
77
|
+
elif section.startswith("## Relevant Code"):
|
|
78
|
+
# Search hits are indivisible evidence. Never let the generic string
|
|
79
|
+
# shortener turn a ranked result into a plausible-looking fragment.
|
|
80
|
+
omission = "## Relevant Code\nTop match omitted; increase max_tokens."
|
|
81
|
+
candidate = "\n\n".join([*selected, omission])
|
|
82
|
+
if response_tokens(compact_result(probe_metadata, {"result": candidate})) <= max_tokens:
|
|
83
|
+
selected.append(omission)
|
|
84
|
+
omitted_search = True
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError("max_tokens is too small to report omitted search evidence; increase it")
|
|
87
|
+
if not selected:
|
|
88
|
+
return bounded_compact(metadata, {"result": body}, max_tokens)
|
|
89
|
+
compact["truncated"] = compact.get("truncated", False) or omitted_search or len(selected) != len(sections)
|
|
90
|
+
return compact_result(compact, {"result": "\n\n".join(selected)})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def bounded_compact(metadata: dict, data, max_tokens: int) -> types.CallToolResult:
|
|
94
|
+
"""Bound the serialized result while retaining valid data and its provenance.
|
|
95
|
+
|
|
96
|
+
The estimate uses the shared tokenizer, not a promise about every client's tokenizer.
|
|
97
|
+
Only evidence collections and prose are shortened; paths, identifiers and flags survive.
|
|
98
|
+
"""
|
|
99
|
+
metadata = compact_metadata(metadata)
|
|
100
|
+
data = deepcopy(data)
|
|
101
|
+
omitted = {}
|
|
102
|
+
match data:
|
|
103
|
+
case {"results": list() as result_rows,
|
|
104
|
+
"ranking": {"candidate_pool_count": _, **ranking}}:
|
|
105
|
+
search_result_count = len(result_rows)
|
|
106
|
+
initial_search_omissions = ranking.get("omitted_results_due_to_budget", 0)
|
|
107
|
+
case _:
|
|
108
|
+
search_result_count = None
|
|
109
|
+
initial_search_omissions = 0
|
|
110
|
+
while True:
|
|
111
|
+
if search_result_count is not None:
|
|
112
|
+
delivered = len(data["results"])
|
|
113
|
+
if delivered < search_result_count:
|
|
114
|
+
data["ranking"]["delivered_count"] = delivered
|
|
115
|
+
data["ranking"]["omitted_results_due_to_budget"] = (
|
|
116
|
+
initial_search_omissions + search_result_count - delivered
|
|
117
|
+
)
|
|
118
|
+
metadata["ranking"] = deepcopy(data["ranking"])
|
|
119
|
+
metadata["truncated"] = True
|
|
120
|
+
# Budget trimming can shorten a source page after inspection constructed its
|
|
121
|
+
# continuation. Compute the next offset from the excerpt actually delivered.
|
|
122
|
+
if (isinstance(data, dict) and isinstance(data.get("follow_up"), dict) and "next_source" in data["follow_up"]
|
|
123
|
+
and "source" in data and "definition" in data):
|
|
124
|
+
source, definition = data["source"], data["definition"]
|
|
125
|
+
if source["end_line"] < source["start_line"]:
|
|
126
|
+
raise ValueError("max_tokens is too small to include a complete source line; increase it")
|
|
127
|
+
data["follow_up"]["next_source"] = ({
|
|
128
|
+
"tool": "inspect_symbol", "symbol_name": definition["qualified_name"],
|
|
129
|
+
"file_path": definition["file_path"], "line": definition["start_line"],
|
|
130
|
+
"source_start_line": source["end_line"] + 1,
|
|
131
|
+
} if source["end_line"] < definition["end_line"] else None)
|
|
132
|
+
result = compact_result(metadata, data)
|
|
133
|
+
if response_tokens(result) <= max_tokens:
|
|
134
|
+
return result
|
|
135
|
+
choices = []
|
|
136
|
+
|
|
137
|
+
def visit(value, parent=None, key=None, path="data", choices=choices):
|
|
138
|
+
if isinstance(value, list) and value:
|
|
139
|
+
# A partial match explanation would falsely imply that omitted
|
|
140
|
+
# query terms did not match. Drop whole hits before splitting it.
|
|
141
|
+
if key not in {"matched_terms", "match_fields"}:
|
|
142
|
+
choices.append((len(json.dumps(value)), parent, key, value, path))
|
|
143
|
+
elif isinstance(value, str) and (key in {"text", "snippet", "result", "details", "description"}):
|
|
144
|
+
if len(value) > 32:
|
|
145
|
+
choices.append((len(value), parent, key, value, path))
|
|
146
|
+
elif isinstance(value, dict):
|
|
147
|
+
if key == "languages" and value:
|
|
148
|
+
choices.append((len(json.dumps(value)), parent, key, value, path))
|
|
149
|
+
else:
|
|
150
|
+
for child, item in value.items():
|
|
151
|
+
if child not in {"omitted", "follow_up", "definition"}:
|
|
152
|
+
visit(item, value, child, path + "." + child)
|
|
153
|
+
|
|
154
|
+
wrapper = {"data": data}
|
|
155
|
+
visit(data, wrapper, "data")
|
|
156
|
+
if not choices:
|
|
157
|
+
raise ValueError("max_tokens is too small to preserve provenance and evidence identifiers; increase it")
|
|
158
|
+
_, parent, key, value, path = max(choices, key=lambda item: item[0])
|
|
159
|
+
if isinstance(value, list):
|
|
160
|
+
keep = len(value) // 2
|
|
161
|
+
parent[key] = value[:keep]
|
|
162
|
+
omitted[path] = omitted.get(path, 0) + len(value) - keep
|
|
163
|
+
elif isinstance(value, dict):
|
|
164
|
+
keep = len(value) // 2
|
|
165
|
+
parent[key] = dict(list(value.items())[:keep])
|
|
166
|
+
omitted[path] = omitted.get(path, 0) + len(value) - keep
|
|
167
|
+
else:
|
|
168
|
+
shortened = value[:len(value) // 2]
|
|
169
|
+
# Source excerpts must end on a complete line with accurate coordinates.
|
|
170
|
+
if key == "text" and "start_line" in parent:
|
|
171
|
+
shortened = shortened.rsplit("\n", 1)[0] if "\n" in shortened else ""
|
|
172
|
+
parent["end_line"] = parent["start_line"] + len(shortened.splitlines()) - 1
|
|
173
|
+
parent["truncated"] = True
|
|
174
|
+
parent[key] = shortened
|
|
175
|
+
omitted[path] = "shortened"
|
|
176
|
+
data = wrapper["data"]
|
|
177
|
+
metadata["truncated"] = True
|
|
178
|
+
metadata["omitted"] = omitted
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Optional workspace-scoped language-server enrichment for daily navigation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ._internal.integrations.context.codebase_ast import detect_language
|
|
10
|
+
from ._internal.integrations.context.syntax_evidence import symbol_position
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrecisionSession:
|
|
14
|
+
def __init__(self, service):
|
|
15
|
+
self.service = service
|
|
16
|
+
self.documents = {}
|
|
17
|
+
self.pending = set()
|
|
18
|
+
self.completed = {}
|
|
19
|
+
self.failures = {}
|
|
20
|
+
self.verified_languages = set()
|
|
21
|
+
|
|
22
|
+
def invalidate(self, paths):
|
|
23
|
+
self.pending.update(paths)
|
|
24
|
+
self.completed.clear()
|
|
25
|
+
self.failures.clear()
|
|
26
|
+
|
|
27
|
+
async def enrich(self, name, file_path=None, line=None):
|
|
28
|
+
if os.environ.get("ATTOCODE_INTEL_PRECISION", "auto") == "off":
|
|
29
|
+
return {"status": "disabled"}
|
|
30
|
+
from .symbol_inspection import select_definitions
|
|
31
|
+
ast = self.service._get_ast_service()
|
|
32
|
+
definitions = select_definitions(self.service, name, file_path, line)
|
|
33
|
+
if not definitions:
|
|
34
|
+
return {"status": "no_definition"}
|
|
35
|
+
if len(definitions) != 1:
|
|
36
|
+
return {"status": "ambiguous", "definitions": len(definitions)}
|
|
37
|
+
key = tuple((d.file_path, d.start_line, d.qualified_name) for d in definitions)
|
|
38
|
+
if key in self.completed:
|
|
39
|
+
return self.completed[key]
|
|
40
|
+
from ._internal.integrations.lsp.client import BUILTIN_SERVERS
|
|
41
|
+
outcomes = []
|
|
42
|
+
deadline = asyncio.get_running_loop().time() + 2.0
|
|
43
|
+
for definition in definitions[:8]:
|
|
44
|
+
language = detect_language(definition.file_path)
|
|
45
|
+
server_id = "typescript" if language == "javascript" else language
|
|
46
|
+
config = BUILTIN_SERVERS.get(server_id)
|
|
47
|
+
if not config or not shutil.which(config.command):
|
|
48
|
+
outcomes.append({"language": language, "status": "unavailable"})
|
|
49
|
+
continue
|
|
50
|
+
if server_id in self.failures:
|
|
51
|
+
outcomes.append({"language": language, "status": self.failures[server_id]})
|
|
52
|
+
continue
|
|
53
|
+
manager = self.service._get_lsp_manager()
|
|
54
|
+
try:
|
|
55
|
+
async with asyncio.timeout_at(deadline):
|
|
56
|
+
if server_id not in manager.get_active_servers():
|
|
57
|
+
manager._timeout = 2
|
|
58
|
+
await manager.start_server(server_id, Path(self.service.project_dir).as_uri())
|
|
59
|
+
# Send edits/deletions for all previously opened documents.
|
|
60
|
+
for rel in list(self.pending):
|
|
61
|
+
path = str(Path(self.service.project_dir) / rel)
|
|
62
|
+
if path in self.documents:
|
|
63
|
+
if Path(path).is_file():
|
|
64
|
+
self.documents[path] += 1
|
|
65
|
+
manager.notify_file_changed(path, Path(path).read_text(), self.documents[path])
|
|
66
|
+
else:
|
|
67
|
+
manager.notify_file_closed(path)
|
|
68
|
+
self.documents.pop(path)
|
|
69
|
+
elif Path(path).is_file() and manager._get_client_for_file(path):
|
|
70
|
+
manager.notify_file_opened(path, Path(path).read_text())
|
|
71
|
+
self.documents[path] = 1
|
|
72
|
+
self.pending.discard(rel)
|
|
73
|
+
path = str(Path(self.service.project_dir) / definition.file_path)
|
|
74
|
+
if path not in self.documents:
|
|
75
|
+
manager.notify_file_opened(path, Path(path).read_text())
|
|
76
|
+
self.documents[path] = 1
|
|
77
|
+
position = symbol_position(path, definition.name, definition.start_line,
|
|
78
|
+
definition.end_line, language)
|
|
79
|
+
if position is None:
|
|
80
|
+
outcomes.append({"language": language, "status": "identifier_unavailable"})
|
|
81
|
+
continue
|
|
82
|
+
# Supply the explicit queried symbol; body ranges alone are ambiguous.
|
|
83
|
+
callback = manager.on_result_callback
|
|
84
|
+
manager.on_result_callback = None
|
|
85
|
+
try:
|
|
86
|
+
resolved = await manager.get_definition(path, *position)
|
|
87
|
+
if resolved is None:
|
|
88
|
+
outcomes.append({"language": language, "status": "not_resolved"})
|
|
89
|
+
continue
|
|
90
|
+
references = await manager.get_references(path, *position, include_declaration=False)
|
|
91
|
+
# Some servers initially know only the opened definition. Prime a
|
|
92
|
+
# bounded set of indexed candidates, then retry once within the same
|
|
93
|
+
# deadline. No sleeps, background warmup, or unbounded file reads.
|
|
94
|
+
candidates = ast.get_callers(definition.qualified_name)
|
|
95
|
+
if not references and candidates:
|
|
96
|
+
root = Path(self.service.project_dir).resolve()
|
|
97
|
+
remaining_bytes = 256_000
|
|
98
|
+
for rel in sorted({ref.file_path for ref in candidates})[:8]:
|
|
99
|
+
if asyncio.get_running_loop().time() >= deadline:
|
|
100
|
+
break
|
|
101
|
+
candidate = (root / rel).resolve()
|
|
102
|
+
if (not candidate.is_relative_to(root) or not candidate.is_file()
|
|
103
|
+
or str(candidate) in self.documents
|
|
104
|
+
or not manager._get_client_for_file(str(candidate))):
|
|
105
|
+
continue
|
|
106
|
+
size = candidate.stat().st_size
|
|
107
|
+
if size > min(64_000, remaining_bytes):
|
|
108
|
+
continue
|
|
109
|
+
with candidate.open("rb") as stream:
|
|
110
|
+
content = stream.read(min(64_000, remaining_bytes) + 1)
|
|
111
|
+
if len(content) > min(64_000, remaining_bytes):
|
|
112
|
+
continue
|
|
113
|
+
manager.notify_file_opened(str(candidate), content.decode("utf-8", errors="replace"))
|
|
114
|
+
self.documents[str(candidate)] = 1
|
|
115
|
+
remaining_bytes -= len(content)
|
|
116
|
+
references = await manager.get_references(path, *position, include_declaration=False)
|
|
117
|
+
finally:
|
|
118
|
+
manager.on_result_callback = callback
|
|
119
|
+
ast.ingest_lsp_results("references", path, references,
|
|
120
|
+
{"line": position[0], "col": position[1],
|
|
121
|
+
"symbol_name": definition.qualified_name})
|
|
122
|
+
self.verified_languages.add(language)
|
|
123
|
+
outcomes.append({"language": language, "status": "verified" if references or not candidates else "no_references",
|
|
124
|
+
"symbol": definition.qualified_name, "references": len(references)})
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
status = "timeout" if isinstance(exc, TimeoutError) or isinstance(exc.__cause__, TimeoutError) else "failed"
|
|
127
|
+
self.failures[server_id] = status
|
|
128
|
+
outcomes.append({"language": language, "status": status})
|
|
129
|
+
status = "verified" if outcomes and all(o["status"] == "verified" for o in outcomes) else "partial"
|
|
130
|
+
result = {"status": status, "queries": outcomes,
|
|
131
|
+
"definitions_truncated": len(definitions) > 8}
|
|
132
|
+
self.completed[key] = result
|
|
133
|
+
return result
|