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,341 @@
|
|
|
1
|
+
"""Shared dependencies for code-intel MCP tool modules.
|
|
2
|
+
|
|
3
|
+
Extracted from server.py to break the circular import:
|
|
4
|
+
server.py → tool modules → server.py
|
|
5
|
+
|
|
6
|
+
Tool modules should import from here instead of server.py.
|
|
7
|
+
server.py re-exports these symbols for backward compatibility.
|
|
8
|
+
|
|
9
|
+
Note: Existing tests set ``server._ast_service = mock`` directly on the
|
|
10
|
+
server module's ``__dict__``. The ``_check_server_override`` helper
|
|
11
|
+
ensures the getters respect those overrides without requiring tests to
|
|
12
|
+
change.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from mcp.server.fastmcp import FastMCP
|
|
24
|
+
except ImportError:
|
|
25
|
+
print(
|
|
26
|
+
"Error: 'mcp' package not installed. "
|
|
27
|
+
"Reinstall with: uv tool install --force --reinstall --from . attocode",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from attocode_intel.api.providers.remote_provider import RemoteTextService
|
|
34
|
+
from attocode_intel.service import CodeIntelService
|
|
35
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
36
|
+
from attocode_intel._internal.integrations.context.code_analyzer import CodeAnalyzer
|
|
37
|
+
from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
|
|
38
|
+
from attocode_intel._internal.integrations.context.frecency import FrecencyTracker
|
|
39
|
+
from attocode_intel._internal.integrations.context.hierarchical_explorer import HierarchicalExplorer
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# FastMCP instance (singleton)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
mcp = FastMCP("attocode-code-intel")
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Lazily initialized singletons
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
_ast_service: ASTService | None = None
|
|
54
|
+
_context_mgr: CodebaseContextManager | None = None
|
|
55
|
+
_code_analyzer: CodeAnalyzer | None = None
|
|
56
|
+
_semantic_search: object | None = None # Backward compat: tests may set this directly
|
|
57
|
+
_memory_store: object | None = None # Backward compat: tests may set this directly
|
|
58
|
+
_explorer: HierarchicalExplorer | None = None
|
|
59
|
+
_service: CodeIntelService | None = None
|
|
60
|
+
_remote_service: RemoteTextService | None = None
|
|
61
|
+
_remote_degraded_reason: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_server_override(name: str) -> object | None:
|
|
65
|
+
"""Return the value of *name* from ``server.__dict__`` if set there.
|
|
66
|
+
|
|
67
|
+
Tests (and some internal code) set e.g. ``server._ast_service = mock``
|
|
68
|
+
directly. Since getters live here, we must check the server module's
|
|
69
|
+
__dict__ so those overrides are respected. Uses sys.modules to avoid
|
|
70
|
+
a circular import.
|
|
71
|
+
"""
|
|
72
|
+
srv = sys.modules.get("attocode_intel.server")
|
|
73
|
+
if srv is not None and name in srv.__dict__:
|
|
74
|
+
return srv.__dict__[name] # type: ignore[no-any-return]
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Project-directory discovery lives in the neutral ``project_dir``
|
|
79
|
+
# module so HTTP providers can import it without dragging in the MCP
|
|
80
|
+
# runtime. Re-export here for backward compatibility with the existing
|
|
81
|
+
# ~25 call sites that do ``from attocode_intel._shared import
|
|
82
|
+
# _get_project_dir`` (or access it as ``_shared._get_project_dir``).
|
|
83
|
+
from attocode_intel.project_dir import ( # noqa: F401, E402
|
|
84
|
+
_get_project_dir,
|
|
85
|
+
_walk_up,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_ast_service() -> ASTService:
|
|
90
|
+
"""Lazily initialize and return the ASTService singleton."""
|
|
91
|
+
from attocode_intel.request_context import current_request
|
|
92
|
+
if (request := current_request.get()) is not None:
|
|
93
|
+
return request.service._get_ast_service()
|
|
94
|
+
global _ast_service
|
|
95
|
+
override = _check_server_override("_ast_service")
|
|
96
|
+
if override is not None:
|
|
97
|
+
return override # type: ignore[return-value]
|
|
98
|
+
if _ast_service is None:
|
|
99
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
100
|
+
|
|
101
|
+
project_dir = _get_project_dir()
|
|
102
|
+
_ast_service = ASTService.get_instance(project_dir)
|
|
103
|
+
if not _ast_service.initialized:
|
|
104
|
+
logger.info("Initializing ASTService (skeleton) for %s...", project_dir)
|
|
105
|
+
_ast_service.initialize_skeleton(indexing_depth="auto")
|
|
106
|
+
if (
|
|
107
|
+
_ast_service._hydration_state
|
|
108
|
+
and _ast_service._hydration_state.phase != "ready"
|
|
109
|
+
):
|
|
110
|
+
_ast_service.start_hydration()
|
|
111
|
+
logger.info(
|
|
112
|
+
"ASTService ready: %d files indexed (skeleton/hydration)",
|
|
113
|
+
len(_ast_service._ast_cache),
|
|
114
|
+
)
|
|
115
|
+
return _ast_service
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _get_context_mgr() -> CodebaseContextManager:
|
|
119
|
+
"""Lazily initialize and return the CodebaseContextManager."""
|
|
120
|
+
from attocode_intel.request_context import current_request
|
|
121
|
+
if (request := current_request.get()) is not None:
|
|
122
|
+
return request.service._get_context_mgr()
|
|
123
|
+
global _context_mgr
|
|
124
|
+
override = _check_server_override("_context_mgr")
|
|
125
|
+
if override is not None:
|
|
126
|
+
return override # type: ignore[return-value]
|
|
127
|
+
if _context_mgr is None:
|
|
128
|
+
from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
|
|
129
|
+
|
|
130
|
+
project_dir = _get_project_dir()
|
|
131
|
+
_context_mgr = CodebaseContextManager(root_dir=project_dir)
|
|
132
|
+
_context_mgr.discover_files()
|
|
133
|
+
return _context_mgr
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_code_analyzer() -> CodeAnalyzer:
|
|
137
|
+
"""Lazily initialize and return the CodeAnalyzer."""
|
|
138
|
+
from attocode_intel.request_context import current_request
|
|
139
|
+
if (request := current_request.get()) is not None:
|
|
140
|
+
return request.service._get_code_analyzer()
|
|
141
|
+
global _code_analyzer
|
|
142
|
+
override = _check_server_override("_code_analyzer")
|
|
143
|
+
if override is not None:
|
|
144
|
+
return override # type: ignore[return-value]
|
|
145
|
+
if _code_analyzer is None:
|
|
146
|
+
from attocode_intel._internal.integrations.context.code_analyzer import CodeAnalyzer
|
|
147
|
+
|
|
148
|
+
_code_analyzer = CodeAnalyzer()
|
|
149
|
+
return _code_analyzer
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _get_service() -> CodeIntelService | RemoteTextService:
|
|
153
|
+
"""Lazily initialize and return the CodeIntelService singleton."""
|
|
154
|
+
from attocode_intel.request_context import current_request
|
|
155
|
+
if (request := current_request.get()) is not None:
|
|
156
|
+
return request.service
|
|
157
|
+
global _service
|
|
158
|
+
srv = sys.modules.get("attocode_intel.server")
|
|
159
|
+
if srv is not None and "_service" in srv.__dict__:
|
|
160
|
+
override = srv.__dict__["_service"]
|
|
161
|
+
if override is None:
|
|
162
|
+
_service = None
|
|
163
|
+
else:
|
|
164
|
+
return override # type: ignore[no-any-return]
|
|
165
|
+
if _remote_service is not None:
|
|
166
|
+
return _remote_service
|
|
167
|
+
if _service is not None:
|
|
168
|
+
from attocode_intel import service as code_intel_service_module
|
|
169
|
+
|
|
170
|
+
if _service.project_dir not in code_intel_service_module._instances:
|
|
171
|
+
_service = None
|
|
172
|
+
if _service is None:
|
|
173
|
+
from attocode_intel.service import CodeIntelService
|
|
174
|
+
|
|
175
|
+
project_dir = _get_project_dir()
|
|
176
|
+
_service = CodeIntelService.get_instance(project_dir)
|
|
177
|
+
return _service
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _get_local_service() -> CodeIntelService:
|
|
181
|
+
"""Return the local CodeIntelService, bypassing any remote proxy.
|
|
182
|
+
|
|
183
|
+
Some tools (e.g. ``regex_search``, ``call_graph``) have no server-side
|
|
184
|
+
HTTP route, so they cannot be proxied to a remote. They call this to run
|
|
185
|
+
against the local index even when a remote is configured, instead of
|
|
186
|
+
crashing with ``AttributeError`` on the missing remote method.
|
|
187
|
+
"""
|
|
188
|
+
from attocode_intel.request_context import current_request
|
|
189
|
+
if (request := current_request.get()) is not None:
|
|
190
|
+
return request.service
|
|
191
|
+
global _service
|
|
192
|
+
srv = sys.modules.get("attocode_intel.server")
|
|
193
|
+
if srv is not None and "_service" in srv.__dict__:
|
|
194
|
+
override = srv.__dict__["_service"]
|
|
195
|
+
if override is not None and not _is_remote_service(override):
|
|
196
|
+
return override # type: ignore[return-value]
|
|
197
|
+
if override is None:
|
|
198
|
+
_service = None
|
|
199
|
+
if _service is not None:
|
|
200
|
+
from attocode_intel import service as code_intel_service_module
|
|
201
|
+
|
|
202
|
+
if _service.project_dir not in code_intel_service_module._instances:
|
|
203
|
+
_service = None
|
|
204
|
+
if _service is None:
|
|
205
|
+
from attocode_intel.service import CodeIntelService
|
|
206
|
+
|
|
207
|
+
_service = CodeIntelService.get_instance(_get_project_dir())
|
|
208
|
+
return _service
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_remote_service(obj: object) -> bool:
|
|
212
|
+
"""True when *obj* is a remote proxy rather than the local service."""
|
|
213
|
+
return type(obj).__name__ == "RemoteTextService"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _get_remote_service() -> RemoteTextService | None:
|
|
217
|
+
"""Return the configured remote text service, if any."""
|
|
218
|
+
from attocode_intel.request_context import current_request
|
|
219
|
+
if current_request.get() is not None:
|
|
220
|
+
return None
|
|
221
|
+
srv = sys.modules.get("attocode_intel.server")
|
|
222
|
+
if srv is not None and "_remote_service" in srv.__dict__:
|
|
223
|
+
return srv.__dict__["_remote_service"] # type: ignore[no-any-return]
|
|
224
|
+
return _remote_service
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def configure_remote_service(remote_url: str, remote_token: str, remote_repo_id: str) -> None:
|
|
228
|
+
"""Proxy the shared service through a remote HTTP server **if it is usable**.
|
|
229
|
+
|
|
230
|
+
Gracefully degrades to local mode (leaving ``_remote_service`` unset) when
|
|
231
|
+
the JWT is expired or the server is unreachable, recording a human-readable
|
|
232
|
+
reason in :func:`get_remote_degraded_reason`. This prevents a stale or dead
|
|
233
|
+
``[remote]`` config from making every tool fail with "Connection refused".
|
|
234
|
+
"""
|
|
235
|
+
global _remote_service, _service, _remote_degraded_reason
|
|
236
|
+
from attocode_intel.api.providers.remote_provider import RemoteTextService
|
|
237
|
+
from attocode_intel.config import token_is_expired
|
|
238
|
+
|
|
239
|
+
# Drop any previously-configured client (avoid a socket leak on reconfigure).
|
|
240
|
+
if _remote_service is not None:
|
|
241
|
+
try:
|
|
242
|
+
_remote_service.close()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
_remote_service = None
|
|
246
|
+
_service = None
|
|
247
|
+
_remote_degraded_reason = ""
|
|
248
|
+
|
|
249
|
+
if token_is_expired(remote_token):
|
|
250
|
+
_remote_degraded_reason = (
|
|
251
|
+
f"remote token expired — using local mode (server={remote_url})"
|
|
252
|
+
)
|
|
253
|
+
logger.warning("Code-intel %s", _remote_degraded_reason)
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
candidate = RemoteTextService(remote_url, remote_token, remote_repo_id)
|
|
257
|
+
if not candidate.ping():
|
|
258
|
+
_remote_degraded_reason = (
|
|
259
|
+
f"remote server unreachable at {remote_url} — using local mode"
|
|
260
|
+
)
|
|
261
|
+
logger.warning("Code-intel %s", _remote_degraded_reason)
|
|
262
|
+
try:
|
|
263
|
+
candidate.close()
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
_remote_service = candidate
|
|
269
|
+
logger.info("Code-intel remote mode active: %s (repo=%s)", remote_url, remote_repo_id)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def clear_remote_service() -> None:
|
|
273
|
+
"""Reset remote service wiring (back to local mode)."""
|
|
274
|
+
global _remote_service, _remote_degraded_reason
|
|
275
|
+
if _remote_service is not None:
|
|
276
|
+
try:
|
|
277
|
+
_remote_service.close()
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
_remote_service = None
|
|
281
|
+
_remote_degraded_reason = ""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_remote_degraded_reason() -> str:
|
|
285
|
+
"""Why remote mode is inactive despite being configured ('' when N/A)."""
|
|
286
|
+
return _remote_degraded_reason
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def enable_remote_if_configured(
|
|
290
|
+
project_dir: str,
|
|
291
|
+
*,
|
|
292
|
+
local_only: bool = False,
|
|
293
|
+
remote_url: str = "",
|
|
294
|
+
remote_token: str = "",
|
|
295
|
+
remote_repo_id: str = "",
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Enable remote mode from explicit args / config / env, else stay local.
|
|
298
|
+
|
|
299
|
+
Single source of truth for the startup wiring previously duplicated (and
|
|
300
|
+
divergent) between ``server.main`` and ``cli._cmd_serve``. Explicit
|
|
301
|
+
``remote_*`` args win; otherwise ``.attocode/config.toml`` + env are read
|
|
302
|
+
via :func:`load_remote_config`. ``configure_remote_service`` then health/
|
|
303
|
+
expiry-gates the remote and falls back to local on any problem.
|
|
304
|
+
"""
|
|
305
|
+
clear_remote_service()
|
|
306
|
+
if local_only:
|
|
307
|
+
return
|
|
308
|
+
if not remote_url:
|
|
309
|
+
from attocode_intel.config import load_remote_config
|
|
310
|
+
|
|
311
|
+
rc = load_remote_config(project_dir)
|
|
312
|
+
if rc.is_configured:
|
|
313
|
+
remote_url, remote_token, remote_repo_id = rc.server, rc.token, rc.repo_id
|
|
314
|
+
if remote_url:
|
|
315
|
+
configure_remote_service(remote_url, remote_token, remote_repo_id)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _get_explorer() -> HierarchicalExplorer:
|
|
319
|
+
"""Lazily initialize the hierarchical explorer."""
|
|
320
|
+
from attocode_intel.request_context import current_request
|
|
321
|
+
if (request := current_request.get()) is not None:
|
|
322
|
+
return request.service._get_explorer()
|
|
323
|
+
global _explorer
|
|
324
|
+
override = _check_server_override("_explorer")
|
|
325
|
+
if override is not None:
|
|
326
|
+
return override # type: ignore[return-value]
|
|
327
|
+
if _explorer is None:
|
|
328
|
+
from attocode_intel._internal.integrations.context.hierarchical_explorer import HierarchicalExplorer
|
|
329
|
+
ctx = _get_context_mgr()
|
|
330
|
+
ast_svc = _get_ast_service()
|
|
331
|
+
_explorer = HierarchicalExplorer(ctx, ast_service=ast_svc)
|
|
332
|
+
return _explorer
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _get_frecency_tracker() -> FrecencyTracker:
|
|
336
|
+
"""Get shared frecency tracker (thread-safe via get_tracker lock)."""
|
|
337
|
+
from attocode_intel._internal.integrations.context.frecency import get_tracker
|
|
338
|
+
|
|
339
|
+
project_dir = _get_project_dir()
|
|
340
|
+
db_path = os.path.join(project_dir, ".attocode", "frecency")
|
|
341
|
+
return get_tracker(db_path=db_path)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Report analysis capabilities independently of index progress."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def language_capabilities(verified_languages=()):
|
|
9
|
+
from ._internal.integrations.context.codebase_ast import LANG_EXTENSIONS
|
|
10
|
+
from ._internal.integrations.context.codebase_context import IMPORT_RESOLVERS
|
|
11
|
+
from ._internal.integrations.context.ts_parser import is_available, supported_languages
|
|
12
|
+
from ._internal.integrations.lsp.client import BUILTIN_SERVERS
|
|
13
|
+
|
|
14
|
+
result = {}
|
|
15
|
+
for language in sorted(set(LANG_EXTENSIONS.values()) | set(supported_languages())):
|
|
16
|
+
server = BUILTIN_SERVERS.get("typescript" if language == "javascript" else language)
|
|
17
|
+
parser = is_available(language)
|
|
18
|
+
result[language] = {
|
|
19
|
+
"search": "available",
|
|
20
|
+
"definitions": "syntax" if parser else "fallback",
|
|
21
|
+
"references": "syntax_candidates" if parser else "text_candidates",
|
|
22
|
+
"dependencies": "partial" if language in IMPORT_RESOLVERS else "unavailable",
|
|
23
|
+
"test_relationships": "heuristic",
|
|
24
|
+
"precision_server": server.command if server else None,
|
|
25
|
+
"precision_installed": bool(server and shutil.which(server.command)),
|
|
26
|
+
"precision_verified": language in verified_languages,
|
|
27
|
+
}
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def analysis_report(service, precision=None):
|
|
32
|
+
ast = service._ast_service
|
|
33
|
+
if ast is None:
|
|
34
|
+
return {"status": "not_started", "absence_proven": False}
|
|
35
|
+
cache = ast._ast_cache.copy()
|
|
36
|
+
tiers = Counter(getattr(a, "parsing_tier", "unknown") or "unknown" for a in cache.values())
|
|
37
|
+
graph = ast._context_mgr._dep_graph
|
|
38
|
+
unresolved = graph.unresolved if graph else {}
|
|
39
|
+
limitations = [
|
|
40
|
+
"Syntax matches are candidates; dynamic dispatch and external dependencies may be unresolved.",
|
|
41
|
+
"No matches do not prove absence of references or impact.",
|
|
42
|
+
]
|
|
43
|
+
return {
|
|
44
|
+
"status": "partial",
|
|
45
|
+
"absence_proven": False,
|
|
46
|
+
"sources": sorted(tiers),
|
|
47
|
+
"files_by_parser": dict(tiers),
|
|
48
|
+
"unresolved_imports": sum(len(v) for v in unresolved.values()),
|
|
49
|
+
"unresolved_import_samples": [
|
|
50
|
+
{"file": path, "module": module}
|
|
51
|
+
for path, modules in sorted(unresolved.items())[:5] for module in modules[:3]
|
|
52
|
+
],
|
|
53
|
+
"unresolved_includes_external": True,
|
|
54
|
+
"precision": precision or {"status": "not_requested"},
|
|
55
|
+
"limitations": limitations,
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI HTTP API for Attocode Code Intelligence."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Apply repository authorization before legacy routes can consult cached services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
from starlette.responses import JSONResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RepositoryAccessMiddleware(BaseHTTPMiddleware):
|
|
13
|
+
async def dispatch(self, request, call_next):
|
|
14
|
+
config = request.app.state.config
|
|
15
|
+
if config.is_service_mode:
|
|
16
|
+
match = re.match(r"/api/v[12]/(?:projects|repos)/([^/]+)", request.url.path)
|
|
17
|
+
if match:
|
|
18
|
+
from attocode_intel.api.auth import resolve_auth
|
|
19
|
+
from attocode_intel.remote import authorized_repo
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
auth = await resolve_auth(request.headers.get("authorization"))
|
|
23
|
+
await authorized_repo(
|
|
24
|
+
match.group(1), auth, write=request.method not in {"GET", "HEAD", "OPTIONS"}
|
|
25
|
+
)
|
|
26
|
+
except HTTPException as exc:
|
|
27
|
+
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
|
|
28
|
+
elif request.url.path.rstrip("/") == "/api/v1/projects":
|
|
29
|
+
return JSONResponse(
|
|
30
|
+
{"detail": "Use organization-scoped repository management"}, status_code=403
|
|
31
|
+
)
|
|
32
|
+
return await call_next(request)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""FastAPI application factory for Attocode Code Intelligence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from attocode_intel import __version__
|
|
10
|
+
from attocode_intel.config import CodeIntelConfig
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
|
|
15
|
+
from fastapi import FastAPI
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _run_migrations(database_url: str) -> None:
|
|
21
|
+
"""Run Alembic migrations programmatically."""
|
|
22
|
+
import asyncio
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from alembic import command
|
|
26
|
+
from alembic.config import Config
|
|
27
|
+
|
|
28
|
+
migrations_dir = Path(__file__).resolve().parent.parent / "migrations"
|
|
29
|
+
alembic_cfg = Config(str(migrations_dir / "alembic.ini"))
|
|
30
|
+
alembic_cfg.set_main_option("script_location", str(migrations_dir))
|
|
31
|
+
alembic_cfg.set_main_option("sqlalchemy.url", database_url)
|
|
32
|
+
|
|
33
|
+
# Alembic is sync — run in thread to avoid blocking the event loop
|
|
34
|
+
loop = asyncio.get_running_loop()
|
|
35
|
+
await loop.run_in_executor(None, command.upgrade, alembic_cfg, "head")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@asynccontextmanager
|
|
39
|
+
async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
40
|
+
"""Startup/shutdown lifecycle — initialize DB engine in service mode."""
|
|
41
|
+
config: CodeIntelConfig = app.state.config
|
|
42
|
+
if config.is_service_mode:
|
|
43
|
+
from attocode_intel.db.engine import dispose_engine, init_engine
|
|
44
|
+
|
|
45
|
+
init_engine(config.database_url)
|
|
46
|
+
logger.info("Service mode: database engine initialized")
|
|
47
|
+
|
|
48
|
+
# Auto-run migrations on startup
|
|
49
|
+
try:
|
|
50
|
+
await _run_migrations(config.database_url)
|
|
51
|
+
logger.info("Service mode: migrations applied")
|
|
52
|
+
except Exception:
|
|
53
|
+
logger.exception("Failed to run migrations on startup")
|
|
54
|
+
raise
|
|
55
|
+
async with app.state.mcp_transport.manager.run():
|
|
56
|
+
yield
|
|
57
|
+
await app.state.mcp_transport.gateway.close()
|
|
58
|
+
|
|
59
|
+
# Shutdown debouncer
|
|
60
|
+
from attocode_intel.api.routes.notify import _debouncer
|
|
61
|
+
|
|
62
|
+
if _debouncer is not None:
|
|
63
|
+
await _debouncer.shutdown()
|
|
64
|
+
|
|
65
|
+
if config.is_service_mode:
|
|
66
|
+
from attocode_intel.db.engine import dispose_engine
|
|
67
|
+
|
|
68
|
+
await dispose_engine()
|
|
69
|
+
logger.info("Service mode: database engine disposed")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_app(config: CodeIntelConfig | None = None) -> FastAPI:
|
|
73
|
+
"""Create and configure the FastAPI application.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
config: Service configuration. If None, reads from environment.
|
|
77
|
+
"""
|
|
78
|
+
from fastapi import FastAPI
|
|
79
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
80
|
+
|
|
81
|
+
from attocode_intel.api import deps
|
|
82
|
+
from attocode_intel.api.middleware import MetricsMiddleware, RequestLoggingMiddleware
|
|
83
|
+
from attocode_intel.api.routes import (
|
|
84
|
+
adr,
|
|
85
|
+
analysis,
|
|
86
|
+
files,
|
|
87
|
+
graph,
|
|
88
|
+
graph_viz,
|
|
89
|
+
health,
|
|
90
|
+
history,
|
|
91
|
+
learning,
|
|
92
|
+
lsp,
|
|
93
|
+
notify,
|
|
94
|
+
projects,
|
|
95
|
+
search,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if config is None:
|
|
99
|
+
config = CodeIntelConfig.from_env()
|
|
100
|
+
|
|
101
|
+
deps.configure(config)
|
|
102
|
+
|
|
103
|
+
app = FastAPI(
|
|
104
|
+
title="Attocode Code Intelligence",
|
|
105
|
+
description=(
|
|
106
|
+
"Code intelligence service with AST parsing, dependency graphs, "
|
|
107
|
+
"semantic search, impact analysis, and more. "
|
|
108
|
+
"v1 endpoints return text (MCP-compatible), "
|
|
109
|
+
"v2 endpoints return structured JSON (UI-ready)."
|
|
110
|
+
),
|
|
111
|
+
version=__version__,
|
|
112
|
+
docs_url="/docs",
|
|
113
|
+
redoc_url="/redoc",
|
|
114
|
+
lifespan=_lifespan,
|
|
115
|
+
)
|
|
116
|
+
app.state.config = config
|
|
117
|
+
from attocode_intel.api.mcp_transport import MCPTransport
|
|
118
|
+
from starlette.routing import Mount, Route
|
|
119
|
+
|
|
120
|
+
transport = MCPTransport(config)
|
|
121
|
+
app.state.mcp_transport = transport
|
|
122
|
+
app.router.routes.append(Route("/mcp", endpoint=transport, methods=["GET", "POST", "DELETE"]))
|
|
123
|
+
app.router.routes.append(Mount("/mcp", app=transport))
|
|
124
|
+
|
|
125
|
+
# Without configured origins, no other web origin can call the API, so no CORS headers.
|
|
126
|
+
# credentials=True is invalid with origins=["*"] per the CORS spec.
|
|
127
|
+
if config.cors_origins:
|
|
128
|
+
app.add_middleware(
|
|
129
|
+
CORSMiddleware,
|
|
130
|
+
allow_origins=config.cors_origins,
|
|
131
|
+
allow_credentials="*" not in config.cors_origins,
|
|
132
|
+
allow_methods=["*"],
|
|
133
|
+
allow_headers=["*"],
|
|
134
|
+
)
|
|
135
|
+
from attocode_intel.api.access import RepositoryAccessMiddleware
|
|
136
|
+
|
|
137
|
+
app.add_middleware(RepositoryAccessMiddleware)
|
|
138
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
139
|
+
app.add_middleware(MetricsMiddleware)
|
|
140
|
+
|
|
141
|
+
# Rate limiting (service mode only)
|
|
142
|
+
if config.is_service_mode:
|
|
143
|
+
from attocode_intel.api.middleware import RateLimitMiddleware
|
|
144
|
+
|
|
145
|
+
app.add_middleware(RateLimitMiddleware, requests_per_minute=300)
|
|
146
|
+
|
|
147
|
+
# Core routes — each module has router_v1 (text) + router_v2 (JSON)
|
|
148
|
+
from attocode_intel.api.routes.operations import router as operations_router
|
|
149
|
+
|
|
150
|
+
app.include_router(operations_router)
|
|
151
|
+
app.include_router(health.router)
|
|
152
|
+
app.include_router(projects.router)
|
|
153
|
+
app.include_router(adr.router_v1)
|
|
154
|
+
app.include_router(analysis.router_v1)
|
|
155
|
+
app.include_router(analysis.router_v2)
|
|
156
|
+
app.include_router(search.router_v1)
|
|
157
|
+
app.include_router(search.router_v2)
|
|
158
|
+
app.include_router(graph.router_v1)
|
|
159
|
+
app.include_router(graph.router_v2)
|
|
160
|
+
app.include_router(lsp.router_v1)
|
|
161
|
+
app.include_router(lsp.router_v2)
|
|
162
|
+
app.include_router(learning.router)
|
|
163
|
+
app.include_router(learning.router_v2)
|
|
164
|
+
app.include_router(notify.router)
|
|
165
|
+
app.include_router(files.router)
|
|
166
|
+
app.include_router(history.router_v1)
|
|
167
|
+
app.include_router(history.router_v2)
|
|
168
|
+
app.include_router(graph_viz.router)
|
|
169
|
+
|
|
170
|
+
# Service mode routes
|
|
171
|
+
if config.is_service_mode:
|
|
172
|
+
from attocode_intel.api.routes import (
|
|
173
|
+
activity,
|
|
174
|
+
api_keys,
|
|
175
|
+
auth,
|
|
176
|
+
branches,
|
|
177
|
+
cross_repo_search,
|
|
178
|
+
embeddings,
|
|
179
|
+
files_v2,
|
|
180
|
+
gc,
|
|
181
|
+
git_v2,
|
|
182
|
+
jobs,
|
|
183
|
+
orgs,
|
|
184
|
+
preferences,
|
|
185
|
+
presence,
|
|
186
|
+
repos,
|
|
187
|
+
snapshots,
|
|
188
|
+
webhooks,
|
|
189
|
+
websocket,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
app.include_router(auth.router)
|
|
193
|
+
app.include_router(orgs.router)
|
|
194
|
+
app.include_router(repos.router)
|
|
195
|
+
app.include_router(api_keys.router)
|
|
196
|
+
app.include_router(branches.router)
|
|
197
|
+
app.include_router(webhooks.router)
|
|
198
|
+
app.include_router(websocket.router)
|
|
199
|
+
app.include_router(jobs.router)
|
|
200
|
+
# v2 service-mode routes
|
|
201
|
+
app.include_router(files_v2.router)
|
|
202
|
+
app.include_router(git_v2.router)
|
|
203
|
+
app.include_router(embeddings.router)
|
|
204
|
+
app.include_router(presence.router)
|
|
205
|
+
app.include_router(activity.router)
|
|
206
|
+
app.include_router(preferences.router)
|
|
207
|
+
app.include_router(cross_repo_search.router)
|
|
208
|
+
app.include_router(gc.router)
|
|
209
|
+
app.include_router(snapshots.router)
|
|
210
|
+
|
|
211
|
+
# Graph visualization — serve the bundled HTML page at /graph
|
|
212
|
+
import os
|
|
213
|
+
from pathlib import Path
|
|
214
|
+
|
|
215
|
+
from starlette.responses import FileResponse as _FileResponse
|
|
216
|
+
|
|
217
|
+
_graph_html = Path(__file__).resolve().parent / "static" / "graph.html"
|
|
218
|
+
|
|
219
|
+
@app.get("/graph", include_in_schema=False)
|
|
220
|
+
async def graph_visualization_page() -> _FileResponse:
|
|
221
|
+
"""Serve the D3 dependency-graph visualization."""
|
|
222
|
+
return _FileResponse(str(_graph_html), media_type="text/html")
|
|
223
|
+
|
|
224
|
+
# SPA fallback — serve frontend static files (registered LAST so it
|
|
225
|
+
# doesn't shadow /api/*, /docs, /redoc, /openapi.json).
|
|
226
|
+
|
|
227
|
+
static_dir = os.environ.get("ATTOCODE_STATIC_DIR", "/app/static")
|
|
228
|
+
if os.path.isdir(static_dir):
|
|
229
|
+
from starlette.responses import FileResponse
|
|
230
|
+
|
|
231
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
232
|
+
async def spa_fallback(full_path: str) -> FileResponse:
|
|
233
|
+
from pathlib import Path
|
|
234
|
+
|
|
235
|
+
file_path = (Path(static_dir) / full_path).resolve()
|
|
236
|
+
if file_path.is_relative_to(Path(static_dir).resolve()) and file_path.is_file():
|
|
237
|
+
return FileResponse(file_path)
|
|
238
|
+
return FileResponse(os.path.join(static_dir, "index.html"))
|
|
239
|
+
|
|
240
|
+
logger.info("SPA fallback enabled from %s", static_dir)
|
|
241
|
+
|
|
242
|
+
logger.info(
|
|
243
|
+
"FastAPI app created for %s (service_mode=%s)",
|
|
244
|
+
config.project_dir or "(no project)",
|
|
245
|
+
config.is_service_mode,
|
|
246
|
+
)
|
|
247
|
+
return app
|