@pugi/cli 0.1.0-beta.10 → 0.1.0-beta.100
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/README.md +53 -11
- 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/retro.js +210 -0
- 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/db.js +506 -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/codegraph/parser.js +71 -0
- package/dist/core/codegraph/types.js +34 -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 +72 -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 +247 -0
- package/dist/core/engine/budgets.js +220 -0
- package/dist/core/engine/compact-llm-summarizer.js +124 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +163 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1559 -227
- package/dist/core/engine/prompts.js +187 -19
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1887 -59
- package/dist/core/engine/verification-patterns.js +195 -0
- 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-config.js +192 -0
- package/dist/core/mcp/orchestrator-tools.js +806 -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/notes/notes-paths.js +113 -0
- package/dist/core/notes/notes-recorder.js +140 -0
- package/dist/core/notes/notes-writer.js +53 -0
- package/dist/core/notes/renderers.js +0 -0
- package/dist/core/notes/slug.js +105 -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 +107 -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-gitignore.js +52 -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/engine-bridge.js +303 -0
- 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 +2690 -229
- package/dist/core/repl/slash-commands.js +540 -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/tool-route.js +382 -0
- 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/retro/git-collector.js +251 -0
- package/dist/core/retro/health-card.js +25 -0
- package/dist/core/retro/metrics.js +342 -0
- package/dist/core/retro/narrative.js +249 -0
- package/dist/core/retro/plane-collector.js +274 -0
- package/dist/core/retro/pr-issue-link.js +65 -0
- package/dist/core/retro/types.js +16 -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/sandboxing/adapter.js +29 -0
- package/dist/core/sandboxing/index.js +49 -0
- package/dist/core/sandboxing/none.js +19 -0
- package/dist/core/sandboxing/seatbelt.js +183 -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 +119 -0
- package/dist/core/settings.js +378 -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 +146 -52
- 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 +4345 -561
- 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 +74 -40
- 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/index-cmd.js +353 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +200 -38
- package/dist/runtime/commands/mcp.js +935 -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/servers.js +236 -0
- 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/deprecation-warning.js +69 -0
- package/dist/runtime/engine-exit-code.js +50 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +548 -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/stream-renderer.js +195 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +89 -28
- package/dist/tools/ask-user-question.js +337 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/cron.js +433 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/http-request.js +336 -0
- 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 +120 -5
- package/dist/tools/server-tools.js +892 -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 +22 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +315 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/multi-file-diff-approval.js +375 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +36 -1
- package/dist/tui/repl-render.js +239 -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 +21 -5
- 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 +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +12 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/rewind` runtime — .
|
|
3
|
+
*
|
|
4
|
+
* Three invocation modes, sharing one runner:
|
|
5
|
+
*
|
|
6
|
+
* - `/rewind N` drop the last N operator turns + every
|
|
7
|
+
* tool call that landed between.
|
|
8
|
+
* - `/rewind --to <id>` rewind to a specific event index from the
|
|
9
|
+
* visible (post-mask) transcript.
|
|
10
|
+
* - `/rewind` interactive picker — surfaces the last 10
|
|
11
|
+
* user-turn boundaries (newest-first) and
|
|
12
|
+
* returns a payload the caller can use to
|
|
13
|
+
* mount a select prompt.
|
|
14
|
+
*
|
|
15
|
+
* The rewind is APPEND-ONLY: we never delete events. `applyRewindMask`
|
|
16
|
+
* elides the masked range on read; `pugi sessions undo-rewind` appends
|
|
17
|
+
* an inverse marker that nullifies the latest rewind so operators have
|
|
18
|
+
* a reliable escape hatch.
|
|
19
|
+
*
|
|
20
|
+
* Surface contract (same shape as `runCompactCommand`):
|
|
21
|
+
*
|
|
22
|
+
* - Returns a structured result for the JSON path.
|
|
23
|
+
* - Calls `ctx.writeOutput(payload, text)` once per invocation.
|
|
24
|
+
* - Throws ONLY on programmer-error. Store failures, missing
|
|
25
|
+
* sessions, etc. are surfaced as `failed_*` statuses.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes (mapped by the dispatcher in cli.ts):
|
|
28
|
+
*
|
|
29
|
+
* 0 — marker appended OR picker surfaced
|
|
30
|
+
* 1 — store unavailable / session not found
|
|
31
|
+
* 2 — noop (asked to drop 0 turns, nothing to rewind, etc.)
|
|
32
|
+
*/
|
|
33
|
+
import { homedir } from 'node:os';
|
|
34
|
+
import { slugForCwd } from '../../core/repl/history.js';
|
|
35
|
+
import { appendRewindMarker, buildRewindPickerRows, pickRewindTargetForTurns, resolveEventIdToIndex, } from '../../core/checkpoint/rewinder.js';
|
|
36
|
+
import { loadFromStore } from '../../core/checkpoint/resumer.js';
|
|
37
|
+
import { SqliteSessionStore, resolveProjectStoreDir, } from '../../core/repl/store/session-store.js';
|
|
38
|
+
/**
|
|
39
|
+
* Entry point reused by the slash command + the top-level dispatcher.
|
|
40
|
+
*
|
|
41
|
+
* `args` accepts:
|
|
42
|
+
* - `[]` picker mode
|
|
43
|
+
* - `["N"]` drop last N turns
|
|
44
|
+
* - `["--to", "<id>"]` rewind to event id
|
|
45
|
+
*
|
|
46
|
+
* Both `-N` and `--turns N` are accepted for parity with the upstream tool's
|
|
47
|
+
* `--turns` flag.
|
|
48
|
+
*/
|
|
49
|
+
export async function runRewindCommand(args, ctx) {
|
|
50
|
+
const parsed = parseRewindArgs(args);
|
|
51
|
+
if (parsed.kind === 'error') {
|
|
52
|
+
return emit(ctx, {
|
|
53
|
+
command: 'rewind',
|
|
54
|
+
status: 'failed_parse',
|
|
55
|
+
reason: parsed.message,
|
|
56
|
+
}, parsed.message);
|
|
57
|
+
}
|
|
58
|
+
// Resolve session + store.
|
|
59
|
+
const slug = slugForCwd(ctx.workspaceRoot);
|
|
60
|
+
let store = ctx.store ?? null;
|
|
61
|
+
let sessionId = ctx.sessionId ?? null;
|
|
62
|
+
let storeOpenedHere = false;
|
|
63
|
+
if (store === null) {
|
|
64
|
+
sessionId = sessionId ?? (await pickMostRecentSessionIdReadOnly(slug));
|
|
65
|
+
if (!sessionId) {
|
|
66
|
+
return emit(ctx, {
|
|
67
|
+
command: 'rewind',
|
|
68
|
+
status: 'failed_no_session',
|
|
69
|
+
reason: 'No active session to rewind. Start a REPL with `pugi`.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const opened = await openLiveStore(slug, sessionId);
|
|
73
|
+
if (!opened) {
|
|
74
|
+
return emit(ctx, {
|
|
75
|
+
command: 'rewind',
|
|
76
|
+
status: 'failed_store',
|
|
77
|
+
sessionId,
|
|
78
|
+
reason: 'Could not open local session store (lock held by another REPL?).',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
store = opened;
|
|
82
|
+
storeOpenedHere = true;
|
|
83
|
+
}
|
|
84
|
+
else if (sessionId === null) {
|
|
85
|
+
const rows = await store.listSessions({ project: slug, limit: 1, status: 'active' });
|
|
86
|
+
if (rows.length === 0) {
|
|
87
|
+
return emit(ctx, {
|
|
88
|
+
command: 'rewind',
|
|
89
|
+
status: 'failed_no_session',
|
|
90
|
+
reason: 'No active session to rewind.',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
sessionId = rows[0].id;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const loaded = await loadFromStore(store, sessionId);
|
|
97
|
+
if (!loaded) {
|
|
98
|
+
return emit(ctx, {
|
|
99
|
+
command: 'rewind',
|
|
100
|
+
status: 'failed_no_session',
|
|
101
|
+
sessionId,
|
|
102
|
+
reason: `Session '${sessionId}' not found.`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Picker mode: surface the last 10 user-turn boundaries.
|
|
106
|
+
if (parsed.kind === 'picker') {
|
|
107
|
+
const rows = buildRewindPickerRows(loaded.rawEvents, 10);
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
return emit(ctx, {
|
|
110
|
+
command: 'rewind',
|
|
111
|
+
status: 'noop_empty',
|
|
112
|
+
sessionId,
|
|
113
|
+
reason: 'No operator turns to rewind to.',
|
|
114
|
+
}, 'Nothing to rewind — no operator turns yet.');
|
|
115
|
+
}
|
|
116
|
+
const pickerRows = rows.map((r) => ({
|
|
117
|
+
visibleIndex: r.visibleIndex,
|
|
118
|
+
turnsAgo: r.turnsAgo,
|
|
119
|
+
preview: r.preview,
|
|
120
|
+
timestampEpochMs: r.timestampEpochMs,
|
|
121
|
+
}));
|
|
122
|
+
const text = renderPicker(pickerRows);
|
|
123
|
+
return emit(ctx, {
|
|
124
|
+
command: 'rewind',
|
|
125
|
+
status: 'picker',
|
|
126
|
+
sessionId,
|
|
127
|
+
pickerRows,
|
|
128
|
+
}, text);
|
|
129
|
+
}
|
|
130
|
+
// Resolve target index.
|
|
131
|
+
let toEventIndex;
|
|
132
|
+
let turnsRewound;
|
|
133
|
+
if (parsed.kind === 'turns') {
|
|
134
|
+
if (parsed.n <= 0) {
|
|
135
|
+
return emit(ctx, {
|
|
136
|
+
command: 'rewind',
|
|
137
|
+
status: 'noop_zero',
|
|
138
|
+
sessionId,
|
|
139
|
+
reason: 'Asked to drop 0 turns — nothing to do.',
|
|
140
|
+
}, 'Asked to drop 0 turns — nothing to do.');
|
|
141
|
+
}
|
|
142
|
+
const target = pickRewindTargetForTurns(loaded.rawEvents, parsed.n);
|
|
143
|
+
if (target.turnsRewound === 0) {
|
|
144
|
+
return emit(ctx, {
|
|
145
|
+
command: 'rewind',
|
|
146
|
+
status: 'noop_empty',
|
|
147
|
+
sessionId,
|
|
148
|
+
reason: 'No operator turns to rewind.',
|
|
149
|
+
}, 'No operator turns to rewind.');
|
|
150
|
+
}
|
|
151
|
+
toEventIndex = target.toEventIndex;
|
|
152
|
+
turnsRewound = target.turnsRewound;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// mode === 'to-event'
|
|
156
|
+
const resolvedIdx = resolveEventIdToIndex(loaded.rawEvents, parsed.eventId);
|
|
157
|
+
if (resolvedIdx === null) {
|
|
158
|
+
return emit(ctx, {
|
|
159
|
+
command: 'rewind',
|
|
160
|
+
status: 'failed_parse',
|
|
161
|
+
sessionId,
|
|
162
|
+
reason: `Could not resolve event id '${parsed.eventId}'. Try \`/rewind\` for the picker.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
toEventIndex = resolvedIdx;
|
|
166
|
+
// Count user turns in the masked range to report turnsRewound.
|
|
167
|
+
turnsRewound = countUserTurnsAfter(loaded.rawEvents, toEventIndex);
|
|
168
|
+
}
|
|
169
|
+
const fromEventIndex = loaded.rawEvents.length;
|
|
170
|
+
await appendRewindMarker({
|
|
171
|
+
store,
|
|
172
|
+
toEventIndex,
|
|
173
|
+
fromEventIndex,
|
|
174
|
+
turnsRewound,
|
|
175
|
+
reason: parsed.kind === 'turns' ? 'manual' : 'to-event',
|
|
176
|
+
...(ctx.now !== undefined ? { now: ctx.now } : {}),
|
|
177
|
+
});
|
|
178
|
+
// Reload + recompute visible count for the operator banner.
|
|
179
|
+
const after = await loadFromStore(store, sessionId);
|
|
180
|
+
const visibleAfter = after?.visibleEvents.length ?? 0;
|
|
181
|
+
const banner = `Rewound ${turnsRewound} turn${turnsRewound === 1 ? '' : 's'} ` +
|
|
182
|
+
`(to event ${toEventIndex < 0 ? 'start' : `#${toEventIndex + 1}`}). ` +
|
|
183
|
+
`${visibleAfter} event${visibleAfter === 1 ? '' : 's'} now visible. ` +
|
|
184
|
+
`Undo with \`pugi sessions undo-rewind\`.`;
|
|
185
|
+
return emit(ctx, {
|
|
186
|
+
command: 'rewind',
|
|
187
|
+
status: 'rewound',
|
|
188
|
+
sessionId,
|
|
189
|
+
turnsRewound,
|
|
190
|
+
toEventIndex,
|
|
191
|
+
fromEventIndex,
|
|
192
|
+
visibleAfter,
|
|
193
|
+
}, banner);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (storeOpenedHere && store) {
|
|
197
|
+
try {
|
|
198
|
+
await store.close();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* idempotent */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Accepts:
|
|
208
|
+
* pugi rewind -> picker
|
|
209
|
+
* pugi rewind 3 -> drop 3 turns
|
|
210
|
+
* pugi rewind --turns 3 -> same
|
|
211
|
+
* pugi rewind --to 12 -> rewind to event index 12 (1-based visible)
|
|
212
|
+
* pugi rewind --to #12 -> rewind to event index 12 (0-based hidden)
|
|
213
|
+
*/
|
|
214
|
+
function parseRewindArgs(args) {
|
|
215
|
+
if (args.length === 0)
|
|
216
|
+
return { kind: 'picker' };
|
|
217
|
+
const head = args[0];
|
|
218
|
+
// --to <id>
|
|
219
|
+
if (head === '--to' || head === '-t') {
|
|
220
|
+
const eventId = args[1];
|
|
221
|
+
if (!eventId) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'error',
|
|
224
|
+
message: 'Usage: pugi rewind --to <event-id>',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return { kind: 'to-event', eventId };
|
|
228
|
+
}
|
|
229
|
+
if (head.startsWith('--to=')) {
|
|
230
|
+
const eventId = head.slice('--to='.length);
|
|
231
|
+
if (eventId.length === 0) {
|
|
232
|
+
return {
|
|
233
|
+
kind: 'error',
|
|
234
|
+
message: 'Usage: pugi rewind --to <event-id>',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { kind: 'to-event', eventId };
|
|
238
|
+
}
|
|
239
|
+
// --turns N OR -N OR positional N
|
|
240
|
+
if (head === '--turns' || head === '-n') {
|
|
241
|
+
const n = Number.parseInt(args[1] ?? '', 10);
|
|
242
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
243
|
+
return {
|
|
244
|
+
kind: 'error',
|
|
245
|
+
message: 'Usage: pugi rewind --turns <N>',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { kind: 'turns', n };
|
|
249
|
+
}
|
|
250
|
+
if (head.startsWith('--turns=')) {
|
|
251
|
+
const n = Number.parseInt(head.slice('--turns='.length), 10);
|
|
252
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
253
|
+
return {
|
|
254
|
+
kind: 'error',
|
|
255
|
+
message: 'Usage: pugi rewind --turns <N>',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { kind: 'turns', n };
|
|
259
|
+
}
|
|
260
|
+
// Bare integer: positional turns count.
|
|
261
|
+
const positional = Number.parseInt(head, 10);
|
|
262
|
+
if (Number.isFinite(positional) && positional >= 0) {
|
|
263
|
+
return { kind: 'turns', n: positional };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: 'error',
|
|
267
|
+
message: `Unknown argument '${head}'. Try \`pugi rewind\`, \`pugi rewind <N>\`, or \`pugi rewind --to <id>\`.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function emit(ctx, payload, text) {
|
|
271
|
+
const human = text ?? payload.reason ?? `rewind: ${payload.status}`;
|
|
272
|
+
ctx.writeOutput(payload, human);
|
|
273
|
+
return payload;
|
|
274
|
+
}
|
|
275
|
+
function renderPicker(rows) {
|
|
276
|
+
const lines = ['Rewind picker — pick a turn boundary:', ''];
|
|
277
|
+
for (const r of rows) {
|
|
278
|
+
const tag = `[#${r.visibleIndex.toString().padStart(3)}]`;
|
|
279
|
+
const ago = `${r.turnsAgo}t ago`.padStart(8);
|
|
280
|
+
lines.push(` ${tag} ${ago} ${r.preview}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push('', 'Rewind with: pugi rewind --to <#N> (or `pugi rewind <turnsToDrop>`).');
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
function countUserTurnsAfter(events, toEventIndex) {
|
|
286
|
+
let count = 0;
|
|
287
|
+
for (let i = toEventIndex + 1; i < events.length; i += 1) {
|
|
288
|
+
if (events[i].kind === 'user')
|
|
289
|
+
count += 1;
|
|
290
|
+
}
|
|
291
|
+
return count;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Open the SqliteSessionStore for the workspace's project slug, bound
|
|
295
|
+
* to `sessionId`. Returns null when the lock is held by another REPL.
|
|
296
|
+
*/
|
|
297
|
+
async function openLiveStore(projectSlug, sessionId) {
|
|
298
|
+
try {
|
|
299
|
+
const store = new SqliteSessionStore({ projectSlug, home: homedir() });
|
|
300
|
+
await store.open({
|
|
301
|
+
id: sessionId,
|
|
302
|
+
workspaceRoot: process.cwd(),
|
|
303
|
+
projectSlug,
|
|
304
|
+
});
|
|
305
|
+
return store;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Discover the most recent active session id for a project slug,
|
|
313
|
+
* without taking the writer lockfile. Used by the standalone CLI path
|
|
314
|
+
* (no `--session` flag) so a live REPL holding the lock does not block
|
|
315
|
+
* the lookup.
|
|
316
|
+
*/
|
|
317
|
+
async function pickMostRecentSessionIdReadOnly(projectSlug) {
|
|
318
|
+
try {
|
|
319
|
+
const dir = resolveProjectStoreDir(projectSlug, homedir());
|
|
320
|
+
const view = await SqliteSessionStore.openReadOnly(dir);
|
|
321
|
+
try {
|
|
322
|
+
const rows = await view.list({ project: projectSlug, limit: 1, status: 'active' });
|
|
323
|
+
return rows.length > 0 ? rows[0].id : null;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
await view.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=rewind.js.map
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `pugi roster` command -
|
|
2
|
+
* `pugi roster` command - Tier 1 instantiation Phase 1.
|
|
3
3
|
*
|
|
4
4
|
* Lists the live Tier 1 personas with display name, role, and routing
|
|
5
5
|
* tag. The CLI walks two sources in order:
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* 1. The local @pugi/personas roster (THE_TEN). Always succeeds; the
|
|
8
|
+
* ten brand-canonical personas are baked into the SDK.
|
|
9
|
+
* 2. The remote `GET /api/pugi/sessions/roster` endpoint when the
|
|
10
|
+
* operator has a valid credential. The remote response carries the
|
|
11
|
+
* server-side dispatch role + dispatchTag for each slug so the
|
|
12
|
+
* operator sees the actual routing decision the dispatcher will
|
|
13
|
+
* apply on a `pugi delegate <slug>` call.
|
|
14
14
|
*
|
|
15
15
|
* The command never fails if the network is unreachable - it falls back
|
|
16
16
|
* to local-only output with a one-line warning. This matches the
|
|
17
|
-
* local-first contract
|
|
17
|
+
* local-first contract : the operator can still see who is on
|
|
18
18
|
* the team without an API key.
|
|
19
19
|
*
|
|
20
20
|
* Output:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - text default: a 3-column table (slug | name | role).
|
|
22
|
+
* - --json: a structured array of { slug, name, role, totem,
|
|
23
|
+
* dispatchTag } records, used by scripted callers.
|
|
24
24
|
*/
|
|
25
25
|
import { THE_TEN } from '@pugi/personas';
|
|
26
26
|
import { fetchPersonaRoster, } from '@pugi/sdk';
|
|
@@ -85,7 +85,7 @@ export function renderRosterTable(rows) {
|
|
|
85
85
|
dispatchTag: Math.max(head.dispatchTag.length, ...rows.map((r) => r.dispatchTag.length)),
|
|
86
86
|
};
|
|
87
87
|
const pad = (s, width) => s + ' '.repeat(Math.max(0, width - s.length));
|
|
88
|
-
const line = (r) => [pad(r.slug, widths.slug), pad(r.name, widths.name), pad(r.totem, widths.totem), pad(r.role, widths.role), pad(r.dispatchTag, widths.dispatchTag)].join('
|
|
88
|
+
const line = (r) => [pad(r.slug, widths.slug), pad(r.name, widths.name), pad(r.totem, widths.totem), pad(r.role, widths.role), pad(r.dispatchTag, widths.dispatchTag)].join(' ');
|
|
89
89
|
const header = line(head);
|
|
90
90
|
const sep = '-'.repeat(header.length);
|
|
91
91
|
return [header, sep, ...rows.map((r) => line(r))].join('\n');
|
|
@@ -107,7 +107,7 @@ export async function resolveRoster(config) {
|
|
|
107
107
|
return { rows: mergeRoster(THE_TEN, result.response.personas), warning: null };
|
|
108
108
|
}
|
|
109
109
|
const reason = result.status === 'endpoint_missing'
|
|
110
|
-
? 'runtime does not expose /api/pugi/sessions/roster (upgrade admin-api to
|
|
110
|
+
? 'runtime does not expose /api/pugi/sessions/roster (upgrade admin-api to +)'
|
|
111
111
|
: result.message;
|
|
112
112
|
return {
|
|
113
113
|
rows: mergeRoster(THE_TEN, null),
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR H (2026-06-05): `/servers` slash command — list + stop server-
|
|
3
|
+
* started processes the operator can't otherwise kill from inside
|
|
4
|
+
* the REPL.
|
|
5
|
+
*
|
|
6
|
+
* Customer pain (CEO 2026-06-05): Hiroshi launched a Vite dev server
|
|
7
|
+
* via `server_start` tool (PID 45800 on :5173), persona answered
|
|
8
|
+
* "use `pugi stop`" — but `/stop <persona>` halts a persona agent,
|
|
9
|
+
* not a server process. Server kept running until manual
|
|
10
|
+
* `kill 45800`.
|
|
11
|
+
*
|
|
12
|
+
* Fix: surface the existing `server_start` meta files (which already
|
|
13
|
+
* persist `.pugi/runs/srv-<uuid>/server.json` per the
|
|
14
|
+
* server-tools.ts spec) as a first-class operator-facing list +
|
|
15
|
+
* kill command. The `server_stop` TOOL already exists for LLM
|
|
16
|
+
* dispatch (SIGTERM → grace → SIGKILL ladder via
|
|
17
|
+
* `dispatchServerStop`); we wire it through to a slash command so
|
|
18
|
+
* the operator no longer needs to copy PIDs out of agent text.
|
|
19
|
+
*
|
|
20
|
+
* Surface:
|
|
21
|
+
*
|
|
22
|
+
* /servers → table of tracked servers
|
|
23
|
+
* /servers stop <runId> → kill one by run id (srv-...)
|
|
24
|
+
* /servers stop <pid> → kill one by pid (numeric)
|
|
25
|
+
* /servers stop all → kill every alive tracked srv
|
|
26
|
+
*
|
|
27
|
+
* Everything is best-effort. A missing `.pugi/runs/` dir = empty
|
|
28
|
+
* list, not an error. A stale `server.json` for a process that
|
|
29
|
+
* already exited stays listed as `dead` so the operator can see it
|
|
30
|
+
* happened.
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
function runsDir(workspaceRoot) {
|
|
35
|
+
return join(workspaceRoot, '.pugi', 'runs');
|
|
36
|
+
}
|
|
37
|
+
function isAlive(pid, killImpl) {
|
|
38
|
+
try {
|
|
39
|
+
killImpl(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read every `<runs>/srv-<uuid>/server.json` and project к `ServerEntry`.
|
|
48
|
+
* Pure: takes the workspace + kill impl (для liveness check), returns
|
|
49
|
+
* a stable-sorted (oldest-first) list. Failure modes:
|
|
50
|
+
* - missing runs dir → []
|
|
51
|
+
* - unreadable / malformed JSON → entry omitted (no throw)
|
|
52
|
+
* - meta carries no pid → entry kept but `alive=false`
|
|
53
|
+
*/
|
|
54
|
+
export function listServers(workspaceRoot, killImpl = (pid, signal) => {
|
|
55
|
+
process.kill(pid, signal);
|
|
56
|
+
}) {
|
|
57
|
+
const dir = runsDir(workspaceRoot);
|
|
58
|
+
if (!existsSync(dir))
|
|
59
|
+
return [];
|
|
60
|
+
let names = [];
|
|
61
|
+
try {
|
|
62
|
+
names = readdirSync(dir, { withFileTypes: true })
|
|
63
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('srv-'))
|
|
64
|
+
.map((e) => e.name)
|
|
65
|
+
.sort();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const runId of names) {
|
|
72
|
+
const metaPath = join(dir, runId, 'server.json');
|
|
73
|
+
if (!existsSync(metaPath))
|
|
74
|
+
continue;
|
|
75
|
+
let meta;
|
|
76
|
+
try {
|
|
77
|
+
meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const pid = typeof meta['pid'] === 'number' ? meta['pid'] : null;
|
|
83
|
+
out.push({
|
|
84
|
+
runId,
|
|
85
|
+
pid,
|
|
86
|
+
port: typeof meta['port'] === 'number' ? meta['port'] : null,
|
|
87
|
+
command: typeof meta['command'] === 'string' ? meta['command'] : null,
|
|
88
|
+
startedAt: typeof meta['startedAt'] === 'string' ? meta['startedAt'] : null,
|
|
89
|
+
healthStatus: typeof meta['healthStatus'] === 'number' ? meta['healthStatus'] : null,
|
|
90
|
+
alive: pid !== null ? isAlive(pid, killImpl) : false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Kill one or more servers. Strategy:
|
|
97
|
+
* - `target === 'all'` — kill every alive entry
|
|
98
|
+
* - target matches `srv-...` — match by runId (exact)
|
|
99
|
+
* - target is numeric — match by pid
|
|
100
|
+
*
|
|
101
|
+
* Uses the same SIGTERM → 5s grace → SIGKILL ladder as the
|
|
102
|
+
* `server_stop` tool. Failures silent (entry stays listed as alive
|
|
103
|
+
* on next `/servers` if SIGKILL also fails — operator escalates
|
|
104
|
+
* manually).
|
|
105
|
+
*/
|
|
106
|
+
export async function stopServers(workspaceRoot, target, options) {
|
|
107
|
+
const killImpl = options.killImpl ??
|
|
108
|
+
((pid, signal) => {
|
|
109
|
+
process.kill(pid, signal);
|
|
110
|
+
});
|
|
111
|
+
const entries = listServers(workspaceRoot, killImpl);
|
|
112
|
+
if (entries.length === 0)
|
|
113
|
+
return { kind: 'empty' };
|
|
114
|
+
const targets = [];
|
|
115
|
+
if (target === 'all') {
|
|
116
|
+
targets.push(...entries.filter((e) => e.alive));
|
|
117
|
+
}
|
|
118
|
+
else if (/^srv-/.test(target)) {
|
|
119
|
+
const hit = entries.find((e) => e.runId === target);
|
|
120
|
+
if (!hit)
|
|
121
|
+
return { kind: 'not-found', query: target };
|
|
122
|
+
targets.push(hit);
|
|
123
|
+
}
|
|
124
|
+
else if (/^\d+$/.test(target)) {
|
|
125
|
+
const wanted = Number(target);
|
|
126
|
+
const hit = entries.find((e) => e.pid === wanted);
|
|
127
|
+
if (!hit)
|
|
128
|
+
return { kind: 'not-found', query: target };
|
|
129
|
+
targets.push(hit);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return { kind: 'not-found', query: target };
|
|
133
|
+
}
|
|
134
|
+
if (targets.length === 0)
|
|
135
|
+
return { kind: 'empty' };
|
|
136
|
+
const killed = [];
|
|
137
|
+
let notFound = 0;
|
|
138
|
+
for (const entry of targets) {
|
|
139
|
+
if (entry.pid === null) {
|
|
140
|
+
notFound += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!isAlive(entry.pid, killImpl)) {
|
|
144
|
+
// already dead — treat as success (operator's intent matched)
|
|
145
|
+
killed.push(entry);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
killImpl(entry.pid, 'SIGTERM');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// SIGTERM refused → escalate
|
|
153
|
+
}
|
|
154
|
+
// Grace window: poll for exit up to 5s.
|
|
155
|
+
const deadline = Date.now() + 5_000;
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
if (!isAlive(entry.pid, killImpl))
|
|
158
|
+
break;
|
|
159
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
160
|
+
}
|
|
161
|
+
if (isAlive(entry.pid, killImpl)) {
|
|
162
|
+
try {
|
|
163
|
+
killImpl(entry.pid, 'SIGKILL');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
notFound += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
killed.push(entry);
|
|
171
|
+
}
|
|
172
|
+
return { kind: 'stopped', killed, notFound };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Slash entry. The session dispatcher calls this с the parsed verdict
|
|
176
|
+
* and an IO sink that pipes lines к the REPL system pane.
|
|
177
|
+
*/
|
|
178
|
+
export async function runServersCommand(mode, io, options) {
|
|
179
|
+
if (mode.kind === 'list') {
|
|
180
|
+
const entries = listServers(options.workspaceRoot, options.killImpl);
|
|
181
|
+
renderList(entries, io);
|
|
182
|
+
return { kind: 'listed', entries };
|
|
183
|
+
}
|
|
184
|
+
const result = await stopServers(options.workspaceRoot, mode.target, options);
|
|
185
|
+
renderStopResult(result, io);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function renderList(entries, io) {
|
|
189
|
+
if (entries.length === 0) {
|
|
190
|
+
io.write('No tracked servers (.pugi/runs/srv-* empty).');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
io.write(`Tracked servers (${entries.length}):`);
|
|
194
|
+
for (const e of entries) {
|
|
195
|
+
const status = e.alive ? 'alive' : 'dead';
|
|
196
|
+
const port = e.port !== null ? `:${e.port}` : '';
|
|
197
|
+
const pid = e.pid !== null ? `pid=${e.pid}` : 'pid=?';
|
|
198
|
+
const cmd = e.command ? truncate(e.command, 60) : '<unknown>';
|
|
199
|
+
io.write(` ${e.runId} [${status}] ${pid} ${port} ${cmd}`);
|
|
200
|
+
}
|
|
201
|
+
io.write('Use `/servers stop <runId|pid|all>` to terminate.');
|
|
202
|
+
}
|
|
203
|
+
function renderStopResult(result, io) {
|
|
204
|
+
switch (result.kind) {
|
|
205
|
+
case 'empty':
|
|
206
|
+
io.write('No alive servers to stop.');
|
|
207
|
+
return;
|
|
208
|
+
case 'not-found':
|
|
209
|
+
io.write(`No server matches '${result.query}'. Run \`/servers\` to list.`);
|
|
210
|
+
return;
|
|
211
|
+
case 'stopped': {
|
|
212
|
+
if (result.killed.length === 0) {
|
|
213
|
+
io.write(`Stop dispatched but 0 servers terminated (${result.notFound} miss).`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
io.write(`Stopped ${result.killed.length} server(s):`);
|
|
217
|
+
for (const e of result.killed) {
|
|
218
|
+
io.write(` ${e.runId} pid=${e.pid ?? '?'}${e.port !== null ? ` :${e.port}` : ''}`);
|
|
219
|
+
}
|
|
220
|
+
if (result.notFound > 0) {
|
|
221
|
+
io.write(`(${result.notFound} entry skipped — no pid or kill failed)`);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
case 'error':
|
|
226
|
+
io.write(`Error: ${result.message}`);
|
|
227
|
+
return;
|
|
228
|
+
case 'listed':
|
|
229
|
+
// not used in stop path
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function truncate(s, max) {
|
|
234
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=servers.js.map
|