@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88
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/THIRD_PARTY_NOTICES.md +40 -0
- 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 +12 -12
- 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 +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -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 +322 -0
- package/dist/core/engine/anvil-client.js +151 -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 +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -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 +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -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/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 +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -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 +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- 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 +298 -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 +457 -0
- 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-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 +4203 -493
- package/dist/runtime/commands/agents.js +30 -30
- 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 +244 -13
- 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 +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- 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 +128 -0
- 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 +177 -0
- 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 +531 -0
- 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/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 +556 -0
- package/dist/tools/ask-user-question.js +288 -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/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 +189 -0
- 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 +85 -0
- 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 +257 -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/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- 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 +124 -44
- 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/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +25 -7
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
package/dist/tools/file-tools.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-tools - Pugi CLI file/bash/glob/grep tool surface.
|
|
3
|
+
*
|
|
4
|
+
* Workspace-binding contract (CEO red-alert follow-up):
|
|
5
|
+
*
|
|
6
|
+
* Every tool dispatch path threads `ctx.root` from the operator's
|
|
7
|
+
* `process.cwd()` through `EngineTask.workspaceRoot` ->
|
|
8
|
+
* `native-pugi.run()` -> `toolCtx.root` -> here. Tools call
|
|
9
|
+
* `resolveWorkspacePath(ctx.root, path)` for every on-disk operation
|
|
10
|
+
* so a dispatched specialist (e.g. Hiroshi writing tic-tac-toe HTML)
|
|
11
|
+
* produces files in the OPERATOR'S cwd, never in a server-side temp
|
|
12
|
+
* space. The path-security gate refuses traversal (`../etc/passwd`,
|
|
13
|
+
* URL-encoded variants, symlink escapes at the target).
|
|
14
|
+
*
|
|
15
|
+
* Wiring chain:
|
|
16
|
+
* 1. runtime/cli.ts: workspaceRoot = process.cwd()
|
|
17
|
+
* 2. EngineTask.workspaceRoot threads through to native-pugi.run().
|
|
18
|
+
* 3. native-pugi: const root = task.workspaceRoot
|
|
19
|
+
* 4. tool-bridge: passes ctx.root to file-tools / bash.
|
|
20
|
+
* 5. file-tools: resolveWorkspacePath(ctx.root, path).
|
|
21
|
+
*
|
|
22
|
+
* The contract is locked by `test/tools-write-to-workspace.spec.ts`
|
|
23
|
+
* (6 cases covering relative + nested + absolute paths + traversal
|
|
24
|
+
* refusal). If any layer of the chain regressed silently, dispatched
|
|
25
|
+
* files would land in `/tmp` instead of the operator's repo, which
|
|
26
|
+
* is the same failure surface as the menu-mode anti-pattern the
|
|
27
|
+
* sibling commits close.
|
|
28
|
+
*/
|
|
1
29
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
31
|
import { dirname, isAbsolute, relative } from 'node:path';
|
|
4
32
|
import { globSync } from 'node:fs';
|
|
5
33
|
import { decidePermission } from '../core/permission.js';
|
|
6
|
-
import { createReadRecord, hashContent } from '../core/file-cache.js';
|
|
34
|
+
import { StaleReadError, createReadRecord, hashContent, } from '../core/file-cache.js';
|
|
7
35
|
import { resolveWorkspacePath } from '../core/path-security.js';
|
|
36
|
+
import { scanForInjection, summarizeFindings } from '../core/security/injection-scanner.js';
|
|
8
37
|
import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
|
|
9
38
|
/**
|
|
10
|
-
*
|
|
39
|
+
* WriteGate marker — thrown by `gateOnCancellation` when the
|
|
11
40
|
* caller supplied a cancellation token that has already aborted. The
|
|
12
41
|
* tool dispatch loop in `tool-bridge.ts` recognises the name and folds
|
|
13
42
|
* the throw into a `status: 'aborted'` tool result rather than a hard
|
|
@@ -19,8 +48,13 @@ export class OperatorAbortedError extends Error {
|
|
|
19
48
|
this.name = 'OperatorAbortedError';
|
|
20
49
|
}
|
|
21
50
|
}
|
|
51
|
+
// Re-export StaleReadError so tool-bridge / test consumers can import
|
|
52
|
+
// the typed error from a single file-tools surface alongside
|
|
53
|
+
// OperatorAbortedError. Same shape as the existing OperatorAbortedError
|
|
54
|
+
// re-surface pattern.
|
|
55
|
+
export { StaleReadError } from '../core/file-cache.js';
|
|
22
56
|
/**
|
|
23
|
-
*
|
|
57
|
+
* WriteGate: refuse the tool dispatch when the active
|
|
24
58
|
* cancellation token has aborted. Idempotent (the token's `isAborted`
|
|
25
59
|
* is a getter, no side effects). Returns void on the happy path so the
|
|
26
60
|
* tool can proceed; throws `OperatorAbortedError` when cancelled.
|
|
@@ -71,7 +105,7 @@ function permissionGatedResolve(ctx, inputPath, action, toolName) {
|
|
|
71
105
|
}
|
|
72
106
|
export function readTool(ctx, path) {
|
|
73
107
|
const toolCallId = recordToolCall(ctx.session, 'read', path);
|
|
74
|
-
//
|
|
108
|
+
// WriteGate: fail fast on operator cancel BEFORE permission
|
|
75
109
|
// decision so a half-second post-cancel race never lands the read.
|
|
76
110
|
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
77
111
|
const reason = 'operator_aborted: read refused';
|
|
@@ -100,7 +134,7 @@ export function readTool(ctx, path) {
|
|
|
100
134
|
}
|
|
101
135
|
export function writeTool(ctx, path, content) {
|
|
102
136
|
const toolCallId = recordToolCall(ctx.session, 'write', path);
|
|
103
|
-
//
|
|
137
|
+
// WriteGate: refuse the write when the operator has cancelled
|
|
104
138
|
// the dispatch. The audit log captures the cancellation reason so a
|
|
105
139
|
// post-mortem can distinguish operator_aborted from settings-deny.
|
|
106
140
|
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
@@ -124,10 +158,45 @@ export function writeTool(ctx, path, content) {
|
|
|
124
158
|
throw error;
|
|
125
159
|
}
|
|
126
160
|
const existed = existsSync(resolved);
|
|
127
|
-
|
|
161
|
+
// stale-read gate for writeTool's update-existing path. The
|
|
162
|
+
// model uses writeTool for two distinct intents:
|
|
163
|
+
//
|
|
164
|
+
// - create-new: path does not exist on disk. There is no prior
|
|
165
|
+
// read to validate against; skip the gate. This is the
|
|
166
|
+
// intentional escape hatch the leak spec also calls out.
|
|
167
|
+
// - overwrite-existing: path exists. Without the gate the model
|
|
168
|
+
// could blind-clobber an externally-modified file, losing the
|
|
169
|
+
// concurrent change silently. Force the model to re-read first.
|
|
170
|
+
//
|
|
171
|
+
// We deliberately apply the SAME stale-validation primitive editTool
|
|
172
|
+
// uses so the two write surfaces stay symmetric and a future fix to
|
|
173
|
+
// either one cannot accidentally weaken the other.
|
|
174
|
+
let before;
|
|
175
|
+
if (existed) {
|
|
176
|
+
before = readFileSync(resolved, 'utf8');
|
|
177
|
+
const currentStat = statSync(resolved);
|
|
178
|
+
const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
|
|
179
|
+
if (validation.stale) {
|
|
180
|
+
const reason = `stale_read: write ${path} refused — ${validation.detail}`;
|
|
181
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
182
|
+
throw new StaleReadError(path, validation.reason, validation.detail);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
128
185
|
const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
|
|
129
186
|
writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
|
|
130
187
|
renameSync(tmp, resolved);
|
|
188
|
+
// Injection scan (ported an external utility,
|
|
189
|
+
// Apache-2.0). Scan the BODY (never the path — path security is
|
|
190
|
+
// owned by `path-security.ts`). Findings are SURFACED as an extra
|
|
191
|
+
// line on the session tool-result, never block the write. Hard-
|
|
192
|
+
// block requires a separate CEO-signed PR. Failure here must NOT
|
|
193
|
+
// throw: a buggy scanner cannot rugpull the write that already
|
|
194
|
+
// landed on disk above.
|
|
195
|
+
surfaceInjectionWarning(ctx, toolCallId, 'write', path, content);
|
|
196
|
+
// Refresh the cache with the post-write content so the model can
|
|
197
|
+
// chain a follow-up read+edit on the same file without an extra
|
|
198
|
+
// round-trip. Same pattern editTool uses below.
|
|
199
|
+
ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
|
|
131
200
|
recordFileMutation(ctx.session, {
|
|
132
201
|
toolCallId,
|
|
133
202
|
path,
|
|
@@ -137,9 +206,39 @@ export function writeTool(ctx, path, content) {
|
|
|
137
206
|
});
|
|
138
207
|
recordToolResult(ctx.session, toolCallId, 'success', `${existed ? 'Updated' : 'Created'} ${path}`);
|
|
139
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Surface an injection-scan warning on a file write/edit BODY. The
|
|
211
|
+
* scan never blocks — it folds findings into the session as a
|
|
212
|
+
* `tool_result` with status `warn` so an operator (or SOC pipeline
|
|
213
|
+
* tailing `<workspace>/.pugi/events.jsonl`) sees the signal without a
|
|
214
|
+
* mid-dispatch rollback.
|
|
215
|
+
*
|
|
216
|
+
* Wrapped in try/catch so a malformed scanner never crashes the tool
|
|
217
|
+
* loop — the write itself has already landed on disk by the time we
|
|
218
|
+
* call this.
|
|
219
|
+
*/
|
|
220
|
+
function surfaceInjectionWarning(ctx, triggeringToolCallId, tool, path, body) {
|
|
221
|
+
try {
|
|
222
|
+
const findings = scanForInjection(body);
|
|
223
|
+
if (findings.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
const summary = summarizeFindings(findings);
|
|
226
|
+
const warnCallId = recordToolCall(ctx.session, 'injection_warning', path);
|
|
227
|
+
const message = `injection_warning: ${tool} ${path} — ${summary.total} pattern(s) ` +
|
|
228
|
+
`(score=${summary.score}, kinds=${summary.kinds.join('|')}). ` +
|
|
229
|
+
`Triggering call: ${triggeringToolCallId}. ` +
|
|
230
|
+
`Detector: external-injection-patterns. Write was NOT blocked.`;
|
|
231
|
+
recordToolResult(ctx.session, warnCallId, 'success', message);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Scanner failure must NEVER throw — the write has already
|
|
235
|
+
// landed and the tool loop has to continue. Silent no-op
|
|
236
|
+
// mirrors the audit-trail contract.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
140
239
|
export function editTool(ctx, path, oldString, newString) {
|
|
141
240
|
const toolCallId = recordToolCall(ctx.session, 'edit', path);
|
|
142
|
-
//
|
|
241
|
+
// WriteGate: refuse the edit when the operator has cancelled
|
|
143
242
|
// the dispatch. Edits are higher-risk than reads — surface the abort
|
|
144
243
|
// BEFORE we even consult permissions so a cancel-during-tool-loop
|
|
145
244
|
// never partially mutates the workspace.
|
|
@@ -154,10 +253,6 @@ export function editTool(ctx, path, oldString, newString) {
|
|
|
154
253
|
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
155
254
|
throw new Error(reason);
|
|
156
255
|
}
|
|
157
|
-
const readRecord = ctx.readCache.get(ctx.root, path);
|
|
158
|
-
if (!readRecord) {
|
|
159
|
-
throw new Error(`Cannot edit ${path}: file must be read first`);
|
|
160
|
-
}
|
|
161
256
|
let resolved;
|
|
162
257
|
try {
|
|
163
258
|
resolved = permissionGatedResolve(ctx, path, 'edit', 'edit');
|
|
@@ -167,20 +262,42 @@ export function editTool(ctx, path, oldString, newString) {
|
|
|
167
262
|
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
168
263
|
throw error;
|
|
169
264
|
}
|
|
265
|
+
// stale-read gate. Validate the model's read-time view of
|
|
266
|
+
// the file against the on-disk state BEFORE applying the mutation.
|
|
267
|
+
// We read disk content once and feed it to the validator so a single
|
|
268
|
+
// syscall covers both the gate decision AND the oldString/newString
|
|
269
|
+
// replacement below.
|
|
170
270
|
const before = readFileSync(resolved, 'utf8');
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
271
|
+
const currentStat = statSync(resolved);
|
|
272
|
+
const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
|
|
273
|
+
if (validation.stale) {
|
|
274
|
+
const reason = `stale_read: edit ${path} refused — ${validation.detail}`;
|
|
275
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
276
|
+
throw new StaleReadError(path, validation.reason, validation.detail);
|
|
174
277
|
}
|
|
278
|
+
const currentHash = hashContent(before);
|
|
175
279
|
const matches = before.split(oldString).length - 1;
|
|
176
|
-
if (matches === 0)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
throw new Error(
|
|
280
|
+
if (matches === 0) {
|
|
281
|
+
const reason = `Cannot edit ${path}: oldString not found`;
|
|
282
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
283
|
+
throw new Error(reason);
|
|
284
|
+
}
|
|
285
|
+
if (matches > 1) {
|
|
286
|
+
const reason = `Cannot edit ${path}: oldString is not unique`;
|
|
287
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
288
|
+
throw new Error(reason);
|
|
289
|
+
}
|
|
180
290
|
const after = before.replace(oldString, newString);
|
|
181
291
|
const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
|
|
182
292
|
writeFileSync(tmp, after, { encoding: 'utf8', mode: 0o600 });
|
|
183
293
|
renameSync(tmp, resolved);
|
|
294
|
+
// Injection scan (ported an external utility,
|
|
295
|
+
// Apache-2.0). We scan the NEW SUBSTRING the model is inserting,
|
|
296
|
+
// not the full post-edit file — the rest of the file is operator-
|
|
297
|
+
// owned content that pre-dates this dispatch. False-positive on
|
|
298
|
+
// legitimate prose that mentions banned phrases is the worst
|
|
299
|
+
// outcome and the warn-only contract bounds the cost.
|
|
300
|
+
surfaceInjectionWarning(ctx, toolCallId, 'edit', path, newString);
|
|
184
301
|
ctx.readCache.set(createReadRecord(ctx.root, path, after, 'read_tool'));
|
|
185
302
|
recordFileMutation(ctx.session, {
|
|
186
303
|
toolCallId,
|
|
@@ -193,7 +310,7 @@ export function editTool(ctx, path, oldString, newString) {
|
|
|
193
310
|
}
|
|
194
311
|
export function globTool(ctx, pattern) {
|
|
195
312
|
const toolCallId = recordToolCall(ctx.session, 'glob', pattern);
|
|
196
|
-
//
|
|
313
|
+
// WriteGate: cancel-aware short-circuit. Glob is read-only but
|
|
197
314
|
// can be expensive on large trees; respecting the abort here keeps
|
|
198
315
|
// the tool loop responsive when the operator hits Ctrl+C mid-scan.
|
|
199
316
|
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
@@ -203,11 +320,11 @@ export function globTool(ctx, pattern) {
|
|
|
203
320
|
}
|
|
204
321
|
// Pugi globs are workspace-scoped. Reject any pattern that could enumerate
|
|
205
322
|
// outside the workspace:
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
323
|
+
// 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
|
|
324
|
+
// regardless of `cwd`, so they fan out outside the repo.
|
|
325
|
+
// 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
|
|
326
|
+
// A substring check would over-reject legitimate names like
|
|
327
|
+
// `src/v1..v2/*` so we split on `/` instead.
|
|
211
328
|
if (isAbsolute(pattern)) {
|
|
212
329
|
const reason = `Absolute glob patterns are not allowed: ${pattern}`;
|
|
213
330
|
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
@@ -230,7 +347,7 @@ export function globTool(ctx, pattern) {
|
|
|
230
347
|
}
|
|
231
348
|
export function grepTool(ctx, query) {
|
|
232
349
|
const toolCallId = recordToolCall(ctx.session, 'grep', query);
|
|
233
|
-
//
|
|
350
|
+
// WriteGate: refuse before scanning. Grep walks the whole
|
|
234
351
|
// workspace and can take seconds on a large repo; check abort first
|
|
235
352
|
// so a cancel mid-scan returns immediately rather than after the
|
|
236
353
|
// full walk completes.
|
|
@@ -244,7 +361,7 @@ export function grepTool(ctx, query) {
|
|
|
244
361
|
for (const path of files) {
|
|
245
362
|
if (matches.length >= 200)
|
|
246
363
|
break;
|
|
247
|
-
//
|
|
364
|
+
// WriteGate: poll abort inside the file loop so a cancel
|
|
248
365
|
// arriving mid-scan terminates early. The per-file branch keeps
|
|
249
366
|
// the responsiveness bounded by the slowest single-file read.
|
|
250
367
|
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
@@ -289,18 +406,18 @@ export function grepTool(ctx, query) {
|
|
|
289
406
|
}
|
|
290
407
|
/**
|
|
291
408
|
* Workspace-scoped bash tool. Sized for the M1 engine adapter:
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
409
|
+
* - Runs through `/bin/sh -c <command>` so the model can use pipes,
|
|
410
|
+
* redirection, and shell builtins (`ls | wc -l`, `git status`).
|
|
411
|
+
* - `cwd` is pinned to the workspace root so a stray `cd /` cannot
|
|
412
|
+
* leak commands outside the repo (the child process inherits root
|
|
413
|
+
* filesystem visibility — destructive patterns are blocked by
|
|
414
|
+
* `decidePermission`, not by chroot).
|
|
415
|
+
* - Output capped at 64KB combined stdout/stderr to keep the
|
|
416
|
+
* transcript bounded; the model gets the head + a `(...truncated)`
|
|
417
|
+
* marker if the cap fires.
|
|
418
|
+
* - 30s wall-clock timeout. The engine loop's per-tool error path
|
|
419
|
+
* surfaces the timeout to the model so it can retry with a narrower
|
|
420
|
+
* command or give up.
|
|
304
421
|
*
|
|
305
422
|
* Permission gating: `kind: 'bash'`. The CLI's permission module already
|
|
306
423
|
* hard-denies the destructive-patterns list (rm -rf /, DROP DATABASE,
|
|
@@ -311,7 +428,7 @@ export const BASH_OUTPUT_CAP = 64 * 1024;
|
|
|
311
428
|
export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
|
|
312
429
|
// Child-process stdio buffer — large enough that the model-facing
|
|
313
430
|
// truncation cap (`BASH_OUTPUT_CAP`) is always the gate, never the
|
|
314
|
-
// child's internal buffer. Code Reviewer P2 retro
|
|
431
|
+
// child's internal buffer. Code Reviewer P2 retro flagged
|
|
315
432
|
// `BASH_OUTPUT_CAP * 2` as too tight: real builds (`pnpm build`,
|
|
316
433
|
// `tsc --noEmit`) routinely exceed 128 KB combined and the model
|
|
317
434
|
// then saw a fatal `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` instead of a
|
|
@@ -319,7 +436,7 @@ export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
319
436
|
export const BASH_CHILD_MAXBUFFER = 10 * 1024 * 1024;
|
|
320
437
|
export function bashTool(ctx, command, options = {}) {
|
|
321
438
|
const toolCallId = recordToolCall(ctx.session, 'bash', command);
|
|
322
|
-
//
|
|
439
|
+
// WriteGate: bash is the highest-risk tool surface. Refuse
|
|
323
440
|
// before the destructive-pattern classifier even runs so a
|
|
324
441
|
// cancelled dispatch never spawns a child process. Note: this is
|
|
325
442
|
// pre-spawn cancellation only; once the /bin/sh -c process is
|
|
@@ -343,7 +460,7 @@ export function bashTool(ctx, command, options = {}) {
|
|
|
343
460
|
//
|
|
344
461
|
// Env sanitisation strategy: build the child env from an explicit
|
|
345
462
|
// allow-list rather than inheriting `process.env` and trying to
|
|
346
|
-
// strip secrets after the fact. Code Reviewer P1
|
|
463
|
+
// strip secrets after the fact. Code Reviewer P1 flagged
|
|
347
464
|
// that the deny-list approach missed ANTHROPIC_API_KEY / GH_TOKEN
|
|
348
465
|
// / AWS_SECRET_ACCESS_KEY / DATABASE_URL / arbitrary *_TOKEN /
|
|
349
466
|
// *_SECRET / *_KEY variables — every CI agent rotation would risk
|
|
@@ -372,7 +489,7 @@ export function bashTool(ctx, command, options = {}) {
|
|
|
372
489
|
}
|
|
373
490
|
const timeoutMs = options.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
374
491
|
// `spawnSync` (vs `execFileSync`) captures stdout AND stderr on
|
|
375
|
-
// BOTH success and failure paths. Code Reviewer P1
|
|
492
|
+
// BOTH success and failure paths. Code Reviewer P1:
|
|
376
493
|
// `execFileSync` returns only stdout on exit 0, silently dropping
|
|
377
494
|
// stderr output from `tsc`, `eslint`, `pytest`, etc. — the model
|
|
378
495
|
// would see `(no output)` for successful runs that emitted real
|
|
@@ -381,7 +498,7 @@ export function bashTool(ctx, command, options = {}) {
|
|
|
381
498
|
// maxBuffer is generous (10 MB) so the child process is never the
|
|
382
499
|
// truncation gate — the post-hoc `.slice(0, BASH_OUTPUT_CAP)` below
|
|
383
500
|
// is the single source of truth for what the model sees. Code
|
|
384
|
-
// Reviewer P2 retro
|
|
501
|
+
// Reviewer P2 retro: the previous `BASH_OUTPUT_CAP * 2`
|
|
385
502
|
// (128 KB) would hard-throw `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
|
|
386
503
|
// on noisy commands (`pnpm build`, `tsc --noEmit` on the whole
|
|
387
504
|
// monorepo) instead of returning the truncated head.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
|
|
2
|
+
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
3
|
+
/** Cap for any single LSP tool's payload size. Keeps model context lean. */
|
|
4
|
+
const LSP_PAYLOAD_CAP_BYTES = 8 * 1024;
|
|
5
|
+
export async function lspHover(ctx, lang, file, line, col) {
|
|
6
|
+
const toolCallId = recordToolCall(ctx.session, 'lsp_hover', `${lang}:${file}:${line}:${col}`);
|
|
7
|
+
return guard(ctx, 'lsp_hover', toolCallId, async () => {
|
|
8
|
+
const client = ctx.lspClients?.get(lang);
|
|
9
|
+
if (!client)
|
|
10
|
+
return unavailable(lang);
|
|
11
|
+
const result = await client.hover(file, { line, character: col }, ctx.cancellation);
|
|
12
|
+
if (!result.ok)
|
|
13
|
+
return failure(result);
|
|
14
|
+
if (!result.value) {
|
|
15
|
+
return { ok: true, value: { content: '' } };
|
|
16
|
+
}
|
|
17
|
+
const content = truncate(result.value.content);
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value: {
|
|
21
|
+
content: content.text,
|
|
22
|
+
...(result.value.range ? { range: result.value.range } : {}),
|
|
23
|
+
},
|
|
24
|
+
...(content.truncated ? { truncated: true } : {}),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function lspDefinition(ctx, lang, file, line, col) {
|
|
29
|
+
const toolCallId = recordToolCall(ctx.session, 'lsp_definition', `${lang}:${file}:${line}:${col}`);
|
|
30
|
+
return guard(ctx, 'lsp_definition', toolCallId, async () => {
|
|
31
|
+
const client = ctx.lspClients?.get(lang);
|
|
32
|
+
if (!client)
|
|
33
|
+
return unavailable(lang);
|
|
34
|
+
const result = await client.definition(file, { line, character: col }, ctx.cancellation);
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
return failure(result);
|
|
37
|
+
const capped = capLocations(result.value);
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
value: capped.value,
|
|
41
|
+
...(capped.truncated ? { truncated: true } : {}),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function lspReferences(ctx, lang, file, line, col) {
|
|
46
|
+
const toolCallId = recordToolCall(ctx.session, 'lsp_references', `${lang}:${file}:${line}:${col}`);
|
|
47
|
+
return guard(ctx, 'lsp_references', toolCallId, async () => {
|
|
48
|
+
const client = ctx.lspClients?.get(lang);
|
|
49
|
+
if (!client)
|
|
50
|
+
return unavailable(lang);
|
|
51
|
+
const result = await client.references(file, { line, character: col }, ctx.cancellation);
|
|
52
|
+
if (!result.ok)
|
|
53
|
+
return failure(result);
|
|
54
|
+
const capped = capLocations(result.value);
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: capped.value,
|
|
58
|
+
...(capped.truncated ? { truncated: true } : {}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export async function lspDiagnostics(ctx, lang, file) {
|
|
63
|
+
const toolCallId = recordToolCall(ctx.session, 'lsp_diagnostics', `${lang}:${file}`);
|
|
64
|
+
return guard(ctx, 'lsp_diagnostics', toolCallId, async () => {
|
|
65
|
+
const client = ctx.lspClients?.get(lang);
|
|
66
|
+
if (!client)
|
|
67
|
+
return unavailable(lang);
|
|
68
|
+
const result = await client.diagnostics(file, ctx.cancellation);
|
|
69
|
+
if (!result.ok)
|
|
70
|
+
return failure(result);
|
|
71
|
+
const capped = capDiagnostics(result.value);
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
value: capped.value,
|
|
75
|
+
...(capped.truncated ? { truncated: true } : {}),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function guard(ctx, toolName, toolCallId, op) {
|
|
80
|
+
try {
|
|
81
|
+
gateOnCancellation(ctx, toolName);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof OperatorAbortedError) {
|
|
85
|
+
recordToolResult(ctx.session, toolCallId, 'cancelled', error.message);
|
|
86
|
+
return { ok: false, reason: 'operator_aborted', detail: error.message };
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const result = await op();
|
|
92
|
+
if (result.ok) {
|
|
93
|
+
recordToolResult(ctx.session, toolCallId, 'success', summarize(result.value));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
recordToolResult(ctx.session, toolCallId, 'error', `${result.reason ?? 'error'}: ${result.detail ?? ''}`);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
recordToolResult(ctx.session, toolCallId, 'error', message);
|
|
103
|
+
return { ok: false, reason: 'lsp_error', detail: message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function unavailable(lang) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: 'lsp_unavailable',
|
|
110
|
+
detail: `no LSP server started for ${lang}. Install the server and re-run ` +
|
|
111
|
+
`with --lsp ${lang}, or fall back to grep.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function failure(result) {
|
|
115
|
+
if (result.ok) {
|
|
116
|
+
// Shouldn't be hit — caller checks first.
|
|
117
|
+
return { ok: true, value: result.value };
|
|
118
|
+
}
|
|
119
|
+
return { ok: false, reason: result.reason, detail: result.detail };
|
|
120
|
+
}
|
|
121
|
+
function summarize(value) {
|
|
122
|
+
if (value === null || value === undefined)
|
|
123
|
+
return 'no result';
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return `${value.length} items`;
|
|
126
|
+
if (typeof value === 'object')
|
|
127
|
+
return Object.keys(value).join(',');
|
|
128
|
+
return String(value);
|
|
129
|
+
}
|
|
130
|
+
function truncate(text) {
|
|
131
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
132
|
+
if (bytes <= LSP_PAYLOAD_CAP_BYTES)
|
|
133
|
+
return { text, truncated: false };
|
|
134
|
+
// Truncate to the cap byte boundary. We don't try to honor codepoint
|
|
135
|
+
// alignment — UTF-8 surrogate splits show up as a single ? at the
|
|
136
|
+
// boundary, which is acceptable for a debug surface; the dispatcher
|
|
137
|
+
// is the trust boundary for "this is what the model will see".
|
|
138
|
+
const buf = Buffer.from(text, 'utf8').subarray(0, LSP_PAYLOAD_CAP_BYTES);
|
|
139
|
+
return { text: `${buf.toString('utf8')}\n... [truncated]`, truncated: true };
|
|
140
|
+
}
|
|
141
|
+
function capLocations(locations) {
|
|
142
|
+
// Cap at 200 locations OR the byte cap, whichever hits first. The
|
|
143
|
+
// 200 number is the operator-facing "this is a hot symbol" threshold —
|
|
144
|
+
// a richer surface (paginated `pugi lsp references --offset N`) is
|
|
145
|
+
// open backlog.
|
|
146
|
+
const COUNT_CAP = 200;
|
|
147
|
+
if (locations.length === 0)
|
|
148
|
+
return { value: locations, truncated: false };
|
|
149
|
+
const trimmed = locations.slice(0, COUNT_CAP);
|
|
150
|
+
const serialized = JSON.stringify(trimmed);
|
|
151
|
+
if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES && trimmed.length === locations.length) {
|
|
152
|
+
return { value: trimmed, truncated: false };
|
|
153
|
+
}
|
|
154
|
+
// Trim by halves until we fit the byte cap. Worst case ~10 iterations
|
|
155
|
+
// for the 200 max, fine for an interactive tool.
|
|
156
|
+
let upper = trimmed.length;
|
|
157
|
+
while (upper > 1) {
|
|
158
|
+
const half = Math.floor(upper / 2);
|
|
159
|
+
const sub = trimmed.slice(0, half);
|
|
160
|
+
if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
161
|
+
return { value: sub, truncated: true };
|
|
162
|
+
}
|
|
163
|
+
upper = half;
|
|
164
|
+
}
|
|
165
|
+
return { value: trimmed.slice(0, 1), truncated: true };
|
|
166
|
+
}
|
|
167
|
+
function capDiagnostics(items) {
|
|
168
|
+
if (items.length === 0)
|
|
169
|
+
return { value: items, truncated: false };
|
|
170
|
+
const serialized = JSON.stringify(items);
|
|
171
|
+
if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
172
|
+
return { value: items, truncated: false };
|
|
173
|
+
}
|
|
174
|
+
// Diagnostics are sorted error-first in LSP convention; trim from the
|
|
175
|
+
// tail so we keep the highest-severity items.
|
|
176
|
+
let upper = items.length;
|
|
177
|
+
while (upper > 1) {
|
|
178
|
+
const half = Math.floor(upper / 2);
|
|
179
|
+
const sub = items.slice(0, half);
|
|
180
|
+
if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
181
|
+
return { value: sub, truncated: true };
|
|
182
|
+
}
|
|
183
|
+
upper = half;
|
|
184
|
+
}
|
|
185
|
+
return { value: items.slice(0, 1), truncated: true };
|
|
186
|
+
}
|
|
187
|
+
/** Test-only surface so specs can poke truncation directly. */
|
|
188
|
+
export const __test__ = { truncate, capLocations, capDiagnostics, LSP_PAYLOAD_CAP_BYTES };
|
|
189
|
+
//# sourceMappingURL=lsp-tools.js.map
|