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,540 @@
|
|
|
1
|
+
"""Named-overlay MCP tools — directory-based working-state swap.
|
|
2
|
+
|
|
3
|
+
An overlay is a named copy of every local store file, stored under
|
|
4
|
+
``.attocode/overlays/<name>/``. Switching between branches / working sets
|
|
5
|
+
means calling ``overlay_activate(name)`` — an atomic per-file replacement
|
|
6
|
+
that preserves the current state as a fresh overlay if you ask it to.
|
|
7
|
+
|
|
8
|
+
Phase 2c-3 ships the minimal viable form: directory-based, no stacking,
|
|
9
|
+
no tombstones, no dedup across overlays. It's the simplest thing that
|
|
10
|
+
delivers the UX goal ("one command to swap code-intel state") and
|
|
11
|
+
forward-compatible with a later stacked-layer design.
|
|
12
|
+
|
|
13
|
+
Format of one overlay directory::
|
|
14
|
+
|
|
15
|
+
.attocode/overlays/<name>/
|
|
16
|
+
overlay.json # {created_at, description, source_hash}
|
|
17
|
+
stores/symbols.db # live-consistent SQLite backup
|
|
18
|
+
stores/embeddings.db
|
|
19
|
+
stores/memory.db
|
|
20
|
+
stores/adrs.db
|
|
21
|
+
stores/frecency.db
|
|
22
|
+
stores/query_history.db
|
|
23
|
+
stores/kw_index.db # copy
|
|
24
|
+
stores/trigrams.{lookup,postings,db}
|
|
25
|
+
cache_manifest.json # project manifest at capture time
|
|
26
|
+
|
|
27
|
+
Tools:
|
|
28
|
+
|
|
29
|
+
- ``overlay_create(name, description)`` — capture current state
|
|
30
|
+
- ``overlay_list`` — enumerate all overlays
|
|
31
|
+
- ``overlay_activate(name, save_current_as)`` — swap over live state
|
|
32
|
+
- ``overlay_delete(name, confirm)`` — remove an overlay directory
|
|
33
|
+
- ``overlay_status`` — currently-active overlay tag
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
import shutil
|
|
42
|
+
import sqlite3
|
|
43
|
+
import time
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
from attocode_intel._shared import _get_project_dir, mcp
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
OVERLAY_SCHEMA_VERSION = 1
|
|
51
|
+
OVERLAY_DIR_NAME = "overlays"
|
|
52
|
+
_OVERLAY_STATE_FILE = "_overlay_state.json"
|
|
53
|
+
|
|
54
|
+
# Same shape as snapshot_tools' _SQLITE_STORES / _BINARY_FILES so the two
|
|
55
|
+
# tool modules stay in lockstep. When a new store lands, update both lists.
|
|
56
|
+
_SQLITE_STORES: tuple[tuple[str, str], ...] = (
|
|
57
|
+
("index/symbols.db", "symbols.db"),
|
|
58
|
+
("vectors/embeddings.db", "embeddings.db"),
|
|
59
|
+
("index/kw_index.db", "kw_index.db"),
|
|
60
|
+
("cache/memory.db", "memory.db"),
|
|
61
|
+
("adrs.db", "adrs.db"),
|
|
62
|
+
("frecency/frecency.db", "frecency.db"),
|
|
63
|
+
("query_history/query_history.db", "query_history.db"),
|
|
64
|
+
)
|
|
65
|
+
_BINARY_FILES: tuple[tuple[str, str], ...] = (
|
|
66
|
+
("index/trigrams.lookup", "trigrams.lookup"),
|
|
67
|
+
("index/trigrams.postings", "trigrams.postings"),
|
|
68
|
+
("index/trigrams.db", "trigrams.db"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Paths + state helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _attocode_dir(project_dir: str) -> str:
|
|
78
|
+
return os.path.join(project_dir, ".attocode")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _overlays_root(project_dir: str) -> str:
|
|
82
|
+
return os.path.join(_attocode_dir(project_dir), OVERLAY_DIR_NAME)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _overlay_dir(project_dir: str, name: str) -> str:
|
|
86
|
+
return os.path.join(_overlays_root(project_dir), name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _state_path(project_dir: str) -> str:
|
|
90
|
+
return os.path.join(_overlays_root(project_dir), _OVERLAY_STATE_FILE)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_state(project_dir: str) -> dict[str, Any]:
|
|
94
|
+
path = _state_path(project_dir)
|
|
95
|
+
if not os.path.exists(path):
|
|
96
|
+
return {"active": None, "history": []}
|
|
97
|
+
try:
|
|
98
|
+
with open(path, encoding="utf-8") as f:
|
|
99
|
+
return json.loads(f.read())
|
|
100
|
+
except (OSError, json.JSONDecodeError):
|
|
101
|
+
return {"active": None, "history": []}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_state(project_dir: str, state: dict[str, Any]) -> None:
|
|
105
|
+
path = _state_path(project_dir)
|
|
106
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
107
|
+
tmp = path + ".tmp"
|
|
108
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
109
|
+
json.dump(state, f, indent=2, sort_keys=True)
|
|
110
|
+
os.replace(tmp, path)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _is_valid_name(name: str) -> bool:
|
|
114
|
+
"""Overlay names must be short filesystem-friendly slugs.
|
|
115
|
+
|
|
116
|
+
Rejected: empty strings, path separators, leading underscore
|
|
117
|
+
(reserved for internal state like ``_overlay_state.json``), and
|
|
118
|
+
anything with shell meta characters.
|
|
119
|
+
"""
|
|
120
|
+
if not name or name.startswith("_") or name in (".", ".."):
|
|
121
|
+
return False
|
|
122
|
+
return all(c.isalnum() or c in ("-", "_", ".") for c in name)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Store copy helpers (shared with snapshot_tools logic, inlined so the
|
|
127
|
+
# two modules can evolve independently)
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _backup_sqlite(source: str, dest: str) -> None:
|
|
132
|
+
"""Live-consistent SQLite backup via the online backup API."""
|
|
133
|
+
src_conn = sqlite3.connect(f"file:{source}?mode=ro", uri=True)
|
|
134
|
+
try:
|
|
135
|
+
dest_conn = sqlite3.connect(dest)
|
|
136
|
+
try:
|
|
137
|
+
src_conn.backup(dest_conn)
|
|
138
|
+
finally:
|
|
139
|
+
dest_conn.close()
|
|
140
|
+
finally:
|
|
141
|
+
src_conn.close()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _copy_store_to(dest_stores_dir: str, attocode: str) -> list[str]:
|
|
145
|
+
"""Copy every tracked store into ``dest_stores_dir``.
|
|
146
|
+
|
|
147
|
+
Returns the list of copied store names (logical, not filenames).
|
|
148
|
+
"""
|
|
149
|
+
os.makedirs(dest_stores_dir, exist_ok=True)
|
|
150
|
+
copied: list[str] = []
|
|
151
|
+
|
|
152
|
+
for src_rel, out_name in _SQLITE_STORES:
|
|
153
|
+
src = os.path.join(attocode, src_rel)
|
|
154
|
+
if not os.path.exists(src):
|
|
155
|
+
continue
|
|
156
|
+
dest = os.path.join(dest_stores_dir, out_name)
|
|
157
|
+
try:
|
|
158
|
+
_backup_sqlite(src, dest)
|
|
159
|
+
except sqlite3.Error as exc:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"overlay: sqlite backup of %s failed (%s); falling back to shutil.copy",
|
|
162
|
+
src, exc,
|
|
163
|
+
)
|
|
164
|
+
shutil.copy2(src, dest)
|
|
165
|
+
copied.append(out_name)
|
|
166
|
+
|
|
167
|
+
for src_rel, out_name in _BINARY_FILES:
|
|
168
|
+
src = os.path.join(attocode, src_rel)
|
|
169
|
+
if not os.path.exists(src):
|
|
170
|
+
continue
|
|
171
|
+
shutil.copy2(src, os.path.join(dest_stores_dir, out_name))
|
|
172
|
+
copied.append(out_name)
|
|
173
|
+
|
|
174
|
+
return copied
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _copy_store_from(src_stores_dir: str, attocode: str) -> list[str]:
|
|
178
|
+
"""Copy every overlay store file back into the live ``.attocode/`` tree.
|
|
179
|
+
|
|
180
|
+
Each destination is atomically replaced via rename-tmp → rename so a
|
|
181
|
+
failure mid-copy can't produce a torn file. The overall restore is
|
|
182
|
+
atomic per-file, not per-collection — which is good enough for
|
|
183
|
+
Phase 2c-3 (single-user local).
|
|
184
|
+
"""
|
|
185
|
+
restored: list[str] = []
|
|
186
|
+
mapping: list[tuple[str, str]] = list(_SQLITE_STORES) + list(_BINARY_FILES)
|
|
187
|
+
for src_rel, out_name in mapping:
|
|
188
|
+
src = os.path.join(src_stores_dir, out_name)
|
|
189
|
+
if not os.path.exists(src):
|
|
190
|
+
continue
|
|
191
|
+
dest = os.path.join(attocode, src_rel)
|
|
192
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
193
|
+
tmp = dest + ".tmp"
|
|
194
|
+
shutil.copy2(src, tmp)
|
|
195
|
+
os.replace(tmp, dest)
|
|
196
|
+
restored.append(out_name)
|
|
197
|
+
return restored
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _count_rows(db_path: str) -> int:
|
|
201
|
+
if not os.path.exists(db_path):
|
|
202
|
+
return 0
|
|
203
|
+
try:
|
|
204
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
205
|
+
except sqlite3.Error:
|
|
206
|
+
return 0
|
|
207
|
+
try:
|
|
208
|
+
# Sum across every user table so a newly-added store shows up too.
|
|
209
|
+
tables = [
|
|
210
|
+
r[0] for r in conn.execute(
|
|
211
|
+
"SELECT name FROM sqlite_master "
|
|
212
|
+
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
213
|
+
).fetchall()
|
|
214
|
+
]
|
|
215
|
+
total = 0
|
|
216
|
+
for t in tables:
|
|
217
|
+
try:
|
|
218
|
+
row = conn.execute(f"SELECT COUNT(*) FROM {t}").fetchone()
|
|
219
|
+
if row:
|
|
220
|
+
total += int(row[0])
|
|
221
|
+
except sqlite3.OperationalError:
|
|
222
|
+
pass
|
|
223
|
+
return total
|
|
224
|
+
finally:
|
|
225
|
+
conn.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _safe_size(path: str) -> int:
|
|
229
|
+
try:
|
|
230
|
+
return os.path.getsize(path)
|
|
231
|
+
except OSError:
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _fmt_bytes(n: int) -> str:
|
|
236
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
237
|
+
if n < 1024:
|
|
238
|
+
return f"{n}{unit}" if unit == "B" else f"{n:.1f}{unit}"
|
|
239
|
+
n //= 1024
|
|
240
|
+
return f"{n}TB"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# MCP tools
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
def overlay_create(name: str, description: str = "") -> str:
|
|
250
|
+
"""Capture the current code-intel state as a named overlay.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
name: Short filesystem-safe overlay name (e.g. ``"main"``,
|
|
254
|
+
``"feature-auth"``). Must be alphanumeric plus ``- _ .``.
|
|
255
|
+
description: Optional human-readable description stored alongside
|
|
256
|
+
the overlay for reference.
|
|
257
|
+
|
|
258
|
+
Returns a summary of what was captured.
|
|
259
|
+
"""
|
|
260
|
+
project_dir = _get_project_dir()
|
|
261
|
+
if not _is_valid_name(name):
|
|
262
|
+
return (
|
|
263
|
+
f"overlay_create: invalid overlay name {name!r}. "
|
|
264
|
+
f"Use alphanumeric plus '-', '_', '.'. Leading underscores reserved."
|
|
265
|
+
)
|
|
266
|
+
odir = _overlay_dir(project_dir, name)
|
|
267
|
+
if os.path.exists(odir):
|
|
268
|
+
return (
|
|
269
|
+
f"overlay_create: overlay {name!r} already exists at {odir}. "
|
|
270
|
+
f"Delete it first with overlay_delete, or pick a different name."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
attocode = _attocode_dir(project_dir)
|
|
274
|
+
if not os.path.exists(attocode):
|
|
275
|
+
return "overlay_create: no .attocode/ directory to capture."
|
|
276
|
+
|
|
277
|
+
stores_dir = os.path.join(odir, "stores")
|
|
278
|
+
os.makedirs(stores_dir, exist_ok=True)
|
|
279
|
+
try:
|
|
280
|
+
copied = _copy_store_to(stores_dir, attocode)
|
|
281
|
+
except Exception as exc: # noqa: BLE001
|
|
282
|
+
shutil.rmtree(odir, ignore_errors=True)
|
|
283
|
+
return f"overlay_create: copy failed, rolled back: {exc}"
|
|
284
|
+
|
|
285
|
+
if not copied:
|
|
286
|
+
shutil.rmtree(odir, ignore_errors=True)
|
|
287
|
+
return "overlay_create: nothing to capture (no store files present)."
|
|
288
|
+
|
|
289
|
+
# Copy cache_manifest.json if present (top-level — not in stores/).
|
|
290
|
+
manifest_src = os.path.join(attocode, "cache_manifest.json")
|
|
291
|
+
if os.path.exists(manifest_src):
|
|
292
|
+
shutil.copy2(manifest_src, os.path.join(odir, "cache_manifest.json"))
|
|
293
|
+
|
|
294
|
+
total_size = sum(
|
|
295
|
+
_safe_size(os.path.join(stores_dir, n)) for n in copied
|
|
296
|
+
)
|
|
297
|
+
metadata = {
|
|
298
|
+
"schema_version": OVERLAY_SCHEMA_VERSION,
|
|
299
|
+
"name": name,
|
|
300
|
+
"description": description,
|
|
301
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
302
|
+
"stores": copied,
|
|
303
|
+
"total_bytes": total_size,
|
|
304
|
+
}
|
|
305
|
+
with open(os.path.join(odir, "overlay.json"), "w", encoding="utf-8") as f:
|
|
306
|
+
json.dump(metadata, f, indent=2, sort_keys=True)
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
f"overlay_create: captured {len(copied)} store(s) "
|
|
310
|
+
f"({_fmt_bytes(total_size)}) as {name!r} at {odir}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def overlay_list() -> str:
|
|
316
|
+
"""List all named overlays under ``.attocode/overlays/``."""
|
|
317
|
+
project_dir = _get_project_dir()
|
|
318
|
+
root = _overlays_root(project_dir)
|
|
319
|
+
if not os.path.isdir(root):
|
|
320
|
+
return "overlay_list: no overlays/ directory."
|
|
321
|
+
|
|
322
|
+
state = _read_state(project_dir)
|
|
323
|
+
active = state.get("active")
|
|
324
|
+
|
|
325
|
+
entries: list[tuple[str, dict[str, Any]]] = []
|
|
326
|
+
for name in sorted(os.listdir(root)):
|
|
327
|
+
if name.startswith("_") or not _is_valid_name(name):
|
|
328
|
+
continue
|
|
329
|
+
odir = os.path.join(root, name)
|
|
330
|
+
if not os.path.isdir(odir):
|
|
331
|
+
continue
|
|
332
|
+
meta_path = os.path.join(odir, "overlay.json")
|
|
333
|
+
if os.path.exists(meta_path):
|
|
334
|
+
try:
|
|
335
|
+
with open(meta_path, encoding="utf-8") as f:
|
|
336
|
+
meta = json.loads(f.read())
|
|
337
|
+
except (OSError, json.JSONDecodeError):
|
|
338
|
+
meta = {}
|
|
339
|
+
else:
|
|
340
|
+
meta = {}
|
|
341
|
+
entries.append((name, meta))
|
|
342
|
+
|
|
343
|
+
if not entries:
|
|
344
|
+
return "overlay_list: no named overlays found."
|
|
345
|
+
|
|
346
|
+
lines = [f"overlay_list: {len(entries)} overlay(s) (active={active or 'none'}):"]
|
|
347
|
+
for name, meta in entries:
|
|
348
|
+
marker = "*" if name == active else " "
|
|
349
|
+
created = meta.get("created_at", "?")
|
|
350
|
+
size = meta.get("total_bytes", 0)
|
|
351
|
+
desc = meta.get("description", "")
|
|
352
|
+
suffix = f" {desc!r}" if desc else ""
|
|
353
|
+
lines.append(
|
|
354
|
+
f" {marker} {name:20s} "
|
|
355
|
+
f"{_fmt_bytes(size):>10s} {created}{suffix}"
|
|
356
|
+
)
|
|
357
|
+
return "\n".join(lines)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@mcp.tool()
|
|
361
|
+
def overlay_status() -> str:
|
|
362
|
+
"""Report which overlay (if any) is currently marked active.
|
|
363
|
+
|
|
364
|
+
The live code-intel state is always whatever is in ``.attocode/``;
|
|
365
|
+
the "active" marker just records the *name* of the overlay that
|
|
366
|
+
was last activated. Creating an overlay does not change the active
|
|
367
|
+
marker — activation does.
|
|
368
|
+
"""
|
|
369
|
+
project_dir = _get_project_dir()
|
|
370
|
+
state = _read_state(project_dir)
|
|
371
|
+
active = state.get("active")
|
|
372
|
+
history = state.get("history", [])
|
|
373
|
+
|
|
374
|
+
if not active:
|
|
375
|
+
return (
|
|
376
|
+
"overlay_status: no overlay activated yet. "
|
|
377
|
+
"Create one with overlay_create(name)."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
lines = [f"overlay_status: active={active!r}"]
|
|
381
|
+
if history:
|
|
382
|
+
lines.append(f" recent: {', '.join(history[-5:])}")
|
|
383
|
+
return "\n".join(lines)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@mcp.tool()
|
|
387
|
+
def overlay_activate(
|
|
388
|
+
name: str,
|
|
389
|
+
save_current_as: str = "",
|
|
390
|
+
confirm: bool = False,
|
|
391
|
+
) -> str:
|
|
392
|
+
"""Swap the live ``.attocode/`` state with an overlay.
|
|
393
|
+
|
|
394
|
+
This copies every store file from ``.attocode/overlays/<name>/``
|
|
395
|
+
into the live ``.attocode/`` tree, overwriting anything in place.
|
|
396
|
+
Per-file atomic via tempfile + rename. If ``save_current_as`` is
|
|
397
|
+
provided, the current live state is captured as a new overlay
|
|
398
|
+
BEFORE the swap so you can return to it later.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
name: Existing overlay name to restore.
|
|
402
|
+
save_current_as: Optional name for a pre-swap backup overlay.
|
|
403
|
+
Empty means "don't back up — just swap". Skipping this
|
|
404
|
+
silently loses the pre-swap state if no prior overlay has it.
|
|
405
|
+
confirm: Must be True to apply. Default False returns a dry run.
|
|
406
|
+
"""
|
|
407
|
+
project_dir = _get_project_dir()
|
|
408
|
+
if not _is_valid_name(name):
|
|
409
|
+
return f"overlay_activate: invalid overlay name {name!r}"
|
|
410
|
+
|
|
411
|
+
odir = _overlay_dir(project_dir, name)
|
|
412
|
+
if not os.path.isdir(odir):
|
|
413
|
+
return f"overlay_activate: no overlay named {name!r}"
|
|
414
|
+
|
|
415
|
+
stores_src = os.path.join(odir, "stores")
|
|
416
|
+
if not os.path.isdir(stores_src):
|
|
417
|
+
return f"overlay_activate: overlay {name!r} missing stores/ directory"
|
|
418
|
+
|
|
419
|
+
attocode = _attocode_dir(project_dir)
|
|
420
|
+
state = _read_state(project_dir)
|
|
421
|
+
|
|
422
|
+
if save_current_as and not _is_valid_name(save_current_as):
|
|
423
|
+
return (
|
|
424
|
+
f"overlay_activate: invalid save_current_as name "
|
|
425
|
+
f"{save_current_as!r}"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Preview the component list so the dry-run summary is useful.
|
|
429
|
+
components = sorted(os.listdir(stores_src)) if os.path.isdir(stores_src) else []
|
|
430
|
+
|
|
431
|
+
if not confirm:
|
|
432
|
+
lines = [
|
|
433
|
+
"overlay_activate: DRY RUN — nothing swapped.",
|
|
434
|
+
f" source: {odir}",
|
|
435
|
+
f" components: {len(components)}",
|
|
436
|
+
]
|
|
437
|
+
if save_current_as:
|
|
438
|
+
lines.append(f" will save current live state as: {save_current_as!r}")
|
|
439
|
+
else:
|
|
440
|
+
lines.append(" will NOT save current live state (pass save_current_as to keep it)")
|
|
441
|
+
lines.append(" Re-run with confirm=True to apply.")
|
|
442
|
+
return "\n".join(lines)
|
|
443
|
+
|
|
444
|
+
# 1. Save current state if requested.
|
|
445
|
+
if save_current_as:
|
|
446
|
+
save_dir = _overlay_dir(project_dir, save_current_as)
|
|
447
|
+
if os.path.exists(save_dir):
|
|
448
|
+
return (
|
|
449
|
+
f"overlay_activate: save_current_as={save_current_as!r} already "
|
|
450
|
+
f"exists. Delete it first or pick another name."
|
|
451
|
+
)
|
|
452
|
+
try:
|
|
453
|
+
os.makedirs(os.path.join(save_dir, "stores"), exist_ok=True)
|
|
454
|
+
saved = _copy_store_to(os.path.join(save_dir, "stores"), attocode)
|
|
455
|
+
manifest_src = os.path.join(attocode, "cache_manifest.json")
|
|
456
|
+
if os.path.exists(manifest_src):
|
|
457
|
+
shutil.copy2(manifest_src, os.path.join(save_dir, "cache_manifest.json"))
|
|
458
|
+
metadata = {
|
|
459
|
+
"schema_version": OVERLAY_SCHEMA_VERSION,
|
|
460
|
+
"name": save_current_as,
|
|
461
|
+
"description": f"auto-saved before activating {name!r}",
|
|
462
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
463
|
+
"stores": saved,
|
|
464
|
+
"total_bytes": sum(
|
|
465
|
+
_safe_size(os.path.join(save_dir, "stores", n)) for n in saved
|
|
466
|
+
),
|
|
467
|
+
}
|
|
468
|
+
with open(os.path.join(save_dir, "overlay.json"), "w", encoding="utf-8") as f:
|
|
469
|
+
json.dump(metadata, f, indent=2, sort_keys=True)
|
|
470
|
+
except Exception as exc: # noqa: BLE001
|
|
471
|
+
shutil.rmtree(save_dir, ignore_errors=True)
|
|
472
|
+
return f"overlay_activate: failed to save current state: {exc}"
|
|
473
|
+
|
|
474
|
+
# 2. Restore from target overlay.
|
|
475
|
+
try:
|
|
476
|
+
restored = _copy_store_from(stores_src, attocode)
|
|
477
|
+
except Exception as exc: # noqa: BLE001
|
|
478
|
+
return f"overlay_activate: restore failed: {exc}"
|
|
479
|
+
|
|
480
|
+
# 3. Copy cache_manifest.json if the overlay has one.
|
|
481
|
+
manifest_src = os.path.join(odir, "cache_manifest.json")
|
|
482
|
+
if os.path.exists(manifest_src):
|
|
483
|
+
shutil.copy2(manifest_src, os.path.join(attocode, "cache_manifest.json"))
|
|
484
|
+
|
|
485
|
+
# 4. Update state.
|
|
486
|
+
history = state.get("history", [])
|
|
487
|
+
prior_active = state.get("active")
|
|
488
|
+
if prior_active and prior_active != name:
|
|
489
|
+
history = [*history, prior_active][-10:]
|
|
490
|
+
state["active"] = name
|
|
491
|
+
state["history"] = history
|
|
492
|
+
_write_state(project_dir, state)
|
|
493
|
+
|
|
494
|
+
lines = [
|
|
495
|
+
f"overlay_activate: activated {name!r}",
|
|
496
|
+
f" restored: {len(restored)} component(s)",
|
|
497
|
+
]
|
|
498
|
+
if save_current_as:
|
|
499
|
+
lines.append(f" saved prior state as: {save_current_as!r}")
|
|
500
|
+
return "\n".join(lines)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@mcp.tool()
|
|
504
|
+
def overlay_delete(name: str, confirm: bool = False) -> str:
|
|
505
|
+
"""Remove a named overlay directory.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
name: Overlay to delete.
|
|
509
|
+
confirm: Must be True to apply.
|
|
510
|
+
"""
|
|
511
|
+
project_dir = _get_project_dir()
|
|
512
|
+
if not _is_valid_name(name):
|
|
513
|
+
return f"overlay_delete: invalid overlay name {name!r}"
|
|
514
|
+
odir = _overlay_dir(project_dir, name)
|
|
515
|
+
if not os.path.isdir(odir):
|
|
516
|
+
return f"overlay_delete: no overlay named {name!r}"
|
|
517
|
+
|
|
518
|
+
# Size preview.
|
|
519
|
+
total = 0
|
|
520
|
+
for root, _, files in os.walk(odir):
|
|
521
|
+
for f in files:
|
|
522
|
+
total += _safe_size(os.path.join(root, f))
|
|
523
|
+
|
|
524
|
+
if not confirm:
|
|
525
|
+
return (
|
|
526
|
+
f"overlay_delete: DRY RUN — would delete {odir} "
|
|
527
|
+
f"({_fmt_bytes(total)}). Re-run with confirm=True."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
shutil.rmtree(odir, ignore_errors=False)
|
|
531
|
+
|
|
532
|
+
# If this was the active overlay, clear the state marker (the live
|
|
533
|
+
# .attocode/ tree is unaffected, but we no longer have a named
|
|
534
|
+
# reference to the state it matches).
|
|
535
|
+
state = _read_state(project_dir)
|
|
536
|
+
if state.get("active") == name:
|
|
537
|
+
state["active"] = None
|
|
538
|
+
_write_state(project_dir, state)
|
|
539
|
+
|
|
540
|
+
return f"overlay_delete: removed {name!r} ({_fmt_bytes(total)} freed)"
|