@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.87
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 +96 -0
- 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 +2 -2
- 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 +140 -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 +286 -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 +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4162 -488
- 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 +32 -32
- 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/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 +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- 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-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 +23 -6
- 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
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pugi MCP server — orchestrator-tools surface .
|
|
3
|
+
*
|
|
4
|
+
* SCOPE — this module is intentionally orthogonal to `server-tools.ts`.
|
|
5
|
+
*
|
|
6
|
+
* - `server-tools.ts` exposes the *engine* surface (read / grep / glob /
|
|
7
|
+
* edit / write / bash) — workspace-scoped, file-tools-backed, used by
|
|
8
|
+
* "external agent borrows Pugi's in-process executor".
|
|
9
|
+
*
|
|
10
|
+
* - `orchestrator-tools.ts` (THIS FILE) exposes the *orchestrator*
|
|
11
|
+
* surface — `pugi.run` / `pugi.read` / `pugi.write` / `pugi.dispatch`
|
|
12
|
+
* / `pugi.publish` / `pugi.deploy`. These are CLI-level operations
|
|
13
|
+
* used by an EXTERNAL the upstream tool (or Cursor) session that wants to
|
|
14
|
+
* loop fix-publish-test against the LIVE Pugi runtime. The motivating
|
|
15
|
+
* use case is the CEO dogfood blocker: Pugi REPL emits
|
|
16
|
+
* pseudo-tool-tags inline (no real file writes / no real shell exec);
|
|
17
|
+
* the operator wants to drive a remote the upstream tool session that
|
|
18
|
+
* programmatically invokes Pugi against the engine VM, captures
|
|
19
|
+
* output, edits source, republishes the CLI, and re-tests — all
|
|
20
|
+
* without an interactive human at every step.
|
|
21
|
+
*
|
|
22
|
+
* SECURITY POSTURE — every orchestrator tool is gated by an env-var
|
|
23
|
+
* permission switch that defaults to OFF. The MCP server's
|
|
24
|
+
* `permissionGate` still applies on top (deny-by-default), but env
|
|
25
|
+
* gates are a coarser kill-switch the operator can flip per-machine
|
|
26
|
+
* without rebuilding the CLI.
|
|
27
|
+
*
|
|
28
|
+
* - PUGI_MCP_EXEC_ENABLED=1 — enables `pugi.run`
|
|
29
|
+
* - PUGI_MCP_PUBLISH_ENABLED=1 — enables `pugi.publish`
|
|
30
|
+
* - PUGI_MCP_DEPLOY_ENABLED=1 — enables `pugi.deploy`
|
|
31
|
+
*
|
|
32
|
+
* `pugi.read` / `pugi.write` do not require an env gate (read+write
|
|
33
|
+
* enforce workspace + protected-path containment). `pugi.dispatch`
|
|
34
|
+
* uses PUGI_MCP_EXEC_ENABLED (shared with `pugi.run`) because it
|
|
35
|
+
* shells the local `pugi` binary to drive the full engine loop
|
|
36
|
+
* client-side. All three still pass through the MCP-server
|
|
37
|
+
* permissionGate, so an operator running `pugi mcp serve` without
|
|
38
|
+
* `--allow-write` still sees `pugi.write` refused at dispatch.
|
|
39
|
+
*/
|
|
40
|
+
import { execFile } from 'node:child_process';
|
|
41
|
+
import { promisify } from 'node:util';
|
|
42
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
43
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
44
|
+
import { fileURLToPath } from 'node:url';
|
|
45
|
+
const execFileAsync = promisify(execFile);
|
|
46
|
+
/**
|
|
47
|
+
* Protected basename patterns — mirror of
|
|
48
|
+
* `core/bash-classifier.ts::PROTECTED_BASENAME_PATTERNS`. We DO NOT
|
|
49
|
+
* import from there because that module is bash-classifier specific
|
|
50
|
+
* (the regex shapes there carry shell-quote boundaries). For path-only
|
|
51
|
+
* matching we use simpler RegExps anchored on the basename. Keeps the
|
|
52
|
+
* two modules independently auditable.
|
|
53
|
+
*/
|
|
54
|
+
const PROTECTED_BASENAMES = [
|
|
55
|
+
/^\.env$/,
|
|
56
|
+
/^\.env\.[A-Za-z0-9_-]+$/,
|
|
57
|
+
/^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/,
|
|
58
|
+
/^\.npmrc$/,
|
|
59
|
+
/^\.pypirc$/,
|
|
60
|
+
/^\.gitconfig$/,
|
|
61
|
+
/^credentials(\.json)?$/,
|
|
62
|
+
];
|
|
63
|
+
const PROTECTED_DIR_SEGMENTS = new Set([
|
|
64
|
+
'.git',
|
|
65
|
+
'.ssh',
|
|
66
|
+
'.gnupg',
|
|
67
|
+
'node_modules',
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Resolve + validate a caller-supplied path against the workspace
|
|
71
|
+
* root. Refuses absolute paths outside the root, parent-traversal
|
|
72
|
+
* escapes, and protected basenames / dir segments.
|
|
73
|
+
*
|
|
74
|
+
* Exported so the spec can drive it directly — pinning the security
|
|
75
|
+
* boundary at a single audited entry point.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveWorkspacePathOrThrow(ctx, requested) {
|
|
78
|
+
if (typeof requested !== 'string' || requested.length === 0) {
|
|
79
|
+
throw new Error('path must be a non-empty string');
|
|
80
|
+
}
|
|
81
|
+
if (requested.includes('\0')) {
|
|
82
|
+
throw new Error('path contains a null byte');
|
|
83
|
+
}
|
|
84
|
+
const root = resolve(ctx.workspaceRoot);
|
|
85
|
+
const candidate = isAbsolute(requested) ? requested : resolve(root, requested);
|
|
86
|
+
const absolute = resolve(candidate);
|
|
87
|
+
// Containment check — absolute must live under root. We use
|
|
88
|
+
// `relative` + `..` detection rather than `startsWith(root)` so a
|
|
89
|
+
// sibling dir whose name happens to share a prefix (e.g. /tmp/wsX
|
|
90
|
+
// vs /tmp/ws) does not accidentally pass.
|
|
91
|
+
const rel = relative(root, absolute);
|
|
92
|
+
if (rel === '' || rel === '.') {
|
|
93
|
+
throw new Error(`path "${requested}" resolves to the workspace root itself`);
|
|
94
|
+
}
|
|
95
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
96
|
+
throw new Error(`path "${requested}" escapes the workspace root`);
|
|
97
|
+
}
|
|
98
|
+
// Protected segment / basename check — applied to EVERY component of
|
|
99
|
+
// the resolved path under the root. We split on the OS separator so
|
|
100
|
+
// Windows + POSIX share the same gate.
|
|
101
|
+
const segments = rel.split(sep);
|
|
102
|
+
for (const segment of segments) {
|
|
103
|
+
if (PROTECTED_DIR_SEGMENTS.has(segment)) {
|
|
104
|
+
throw new Error(`path "${requested}" touches protected segment "${segment}"`);
|
|
105
|
+
}
|
|
106
|
+
for (const pattern of PROTECTED_BASENAMES) {
|
|
107
|
+
if (pattern.test(segment)) {
|
|
108
|
+
throw new Error(`path "${requested}" touches protected basename "${segment}"`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { absolute, relativeToRoot: rel };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the orchestrator tool surface. The MCP server consumes the
|
|
116
|
+
* returned array via `createPugiMcpServer({ tools })`. Permission
|
|
117
|
+
* gating happens at TWO layers:
|
|
118
|
+
*
|
|
119
|
+
* 1. `capabilities.{exec,publish,deploy}` — env-var kill-switch
|
|
120
|
+
* checked at tool-execute time. A tool whose capability is OFF
|
|
121
|
+
* throws a deterministic refusal message; the MCP wire surfaces
|
|
122
|
+
* it as `isError: true` content.
|
|
123
|
+
*
|
|
124
|
+
* 2. The MCP server's `permissionGate` — checked BEFORE execute
|
|
125
|
+
* runs. The `pugi mcp serve` wiring in `runtime/commands/mcp.ts`
|
|
126
|
+
* synthesises a default gate; callers (tests) can pass
|
|
127
|
+
* `() => true` to bypass.
|
|
128
|
+
*
|
|
129
|
+
* The double-layer design is intentional — it lets an operator
|
|
130
|
+
* configure `PUGI_MCP_EXEC_ENABLED=1` system-wide AND still refuse a
|
|
131
|
+
* specific `pugi.run` call via the per-tool prompt without restarting
|
|
132
|
+
* the server.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Allowed dispatch subcommands. Mirror of the `command` enum in the
|
|
136
|
+
* admin-api `EngineRequestDto` (apps/admin-api/src/pugi-engine/
|
|
137
|
+
* pugi-engine.controller.ts). Kept as a local literal so this surface
|
|
138
|
+
* stays decoupled from the admin-api package — the CLI must work
|
|
139
|
+
* standalone after `npm i -g @pugi/cli`.
|
|
140
|
+
*/
|
|
141
|
+
const ALLOWED_DISPATCH_COMMANDS = ['code', 'explain', 'fix', 'plan', 'build'];
|
|
142
|
+
export function buildOrchestratorTools(ctx) {
|
|
143
|
+
const execImpl = ctx.execFileImpl ?? execFileAsync;
|
|
144
|
+
const tools = [
|
|
145
|
+
{
|
|
146
|
+
name: 'pugi.run',
|
|
147
|
+
description: 'Execute a pugi CLI subcommand and capture stdout/stderr/exitCode. ' +
|
|
148
|
+
'Use for `--version`, `explain`, `smoke`, etc. ' +
|
|
149
|
+
'Requires PUGI_MCP_EXEC_ENABLED=1 at server boot.',
|
|
150
|
+
permission: 'bash',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
required: ['command'],
|
|
155
|
+
properties: {
|
|
156
|
+
command: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Whitespace-tokenised argv tail (e.g. "explain README.md").',
|
|
159
|
+
},
|
|
160
|
+
cwd: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'Optional workspace-relative cwd; defaults to workspace root.',
|
|
163
|
+
},
|
|
164
|
+
timeoutMs: {
|
|
165
|
+
type: 'number',
|
|
166
|
+
minimum: 100,
|
|
167
|
+
maximum: 300000,
|
|
168
|
+
description: 'Hard timeout in ms (default 30000).',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
async execute(args) {
|
|
173
|
+
if (!ctx.capabilities.exec) {
|
|
174
|
+
throw new Error('pugi.run: PUGI_MCP_EXEC_ENABLED is not set. ' +
|
|
175
|
+
'Restart `pugi mcp serve` with PUGI_MCP_EXEC_ENABLED=1 to enable shell execution.');
|
|
176
|
+
}
|
|
177
|
+
const command = requireString(args, 'command');
|
|
178
|
+
const tokens = tokeniseArgv(command);
|
|
179
|
+
if (tokens.length === 0) {
|
|
180
|
+
throw new Error('pugi.run: command tokenises to zero args');
|
|
181
|
+
}
|
|
182
|
+
const timeoutMs = optionalNumber(args, 'timeoutMs', 30000);
|
|
183
|
+
const cwdInput = optionalString(args, 'cwd');
|
|
184
|
+
const cwd = cwdInput
|
|
185
|
+
? resolveWorkspacePathOrThrow(ctx, cwdInput).absolute
|
|
186
|
+
: ctx.workspaceRoot;
|
|
187
|
+
const started = Date.now();
|
|
188
|
+
try {
|
|
189
|
+
const { stdout, stderr } = await execImpl(ctx.pugiBin, tokens, {
|
|
190
|
+
cwd,
|
|
191
|
+
timeout: timeoutMs,
|
|
192
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
193
|
+
// Strip secret envs — orchestrator-driven CLI runs do NOT
|
|
194
|
+
// need the operator's NPM_TOKEN / GH_TOKEN / OPENAI_API_KEY
|
|
195
|
+
// visible. We pass through only PATH + HOME + a minimal
|
|
196
|
+
// shell. Same posture as bashToolSync(source='mcp').
|
|
197
|
+
env: sanitisedEnv(),
|
|
198
|
+
});
|
|
199
|
+
const durationMs = Date.now() - started;
|
|
200
|
+
return JSON.stringify({
|
|
201
|
+
stdout: clamp(stdout, 32 * 1024),
|
|
202
|
+
stderr: clamp(stderr, 32 * 1024),
|
|
203
|
+
exitCode: 0,
|
|
204
|
+
durationMs,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const e = err;
|
|
209
|
+
const durationMs = Date.now() - started;
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
stdout: clamp(e.stdout ?? '', 32 * 1024),
|
|
212
|
+
stderr: clamp(e.stderr ?? (e.message ?? ''), 32 * 1024),
|
|
213
|
+
exitCode: typeof e.code === 'number' ? e.code : 1,
|
|
214
|
+
durationMs,
|
|
215
|
+
...(e.signal ? { signal: e.signal } : {}),
|
|
216
|
+
...(e.killed ? { killed: true } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'pugi.read',
|
|
223
|
+
description: 'Read a file inside the configured workspace root. Refuses paths outside ' +
|
|
224
|
+
'the root, parent-traversal escapes, and protected basenames (.env / .git / ' +
|
|
225
|
+
'.ssh / id_rsa / .npmrc / credentials.json). Default cap 256KB.',
|
|
226
|
+
permission: 'read',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
required: ['path'],
|
|
231
|
+
properties: {
|
|
232
|
+
path: { type: 'string' },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
async execute(args) {
|
|
236
|
+
const path = requireString(args, 'path');
|
|
237
|
+
const { absolute, relativeToRoot } = resolveWorkspacePathOrThrow(ctx, path);
|
|
238
|
+
const stat = statSync(absolute);
|
|
239
|
+
if (!stat.isFile()) {
|
|
240
|
+
throw new Error(`pugi.read: "${relativeToRoot}" is not a regular file`);
|
|
241
|
+
}
|
|
242
|
+
const CAP = 256 * 1024;
|
|
243
|
+
const content = readFileSync(absolute, 'utf8');
|
|
244
|
+
const sizeBytes = Buffer.byteLength(content, 'utf8');
|
|
245
|
+
const truncated = sizeBytes > CAP;
|
|
246
|
+
return JSON.stringify({
|
|
247
|
+
path: relativeToRoot,
|
|
248
|
+
content: truncated ? content.slice(0, CAP) : content,
|
|
249
|
+
sizeBytes,
|
|
250
|
+
mtime: stat.mtime.toISOString(),
|
|
251
|
+
...(truncated ? { truncated: true, capBytes: CAP } : {}),
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'pugi.write',
|
|
257
|
+
description: 'Create or overwrite a workspace file using atomic tmp+rename. Refuses paths ' +
|
|
258
|
+
'outside the workspace root and protected basenames.',
|
|
259
|
+
permission: 'edit',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
required: ['path', 'content'],
|
|
264
|
+
properties: {
|
|
265
|
+
path: { type: 'string' },
|
|
266
|
+
content: { type: 'string' },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
async execute(args) {
|
|
270
|
+
const path = requireString(args, 'path');
|
|
271
|
+
const content = requireString(args, 'content');
|
|
272
|
+
const { absolute, relativeToRoot } = resolveWorkspacePathOrThrow(ctx, path);
|
|
273
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
274
|
+
const tmpPath = `${absolute}.pugi-mcp-tmp-${process.pid}-${Date.now()}`;
|
|
275
|
+
// Open with O_CREAT|O_EXCL so a concurrent writer cannot race
|
|
276
|
+
// a same-named tmp file out from under us. Mode 0o600 (operator
|
|
277
|
+
// only) — orchestrator writes are NOT shared artefacts.
|
|
278
|
+
const fd = openSync(tmpPath, 'wx', 0o600);
|
|
279
|
+
try {
|
|
280
|
+
writeFileSync(fd, content, 'utf8');
|
|
281
|
+
// fsync via fstatSync is a no-op on most kernels — the real
|
|
282
|
+
// durability win comes from rename being atomic at the inode
|
|
283
|
+
// layer. We still touch the fd to surface any late-EIO before
|
|
284
|
+
// rename commits.
|
|
285
|
+
fstatSync(fd);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
closeSync(fd);
|
|
289
|
+
}
|
|
290
|
+
renameSync(tmpPath, absolute);
|
|
291
|
+
const bytesWritten = Buffer.byteLength(content, 'utf8');
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
path: relativeToRoot,
|
|
294
|
+
bytesWritten,
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'pugi.dispatch',
|
|
300
|
+
description: 'Run the Pugi engine loop end-to-end by shelling to `pugi <command> <prompt>` ' +
|
|
301
|
+
'(default command "code"). Drives the full client-side tool-use loop, so the ' +
|
|
302
|
+
'caller sees real file writes, real shell exec, real cost — not just one Anvil ' +
|
|
303
|
+
'turn. Workspace cwd must be `pugi init`-ed already; auth resolves through the ' +
|
|
304
|
+
'CLI (PUGI_API_KEY env or on-disk `pugi login` state). ' +
|
|
305
|
+
'Requires PUGI_MCP_EXEC_ENABLED=1 at server boot.',
|
|
306
|
+
permission: 'bash',
|
|
307
|
+
inputSchema: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
additionalProperties: false,
|
|
310
|
+
required: ['prompt'],
|
|
311
|
+
properties: {
|
|
312
|
+
prompt: { type: 'string' },
|
|
313
|
+
command: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
enum: ['code', 'explain', 'fix', 'plan', 'build'],
|
|
316
|
+
description: 'Pugi CLI subcommand. Default "code".',
|
|
317
|
+
},
|
|
318
|
+
cwd: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'Optional workspace-relative cwd; defaults to the MCP workspace root. ' +
|
|
321
|
+
'Must already be `pugi init`-ed.',
|
|
322
|
+
},
|
|
323
|
+
timeoutMs: {
|
|
324
|
+
type: 'number',
|
|
325
|
+
minimum: 100,
|
|
326
|
+
maximum: 600000,
|
|
327
|
+
description: 'Hard timeout in ms (default 180000).',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
async execute(args) {
|
|
332
|
+
if (!ctx.capabilities.exec) {
|
|
333
|
+
throw new Error('pugi.dispatch: PUGI_MCP_EXEC_ENABLED is not set. ' +
|
|
334
|
+
'Restart `pugi mcp serve` with PUGI_MCP_EXEC_ENABLED=1 to enable shell-driven dispatch.');
|
|
335
|
+
}
|
|
336
|
+
const prompt = requireString(args, 'prompt');
|
|
337
|
+
// Argv-injection guard. The `pugi` CLI parser (runtime/cli.ts) uses
|
|
338
|
+
// an allowlist of known global flags (`--remote`, `--allow-fetch`,
|
|
339
|
+
// `--allow-search`, `--triple`, etc.) and does not honour a `--`
|
|
340
|
+
// end-of-options sentinel. Passing a prompt that begins with `--`
|
|
341
|
+
// risks the parser swallowing it as a CLI flag (e.g. an attacker-
|
|
342
|
+
// controlled MCP client sending `prompt: "--allow-fetch"` to
|
|
343
|
+
// silently unlock a capability the operator did not intend to
|
|
344
|
+
// grant for this turn). Reject at the MCP boundary so we fail
|
|
345
|
+
// loud rather than silently shift CLI behaviour. Operators with
|
|
346
|
+
// legitimate prompts starting with `--` can prepend a space.
|
|
347
|
+
if (prompt.startsWith('--')) {
|
|
348
|
+
throw new Error('pugi.dispatch: prompt cannot start with "--" — the child CLI parser would ' +
|
|
349
|
+
'interpret it as a flag. Prepend a space (" --foo") or rephrase.');
|
|
350
|
+
}
|
|
351
|
+
const command = optionalString(args, 'command') ?? 'code';
|
|
352
|
+
if (!ALLOWED_DISPATCH_COMMANDS.includes(command)) {
|
|
353
|
+
throw new Error(`pugi.dispatch: invalid command "${command}" (allowed: ${ALLOWED_DISPATCH_COMMANDS.join(', ')})`);
|
|
354
|
+
}
|
|
355
|
+
const cwdInput = optionalString(args, 'cwd');
|
|
356
|
+
const cwd = cwdInput
|
|
357
|
+
? resolveWorkspacePathOrThrow(ctx, cwdInput).absolute
|
|
358
|
+
: ctx.workspaceRoot;
|
|
359
|
+
const timeoutMs = optionalNumber(args, 'timeoutMs', 180000);
|
|
360
|
+
const started = Date.now();
|
|
361
|
+
try {
|
|
362
|
+
const { stdout, stderr } = await execImpl(ctx.pugiBin, [command, prompt, '--no-tty'], {
|
|
363
|
+
cwd,
|
|
364
|
+
timeout: timeoutMs,
|
|
365
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
366
|
+
// Auth-bearing envs are passed through here even though
|
|
367
|
+
// `sanitisedEnv()` strips them for `pugi.run`. Rationale:
|
|
368
|
+
// dispatch is explicitly an authenticated engine call, so
|
|
369
|
+
// the child must reach Anvil. The CLI prefers on-disk
|
|
370
|
+
// `pugi login` state when both are present.
|
|
371
|
+
env: dispatchEnv(),
|
|
372
|
+
});
|
|
373
|
+
return JSON.stringify({
|
|
374
|
+
command,
|
|
375
|
+
cwd,
|
|
376
|
+
exitCode: 0,
|
|
377
|
+
durationMs: Date.now() - started,
|
|
378
|
+
stdout: clamp(stdout, 16 * 1024),
|
|
379
|
+
stderr: clamp(stderr, 4 * 1024),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const e = err;
|
|
384
|
+
// `||` chain (not `??`) so an empty / whitespace-only `e.stderr`
|
|
385
|
+
// does not swallow a spawn-side `e.message` like `"spawn pugi
|
|
386
|
+
// ENOENT"`. Operators need to distinguish "pugi binary missing"
|
|
387
|
+
// from "pugi ran and exited 1 silently."
|
|
388
|
+
const stderrText = e.stderr || e.message || '';
|
|
389
|
+
return JSON.stringify({
|
|
390
|
+
command,
|
|
391
|
+
cwd,
|
|
392
|
+
exitCode: typeof e.code === 'number' ? e.code : 1,
|
|
393
|
+
durationMs: Date.now() - started,
|
|
394
|
+
stdout: clamp(e.stdout ?? '', 16 * 1024),
|
|
395
|
+
stderr: clamp(stderrText, 4 * 1024),
|
|
396
|
+
...(e.signal ? { signal: e.signal } : {}),
|
|
397
|
+
...(e.killed ? { killed: true } : {}),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'pugi.publish',
|
|
404
|
+
description: 'Bump @pugi/cli version + build + publish to npm. Use bumpType "beta" for ' +
|
|
405
|
+
'prerelease bumps (default) or "patch" for stable. Requires ' +
|
|
406
|
+
'PUGI_MCP_PUBLISH_ENABLED=1 AND a configured ~/.npmrc auth token.',
|
|
407
|
+
permission: 'network',
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
additionalProperties: false,
|
|
411
|
+
properties: {
|
|
412
|
+
bumpType: {
|
|
413
|
+
type: 'string',
|
|
414
|
+
enum: ['patch', 'beta'],
|
|
415
|
+
description: 'Default "beta" — pre-release bump.',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
async execute(args) {
|
|
420
|
+
if (!ctx.capabilities.publish) {
|
|
421
|
+
throw new Error('pugi.publish: PUGI_MCP_PUBLISH_ENABLED is not set. ' +
|
|
422
|
+
'Restart `pugi mcp serve` with PUGI_MCP_PUBLISH_ENABLED=1 to enable.');
|
|
423
|
+
}
|
|
424
|
+
const bumpType = optionalString(args, 'bumpType') ?? 'beta';
|
|
425
|
+
if (bumpType !== 'patch' && bumpType !== 'beta') {
|
|
426
|
+
throw new Error(`pugi.publish: invalid bumpType "${bumpType}"`);
|
|
427
|
+
}
|
|
428
|
+
// npm version semantics: "patch" bumps z; "prerelease --preid beta"
|
|
429
|
+
// bumps the beta tag. We thread through `pnpm` because the
|
|
430
|
+
// monorepo build expects the workspace-aware variant.
|
|
431
|
+
const versionArgs = bumpType === 'beta'
|
|
432
|
+
? ['version', 'prerelease', '--preid', 'beta', '--no-git-tag-version']
|
|
433
|
+
: ['version', 'patch', '--no-git-tag-version'];
|
|
434
|
+
const versionOut = await execImpl('npm', versionArgs, {
|
|
435
|
+
cwd: ctx.workspaceRoot,
|
|
436
|
+
timeout: 60000,
|
|
437
|
+
env: sanitisedEnv(),
|
|
438
|
+
});
|
|
439
|
+
const newVersion = (versionOut.stdout || '').trim().replace(/^v/, '');
|
|
440
|
+
const buildOut = await execImpl('pnpm', ['build'], {
|
|
441
|
+
cwd: ctx.workspaceRoot,
|
|
442
|
+
timeout: 180000,
|
|
443
|
+
env: sanitisedEnv(),
|
|
444
|
+
});
|
|
445
|
+
const publishOut = await execImpl('pnpm', ['publish', '--no-git-checks', '--access', 'public'], {
|
|
446
|
+
cwd: ctx.workspaceRoot,
|
|
447
|
+
timeout: 180000,
|
|
448
|
+
env: sanitisedEnv(),
|
|
449
|
+
});
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
newVersion,
|
|
452
|
+
registry: 'https://registry.npmjs.org',
|
|
453
|
+
npmExitCode: 0,
|
|
454
|
+
buildStdoutTail: clamp(buildOut.stdout, 2000),
|
|
455
|
+
publishStdoutTail: clamp(publishOut.stdout, 2000),
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: 'pugi.deploy',
|
|
461
|
+
description: 'SSH-redeploy a Pugi service on the engine VM (admin-api / admin-web / ' +
|
|
462
|
+
'pugi-web / all). Runs git pull + pnpm install + build + pm2 restart. ' +
|
|
463
|
+
'Requires PUGI_MCP_DEPLOY_ENABLED=1.',
|
|
464
|
+
permission: 'network',
|
|
465
|
+
inputSchema: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
required: ['target'],
|
|
469
|
+
properties: {
|
|
470
|
+
target: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
enum: ['admin-api', 'admin-web', 'pugi-web', 'all'],
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
async execute(args) {
|
|
477
|
+
if (!ctx.capabilities.deploy) {
|
|
478
|
+
throw new Error('pugi.deploy: PUGI_MCP_DEPLOY_ENABLED is not set. ' +
|
|
479
|
+
'Restart `pugi mcp serve` with PUGI_MCP_DEPLOY_ENABLED=1 to enable.');
|
|
480
|
+
}
|
|
481
|
+
const target = requireString(args, 'target');
|
|
482
|
+
const allowed = ['admin-api', 'admin-web', 'pugi-web', 'all'];
|
|
483
|
+
if (!allowed.includes(target)) {
|
|
484
|
+
throw new Error(`pugi.deploy: invalid target "${target}" (allowed: ${allowed.join(', ')})`);
|
|
485
|
+
}
|
|
486
|
+
// The redeploy script lives on the engine VM at ~/deploy/<target>.sh.
|
|
487
|
+
// We do NOT inline the shell — the operator owns the remote
|
|
488
|
+
// script and can tune it without rebuilding the CLI.
|
|
489
|
+
const remoteCmd = `set -euo pipefail; ~/deploy/${target}.sh`;
|
|
490
|
+
const started = Date.now();
|
|
491
|
+
const { stdout, stderr } = await execImpl('ssh', [
|
|
492
|
+
// BatchMode rejects password prompts so a misconfigured
|
|
493
|
+
// ssh-agent fails fast instead of blocking the dispatch.
|
|
494
|
+
'-o',
|
|
495
|
+
'BatchMode=yes',
|
|
496
|
+
'-o',
|
|
497
|
+
'StrictHostKeyChecking=accept-new',
|
|
498
|
+
ctx.sshAlias,
|
|
499
|
+
remoteCmd,
|
|
500
|
+
], {
|
|
501
|
+
cwd: ctx.workspaceRoot,
|
|
502
|
+
timeout: 300000,
|
|
503
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
504
|
+
env: sanitisedEnv(),
|
|
505
|
+
});
|
|
506
|
+
const durationMs = Date.now() - started;
|
|
507
|
+
return JSON.stringify({
|
|
508
|
+
host: ctx.sshAlias,
|
|
509
|
+
target,
|
|
510
|
+
gitPullHead: extractGitHead(stdout) ?? null,
|
|
511
|
+
pm2Status: extractPm2Status(stdout, stderr) ?? null,
|
|
512
|
+
durationMs,
|
|
513
|
+
stdoutTail: clamp(stdout, 4000),
|
|
514
|
+
stderrTail: clamp(stderr, 2000),
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
];
|
|
519
|
+
return tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
520
|
+
}
|
|
521
|
+
/* ---------- helpers ---------------------------------------------------- */
|
|
522
|
+
function requireString(args, key) {
|
|
523
|
+
const v = args[key];
|
|
524
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
525
|
+
throw new Error(`argument "${key}" must be a non-empty string`);
|
|
526
|
+
}
|
|
527
|
+
return v;
|
|
528
|
+
}
|
|
529
|
+
function optionalString(args, key) {
|
|
530
|
+
const v = args[key];
|
|
531
|
+
if (v === undefined || v === null)
|
|
532
|
+
return undefined;
|
|
533
|
+
if (typeof v !== 'string') {
|
|
534
|
+
throw new Error(`argument "${key}" must be a string when set`);
|
|
535
|
+
}
|
|
536
|
+
return v;
|
|
537
|
+
}
|
|
538
|
+
function optionalNumber(args, key, fallback) {
|
|
539
|
+
const v = args[key];
|
|
540
|
+
if (v === undefined || v === null)
|
|
541
|
+
return fallback;
|
|
542
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
543
|
+
throw new Error(`argument "${key}" must be a finite number when set`);
|
|
544
|
+
}
|
|
545
|
+
return v;
|
|
546
|
+
}
|
|
547
|
+
function clamp(s, max) {
|
|
548
|
+
if (typeof s !== 'string')
|
|
549
|
+
return '';
|
|
550
|
+
if (s.length <= max)
|
|
551
|
+
return s;
|
|
552
|
+
return `${s.slice(0, max)}\n…(truncated at ${max} bytes)`;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Tokenise an argv tail the same way the upstream tool's `pugi run` quoting
|
|
556
|
+
* convention does — whitespace-split with double-quote groups
|
|
557
|
+
* preserved. We do NOT eval a shell because that would let the model
|
|
558
|
+
* inject arbitrary commands (e.g. `; rm -rf ~`) into the orchestrator
|
|
559
|
+
* surface. Anything fancier (env-var expansion, globbing) must be
|
|
560
|
+
* delegated to the model via a `bash` capability flag — which is
|
|
561
|
+
* intentionally not part of this surface.
|
|
562
|
+
*
|
|
563
|
+
* Exported for the spec.
|
|
564
|
+
*/
|
|
565
|
+
export function tokeniseArgv(command) {
|
|
566
|
+
const out = [];
|
|
567
|
+
let buf = '';
|
|
568
|
+
let inQuotes = false;
|
|
569
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
570
|
+
const ch = command[i];
|
|
571
|
+
if (ch === '"') {
|
|
572
|
+
inQuotes = !inQuotes;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (ch === '\\' && command[i + 1] === '"') {
|
|
576
|
+
buf += '"';
|
|
577
|
+
i += 1;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (!inQuotes && (ch === ' ' || ch === '\t')) {
|
|
581
|
+
if (buf.length > 0) {
|
|
582
|
+
out.push(buf);
|
|
583
|
+
buf = '';
|
|
584
|
+
}
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
buf += ch;
|
|
588
|
+
}
|
|
589
|
+
if (inQuotes) {
|
|
590
|
+
throw new Error('pugi.run: unterminated double-quote in command');
|
|
591
|
+
}
|
|
592
|
+
if (buf.length > 0)
|
|
593
|
+
out.push(buf);
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
function sanitisedEnv() {
|
|
597
|
+
// Allowlist — pass through only what `pugi` needs to find itself
|
|
598
|
+
// and the local toolchain. NPM_TOKEN is added back for
|
|
599
|
+
// `pugi.publish` via the npm CLI's own ~/.npmrc lookup — we do not
|
|
600
|
+
// pass it via env because that surface ends up in `ps` output on
|
|
601
|
+
// some kernels.
|
|
602
|
+
const allow = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'NODE_OPTIONS'];
|
|
603
|
+
const out = {};
|
|
604
|
+
for (const key of allow) {
|
|
605
|
+
const value = process.env[key];
|
|
606
|
+
if (value !== undefined)
|
|
607
|
+
out[key] = value;
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
function dispatchEnv() {
|
|
612
|
+
// Like sanitisedEnv() but threads PUGI_API_KEY / PUGI_API_URL through
|
|
613
|
+
// so the child `pugi <command>` invocation can resolve auth from env
|
|
614
|
+
// when on-disk `pugi login` state is unavailable (CI, fresh container).
|
|
615
|
+
const allow = [
|
|
616
|
+
'PATH',
|
|
617
|
+
'HOME',
|
|
618
|
+
'USER',
|
|
619
|
+
'SHELL',
|
|
620
|
+
'LANG',
|
|
621
|
+
'LC_ALL',
|
|
622
|
+
'TERM',
|
|
623
|
+
'NODE_OPTIONS',
|
|
624
|
+
'PUGI_API_KEY',
|
|
625
|
+
'PUGI_API_URL',
|
|
626
|
+
];
|
|
627
|
+
const out = {};
|
|
628
|
+
for (const key of allow) {
|
|
629
|
+
const value = process.env[key];
|
|
630
|
+
if (value !== undefined)
|
|
631
|
+
out[key] = value;
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
function extractGitHead(stdout) {
|
|
636
|
+
// Match "HEAD is now at <sha> …" or "<sha> commit message" — the
|
|
637
|
+
// remote redeploy script logs `git rev-parse HEAD` after pull.
|
|
638
|
+
const m = stdout.match(/(?:HEAD is now at|^|\n)([0-9a-f]{7,40})\b/);
|
|
639
|
+
return m ? m[1] : null;
|
|
640
|
+
}
|
|
641
|
+
function extractPm2Status(stdout, stderr) {
|
|
642
|
+
const haystack = `${stdout}\n${stderr}`;
|
|
643
|
+
// Match "[PM2] Process pugi-admin-api restarted" or "online" / "stopped"
|
|
644
|
+
const restart = haystack.match(/\[PM2\][^\n]+(restarted|online|stopped|errored)/i);
|
|
645
|
+
if (restart)
|
|
646
|
+
return restart[0].trim();
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
/* ---------- helper: load this module from compiled JS at runtime ------- */
|
|
650
|
+
// `fileURLToPath(import.meta.url)` is used by sibling modules to find
|
|
651
|
+
// fixtures at runtime; we re-export it here so the spec can build an
|
|
652
|
+
// isolated workspace next to the compiled module without hard-coding
|
|
653
|
+
// paths. Defensive — not currently used by the production wiring.
|
|
654
|
+
export const ORCHESTRATOR_TOOLS_MODULE_FILE = (() => {
|
|
655
|
+
try {
|
|
656
|
+
return fileURLToPath(import.meta.url);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return '';
|
|
660
|
+
}
|
|
661
|
+
})();
|
|
662
|
+
//# sourceMappingURL=orchestrator-tools.js.map
|