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,160 @@
|
|
|
1
|
+
"""Redis-backed presence tracking for collaboration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Presence entries expire after 120 seconds without heartbeat
|
|
13
|
+
_PRESENCE_TTL = 120
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class PresenceEntry:
|
|
18
|
+
user_id: str
|
|
19
|
+
user_name: str
|
|
20
|
+
file: str
|
|
21
|
+
session_id: str
|
|
22
|
+
timestamp: float
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def set_presence(
|
|
26
|
+
repo_id: str,
|
|
27
|
+
user_id: str,
|
|
28
|
+
user_name: str,
|
|
29
|
+
file_path: str,
|
|
30
|
+
session_id: str,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Set or update presence for a user in a repo."""
|
|
33
|
+
from attocode_intel.redis import get_redis
|
|
34
|
+
|
|
35
|
+
redis = await get_redis()
|
|
36
|
+
key = f"presence:{repo_id}"
|
|
37
|
+
field = f"{user_id}:{session_id}"
|
|
38
|
+
|
|
39
|
+
# Check for file change to publish event
|
|
40
|
+
old_raw = await redis.hget(key, field)
|
|
41
|
+
old_file = ""
|
|
42
|
+
if old_raw:
|
|
43
|
+
try:
|
|
44
|
+
old_data = json.loads(old_raw)
|
|
45
|
+
old_file = old_data.get("file", "")
|
|
46
|
+
except (json.JSONDecodeError, TypeError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
value = json.dumps({
|
|
50
|
+
"user_id": user_id,
|
|
51
|
+
"user_name": user_name,
|
|
52
|
+
"file": file_path,
|
|
53
|
+
"session_id": session_id,
|
|
54
|
+
"timestamp": time.time(),
|
|
55
|
+
})
|
|
56
|
+
await redis.hset(key, field, value)
|
|
57
|
+
|
|
58
|
+
# Set TTL on the hash key (renews on each update)
|
|
59
|
+
await redis.expire(key, _PRESENCE_TTL)
|
|
60
|
+
|
|
61
|
+
# Publish presence events
|
|
62
|
+
from attocode_intel.pubsub import publish_event
|
|
63
|
+
|
|
64
|
+
if not old_raw:
|
|
65
|
+
await publish_event(repo_id, "presence.joined", {
|
|
66
|
+
"user_id": user_id, "user_name": user_name, "file": file_path,
|
|
67
|
+
})
|
|
68
|
+
elif old_file != file_path:
|
|
69
|
+
await publish_event(repo_id, "presence.file_changed", {
|
|
70
|
+
"user_id": user_id, "user_name": user_name,
|
|
71
|
+
"from_file": old_file, "to_file": file_path,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def remove_presence(
|
|
76
|
+
repo_id: str,
|
|
77
|
+
user_id: str,
|
|
78
|
+
session_id: str,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Remove presence for a user session."""
|
|
81
|
+
from attocode_intel.redis import get_redis
|
|
82
|
+
|
|
83
|
+
redis = await get_redis()
|
|
84
|
+
key = f"presence:{repo_id}"
|
|
85
|
+
field = f"{user_id}:{session_id}"
|
|
86
|
+
|
|
87
|
+
# Get user name before removing
|
|
88
|
+
raw = await redis.hget(key, field)
|
|
89
|
+
user_name = ""
|
|
90
|
+
if raw:
|
|
91
|
+
try:
|
|
92
|
+
data = json.loads(raw)
|
|
93
|
+
user_name = data.get("user_name", "")
|
|
94
|
+
except (json.JSONDecodeError, TypeError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
await redis.hdel(key, field)
|
|
98
|
+
|
|
99
|
+
from attocode_intel.pubsub import publish_event
|
|
100
|
+
|
|
101
|
+
await publish_event(repo_id, "presence.left", {
|
|
102
|
+
"user_id": user_id, "user_name": user_name,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def get_presence(repo_id: str) -> list[PresenceEntry]:
|
|
107
|
+
"""Get all active presence entries for a repo."""
|
|
108
|
+
from attocode_intel.redis import get_redis
|
|
109
|
+
|
|
110
|
+
redis = await get_redis()
|
|
111
|
+
key = f"presence:{repo_id}"
|
|
112
|
+
|
|
113
|
+
all_entries = await redis.hgetall(key)
|
|
114
|
+
now = time.time()
|
|
115
|
+
result = []
|
|
116
|
+
|
|
117
|
+
for field, raw in all_entries.items():
|
|
118
|
+
try:
|
|
119
|
+
data = json.loads(raw)
|
|
120
|
+
except (json.JSONDecodeError, TypeError):
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Filter out stale entries
|
|
124
|
+
ts = data.get("timestamp", 0)
|
|
125
|
+
if now - ts > _PRESENCE_TTL:
|
|
126
|
+
await redis.hdel(key, field)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
result.append(PresenceEntry(
|
|
130
|
+
user_id=data["user_id"],
|
|
131
|
+
user_name=data.get("user_name", ""),
|
|
132
|
+
file=data.get("file", ""),
|
|
133
|
+
session_id=data.get("session_id", ""),
|
|
134
|
+
timestamp=ts,
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def heartbeat(
|
|
141
|
+
repo_id: str,
|
|
142
|
+
user_id: str,
|
|
143
|
+
session_id: str,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Renew presence TTL via heartbeat."""
|
|
146
|
+
from attocode_intel.redis import get_redis
|
|
147
|
+
|
|
148
|
+
redis = await get_redis()
|
|
149
|
+
key = f"presence:{repo_id}"
|
|
150
|
+
field = f"{user_id}:{session_id}"
|
|
151
|
+
|
|
152
|
+
raw = await redis.hget(key, field)
|
|
153
|
+
if raw:
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(raw)
|
|
156
|
+
data["timestamp"] = time.time()
|
|
157
|
+
await redis.hset(key, field, json.dumps(data))
|
|
158
|
+
await redis.expire(key, _PRESENCE_TTL)
|
|
159
|
+
except (json.JSONDecodeError, TypeError):
|
|
160
|
+
pass
|
attocode_intel/probe.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Runtime MCP install probing for file-based assistant configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
|
|
10
|
+
from attocode_intel.installer import ResolvedInstallSpec, resolve_install_spec
|
|
11
|
+
from attocode_intel._internal.integrations.mcp.client import MCPClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _substitute_workspace_folder(value: str, project_dir: str) -> str:
|
|
15
|
+
return value.replace("${workspaceFolder}", project_dir)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _materialize_spec(spec: ResolvedInstallSpec, project_dir: str) -> ResolvedInstallSpec:
|
|
19
|
+
"""Resolve runtime placeholders before spawning the stdio process."""
|
|
20
|
+
env = {
|
|
21
|
+
key: _substitute_workspace_folder(value, project_dir)
|
|
22
|
+
for key, value in spec.env.items()
|
|
23
|
+
}
|
|
24
|
+
return replace(
|
|
25
|
+
spec,
|
|
26
|
+
command=_substitute_workspace_folder(spec.command, project_dir),
|
|
27
|
+
args=[_substitute_workspace_folder(arg, project_dir) for arg in spec.args],
|
|
28
|
+
env=env,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_concrete_project_arg(args: list[str]) -> bool:
|
|
33
|
+
for idx, arg in enumerate(args):
|
|
34
|
+
if arg == "--project" and idx + 1 < len(args):
|
|
35
|
+
return True
|
|
36
|
+
if arg.startswith("--project="):
|
|
37
|
+
return True
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _run_probe(
|
|
42
|
+
spec: ResolvedInstallSpec,
|
|
43
|
+
*,
|
|
44
|
+
project_dir: str,
|
|
45
|
+
should_call_project_summary: bool,
|
|
46
|
+
) -> tuple[int, int]:
|
|
47
|
+
client = MCPClient(
|
|
48
|
+
spec.command,
|
|
49
|
+
server_args=spec.args,
|
|
50
|
+
server_name=spec.target,
|
|
51
|
+
env=spec.env,
|
|
52
|
+
cwd=project_dir,
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
await client.connect()
|
|
56
|
+
if not client.is_connected:
|
|
57
|
+
raise RuntimeError("MCP initialize did not complete")
|
|
58
|
+
if should_call_project_summary:
|
|
59
|
+
result = await client.call_tool("project_summary", {"max_tokens": 128})
|
|
60
|
+
if not result.success:
|
|
61
|
+
raise RuntimeError(result.error or "project_summary probe failed")
|
|
62
|
+
return len(client.tools), 0
|
|
63
|
+
finally:
|
|
64
|
+
await client.disconnect()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def probe_install(
|
|
68
|
+
target: str,
|
|
69
|
+
project_dir: str = ".",
|
|
70
|
+
scope: str = "local",
|
|
71
|
+
*,
|
|
72
|
+
force_project_probe: bool = False,
|
|
73
|
+
) -> int:
|
|
74
|
+
"""Run a runtime MCP probe for an installed assistant target.
|
|
75
|
+
|
|
76
|
+
Exit codes:
|
|
77
|
+
0: success
|
|
78
|
+
1: target missing or probe failed
|
|
79
|
+
2: unsupported target for v1 probing
|
|
80
|
+
"""
|
|
81
|
+
abs_project = os.path.abspath(project_dir)
|
|
82
|
+
resolved = resolve_install_spec(target, project_dir=abs_project, scope=scope)
|
|
83
|
+
if resolved is None:
|
|
84
|
+
print(f"{target}: attocode-code-intel is not installed for scope={scope}.", file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
if not resolved.is_supported:
|
|
87
|
+
print(f"{target}: {resolved.unsupported_reason}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
runtime_spec = _materialize_spec(resolved, abs_project)
|
|
91
|
+
should_call_project_summary = force_project_probe or _has_concrete_project_arg(runtime_spec.args)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
tools_count, _ = asyncio.run(
|
|
95
|
+
_run_probe(
|
|
96
|
+
runtime_spec,
|
|
97
|
+
project_dir=abs_project,
|
|
98
|
+
should_call_project_summary=should_call_project_summary,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
print(f"{target}: probe failed ({type(exc).__name__}: {exc})", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
project_note = " with project_summary probe" if should_call_project_summary else ""
|
|
106
|
+
print(
|
|
107
|
+
f"{target}: probe succeeded{project_note} "
|
|
108
|
+
f"({runtime_spec.command} {' '.join(runtime_spec.args)}; tools={tools_count})"
|
|
109
|
+
)
|
|
110
|
+
return 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Project-directory discovery — MCP-free neutral helpers.
|
|
2
|
+
|
|
3
|
+
These functions used to live in ``attocode_intel._shared``, but
|
|
4
|
+
``_shared`` unconditionally imports ``mcp.server.fastmcp`` at module
|
|
5
|
+
load time and ``sys.exit(1)`` s if ``mcp`` isn't installed. The HTTP
|
|
6
|
+
API layer (``api/providers/``) needs to discover the current project
|
|
7
|
+
directory without dragging in the MCP runtime, so the logic is hosted
|
|
8
|
+
here.
|
|
9
|
+
|
|
10
|
+
``_shared`` re-exports both symbols for backward compatibility with
|
|
11
|
+
the 25-odd call sites that already import them via ``_shared``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Iterator
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _walk_up(start: str, max_depth: int = 20) -> Iterator[str]:
|
|
28
|
+
"""Walk upward from start, yielding parent directories up to max_depth.
|
|
29
|
+
|
|
30
|
+
Yields each ancestor directory starting from dirname(start) (the immediate
|
|
31
|
+
parent), then climbing upward until filesystem root.
|
|
32
|
+
"""
|
|
33
|
+
path = os.path.dirname(start)
|
|
34
|
+
for _ in range(max_depth):
|
|
35
|
+
yield path
|
|
36
|
+
parent = os.path.dirname(path)
|
|
37
|
+
if parent == path:
|
|
38
|
+
break # Reached filesystem root
|
|
39
|
+
path = parent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_project_dir() -> str:
|
|
43
|
+
"""Get the project directory from env var, CLI arg, or auto-discovery.
|
|
44
|
+
|
|
45
|
+
Resolution order:
|
|
46
|
+
1. ATTOCODE_PROJECT_DIR env var (set by --project CLI arg)
|
|
47
|
+
2. Walk up from CWD looking for .git/ or .attocode/ marker
|
|
48
|
+
3. Fall back to CWD
|
|
49
|
+
"""
|
|
50
|
+
from attocode_intel.request_context import current_request
|
|
51
|
+
if (request := current_request.get()) is not None:
|
|
52
|
+
return request.project_dir
|
|
53
|
+
project_dir = os.environ.get("ATTOCODE_PROJECT_DIR", "")
|
|
54
|
+
if project_dir:
|
|
55
|
+
return os.path.abspath(project_dir)
|
|
56
|
+
return find_project_root(os.getcwd())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_project_root(start: str) -> str:
|
|
60
|
+
"""Return the nearest directory at or above ``start`` with a project marker.
|
|
61
|
+
|
|
62
|
+
The markers are ``.git`` (a directory, or a file in a worktree) and
|
|
63
|
+
``.attocode``. The search skips the home directory: ``~/.attocode`` holds
|
|
64
|
+
user-wide settings and caches, so it does not make home a project. Before
|
|
65
|
+
this rule, a server started in a folder without markers indexed the whole
|
|
66
|
+
home directory. Without a marker, ``start`` is the root.
|
|
67
|
+
"""
|
|
68
|
+
home = os.path.realpath(os.path.expanduser("~"))
|
|
69
|
+
for dir_path in [start, *_walk_up(start)]:
|
|
70
|
+
if os.path.realpath(dir_path) == home:
|
|
71
|
+
continue
|
|
72
|
+
for marker in (".git", ".attocode"):
|
|
73
|
+
if os.path.exists(os.path.join(dir_path, marker)):
|
|
74
|
+
logger.debug(
|
|
75
|
+
"Auto-discovered project root: %s (marker: %s)",
|
|
76
|
+
dir_path, marker,
|
|
77
|
+
)
|
|
78
|
+
return dir_path
|
|
79
|
+
|
|
80
|
+
logger.debug("No project marker found, falling back to %s", start)
|
|
81
|
+
return start
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cache_folder(project_dir: str | os.PathLike) -> Path:
|
|
85
|
+
"""Return ``<project>/.attocode``, and make it if it does not exist.
|
|
86
|
+
|
|
87
|
+
A folder that this function makes gets a ``.gitignore`` with ``*``, so the
|
|
88
|
+
index stays out of commits. A folder that exists keeps its own rules: some
|
|
89
|
+
projects commit settings in it.
|
|
90
|
+
"""
|
|
91
|
+
folder = Path(project_dir) / ".attocode"
|
|
92
|
+
try:
|
|
93
|
+
folder.mkdir()
|
|
94
|
+
except FileExistsError:
|
|
95
|
+
return folder
|
|
96
|
+
(folder / ".gitignore").write_text("# Local index data of attocode-code-intel\n*\n", encoding="utf-8")
|
|
97
|
+
return folder
|
attocode_intel/pubsub.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Redis Streams — persistent event publishing and subscription for real-time updates.
|
|
2
|
+
|
|
3
|
+
Uses Redis Streams (XADD/XREAD) instead of ephemeral PubSub so that clients
|
|
4
|
+
that disconnect can resume from their last-seen event ID. Stream entries are
|
|
5
|
+
auto-trimmed to keep memory bounded.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import AsyncGenerator
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Max entries per stream before auto-trimming (MAXLEN ~)
|
|
17
|
+
_MAX_STREAM_LEN = 10_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def publish_event(repo_id: str, event_type: str, payload: dict) -> str | None:
|
|
21
|
+
"""Publish an event to the repo's stream. Returns the stream entry ID, or None on error.
|
|
22
|
+
|
|
23
|
+
Fire-and-forget semantics — never raises.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
from attocode_intel.redis import get_redis
|
|
27
|
+
|
|
28
|
+
redis = await get_redis()
|
|
29
|
+
stream_key = f"repo:{repo_id}:stream"
|
|
30
|
+
message = json.dumps({"type": event_type, "payload": payload})
|
|
31
|
+
entry_id = await redis.xadd(
|
|
32
|
+
stream_key,
|
|
33
|
+
{"event": message},
|
|
34
|
+
maxlen=_MAX_STREAM_LEN,
|
|
35
|
+
approximate=True,
|
|
36
|
+
)
|
|
37
|
+
return entry_id
|
|
38
|
+
except Exception:
|
|
39
|
+
logger.warning("Failed to publish event %s for repo %s", event_type, repo_id, exc_info=True)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def subscribe(
|
|
44
|
+
repo_id: str,
|
|
45
|
+
last_event_id: str = "$",
|
|
46
|
+
block_ms: int = 5000,
|
|
47
|
+
) -> AsyncGenerator[dict, None]:
|
|
48
|
+
"""Subscribe to events for a repository using Redis Streams.
|
|
49
|
+
|
|
50
|
+
Yields event dicts. If last_event_id is provided, replays all events
|
|
51
|
+
after that ID before switching to live mode.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
repo_id: Repository ID to subscribe to.
|
|
55
|
+
last_event_id: Resume from this event ID. "$" = new events only.
|
|
56
|
+
"0" = replay all available events.
|
|
57
|
+
block_ms: How long to block waiting for new events (milliseconds).
|
|
58
|
+
"""
|
|
59
|
+
from attocode_intel.redis import get_redis
|
|
60
|
+
|
|
61
|
+
redis = await get_redis()
|
|
62
|
+
stream_key = f"repo:{repo_id}:stream"
|
|
63
|
+
cursor = last_event_id
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
while True:
|
|
67
|
+
# XREAD blocks for up to block_ms waiting for new entries
|
|
68
|
+
results = await redis.xread(
|
|
69
|
+
{stream_key: cursor},
|
|
70
|
+
count=100,
|
|
71
|
+
block=block_ms,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not results:
|
|
75
|
+
# Timeout — no new events, yield control and retry
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
for _stream_name, entries in results:
|
|
79
|
+
for entry_id, fields in entries:
|
|
80
|
+
cursor = entry_id
|
|
81
|
+
event_data = fields.get("event") or fields.get(b"event")
|
|
82
|
+
if event_data is None:
|
|
83
|
+
continue
|
|
84
|
+
if isinstance(event_data, bytes):
|
|
85
|
+
event_data = event_data.decode("utf-8")
|
|
86
|
+
try:
|
|
87
|
+
data = json.loads(event_data)
|
|
88
|
+
# Include the stream entry ID so clients can resume
|
|
89
|
+
data["_stream_id"] = entry_id if isinstance(entry_id, str) else entry_id.decode("utf-8")
|
|
90
|
+
yield data
|
|
91
|
+
except (json.JSONDecodeError, TypeError):
|
|
92
|
+
continue
|
|
93
|
+
except GeneratorExit:
|
|
94
|
+
pass
|
|
95
|
+
except Exception:
|
|
96
|
+
logger.warning("Stream subscription ended for repo %s", repo_id, exc_info=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def get_stream_info(repo_id: str) -> dict:
|
|
100
|
+
"""Get info about the repo's event stream (length, first/last entry IDs)."""
|
|
101
|
+
try:
|
|
102
|
+
from attocode_intel.redis import get_redis
|
|
103
|
+
|
|
104
|
+
redis = await get_redis()
|
|
105
|
+
stream_key = f"repo:{repo_id}:stream"
|
|
106
|
+
info = await redis.xinfo_stream(stream_key)
|
|
107
|
+
return {
|
|
108
|
+
"length": info.get("length", 0),
|
|
109
|
+
"first_entry": info.get("first-entry", [None])[0] if info.get("first-entry") else None,
|
|
110
|
+
"last_entry": info.get("last-entry", [None])[0] if info.get("last-entry") else None,
|
|
111
|
+
}
|
|
112
|
+
except Exception:
|
|
113
|
+
return {"length": 0, "first_entry": None, "last_entry": None}
|