@pugi/cli 0.1.0-beta.9 → 0.1.0-beta.91
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.
- package/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +13 -13
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +333 -7
- package/dist/core/edits/format-detector.js +260 -0
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +5 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +29 -29
- package/dist/core/engine/anvil-client.js +214 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +129 -19
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1792 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +46 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +216 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +551 -41
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +664 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2148 -217
- package/dist/core/repl/slash-commands.js +501 -41
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +324 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +30 -30
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4185 -549
- package/dist/runtime/commands/agents.js +31 -31
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +73 -39
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +27 -4
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +187 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +200 -38
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +12 -12
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +8 -8
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +22 -22
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +89 -28
- package/dist/tools/ask-user-question.js +337 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/cron.js +433 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +99 -4
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +315 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/multi-file-diff-approval.js +375 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +36 -1
- package/dist/tui/repl-render.js +176 -25
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +125 -45
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/package.json +31 -16
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +12 -0
- package/test/scenarios/identity.scenario.txt +12 -0
- package/test/scenarios/persona-handoff.scenario.txt +12 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-from-extension detection — .
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for "given a file path, which `LspLanguage`
|
|
5
|
+
* slug do we route to". The `runtime/commands/lsp.ts` shipped its
|
|
6
|
+
* own inline `inferLanguage` switch; L15 (post-edit auto-diagnostics)
|
|
7
|
+
* needs the same lookup from `core/engine/tool-bridge.ts`, so we lift
|
|
8
|
+
* the table into a dedicated module to avoid a second copy drifting
|
|
9
|
+
* out of sync.
|
|
10
|
+
*
|
|
11
|
+
* Returning `undefined` is the calling code's signal to silently skip
|
|
12
|
+
* LSP — an unsupported extension is NOT an error, it just means "no
|
|
13
|
+
* diagnostics for this file". The tool-bridge hook treats this as a
|
|
14
|
+
* no-op envelope tail.
|
|
15
|
+
*
|
|
16
|
+
* Adding a new language requires THREE coordinated changes:
|
|
17
|
+
* 1. Add the `LspLanguage` slug + server descriptor in `client.ts`.
|
|
18
|
+
* 2. Map its extensions here.
|
|
19
|
+
* 3. Add a `lsp-language-matrix` spec row exercising the new ext.
|
|
20
|
+
*
|
|
21
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
22
|
+
*/
|
|
23
|
+
import { extname } from 'node:path';
|
|
24
|
+
/**
|
|
25
|
+
* Lower-case extension (including the dot) → LSP language slug.
|
|
26
|
+
* Mirror of the switch in `runtime/commands/lsp.ts::inferLanguage`.
|
|
27
|
+
* The table form lets tests assert coverage and lets new languages
|
|
28
|
+
* land with one edit instead of two.
|
|
29
|
+
*/
|
|
30
|
+
export const EXTENSION_TO_LANGUAGE = {
|
|
31
|
+
'.ts': 'ts',
|
|
32
|
+
'.tsx': 'ts',
|
|
33
|
+
'.mts': 'ts',
|
|
34
|
+
'.cts': 'ts',
|
|
35
|
+
'.js': 'js',
|
|
36
|
+
'.jsx': 'js',
|
|
37
|
+
'.mjs': 'js',
|
|
38
|
+
'.cjs': 'js',
|
|
39
|
+
'.py': 'py',
|
|
40
|
+
'.pyi': 'py',
|
|
41
|
+
'.go': 'go',
|
|
42
|
+
'.rs': 'rust',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Infer the `LspLanguage` for a workspace-relative or absolute path.
|
|
46
|
+
* Returns `undefined` for unmapped extensions — the caller decides
|
|
47
|
+
* whether that is silently skipped (post-edit hook) or surfaced as
|
|
48
|
+
* `language_unsupported` (`pugi lsp` CLI).
|
|
49
|
+
*/
|
|
50
|
+
export function languageForFile(file) {
|
|
51
|
+
const ext = extname(file).toLowerCase();
|
|
52
|
+
if (!ext)
|
|
53
|
+
return undefined;
|
|
54
|
+
return EXTENSION_TO_LANGUAGE[ext];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Return every extension currently mapped to the given language.
|
|
58
|
+
* Used by the matrix spec to assert coverage without re-typing the
|
|
59
|
+
* extension list.
|
|
60
|
+
*/
|
|
61
|
+
export function extensionsForLanguage(lang) {
|
|
62
|
+
return Object.entries(EXTENSION_TO_LANGUAGE)
|
|
63
|
+
.filter(([, value]) => value === lang)
|
|
64
|
+
.map(([ext]) => ext);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=language-detect.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-edit diagnostics — .
|
|
3
|
+
*
|
|
4
|
+
* the upstream tool's leak intel surfaced this pattern: after a `FileEdit` /
|
|
5
|
+
* `Write` tool call lands, an LSP diagnostic pass runs on the touched
|
|
6
|
+
* file and the result is appended to the tool envelope before the
|
|
7
|
+
* model sees it. The model then self-corrects in the same turn —
|
|
8
|
+
* "TS2304: Cannot find name 'undef'" comes back, the model fixes the
|
|
9
|
+
* typo in the next tool call, no operator round-trip needed.
|
|
10
|
+
*
|
|
11
|
+
* This module is the Pugi side of that pattern:
|
|
12
|
+
*
|
|
13
|
+
* 1. The tool-bridge calls `runPostEditDiagnostics(path, ctx)` after
|
|
14
|
+
* a successful `edit` / `write` / `multi_edit`.
|
|
15
|
+
* 2. We infer the language from the extension (`language-detect`).
|
|
16
|
+
* Unsupported extension → `{ skip: true }` and the bridge appends
|
|
17
|
+
* nothing.
|
|
18
|
+
* 3. We borrow (or lazily start) the per-language cached client
|
|
19
|
+
* from `cache.ts`. A spawn failure → `{ skip: true }` and the
|
|
20
|
+
* envelope stays clean. Silence on failure is intentional: an
|
|
21
|
+
* operator who has not installed `typescript-language-server`
|
|
22
|
+
* should not see an LSP nag on every edit.
|
|
23
|
+
* 4. We pull diagnostics with a hard 5s ceiling. A timeout logs a
|
|
24
|
+
* warning on stderr (gated on `PUGI_LSP_DEBUG=1`) and skips —
|
|
25
|
+
* the envelope is never blocked on LSP.
|
|
26
|
+
* 5. We format the surviving diagnostics into a readable tail
|
|
27
|
+
* mirroring the leak format:
|
|
28
|
+
*
|
|
29
|
+
* LSP DIAGNOSTICS (typescript):
|
|
30
|
+
* foo.ts:42:5 error TS2304: Cannot find name 'undef'.
|
|
31
|
+
* foo.ts:51:1 warn TS6133: 'unused' is declared.
|
|
32
|
+
*
|
|
33
|
+
* The bridge concatenates this tail onto its existing `wrote ...` /
|
|
34
|
+
* `edited ...` body with a single newline separator. When there are
|
|
35
|
+
* zero diagnostics we return `{ skip: true }` so the existing body
|
|
36
|
+
* is unchanged — the "no news is good news" path stays terse.
|
|
37
|
+
*
|
|
38
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
39
|
+
*/
|
|
40
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
41
|
+
import { getOrStartLspClient } from './cache.js';
|
|
42
|
+
import { languageForFile } from './language-detect.js';
|
|
43
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
44
|
+
/**
|
|
45
|
+
* Hard cap on how many diagnostics we surface to the model. A file
|
|
46
|
+
* with 200 errors after a broken bulk edit would otherwise blow the
|
|
47
|
+
* context window; the model can re-run `pugi lsp diagnostics` if
|
|
48
|
+
* it needs the full list.
|
|
49
|
+
*/
|
|
50
|
+
const MAX_DIAGNOSTICS = 25;
|
|
51
|
+
export async function runPostEditDiagnostics(filePath, opts) {
|
|
52
|
+
const lang = languageForFile(filePath);
|
|
53
|
+
if (!lang) {
|
|
54
|
+
return { skip: true, reason: 'unsupported_language' };
|
|
55
|
+
}
|
|
56
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
const clientResult = await loadClient(lang, opts);
|
|
58
|
+
if (!clientResult.ok) {
|
|
59
|
+
return { skip: true, reason: mapStartFailure(clientResult.reason) };
|
|
60
|
+
}
|
|
61
|
+
// Run diagnostics with a hard timeout. The underlying LspClient has
|
|
62
|
+
// its own per-request timeout (5s default) but a slow handshake
|
|
63
|
+
// can blow past it; we belt-and-suspenders here so the agent loop
|
|
64
|
+
// never blocks on LSP.
|
|
65
|
+
const relPath = toWorkspaceRelative(filePath, opts.cwd);
|
|
66
|
+
const diagnosticsPromise = clientResult.client.diagnostics(relPath);
|
|
67
|
+
let timer;
|
|
68
|
+
const timeoutPromise = new Promise((resolveFn) => {
|
|
69
|
+
timer = setTimeout(() => resolveFn({ timedOut: true }), timeoutMs);
|
|
70
|
+
timer.unref();
|
|
71
|
+
});
|
|
72
|
+
const race = await Promise.race([
|
|
73
|
+
diagnosticsPromise.then((value) => ({ timedOut: false, value })),
|
|
74
|
+
timeoutPromise,
|
|
75
|
+
]);
|
|
76
|
+
if (timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (race.timedOut) {
|
|
79
|
+
const writeFn = opts.debugWrite ?? ((line) => {
|
|
80
|
+
if (process.env.PUGI_LSP_DEBUG === '1')
|
|
81
|
+
process.stderr.write(`${line}\n`);
|
|
82
|
+
});
|
|
83
|
+
writeFn(`[pugi-lsp] post-edit diagnostics for ${relPath} timed out after ${timeoutMs}ms (lang=${lang})`);
|
|
84
|
+
return { skip: true, reason: 'timeout' };
|
|
85
|
+
}
|
|
86
|
+
const diag = race.value;
|
|
87
|
+
if (!diag.ok) {
|
|
88
|
+
return { skip: true, reason: 'lsp_error' };
|
|
89
|
+
}
|
|
90
|
+
if (diag.value.length === 0) {
|
|
91
|
+
return { skip: true, reason: 'no_diagnostics' };
|
|
92
|
+
}
|
|
93
|
+
const tail = formatDiagnosticsTail(relPath, lang, diag.value);
|
|
94
|
+
return { skip: false, tail, count: diag.value.length, language: lang };
|
|
95
|
+
}
|
|
96
|
+
async function loadClient(lang, opts) {
|
|
97
|
+
if (opts.clientLoader) {
|
|
98
|
+
return opts.clientLoader(lang);
|
|
99
|
+
}
|
|
100
|
+
const { cwd, timeoutMs, clientLoader: _ignoredA, debugWrite: _ignoredB, ...rest } = opts;
|
|
101
|
+
const result = await getOrStartLspClient(lang, { cwd, ...rest });
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
return { ok: false, reason: result.reason, detail: result.detail };
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, client: result.client };
|
|
106
|
+
}
|
|
107
|
+
function mapStartFailure(reason) {
|
|
108
|
+
if (reason === 'lsp_unavailable' || reason === 'language_unsupported')
|
|
109
|
+
return 'lsp_unavailable';
|
|
110
|
+
if (reason === 'lsp_disabled')
|
|
111
|
+
return 'lsp_disabled';
|
|
112
|
+
return 'lsp_error';
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Convert an absolute or workspace-relative path into the form the
|
|
116
|
+
* LSP client expects — same shape as `runtime/commands/lsp.ts` uses.
|
|
117
|
+
*/
|
|
118
|
+
function toWorkspaceRelative(filePath, cwd) {
|
|
119
|
+
if (!isAbsolute(filePath))
|
|
120
|
+
return filePath;
|
|
121
|
+
const rel = relative(cwd, resolve(cwd, filePath));
|
|
122
|
+
return rel || filePath;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format diagnostics into the leak-shaped envelope tail. Pure function
|
|
126
|
+
* exported for unit tests to assert the line format independent of
|
|
127
|
+
* any LSP plumbing.
|
|
128
|
+
*/
|
|
129
|
+
export function formatDiagnosticsTail(relPath, lang, diagnostics) {
|
|
130
|
+
const visible = diagnostics.slice(0, MAX_DIAGNOSTICS);
|
|
131
|
+
const truncated = diagnostics.length > visible.length;
|
|
132
|
+
const lines = [`LSP DIAGNOSTICS (${LANGUAGE_LABELS[lang]}):`];
|
|
133
|
+
for (const diag of visible) {
|
|
134
|
+
const line = diag.range.start.line + 1; // LSP is zero-based; humans expect 1-based.
|
|
135
|
+
const col = diag.range.start.character + 1;
|
|
136
|
+
const severity = SEVERITY_LABELS[diag.severityLabel];
|
|
137
|
+
const code = diag.code !== undefined && diag.code !== '' ? ` ${diag.code}` : '';
|
|
138
|
+
const source = diag.source ? `${diag.source}` : '';
|
|
139
|
+
const head = source ? `${severity}${code} (${source}):` : `${severity}${code}:`;
|
|
140
|
+
lines.push(` ${relPath}:${line}:${col} ${head} ${diag.message}`);
|
|
141
|
+
}
|
|
142
|
+
if (truncated) {
|
|
143
|
+
lines.push(` ... ${diagnostics.length - visible.length} more diagnostic(s) — re-run pugi lsp diagnostics ${relPath} for the full list`);
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
const LANGUAGE_LABELS = {
|
|
148
|
+
ts: 'typescript',
|
|
149
|
+
js: 'javascript',
|
|
150
|
+
py: 'python',
|
|
151
|
+
go: 'go',
|
|
152
|
+
rust: 'rust',
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Map LSP severity label → the short token the leak envelope uses.
|
|
156
|
+
* "warn" is shorter than "warning" and matches the upstream tool's leak
|
|
157
|
+
* verbatim; the rest mirror LSP terminology.
|
|
158
|
+
*/
|
|
159
|
+
const SEVERITY_LABELS = {
|
|
160
|
+
error: 'error',
|
|
161
|
+
warning: 'warn ',
|
|
162
|
+
info: 'info ',
|
|
163
|
+
hint: 'hint ',
|
|
164
|
+
};
|
|
165
|
+
/** Test-only surface so specs can poke the pure helpers without LSP. */
|
|
166
|
+
export const __test__ = {
|
|
167
|
+
formatDiagnosticsTail,
|
|
168
|
+
MAX_DIAGNOSTICS,
|
|
169
|
+
DEFAULT_TIMEOUT_MS,
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=post-edit-diagnostics.js.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PUGI-78 Phase 1: LSP server discovery via `~/.pugi/lsp-config.json`
|
|
3
|
+
* + PATH probe.
|
|
4
|
+
*
|
|
5
|
+
* The base `LANGUAGE_SERVERS` registry in `client.ts` ships sane
|
|
6
|
+
* defaults (TypeScript via npx, pyright/gopls/rust-analyzer assumed on
|
|
7
|
+
* PATH). Operators who run a non-standard layout (NixOS, asdf, custom
|
|
8
|
+
* monorepo binaries) override per-language commands via a small JSON
|
|
9
|
+
* file at `$HOME/.pugi/lsp-config.json`:
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* {
|
|
13
|
+
* "typescript": {
|
|
14
|
+
* "command": "/usr/local/bin/typescript-language-server",
|
|
15
|
+
* "args": ["--stdio"]
|
|
16
|
+
* },
|
|
17
|
+
* "python": {
|
|
18
|
+
* "command": "pylsp",
|
|
19
|
+
* "args": []
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The file is loaded once per process; failures are non-fatal (missing
|
|
25
|
+
* file => empty override map, parse error => log + empty override).
|
|
26
|
+
* The standard registry stays the fallback. The operator-facing CLI
|
|
27
|
+
* surface (`pugi lsp servers`) reports both detected binaries (via
|
|
28
|
+
* `inspectLspServers` in client.ts) and any operator overrides loaded
|
|
29
|
+
* from this module.
|
|
30
|
+
*
|
|
31
|
+
* Why a JSON file (not a flag): the operator may have several distinct
|
|
32
|
+
* LSP layouts (e.g. one for the monorepo, one for an isolated package).
|
|
33
|
+
* A central config in `~/.pugi/` lets every workspace inherit the same
|
|
34
|
+
* defaults without re-typing flags on every `pugi` invocation; the
|
|
35
|
+
* workspace-local `.pugi/settings.json::lsp` toggle (already shipped
|
|
36
|
+
* in β7 L9) layers on top to disable specific languages per-workspace.
|
|
37
|
+
*
|
|
38
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
39
|
+
*/
|
|
40
|
+
import { homedir } from 'node:os';
|
|
41
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
42
|
+
import { join } from 'node:path';
|
|
43
|
+
import { spawnSync } from 'node:child_process';
|
|
44
|
+
/**
|
|
45
|
+
* Settings file path. Computed lazily so a spec can inject `HOME` via
|
|
46
|
+
* `process.env.HOME = '/tmp/test'` before importing this module.
|
|
47
|
+
*/
|
|
48
|
+
export function lspConfigPath() {
|
|
49
|
+
return join(homedir(), '.pugi', 'lsp-config.json');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load the operator override map from `$HOME/.pugi/lsp-config.json`.
|
|
53
|
+
* Non-fatal failures: missing file -> empty map; malformed JSON ->
|
|
54
|
+
* empty map + stderr warning when `PUGI_LSP_DEBUG=1`. Returns the
|
|
55
|
+
* parsed map; any unrecognized language slug is dropped silently.
|
|
56
|
+
*
|
|
57
|
+
* Synchronous because this is a one-shot bootstrap path called from
|
|
58
|
+
* the CLI surface before any LSP client spawn; the file is tiny (<2 KB
|
|
59
|
+
* in practice) so the sync read cost is negligible.
|
|
60
|
+
*/
|
|
61
|
+
export function loadOperatorOverrides(path) {
|
|
62
|
+
const resolved = path ?? lspConfigPath();
|
|
63
|
+
if (!existsSync(resolved))
|
|
64
|
+
return {};
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = readFileSync(resolved, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
if (process.env.PUGI_LSP_DEBUG === '1') {
|
|
78
|
+
process.stderr.write(`[pugi lsp] ignored ${resolved} - invalid JSON\n`);
|
|
79
|
+
}
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
83
|
+
return {};
|
|
84
|
+
const out = {};
|
|
85
|
+
const supported = ['ts', 'js', 'py', 'go', 'rust'];
|
|
86
|
+
// Accept both short slug ("ts") and long name ("typescript") on the
|
|
87
|
+
// operator side - long names are friendlier in a hand-edited file.
|
|
88
|
+
const longNameMap = {
|
|
89
|
+
typescript: 'ts',
|
|
90
|
+
javascript: 'js',
|
|
91
|
+
python: 'py',
|
|
92
|
+
go: 'go',
|
|
93
|
+
rust: 'rust',
|
|
94
|
+
ts: 'ts',
|
|
95
|
+
js: 'js',
|
|
96
|
+
py: 'py',
|
|
97
|
+
rs: 'rust',
|
|
98
|
+
};
|
|
99
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
100
|
+
const lang = longNameMap[key.toLowerCase()];
|
|
101
|
+
if (!lang || !supported.includes(lang))
|
|
102
|
+
continue;
|
|
103
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
104
|
+
continue;
|
|
105
|
+
const v = value;
|
|
106
|
+
if (typeof v.command !== 'string' || v.command.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const args = [];
|
|
109
|
+
if (Array.isArray(v.args)) {
|
|
110
|
+
for (const a of v.args) {
|
|
111
|
+
if (typeof a === 'string')
|
|
112
|
+
args.push(a);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
out[lang] = { command: v.command, args };
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Discover every supported language's server given the registry defaults
|
|
121
|
+
* + the operator override map. `defaultRegistry` is injected so the
|
|
122
|
+
* caller (the CLI surface) can pass the live `LANGUAGE_SERVERS` from
|
|
123
|
+
* `client.ts` without forming a circular module dep.
|
|
124
|
+
*/
|
|
125
|
+
export function detectServers(defaultRegistry, overrides = loadOperatorOverrides()) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const lang of Object.keys(defaultRegistry)) {
|
|
128
|
+
const base = defaultRegistry[lang];
|
|
129
|
+
const override = overrides[lang];
|
|
130
|
+
if (override) {
|
|
131
|
+
out.push({
|
|
132
|
+
language: lang,
|
|
133
|
+
source: 'override',
|
|
134
|
+
command: override.command,
|
|
135
|
+
args: override.args,
|
|
136
|
+
available: detectBinaryOnPath(override.command),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
out.push({
|
|
141
|
+
language: lang,
|
|
142
|
+
source: 'default',
|
|
143
|
+
command: base.command,
|
|
144
|
+
args: base.args,
|
|
145
|
+
available: detectBinaryOnPath(base.probe),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Cross-platform binary detection. POSIX = `which`, Windows = `where`.
|
|
153
|
+
* Failures are non-fatal — we just report unavailable. We avoid
|
|
154
|
+
* `spawnSync(name, ['--version'])` because some servers (gopls, older
|
|
155
|
+
* pyright) do not honor `--version` and exit non-zero, which would
|
|
156
|
+
* mis-flag them as missing.
|
|
157
|
+
*/
|
|
158
|
+
export function detectBinaryOnPath(name) {
|
|
159
|
+
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
160
|
+
try {
|
|
161
|
+
const result = spawnSync(probe, [name], { stdio: 'ignore' });
|
|
162
|
+
return result.status === 0;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Test-only — direct access to the long-name accepting map. */
|
|
169
|
+
export const __test__ = {
|
|
170
|
+
detectBinaryOnPath,
|
|
171
|
+
lspConfigPath,
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=server-detect.js.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Default TTL for cached entries. 5 minutes matches the spec target. */
|
|
2
|
+
export const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
3
|
+
/** Default size cap. Empirically large enough for a heavy navigate session. */
|
|
4
|
+
export const DEFAULT_MAX_ENTRIES = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Generic TTL cache. The unknown value type is intentional — the cache
|
|
7
|
+
* stores serialized result objects produced by the 13 symbol tools; we
|
|
8
|
+
* surface them back to the caller as the SAME unknown so the cache
|
|
9
|
+
* does not encode the result schema.
|
|
10
|
+
*
|
|
11
|
+
* Thread-safety: Node single-threaded, no concurrent writes possible
|
|
12
|
+
* inside one `pugi` invocation. The cache is NOT shared across
|
|
13
|
+
* spawned subagents — each subagent runs its own LspClient + cache.
|
|
14
|
+
*/
|
|
15
|
+
export class SymbolCache {
|
|
16
|
+
store = new Map();
|
|
17
|
+
ttlMs;
|
|
18
|
+
maxEntries;
|
|
19
|
+
now;
|
|
20
|
+
metrics = { hits: 0, misses: 0, evictions: 0, size: 0 };
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
23
|
+
this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
24
|
+
this.now = options?.now ?? (() => Date.now());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a canonical key from the dispatch coordinates. Workspace +
|
|
28
|
+
* language scopes the cache so two different repos opened in the same
|
|
29
|
+
* REPL session do not pollute each other.
|
|
30
|
+
*/
|
|
31
|
+
static makeKey(lang, workspace, verb, args) {
|
|
32
|
+
// Sort keys deterministically so `{file: 'a', line: 1}` and
|
|
33
|
+
// `{line: 1, file: 'a'}` map to the same cache entry.
|
|
34
|
+
const sorted = {};
|
|
35
|
+
for (const key of Object.keys(args).sort()) {
|
|
36
|
+
sorted[key] = args[key];
|
|
37
|
+
}
|
|
38
|
+
return `${lang}::${workspace}::${verb}::${JSON.stringify(sorted)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Look up a cached value. Returns undefined when:
|
|
42
|
+
*
|
|
43
|
+
* - the key is not in the cache
|
|
44
|
+
* - the entry has aged past `ttlMs` (the entry is deleted in this
|
|
45
|
+
* case so the eviction counter is accurate)
|
|
46
|
+
*
|
|
47
|
+
* Hit updates the entry's `lastAccessed` so the LRU policy keeps
|
|
48
|
+
* frequently-touched entries warm.
|
|
49
|
+
*/
|
|
50
|
+
get(key) {
|
|
51
|
+
const entry = this.store.get(key);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
this.metrics.misses++;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const now = this.now();
|
|
57
|
+
if (now - entry.storedAt > this.ttlMs) {
|
|
58
|
+
this.store.delete(key);
|
|
59
|
+
this.metrics.evictions++;
|
|
60
|
+
this.metrics.misses++;
|
|
61
|
+
this.metrics.size = this.store.size;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
entry.lastAccessed = now;
|
|
65
|
+
this.metrics.hits++;
|
|
66
|
+
return entry.result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Store a value. Evicts the LRU entry when the cache hits `maxEntries`.
|
|
70
|
+
* The eviction counter increments for each LRU drop AND each TTL drop
|
|
71
|
+
* so the operator surface can distinguish "cache too small" from "TTL
|
|
72
|
+
* too short".
|
|
73
|
+
*/
|
|
74
|
+
set(key, value) {
|
|
75
|
+
const now = this.now();
|
|
76
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
77
|
+
// Find the LRU entry.
|
|
78
|
+
let oldestKey;
|
|
79
|
+
let oldestAccess = Number.POSITIVE_INFINITY;
|
|
80
|
+
for (const [k, entry] of this.store) {
|
|
81
|
+
if (entry.lastAccessed < oldestAccess) {
|
|
82
|
+
oldestAccess = entry.lastAccessed;
|
|
83
|
+
oldestKey = k;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (oldestKey !== undefined) {
|
|
87
|
+
this.store.delete(oldestKey);
|
|
88
|
+
this.metrics.evictions++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.store.set(key, { result: value, storedAt: now, lastAccessed: now });
|
|
92
|
+
this.metrics.size = this.store.size;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Invalidate every entry for a workspace. Called by the post-edit
|
|
96
|
+
* hook so a successful `edit`/`write` clears stale symbol locations.
|
|
97
|
+
* Returns the number of entries dropped.
|
|
98
|
+
*/
|
|
99
|
+
invalidateWorkspace(workspace) {
|
|
100
|
+
const prefix = `${workspace}::`;
|
|
101
|
+
// Two prefix forms: `<lang>::<workspace>::...` — we scan the full
|
|
102
|
+
// store and drop entries whose key contains the workspace literal.
|
|
103
|
+
let dropped = 0;
|
|
104
|
+
for (const key of Array.from(this.store.keys())) {
|
|
105
|
+
// The key shape is `lang::workspace::verb::args` — split on `::`
|
|
106
|
+
// and check the second segment. Avoid substring match because a
|
|
107
|
+
// workspace path could legally contain `::` (e.g. a Windows
|
|
108
|
+
// network path).
|
|
109
|
+
const parts = key.split('::');
|
|
110
|
+
if (parts.length >= 2 && parts[1] === workspace) {
|
|
111
|
+
this.store.delete(key);
|
|
112
|
+
dropped++;
|
|
113
|
+
}
|
|
114
|
+
else if (parts.length >= 2 && parts[1]?.startsWith(prefix)) {
|
|
115
|
+
this.store.delete(key);
|
|
116
|
+
dropped++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (dropped > 0) {
|
|
120
|
+
this.metrics.evictions += dropped;
|
|
121
|
+
this.metrics.size = this.store.size;
|
|
122
|
+
}
|
|
123
|
+
return dropped;
|
|
124
|
+
}
|
|
125
|
+
/** Drop every cached entry. */
|
|
126
|
+
clear() {
|
|
127
|
+
this.metrics.evictions += this.store.size;
|
|
128
|
+
this.store.clear();
|
|
129
|
+
this.metrics.size = 0;
|
|
130
|
+
}
|
|
131
|
+
/** Snapshot of the current metrics. The returned record is a copy. */
|
|
132
|
+
snapshot() {
|
|
133
|
+
return { ...this.metrics, size: this.store.size };
|
|
134
|
+
}
|
|
135
|
+
/** Test-only: reset metrics counters without dropping entries. */
|
|
136
|
+
resetMetrics() {
|
|
137
|
+
this.metrics = { hits: 0, misses: 0, evictions: 0, size: this.store.size };
|
|
138
|
+
}
|
|
139
|
+
/** Test-only: number of entries currently stored. */
|
|
140
|
+
size() {
|
|
141
|
+
return this.store.size;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Process-global symbol cache, lazily constructed on first access. The
|
|
146
|
+
* CLI surface picks this up via `getGlobalSymbolCache()` so every
|
|
147
|
+
* subagent / tool invocation in the same process shares the same cache.
|
|
148
|
+
* Subagents spawned in a fresh child process get a fresh cache (by
|
|
149
|
+
* design — the cache is process-scoped, not user-scoped).
|
|
150
|
+
*/
|
|
151
|
+
let globalCache;
|
|
152
|
+
export function getGlobalSymbolCache(options) {
|
|
153
|
+
if (!globalCache) {
|
|
154
|
+
globalCache = new SymbolCache(options);
|
|
155
|
+
}
|
|
156
|
+
return globalCache;
|
|
157
|
+
}
|
|
158
|
+
/** Test-only: drop the singleton so a spec starts with a fresh cache. */
|
|
159
|
+
export function __resetGlobalSymbolCache() {
|
|
160
|
+
globalCache = undefined;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=symbol-cache.js.map
|