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,353 @@
|
|
|
1
|
+
"""Distillation tools for the code-intel MCP server.
|
|
2
|
+
|
|
3
|
+
Provides a ``distill`` tool that compresses codebase information at
|
|
4
|
+
three fidelity levels: full (existing repo_map), signatures (public API
|
|
5
|
+
surface), and structure (file tree + import graph).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from collections import deque
|
|
12
|
+
|
|
13
|
+
from attocode_intel._shared import (
|
|
14
|
+
_get_ast_service,
|
|
15
|
+
_get_context_mgr,
|
|
16
|
+
_get_remote_service,
|
|
17
|
+
mcp,
|
|
18
|
+
)
|
|
19
|
+
from attocode_intel._internal.integrations.utilities.token_estimate import estimate_tokens
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Helpers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_public_name(name: str) -> bool:
|
|
27
|
+
"""Return True if *name* looks public (no leading underscore)."""
|
|
28
|
+
return not name.startswith("_")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_signatures(
|
|
32
|
+
ast_cache: dict,
|
|
33
|
+
rel_paths: list[str],
|
|
34
|
+
max_tokens: int,
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Extract public API surface from AST cache for *rel_paths*.
|
|
37
|
+
|
|
38
|
+
Produces compact Python-like signatures for public functions, classes,
|
|
39
|
+
and their methods. Each function includes parameter names + types and
|
|
40
|
+
return type. Docstrings are reduced to first line only.
|
|
41
|
+
|
|
42
|
+
Uses ``estimate_tokens()`` for token estimation (same as ``repo_map``).
|
|
43
|
+
"""
|
|
44
|
+
sections: list[str] = []
|
|
45
|
+
token_est = 0
|
|
46
|
+
|
|
47
|
+
for rel in rel_paths:
|
|
48
|
+
file_ast = ast_cache.get(rel)
|
|
49
|
+
if file_ast is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
file_lines: list[str] = [f"# {rel}"]
|
|
53
|
+
has_content = False
|
|
54
|
+
|
|
55
|
+
# Public top-level functions
|
|
56
|
+
for fn in file_ast.functions:
|
|
57
|
+
if fn.visibility != "public" and not _is_public_name(fn.name):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
sig = _format_function_sig(fn)
|
|
61
|
+
file_lines.append(sig)
|
|
62
|
+
|
|
63
|
+
# First-line docstring
|
|
64
|
+
doc_line = _first_line_docstring(fn.docstring)
|
|
65
|
+
if doc_line:
|
|
66
|
+
file_lines.append(f' """{doc_line}"""')
|
|
67
|
+
|
|
68
|
+
has_content = True
|
|
69
|
+
|
|
70
|
+
# Public classes
|
|
71
|
+
for cls in file_ast.classes:
|
|
72
|
+
if not _is_public_name(cls.name):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
bases = f"({', '.join(cls.bases)})" if cls.bases else ""
|
|
76
|
+
file_lines.append(f"class {cls.name}{bases}:")
|
|
77
|
+
|
|
78
|
+
# Class docstring
|
|
79
|
+
cls_doc = _first_line_docstring(cls.docstring)
|
|
80
|
+
if cls_doc:
|
|
81
|
+
file_lines.append(f' """{cls_doc}"""')
|
|
82
|
+
|
|
83
|
+
# Public methods
|
|
84
|
+
method_count = 0
|
|
85
|
+
for method in cls.methods:
|
|
86
|
+
if method.visibility != "public" and not _is_public_name(method.name):
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
sig = _format_function_sig(method, indent=4)
|
|
90
|
+
file_lines.append(sig)
|
|
91
|
+
|
|
92
|
+
m_doc = _first_line_docstring(method.docstring)
|
|
93
|
+
if m_doc:
|
|
94
|
+
file_lines.append(f' """{m_doc}"""')
|
|
95
|
+
|
|
96
|
+
method_count += 1
|
|
97
|
+
|
|
98
|
+
if method_count == 0 and not cls_doc:
|
|
99
|
+
file_lines.append(" ...")
|
|
100
|
+
|
|
101
|
+
has_content = True
|
|
102
|
+
|
|
103
|
+
if not has_content:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
section_text = "\n".join(file_lines)
|
|
107
|
+
section_tokens = estimate_tokens(section_text)
|
|
108
|
+
|
|
109
|
+
if token_est + section_tokens > max_tokens and sections:
|
|
110
|
+
remaining = len(rel_paths) - len(sections)
|
|
111
|
+
if remaining > 0:
|
|
112
|
+
sections.append(f"\n# ... {remaining} more file(s) omitted (token budget)")
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
sections.append(section_text)
|
|
116
|
+
token_est += section_tokens
|
|
117
|
+
|
|
118
|
+
return "\n\n".join(sections)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _format_function_sig(fn, indent: int = 0) -> str:
|
|
122
|
+
"""Format a FunctionDef as a compact signature line."""
|
|
123
|
+
prefix = " " * indent
|
|
124
|
+
async_kw = "async " if fn.is_async else ""
|
|
125
|
+
|
|
126
|
+
# Build parameter list from detailed parameters if available
|
|
127
|
+
if fn.parameters:
|
|
128
|
+
params = []
|
|
129
|
+
for p in fn.parameters:
|
|
130
|
+
part = p.name
|
|
131
|
+
if p.type_annotation:
|
|
132
|
+
part += f": {p.type_annotation}"
|
|
133
|
+
if p.default_value:
|
|
134
|
+
part += f" = {p.default_value}"
|
|
135
|
+
if p.is_rest:
|
|
136
|
+
part = f"*{part}"
|
|
137
|
+
elif p.is_kwargs:
|
|
138
|
+
part = f"**{part}"
|
|
139
|
+
params.append(part)
|
|
140
|
+
param_str = ", ".join(params)
|
|
141
|
+
elif fn.params:
|
|
142
|
+
# Fallback to simple param names
|
|
143
|
+
param_str = ", ".join(fn.params)
|
|
144
|
+
else:
|
|
145
|
+
param_str = ""
|
|
146
|
+
|
|
147
|
+
ret = f" -> {fn.return_type}" if fn.return_type else ""
|
|
148
|
+
return f"{prefix}{async_kw}def {fn.name}({param_str}){ret}: ..."
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _first_line_docstring(docstring: str) -> str:
|
|
152
|
+
"""Return the first non-empty line of a docstring, or ''."""
|
|
153
|
+
if not docstring:
|
|
154
|
+
return ""
|
|
155
|
+
for line in docstring.strip().splitlines():
|
|
156
|
+
stripped = line.strip().strip('"').strip("'").strip()
|
|
157
|
+
if stripped:
|
|
158
|
+
# Truncate very long first lines
|
|
159
|
+
if len(stripped) > 120:
|
|
160
|
+
return stripped[:117] + "..."
|
|
161
|
+
return stripped
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_structure(
|
|
166
|
+
files: list,
|
|
167
|
+
dep_graph,
|
|
168
|
+
rel_paths: list[str] | None,
|
|
169
|
+
max_tokens: int,
|
|
170
|
+
) -> str:
|
|
171
|
+
"""Build file tree + import graph adjacency list.
|
|
172
|
+
|
|
173
|
+
This is the maximum compression level (~90%+).
|
|
174
|
+
"""
|
|
175
|
+
sections: list[str] = []
|
|
176
|
+
|
|
177
|
+
# File tree
|
|
178
|
+
tree_files = sorted(rel_paths) if rel_paths else sorted(fi.relative_path for fi in files)
|
|
179
|
+
|
|
180
|
+
tree_lines = ["# File tree"]
|
|
181
|
+
for path in tree_files:
|
|
182
|
+
tree_lines.append(f" {path}")
|
|
183
|
+
tree_text = "\n".join(tree_lines)
|
|
184
|
+
sections.append(tree_text)
|
|
185
|
+
|
|
186
|
+
# Import graph adjacency list
|
|
187
|
+
if dep_graph is not None:
|
|
188
|
+
graph_lines = ["# Import graph (file -> imports)"]
|
|
189
|
+
graph_set = set(rel_paths) if rel_paths else None
|
|
190
|
+
for src in sorted(dep_graph.forward.keys()):
|
|
191
|
+
if graph_set is not None and src not in graph_set:
|
|
192
|
+
continue
|
|
193
|
+
targets = dep_graph.forward.get(src, set())
|
|
194
|
+
if graph_set is not None:
|
|
195
|
+
targets = targets & graph_set
|
|
196
|
+
if targets:
|
|
197
|
+
target_list = ", ".join(sorted(targets))
|
|
198
|
+
graph_lines.append(f" {src} -> {target_list}")
|
|
199
|
+
|
|
200
|
+
if len(graph_lines) > 1:
|
|
201
|
+
graph_text = "\n".join(graph_lines)
|
|
202
|
+
sections.append(graph_text)
|
|
203
|
+
|
|
204
|
+
result = "\n\n".join(sections)
|
|
205
|
+
|
|
206
|
+
# Truncate if over budget
|
|
207
|
+
max_chars = int(max_tokens * 3.5)
|
|
208
|
+
if len(result) > max_chars:
|
|
209
|
+
result = result[:max_chars] + "\n# ... truncated (token budget)"
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _expand_by_depth(
|
|
215
|
+
center_files: list[str],
|
|
216
|
+
dep_graph,
|
|
217
|
+
depth: int,
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
"""BFS expand from center files through dependency graph up to *depth* hops."""
|
|
220
|
+
if dep_graph is None:
|
|
221
|
+
return center_files
|
|
222
|
+
|
|
223
|
+
visited: set[str] = set(center_files)
|
|
224
|
+
queue: deque[tuple[str, int]] = deque()
|
|
225
|
+
for f in center_files:
|
|
226
|
+
queue.append((f, 0))
|
|
227
|
+
|
|
228
|
+
while queue:
|
|
229
|
+
current, d = queue.popleft()
|
|
230
|
+
if d >= depth:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Forward: files this one imports
|
|
234
|
+
for dep in dep_graph.get_imports(current):
|
|
235
|
+
if dep not in visited:
|
|
236
|
+
visited.add(dep)
|
|
237
|
+
queue.append((dep, d + 1))
|
|
238
|
+
|
|
239
|
+
# Reverse: files that import this one
|
|
240
|
+
for dep in dep_graph.get_importers(current):
|
|
241
|
+
if dep not in visited:
|
|
242
|
+
visited.add(dep)
|
|
243
|
+
queue.append((dep, d + 1))
|
|
244
|
+
|
|
245
|
+
return sorted(visited)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# MCP Tool
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@mcp.tool()
|
|
254
|
+
def distill(
|
|
255
|
+
files: list[str] | None = None,
|
|
256
|
+
depth: int = 1,
|
|
257
|
+
level: str = "signatures",
|
|
258
|
+
max_tokens: int = 4000,
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Compress codebase information at varying fidelity levels.
|
|
261
|
+
|
|
262
|
+
Three distillation levels for different context budget needs:
|
|
263
|
+
- "full": Complete repo map with symbols (delegates to repo_map)
|
|
264
|
+
- "signatures": Public API surface — function/method signatures,
|
|
265
|
+
class definitions, type hints, first-line docstrings (~70% compression)
|
|
266
|
+
- "structure": File tree + import graph adjacency list only (~90%+ compression)
|
|
267
|
+
|
|
268
|
+
File selection:
|
|
269
|
+
- If files are specified, includes those files plus neighbors up to
|
|
270
|
+
`depth` hops via the dependency graph.
|
|
271
|
+
- If files is None, auto-selects the most important files (by PageRank
|
|
272
|
+
importance) that fit within the token budget.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
files: Specific file paths to distill (relative to project root).
|
|
276
|
+
When None, auto-selects by importance.
|
|
277
|
+
depth: Number of dependency graph hops to expand from specified files
|
|
278
|
+
(default 1, max 3). Ignored when files is None.
|
|
279
|
+
level: Distillation level — "full", "signatures", or "structure".
|
|
280
|
+
max_tokens: Token budget for the output (default 4000).
|
|
281
|
+
"""
|
|
282
|
+
remote = _get_remote_service()
|
|
283
|
+
if remote is not None:
|
|
284
|
+
return remote.distill(
|
|
285
|
+
files=files,
|
|
286
|
+
depth=depth,
|
|
287
|
+
level=level,
|
|
288
|
+
max_tokens=max_tokens,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Level: full — delegate to existing repo_map
|
|
292
|
+
if level == "full":
|
|
293
|
+
from attocode_intel.tools.navigation_tools import repo_map as _repo_map
|
|
294
|
+
|
|
295
|
+
return _repo_map(include_symbols=True, max_tokens=max_tokens)
|
|
296
|
+
|
|
297
|
+
ctx = _get_context_mgr()
|
|
298
|
+
all_files = ctx._files
|
|
299
|
+
|
|
300
|
+
if not all_files:
|
|
301
|
+
return "No files discovered in this project."
|
|
302
|
+
|
|
303
|
+
svc = _get_ast_service()
|
|
304
|
+
ast_cache = svc._ast_cache
|
|
305
|
+
dep_graph = ctx.dependency_graph
|
|
306
|
+
depth = min(depth, 3)
|
|
307
|
+
|
|
308
|
+
# Determine which files to include
|
|
309
|
+
if files is not None:
|
|
310
|
+
# Normalize to relative paths
|
|
311
|
+
project_dir = ctx.root_dir
|
|
312
|
+
center_rels: list[str] = []
|
|
313
|
+
for f in files:
|
|
314
|
+
if os.path.isabs(f):
|
|
315
|
+
try:
|
|
316
|
+
rel = os.path.relpath(f, project_dir)
|
|
317
|
+
except ValueError:
|
|
318
|
+
rel = f
|
|
319
|
+
else:
|
|
320
|
+
rel = f
|
|
321
|
+
center_rels.append(rel)
|
|
322
|
+
|
|
323
|
+
# Expand by dependency depth
|
|
324
|
+
if depth > 0 and dep_graph is not None:
|
|
325
|
+
selected = _expand_by_depth(center_rels, dep_graph, depth)
|
|
326
|
+
else:
|
|
327
|
+
selected = sorted(set(center_rels))
|
|
328
|
+
else:
|
|
329
|
+
# Auto-select by importance (PageRank), fitting token budget
|
|
330
|
+
sorted_files = sorted(all_files, key=lambda fi: fi.importance, reverse=True)
|
|
331
|
+
|
|
332
|
+
# For signatures, estimate ~80 chars per file on average; for structure ~40 chars
|
|
333
|
+
avg_chars = 80 if level == "signatures" else 40
|
|
334
|
+
max_files_estimate = int(max_tokens * 3.5 / avg_chars)
|
|
335
|
+
selected = [fi.relative_path for fi in sorted_files[:max_files_estimate]]
|
|
336
|
+
|
|
337
|
+
if not selected:
|
|
338
|
+
return "No files matched the selection criteria."
|
|
339
|
+
|
|
340
|
+
# Generate output based on level
|
|
341
|
+
if level == "signatures":
|
|
342
|
+
output = _extract_signatures(ast_cache, selected, max_tokens)
|
|
343
|
+
elif level == "structure":
|
|
344
|
+
output = _build_structure(all_files, dep_graph, selected, max_tokens)
|
|
345
|
+
else:
|
|
346
|
+
return f"Unknown distillation level: '{level}'. Use 'full', 'signatures', or 'structure'."
|
|
347
|
+
|
|
348
|
+
if not output:
|
|
349
|
+
return "No content extracted (files may not have parseable ASTs)."
|
|
350
|
+
|
|
351
|
+
# Add summary footer
|
|
352
|
+
footer = f"\n\n({len(selected)} files, level={level}, ~{estimate_tokens(output)} tokens)"
|
|
353
|
+
return output + footer
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Frecency MCP tools for tracking and querying file access patterns.
|
|
2
|
+
|
|
3
|
+
Exposes frecency tracking to AI agents so they can learn which files
|
|
4
|
+
are commonly accessed and boost their relevance in search results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from attocode_intel._shared import _get_frecency_tracker, _get_project_dir, mcp
|
|
15
|
+
from attocode_intel.tools.pin_tools import pin_stamped
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_git_status(file_path: str) -> tuple[bool, float | None]:
|
|
19
|
+
"""Check if a file has uncommitted changes and get modification time.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
(is_modified, modified_timestamp or None)
|
|
23
|
+
"""
|
|
24
|
+
project_dir = _get_project_dir()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Get git status for the specific file
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["git", "status", "--porcelain", "--", file_path],
|
|
30
|
+
cwd=project_dir,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
timeout=10,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
is_modified = bool(result.stdout.strip())
|
|
37
|
+
|
|
38
|
+
# Get modification time
|
|
39
|
+
abs_path = Path(project_dir) / file_path
|
|
40
|
+
mtime = abs_path.stat().st_mtime if abs_path.exists() else None
|
|
41
|
+
|
|
42
|
+
return is_modified, mtime
|
|
43
|
+
|
|
44
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
45
|
+
return False, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
def track_file_access(path: str) -> str:
|
|
50
|
+
"""Track that a file was accessed (opened/viewed).
|
|
51
|
+
|
|
52
|
+
Call this whenever an agent opens or reads a file. This builds up
|
|
53
|
+
access patterns that improve file search relevance over time.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
path: Path to the file that was accessed (relative to project root
|
|
57
|
+
or absolute).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Confirmation message with the file that was tracked.
|
|
61
|
+
"""
|
|
62
|
+
tracker = _get_frecency_tracker()
|
|
63
|
+
|
|
64
|
+
# Resolve to absolute path if needed
|
|
65
|
+
project_dir = _get_project_dir()
|
|
66
|
+
if os.path.isabs(path):
|
|
67
|
+
try:
|
|
68
|
+
rel_path = os.path.relpath(path, project_dir)
|
|
69
|
+
except ValueError:
|
|
70
|
+
rel_path = path
|
|
71
|
+
else:
|
|
72
|
+
rel_path = path
|
|
73
|
+
|
|
74
|
+
tracker.track_access(rel_path)
|
|
75
|
+
|
|
76
|
+
return f"Tracked access to: {rel_path}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@mcp.tool()
|
|
80
|
+
def get_file_frecency(path: str, ai_mode: bool = True) -> str:
|
|
81
|
+
"""Get the frecency score for a file.
|
|
82
|
+
|
|
83
|
+
Frecency combines how often and how recently a file was accessed.
|
|
84
|
+
Higher scores indicate more frequently/recently accessed files.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: Path to the file (relative to project root or absolute).
|
|
88
|
+
ai_mode: Use AI mode decay (faster, 3-day half-life vs 10-day).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Frecency score and metadata for the file.
|
|
92
|
+
"""
|
|
93
|
+
tracker = _get_frecency_tracker()
|
|
94
|
+
|
|
95
|
+
# Resolve path
|
|
96
|
+
project_dir = _get_project_dir()
|
|
97
|
+
if os.path.isabs(path):
|
|
98
|
+
try:
|
|
99
|
+
rel_path = os.path.relpath(path, project_dir)
|
|
100
|
+
except ValueError:
|
|
101
|
+
rel_path = path
|
|
102
|
+
else:
|
|
103
|
+
rel_path = path
|
|
104
|
+
|
|
105
|
+
# Get git status for modification bonus
|
|
106
|
+
is_modified, mtime = _get_git_status(rel_path)
|
|
107
|
+
|
|
108
|
+
result = tracker.get_score(
|
|
109
|
+
rel_path,
|
|
110
|
+
modified_time=mtime,
|
|
111
|
+
is_modified_git=is_modified,
|
|
112
|
+
ai_mode=ai_mode,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Format output
|
|
116
|
+
mode_str = "AI" if result.is_ai_mode else "Human"
|
|
117
|
+
last_access_str = "never"
|
|
118
|
+
if result.last_access is not None:
|
|
119
|
+
dt = datetime.fromtimestamp(result.last_access)
|
|
120
|
+
last_access_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
121
|
+
|
|
122
|
+
lines = [
|
|
123
|
+
f"Frecency score for: {rel_path}",
|
|
124
|
+
f"Score: {result.score}",
|
|
125
|
+
f"Access count: {result.accesses}",
|
|
126
|
+
f"Last access: {last_access_str}",
|
|
127
|
+
f"Mode: {mode_str}",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
if is_modified:
|
|
131
|
+
lines.append("(Git modified bonus applied)")
|
|
132
|
+
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
@pin_stamped
|
|
138
|
+
def get_frecency_leaderboard(top_n: int = 20, ai_mode: bool = True) -> str:
|
|
139
|
+
"""Get the top N most frequently accessed files.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
top_n: Number of top files to return (default 20).
|
|
143
|
+
ai_mode: Use AI mode decay (faster, 3-day half-life vs 10-day).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Leaderboard of most accessed files.
|
|
147
|
+
"""
|
|
148
|
+
tracker = _get_frecency_tracker()
|
|
149
|
+
leaderboard = tracker.get_leaderboard(top_n=top_n, ai_mode=ai_mode)
|
|
150
|
+
|
|
151
|
+
if not leaderboard:
|
|
152
|
+
stats = tracker.get_stats()
|
|
153
|
+
return (
|
|
154
|
+
f"No files with positive frecency scores.\n"
|
|
155
|
+
f"Database has {stats['entries']} tracked files.\n"
|
|
156
|
+
f"Files need recent accesses to appear on the leaderboard."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
mode_str = "AI (3-day decay)" if ai_mode else "Human (10-day decay)"
|
|
160
|
+
lines = [
|
|
161
|
+
f"Frecency Leaderboard (top {top_n}, {mode_str})",
|
|
162
|
+
"=" * 50,
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
for rank, (path, result) in enumerate(leaderboard, 1):
|
|
166
|
+
last_str = "never"
|
|
167
|
+
if result.last_access is not None:
|
|
168
|
+
dt = datetime.fromtimestamp(result.last_access)
|
|
169
|
+
last_str = dt.strftime("%Y-%m-%d %H:%M")
|
|
170
|
+
lines.append(
|
|
171
|
+
f"{rank:3d}. [{result.score:4d}] {path} "
|
|
172
|
+
f"({result.accesses} accesses, last: {last_str})"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return "\n".join(lines)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool()
|
|
179
|
+
def clear_frecency(path: str | None = None) -> str:
|
|
180
|
+
"""Clear frecency data.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
path: Optional specific file path to clear. If not provided,
|
|
184
|
+
all frecency data is cleared.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Confirmation message.
|
|
188
|
+
"""
|
|
189
|
+
tracker = _get_frecency_tracker()
|
|
190
|
+
|
|
191
|
+
if path is None:
|
|
192
|
+
tracker.clear()
|
|
193
|
+
return "All frecency data cleared."
|
|
194
|
+
else:
|
|
195
|
+
count = tracker.clear(path)
|
|
196
|
+
if count > 0:
|
|
197
|
+
return f"Cleared frecency data for: {path}"
|
|
198
|
+
else:
|
|
199
|
+
return f"No frecency data found for: {path}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@mcp.tool()
|
|
203
|
+
def get_frecency_stats() -> str:
|
|
204
|
+
"""Get overall frecency database statistics.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Statistics about the frecency database.
|
|
208
|
+
"""
|
|
209
|
+
tracker = _get_frecency_tracker()
|
|
210
|
+
stats = tracker.get_stats()
|
|
211
|
+
|
|
212
|
+
lines = [
|
|
213
|
+
"Frecency Statistics",
|
|
214
|
+
"=" * 40,
|
|
215
|
+
f"Tracked files: {stats['entries']}",
|
|
216
|
+
f"Mode: {'AI (3-day decay)' if stats['ai_mode'] else 'Human (10-day decay)'}",
|
|
217
|
+
f"Database path: {stats.get('db_path', 'unknown')}",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
return "\n".join(lines)
|