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,770 @@
|
|
|
1
|
+
"""Search and security tools for the code-intel MCP server.
|
|
2
|
+
|
|
3
|
+
Tools: semantic_search, semantic_search_status, security_scan, fast_search,
|
|
4
|
+
regex_search.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from attocode_intel._shared import (
|
|
14
|
+
_get_ast_service,
|
|
15
|
+
_get_context_mgr,
|
|
16
|
+
_get_frecency_tracker,
|
|
17
|
+
_get_remote_service,
|
|
18
|
+
_get_service,
|
|
19
|
+
mcp,
|
|
20
|
+
)
|
|
21
|
+
from attocode_intel.tools.pin_tools import pin_stamped
|
|
22
|
+
from attocode_intel._internal.integrations.context.frecency import FrecencyResult
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from attocode_intel._internal.integrations.context.semantic_search import SemanticSearchManager
|
|
26
|
+
from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
|
|
27
|
+
from attocode_intel._internal.integrations.security.scanner import SecurityScanner
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_project_dir() -> str:
|
|
31
|
+
"""Dynamic dispatch to :func:`attocode_intel._shared._get_project_dir`.
|
|
32
|
+
|
|
33
|
+
Previously this module did ``from ..._shared import _get_project_dir``
|
|
34
|
+
which cached the original function reference at import time. That
|
|
35
|
+
made it invisible to test fixtures that monkeypatch
|
|
36
|
+
``_shared._get_project_dir`` — the fixture's new function would be
|
|
37
|
+
installed in ``_shared`` but ``search_tools._get_project_dir`` would
|
|
38
|
+
keep returning the original.
|
|
39
|
+
|
|
40
|
+
Looking the function up fresh on every call keeps cross-module
|
|
41
|
+
monkeypatching consistent AND avoids a whole class of test
|
|
42
|
+
pollution where a fixture-scoped patch of ``_shared`` leaks into
|
|
43
|
+
search_tools via some other path.
|
|
44
|
+
"""
|
|
45
|
+
from attocode_intel import _shared
|
|
46
|
+
return _shared._get_project_dir()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _empty_frecency(ai_mode: bool) -> FrecencyResult:
|
|
50
|
+
"""Create a zero-score FrecencyResult."""
|
|
51
|
+
return FrecencyResult(score=0, accesses=0, last_access=None, is_ai_mode=ai_mode)
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Lazy singletons
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
_semantic_search: SemanticSearchManager | None = None
|
|
58
|
+
_semantic_search_lock = threading.Lock()
|
|
59
|
+
|
|
60
|
+
_trigram_index: TrigramIndex | None = None
|
|
61
|
+
_trigram_index_lock = threading.Lock()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_semantic_search() -> SemanticSearchManager:
|
|
65
|
+
"""Lazily initialize the semantic search manager (thread-safe)."""
|
|
66
|
+
from attocode_intel.request_context import current_request
|
|
67
|
+
if (request := current_request.get()) is not None:
|
|
68
|
+
return request.service._get_semantic_search()
|
|
69
|
+
global _semantic_search
|
|
70
|
+
if _semantic_search is None:
|
|
71
|
+
with _semantic_search_lock:
|
|
72
|
+
if _semantic_search is None:
|
|
73
|
+
from attocode_intel._internal.integrations.context.semantic_search import SemanticSearchManager
|
|
74
|
+
project_dir = _get_project_dir()
|
|
75
|
+
_semantic_search = SemanticSearchManager(root_dir=project_dir)
|
|
76
|
+
return _semantic_search
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_security_scanner: SecurityScanner | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_security_scanner() -> SecurityScanner:
|
|
83
|
+
"""Lazily initialize the security scanner."""
|
|
84
|
+
from attocode_intel.request_context import current_request
|
|
85
|
+
if (request := current_request.get()) is not None:
|
|
86
|
+
return request.service._get_security_scanner()
|
|
87
|
+
global _security_scanner
|
|
88
|
+
if _security_scanner is None:
|
|
89
|
+
from attocode_intel._internal.integrations.security.scanner import SecurityScanner
|
|
90
|
+
project_dir = _get_project_dir()
|
|
91
|
+
_security_scanner = SecurityScanner(root_dir=project_dir)
|
|
92
|
+
return _security_scanner
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_trigram_index() -> TrigramIndex | None:
|
|
96
|
+
"""Lazily initialize the trigram index (thread-safe).
|
|
97
|
+
|
|
98
|
+
Loads an existing index from disk if available. Returns None
|
|
99
|
+
if no index has been built yet.
|
|
100
|
+
"""
|
|
101
|
+
from attocode_intel.request_context import current_request, scoped_store
|
|
102
|
+
if current_request.get() is not None:
|
|
103
|
+
def build(root):
|
|
104
|
+
import os
|
|
105
|
+
from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
|
|
106
|
+
index = TrigramIndex(index_dir=os.path.join(root, ".attocode", "index"))
|
|
107
|
+
return index if index.load() else None
|
|
108
|
+
return scoped_store("trigrams", build)
|
|
109
|
+
global _trigram_index
|
|
110
|
+
if _trigram_index is None:
|
|
111
|
+
with _trigram_index_lock:
|
|
112
|
+
if _trigram_index is None:
|
|
113
|
+
import os
|
|
114
|
+
project_dir = _get_project_dir()
|
|
115
|
+
index_dir = os.path.join(project_dir, ".attocode", "index")
|
|
116
|
+
if os.path.isdir(index_dir):
|
|
117
|
+
try:
|
|
118
|
+
from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
|
|
119
|
+
idx = TrigramIndex(index_dir=index_dir)
|
|
120
|
+
if idx.load():
|
|
121
|
+
_trigram_index = idx
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
return _trigram_index
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Tools
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
@pin_stamped
|
|
134
|
+
def semantic_search(
|
|
135
|
+
query: str,
|
|
136
|
+
top_k: int = 10,
|
|
137
|
+
file_filter: str = "",
|
|
138
|
+
branch: str = "",
|
|
139
|
+
mode: str = "auto",
|
|
140
|
+
) -> str:
|
|
141
|
+
"""Search the code by meaning with a natural-language query. Returns files, functions and classes, best first.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
query: For example "authentication middleware".
|
|
145
|
+
file_filter: Optional glob, for example "*.py".
|
|
146
|
+
branch: Remote service only. Local search reads the working tree.
|
|
147
|
+
mode: "auto" (default), "keyword", or "vector" (waits for embeddings).
|
|
148
|
+
"""
|
|
149
|
+
return _get_service().semantic_search(
|
|
150
|
+
query=query,
|
|
151
|
+
top_k=top_k,
|
|
152
|
+
file_filter=file_filter,
|
|
153
|
+
branch=branch,
|
|
154
|
+
mode=mode,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
def semantic_search_status() -> str:
|
|
160
|
+
"""Get the status of the semantic search index.
|
|
161
|
+
|
|
162
|
+
Returns: provider name, coverage percentage, files indexed/total,
|
|
163
|
+
indexing status, and whether vector search is active.
|
|
164
|
+
"""
|
|
165
|
+
remote = _get_remote_service()
|
|
166
|
+
if remote is not None:
|
|
167
|
+
return remote.semantic_search_status()
|
|
168
|
+
|
|
169
|
+
mgr = _get_semantic_search()
|
|
170
|
+
progress = mgr.get_index_progress()
|
|
171
|
+
discovered_files = 0
|
|
172
|
+
ast_indexed_files = 0
|
|
173
|
+
degradation_reasons: list[str] = []
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
ctx = _get_context_mgr()
|
|
177
|
+
discovered = getattr(ctx, "_files", None)
|
|
178
|
+
if isinstance(discovered, list):
|
|
179
|
+
discovered_files = len(discovered)
|
|
180
|
+
if discovered_files == 0:
|
|
181
|
+
discovered_files = len(ctx.discover_files())
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
degradation_reasons.append(f"context_error:{type(exc).__name__}")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
ast_svc = _get_ast_service()
|
|
187
|
+
stats = ast_svc._store.stats() if hasattr(ast_svc, "_store") else {}
|
|
188
|
+
ast_indexed_files = int(
|
|
189
|
+
stats.get("files", 0)
|
|
190
|
+
or stats.get("files_indexed", 0)
|
|
191
|
+
or len(getattr(ast_svc, "_ast_cache", {}))
|
|
192
|
+
)
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
degradation_reasons.append(f"ast_error:{type(exc).__name__}")
|
|
195
|
+
|
|
196
|
+
if discovered_files == 0:
|
|
197
|
+
degradation_reasons.append("no_files_discovered")
|
|
198
|
+
if ast_indexed_files == 0:
|
|
199
|
+
degradation_reasons.append("no_files_indexed")
|
|
200
|
+
|
|
201
|
+
lines = [
|
|
202
|
+
"Semantic search status:",
|
|
203
|
+
f" Provider: {mgr.provider_name}",
|
|
204
|
+
f" Available: {mgr.is_available}",
|
|
205
|
+
f" Status: {progress.status}",
|
|
206
|
+
f" Coverage: {progress.coverage:.0%} ({progress.indexed_files}/{progress.total_files} files)",
|
|
207
|
+
f" Failed: {progress.failed_files}",
|
|
208
|
+
f" Vector search active: {mgr.is_index_ready()}",
|
|
209
|
+
f" Discovery count: {discovered_files}",
|
|
210
|
+
f" AST indexed files: {ast_indexed_files}",
|
|
211
|
+
f" Health: {'degraded' if degradation_reasons else 'healthy'}",
|
|
212
|
+
]
|
|
213
|
+
try:
|
|
214
|
+
_store = getattr(mgr, "_store", None)
|
|
215
|
+
_store_model = getattr(_store, "model_name", "") if _store else ""
|
|
216
|
+
if _store_model and _store_model != mgr.provider_name:
|
|
217
|
+
lines.append(
|
|
218
|
+
f" Model mismatch: index built with {_store_model}, active "
|
|
219
|
+
f"{mgr.provider_name} — run reindex to rebuild vectors."
|
|
220
|
+
)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
if degradation_reasons:
|
|
224
|
+
lines.append(f" Degradation reason: {', '.join(degradation_reasons)}")
|
|
225
|
+
if getattr(progress, "degraded_reason", ""):
|
|
226
|
+
lines.append(f" Backend degradation: {progress.degraded_reason}")
|
|
227
|
+
if getattr(progress, "last_error", ""):
|
|
228
|
+
lines.append(f" Last error: {progress.last_error}")
|
|
229
|
+
if progress.elapsed_seconds > 0:
|
|
230
|
+
lines.append(f" Elapsed: {progress.elapsed_seconds:.1f}s")
|
|
231
|
+
return "\n".join(lines)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@mcp.tool()
|
|
235
|
+
def security_scan(
|
|
236
|
+
mode: str = "full",
|
|
237
|
+
path: str = "",
|
|
238
|
+
) -> str:
|
|
239
|
+
"""Scan the codebase for security issues.
|
|
240
|
+
|
|
241
|
+
Detects hardcoded secrets, code anti-patterns, and dependency
|
|
242
|
+
pinning issues. All scanning is local (no external API calls).
|
|
243
|
+
Returns a compliance score (0-100) and categorized findings.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
mode: Scan mode -- 'quick' (secrets), 'full' (all), 'secrets', 'patterns', 'dependencies'.
|
|
247
|
+
path: Subdirectory to scan (relative to project root, empty for all).
|
|
248
|
+
"""
|
|
249
|
+
return _get_service().security_scan(mode=mode, path=path)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@mcp.tool()
|
|
253
|
+
def dataflow_scan(
|
|
254
|
+
files: list[str] | None = None,
|
|
255
|
+
path: str = "",
|
|
256
|
+
) -> str:
|
|
257
|
+
"""Scan for data flow vulnerabilities using taint analysis.
|
|
258
|
+
|
|
259
|
+
Tracks user-controlled data (request parameters, input, argv) through
|
|
260
|
+
variable assignments to dangerous sinks (SQL execution, shell commands,
|
|
261
|
+
file operations, HTML output). Detects SQL injection (CWE-89), command
|
|
262
|
+
injection (CWE-78), XSS (CWE-79), path traversal (CWE-22), and SSRF
|
|
263
|
+
(CWE-918).
|
|
264
|
+
|
|
265
|
+
Currently supports Python and JavaScript/TypeScript. Analysis is
|
|
266
|
+
intra-procedural (within individual functions).
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
files: Specific files to analyze (relative paths). Default: all.
|
|
270
|
+
path: Subdirectory to restrict analysis to.
|
|
271
|
+
"""
|
|
272
|
+
from attocode_intel._internal.integrations.security.dataflow import analyze_project, format_report
|
|
273
|
+
|
|
274
|
+
project_dir = _get_project_dir()
|
|
275
|
+
if path and not files:
|
|
276
|
+
import os
|
|
277
|
+
scan_dir = os.path.join(project_dir, path)
|
|
278
|
+
if not os.path.isdir(scan_dir):
|
|
279
|
+
return f"Error: Path not found: {path}"
|
|
280
|
+
# Discover files in subdirectory
|
|
281
|
+
_EXTS = {".py", ".js", ".ts", ".jsx", ".tsx"}
|
|
282
|
+
files = []
|
|
283
|
+
for dirpath, _, filenames in os.walk(scan_dir):
|
|
284
|
+
for fname in filenames:
|
|
285
|
+
if os.path.splitext(fname)[1].lower() in _EXTS:
|
|
286
|
+
files.append(os.path.relpath(
|
|
287
|
+
os.path.join(dirpath, fname), project_dir,
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
report = analyze_project(project_dir, paths=files or None)
|
|
291
|
+
return format_report(report)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@mcp.tool()
|
|
295
|
+
@pin_stamped
|
|
296
|
+
def fast_search(
|
|
297
|
+
pattern: str,
|
|
298
|
+
path: str = "",
|
|
299
|
+
max_results: int = 50,
|
|
300
|
+
case_insensitive: bool = False,
|
|
301
|
+
selectivity_threshold: float = 0.10,
|
|
302
|
+
explain: bool = False,
|
|
303
|
+
) -> str:
|
|
304
|
+
"""Search file contents with a regular expression. A trigram index selects the candidate files first.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
pattern: A regular expression, for example "def process_.*event".
|
|
308
|
+
path: Optional subdirectory relative to the project root.
|
|
309
|
+
selectivity_threshold: Skip the index when more than this share of files can match (default 0.10).
|
|
310
|
+
explain: Add search diagnostics to the output.
|
|
311
|
+
"""
|
|
312
|
+
remote = _get_remote_service()
|
|
313
|
+
if remote is not None:
|
|
314
|
+
return remote.fast_search(
|
|
315
|
+
pattern=pattern,
|
|
316
|
+
path=path,
|
|
317
|
+
max_results=max_results,
|
|
318
|
+
case_insensitive=case_insensitive,
|
|
319
|
+
selectivity_threshold=selectivity_threshold,
|
|
320
|
+
explain=explain,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
import re
|
|
324
|
+
from pathlib import Path
|
|
325
|
+
|
|
326
|
+
project_dir = _get_project_dir()
|
|
327
|
+
root = Path(project_dir)
|
|
328
|
+
if path:
|
|
329
|
+
root = root / path
|
|
330
|
+
root = root.resolve()
|
|
331
|
+
|
|
332
|
+
if not root.exists():
|
|
333
|
+
return f"Error: Path not found: {root}"
|
|
334
|
+
|
|
335
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
336
|
+
try:
|
|
337
|
+
regex = re.compile(pattern, flags)
|
|
338
|
+
except re.error as e:
|
|
339
|
+
return f"Error: Invalid regex pattern: {e}"
|
|
340
|
+
|
|
341
|
+
# Try trigram pre-filtering
|
|
342
|
+
trigram_idx = _get_trigram_index()
|
|
343
|
+
candidates: list[str] | None = None
|
|
344
|
+
index_status = "no index"
|
|
345
|
+
query_result = None # QueryResult when explain=True
|
|
346
|
+
|
|
347
|
+
if trigram_idx is not None and trigram_idx.is_ready():
|
|
348
|
+
if explain:
|
|
349
|
+
from attocode_intel._internal.integrations.context.trigram_index import QueryResult
|
|
350
|
+
|
|
351
|
+
query_result = trigram_idx.query(
|
|
352
|
+
pattern,
|
|
353
|
+
case_insensitive=case_insensitive,
|
|
354
|
+
selectivity_threshold=selectivity_threshold,
|
|
355
|
+
explain=True,
|
|
356
|
+
)
|
|
357
|
+
candidates = query_result.candidates
|
|
358
|
+
index_status = query_result.mode
|
|
359
|
+
else:
|
|
360
|
+
candidates = trigram_idx.query(
|
|
361
|
+
pattern,
|
|
362
|
+
case_insensitive=case_insensitive,
|
|
363
|
+
selectivity_threshold=selectivity_threshold,
|
|
364
|
+
)
|
|
365
|
+
if candidates is not None:
|
|
366
|
+
index_status = f"trigram filter: {len(candidates)} candidates"
|
|
367
|
+
else:
|
|
368
|
+
index_status = "no trigrams extractable or threshold exceeded, full scan"
|
|
369
|
+
else:
|
|
370
|
+
# Try to build the index on first use
|
|
371
|
+
try:
|
|
372
|
+
from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
|
|
373
|
+
index_dir = os.path.join(project_dir, ".attocode", "index")
|
|
374
|
+
idx = TrigramIndex(index_dir=index_dir)
|
|
375
|
+
stats = idx.build(project_dir)
|
|
376
|
+
global _trigram_index
|
|
377
|
+
_trigram_index = idx
|
|
378
|
+
if explain:
|
|
379
|
+
query_result = idx.query(
|
|
380
|
+
pattern,
|
|
381
|
+
case_insensitive=case_insensitive,
|
|
382
|
+
selectivity_threshold=selectivity_threshold,
|
|
383
|
+
explain=True,
|
|
384
|
+
)
|
|
385
|
+
candidates = query_result.candidates
|
|
386
|
+
index_status = (
|
|
387
|
+
f"built index ({stats['files_indexed']} files, "
|
|
388
|
+
f"{stats['build_time_ms']}ms), mode: {query_result.mode}"
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
candidates = idx.query(
|
|
392
|
+
pattern,
|
|
393
|
+
case_insensitive=case_insensitive,
|
|
394
|
+
selectivity_threshold=selectivity_threshold,
|
|
395
|
+
)
|
|
396
|
+
index_status = (
|
|
397
|
+
f"built index ({stats['files_indexed']} files, "
|
|
398
|
+
f"{stats['build_time_ms']}ms), "
|
|
399
|
+
f"{len(candidates) if candidates is not None else 'N/A'} candidates"
|
|
400
|
+
)
|
|
401
|
+
except Exception:
|
|
402
|
+
index_status = "index build failed, full scan"
|
|
403
|
+
if explain:
|
|
404
|
+
from attocode_intel._internal.integrations.context.trigram_index import QueryResult
|
|
405
|
+
|
|
406
|
+
query_result = QueryResult(
|
|
407
|
+
candidates=None, trigram_literals=[], posting_sizes=[],
|
|
408
|
+
total_files=0, candidate_count=0, selectivity=0.0,
|
|
409
|
+
threshold=selectivity_threshold, threshold_triggered=False,
|
|
410
|
+
mode="index-build-failed",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Determine files to search
|
|
414
|
+
if candidates is not None:
|
|
415
|
+
files = sorted(root / c for c in candidates)
|
|
416
|
+
else:
|
|
417
|
+
files = sorted(root.rglob("*"))
|
|
418
|
+
|
|
419
|
+
matches: list[str] = []
|
|
420
|
+
for file in files:
|
|
421
|
+
if not file.is_file() or file.name.startswith("."):
|
|
422
|
+
continue
|
|
423
|
+
try:
|
|
424
|
+
content = file.read_text(encoding="utf-8", errors="strict")
|
|
425
|
+
except (UnicodeDecodeError, OSError):
|
|
426
|
+
continue
|
|
427
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
428
|
+
if regex.search(line):
|
|
429
|
+
try:
|
|
430
|
+
rel = file.relative_to(Path(project_dir))
|
|
431
|
+
except ValueError:
|
|
432
|
+
rel = file.name
|
|
433
|
+
matches.append(f"{rel}:{i}: {line.strip()}")
|
|
434
|
+
if len(matches) >= max_results:
|
|
435
|
+
break
|
|
436
|
+
if len(matches) >= max_results:
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
if not matches:
|
|
440
|
+
result = f"No matches found ({index_status})"
|
|
441
|
+
else:
|
|
442
|
+
result = "\n".join(matches)
|
|
443
|
+
if len(matches) >= max_results:
|
|
444
|
+
result += f"\n... (limited to {max_results} results)"
|
|
445
|
+
result += f"\n({index_status})"
|
|
446
|
+
|
|
447
|
+
# Append diagnostics when explain=True
|
|
448
|
+
if explain and query_result is not None:
|
|
449
|
+
qr = query_result
|
|
450
|
+
sel_pct = f"{qr.selectivity * 100:.2f}%"
|
|
451
|
+
threshold_pct = f"{qr.threshold * 100:.1f}%"
|
|
452
|
+
verdict = "SKIP (threshold exceeded)" if qr.threshold_triggered else "PASS"
|
|
453
|
+
diag_lines = [
|
|
454
|
+
"",
|
|
455
|
+
"--- Search Diagnostics ---",
|
|
456
|
+
f"Pattern: {pattern}",
|
|
457
|
+
f"Trigrams: {qr.trigram_literals!r}",
|
|
458
|
+
f"Posting sizes: {qr.posting_sizes!r}",
|
|
459
|
+
f"Candidates: {qr.candidate_count} / {qr.total_files} files ({sel_pct})",
|
|
460
|
+
f"Selectivity: {verdict} (threshold: {threshold_pct})",
|
|
461
|
+
f"Mode: {qr.mode}",
|
|
462
|
+
]
|
|
463
|
+
result += "\n".join(diag_lines)
|
|
464
|
+
|
|
465
|
+
return result
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@mcp.tool()
|
|
469
|
+
@pin_stamped
|
|
470
|
+
def regex_search(
|
|
471
|
+
pattern: str,
|
|
472
|
+
path: str = "",
|
|
473
|
+
max_results: int = 50,
|
|
474
|
+
case_insensitive: bool = False,
|
|
475
|
+
) -> str:
|
|
476
|
+
"""Search code with regex, accelerated by trigram index.
|
|
477
|
+
|
|
478
|
+
A straightforward regex search tool that leverages the trigram inverted
|
|
479
|
+
index for 10-100x speedup over brute-force grep. Useful for finding
|
|
480
|
+
specific code patterns like function signatures, import statements,
|
|
481
|
+
or configuration values.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
pattern: Regex pattern to search for.
|
|
485
|
+
path: Subdirectory to restrict search to (relative to project root).
|
|
486
|
+
max_results: Maximum number of matching lines to return.
|
|
487
|
+
case_insensitive: Whether to match case-insensitively.
|
|
488
|
+
"""
|
|
489
|
+
# NOTE: regex_search has no server-side HTTP route, so it cannot be
|
|
490
|
+
# proxied to a remote. It always runs locally (against the trigram index /
|
|
491
|
+
# local files), even when a remote is configured.
|
|
492
|
+
import re
|
|
493
|
+
from pathlib import Path
|
|
494
|
+
|
|
495
|
+
project_dir = _get_project_dir()
|
|
496
|
+
root = Path(project_dir)
|
|
497
|
+
if path:
|
|
498
|
+
root = root / path
|
|
499
|
+
root = root.resolve()
|
|
500
|
+
|
|
501
|
+
if not root.exists():
|
|
502
|
+
return f"Error: Path not found: {root}"
|
|
503
|
+
|
|
504
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
505
|
+
try:
|
|
506
|
+
regex = re.compile(pattern, flags)
|
|
507
|
+
except re.error as e:
|
|
508
|
+
return f"Error: Invalid regex pattern: {e}"
|
|
509
|
+
|
|
510
|
+
# Try trigram pre-filtering
|
|
511
|
+
_SELECTIVITY_THRESHOLD = 0.10
|
|
512
|
+
trigram_idx = _get_trigram_index()
|
|
513
|
+
candidates: list[str] | None = None
|
|
514
|
+
used_index = False
|
|
515
|
+
|
|
516
|
+
if trigram_idx is not None and trigram_idx.is_ready():
|
|
517
|
+
candidates = trigram_idx.query(
|
|
518
|
+
pattern,
|
|
519
|
+
case_insensitive=case_insensitive,
|
|
520
|
+
selectivity_threshold=_SELECTIVITY_THRESHOLD,
|
|
521
|
+
)
|
|
522
|
+
if candidates is not None:
|
|
523
|
+
used_index = True
|
|
524
|
+
|
|
525
|
+
# Determine files to search
|
|
526
|
+
if candidates is not None:
|
|
527
|
+
files = sorted(root / c for c in candidates)
|
|
528
|
+
else:
|
|
529
|
+
files = sorted(root.rglob("*"))
|
|
530
|
+
|
|
531
|
+
matches: list[str] = []
|
|
532
|
+
for file in files:
|
|
533
|
+
if not file.is_file() or file.name.startswith("."):
|
|
534
|
+
continue
|
|
535
|
+
try:
|
|
536
|
+
content = file.read_text(encoding="utf-8", errors="strict")
|
|
537
|
+
except (UnicodeDecodeError, OSError):
|
|
538
|
+
continue
|
|
539
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
540
|
+
if regex.search(line):
|
|
541
|
+
try:
|
|
542
|
+
rel = file.relative_to(Path(project_dir))
|
|
543
|
+
except ValueError:
|
|
544
|
+
rel = file.name
|
|
545
|
+
matches.append(f"{rel}:{i}: {line.strip()}")
|
|
546
|
+
if len(matches) >= max_results:
|
|
547
|
+
break
|
|
548
|
+
if len(matches) >= max_results:
|
|
549
|
+
break
|
|
550
|
+
|
|
551
|
+
total = len(matches)
|
|
552
|
+
if not matches:
|
|
553
|
+
return "No matches found."
|
|
554
|
+
|
|
555
|
+
result = "\n".join(matches)
|
|
556
|
+
if total >= max_results:
|
|
557
|
+
result += f"\n... (limited to {max_results} results)"
|
|
558
|
+
index_note = "trigram-indexed" if used_index else "brute-force"
|
|
559
|
+
result += f"\n{total} match(es) found ({index_note})."
|
|
560
|
+
return result
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@mcp.tool()
|
|
564
|
+
@pin_stamped
|
|
565
|
+
def frecency_search(
|
|
566
|
+
pattern: str,
|
|
567
|
+
path: str = "",
|
|
568
|
+
max_results: int = 50,
|
|
569
|
+
case_insensitive: bool = False,
|
|
570
|
+
selectivity_threshold: float = 0.10,
|
|
571
|
+
use_frecency: bool = True,
|
|
572
|
+
ai_mode: bool = True,
|
|
573
|
+
) -> str:
|
|
574
|
+
"""Frecency-boosted regex search for AI agents.
|
|
575
|
+
|
|
576
|
+
Like fast_search but boosts results by frecency score (how often and
|
|
577
|
+
recently files were accessed). This helps prioritize commonly-used files
|
|
578
|
+
when there are many matches.
|
|
579
|
+
|
|
580
|
+
The frecency score is calculated from:
|
|
581
|
+
- Access frequency (exponential decay with 10-day or 3-day half-life)
|
|
582
|
+
- Recency (more recent = higher score)
|
|
583
|
+
- Git modification bonus (recently modified files get +1-16 points)
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
pattern: Regex pattern to search for.
|
|
587
|
+
path: Subdirectory to search (relative to project root, empty for all).
|
|
588
|
+
max_results: Maximum number of matching lines to return (default 50).
|
|
589
|
+
case_insensitive: Whether to match case-insensitively.
|
|
590
|
+
selectivity_threshold: Skip trigram index when matching files exceeds
|
|
591
|
+
this fraction (0.0-1.0, default 0.10).
|
|
592
|
+
use_frecency: Whether to apply frecency boosting (default True).
|
|
593
|
+
ai_mode: Use AI mode decay (3-day half-life) vs human mode (10-day).
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Search results with frecency-boosted ranking.
|
|
597
|
+
"""
|
|
598
|
+
import re
|
|
599
|
+
from collections import defaultdict
|
|
600
|
+
from pathlib import Path
|
|
601
|
+
|
|
602
|
+
project_dir = _get_project_dir()
|
|
603
|
+
root = Path(project_dir)
|
|
604
|
+
if path:
|
|
605
|
+
root = root / path
|
|
606
|
+
root = root.resolve()
|
|
607
|
+
|
|
608
|
+
if not root.exists():
|
|
609
|
+
return f"Error: Path not found: {root}"
|
|
610
|
+
|
|
611
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
612
|
+
try:
|
|
613
|
+
regex = re.compile(pattern, flags)
|
|
614
|
+
except re.error as e:
|
|
615
|
+
return f"Error: Invalid regex pattern: {e}"
|
|
616
|
+
|
|
617
|
+
# Get trigram candidates
|
|
618
|
+
trigram_idx = _get_trigram_index()
|
|
619
|
+
candidates: list[str] | None = None
|
|
620
|
+
index_status = "no index"
|
|
621
|
+
|
|
622
|
+
if trigram_idx is not None and trigram_idx.is_ready():
|
|
623
|
+
candidates = trigram_idx.query(
|
|
624
|
+
pattern,
|
|
625
|
+
case_insensitive=case_insensitive,
|
|
626
|
+
selectivity_threshold=selectivity_threshold,
|
|
627
|
+
)
|
|
628
|
+
if candidates is not None:
|
|
629
|
+
index_status = f"trigram filter: {len(candidates)} candidates"
|
|
630
|
+
else:
|
|
631
|
+
index_status = "no trigrams, full scan"
|
|
632
|
+
else:
|
|
633
|
+
index_status = "no index, full scan"
|
|
634
|
+
|
|
635
|
+
# Get frecency scores
|
|
636
|
+
frecency_tracker = _get_frecency_tracker() if use_frecency else None
|
|
637
|
+
|
|
638
|
+
# Determine files to search
|
|
639
|
+
if candidates is not None:
|
|
640
|
+
files = sorted(root / c for c in candidates)
|
|
641
|
+
else:
|
|
642
|
+
all_files = sorted(root.rglob("*"))
|
|
643
|
+
try:
|
|
644
|
+
from attocode_intel._internal.integrations.utilities.ignore import IgnoreManager
|
|
645
|
+
|
|
646
|
+
ignore_mgr = IgnoreManager(root=Path(project_dir))
|
|
647
|
+
files = [
|
|
648
|
+
f
|
|
649
|
+
for f in all_files
|
|
650
|
+
if f.is_file()
|
|
651
|
+
and not f.name.startswith(".")
|
|
652
|
+
and not ignore_mgr.is_ignored(
|
|
653
|
+
str(f.relative_to(Path(project_dir)))
|
|
654
|
+
)
|
|
655
|
+
]
|
|
656
|
+
except (ImportError, ValueError):
|
|
657
|
+
files = [
|
|
658
|
+
f
|
|
659
|
+
for f in all_files
|
|
660
|
+
if f.is_file() and not f.name.startswith(".")
|
|
661
|
+
]
|
|
662
|
+
|
|
663
|
+
# Phase 1: Collect candidate file paths
|
|
664
|
+
candidate_paths: list[Path] = []
|
|
665
|
+
for file in files:
|
|
666
|
+
if not file.is_file() or file.name.startswith("."):
|
|
667
|
+
continue
|
|
668
|
+
candidate_paths.append(file)
|
|
669
|
+
|
|
670
|
+
# Phase 2: Pre-sort by frecency so high-value files are searched first
|
|
671
|
+
_FRECENCY_PRESORT_LIMIT = 10_000
|
|
672
|
+
file_scores: dict[str, FrecencyResult] = {}
|
|
673
|
+
if (
|
|
674
|
+
frecency_tracker is not None
|
|
675
|
+
and use_frecency
|
|
676
|
+
and len(candidate_paths) <= _FRECENCY_PRESORT_LIMIT
|
|
677
|
+
):
|
|
678
|
+
path_strs: list[str] = []
|
|
679
|
+
path_map: dict[str, Path] = {}
|
|
680
|
+
for f in candidate_paths:
|
|
681
|
+
try:
|
|
682
|
+
rel = str(f.relative_to(Path(project_dir)))
|
|
683
|
+
except ValueError:
|
|
684
|
+
rel = str(f)
|
|
685
|
+
path_strs.append(rel)
|
|
686
|
+
path_map[rel] = f
|
|
687
|
+
|
|
688
|
+
file_scores = frecency_tracker.get_scores_batch(path_strs, ai_mode=ai_mode)
|
|
689
|
+
|
|
690
|
+
# Build reverse lookup to avoid recomputing relative_to() in sort
|
|
691
|
+
path_to_rel: dict[Path, str] = {v: k for k, v in path_map.items()}
|
|
692
|
+
|
|
693
|
+
# Sort: high-frecency files first, then alphabetical as tiebreaker
|
|
694
|
+
candidate_paths.sort(
|
|
695
|
+
key=lambda f: (
|
|
696
|
+
-file_scores.get(
|
|
697
|
+
path_to_rel.get(f, str(f)),
|
|
698
|
+
_empty_frecency(ai_mode),
|
|
699
|
+
).score,
|
|
700
|
+
str(f),
|
|
701
|
+
),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# Phase 3: Read content from frecency-sorted files
|
|
705
|
+
file_matches: dict[str, list[tuple[int, str]]] = defaultdict(list)
|
|
706
|
+
|
|
707
|
+
for file in candidate_paths:
|
|
708
|
+
try:
|
|
709
|
+
content = file.read_text(encoding="utf-8", errors="strict")
|
|
710
|
+
except (UnicodeDecodeError, OSError):
|
|
711
|
+
continue
|
|
712
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
713
|
+
if regex.search(line):
|
|
714
|
+
try:
|
|
715
|
+
rel = str(file.relative_to(Path(project_dir)))
|
|
716
|
+
except ValueError:
|
|
717
|
+
rel = file.name
|
|
718
|
+
file_matches[rel].append((i, line.strip()))
|
|
719
|
+
if sum(len(v) for v in file_matches.values()) >= max_results * 3:
|
|
720
|
+
break
|
|
721
|
+
if sum(len(v) for v in file_matches.values()) >= max_results * 3:
|
|
722
|
+
break
|
|
723
|
+
|
|
724
|
+
if not file_matches:
|
|
725
|
+
return f"No matches found ({index_status})"
|
|
726
|
+
|
|
727
|
+
# Fetch scores for any files not yet scored (e.g. from trigram path)
|
|
728
|
+
unscored = [p for p in file_matches if p not in file_scores]
|
|
729
|
+
if unscored and frecency_tracker is not None:
|
|
730
|
+
file_scores.update(
|
|
731
|
+
frecency_tracker.get_scores_batch(unscored, ai_mode=ai_mode)
|
|
732
|
+
)
|
|
733
|
+
# Fill in defaults for unscored files
|
|
734
|
+
for p in file_matches:
|
|
735
|
+
if p not in file_scores:
|
|
736
|
+
file_scores[p] = _empty_frecency(ai_mode)
|
|
737
|
+
|
|
738
|
+
# Sort files by frecency score (descending), then by match count
|
|
739
|
+
sorted_files = sorted(
|
|
740
|
+
file_matches.items(),
|
|
741
|
+
key=lambda kv: (
|
|
742
|
+
-file_scores.get(kv[0], _empty_frecency(ai_mode)).score,
|
|
743
|
+
-len(kv[1]),
|
|
744
|
+
),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Build results respecting max_results
|
|
748
|
+
results: list[str] = []
|
|
749
|
+
for file_path, matches in sorted_files:
|
|
750
|
+
score = file_scores.get(file_path, _empty_frecency(ai_mode)).score
|
|
751
|
+
for line_num, line_content in matches[:5]: # Max 5 matches per file
|
|
752
|
+
results.append(f"{file_path}:{line_num}: [{score}] {line_content}")
|
|
753
|
+
if len(results) >= max_results:
|
|
754
|
+
break
|
|
755
|
+
if len(results) >= max_results:
|
|
756
|
+
break
|
|
757
|
+
|
|
758
|
+
if not results:
|
|
759
|
+
return f"No matches found ({index_status})"
|
|
760
|
+
|
|
761
|
+
result = "\n".join(results)
|
|
762
|
+
if len(results) >= max_results:
|
|
763
|
+
result += f"\n... (limited to {max_results} results)"
|
|
764
|
+
|
|
765
|
+
# Add frecency stats header
|
|
766
|
+
total_files = len(file_matches)
|
|
767
|
+
avg_score = sum(s.score for s in file_scores.values()) / max(len(file_scores), 1)
|
|
768
|
+
result += f"\n({index_status}, {total_files} files matched, avg frecency: {avg_score:.1f})"
|
|
769
|
+
|
|
770
|
+
return result
|