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,185 @@
|
|
|
1
|
+
"""Standalone CLI; importing it does not load the agent or analysis engines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
COMMANDS = """commands:
|
|
14
|
+
init Add the server to Codex, Claude Code or Cursor (--client)
|
|
15
|
+
doctor Start the configured server and check that it answers
|
|
16
|
+
install <target> Add the server to another client, for example vscode or zed
|
|
17
|
+
share Send a learning to a team server
|
|
18
|
+
query <text> Search the code
|
|
19
|
+
symbols <file> List the symbols of a file, or find one with --search <name>
|
|
20
|
+
impact <file>... Show the files that a change to these files can affect
|
|
21
|
+
hotspots Show the files with the most risk or complexity
|
|
22
|
+
deps <file> Show what a file imports and what imports it
|
|
23
|
+
commands Show all commands, with maintenance commands and targets
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def exit_when_client_exits(interval: float = 5.0) -> None:
|
|
28
|
+
"""Stop a stdio server when the client process that started it is gone.
|
|
29
|
+
|
|
30
|
+
At end of input the MCP SDK still waits for running tool calls. A call
|
|
31
|
+
that never ended kept an orphaned server alive, at full CPU, for days.
|
|
32
|
+
SQLite indexes stay consistent after an abrupt exit. The watchdog does not
|
|
33
|
+
write a message: stderr usually goes to the closed client, and a write
|
|
34
|
+
error would stop the thread before the exit.
|
|
35
|
+
"""
|
|
36
|
+
parent = os.getppid()
|
|
37
|
+
|
|
38
|
+
def watch() -> None:
|
|
39
|
+
while os.getppid() == parent:
|
|
40
|
+
time.sleep(interval)
|
|
41
|
+
os._exit(0)
|
|
42
|
+
|
|
43
|
+
threading.Thread(target=watch, name="client-watchdog", daemon=True).start()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv=None):
|
|
47
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
48
|
+
if args and args[0] == "share":
|
|
49
|
+
from attocode_intel.sharing import share
|
|
50
|
+
|
|
51
|
+
share(args[1:])
|
|
52
|
+
return
|
|
53
|
+
if args and args[0] in {"init", "doctor"}:
|
|
54
|
+
command = args.pop(0)
|
|
55
|
+
parser = argparse.ArgumentParser(prog=f"attocode-code-intel {command}")
|
|
56
|
+
parser.add_argument("--client", choices=["codex", "claude", "cursor", "all"], default="all")
|
|
57
|
+
parser.add_argument("--project", default=".")
|
|
58
|
+
parser.add_argument("--global", dest="global_scope", action="store_true")
|
|
59
|
+
parser.add_argument("--profile", choices=["daily", "full"], default="daily")
|
|
60
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
61
|
+
parser.add_argument("--remove", action="store_true")
|
|
62
|
+
parser.add_argument("--server", default="")
|
|
63
|
+
parser.add_argument("--repo", default="")
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--remote", action="store_true", help="Doctor/remove: select the remote integration"
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("--token-env", default="ATTOCODE_API_KEY")
|
|
68
|
+
opts = parser.parse_args(args)
|
|
69
|
+
from attocode_intel.onboarding import configure_client, diagnose
|
|
70
|
+
|
|
71
|
+
clients = ["codex", "claude", "cursor"] if opts.client == "all" else [opts.client]
|
|
72
|
+
failed, ready = False, 0
|
|
73
|
+
for client in clients:
|
|
74
|
+
try:
|
|
75
|
+
if command == "init":
|
|
76
|
+
result = configure_client(
|
|
77
|
+
client,
|
|
78
|
+
opts.project,
|
|
79
|
+
global_scope=opts.global_scope,
|
|
80
|
+
profile=opts.profile,
|
|
81
|
+
dry_run=opts.dry_run,
|
|
82
|
+
remove=opts.remove,
|
|
83
|
+
server=opts.server,
|
|
84
|
+
token_env=opts.token_env,
|
|
85
|
+
repo=opts.repo or ("remote" if opts.remote else ""),
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
result = asyncio.run(
|
|
89
|
+
diagnose(
|
|
90
|
+
client,
|
|
91
|
+
opts.project,
|
|
92
|
+
global_scope=opts.global_scope,
|
|
93
|
+
remote=bool(opts.remote or opts.server or opts.repo),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
print(json.dumps(result, indent=2))
|
|
97
|
+
ready += result.get("status") != "not configured"
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
# A timeout has no message of its own.
|
|
100
|
+
reason = "the server did not answer in 30 s" if isinstance(exc, TimeoutError) else exc
|
|
101
|
+
print(f"{client}: {reason}", file=sys.stderr)
|
|
102
|
+
failed = True
|
|
103
|
+
# With --client all, a client without configuration is not a failure, but one client must be ready.
|
|
104
|
+
if failed or not ready:
|
|
105
|
+
raise SystemExit(1)
|
|
106
|
+
return
|
|
107
|
+
if args and args[0] not in {"serve", "help"} and not args[0].startswith("-"):
|
|
108
|
+
from attocode_intel.cli import dispatch_code_intel
|
|
109
|
+
|
|
110
|
+
dispatch_code_intel(args)
|
|
111
|
+
return
|
|
112
|
+
if args and args[0] == "serve":
|
|
113
|
+
args.pop(0)
|
|
114
|
+
if args == ["help"]:
|
|
115
|
+
args = ["--help"]
|
|
116
|
+
parser = argparse.ArgumentParser(
|
|
117
|
+
prog="attocode-code-intel",
|
|
118
|
+
description="Code search and navigation for coding agents and for people. "
|
|
119
|
+
"Without a command, it runs the MCP server.",
|
|
120
|
+
epilog=COMMANDS,
|
|
121
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument("--project", default=os.environ.get("ATTOCODE_PROJECT_DIR", ""))
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--profile", choices=["daily", "full"], default="daily",
|
|
126
|
+
help="daily: a short tool list for agents (default); full: all tools",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--transport", choices=["stdio", "sse", "http", "streamable-http"], default="stdio"
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
132
|
+
parser.add_argument("--port", type=int, default=8080)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--local-only", action="store_true", help="Accepted for old configurations; it has no effect"
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument("--no-watch", action="store_true")
|
|
137
|
+
parser.add_argument("--watch-debounce", type=int, default=500)
|
|
138
|
+
from attocode_intel import __version__
|
|
139
|
+
|
|
140
|
+
parser.add_argument("--version", action="version", version=f"attocode-code-intel {__version__}")
|
|
141
|
+
opts = parser.parse_args(args)
|
|
142
|
+
if opts.transport in {"http", "streamable-http"}:
|
|
143
|
+
from attocode_intel.cli import _serve_http
|
|
144
|
+
|
|
145
|
+
os.environ["ATTOCODE_MCP_PROFILE"] = opts.profile
|
|
146
|
+
os.environ["ATTOCODE_INTEL_WATCH"] = "0" if opts.no_watch else "1"
|
|
147
|
+
os.environ["ATTOCODE_INTEL_WATCH_DEBOUNCE"] = str(opts.watch_debounce)
|
|
148
|
+
_serve_http(opts.project, host=opts.host, port=opts.port, debug=False)
|
|
149
|
+
elif opts.transport == "sse":
|
|
150
|
+
from attocode_intel.config import is_loopback
|
|
151
|
+
|
|
152
|
+
if not is_loopback(opts.host):
|
|
153
|
+
# SSE has no sign-in, so any computer that reaches the port could read the index.
|
|
154
|
+
raise SystemExit("Error: SSE serves only this computer. For other hosts, use "
|
|
155
|
+
"--transport http with ATTOCODE_API_KEY.")
|
|
156
|
+
# ponytail: SSE runs the older server, which lists all tools. Use stdio or http for profiles.
|
|
157
|
+
from attocode_intel.server import mcp
|
|
158
|
+
|
|
159
|
+
mcp.settings.host, mcp.settings.port = opts.host, opts.port
|
|
160
|
+
if opts.project:
|
|
161
|
+
os.environ["ATTOCODE_PROJECT_DIR"] = opts.project
|
|
162
|
+
mcp.run(transport="sse")
|
|
163
|
+
else:
|
|
164
|
+
asyncio.run(_stdio(opts))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _stdio(opts):
|
|
168
|
+
from mcp.server.stdio import stdio_server
|
|
169
|
+
|
|
170
|
+
from attocode_intel.gateway import OperationGateway, create_mcp_server
|
|
171
|
+
|
|
172
|
+
gateway = OperationGateway(
|
|
173
|
+
opts.project, opts.profile, watch=not opts.no_watch, watch_debounce=opts.watch_debounce
|
|
174
|
+
)
|
|
175
|
+
server = create_mcp_server(gateway)
|
|
176
|
+
exit_when_client_exits()
|
|
177
|
+
try:
|
|
178
|
+
async with stdio_server() as (read, write):
|
|
179
|
+
await server.run(read, write, server.create_initialization_options())
|
|
180
|
+
finally:
|
|
181
|
+
await gateway.close()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Deterministic lexical evidence selection; excerpts are never stitched together."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import re
|
|
6
|
+
import tokenize
|
|
7
|
+
from collections import Counter
|
|
8
|
+
|
|
9
|
+
STOP_WORDS = frozenset({"a", "an", "and", "are", "as", "at", "be", "by", "code", "def", "do", "does", "for", "from", "function", "get", "how", "in", "is", "it", "method", "of", "on", "or", "return", "self", "test", "tests", "the", "this", "to", "use", "value", "with"})
|
|
10
|
+
_EXCLUSION = re.compile(r"\b(?:without|excluding|exclude|except|avoid)\b", re.IGNORECASE)
|
|
11
|
+
_EXCLUSION_END = re.compile(r"[,;.!?]|\b(?:but|instead|rather|while)\b", re.IGNORECASE)
|
|
12
|
+
_EXCLUSION_MODIFIERS = frozenset({"unrelated", "irrelevant", "unwanted", "other"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def terms(text, fold=True):
|
|
16
|
+
"""Query or evidence words; ``fold`` cuts a plural "s", so both sides must use the same rule."""
|
|
17
|
+
text = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", text)
|
|
18
|
+
words = re.findall(r"[a-zA-Z][a-zA-Z0-9]*", text.lower())
|
|
19
|
+
return {word[:-1] if fold and word.endswith("s") and not word.endswith("ss") and len(word) > 4 else word
|
|
20
|
+
for word in words if len(word) > 2 and word not in STOP_WORDS}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def task_terms(text: str, fold: bool = True) -> tuple[set[str], set[str]]:
|
|
24
|
+
"""Separate explicit exclusions from positive task terms.
|
|
25
|
+
|
|
26
|
+
The negative set is a *ranking hint*, never a hard filter. Deliberately
|
|
27
|
+
recognize only explicit exclusion words: ordinary negation such as
|
|
28
|
+
``not found`` often describes the bug rather than irrelevant code.
|
|
29
|
+
"""
|
|
30
|
+
positive_parts: list[str] = []
|
|
31
|
+
negative_parts: list[str] = []
|
|
32
|
+
remaining = text
|
|
33
|
+
while match := _EXCLUSION.search(remaining):
|
|
34
|
+
positive_parts.append(remaining[:match.start()])
|
|
35
|
+
tail = remaining[match.end():]
|
|
36
|
+
end = _EXCLUSION_END.search(tail)
|
|
37
|
+
if end is None:
|
|
38
|
+
negative_parts.append(tail)
|
|
39
|
+
remaining = ""
|
|
40
|
+
break
|
|
41
|
+
negative_parts.append(tail[:end.start()])
|
|
42
|
+
remaining = tail[end.end():]
|
|
43
|
+
positive_parts.append(remaining)
|
|
44
|
+
positive = terms(" ".join(positive_parts), fold) - _EXCLUSION_MODIFIERS
|
|
45
|
+
negative = terms(" ".join(negative_parts), fold) - _EXCLUSION_MODIFIERS
|
|
46
|
+
return positive, negative
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def file_excerpt(lines: list[str], path: str, query: str) -> str:
|
|
50
|
+
"""Query-focused file evidence for a shortlist decision.
|
|
51
|
+
|
|
52
|
+
The product reranker and the eval trials show a model this same text. It
|
|
53
|
+
has up to two numbered windows around the lines with the most task terms.
|
|
54
|
+
"""
|
|
55
|
+
if not lines:
|
|
56
|
+
return f"File: {path}\n(empty file)"
|
|
57
|
+
positive, _ = task_terms(query)
|
|
58
|
+
centers: list[int] = []
|
|
59
|
+
scored = sorted(((len(terms(line) & positive), -index, index)
|
|
60
|
+
for index, line in enumerate(lines)), reverse=True)
|
|
61
|
+
for overlap, _order, index in scored:
|
|
62
|
+
if overlap == 0 and centers:
|
|
63
|
+
break
|
|
64
|
+
if all(abs(index - old) > 24 for old in centers):
|
|
65
|
+
centers.append(index)
|
|
66
|
+
if len(centers) == 2:
|
|
67
|
+
break
|
|
68
|
+
windows = []
|
|
69
|
+
for center in sorted(centers or [0]):
|
|
70
|
+
start, end = max(0, center - 8), min(len(lines), center + 9)
|
|
71
|
+
windows.append("\n".join(f"{number + 1}: {lines[number]}"
|
|
72
|
+
for number in range(start, end)))
|
|
73
|
+
return (f"File: {path}\n" + "\n...\n".join(windows))[:1350]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def source_excerpts(source, definition, task_hint, *, preview_end=0):
|
|
77
|
+
"""Rank code identifiers, favor branches/assignments, and return disjoint ranges.
|
|
78
|
+
|
|
79
|
+
This is a lexical aid, not semantic relevance or complete control-flow coverage.
|
|
80
|
+
Python strings/comments do not become evidence just by repeating query words.
|
|
81
|
+
"""
|
|
82
|
+
from attocode_intel.structural_evidence import structural_excerpts
|
|
83
|
+
structured = structural_excerpts(source, definition, task_hint, preview_end)
|
|
84
|
+
if structured is not None:
|
|
85
|
+
return structured
|
|
86
|
+
query, _ = task_terms(task_hint)
|
|
87
|
+
if not query:
|
|
88
|
+
return []
|
|
89
|
+
first, last = definition.start_line, min(definition.end_line, len(source))
|
|
90
|
+
identifiers = {}
|
|
91
|
+
if definition.file_path.endswith(".py"):
|
|
92
|
+
try:
|
|
93
|
+
for token in tokenize.generate_tokens(io.StringIO("\n".join(source)).readline):
|
|
94
|
+
if token.type == tokenize.NAME and first <= token.start[0] <= last:
|
|
95
|
+
identifiers.setdefault(token.start[0], []).append(token.string)
|
|
96
|
+
except (tokenize.TokenError, IndentationError, SyntaxError):
|
|
97
|
+
pass # Only retain tokens actually read; do not invent coverage.
|
|
98
|
+
else:
|
|
99
|
+
for number in range(first, last + 1):
|
|
100
|
+
line = source[number - 1].strip()
|
|
101
|
+
if line.startswith(("#", "//", "/*", "*")):
|
|
102
|
+
continue
|
|
103
|
+
code = re.sub(r"(['\"]).*?\1", "", line).split("//", 1)[0]
|
|
104
|
+
identifiers[number] = re.findall(r"[A-Za-z_][A-Za-z_0-9]*", code)
|
|
105
|
+
candidates = []
|
|
106
|
+
for number, names in identifiers.items():
|
|
107
|
+
if number <= preview_end:
|
|
108
|
+
continue # The caller already supplies this source verbatim.
|
|
109
|
+
hits = Counter(term for name in names for term in terms(name) if term in query)
|
|
110
|
+
if not hits:
|
|
111
|
+
continue
|
|
112
|
+
code = source[number - 1].strip()
|
|
113
|
+
# Guards and stores often express behavior more directly than annotations.
|
|
114
|
+
action = bool(re.match(r"(?:if|elif|else|return|case|throw|raise)\b", code) or
|
|
115
|
+
(not code.endswith(",") and re.search(r"(?<![=!<>])=(?!=)", code)))
|
|
116
|
+
# Multiple participating identifiers help distinguish access from simple
|
|
117
|
+
# initialization, but repetition is capped so annotations cannot dominate.
|
|
118
|
+
score = sum(min(count, 2) for count in hits.values()) + 2 * int(action)
|
|
119
|
+
candidates.append((number, score, set(hits)))
|
|
120
|
+
selected, covered = [], set()
|
|
121
|
+
while candidates and len(selected) < 3:
|
|
122
|
+
number, _, hits = max(candidates, key=lambda row: (row[1] + len(row[2] - covered), -row[0]))
|
|
123
|
+
start, end = max(first, preview_end + 1, number - 1), min(last, number + 3)
|
|
124
|
+
selected.append({"file_path": definition.file_path, "start_line": start, "end_line": end,
|
|
125
|
+
"text": "\n".join(source[start - 1:end]), "matched_terms": sorted(hits), "context_ranges": [],
|
|
126
|
+
"context_incomplete": True, "context_relation": "structural context unavailable"})
|
|
127
|
+
covered.update(hits)
|
|
128
|
+
candidates = [row for row in candidates if min(last, row[0] + 3) < start or max(first, row[0] - 1) > end]
|
|
129
|
+
# Keep relevance ordering, so output budgeting drops lower ranked whole excerpts.
|
|
130
|
+
return selected
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Incremental invalidation based on filesystem identity, including import-only edits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from attocode_intel._internal.integrations.utilities.ignore import IgnoreManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FreshnessTracker:
|
|
14
|
+
def __init__(self, root: str):
|
|
15
|
+
self.root = root
|
|
16
|
+
self.manifest = None
|
|
17
|
+
self.generation = 0
|
|
18
|
+
self._ignore = None
|
|
19
|
+
self._ignore_identity = None
|
|
20
|
+
self._eligibility = {}
|
|
21
|
+
|
|
22
|
+
def scan_manifest(self):
|
|
23
|
+
"""Stat every eligible file, reusing only path-filter decisions.
|
|
24
|
+
|
|
25
|
+
No time window or watcher delivery is required for edit detection.
|
|
26
|
+
DirEntry avoids allocating/resolving a Path for each file on each call.
|
|
27
|
+
"""
|
|
28
|
+
from attocode_intel._internal.integrations.context.codebase_context import (
|
|
29
|
+
SKIP_EXTENSIONS,
|
|
30
|
+
SKIP_FILENAMES,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
stat = os.stat(os.path.join(self.root, ".gitignore"))
|
|
35
|
+
ignore_identity = (stat.st_mtime_ns, stat.st_ctime_ns, stat.st_size)
|
|
36
|
+
except OSError:
|
|
37
|
+
ignore_identity = None
|
|
38
|
+
if self._ignore is None or ignore_identity != self._ignore_identity:
|
|
39
|
+
self._ignore = IgnoreManager(self.root)
|
|
40
|
+
self._ignore_identity = ignore_identity
|
|
41
|
+
self._eligibility.clear()
|
|
42
|
+
ignore = self._ignore
|
|
43
|
+
manifest = {}
|
|
44
|
+
eligibility = {}
|
|
45
|
+
pending = [(self.root, "")]
|
|
46
|
+
while pending:
|
|
47
|
+
directory, prefix = pending.pop()
|
|
48
|
+
try:
|
|
49
|
+
with os.scandir(directory) as entries:
|
|
50
|
+
for entry in entries:
|
|
51
|
+
name = entry.name
|
|
52
|
+
if name.startswith("."):
|
|
53
|
+
continue
|
|
54
|
+
rel = prefix + name
|
|
55
|
+
try:
|
|
56
|
+
is_dir = entry.is_dir()
|
|
57
|
+
key = rel + "/" if is_dir else rel
|
|
58
|
+
allowed = self._eligibility.get(key)
|
|
59
|
+
if allowed is None:
|
|
60
|
+
allowed = (is_dir or (
|
|
61
|
+
name not in SKIP_FILENAMES
|
|
62
|
+
and os.path.splitext(name)[1].lower() not in SKIP_EXTENSIONS
|
|
63
|
+
)) and not ignore.is_ignored(key)
|
|
64
|
+
eligibility[key] = allowed
|
|
65
|
+
if not allowed:
|
|
66
|
+
continue
|
|
67
|
+
if is_dir:
|
|
68
|
+
if not entry.is_symlink():
|
|
69
|
+
pending.append((entry.path, key))
|
|
70
|
+
else:
|
|
71
|
+
stat = entry.stat()
|
|
72
|
+
if stat.st_size <= 1_000_000:
|
|
73
|
+
manifest[rel] = (stat.st_mtime_ns, stat.st_ctime_ns, stat.st_size)
|
|
74
|
+
except OSError:
|
|
75
|
+
continue
|
|
76
|
+
except OSError:
|
|
77
|
+
continue
|
|
78
|
+
self._eligibility = eligibility
|
|
79
|
+
return manifest
|
|
80
|
+
|
|
81
|
+
def refresh(self, service):
|
|
82
|
+
manifest = self.scan_manifest()
|
|
83
|
+
previous = self.manifest
|
|
84
|
+
if previous is None or service._ast_service is None:
|
|
85
|
+
self.manifest = manifest
|
|
86
|
+
return
|
|
87
|
+
changed = {
|
|
88
|
+
p for p in previous.keys() | manifest.keys() if previous.get(p) != manifest.get(p)
|
|
89
|
+
}
|
|
90
|
+
if not changed:
|
|
91
|
+
self.manifest = manifest
|
|
92
|
+
return
|
|
93
|
+
self.generation += 1
|
|
94
|
+
from attocode_intel.request_context import current_request
|
|
95
|
+
request = current_request.get()
|
|
96
|
+
if request and (precision := request.stores.get("precision")):
|
|
97
|
+
precision.invalidate(changed)
|
|
98
|
+
if request:
|
|
99
|
+
request.stores.pop("reference_pages", None)
|
|
100
|
+
ast = service._ast_service
|
|
101
|
+
if not ast.repair_changed_files(expected_manifest=manifest):
|
|
102
|
+
ast.stop_hydration()
|
|
103
|
+
if previous.keys() != manifest.keys():
|
|
104
|
+
ast._context_mgr.discover_files()
|
|
105
|
+
for path in changed:
|
|
106
|
+
if path not in manifest:
|
|
107
|
+
ast._store.set_meta("structural_snapshot_v1", "")
|
|
108
|
+
ast._snapshot_start = None
|
|
109
|
+
ast._index.remove_file(path)
|
|
110
|
+
ast._ast_cache.pop(path, None)
|
|
111
|
+
ast._reference_indexed_files.discard(path)
|
|
112
|
+
else:
|
|
113
|
+
ast.notify_file_changed(path)
|
|
114
|
+
graph = ast._context_mgr.dependency_graph
|
|
115
|
+
if graph:
|
|
116
|
+
ast._index.file_dependencies.clear()
|
|
117
|
+
ast._index.file_dependents.clear()
|
|
118
|
+
for source, targets in graph.forward.items():
|
|
119
|
+
for target in targets:
|
|
120
|
+
ast._index.add_file_dependency(source, target)
|
|
121
|
+
if ast._hydration_state and ast._hydration_state.phase != "ready":
|
|
122
|
+
ast.start_hydration()
|
|
123
|
+
for path in changed:
|
|
124
|
+
if service._semantic_search:
|
|
125
|
+
service._semantic_search.invalidate_file(str(Path(self.root) / path))
|
|
126
|
+
service._semantic_search._kw_index_built = False
|
|
127
|
+
self.manifest = manifest
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WorkspaceWatcher:
|
|
131
|
+
"""Watch only; refreshes run on the workspace's serial worker."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, root, callback, debounce=500):
|
|
134
|
+
self.stop_event = threading.Event()
|
|
135
|
+
self.thread = threading.Thread(
|
|
136
|
+
target=self._run, args=(root, callback, debounce), daemon=True, name="intel-watch"
|
|
137
|
+
)
|
|
138
|
+
self.thread.start()
|
|
139
|
+
|
|
140
|
+
def _run(self, root, callback, debounce):
|
|
141
|
+
try:
|
|
142
|
+
from watchfiles import watch
|
|
143
|
+
|
|
144
|
+
for changes in watch(root, stop_event=self.stop_event, debounce=max(debounce, 50)):
|
|
145
|
+
if any(".attocode" not in Path(path).parts for _, path in changes):
|
|
146
|
+
callback()
|
|
147
|
+
except Exception:
|
|
148
|
+
logging.getLogger(__name__).warning(
|
|
149
|
+
"File watcher unavailable; per-operation stat checks remain active", exc_info=True
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def close(self):
|
|
153
|
+
self.stop_event.set()
|
|
154
|
+
self.thread.join(timeout=2)
|