@pugi/cli 0.1.0-beta.9 → 0.1.0-beta.90
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 +1731 -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/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 +86 -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/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
package/dist/tools/lsp-tools.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { callHierarchyAt, codeActionsAt, diagnosticsFor, findDefinition as symbolsFindDefinition, findImplementations, findReferences as symbolsFindReferences, findTypeDefinition, findWorkspaceSymbol, formatFile, hoverSymbol, listDocumentSymbols, renameSymbol, signatureAt, transportFromLspClient, } from '../core/lsp/symbol-tools.js';
|
|
1
2
|
import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
|
|
2
3
|
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
4
|
+
import { getGlobalSymbolCache, SymbolCache } from '../core/lsp/symbol-cache.js';
|
|
3
5
|
/** Cap for any single LSP tool's payload size. Keeps model context lean. */
|
|
4
6
|
const LSP_PAYLOAD_CAP_BYTES = 8 * 1024;
|
|
5
7
|
export async function lspHover(ctx, lang, file, line, col) {
|
|
@@ -184,6 +186,380 @@ function capDiagnostics(items) {
|
|
|
184
186
|
}
|
|
185
187
|
return { value: items.slice(0, 1), truncated: true };
|
|
186
188
|
}
|
|
189
|
+
/* ------------------------------------------------------------------------- */
|
|
190
|
+
/* PUGI-78 Phase 1: symbols.* namespace tools (13 categories). */
|
|
191
|
+
/* ------------------------------------------------------------------------- */
|
|
192
|
+
/**
|
|
193
|
+
* PUGI-78 Phase 1: symbols.find_definition.
|
|
194
|
+
*
|
|
195
|
+
* Pugi reads a whole file (~5-50 KB tokens) every time the model wants
|
|
196
|
+
* to know "where is foo defined?". The LSP wire here returns ~200 bytes
|
|
197
|
+
* (file + line + character). Two orders of magnitude reduction in token
|
|
198
|
+
* cost per refactor turn, hence the BIG-leverage classification in the
|
|
199
|
+
* spec (10-100x token savings per refactor).
|
|
200
|
+
*
|
|
201
|
+
* Failure folds to `lsp_unavailable` when no client is registered for
|
|
202
|
+
* the inferred language. The agent surface is expected to fall back to
|
|
203
|
+
* grep when this fires (the tool-preference prompt rule steers it).
|
|
204
|
+
*/
|
|
205
|
+
export async function symbolsFindDefinitionTool(ctx, lang, file, line, col) {
|
|
206
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_definition', `${lang}:${file}:${line}:${col}`);
|
|
207
|
+
return guard(ctx, 'symbols_find_definition', toolCallId, async () => {
|
|
208
|
+
const client = ctx.lspClients?.get(lang);
|
|
209
|
+
if (!client)
|
|
210
|
+
return unavailable(lang);
|
|
211
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
212
|
+
const cwd = workspaceForCache(ctx);
|
|
213
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_definition', { file, line, col });
|
|
214
|
+
const cached = cache?.get(key);
|
|
215
|
+
if (cached !== undefined) {
|
|
216
|
+
if (cached === null)
|
|
217
|
+
return notFound('definition');
|
|
218
|
+
return { ok: true, value: cached };
|
|
219
|
+
}
|
|
220
|
+
const result = await symbolsFindDefinition(transportFromLspClient(client), file, line, col);
|
|
221
|
+
cache?.set(key, result);
|
|
222
|
+
if (!result)
|
|
223
|
+
return notFound('definition');
|
|
224
|
+
return { ok: true, value: result };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* PUGI-78 Phase 1: symbols.find_references.
|
|
229
|
+
*
|
|
230
|
+
* Returns the flat list of call sites for the symbol at (line, col).
|
|
231
|
+
* The 200-row cap from the legacy `lspReferences` carries over via
|
|
232
|
+
* `capSymbolReferences`.
|
|
233
|
+
*/
|
|
234
|
+
export async function symbolsFindReferencesTool(ctx, lang, file, line, col) {
|
|
235
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_references', `${lang}:${file}:${line}:${col}`);
|
|
236
|
+
return guard(ctx, 'symbols_find_references', toolCallId, async () => {
|
|
237
|
+
const client = ctx.lspClients?.get(lang);
|
|
238
|
+
if (!client)
|
|
239
|
+
return unavailable(lang);
|
|
240
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
241
|
+
const cwd = workspaceForCache(ctx);
|
|
242
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_references', { file, line, col });
|
|
243
|
+
const cached = cache?.get(key);
|
|
244
|
+
if (cached !== undefined) {
|
|
245
|
+
const capped = capSymbolReferences(cached);
|
|
246
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
247
|
+
}
|
|
248
|
+
const result = await symbolsFindReferences(transportFromLspClient(client), file, line, col);
|
|
249
|
+
cache?.set(key, result);
|
|
250
|
+
const capped = capSymbolReferences(result);
|
|
251
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* PUGI-78 Phase 1: symbols.list_in_file.
|
|
256
|
+
*
|
|
257
|
+
* Outline view — the document-symbol surface. Returns the flat list of
|
|
258
|
+
* top-level + nested symbols with their kind. Cache HIT on the same
|
|
259
|
+
* file across consecutive turns is the common case (the model usually
|
|
260
|
+
* lists, then dives into a single symbol).
|
|
261
|
+
*/
|
|
262
|
+
export async function symbolsListInFileTool(ctx, lang, file) {
|
|
263
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_list_in_file', `${lang}:${file}`);
|
|
264
|
+
return guard(ctx, 'symbols_list_in_file', toolCallId, async () => {
|
|
265
|
+
const client = ctx.lspClients?.get(lang);
|
|
266
|
+
if (!client)
|
|
267
|
+
return unavailable(lang);
|
|
268
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
269
|
+
const cwd = workspaceForCache(ctx);
|
|
270
|
+
const key = SymbolCache.makeKey(lang, cwd, 'list_in_file', { file });
|
|
271
|
+
const cached = cache?.get(key);
|
|
272
|
+
if (cached !== undefined) {
|
|
273
|
+
return { ok: true, value: [...cached] };
|
|
274
|
+
}
|
|
275
|
+
const result = await listDocumentSymbols(transportFromLspClient(client), file);
|
|
276
|
+
cache?.set(key, result);
|
|
277
|
+
return { ok: true, value: result };
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* PUGI-78 Phase 1: symbols.rename.
|
|
282
|
+
*
|
|
283
|
+
* Returns the workspace-edit preview the server proposes. The
|
|
284
|
+
* dispatcher applies via `apply_patch` in a future ticket; Phase 1 is
|
|
285
|
+
* read-only — the agent surface gets the affected file list + per-edit
|
|
286
|
+
* line/character so the model can summarize the change.
|
|
287
|
+
*/
|
|
288
|
+
export async function symbolsRenameTool(ctx, lang, file, line, col, newName) {
|
|
289
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_rename', `${lang}:${file}:${line}:${col}:${newName}`);
|
|
290
|
+
return guard(ctx, 'symbols_rename', toolCallId, async () => {
|
|
291
|
+
const client = ctx.lspClients?.get(lang);
|
|
292
|
+
if (!client)
|
|
293
|
+
return unavailable(lang);
|
|
294
|
+
// No cache for rename — the edit set depends on the live source
|
|
295
|
+
// file state. A subsequent rename of the same symbol after the
|
|
296
|
+
// first applied would otherwise return stale edits.
|
|
297
|
+
const result = await renameSymbol(transportFromLspClient(client), file, line, col, newName);
|
|
298
|
+
if (!result)
|
|
299
|
+
return notFound('rename');
|
|
300
|
+
return { ok: true, value: result };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* PUGI-78 Phase 1: symbols.hover.
|
|
305
|
+
*
|
|
306
|
+
* Returns the hover content (type info + docstring) at the position.
|
|
307
|
+
* Body capped at 4 KB so a verbose generic does not blow context.
|
|
308
|
+
*/
|
|
309
|
+
export async function symbolsHoverTool(ctx, lang, file, line, col) {
|
|
310
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_hover', `${lang}:${file}:${line}:${col}`);
|
|
311
|
+
return guard(ctx, 'symbols_hover', toolCallId, async () => {
|
|
312
|
+
const client = ctx.lspClients?.get(lang);
|
|
313
|
+
if (!client)
|
|
314
|
+
return unavailable(lang);
|
|
315
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
316
|
+
const cwd = workspaceForCache(ctx);
|
|
317
|
+
const key = SymbolCache.makeKey(lang, cwd, 'hover', { file, line, col });
|
|
318
|
+
const cached = cache?.get(key);
|
|
319
|
+
if (cached !== undefined) {
|
|
320
|
+
if (cached === null)
|
|
321
|
+
return notFound('hover');
|
|
322
|
+
const next = {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: { content: cached.content, ...(cached.range ? { range: cached.range } : {}) },
|
|
325
|
+
...(cached.truncated ? { truncated: true } : {}),
|
|
326
|
+
};
|
|
327
|
+
return next;
|
|
328
|
+
}
|
|
329
|
+
const result = await hoverSymbol(transportFromLspClient(client), file, line, col);
|
|
330
|
+
cache?.set(key, result);
|
|
331
|
+
if (!result)
|
|
332
|
+
return notFound('hover');
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
value: { content: result.content, ...(result.range ? { range: result.range } : {}) },
|
|
336
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* PUGI-78 Phase 1: symbols.signature.
|
|
342
|
+
*
|
|
343
|
+
* Function signature at a call site. Returns the active overload's
|
|
344
|
+
* label, parameters, and active-parameter index.
|
|
345
|
+
*/
|
|
346
|
+
export async function symbolsSignatureTool(ctx, lang, file, line, col) {
|
|
347
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_signature', `${lang}:${file}:${line}:${col}`);
|
|
348
|
+
return guard(ctx, 'symbols_signature', toolCallId, async () => {
|
|
349
|
+
const client = ctx.lspClients?.get(lang);
|
|
350
|
+
if (!client)
|
|
351
|
+
return unavailable(lang);
|
|
352
|
+
const result = await signatureAt(transportFromLspClient(client), file, line, col);
|
|
353
|
+
if (!result)
|
|
354
|
+
return notFound('signature');
|
|
355
|
+
return { ok: true, value: result };
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* PUGI-78 Phase 1: symbols.workspace_symbols.
|
|
360
|
+
*
|
|
361
|
+
* Workspace-wide fuzzy search. The server picks the matching algorithm
|
|
362
|
+
* (substring / fuzzy / prefix); we forward verbatim.
|
|
363
|
+
*/
|
|
364
|
+
export async function symbolsWorkspaceSymbolsTool(ctx, lang, query) {
|
|
365
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_workspace_symbols', `${lang}:${query}`);
|
|
366
|
+
return guard(ctx, 'symbols_workspace_symbols', toolCallId, async () => {
|
|
367
|
+
const client = ctx.lspClients?.get(lang);
|
|
368
|
+
if (!client)
|
|
369
|
+
return unavailable(lang);
|
|
370
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
371
|
+
const cwd = workspaceForCache(ctx);
|
|
372
|
+
const key = SymbolCache.makeKey(lang, cwd, 'workspace_symbols', { query });
|
|
373
|
+
const cached = cache?.get(key);
|
|
374
|
+
if (cached !== undefined) {
|
|
375
|
+
return { ok: true, value: [...cached] };
|
|
376
|
+
}
|
|
377
|
+
const result = await findWorkspaceSymbol(transportFromLspClient(client), query);
|
|
378
|
+
cache?.set(key, result);
|
|
379
|
+
return { ok: true, value: result };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* PUGI-78 Phase 1: symbols.call_hierarchy.
|
|
384
|
+
*
|
|
385
|
+
* Returns the incoming + outgoing call edges at the symbol position.
|
|
386
|
+
* The two arrays are independent — a function may have many incoming
|
|
387
|
+
* callers and zero outgoing calls (a pure leaf), or vice versa.
|
|
388
|
+
*/
|
|
389
|
+
export async function symbolsCallHierarchyTool(ctx, lang, file, line, col) {
|
|
390
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_call_hierarchy', `${lang}:${file}:${line}:${col}`);
|
|
391
|
+
return guard(ctx, 'symbols_call_hierarchy', toolCallId, async () => {
|
|
392
|
+
const client = ctx.lspClients?.get(lang);
|
|
393
|
+
if (!client)
|
|
394
|
+
return unavailable(lang);
|
|
395
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
396
|
+
const cwd = workspaceForCache(ctx);
|
|
397
|
+
const key = SymbolCache.makeKey(lang, cwd, 'call_hierarchy', { file, line, col });
|
|
398
|
+
const cached = cache?.get(key);
|
|
399
|
+
if (cached !== undefined) {
|
|
400
|
+
return { ok: true, value: { incoming: [...cached.incoming], outgoing: [...cached.outgoing] } };
|
|
401
|
+
}
|
|
402
|
+
const result = await callHierarchyAt(transportFromLspClient(client), file, line, col);
|
|
403
|
+
cache?.set(key, result);
|
|
404
|
+
return { ok: true, value: result };
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* PUGI-78 Phase 1: symbols.implementations.
|
|
409
|
+
*
|
|
410
|
+
* Implementations of an interface / abstract method. Returns the flat
|
|
411
|
+
* list of concrete sites.
|
|
412
|
+
*/
|
|
413
|
+
export async function symbolsImplementationsTool(ctx, lang, file, line, col) {
|
|
414
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_implementations', `${lang}:${file}:${line}:${col}`);
|
|
415
|
+
return guard(ctx, 'symbols_implementations', toolCallId, async () => {
|
|
416
|
+
const client = ctx.lspClients?.get(lang);
|
|
417
|
+
if (!client)
|
|
418
|
+
return unavailable(lang);
|
|
419
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
420
|
+
const cwd = workspaceForCache(ctx);
|
|
421
|
+
const key = SymbolCache.makeKey(lang, cwd, 'implementations', { file, line, col });
|
|
422
|
+
const cached = cache?.get(key);
|
|
423
|
+
if (cached !== undefined) {
|
|
424
|
+
const capped = capSymbolReferences(cached);
|
|
425
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
426
|
+
}
|
|
427
|
+
const result = await findImplementations(transportFromLspClient(client), file, line, col);
|
|
428
|
+
cache?.set(key, result);
|
|
429
|
+
const capped = capSymbolReferences(result);
|
|
430
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* PUGI-78 Phase 1: symbols.type_definition.
|
|
435
|
+
*
|
|
436
|
+
* Type definition (vs value definition). Useful for "what type is foo?"
|
|
437
|
+
* questions when the model has a variable and wants its type declaration.
|
|
438
|
+
*/
|
|
439
|
+
export async function symbolsTypeDefinitionTool(ctx, lang, file, line, col) {
|
|
440
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_type_definition', `${lang}:${file}:${line}:${col}`);
|
|
441
|
+
return guard(ctx, 'symbols_type_definition', toolCallId, async () => {
|
|
442
|
+
const client = ctx.lspClients?.get(lang);
|
|
443
|
+
if (!client)
|
|
444
|
+
return unavailable(lang);
|
|
445
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
446
|
+
const cwd = workspaceForCache(ctx);
|
|
447
|
+
const key = SymbolCache.makeKey(lang, cwd, 'type_definition', { file, line, col });
|
|
448
|
+
const cached = cache?.get(key);
|
|
449
|
+
if (cached !== undefined) {
|
|
450
|
+
if (cached === null)
|
|
451
|
+
return notFound('type_definition');
|
|
452
|
+
return { ok: true, value: cached };
|
|
453
|
+
}
|
|
454
|
+
const result = await findTypeDefinition(transportFromLspClient(client), file, line, col);
|
|
455
|
+
cache?.set(key, result);
|
|
456
|
+
if (!result)
|
|
457
|
+
return notFound('type_definition');
|
|
458
|
+
return { ok: true, value: result };
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* PUGI-78 Phase 1: symbols.code_actions.
|
|
463
|
+
*
|
|
464
|
+
* Quick-fix list at the given range. Server-provided actions include
|
|
465
|
+
* auto-import, unused-import removal, refactor extractions, etc.
|
|
466
|
+
*/
|
|
467
|
+
export async function symbolsCodeActionsTool(ctx, lang, file, startLine, startChar, endLine, endChar) {
|
|
468
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_code_actions', `${lang}:${file}:${startLine}:${startChar}-${endLine}:${endChar}`);
|
|
469
|
+
return guard(ctx, 'symbols_code_actions', toolCallId, async () => {
|
|
470
|
+
const client = ctx.lspClients?.get(lang);
|
|
471
|
+
if (!client)
|
|
472
|
+
return unavailable(lang);
|
|
473
|
+
const result = await codeActionsAt(transportFromLspClient(client), file, startLine, startChar, endLine, endChar);
|
|
474
|
+
return { ok: true, value: result };
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* PUGI-78 Phase 1: symbols.format.
|
|
479
|
+
*
|
|
480
|
+
* Formatter — returns the text edits the LSP server would apply for
|
|
481
|
+
* `textDocument/formatting`. Phase 1 is read-only (returns the edits);
|
|
482
|
+
* the dispatcher composes + applies in a future ticket.
|
|
483
|
+
*/
|
|
484
|
+
export async function symbolsFormatTool(ctx, lang, file, options) {
|
|
485
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_format', `${lang}:${file}`);
|
|
486
|
+
return guard(ctx, 'symbols_format', toolCallId, async () => {
|
|
487
|
+
const client = ctx.lspClients?.get(lang);
|
|
488
|
+
if (!client)
|
|
489
|
+
return unavailable(lang);
|
|
490
|
+
const result = await formatFile(transportFromLspClient(client), file, options);
|
|
491
|
+
return { ok: true, value: result };
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* PUGI-78 Phase 1: symbols.diagnostics.
|
|
496
|
+
*
|
|
497
|
+
* Pulls the cached `publishDiagnostics` payload for the file. Mirrors
|
|
498
|
+
* the legacy `lspDiagnostics` tool but exposed via the namespaced
|
|
499
|
+
* `symbols.*` surface for discoverability.
|
|
500
|
+
*/
|
|
501
|
+
export async function symbolsDiagnosticsTool(ctx, lang, file) {
|
|
502
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_diagnostics', `${lang}:${file}`);
|
|
503
|
+
return guard(ctx, 'symbols_diagnostics', toolCallId, async () => {
|
|
504
|
+
const client = ctx.lspClients?.get(lang);
|
|
505
|
+
if (!client)
|
|
506
|
+
return unavailable(lang);
|
|
507
|
+
const result = await diagnosticsFor(transportFromLspClient(client), file);
|
|
508
|
+
const capped = capDiagnostics(result);
|
|
509
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function workspaceForCache(ctx) {
|
|
513
|
+
// ToolContext exposes `root` (the workspace root). Fall back to a
|
|
514
|
+
// tagged anonymous workspace when somehow absent so the cache still
|
|
515
|
+
// keys deterministically.
|
|
516
|
+
const root = ctx.root;
|
|
517
|
+
if (typeof root === 'string' && root.length > 0)
|
|
518
|
+
return root;
|
|
519
|
+
return '<unknown>';
|
|
520
|
+
}
|
|
521
|
+
function getSymbolCacheFromCtx(ctx) {
|
|
522
|
+
// The agent dispatcher injects an optional cache via the ctx — when
|
|
523
|
+
// present we use the dispatcher-scoped cache so test harnesses can
|
|
524
|
+
// inject a deterministic clock. When absent, we fall back to the
|
|
525
|
+
// process-global cache.
|
|
526
|
+
if (ctx.symbolCache)
|
|
527
|
+
return ctx.symbolCache;
|
|
528
|
+
return getGlobalSymbolCache();
|
|
529
|
+
}
|
|
530
|
+
function notFound(verb) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
reason: 'lsp_not_found',
|
|
534
|
+
detail: `LSP returned no ${verb} for the requested position.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function capSymbolReferences(items) {
|
|
538
|
+
const COUNT_CAP = 200;
|
|
539
|
+
if (items.length === 0)
|
|
540
|
+
return { value: items, truncated: false };
|
|
541
|
+
const trimmed = items.slice(0, COUNT_CAP);
|
|
542
|
+
const serialized = JSON.stringify(trimmed);
|
|
543
|
+
if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES && trimmed.length === items.length) {
|
|
544
|
+
return { value: trimmed, truncated: false };
|
|
545
|
+
}
|
|
546
|
+
let upper = trimmed.length;
|
|
547
|
+
while (upper > 1) {
|
|
548
|
+
const half = Math.floor(upper / 2);
|
|
549
|
+
const sub = trimmed.slice(0, half);
|
|
550
|
+
if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
551
|
+
return { value: sub, truncated: true };
|
|
552
|
+
}
|
|
553
|
+
upper = half;
|
|
554
|
+
}
|
|
555
|
+
return { value: trimmed.slice(0, 1), truncated: true };
|
|
556
|
+
}
|
|
187
557
|
/** Test-only surface so specs can poke truncation directly. */
|
|
188
|
-
export const __test__ = {
|
|
558
|
+
export const __test__ = {
|
|
559
|
+
truncate,
|
|
560
|
+
capLocations,
|
|
561
|
+
capDiagnostics,
|
|
562
|
+
capSymbolReferences,
|
|
563
|
+
LSP_PAYLOAD_CAP_BYTES,
|
|
564
|
+
};
|
|
189
565
|
//# sourceMappingURL=lsp-tools.js.map
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { callTool } from '../core/mcp/client.js';
|
|
2
|
+
import { getMcpPermission, setMcpPermission, } from '../core/mcp/permission.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool dispatcher for MCP-invoked tools (β4 M1 + M3 + M5).
|
|
5
|
+
*
|
|
6
|
+
* Tool names use the `mcp__<server>__<tool>` namespace (double-underscore
|
|
7
|
+
* separator, mirroring the upstream tool's MCP envelope). The triple-underscore
|
|
8
|
+
* forms (`mcp__server__tool__sub`) collapse into the third segment when
|
|
9
|
+
* the upstream server itself uses underscores in its tool names — `split`
|
|
10
|
+
* on the first two `__` only, so any further `__` in the tool name part
|
|
11
|
+
* survive intact (e.g. `mcp__github__create_issue` -> server=`github`,
|
|
12
|
+
* tool=`create_issue`).
|
|
13
|
+
*
|
|
14
|
+
* Why double-underscore: native Pugi tools use single-token names
|
|
15
|
+
* (`read`, `grep`, `edit`, `bash`). The double-underscore prefix
|
|
16
|
+
* unambiguously segregates the MCP namespace from native names without
|
|
17
|
+
* needing per-name regex matching at every dispatch site.
|
|
18
|
+
*
|
|
19
|
+
* Permission flow:
|
|
20
|
+
* 1. Server trust gate (handled at registry-load time). If a server is
|
|
21
|
+
* not `trusted`, its tools never reach the engine loop.
|
|
22
|
+
* 2. Per-(server, tool) permission cache (`./mcp/permission.ts`).
|
|
23
|
+
* Unset on first dispatch -> caller must prompt. Cached `allow_always`
|
|
24
|
+
* auto-passes; cached `deny` auto-refuses.
|
|
25
|
+
*
|
|
26
|
+
* This module is the bridge — it parses the namespaced name, finds the
|
|
27
|
+
* live connection in the registry, consults the cache, and (when
|
|
28
|
+
* approved) routes through `client.callTool`. Prompting is the executor's
|
|
29
|
+
* responsibility; this module exposes the cache lookup + dispatch
|
|
30
|
+
* primitives so the executor stays small.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Prefix every MCP tool name carries on the engine-loop wire.
|
|
34
|
+
*/
|
|
35
|
+
export const MCP_TOOL_PREFIX = 'mcp__';
|
|
36
|
+
/**
|
|
37
|
+
* Parse `mcp__<server>__<tool>` into `{ serverName, toolName }`. Returns
|
|
38
|
+
* null when the input does not match the namespace — callers use this as
|
|
39
|
+
* the "is this an MCP tool?" predicate.
|
|
40
|
+
*
|
|
41
|
+
* Server names cannot contain `__` by registry validation (they are JSON
|
|
42
|
+
* object keys); tool names CAN (e.g. `create_issue` has a single `_` but
|
|
43
|
+
* `read_directory` has none, so the only ambiguity is when an upstream
|
|
44
|
+
* tool uses double-underscore in its slug — extremely rare, but if it
|
|
45
|
+
* happens the second `__` boundary still parses correctly because we
|
|
46
|
+
* split on the FIRST occurrence after the prefix).
|
|
47
|
+
*/
|
|
48
|
+
export function parseMcpToolName(name) {
|
|
49
|
+
if (!name.startsWith(MCP_TOOL_PREFIX))
|
|
50
|
+
return null;
|
|
51
|
+
const tail = name.slice(MCP_TOOL_PREFIX.length);
|
|
52
|
+
const sep = tail.indexOf('__');
|
|
53
|
+
if (sep === -1)
|
|
54
|
+
return null;
|
|
55
|
+
const serverName = tail.slice(0, sep);
|
|
56
|
+
const toolName = tail.slice(sep + 2);
|
|
57
|
+
if (serverName.length === 0 || toolName.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
return { serverName, toolName };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the namespaced tool name from a server + tool pair. Inverse of
|
|
63
|
+
* `parseMcpToolName`. Used by `buildMcpToolDefs` to emit the schema.
|
|
64
|
+
*/
|
|
65
|
+
export function buildMcpToolName(serverName, toolName) {
|
|
66
|
+
return `${MCP_TOOL_PREFIX}${serverName}__${toolName}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build engine-loop tool definitions from every trusted server's
|
|
70
|
+
* surfaced tools. Empty array when no MCP servers are trusted — the
|
|
71
|
+
* schema builder can call this unconditionally without checking first.
|
|
72
|
+
*/
|
|
73
|
+
export function buildMcpToolDefs(registry) {
|
|
74
|
+
if (!registry)
|
|
75
|
+
return [];
|
|
76
|
+
const defs = [];
|
|
77
|
+
for (const state of registry.servers.values()) {
|
|
78
|
+
if (state.trust !== 'trusted')
|
|
79
|
+
continue;
|
|
80
|
+
for (const tool of state.surfacedTools) {
|
|
81
|
+
defs.push({
|
|
82
|
+
name: buildMcpToolName(state.name, tool.name),
|
|
83
|
+
description: descriptionFor(state.name, tool),
|
|
84
|
+
// The upstream server returns its own JSON Schema in `inputSchema`.
|
|
85
|
+
// We surface it verbatim — the loop client passes it through to
|
|
86
|
+
// the model, and the model emits arguments matching the upstream
|
|
87
|
+
// shape. Default to `{ type: 'object' }` when missing so the
|
|
88
|
+
// OpenAI-shaped tool envelope still validates.
|
|
89
|
+
parameters: tool.inputSchema ?? { type: 'object' },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Sort stable so the schema bundle hash (used for caching/audit) is
|
|
94
|
+
// deterministic regardless of Map iteration order.
|
|
95
|
+
return defs.sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
}
|
|
97
|
+
function descriptionFor(serverName, tool) {
|
|
98
|
+
const base = tool.description?.trim() ?? `MCP tool ${tool.name} on server ${serverName}.`;
|
|
99
|
+
return `[MCP:${serverName}] ${base}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Look up the live connection + tool metadata for a parsed MCP tool name.
|
|
103
|
+
* Returns null when the server is not trusted, not connected, or does
|
|
104
|
+
* not expose the named tool. Callers MUST handle null — never throw,
|
|
105
|
+
* because the model may emit stale tool names after a server restart.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveMcpTool(registry, parsed) {
|
|
108
|
+
if (!registry)
|
|
109
|
+
return null;
|
|
110
|
+
const state = registry.servers.get(parsed.serverName);
|
|
111
|
+
if (!state || state.trust !== 'trusted' || !state.connection)
|
|
112
|
+
return null;
|
|
113
|
+
const tool = state.surfacedTools.find((t) => t.name === parsed.toolName);
|
|
114
|
+
if (!tool)
|
|
115
|
+
return null;
|
|
116
|
+
return { state, connection: state.connection, tool };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The default prompt — used when no interactive bridge is wired (CI,
|
|
120
|
+
* non-TTY pipes). Returns `deny` so an unattended run never silently
|
|
121
|
+
* fires an MCP call the operator never approved. The deny is NOT
|
|
122
|
+
* persisted, so the next run with a wired prompt still has a chance to
|
|
123
|
+
* approve.
|
|
124
|
+
*/
|
|
125
|
+
export const defaultNonInteractiveMcpPrompt = async () => 'unset';
|
|
126
|
+
/**
|
|
127
|
+
* Dispatch one MCP tool call. The flow:
|
|
128
|
+
*
|
|
129
|
+
* 1. Parse the namespaced tool name. Return error string when
|
|
130
|
+
* malformed — the model sees the error and can self-correct.
|
|
131
|
+
* 2. Resolve the live connection. Return error when the server is not
|
|
132
|
+
* trusted/connected or the tool is unknown.
|
|
133
|
+
* 3. Consult the permission cache. `deny` short-circuits. `allow_always`
|
|
134
|
+
* proceeds. `unset` invokes the prompt; the operator's verdict is
|
|
135
|
+
* persisted (allow_always/deny) or used one-shot (allow_once).
|
|
136
|
+
* 4. Parse the arguments string. Bad JSON -> error string.
|
|
137
|
+
* 5. Call `client.callTool` and stringify the content for the model.
|
|
138
|
+
*
|
|
139
|
+
* Throws ONLY on unrecoverable transport failures (e.g. the connection
|
|
140
|
+
* died mid-call). Tool-level errors from the upstream server are
|
|
141
|
+
* surfaced as `[MCP error] <message>` strings so the model can recover.
|
|
142
|
+
*/
|
|
143
|
+
export async function dispatchMcpTool(input) {
|
|
144
|
+
const parsed = parseMcpToolName(input.name);
|
|
145
|
+
if (!parsed) {
|
|
146
|
+
return `[MCP dispatch error] tool name "${input.name}" does not match the ${MCP_TOOL_PREFIX}<server>__<tool> namespace`;
|
|
147
|
+
}
|
|
148
|
+
const resolved = resolveMcpTool(input.registry, parsed);
|
|
149
|
+
if (!resolved) {
|
|
150
|
+
return `[MCP dispatch error] no trusted+connected server "${parsed.serverName}" exposes a tool named "${parsed.toolName}"`;
|
|
151
|
+
}
|
|
152
|
+
let args;
|
|
153
|
+
try {
|
|
154
|
+
args = parseArgumentsRaw(input.argumentsRaw);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return `[MCP dispatch error] invalid JSON in arguments for ${input.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
158
|
+
}
|
|
159
|
+
// Permission gate.
|
|
160
|
+
const cached = getMcpPermission(parsed.serverName, parsed.toolName);
|
|
161
|
+
let effective = cached;
|
|
162
|
+
if (cached === 'unset') {
|
|
163
|
+
const verdict = await input.prompt({
|
|
164
|
+
serverName: parsed.serverName,
|
|
165
|
+
toolName: parsed.toolName,
|
|
166
|
+
toolDescription: resolved.tool.description ?? '',
|
|
167
|
+
callArguments: args,
|
|
168
|
+
});
|
|
169
|
+
effective = verdict;
|
|
170
|
+
if (verdict === 'allow_always' || verdict === 'deny') {
|
|
171
|
+
setMcpPermission(parsed.serverName, parsed.toolName, verdict, resolveDecidedBy(input.decidedBy));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (effective === 'deny') {
|
|
175
|
+
return `[MCP refused] operator denied ${parsed.serverName}:${parsed.toolName}`;
|
|
176
|
+
}
|
|
177
|
+
if (effective !== 'allow_once' && effective !== 'allow_always') {
|
|
178
|
+
// Includes `unset` returned by the non-interactive default prompt.
|
|
179
|
+
return `[MCP refused] no operator approval for ${parsed.serverName}:${parsed.toolName} (run from a TTY to approve)`;
|
|
180
|
+
}
|
|
181
|
+
// Dispatch.
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = await callTool(resolved.connection, parsed.toolName, args, {
|
|
185
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// Transport-level failure (timeout, child died mid-call). Surface
|
|
190
|
+
// as a recoverable string so the model can degrade gracefully.
|
|
191
|
+
return `[MCP transport error] ${parsed.serverName}:${parsed.toolName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
192
|
+
}
|
|
193
|
+
return renderMcpToolResult(result.content, result.isError, parsed);
|
|
194
|
+
}
|
|
195
|
+
function parseArgumentsRaw(raw) {
|
|
196
|
+
if (!raw || raw.trim() === '')
|
|
197
|
+
return {};
|
|
198
|
+
const parsed = JSON.parse(raw);
|
|
199
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
200
|
+
throw new Error('arguments must be a JSON object');
|
|
201
|
+
}
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
204
|
+
function resolveDecidedBy(override) {
|
|
205
|
+
return (override?.trim() ||
|
|
206
|
+
process.env.PUGI_TRUSTED_BY?.trim() ||
|
|
207
|
+
process.env.USER?.trim() ||
|
|
208
|
+
process.env.USERNAME?.trim() ||
|
|
209
|
+
'cli');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Project the MCP `content` payload into a single text string the model
|
|
213
|
+
* can ingest. MCP servers reply with `content: [{ type: 'text', text }]`
|
|
214
|
+
* by convention; we concatenate every `type: text` chunk and surface a
|
|
215
|
+
* `[MCP non-text content]` marker for other content kinds (images,
|
|
216
|
+
* resource references) which are not yet wired into Pugi's loop.
|
|
217
|
+
*
|
|
218
|
+
* `isError: true` from the upstream maps to a `[MCP error] ...` prefix
|
|
219
|
+
* so the model knows the call failed at the server, not at the
|
|
220
|
+
* transport.
|
|
221
|
+
*/
|
|
222
|
+
export function renderMcpToolResult(content, isError, parsed) {
|
|
223
|
+
const text = projectTextContent(content);
|
|
224
|
+
const prefix = isError ? `[MCP error ${parsed.serverName}:${parsed.toolName}] ` : '';
|
|
225
|
+
if (text === null) {
|
|
226
|
+
// Fallback to a JSON dump so the model sees SOMETHING — better than
|
|
227
|
+
// an opaque empty string when the upstream uses image / resource
|
|
228
|
+
// content kinds.
|
|
229
|
+
try {
|
|
230
|
+
return `${prefix}${JSON.stringify(content)}`;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return `${prefix}[MCP non-serialisable content]`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return `${prefix}${text}`;
|
|
237
|
+
}
|
|
238
|
+
function projectTextContent(content) {
|
|
239
|
+
if (content === null || content === undefined)
|
|
240
|
+
return '';
|
|
241
|
+
if (typeof content === 'string')
|
|
242
|
+
return content;
|
|
243
|
+
if (!Array.isArray(content))
|
|
244
|
+
return null;
|
|
245
|
+
const parts = [];
|
|
246
|
+
for (const entry of content) {
|
|
247
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
248
|
+
const obj = entry;
|
|
249
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
250
|
+
parts.push(obj.text);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Non-text chunk — record a marker so the model knows something was
|
|
255
|
+
// dropped from the response.
|
|
256
|
+
parts.push('[MCP non-text content chunk]');
|
|
257
|
+
}
|
|
258
|
+
return parts.join('\n');
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=mcp-tool.js.map
|